feat(compute): 声明 Anthropic 模型拒绝的采样参数与强制工具选择 (#103)

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
This commit is contained in:
2026-09-13 22:56:51 -04:00
committed by GitHub
parent 0be8207e32
commit 5171219a13
6 changed files with 101 additions and 7 deletions

View File

@@ -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 } } }],

View File

@@ -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": {

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"presetDataVersion": 116,
"presetDataVersion": 117,
"updatedAt": "2026-09-14",
"description": "DesireCore 官方配置中心"
}

View File

@@ -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 中。"

View File

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