From 5171219a136bc7ca3b967b585a433c3cde3a8c23 Mon Sep 17 00:00:00 2001 From: Yige Date: Sun, 13 Sep 2026 22:56:51 -0400 Subject: [PATCH] =?UTF-8?q?feat(compute):=20=E5=A3=B0=E6=98=8E=20Anthropic?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E6=8B=92=E7=BB=9D=E7=9A=84=E9=87=87?= =?UTF-8?q?=E6=A0=B7=E5=8F=82=E6=95=B0=E4=B8=8E=E5=BC=BA=E5=88=B6=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E9=80=89=E6=8B=A9=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Opus 4.7+ / Opus 5 / Fable 携带 temperature / top_p / top_k 会被官方 400 (Sonnet 5 非默认值 400),Claude Fable 5.1 对 tool_choice any / tool 回 400。客户端 改为纯按声明在出站删除采样参数、把强制工具选择降级为 auto(desirecore 主仓配套 PR)。 - providers/anthropic.json:claude-fable-5-1 增加 forcedToolChoiceUnsupported - model-specs/anthropic.json:fable-5-1 / opus-5 / opus-4-8 / opus-4-7 / sonnet-5 声明 samplingParametersDeprecated,fable-5-1 声明 forcedToolChoiceUnsupported,供精确命中 补全的动态 Provider 使用;opus-4-7 的 defaultTemperature 由 1 改为 null(不支持温度调节) - schemas:provider model extra 与 ModelSpec spec.extra 声明两个布尔字段(含 description) - 测试:已知拒绝代际必须声明(单向)、布尔类型校验 - presetDataVersion 116 → 117 --- __tests__/validate.test.mjs | 71 ++++++++++++++++++++++++++++++ compute/model-specs/anthropic.json | 18 +++++--- compute/providers/anthropic.json | 1 + manifest.json | 2 +- schemas/model-spec.schema.json | 8 ++++ schemas/provider.schema.json | 8 ++++ 6 files changed, 101 insertions(+), 7 deletions(-) diff --git a/__tests__/validate.test.mjs b/__tests__/validate.test.mjs index de373c6..0ba8c28 100644 --- a/__tests__/validate.test.mjs +++ b/__tests__/validate.test.mjs @@ -680,6 +680,50 @@ describe('真实数据全量校验', () => { }) }) + it('拒绝采样参数 / 强制工具选择的 Anthropic 模型应在规格与 API Key 预置条目中声明', () => { + // 这些模型收到 temperature / top_p / top_k(Fable 5.1 还有 tool_choice any/tool)会直接 400。 + // 客户端纯按声明改写出站请求体、不按模型 ID 推断:API Key 预置条目不经 ModelSpec 补全, + // desirecore-cloud 等动态 Provider 只能从精确命中的 ModelSpec 拿到声明,两处漏写都会把参数原样发出。 + // 只做单向断言(已知拒绝的代际必须声明),新代际按官方文档补声明即可,不维护反向代际表。 + // 订阅(claude-oauth)请求由 Claude CLI 自建,客户端 compat 层对这两类参数显式 fail closed,不依赖声明。 + const samplingRemoved = /^claude-(?:fable|mythos)-|^claude-(?:opus|sonnet)-5(?:$|-)|^claude-opus-4-[78](?:$|-)/ + const forcedToolChoiceRemoved = /^claude-(?:fable|mythos)-5-1(?:$|-)/ + + const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'anthropic.json'), 'utf8')) + for (const id of ['claude-fable-5-1', 'claude-opus-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-sonnet-5']) { + assert.ok(specFile.specs.some((item) => item.id === id), `model-specs 缺少 ${id}`) + } + for (const modelSpec of specFile.specs) { + const extra = modelSpec.spec.extra ?? {} + if (samplingRemoved.test(modelSpec.id)) { + assert.equal(extra.samplingParametersDeprecated, true, `${modelSpec.id} 缺少 spec.extra.samplingParametersDeprecated`) + assert.notEqual(typeof modelSpec.spec.defaultTemperature, 'number', `${modelSpec.id} 拒绝采样参数,不应声明数值 defaultTemperature`) + } + if (forcedToolChoiceRemoved.test(modelSpec.id)) { + assert.equal(extra.forcedToolChoiceUnsupported, true, `${modelSpec.id} 缺少 spec.extra.forcedToolChoiceUnsupported`) + } + } + + const presetDirs = [join(ROOT, 'compute', 'providers'), join(ROOT, 'compute', 'coding-plans')] + for (const dir of presetDirs) { + for (const file of readdirSync(dir).filter((name) => name.endsWith('.json') && !name.startsWith('_'))) { + const provider = JSON.parse(readFileSync(join(dir, file), 'utf8')) + if (provider.credentialSource === 'claude-oauth') continue + for (const model of provider.models ?? []) { + const apiFormat = model.apiFormat ?? provider.apiFormat + const upstreamId = model.apiModelId ?? model.modelName + if (apiFormat !== 'anthropic-messages') continue + if (samplingRemoved.test(upstreamId)) { + assert.equal(model.extra?.samplingParametersDeprecated, true, `${file} 的 ${model.modelName} 缺少 extra.samplingParametersDeprecated`) + } + if (forcedToolChoiceRemoved.test(upstreamId)) { + assert.equal(model.extra?.forcedToolChoiceUnsupported, true, `${file} 的 ${model.modelName} 缺少 extra.forcedToolChoiceUnsupported`) + } + } + } + } + }) + it('所有 Provider 应按供应商归属计价,模型来源不覆盖供应商币种', () => { const expectedCurrencies = { anthropic: 'USD', @@ -886,6 +930,21 @@ describe('provider schema 反例(防 PR #1 重演)', () => { } }) + it('Provider model 的 samplingParametersDeprecated / forcedToolChoiceUnsupported 只接受布尔值', () => { + for (const key of ['samplingParametersDeprecated', 'forcedToolChoiceUnsupported']) { + for (const value of [true, false]) { + const valid = makeValidProvider() + valid.models[0].extra = { [key]: value } + assert.equal(validate(valid), true, JSON.stringify(validate.errors)) + } + for (const value of ['true', 1, null]) { + const invalid = makeValidProvider() + invalid.models[0].extra = { [key]: value } + assert.equal(validate(invalid), false) + } + } + }) + it('严格校验 Provider model 的 native thinking round-trip 能力', () => { const valid = makeValidProvider() valid.models[0].extra = { @@ -1049,6 +1108,18 @@ describe('model-spec schema 接入面边界', () => { } }) + it('samplingParametersDeprecated / forcedToolChoiceUnsupported 是模型内在属性,只接受布尔值', () => { + for (const key of ['samplingParametersDeprecated', 'forcedToolChoiceUnsupported']) { + for (const value of [true, false]) { + const data = { specs: [{ id: 'claude-test', spec: { extra: { [key]: value } } }] } + assert.equal(validate(data), true, JSON.stringify(validate.errors)) + } + for (const value of ['true', 1, null]) { + assert.equal(validate({ specs: [{ id: 'claude-test', spec: { extra: { [key]: value } } }] }), false) + } + } + }) + it('仍允许模型内在扩展参数', () => { const data = { specs: [{ id: 'gpt-test', spec: { extra: { intrinsicBudgetHint: 8192 } } }], diff --git a/compute/model-specs/anthropic.json b/compute/model-specs/anthropic.json index 964c7cb..c894561 100644 --- a/compute/model-specs/anthropic.json +++ b/compute/model-specs/anthropic.json @@ -33,7 +33,9 @@ "extra": { "adaptiveThinking": true, "thinkingOnByDefault": true, - "thinkingOnly": true + "thinkingOnly": true, + "samplingParametersDeprecated": true, + "forcedToolChoiceUnsupported": true } }, "routing": { @@ -80,7 +82,8 @@ "supportsReasoning": true, "description": "Claude Opus 5,Anthropic Opus 系列当前旗舰;思考默认开启,effort 支持到 max", "extra": { - "adaptiveThinking": true + "adaptiveThinking": true, + "samplingParametersDeprecated": true } }, "routing": { @@ -127,7 +130,8 @@ "supportsReasoning": true, "description": "Claude Opus 4.8,Anthropic Opus 系列最新旗舰", "extra": { - "adaptiveThinking": true + "adaptiveThinking": true, + "samplingParametersDeprecated": true } } }, @@ -153,11 +157,12 @@ "serviceType": [ "chat" ], - "defaultTemperature": 1, + "defaultTemperature": null, "supportsReasoning": true, "description": "Anthropic 当前最强通用模型,适合复杂推理和智能体编码任务", "extra": { - "adaptiveThinking": true + "adaptiveThinking": true, + "samplingParametersDeprecated": true } } }, @@ -208,7 +213,8 @@ "supportsReasoning": true, "description": "Claude Sonnet 5,速度与智能平衡;思考默认开启,effort 支持到 max", "extra": { - "adaptiveThinking": true + "adaptiveThinking": true, + "samplingParametersDeprecated": true } }, "routing": { diff --git a/compute/providers/anthropic.json b/compute/providers/anthropic.json index 26c51e0..4f0bbed 100644 --- a/compute/providers/anthropic.json +++ b/compute/providers/anthropic.json @@ -66,6 +66,7 @@ "defaultEffort": "high" }, "samplingParametersDeprecated": true, + "forcedToolChoiceUnsupported": true, "pricingNotes": "Prices are per 1M tokens. Full 1M context is billed at standard pricing.", "thinkingOnByDefault": true, "thinkingOnly": true diff --git a/manifest.json b/manifest.json index 3938d97..e1dc2f2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "presetDataVersion": 116, + "presetDataVersion": 117, "updatedAt": "2026-09-14", "description": "DesireCore 官方配置中心" } diff --git a/schemas/model-spec.schema.json b/schemas/model-spec.schema.json index 89a725c..422ca02 100644 --- a/schemas/model-spec.schema.json +++ b/schemas/model-spec.schema.json @@ -268,6 +268,14 @@ "properties": { "modelOrigin": { "$ref": "#/definitions/modelOrigin" }, "speech": { "$ref": "#/definitions/speechModelProfile" }, + "samplingParametersDeprecated": { + "type": "boolean", + "description": "模型是否拒绝采样参数(如 Claude Opus 4.7+ / Opus 5 / Fable / Mythos 携带 temperature、top_p 或 top_k 即 400,Sonnet 5 对非默认值 400)。true 时客户端出站删除请求体顶层的 temperature / top_p / top_k——无论取值来自模型 defaultTemperature、用户覆写、工作流默认值还是父会话继承;false 或缺省时原样发送,客户端不按模型 ID 推断。只描述模型固有属性:精确命中的 ModelSpec 会补全到各接入面的同名模型,family 兜底不继承;作用于 Agent 引擎发出的模型请求,Claude 订阅(claude-oauth)请求由 Claude CLI 自建、不受本字段影响。" + }, + "forcedToolChoiceUnsupported": { + "type": "boolean", + "description": "模型是否拒绝强制工具选择(如 Claude Fable 5.1 / Mythos 5.1:tool_choice.type 为 any 或 tool 即 400)。true 时客户端把强制工具选择降级为 auto(Anthropic 的 any / tool、OpenAI 的 required / 指定函数),Anthropic 请求保留 disable_parallel_tool_use,none 与 auto 不受影响;false 或缺省时按调用方要求发送,客户端不按模型 ID 推断。只描述模型固有属性:精确命中的 ModelSpec 会补全到各接入面的同名模型,family 兜底不继承;作用于 Agent 引擎发出的模型请求,Claude 订阅(claude-oauth)请求由 Claude CLI 自建、不受本字段影响。" + }, "thinkingOnly": { "type": "boolean", "description": "模型强制思考、无法关闭(如 GLM-5.3 系列 thinking.type 仅接受 enabled)。true 时客户端把 off 从可选档位移除,并把内部 suppressThinking 降级为 auto,避免发出必然被上游拒绝的关闭参数。只描述模型固有属性,不声明接入面支持哪些深度档位——后者只能写在 provider model 的 extra.reasoning 中。" diff --git a/schemas/provider.schema.json b/schemas/provider.schema.json index 2fd85ea..8a86f9c 100644 --- a/schemas/provider.schema.json +++ b/schemas/provider.schema.json @@ -284,6 +284,14 @@ }, "additionalProperties": false }, + "samplingParametersDeprecated": { + "type": "boolean", + "description": "模型是否拒绝采样参数(如 Claude Opus 4.7+ / Opus 5 / Fable / Mythos 携带 temperature、top_p 或 top_k 即 400,Sonnet 5 对非默认值 400)。true 时客户端出站删除请求体顶层的 temperature / top_p / top_k——无论取值来自模型 defaultTemperature、用户覆写、工作流默认值还是父会话继承;false 或缺省时原样发送,客户端不按模型 ID 推断。只描述模型固有属性:精确命中的 ModelSpec 会补全到各接入面的同名模型,family 兜底不继承;作用于 Agent 引擎发出的模型请求,Claude 订阅(claude-oauth)请求由 Claude CLI 自建、不受本字段影响。" + }, + "forcedToolChoiceUnsupported": { + "type": "boolean", + "description": "模型是否拒绝强制工具选择(如 Claude Fable 5.1 / Mythos 5.1:tool_choice.type 为 any 或 tool 即 400)。true 时客户端把强制工具选择降级为 auto(Anthropic 的 any / tool、OpenAI 的 required / 指定函数),Anthropic 请求保留 disable_parallel_tool_use,none 与 auto 不受影响;false 或缺省时按调用方要求发送,客户端不按模型 ID 推断。只描述模型固有属性:精确命中的 ModelSpec 会补全到各接入面的同名模型,family 兜底不继承;作用于 Agent 引擎发出的模型请求,Claude 订阅(claude-oauth)请求由 Claude CLI 自建、不受本字段影响。" + }, "thinkingToolTurnValidation": { "type": "string", "enum": ["requires-thinking", "accepts-thinkless"],