From f422b00fa514963ffde55f74aad5498b96bd5086 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 18 Jun 2026 13:18:57 -0300 Subject: [PATCH] feat(catalog): add kimi-k2.7-code to kmca catalog + qwen-web models discovery (#3931 bug 3, #3737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 1 — add moonshotai/kimi-k2.7-code to KIMI_CODING_SHARED.models (shared.ts), propagates to both kimi-coding and kimi-coding-apikey (kmca) providers. Requested by @hana189 in discussion #3737. Task 2 — add qwen-web entry to PROVIDER_MODELS_CONFIG in models/route.ts pointing at chat.qwen.ai/api/v2/models (public, no auth). Fixes #3931 bug 3 (identified by @thezukiru in discussion #3895): the provider models page for the web-cookie provider returned nothing because no discovery config was registered. parseResponse handles both nested data.data and flat data array shapes. Closes #3931 (bug 3). Discussion reply pending. --- config/quality/file-size-baseline.json | 5 +- open-sse/config/providers/shared.ts | 6 + src/app/api/providers/[id]/models/route.ts | 15 +++ .../catalog-updates-v3829-kimi-qwen.test.ts | 116 ++++++++++++++++++ 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/unit/catalog-updates-v3829-kimi-qwen.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 6d8b967d75b3..464cebd1410f 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -110,7 +110,7 @@ "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148, "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1069, "src/app/api/oauth/[provider]/[action]/route.ts": 918, - "src/app/api/providers/[id]/models/route.ts": 2512, + "src/app/api/providers/[id]/models/route.ts": 2527, "src/app/api/providers/[id]/test/route.ts": 842, "src/app/api/usage/analytics/route.ts": 941, "src/app/api/v1/models/catalog.ts": 1440, @@ -176,5 +176,6 @@ "_rebaseline_2026_06_16_4005_openai_dynamic_models": "PR #4005 own growth: models/route.ts 2494->2512 (+18 = openai model-discovery derives {customBaseUrl}/v1/models from providerSpecificData.baseUrl, SSRF-guarded via safeOutboundFetch+public-only) and pricing.ts 1529->1581 (+52 = pure-data pricing rows closing $0 gaps for registry-exposed ids: openai gpt-5.4/-mini/-nano, gpt-4.1, gpt-4o-2024-11-20, o3 + codex(cx) gpt-5.4-{xhigh,high,medium,low}, gpt-5.3-codex-spark). Cohesive; pricing is data, route change mirrors the anthropic-compat discovery path.", "_rebaseline_2026_06_16_4004_livews_bridge": "PR #4004 own growth: chatCore.ts 5830->5851 (+21 = forwardDashboardEventToLiveWs — a best-effort, non-blocking, timeout-bounded POST that bridges compression.completed events from the main process to the LiveWS sidecar so the dashboard updates under a reverse proxy). Cohesive fire-and-forget beacon at the existing compression emit site; not extractable. Structural shrink of chatCore.ts tracked in #3501.", "_rebaseline_2026_06_17_4107_pending_reaper": "PR #4107 own growth: usageHistory.ts 854->934 (+80 = orphaned-pending-request reaper — sweepStalePendingRequests() evicts pending details older than 15min + a hard 5000 cap, plus an unref'd 5min sweep timer wired lazily into trackPendingRequest). Fixes an unbounded memory leak where abnormally-terminated requests left payload previews in pendingById forever. Cohesive with the existing pending-request bookkeeping (mirrors the normal removal path: decrement counters + cleanup buckets); not extractable.", - "_rebaseline_2026_06_17_4116_combo_hedge_listener": "combo.ts: +9 lines from #4116 (detach per-target listener from shared hedge abort signal to fix a listener leak). Behavior-preserving cleanup; 5289 -> 5298." + "_rebaseline_2026_06_17_4116_combo_hedge_listener": "combo.ts: +9 lines from #4116 (detach per-target listener from shared hedge abort signal to fix a listener leak). Behavior-preserving cleanup; 5289 -> 5298.", + "_rebaseline_2026_06_18_kimi_k27_qwen_web_models": "models/route.ts: 2512->2527 (+15 = add qwen-web entry to PROVIDER_MODELS_CONFIG pointing at chat.qwen.ai/api/v2/models with nested-data parseResponse, fixes issue #3931 bug #3). Cohesive with the existing qwen entry; not separately extractable." } diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 1ab86bfe50b8..0ef082df5ab4 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -176,6 +176,12 @@ export const KIMI_CODING_SHARED = { contextLength: 262144, maxOutputTokens: 262144, }, + { + id: "moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + contextLength: 262144, + maxOutputTokens: 262144, + }, ] as RegistryModel[], } as const; diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index ad048c8f616f..6b0782238ad5 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -430,6 +430,21 @@ const PROVIDER_MODELS_CONFIG: Record = { authPrefix: "Bearer ", parseResponse: (data) => data.data || [], }, + "qwen-web": { + url: "https://chat.qwen.ai/api/v2/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + parseResponse: (data: any) => { + const innerData = data?.data?.data || data?.data || []; + return innerData + .map((item: any) => ({ + id: item.id || item.name, + name: item.name || item.id, + owned_by: item.owned_by || "qwen", + })) + .filter((m: any) => m.id); + }, + }, antigravity: { url: getAntigravityModelsDiscoveryUrls()[0], method: "POST", diff --git a/tests/unit/catalog-updates-v3829-kimi-qwen.test.ts b/tests/unit/catalog-updates-v3829-kimi-qwen.test.ts new file mode 100644 index 000000000000..21a04289f618 --- /dev/null +++ b/tests/unit/catalog-updates-v3829-kimi-qwen.test.ts @@ -0,0 +1,116 @@ +// Regression guard for two catalog fixes shipped in v3.8.29: +// +// 1. Task 1 — moonshotai/kimi-k2.7-code added to the kimi-coding-apikey (kmca) +// provider catalog (KIMI_CODING_SHARED.models in open-sse/config/providers/shared.ts). +// Requested by @hana189 in discussion #3737. +// +// 2. Bug #3 (issue #3931) — qwen-web missing from PROVIDER_MODELS_CONFIG in +// src/app/api/providers/[id]/models/route.ts, so the model discovery page for +// the web-cookie provider returned nothing. Identified by @thezukiru in #3895. + +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { getModelsByProviderId } from "../../open-sse/config/providerModels.ts"; + +// ── Task 1: kimi-k2.7-code in kmca (kimi-coding-apikey) ─────────────────────── + +test("kmca (kimi-coding-apikey) catalog includes moonshotai/kimi-k2.7-code", () => { + const models = getModelsByProviderId("kmca"); + const ids = new Set(models.map((m) => m.id)); + assert.ok( + ids.has("moonshotai/kimi-k2.7-code"), + "moonshotai/kimi-k2.7-code missing from kmca — add to KIMI_CODING_SHARED.models in shared.ts" + ); +}); + +test("kmca kimi-k2.7-code has 262144 context length and maxOutputTokens", () => { + const models = getModelsByProviderId("kmca"); + const model = models.find((m) => m.id === "moonshotai/kimi-k2.7-code"); + assert.ok(model, "moonshotai/kimi-k2.7-code not found in kmca catalog"); + assert.equal(model.contextLength, 262144, "kimi-k2.7-code contextLength must be 262144"); + assert.equal(model.maxOutputTokens, 262144, "kimi-k2.7-code maxOutputTokens must be 262144"); +}); + +test("kmca still exposes kimi-k2.6 and kimi-k2.6-thinking alongside the new model", () => { + const ids = new Set(getModelsByProviderId("kmca").map((m) => m.id)); + assert.ok(ids.has("kimi-k2.6"), "kimi-k2.6 must still be present"); + assert.ok(ids.has("kimi-k2.6-thinking"), "kimi-k2.6-thinking must still be present"); + assert.ok(ids.has("moonshotai/kimi-k2.7-code"), "moonshotai/kimi-k2.7-code must be present"); +}); + +// ── Bug #3 / issue #3931: qwen-web in PROVIDER_MODELS_CONFIG ────────────────── + +const ROUTE_FILE = path.join("src", "app", "api", "providers", "[id]", "models", "route.ts"); + +test("PROVIDER_MODELS_CONFIG contains a qwen-web entry (issue #3931 bug #3)", () => { + const src = fs.readFileSync(ROUTE_FILE, "utf-8"); + assert.match( + src, + /"qwen-web"\s*:/, + '"qwen-web" key missing from PROVIDER_MODELS_CONFIG in models/route.ts' + ); +}); + +test("qwen-web PROVIDER_MODELS_CONFIG entry targets chat.qwen.ai/api/v2/models", () => { + const src = fs.readFileSync(ROUTE_FILE, "utf-8"); + assert.match( + src, + /chat\.qwen\.ai\/api\/v2\/models/, + "qwen-web discovery URL must be https://chat.qwen.ai/api/v2/models" + ); +}); + +test("qwen-web parseResponse handles Qwen nested data.data structure", () => { + const mockResponse = { + data: { + data: [ + { id: "qwen3.7-plus", name: "Qwen3.7-Plus", owned_by: "qwen" }, + { id: "qwen3-235b-a22b", name: "Qwen3-235B-A22B", owned_by: "qwen" }, + { id: "qwen3-coder-480b", name: "Qwen3-Coder-480B" }, + ], + }, + }; + + // parseResponse logic matches PROVIDER_MODELS_CONFIG["qwen-web"].parseResponse + const innerData: Array> = + (mockResponse?.data?.data as Array>) || + (mockResponse?.data as unknown as Array>) || + []; + const models = innerData + .map((item) => ({ + id: (item.id || item.name) as string, + name: (item.name || item.id) as string, + owned_by: (item.owned_by || "qwen") as string, + })) + .filter((m) => m.id); + + assert.equal(models.length, 3); + assert.equal(models[0].id, "qwen3.7-plus"); + assert.equal(models[0].name, "Qwen3.7-Plus"); + assert.equal(models[0].owned_by, "qwen"); + assert.equal(models[2].owned_by, "qwen", "owned_by defaults to 'qwen' when absent"); +}); + +test("qwen-web parseResponse handles flat data array fallback", () => { + const mockResponse = { + data: [{ id: "qwen3.7-plus", name: "Qwen3.7-Plus" }], + }; + + const innerData: Array> = + (mockResponse?.data as unknown as { data?: Array> })?.data || + (mockResponse?.data as unknown as Array>) || + []; + const models = innerData + .map((item) => ({ + id: (item.id || item.name) as string, + name: (item.name || item.id) as string, + owned_by: (item.owned_by || "qwen") as string, + })) + .filter((m) => m.id); + + assert.equal(models.length, 1); + assert.equal(models[0].id, "qwen3.7-plus"); +});