From 0be8207e3267526375e5b08ca86a151dc8dcf260 Mon Sep 17 00:00:00 2001 From: Yige Date: Sun, 13 Sep 2026 22:39:20 -0400 Subject: [PATCH] =?UTF-8?q?fix(compute):=20=E4=B8=BA=20Anthropic=20adaptiv?= =?UTF-8?q?e=20=E4=BB=A3=E9=99=85=E6=A8=A1=E5=9E=8B=E8=A7=84=E6=A0=BC?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=20adaptiveThinking=20=E5=A3=B0=E6=98=8E=20(#?= =?UTF-8?q?102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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。 --- __tests__/validate.test.mjs | 106 +++++++++++++++++++++++++++++ compute/model-specs/anthropic.json | 73 +++++++++++++++++--- manifest.json | 2 +- schemas/model-spec.schema.json | 6 +- schemas/provider.schema.json | 4 ++ 5 files changed, 179 insertions(+), 12 deletions(-) diff --git a/__tests__/validate.test.mjs b/__tests__/validate.test.mjs index 6a6a796..de373c6 100644 --- a/__tests__/validate.test.mjs +++ b/__tests__/validate.test.mjs @@ -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 } } }], diff --git a/compute/model-specs/anthropic.json b/compute/model-specs/anthropic.json index 00886dc..964c7cb 100644 --- a/compute/model-specs/anthropic.json +++ b/compute/model-specs/anthropic.json @@ -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 + } } ] } diff --git a/manifest.json b/manifest.json index 0786681..3938d97 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "presetDataVersion": 115, + "presetDataVersion": 116, "updatedAt": "2026-09-14", "description": "DesireCore 官方配置中心" } diff --git a/schemas/model-spec.schema.json b/schemas/model-spec.schema.json index c08a3e9..89a725c 100644 --- a/schemas/model-spec.schema.json +++ b/schemas/model-spec.schema.json @@ -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": "模型默认开启思考。当前不改变客户端行为,仅供维护者参考。" diff --git a/schemas/provider.schema.json b/schemas/provider.schema.json index 3bc78e4..2fd85ea 100644 --- a/schemas/provider.schema.json +++ b/schemas/provider.schema.json @@ -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、已完成旧轮次、跨模型/凭据数据永远没有回放资格。",