mirror of
https://git.openapi.site/https://github.com/desirecore/config-center.git
synced 2026-10-21 05:44:26 +08:00
fix(compute): 为 Anthropic adaptive 代际模型规格补齐 adaptiveThinking 声明 (#102)
Opus 5 / Sonnet 5 / Opus 4.8 / Opus 4.7 规格补 spec.extra.adaptiveThinking;各版本规格 family 收窄为自身版本,新增同名 claude-opus / claude-sonnet 家族规格承接兜底,防止声明泄漏给 Opus 4.5 等旧型号;schema 声明 adaptiveThinking 并写明匹配键约束;测试约束代际声明与 id/exact/pattern/family 不变式;presetDataVersion 115 → 116。
This commit is contained in:
@@ -597,6 +597,89 @@ describe('真实数据全量校验', () => {
|
||||
assert.equal(deepseekFlash.spec.maxOutputTokens, deepseekFlash0731.spec.maxOutputTokens)
|
||||
})
|
||||
|
||||
describe('Anthropic adaptive thinking 代际声明', () => {
|
||||
// 这些代际收到 thinking.type=enabled + budget_tokens 会直接 400。客户端只在官方
|
||||
// api.anthropic.com 端点按模型 ID 兜底;desirecore-cloud 等动态 Provider、非官方域名的
|
||||
// Anthropic Messages 中转只能从 ModelSpec 补全拿到声明,预置 Provider 条目不经补全,
|
||||
// 两处漏写都会回退成 budget_tokens(desirecore#2940)。Opus 4.6 / Sonnet 4.6 仍接受
|
||||
// budget_tokens(仅弃用),不在强制范围内。新代际若同样拒绝 budget_tokens,先扩展这里的正则。
|
||||
const adaptiveGeneration = /^claude-(?:fable|mythos)(?:$|-)|^claude-(?:opus|sonnet)-(?:[5-9]|[1-9]\d)(?:$|-)|^claude-opus-4-[78](?:$|-)/
|
||||
const thinkingOnlyGeneration = /^claude-(?:fable|mythos)(?:$|-)/
|
||||
// 与客户端匹配器同口径:小写、去 vendor 前缀、统一分隔符
|
||||
const normalizeId = (id) => id.toLowerCase().trim().split('/').at(-1).replace(/[._:\s]+/g, '-')
|
||||
const specsDir = join(ROOT, 'compute', 'model-specs')
|
||||
const allSpecs = JSON.parse(readFileSync(join(specsDir, '_index.json'), 'utf8')).order
|
||||
.flatMap((name) => JSON.parse(readFileSync(join(specsDir, `${name}.json`), 'utf8')).specs)
|
||||
|
||||
it('规格按代际声明 adaptiveThinking,只有 Fable/Mythos 声明思考恒开', () => {
|
||||
const adaptiveSpecIds = allSpecs.map((item) => item.id).filter((id) => adaptiveGeneration.test(normalizeId(id)))
|
||||
for (const id of ['claude-fable-5-1', 'claude-opus-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-sonnet-5']) {
|
||||
assert.ok(adaptiveSpecIds.includes(id), `model-specs 缺少 ${id}`)
|
||||
}
|
||||
for (const modelSpec of allSpecs) {
|
||||
const id = normalizeId(modelSpec.id)
|
||||
const extra = modelSpec.spec.extra ?? {}
|
||||
assert.equal(
|
||||
extra.adaptiveThinking === true,
|
||||
adaptiveGeneration.test(id),
|
||||
`${modelSpec.id} 的 adaptiveThinking 与代际不符;若它是新的拒绝 budget_tokens 代际,先扩展本测试的代际正则`,
|
||||
)
|
||||
if (id.startsWith('claude-')) {
|
||||
assert.equal(extra.thinkingOnly === true, thinkingOnlyGeneration.test(id), `${modelSpec.id} 的 thinkingOnly 与代际不符`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('匹配键与 family 兜底不会把代际声明带给其他代际', () => {
|
||||
// 客户端依次按 id/exact、pattern、最长 family 前缀匹配,并把规格 extra 写进模型条目
|
||||
// (精确与 pattern 命中时 cloud 同步直接覆盖)。任何一个键过宽——family 写成 claude-opus、
|
||||
// pattern 写成 claude-opus-4*——Opus 4.5 等旧型号就会继承 adaptiveThinking,
|
||||
// 连官方端点都被改写成 adaptive 而 400。pattern 取第一个 * 之前的前缀判断。
|
||||
const matchKeys = (modelSpec) => [
|
||||
modelSpec.id,
|
||||
...(modelSpec.family ? [modelSpec.family] : []),
|
||||
...(modelSpec.match?.exact ?? []),
|
||||
...(modelSpec.match?.patterns ?? []).map((pattern) => pattern.split('*')[0]),
|
||||
].map(normalizeId)
|
||||
|
||||
for (const modelSpec of allSpecs) {
|
||||
const extra = modelSpec.spec.extra ?? {}
|
||||
for (const key of matchKeys(modelSpec)) {
|
||||
if (extra.adaptiveThinking === true) {
|
||||
assert.ok(adaptiveGeneration.test(key), `${modelSpec.id} 声明了 adaptiveThinking,匹配键「${key}」必须收窄到同一代际`)
|
||||
}
|
||||
if (extra.thinkingOnly === true && key.startsWith('claude-')) {
|
||||
assert.ok(thinkingOnlyGeneration.test(key), `${modelSpec.id} 声明了 thinkingOnly,匹配键「${key}」必须收窄到同一代际`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 未单独收录的 Opus / Sonnet 版本由同名家族规格保守补全,不带任何代际声明
|
||||
for (const family of ['claude-opus', 'claude-sonnet']) {
|
||||
const holders = allSpecs.filter((item) => item.family === family)
|
||||
assert.deepEqual(holders.map((item) => item.id), [family], `${family} 家族兜底应只由同名规格承接`)
|
||||
assert.deepEqual(holders[0].spec.extra ?? {}, {}, `${family} 家族兜底规格不得声明代际属性`)
|
||||
}
|
||||
})
|
||||
|
||||
it('预置 anthropic-messages 条目自行声明 adaptiveThinking', () => {
|
||||
let checked = 0
|
||||
for (const dir of [join(ROOT, 'compute', 'providers'), join(ROOT, 'compute', 'coding-plans')]) {
|
||||
for (const file of readdirSync(dir).filter((name) => name.endsWith('.json') && !name.startsWith('_'))) {
|
||||
const provider = JSON.parse(readFileSync(join(dir, file), 'utf8'))
|
||||
for (const model of provider.models ?? []) {
|
||||
const apiFormat = model.apiFormat ?? provider.apiFormat
|
||||
if (apiFormat !== 'anthropic-messages') continue
|
||||
const adaptive = adaptiveGeneration.test(normalizeId(model.apiModelId ?? model.modelName))
|
||||
if (!adaptive) continue
|
||||
checked += 1
|
||||
assert.equal(model.extra?.adaptiveThinking, true, `${file} 的 ${model.modelName} 缺少 extra.adaptiveThinking`)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(checked > 0, '未检查到任何 adaptive 代际的预置条目,数据结构可能已变化')
|
||||
})
|
||||
})
|
||||
|
||||
it('所有 Provider 应按供应商归属计价,模型来源不覆盖供应商币种', () => {
|
||||
const expectedCurrencies = {
|
||||
anthropic: 'USD',
|
||||
@@ -790,6 +873,19 @@ describe('provider schema 反例(防 PR #1 重演)', () => {
|
||||
assert.equal(validate(data), true, JSON.stringify(validate.errors))
|
||||
})
|
||||
|
||||
it('Provider model 的 adaptiveThinking 只接受布尔值', () => {
|
||||
for (const value of [true, false]) {
|
||||
const valid = makeValidProvider()
|
||||
valid.models[0].extra = { adaptiveThinking: value }
|
||||
assert.equal(validate(valid), true, JSON.stringify(validate.errors))
|
||||
}
|
||||
for (const value of ['true', 1, null]) {
|
||||
const invalid = makeValidProvider()
|
||||
invalid.models[0].extra = { adaptiveThinking: value }
|
||||
assert.equal(validate(invalid), false)
|
||||
}
|
||||
})
|
||||
|
||||
it('严格校验 Provider model 的 native thinking round-trip 能力', () => {
|
||||
const valid = makeValidProvider()
|
||||
valid.models[0].extra = {
|
||||
@@ -943,6 +1039,16 @@ describe('model-spec schema 接入面边界', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('adaptiveThinking 是模型内在属性,只接受布尔值', () => {
|
||||
for (const value of [true, false]) {
|
||||
const data = { specs: [{ id: 'claude-test', spec: { extra: { adaptiveThinking: 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: { adaptiveThinking: value } } }] }), false)
|
||||
}
|
||||
})
|
||||
|
||||
it('仍允许模型内在扩展参数', () => {
|
||||
const data = {
|
||||
specs: [{ id: 'gpt-test', spec: { extra: { intrinsicBudgetHint: 8192 } } }],
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{
|
||||
"id": "claude-opus-5",
|
||||
"displayName": "Claude Opus 5",
|
||||
"family": "claude-opus",
|
||||
"family": "claude-opus-5",
|
||||
"match": {
|
||||
"patterns": [
|
||||
"claude-opus-5*"
|
||||
@@ -78,7 +78,10 @@
|
||||
"chat"
|
||||
],
|
||||
"supportsReasoning": true,
|
||||
"description": "Claude Opus 5,Anthropic Opus 系列当前旗舰;思考默认开启,effort 支持到 max"
|
||||
"description": "Claude Opus 5,Anthropic Opus 系列当前旗舰;思考默认开启,effort 支持到 max",
|
||||
"extra": {
|
||||
"adaptiveThinking": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"tier": "flagship",
|
||||
@@ -101,7 +104,7 @@
|
||||
{
|
||||
"id": "claude-opus-4-8",
|
||||
"displayName": "Claude Opus 4.8",
|
||||
"family": "claude-opus",
|
||||
"family": "claude-opus-4-8",
|
||||
"match": {
|
||||
"patterns": [
|
||||
"claude-opus-4-8*"
|
||||
@@ -122,13 +125,16 @@
|
||||
"chat"
|
||||
],
|
||||
"supportsReasoning": true,
|
||||
"description": "Claude Opus 4.8,Anthropic Opus 系列最新旗舰"
|
||||
"description": "Claude Opus 4.8,Anthropic Opus 系列最新旗舰",
|
||||
"extra": {
|
||||
"adaptiveThinking": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-7",
|
||||
"displayName": "Claude Opus 4.7",
|
||||
"family": "claude-opus",
|
||||
"family": "claude-opus-4-7",
|
||||
"match": {
|
||||
"patterns": [
|
||||
"claude-opus-4-7*"
|
||||
@@ -149,13 +155,36 @@
|
||||
],
|
||||
"defaultTemperature": 1,
|
||||
"supportsReasoning": true,
|
||||
"description": "Anthropic 当前最强通用模型,适合复杂推理和智能体编码任务"
|
||||
"description": "Anthropic 当前最强通用模型,适合复杂推理和智能体编码任务",
|
||||
"extra": {
|
||||
"adaptiveThinking": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "claude-opus",
|
||||
"family": "claude-opus",
|
||||
"spec": {
|
||||
"contextWindow": 1000000,
|
||||
"maxOutputTokens": 128000,
|
||||
"capabilities": [
|
||||
"chat",
|
||||
"reasoning",
|
||||
"code",
|
||||
"vision",
|
||||
"tool_use",
|
||||
"long_context"
|
||||
],
|
||||
"serviceType": [
|
||||
"chat"
|
||||
],
|
||||
"supportsReasoning": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "claude-sonnet-5",
|
||||
"displayName": "Claude Sonnet 5",
|
||||
"family": "claude-sonnet",
|
||||
"family": "claude-sonnet-5",
|
||||
"match": {
|
||||
"patterns": [
|
||||
"claude-sonnet-5*"
|
||||
@@ -177,7 +206,10 @@
|
||||
"chat"
|
||||
],
|
||||
"supportsReasoning": true,
|
||||
"description": "Claude Sonnet 5,速度与智能平衡;思考默认开启,effort 支持到 max"
|
||||
"description": "Claude Sonnet 5,速度与智能平衡;思考默认开启,effort 支持到 max",
|
||||
"extra": {
|
||||
"adaptiveThinking": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"tier": "balanced",
|
||||
@@ -200,7 +232,7 @@
|
||||
{
|
||||
"id": "claude-sonnet-4-6",
|
||||
"displayName": "Claude Sonnet 4.6",
|
||||
"family": "claude-sonnet",
|
||||
"family": "claude-sonnet-4-6",
|
||||
"match": {
|
||||
"patterns": [
|
||||
"claude-sonnet-4-6*"
|
||||
@@ -229,7 +261,7 @@
|
||||
{
|
||||
"id": "claude-sonnet-4-5",
|
||||
"displayName": "Claude Sonnet 4.5",
|
||||
"family": "claude-sonnet",
|
||||
"family": "claude-sonnet-4-5",
|
||||
"match": {
|
||||
"patterns": [
|
||||
"claude-sonnet-4-5*"
|
||||
@@ -252,6 +284,27 @@
|
||||
"supportsReasoning": true,
|
||||
"description": "Anthropic 主力模型,长文本与代码任务表现优异"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "claude-sonnet",
|
||||
"family": "claude-sonnet",
|
||||
"spec": {
|
||||
"contextWindow": 1000000,
|
||||
"maxOutputTokens": 128000,
|
||||
"capabilities": [
|
||||
"chat",
|
||||
"reasoning",
|
||||
"code",
|
||||
"vision",
|
||||
"tool_use",
|
||||
"computer_use",
|
||||
"long_context"
|
||||
],
|
||||
"serviceType": [
|
||||
"chat"
|
||||
],
|
||||
"supportsReasoning": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"presetDataVersion": 115,
|
||||
"presetDataVersion": 116,
|
||||
"updatedAt": "2026-09-14",
|
||||
"description": "DesireCore 官方配置中心"
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"type": "object",
|
||||
"description": "模型内在开放扩展:modelOrigin 描述真实原厂身份,speech 描述可跨小版本复用的语音协议;不含价格,也不得声明 Provider/接入面的 reasoning effort、thinking 回放能力(thinkingRoundTrip)或工具轮 thinking 校验口径(thinkingToolTurnValidation)——这些只能写在 provider model 的 extra 中,客户端 Schema 会拒收整份规格。",
|
||||
"description": "模型内在开放扩展:modelOrigin 描述真实原厂身份,speech 描述可跨小版本复用的语音协议,thinkingOnly 描述模型是否强制思考,adaptiveThinking 描述 Anthropic 模型是否只接受 adaptive thinking 线上形态;不含价格,也不得声明 Provider/接入面的 reasoning effort、thinking 回放能力(thinkingRoundTrip)或工具轮 thinking 校验口径(thinkingToolTurnValidation)——这些只能写在 provider model 的 extra 中,客户端 Schema 会拒收整份规格。",
|
||||
"allOf": [
|
||||
{ "not": { "required": ["reasoning"] } },
|
||||
{ "not": { "required": ["thinkingRoundTrip"] } },
|
||||
@@ -272,6 +272,10 @@
|
||||
"type": "boolean",
|
||||
"description": "模型强制思考、无法关闭(如 GLM-5.3 系列 thinking.type 仅接受 enabled)。true 时客户端把 off 从可选档位移除,并把内部 suppressThinking 降级为 auto,避免发出必然被上游拒绝的关闭参数。只描述模型固有属性,不声明接入面支持哪些深度档位——后者只能写在 provider model 的 extra.reasoning 中。"
|
||||
},
|
||||
"adaptiveThinking": {
|
||||
"type": "boolean",
|
||||
"description": "Anthropic Messages 模型是否属于 adaptive thinking 代际(Claude Opus 4.7+ / Opus 5 / Sonnet 5 / Fable / Mythos)。true 时客户端把思考参数发成 thinking.type=adaptive + output_config.effort,而不是会被上游 400 拒绝的 thinking.type=enabled + budget_tokens;false 表示明确不是,禁止客户端按模型 ID 推断;缺省时客户端仅在官方 api.anthropic.com 端点上按模型代际表兜底。只作用于 anthropic-messages 接入面,Claude 订阅(claude-oauth)由 Claude CLI 自行改写、不受本字段影响。命中本规格的模型由客户端 applySpec 复制到 provider model extra,运行时读的是那份副本——desirecore-cloud 等动态 Provider、非官方域名的 Anthropic Messages 中转只能从这里拿到声明,漏写即回退为 budget_tokens。match.exact、match.patterns 与 family 兜底命中时都会带上本键,因此声明本键的规格,其 exact、pattern 前缀与 family 都必须收窄到同一代际(如 family 写 claude-opus-5 而不是 claude-opus,pattern 写 claude-opus-4-7* 而不是 claude-opus-4*),否则 Opus 4.5 等旧型号会继承声明并被上游 400 拒绝。"
|
||||
},
|
||||
"thinkingDefault": {
|
||||
"type": "boolean",
|
||||
"description": "模型默认开启思考。当前不改变客户端行为,仅供维护者参考。"
|
||||
|
||||
@@ -254,6 +254,10 @@
|
||||
"type": "object",
|
||||
"description": "接入面特定配置:如 TTS 模型音色列表、ASR 支持格式、reasoning effort、native thinking 回放能力与工具轮 thinking 校验口径。这里是兼容扩展面:旧客户端会安全保留并忽略未知子字段。",
|
||||
"properties": {
|
||||
"adaptiveThinking": {
|
||||
"type": "boolean",
|
||||
"description": "Anthropic Messages 模型是否属于 adaptive thinking 代际(Claude Opus 4.7+ / Opus 5 / Sonnet 5 / Fable / Mythos)。true 时客户端把思考参数发成 thinking.type=adaptive + output_config.effort,而不是会被上游 400 拒绝的 thinking.type=enabled + budget_tokens;false 表示明确不是,禁止客户端按模型 ID 推断;缺省时客户端仅在官方 api.anthropic.com 端点上按模型代际表兜底。只作用于 anthropic-messages 接入面,Claude 订阅(claude-oauth)由 Claude CLI 自行改写、不受本字段影响。这是模型固有属性,权威声明写在 model-spec 的 spec.extra.adaptiveThinking;预置 Provider 条目不经规格库补全,必须在这里同样声明。"
|
||||
},
|
||||
"thinkingRoundTrip": {
|
||||
"type": "object",
|
||||
"description": "当前 Provider/model 接入面是否允许回放 Provider 原样返回的 Anthropic thinking。只授权当前未闭合工具轨迹;UI 展示 thinking、已完成旧轮次、跨模型/凭据数据永远没有回放资格。",
|
||||
|
||||
Reference in New Issue
Block a user