fix(kimi-web, qwen-web): align model catalog with live /models + map scenario per model (#5915)

* fix(kimi-web): align catalog with live models

Update the kimi-web catalog and request scenario selection to match
www.kimi.com's live GetAvailableModels response.

* fix(qwen-web): stop aliasing qwen3-coder-plus

Keep qwen3-coder-plus as its own model because it is present in the
live Qwen web models catalog.
This commit is contained in:
janeza2
2026-07-03 18:35:56 +07:00
committed by GitHub
parent f496738d7f
commit 8d0ed4f936
6 changed files with 94 additions and 19 deletions

View File

@@ -14,8 +14,14 @@ export const kimi_webProvider: RegistryEntry = {
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "kimi-default", name: "Kimi Default" },
{ id: "kimi-k2.6", name: "Kimi K2.6 (Thinking)" },
{ id: "kimi-128k", name: "Kimi 128K (Long Context)" },
// Model ids are the `key` field from www.kimi.com's
// `/apiv2/kimi.gateway.config.v1.ConfigService/GetAvailableModels` response.
// Agent / Agent-Swarm variants (`k2d6-agent`, `k2d6-agent-ultra`) are
// intentionally NOT exposed — they need a different scenario
// (`SCENARIO_OK_COMPUTER`) plus `kimiPlusId` / `agentMode` fields, which
// the executor does not yet shape. Use `kimi-coding` (api.kimi.com) for
// agentic flows.
{ id: "k2d6", name: "K2.6 Instant" },
{ id: "k2d6-thinking", name: "K2.6 Thinking", supportsReasoning: true },
],
};

View File

