mirror of
https://git.openapi.site/https://github.com/desirecore/config-center.git
synced 2026-10-21 05:44:26 +08:00
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:
@@ -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 } } }],
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"presetDataVersion": 116,
|
||||
"presetDataVersion": 117,
|
||||
"updatedAt": "2026-09-14",
|
||||
"description": "DesireCore 官方配置中心"
|
||||
}
|
||||
|
||||
@@ -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 中。"
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user