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