Просмотр исходного кода

fix(core): kimi-k2.6 400 + temperature clamp 改为 bank 驱动 + 400 错误抽上游 body

根因:Moonshot kimi-k2.6 是 thinking 模型,API 硬拒绝非 1 的 temperature
(返回 `invalid temperature: only 1 is allowed for this model`)。原来的
requiresFixedTemperature 正则 startsWith("kimi-k2.5") || includes("thinking")
没覆盖 k2.6,inkos 传 0.7 → 400。

改法(更干净的重构,不是简单补正则):
- InkosModel 新增 temperature?: number 字段,语义是 **API 硬约束**
  (有值即 clamp per-call 温度,不是"推荐值";普通模型别设)
- provider.ts clampTemperatureForModel 改成 bank 驱动,删除正则兜底
- 给各 endpoint 的 k2.5 / k2.6 / kimi-k2-thinking 系列标 temperature: 1
  涉及 moonshot / bailian / bailianCodingPlan / infiniai / kimiCodingPlan /
  opencodeCodingPlan / ppio / siliconcloud / volcengine / volcengineCodingPlan
  共 21 处

顺手改 wrapLLMError 400 支路:按 R4 对 5xx 的做法,把上游 error body 的
message / type 抽出来放在错误消息开头——原来只报"API 返回 400 请求参数错误"
通用模板,吞掉了 Moonshot 返回的关键提示 "only 1 is allowed",debug 时必须
curl 才能看到。现在直接打印。

tests:
- `also clamps any model name containing 'thinking'` 依赖旧正则兜底,
  改成用 bank 里实际标了 temperature:1 的 kimi-k2-thinking(语义一致)
- 400 错误测试的期望从 "检查提供方文档" 改为 "temperature"(新模板里提到它)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fanghanjun 6 месяцев назад
Родитель
Сommit
c5e93f8d48

+ 3 - 3
packages/core/src/__tests__/provider.test.ts