@@ -36,7 +36,24 @@ const CHAT_URL = `${BASE_URL}/apiv2/kimi.gateway.chat.v1.ChatService/Chat`;
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
const DEFAULT_SCENARIO = "SCENARIO_K2D5";
/**
* Map a Kimi model id (the `key` field from `GetAvailableModels`) to the
* request shape the upstream expects. Today only the chat-tier `k2d6` family
* is supported — the agent variants (`k2d6-agent`, `k2d6-agent-ultra`) need
* a different scenario (`SCENARIO_OK_COMPUTER`) plus `kimiPlusId` /
* `agentMode` fields that this executor does not shape; users who need
* agentic Kimi should use the `kimi-coding` (api.kimi.com) provider.
*/
export interface KimiModelConfig {
scenario: string;
thinking: boolean;
}
export function resolveModelConfig(modelId: string): KimiModelConfig {
if (modelId === "k2d6-thinking") return { scenario: "SCENARIO_K2D5", thinking: true };
// `k2d6` (Instant) and any unknown id fall back to the default chat scenario.
return { scenario: "SCENARIO_K2D5", thinking: false };
}
/** Wrap a JSON message in the 5-byte Connect streaming envelope (flags + length). */
export function frameConnectMessage(json: string): Uint8Array {
@@ -206,14 +223,14 @@ export class KimiWebExecutor extends BaseExecutor {
return headers;
}
private buildRequestBody(prompt: string, wantThinking: boolean): string {
private buildRequestBody(prompt: string, wantThinking: boolean, scenario: string): string {
return JSON.stringify({
scenario: DEFAULT_SCENARIO,
scenario,
tools: [{ type: "TOOL_TYPE_SEARCH", search: {} }, { type: "TOOL_TYPE_CRON_JOB" }],
message: {
role: "user",
blocks: [{ message_id: "", text: { content: prompt } }],
scenario: DEFAULT_SCENARIO,
scenario,
},
options: { thinking: wantThinking, enable_plugin: true },
});
@@ -236,14 +253,13 @@ export class KimiWebExecutor extends BaseExecutor {
const messages = (bodyObj.messages as Array<{ role: string; content: unknown }>) || [];
const modelId = (bodyObj.model as string) || "kimi-default";
// Decide thinking intent. A user sending `reasoning_effort: "none"` is
// explicit — honour it even when the model id suggests a thinking variant.
// Otherwise thinking models (kimi-k2.6 etc.) default to thinking on.
const modelWantsThinking = /k2\.6|k2-6|think/i.test(modelId);
const wantThinking = bodyObj.reasoning_effort === "none" ? false : modelWantsThinking;
// Resolve scenario + default thinking flag from the model id (catalog truth),
// then honour an explicit `reasoning_effort: "none"` override from the caller.
const modelConfig = resolveModelConfig(modelId);
const wantThinking = bodyObj.reasoning_effort === "none" ? false : modelConfig.thinking;
const prompt = foldMessages(messages);
const reqBody = this.buildRequestBody(prompt, wantThinking);
const reqBody = this.buildRequestBody(prompt, wantThinking, modelConfig.scenario);
const reqHeaders = this.buildKimiHeaders(jwt);
// Connect framing wraps the JSON body in a 5-byte envelope. Without it the

View File

@@ -58,7 +58,9 @@ const MODEL_ALIASES: Record<string, string> = {
"qwen3-plus": "qwen3.7-plus",
"qwen3-max": "qwen3.7-max",
"qwen3-flash": "qwen3.6-plus",
"qwen3-coder-plus": "qwen3.7-max",
// Note: `qwen3-coder-plus` is a real upstream model id (Qwen3-Coder) and
// must NOT be aliased — the previous `"qwen3-coder-plus": "qwen3.7-max"`
// entry silently rewrote valid coder requests to the wrong model.
"qwen3-coder-flash": "qwen3.6-plus",
qwen: "qwen3.7-max",
qwen3: "qwen3.7-max",

View File

@@ -77,6 +77,35 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
.filter((m: any) => m.id);
},
},
// #5858 follow-up: kimi-web (cookie provider) on the international domain.
// `GetAvailableModels` returns the model list as a plain JSON envelope
// (no Connect framing on either request or response — only the chat
// completion endpoint uses the 5-byte envelope). Auth: Bearer JWT extracted
// from the `kimi-auth` cookie the user pasted. Agent variants
// (`k2d6-agent*`) need a different scenario + agent fields this executor
// doesn't shape, so they're filtered out.
"kimi-web": {
url: "https://www.kimi.com/apiv2/kimi.gateway.config.v1.ConfigService/GetAvailableModels",
method: "GET",
headers: { accept: "application/json, text/plain, */*", "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: (data) => {
const list = (data?.availableModels || []) as Array<{
key?: string;
displayName?: string;
thinking?: boolean;
}>;
return list
.filter((m) => typeof m.key === "string" && !m.key?.includes("agent"))
.map((m) => ({
id: m.key as string,
name: m.displayName || (m.key as string),
supportsReasoning: !!m.thinking,
owned_by: "kimi",
}));
},
},
antigravity: {
url: getAntigravityModelsDiscoveryUrls()[0],
method: "POST",

View File

@@ -19,7 +19,7 @@ describe("KimiWebExecutor", () => {
it("execute returns a 400 error when no JWT is provided", async () => {
const executor = new mod.KimiWebExecutor();
const result = await executor.execute({
model: "kimi-default",
model: "k2d6",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "" },
@@ -43,7 +43,7 @@ describe("KimiWebExecutor", () => {
});
}) as typeof fetch;
await executor.execute({
model: "kimi-default",
model: "k2d6",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "kimi-auth=fake.jwt.token" },
@@ -57,6 +57,28 @@ describe("KimiWebExecutor", () => {
});
});
describe("resolveModelConfig", () => {
const { resolveModelConfig } = mod;
it("maps k2d6-thinking to the K2D5 scenario with thinking enabled", () => {
const cfg = resolveModelConfig("k2d6-thinking");
assert.equal(cfg.scenario, "SCENARIO_K2D5");
assert.equal(cfg.thinking, true);
});
it("maps k2d6 (Instant) to the K2D5 scenario without thinking", () => {
const cfg = resolveModelConfig("k2d6");
assert.equal(cfg.scenario, "SCENARIO_K2D5");
assert.equal(cfg.thinking, false);
});
it("falls back to K2D5 + no thinking for an unknown model id", () => {
const cfg = resolveModelConfig("k2d6-agent");
assert.equal(cfg.scenario, "SCENARIO_K2D5");
assert.equal(cfg.thinking, false);
});
});
describe("extractKimiJwt", () => {
const { extractKimiJwt } = mod;

View File

@@ -675,7 +675,7 @@ test("Kimi Web: targets www.kimi.com (international)", async () => {
const executor = new KimiWebExecutor();
const result = await executor.execute({
...noopExecuteInput,
model: "kimi-default",
model: "k2d6",
credentials: { apiKey: "kimi-auth=eyJ.eyJzdWI.signature" },
});
assert.ok(result.response instanceof Response);
@@ -695,7 +695,7 @@ test("Kimi Web: missing JWT returns a 400 before fetching", async () => {
const executor = new KimiWebExecutor();
const result = await executor.execute({
...noopExecuteInput,
model: "kimi-default",
model: "k2d6",
credentials: { apiKey: "" },
});
assert.equal(result.response.status, 400);
@@ -707,7 +707,7 @@ test("Kimi Web: error response returns error result", async () => {
const executor = new KimiWebExecutor();
const result = await executor.execute({
...noopExecuteInput,
model: "kimi-default",
model: "k2d6",
credentials: { apiKey: "kimi-auth=eyJ.eyJzdWI.signature" },
});
assert.ok(result.response instanceof Response);