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"],