@@ -180,7 +180,7 @@ describe("chatCompletion via pi-ai", () => {
     );
 
     expect(error.message).toContain("API 返回 400");
-    expect(error.message).toContain("检查提供方文档");
+    expect(error.message).toContain("temperature");
   });
 
   it("wraps 401 errors with an unauthorized message", async () => {
@@ -443,11 +443,11 @@ describe("chatCompletion fixed-temperature clamp (thinking models)", () => {
     warn.mockRestore();
   });
 
-  it("also clamps any model name containing 'thinking'", async () => {
+  it("clamps kimi-k2-thinking (bank-marked temperature:1)", async () => {
     const client = makeClient(0.5);
     vi.spyOn(console, "warn").mockImplementation(() => {});
 
-    await chatCompletion(client, "kimi-thinking-preview", [
+    await chatCompletion(client, "kimi-k2-thinking", [
       { role: "user", content: "hi" },
     ]);
 

+ 38 - 21
packages/core/src/llm/provider.ts

@@ -252,30 +252,34 @@ function stripReservedKeys(extra: Record<string, unknown>): Record<string, unkno
 
 // === Fixed-Temperature Model Clamp ===
 //
-// 部分 thinking 模型(如 Moonshot kimi-k2.5、kimi-thinking-preview)强制要求
-// temperature === 1,其他值会被 API 直接 400 拒绝。为让这类模型能和 inkos
-// 已有的 per-call 温度调参(0.1 validator → 0.8 architect brainstorm)共存,
-// 在 provider 层统一夹制:命中名单就把传入的 temperature 强制改成 1,并对
-// 每个模型名打一次 warning 提示用户。
-
-function requiresFixedTemperature(model: string): boolean {
-  const lower = model.toLowerCase();
-  // kimi-k2.5 及其子变体(k2.5-preview 等),以及任何名字里带 "thinking" 的模型
-  return lower.startsWith("kimi-k2.5") || lower.includes("thinking");
-}
+// 部分 thinking 模型(如 Moonshot kimi-k2.5/k2.6、kimi-k2-thinking)的 API
+// 硬要求 temperature === 1,其他值会被直接 400 拒绝(Moonshot 返回
+// `invalid temperature: only 1 is allowed for this model`)。
+//
+// inkos 让 writer/validator/architect 各自带 per-call 温度(0.1~1.5),
+// 所以 provider 层统一夹制:如果 bank 里模型卡标了 temperature 字段,
+// 就把 per-call 温度 clamp 到那个值,并对每个模型名打一次 warning。
+//
+// 这个字段只表达"服务端硬约束",普通模型不要标,避免误伤 per-call 调参。
 
 const warnedFixedTemperatureModels = new Set<string>();
 
-function clampTemperatureForModel(model: string, requested: number): number {
-  if (!requiresFixedTemperature(model)) return requested;
-  if (requested === 1) return 1;
+function clampTemperatureForModel(
+  service: string | undefined,
+  model: string,
+  requested: number,
+): number {
+  const card = service ? lookupModel(service, model) : undefined;
+  if (card?.temperature === undefined) return requested;
+  const locked = card.temperature;
+  if (requested === locked) return locked;
   if (!warnedFixedTemperatureModels.has(model)) {
     warnedFixedTemperatureModels.add(model);
     console.warn(
-      `[inkos] 模型 "${model}" 是 thinking 模型,强制 temperature=1(原请求值 ${requested})`,
+      `[inkos] 模型 "${model}" API 要求 temperature=${locked},已 clamp(原值 ${requested})`,
     );
   }
-  return 1;
+  return locked;
 }
 
 // 仅测试用:清空 warning 去重集合。
@@ -292,12 +296,23 @@ function wrapLLMError(error: unknown, context?: { readonly baseUrl?: string; rea
     : "";
 
   if (msg.includes("400")) {
+    // 抽上游 error body 的 message / reason / code(和下方 5xx 一致),让真实错因浮到用户面前
+    let detail = "";
+    if (error && typeof error === "object") {
+      const err = error as { error?: unknown; body?: unknown; message?: string };
+      const bodyLike = err.error ?? err.body;
+      if (bodyLike && typeof bodyLike === "object") {
+        const b = bodyLike as { reason?: string; message?: string; code?: number | string; type?: string };
+        if (b.message) detail = b.type ? `${b.type}: ${b.message}` : b.message;
+        else if (b.reason) detail = b.reason;
+      }
+    }
     return new Error(
-      `API 返回 400 (请求参数错误)。可能原因:\n` +
-      `  1. 模型名称不正确(检查 INKOS_LLM_MODEL)\n` +
-      `  2. 提供方不支持某些参数(如 max_tokens、stream)\n` +
-      `  3. 消息格式不兼容(部分提供方不支持 system role)\n` +
-      `  建议:检查提供方文档,确认该接口要求流式开启、流式关闭,还是根本不支持 stream${ctxLine}`,
+      `API 返回 400(请求参数错误)。${detail ? `上游详情:${detail}。\n` : ""}` +
+      `常见原因:\n` +
+      `  1. temperature / max_tokens 超出模型约束(如 Moonshot kimi-k2.X 强制 temperature=1)\n` +
+      `  2. 模型名称不正确或未上架\n` +
+      `  3. 消息格式不兼容(部分服务不支持 system role 或 developer role)${ctxLine}`,
     );
   }
   if (msg.includes("403")) {
@@ -779,6 +794,7 @@ export async function chatCompletion(
   // C1 (v2.0.0):删除 maxTokensCap 机制。per-call 显式传的 maxTokens 永远不被裁剪。
   const resolved = {
     temperature: clampTemperatureForModel(
+      client.service,
       model,
       options?.temperature ?? client.defaults.temperature,
     ),
@@ -821,6 +837,7 @@ export async function chatWithTools(
   try {
     const resolved = {
       temperature: clampTemperatureForModel(
+        client.service,
         model,
         options?.temperature ?? client.defaults.temperature,
       ),

+ 3 - 3
packages/core/src/llm/providers/endpoints/bailian.ts

@@ -30,8 +30,8 @@ export const BAILIAN: InkosEndpoint = {
     { id: "qwen3.6-27b", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2026-04-23" },
     { id: "qwen3.6-35b-a3b", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2026-04-17" },
     // --- 第三方代理(百炼平台上的) ---
-    { id: "kimi-k2.6", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21" },
-    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144 },
+    { id: "kimi-k2.6", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21", temperature: 1 },
+    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, temperature: 1 },
     { id: "glm-5.1", maxOutput: 16384, contextWindowTokens: 202752, enabled: true, releasedAt: "2026-04-23" },
     { id: "MiniMax-M2.5", maxOutput: 32768, contextWindowTokens: 196608 },
     { id: "MiniMax-M2.1", maxOutput: 32768, contextWindowTokens: 204800 },
@@ -40,7 +40,7 @@ export const BAILIAN: InkosEndpoint = {
     { id: "deepseek-v3.2", maxOutput: 65536, contextWindowTokens: 131072 },
     { id: "deepseek-v3.2-exp", maxOutput: 65536, contextWindowTokens: 131072 },
     { id: "deepseek-v3.1", maxOutput: 65536, contextWindowTokens: 131072 },
-    { id: "kimi-k2-thinking", maxOutput: 16384, contextWindowTokens: 262144, releasedAt: "2025-11-10" },
+    { id: "kimi-k2-thinking", maxOutput: 16384, contextWindowTokens: 262144, releasedAt: "2025-11-10", temperature: 1 },
     { id: "Moonshot-Kimi-K2-Instruct", maxOutput: 8192, contextWindowTokens: 131072, releasedAt: "2025-07-17" },
     { id: "glm-5", maxOutput: 16384, contextWindowTokens: 202752 },
     { id: "glm-4.7", maxOutput: 16384, contextWindowTokens: 202752 },

+ 1 - 1
packages/core/src/llm/providers/endpoints/bailianCodingPlan.ts

@@ -23,7 +23,7 @@ export const BAILIAN_CODING_PLAN: InkosEndpoint = {
     { id: "qwen3-coder-next", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2026-02-15" },
     { id: "glm-5", maxOutput: 131072, contextWindowTokens: 200000, enabled: true, releasedAt: "2026-02-12" },
     { id: "glm-4.7", maxOutput: 131072, contextWindowTokens: 200000, enabled: true, releasedAt: "2025-12-01" },
-    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-01-27" },
+    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-01-27", temperature: 1 },
     { id: "MiniMax-M2.5", maxOutput: 131072, contextWindowTokens: 204800, enabled: true, releasedAt: "2026-02-12" },
   ],
 };

+ 1 - 1
packages/core/src/llm/providers/endpoints/infiniai.ts

@@ -23,7 +23,7 @@ export const INFINIAI: InkosEndpoint = {
     { id: "glm-4.6v", maxOutput: 4096, contextWindowTokens: 131072 },
     { id: "glm-5", maxOutput: 4096, contextWindowTokens: 198000, enabled: true, releasedAt: "2026-02-13" },
     { id: "minimax-m2.5", maxOutput: 4096, contextWindowTokens: 204800, releasedAt: "2026-02-13" },
-    { id: "kimi-k2-thinking", maxOutput: 32768, contextWindowTokens: 262144, releasedAt: "2025-11-07" },
+    { id: "kimi-k2-thinking", maxOutput: 32768, contextWindowTokens: 262144, releasedAt: "2025-11-07", temperature: 1 },
     { id: "deepseek-ocr", maxOutput: 8192, contextWindowTokens: 8192, releasedAt: "2025-10-20" },
     { id: "minimax-m2.1", maxOutput: 200000, contextWindowTokens: 200000 },
     { id: "minimax-m2", maxOutput: 200000, contextWindowTokens: 200000 },

+ 2 - 2
packages/core/src/llm/providers/endpoints/kimiCodingPlan.ts

@@ -17,7 +17,7 @@ export const KIMI_CODING_PLAN: InkosEndpoint = {
   writingTemperature: 1,
   temperatureHint: "kimi-k2.5 推荐 temperature=1.0",
   models: [
-    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-01-27", deploymentName: "k2p5" },
-    { id: "kimi-k2-thinking", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-11-06" },
+    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-01-27", deploymentName: "k2p5", temperature: 1 },
+    { id: "kimi-k2-thinking", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-11-06", temperature: 1 },
   ],
 };

+ 4 - 4
packages/core/src/llm/providers/endpoints/moonshot.ts

@@ -20,10 +20,10 @@ export const MOONSHOT: InkosEndpoint = {
   writingTemperature: 1,
   temperatureHint: "kimi-k2.5/k2.6 推荐 temperature=1.0",
   models: [
-    { id: "kimi-k2.6", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21" },
-    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-01-27" },
-    { id: "kimi-k2-thinking", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-11-06" },
-    { id: "kimi-k2-thinking-turbo", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-11-06" },
+    { id: "kimi-k2.6", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21", temperature: 1 },
+    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-01-27", temperature: 1 },
+    { id: "kimi-k2-thinking", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-11-06", temperature: 1 },
+    { id: "kimi-k2-thinking-turbo", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-11-06", temperature: 1 },
     { id: "kimi-k2-0905-preview", maxOutput: 65536, contextWindowTokens: 262144, releasedAt: "2025-09-05" },
     { id: "kimi-k2-0711-preview", maxOutput: 4096, contextWindowTokens: 131072, releasedAt: "2025-07-11" },
     { id: "kimi-k2-turbo-preview", maxOutput: 4096, contextWindowTokens: 262144, enabled: true, releasedAt: "2025-09-05" },

+ 1 - 1
packages/core/src/llm/providers/endpoints/opencodeCodingPlan.ts

@@ -18,7 +18,7 @@ export const OPENCODE_CODING_PLAN: InkosEndpoint = {
   models: [
     { id: "glm-5.1", maxOutput: 32000, contextWindowTokens: 204800, enabled: true, releasedAt: "2026-04-07" },
     { id: "glm-5", maxOutput: 32000, contextWindowTokens: 204800, enabled: false, releasedAt: "2026-02-11" },
-    { id: "kimi-k2.5", maxOutput: 32000, contextWindowTokens: 262144, enabled: false, releasedAt: "2026-01-27" },
+    { id: "kimi-k2.5", maxOutput: 32000, contextWindowTokens: 262144, enabled: false, releasedAt: "2026-01-27", temperature: 1 },
     { id: "mimo-v2-omni", maxOutput: 32000, contextWindowTokens: 262144, enabled: false, releasedAt: "2026-03-18" },
     { id: "qwen3.6-plus", maxOutput: 32000, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-02" },
     { id: "minimax-m2.5", maxOutput: 32000, contextWindowTokens: 204800, enabled: false, releasedAt: "2026-02-12" },

+ 3 - 3
packages/core/src/llm/providers/endpoints/ppio.ts

@@ -33,9 +33,9 @@ export const PPIO: InkosEndpoint = {
     { id: "deepseek/deepseek-r1-turbo", maxOutput: 65536, contextWindowTokens: 131072 },
     { id: "deepseek/deepseek-prover-v2-671b", maxOutput: 32768, contextWindowTokens: 131072 },
     // --- Moonshot Kimi 系列 ---
-    { id: "moonshotai/kimi-k2.6", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21" },
-    { id: "moonshotai/kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, releasedAt: "2026-01-27" },
-    { id: "moonshotai/kimi-k2-thinking", maxOutput: 65536, contextWindowTokens: 262144 },
+    { id: "moonshotai/kimi-k2.6", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21", temperature: 1 },
+    { id: "moonshotai/kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, releasedAt: "2026-01-27", temperature: 1 },
+    { id: "moonshotai/kimi-k2-thinking", maxOutput: 65536, contextWindowTokens: 262144, temperature: 1 },
     { id: "moonshotai/kimi-k2-0905", maxOutput: 8192, contextWindowTokens: 262144 },
     { id: "moonshotai/kimi-k2-instruct", maxOutput: 8192, contextWindowTokens: 131072 },
     // --- MiniMax ---

+ 4 - 4
packages/core/src/llm/providers/endpoints/siliconcloud.ts

@@ -22,7 +22,7 @@ export const SILICONCLOUD: InkosEndpoint = {
   writingTemperature: 1,
   models: [
     // --- 2026-04 新上 ---
-    { id: "Pro/moonshotai/Kimi-K2.6", maxOutput: 4096, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21" },
+    { id: "Pro/moonshotai/Kimi-K2.6", maxOutput: 4096, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-21", temperature: 1 },
     { id: "Pro/zai-org/GLM-5", maxOutput: 4096, contextWindowTokens: 198000 },
     { id: "Pro/zai-org/GLM-4.7", maxOutput: 4096, contextWindowTokens: 200000 },
     { id: "Qwen/Qwen3.6-35B-A3B", maxOutput: 4096, contextWindowTokens: 262144, enabled: true, releasedAt: "2026-04-17" },
@@ -40,7 +40,7 @@ export const SILICONCLOUD: InkosEndpoint = {
     { id: "zai-org/GLM-4.6V", maxOutput: 4096, contextWindowTokens: 131072, releasedAt: "2025-12-08" },
     { id: "deepseek-ai/DeepSeek-V3.2", maxOutput: 4096, contextWindowTokens: 163840, enabled: true, releasedAt: "2025-12-01" },
     { id: "Pro/deepseek-ai/DeepSeek-V3.2", maxOutput: 4096, contextWindowTokens: 163840, releasedAt: "2025-12-01" },
-    { id: "Pro/moonshotai/Kimi-K2.5", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-01-27" },
+    { id: "Pro/moonshotai/Kimi-K2.5", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-01-27", temperature: 1 },
     { id: "qwen/qwen3.5-397b-a17b", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-03-03" },
     { id: "qwen/qwen3.5-122b-a10b", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-03-03" },
     { id: "qwen/qwen3.5-35b-a3b", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-03-03" },
@@ -48,8 +48,8 @@ export const SILICONCLOUD: InkosEndpoint = {
     { id: "qwen/qwen3.5-9b", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-03-03" },
     { id: "qwen/qwen3.5-4b", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2026-03-03" },
     { id: "Pro/zai-org/glm-4.7", maxOutput: 4096, contextWindowTokens: 200000, releasedAt: "2025-12-22" },
-    { id: "moonshotai/Kimi-K2-Thinking", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2025-11-07" },
-    { id: "Pro/moonshotai/Kimi-K2-Thinking", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2025-11-07" },
+    { id: "moonshotai/Kimi-K2-Thinking", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2025-11-07", temperature: 1 },
+    { id: "Pro/moonshotai/Kimi-K2-Thinking", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2025-11-07", temperature: 1 },
     { id: "MiniMaxAI/MiniMax-M2", maxOutput: 4096, contextWindowTokens: 131072, releasedAt: "2025-10-28" },
     { id: "Qwen/Qwen3-VL-32B-Instruct", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2025-10-21" },
     { id: "Qwen/Qwen3-VL-32B-Thinking", maxOutput: 4096, contextWindowTokens: 262144, releasedAt: "2025-10-21" },

+ 1 - 1
packages/core/src/llm/providers/endpoints/volcengine.ts

@@ -26,7 +26,7 @@ export const VOLCENGINE: InkosEndpoint = {
     { id: "glm-4-7", maxOutput: 16000, contextWindowTokens: 200000 },
     { id: "deepseek-v3.2", maxOutput: 32768, contextWindowTokens: 131072 },
     { id: "deepseek-v3.1", maxOutput: 32768, contextWindowTokens: 131072 },
-    { id: "kimi-k2-thinking", maxOutput: 32768, contextWindowTokens: 262144 },
+    { id: "kimi-k2-thinking", maxOutput: 32768, contextWindowTokens: 262144, temperature: 1 },
     { id: "kimi-k2", maxOutput: 32768, contextWindowTokens: 262144 },
     { id: "doubao-seed-1.6-vision", maxOutput: 32000, contextWindowTokens: 256000 },
     { id: "doubao-seed-1.6-thinking", maxOutput: 32000, contextWindowTokens: 256000 },

+ 1 - 1
packages/core/src/llm/providers/endpoints/volcengineCodingPlan.ts

@@ -36,6 +36,6 @@ export const VOLCENGINE_CODING_PLAN: InkosEndpoint = {
     { id: "minimax-m2.5", maxOutput: 131072, contextWindowTokens: 204800, enabled: true },
     { id: "glm-4.7", maxOutput: 131072, contextWindowTokens: 200000, enabled: true },
     { id: "deepseek-v3.2", maxOutput: 65536, contextWindowTokens: 262144, enabled: true },
-    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true },
+    { id: "kimi-k2.5", maxOutput: 32768, contextWindowTokens: 262144, enabled: true, temperature: 1 },
   ],
 };

+ 6 - 0
packages/core/src/llm/providers/types.ts

@@ -24,6 +24,12 @@ export interface InkosModel {
   readonly deploymentName?: string;
   /** 发布日期 ISO 字符串,可选(用于 UI 的"新模型"徽章) */
   readonly releasedAt?: string;
+  /**
+   * API 硬要求的 temperature 值(例:Moonshot kimi-k2.5/k2.6 强制 1,违反直接 400)。
+   * 有值时 provider 层会 clamp 所有 per-call 温度到该值——这是服务端约束,
+   * 不是"推荐值"。普通模型不要设这个字段。
+   */
+  readonly temperature?: number;
 }
 
 export interface InkosEndpoint {