mirror of
https://git.openapi.site/https://github.com/desirecore/config-center.git
synced 2026-10-21 05:24:25 +08:00
1478 lines
65 KiB
JavaScript
1478 lines
65 KiB
JavaScript
/**
|
||
* config-center frozen schema 校验测试
|
||
*
|
||
* 关键场景:
|
||
* - 现有所有数据文件必须通过 frozen schema 校验
|
||
* - PR #1 引发死锁的反例(defaultTemperature: null)必须被拒绝
|
||
* - 防御未知字段(additionalProperties: false)保护老客户端
|
||
*
|
||
* 运行:
|
||
* npm test
|
||
* node --test __tests__/
|
||
*/
|
||
|
||
import { describe, it } from 'node:test'
|
||
import { strict as assert } from 'node:assert'
|
||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||
import { dirname, join, resolve } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { Ajv } from 'ajv'
|
||
import addFormats from 'ajv-formats'
|
||
|
||
import { validateFile, loadSchemas } from '../scripts/validate.mjs'
|
||
|
||
const validators = loadSchemas()
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||
const ROOT = resolve(__dirname, '..')
|
||
|
||
function loadSchema(name) {
|
||
const p = join(ROOT, 'schemas', `${name}.schema.json`)
|
||
return JSON.parse(readFileSync(p, 'utf8'))
|
||
}
|
||
|
||
function compile(name) {
|
||
const ajv = new Ajv({ allErrors: true, strict: false })
|
||
addFormats(ajv)
|
||
return ajv.compile(loadSchema(name))
|
||
}
|
||
|
||
// ==================== 真实数据 happy path ====================
|
||
|
||
describe('真实数据全量校验', () => {
|
||
it('所有 compute/providers/*.json 应通过 provider schema', () => {
|
||
const dir = join(ROOT, 'compute', 'providers')
|
||
const failures = []
|
||
for (const file of readdirSync(dir)) {
|
||
if (!file.endsWith('.json') || file === '_index.json') continue
|
||
const result = validateFile(join(dir, file), validators)
|
||
if (!result.ok) failures.push({ file, errors: result.errors })
|
||
}
|
||
assert.equal(
|
||
failures.length,
|
||
0,
|
||
`${failures.length} provider 文件校验失败:\n${JSON.stringify(failures, null, 2)}`,
|
||
)
|
||
})
|
||
|
||
it('所有 compute/coding-plans/*.json 应通过 provider schema', () => {
|
||
const dir = join(ROOT, 'compute', 'coding-plans')
|
||
const failures = []
|
||
for (const file of readdirSync(dir)) {
|
||
if (!file.endsWith('.json') || file === '_index.json') continue
|
||
const result = validateFile(join(dir, file), validators)
|
||
if (!result.ok) failures.push({ file, errors: result.errors })
|
||
}
|
||
assert.equal(
|
||
failures.length,
|
||
0,
|
||
`${failures.length} coding-plan 文件校验失败:\n${JSON.stringify(failures, null, 2)}`,
|
||
)
|
||
})
|
||
|
||
it('manifest.json 应通过 manifest schema', () => {
|
||
const result = validateFile(join(ROOT, 'manifest.json'), validators)
|
||
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
|
||
})
|
||
|
||
it('compute/pricing.json 应通过 pricing schema', () => {
|
||
const result = validateFile(join(ROOT, 'compute', 'pricing.json'), validators)
|
||
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
|
||
})
|
||
|
||
it('compute/service-map.json 应通过 service-map schema', () => {
|
||
const result = validateFile(join(ROOT, 'compute', 'service-map.json'), validators)
|
||
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
|
||
})
|
||
|
||
it('model-specs 是智能路由的唯一规格主数据', () => {
|
||
const indexPath = join(ROOT, 'compute', 'model-specs', '_index.json')
|
||
const result = validateFile(indexPath, validators)
|
||
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
|
||
|
||
const index = JSON.parse(readFileSync(indexPath, 'utf8'))
|
||
assert.deepEqual(index.routingTiers.map((tier) => tier.id), [
|
||
'flagship',
|
||
'balanced',
|
||
'lightweight',
|
||
])
|
||
assert.equal(existsSync(join(ROOT, 'compute', 'smart-routing')), false)
|
||
|
||
const routed = []
|
||
for (const name of index.order) {
|
||
const file = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', `${name}.json`), 'utf8'))
|
||
for (const spec of file.specs) {
|
||
if (spec.routing) routed.push(spec)
|
||
}
|
||
}
|
||
assert.equal(routed.length, 60)
|
||
assert.equal(routed.every((spec) => spec.routing.reasoning.supportedModes.includes(spec.routing.reasoning.defaultMode)), true)
|
||
assert.equal(routed.every((spec) => Array.isArray(spec.spec.capabilities)), true)
|
||
})
|
||
|
||
it('智能路由三档必须完整唯一,且每档从自身开始回退', () => {
|
||
const indexPath = join(ROOT, 'compute', 'model-specs', '_index.json')
|
||
const index = JSON.parse(readFileSync(indexPath, 'utf8'))
|
||
const validate = compile('model-specs-index')
|
||
|
||
const duplicateTier = structuredClone(index)
|
||
duplicateTier.routingTiers[1] = structuredClone(duplicateTier.routingTiers[0])
|
||
assert.equal(validate(duplicateTier), false)
|
||
|
||
const wrongFirstFallback = structuredClone(index)
|
||
wrongFirstFallback.routingTiers[0].fallbackOrder = ['balanced', 'flagship', 'lightweight']
|
||
assert.equal(validate(wrongFirstFallback), false)
|
||
})
|
||
|
||
it('Provider 与 coding plan 的 _index.json 应通过 providers-index schema', () => {
|
||
const r1 = validateFile(join(ROOT, 'compute', 'providers', '_index.json'), validators)
|
||
const r2 = validateFile(join(ROOT, 'compute', 'coding-plans', '_index.json'), validators)
|
||
assert.equal(r1.ok, true, JSON.stringify(r1.errors, null, 2))
|
||
assert.equal(r2.ok, true, JSON.stringify(r2.errors, null, 2))
|
||
})
|
||
|
||
it('runtimes/recommended.json 应通过 runtime-recommended schema', () => {
|
||
const result = validateFile(join(ROOT, 'runtimes', 'recommended.json'), validators)
|
||
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
|
||
})
|
||
|
||
it('runtimes/versions-fallback.json 应通过 runtime-versions-fallback schema', () => {
|
||
const result = validateFile(join(ROOT, 'runtimes', 'versions-fallback.json'), validators)
|
||
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
|
||
})
|
||
|
||
it('WebSearch 服务端能力仅标记官方直连 Anthropic/OpenAI 模型', () => {
|
||
const anthropic = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'anthropic.json'), 'utf8'))
|
||
const openai = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'openai.json'), 'utf8'))
|
||
const openaiCodex = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'openai-codex.json'), 'utf8'))
|
||
|
||
const enabled = (provider) => provider.models
|
||
.filter((model) => model.extra?.serverSideWebSearch?.enabled === true)
|
||
.map((model) => model.modelName)
|
||
|
||
assert.deepEqual(enabled(anthropic), [
|
||
'claude-opus-5-5',
|
||
'claude-fable-5-1',
|
||
'claude-opus-5',
|
||
'claude-sonnet-5-5',
|
||
'claude-sonnet-5',
|
||
])
|
||
assert.deepEqual(enabled(openai), [
|
||
'gpt-6.1-sol',
|
||
'gpt-6-sol',
|
||
'gpt-6-luna',
|
||
'gpt-6-astra',
|
||
'gpt-5.5',
|
||
'gpt-5.5-pro',
|
||
'gpt-5.4',
|
||
'gpt-5.4-pro',
|
||
'gpt-5.4-mini',
|
||
'gpt-5.4-nano',
|
||
'gpt-5',
|
||
'gpt-4.1',
|
||
'gpt-4.1-mini',
|
||
'o4-mini',
|
||
])
|
||
assert.deepEqual(enabled(openaiCodex), [])
|
||
|
||
for (const model of anthropic.models.filter((item) => enabled(anthropic).includes(item.modelName))) {
|
||
assert.equal(model.extra.serverSideWebSearch.dialect, 'anthropic-messages')
|
||
assert.equal(model.extra.serverSideWebSearch.toolType, 'web_search_20260318')
|
||
assert.equal(model.extra.serverSideWebSearch.searchRequestPriceUsd, 0.01)
|
||
assert.equal('maxUses' in model.extra.serverSideWebSearch, false)
|
||
}
|
||
for (const model of openai.models.filter((item) => enabled(openai).includes(item.modelName))) {
|
||
assert.equal(model.extra.serverSideWebSearch.dialect, 'openai-responses')
|
||
assert.equal(model.extra.serverSideWebSearch.toolType, 'web_search')
|
||
assert.equal(model.extra.serverSideWebSearch.searchRequestPriceUsd, 0.01)
|
||
assert.equal('maxUses' in model.extra.serverSideWebSearch, false)
|
||
}
|
||
})
|
||
|
||
it('WebSearch API providers 使用显式密钥声明且默认关闭', () => {
|
||
const brave = JSON.parse(readFileSync(join(ROOT, 'api-providers', 'web-search', 'brave.json'), 'utf8'))
|
||
const doubao = JSON.parse(readFileSync(join(ROOT, 'api-providers', 'web-search', 'doubao-search-global.json'), 'utf8'))
|
||
const index = JSON.parse(readFileSync(join(ROOT, 'api-providers', 'web-search', '_index.json'), 'utf8'))
|
||
|
||
assert.equal(brave.enabled, false)
|
||
assert.equal(brave.endpoint, 'https://api.search.brave.com/res/v1/web/search')
|
||
assert.deepEqual(brave.auth, {
|
||
type: 'header',
|
||
headerName: 'X-Subscription-Token',
|
||
apiKeyRef: 'brave',
|
||
})
|
||
assert.equal(doubao.enabled, false)
|
||
assert.equal(doubao.priority, 5)
|
||
assert.equal(doubao.endpoint, 'https://open.feedcoopapi.com/search_api/global_search')
|
||
assert.deepEqual(doubao.auth, {
|
||
type: 'bearer',
|
||
apiKeyRef: 'doubao-search-global',
|
||
})
|
||
assert.deepEqual(doubao.request.bodyTemplate, {
|
||
Query: '${query}',
|
||
SearchType: 'web',
|
||
DocCount: '${maxResults}',
|
||
MaxSnippetLength: 500,
|
||
})
|
||
assert.equal(doubao.response.resultsPath, 'Result.Documents')
|
||
assert.deepEqual(doubao.response.item, {
|
||
titlePath: 'Title',
|
||
urlPath: 'Url',
|
||
snippetPath: 'Snippet.0.Text',
|
||
pageAgePath: 'DocumentInfo.PublishTime',
|
||
})
|
||
assert.deepEqual(doubao.response.successCondition, {
|
||
path: 'Result.ErrorCode',
|
||
equals: 0,
|
||
})
|
||
assert.deepEqual(index.order, ['tavily', 'brave', 'serper', 'doubao-search-global'])
|
||
assert.equal(
|
||
validateFile(join(ROOT, 'api-providers', 'web-search', 'brave.json'), validators).ok,
|
||
true,
|
||
)
|
||
assert.equal(
|
||
validateFile(join(ROOT, 'api-providers', 'web-search', 'doubao-search-global.json'), validators).ok,
|
||
true,
|
||
)
|
||
})
|
||
|
||
it('DeepSeek V4 Pro/Flash 应与官方的 1M/384K reasoning profile 一致', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'deepseek.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'deepseek.json'), 'utf8'))
|
||
const modelIds = ['deepseek-v4-pro', 'deepseek-v4-flash']
|
||
|
||
for (const modelId of modelIds) {
|
||
const model = provider.models.find((item) => item.modelName === modelId)
|
||
assert.ok(model, `provider 缺少 ${modelId}`)
|
||
assert.equal(model.contextWindow, 1000000)
|
||
assert.equal(model.maxOutputTokens, 384000)
|
||
assert.ok(model.capabilities.includes('reasoning'))
|
||
assert.ok(model.capabilities.includes('deep_thinking'))
|
||
assert.ok(model.serviceType.includes('reasoning'))
|
||
assert.equal(model.extra.supportsThinking, true)
|
||
assert.equal(model.extra.thinkingDefault, true)
|
||
assert.deepEqual(model.extra.reasoningEffort, ['high', 'max'])
|
||
|
||
const specs = specFile.specs.filter((item) => item.id === modelId)
|
||
assert.equal(specs.length, 1, `model-specs 中 ${modelId} 应且仅应有一条规格`)
|
||
for (const { spec } of specs) {
|
||
assert.equal(spec.contextWindow, 1000000)
|
||
assert.equal(spec.maxOutputTokens, 384000)
|
||
assert.equal(spec.supportsReasoning, true)
|
||
assert.ok(spec.capabilities.includes('reasoning'))
|
||
assert.ok(spec.capabilities.includes('deep_thinking'))
|
||
assert.ok(spec.serviceType.includes('reasoning'))
|
||
assert.equal(spec.extra.supportsThinking, true)
|
||
assert.equal(spec.extra.thinkingDefault, true)
|
||
assert.deepEqual(spec.extra.reasoningEffort, ['high', 'max'])
|
||
}
|
||
}
|
||
})
|
||
|
||
it('DeepSeek V4 Flash Vision Exp 应提供独立的视觉规格', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'deepseek.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'deepseek.json'), 'utf8'))
|
||
const modelId = 'deepseek-v4-flash-vision-exp'
|
||
const model = provider.models.find((item) => item.modelName === modelId)
|
||
|
||
assert.ok(provider.services.includes('vision'))
|
||
assert.ok(model, `provider 缺少 ${modelId}`)
|
||
assert.equal(model.contextWindow, 1000000)
|
||
assert.equal(model.maxOutputTokens, 384000)
|
||
assert.ok(model.capabilities.includes('vision'))
|
||
assert.ok(model.serviceType.includes('vision'))
|
||
assert.equal(model.extra.supportsThinking, true)
|
||
assert.equal(model.extra.thinkingDefault, true)
|
||
|
||
const specs = specFile.specs.filter((item) => item.id === modelId)
|
||
assert.equal(specs.length, 1, `model-specs 中 ${modelId} 应且仅应有一条规格`)
|
||
assert.deepEqual(specs[0].match.exact, [modelId])
|
||
assert.equal('patterns' in specs[0].match, false)
|
||
assert.equal(specs[0].family, modelId)
|
||
assert.equal(specs[0].spec.contextWindow, 1000000)
|
||
assert.equal(specs[0].spec.maxOutputTokens, 384000)
|
||
assert.equal(specs[0].spec.supportsReasoning, true)
|
||
assert.ok(specs[0].spec.capabilities.includes('vision'))
|
||
assert.ok(specs[0].spec.serviceType.includes('vision'))
|
||
|
||
const flash = specFile.specs.find((item) => item.id === 'deepseek-v4-flash')
|
||
assert.deepEqual(flash.match.exact, ['deepseek-v4-flash'])
|
||
assert.equal('patterns' in flash.match, false)
|
||
})
|
||
|
||
it('DeepSeek V4.1 Flash 应使用官方模型 ID、视觉能力和分时价格', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'deepseek.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'deepseek.json'), 'utf8'))
|
||
const serviceMap = JSON.parse(readFileSync(join(ROOT, 'compute', 'service-map.json'), 'utf8'))
|
||
const modelId = 'deepseek-flash'
|
||
const aliasId = 'deepseek-v4.1-flash'
|
||
const model = provider.models.find((item) => item.modelName === modelId)
|
||
const modelSpec = specFile.specs.find((item) => item.id === modelId)
|
||
|
||
assert.equal(serviceMap.reasoning.modelName, modelId)
|
||
assert.ok(model, `provider 缺少 ${modelId}`)
|
||
assert.deepEqual(model.serviceType, ['chat', 'reasoning', 'vision'])
|
||
assert.equal(model.contextWindow, 1000000)
|
||
assert.equal(model.maxOutputTokens, 384000)
|
||
assert.ok(model.capabilities.includes('vision'))
|
||
assert.equal(model.inputPrice, 1)
|
||
assert.equal(model.outputPrice, 4)
|
||
assert.equal(model.extra.cacheHitPrice, 0.02)
|
||
assert.equal(model.extra.concurrencyLimit, 2500)
|
||
assert.deepEqual(model.extra.reasoning, {
|
||
supportedEfforts: ['high', 'max'],
|
||
defaultEffort: 'high',
|
||
})
|
||
assert.deepEqual(model.extra.pricingTiers, [
|
||
{ condition: 'idle', inputPrice: 1, outputPrice: 4, cacheHitPrice: 0.02 },
|
||
{ condition: 'peak', inputPrice: 2, outputPrice: 8, cacheHitPrice: 0.04 },
|
||
])
|
||
|
||
assert.ok(modelSpec, `model-specs 缺少 ${modelId}`)
|
||
assert.deepEqual(modelSpec.match.exact, [modelId, aliasId])
|
||
assert.equal(
|
||
specFile.specs.filter((item) => item.match?.exact?.includes(aliasId)).length,
|
||
1,
|
||
)
|
||
assert.ok(modelSpec.match.exact.includes(aliasId))
|
||
assert.equal(modelSpec.spec.contextWindow, 1000000)
|
||
assert.equal(modelSpec.spec.maxOutputTokens, 384000)
|
||
assert.ok(modelSpec.spec.capabilities.includes('vision'))
|
||
assert.deepEqual(modelSpec.spec.serviceType, ['chat', 'reasoning', 'vision'])
|
||
assert.equal(modelSpec.spec.supportsReasoning, true)
|
||
assert.deepEqual(modelSpec.routing.reasoning.supportedModes, ['auto', 'high', 'max'])
|
||
})
|
||
|
||
it('MiMo V2.5 ASR 应有独立精确规格,避免回落到 MiMo V2.5 family', () => {
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'xiaomi.json'), 'utf8'))
|
||
const specs = specFile.specs.filter((item) => item.id === 'mimo-v2.5-asr')
|
||
|
||
assert.equal(specs.length, 1, 'mimo-v2.5-asr 应且仅应有一条规格')
|
||
assert.deepEqual(specs[0].match.exact, ['mimo-v2.5-asr'])
|
||
assert.equal(specs[0].spec.contextWindow, 8192)
|
||
assert.equal(specs[0].spec.maxOutputTokens, 2048)
|
||
assert.deepEqual(specs[0].spec.serviceType, ['asr'])
|
||
assert.ok(specs[0].spec.capabilities.includes('asr'))
|
||
assert.deepEqual(specs[0].spec.extra.modelOrigin, {
|
||
vendor: 'xiaomi',
|
||
canonicalModelId: 'mimo-v2.5-asr',
|
||
family: 'mimo-asr',
|
||
})
|
||
assert.deepEqual(specs[0].spec.extra.speech, {
|
||
protocol: 'xiaomi-mimo-chat-asr',
|
||
protocolVersion: '1',
|
||
modes: ['batch'],
|
||
inputFormats: ['wav', 'mp3'],
|
||
languageHints: true,
|
||
})
|
||
})
|
||
|
||
it('MiMo V2.5 TTS 应声明稳定协议与官方音色', () => {
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'xiaomi.json'), 'utf8'))
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'xiaomi.json'), 'utf8'))
|
||
const tts = specFile.specs.find((item) => item.id === 'mimo-v2.5-tts')
|
||
const providerTts = provider.models.find((item) => item.modelName === 'mimo-v2.5-tts')
|
||
const providerAsr = provider.models.find((item) => item.modelName === 'mimo-v2.5-asr')
|
||
|
||
assert.ok(tts, 'model-specs 中缺少 mimo-v2.5-tts')
|
||
assert.ok(providerTts, 'provider 中缺少 mimo-v2.5-tts')
|
||
assert.ok(providerAsr, 'provider 中缺少 mimo-v2.5-asr')
|
||
assert.ok(provider.services.includes('asr'))
|
||
assert.equal(tts.spec.contextWindow, 8192)
|
||
assert.equal(tts.spec.maxOutputTokens, 8192)
|
||
assert.deepEqual(tts.spec.extra.modelOrigin, {
|
||
vendor: 'xiaomi',
|
||
canonicalModelId: 'mimo-v2.5-tts',
|
||
family: 'mimo-tts',
|
||
})
|
||
assert.equal(tts.spec.extra.speech.protocol, 'xiaomi-mimo-chat-tts')
|
||
assert.equal(tts.spec.extra.speech.protocolVersion, '1')
|
||
assert.deepEqual(tts.spec.extra.speech.modes, ['batch', 'streaming'])
|
||
assert.deepEqual(tts.spec.extra.speech.outputFormats, ['wav', 'mp3', 'pcm16le'])
|
||
assert.deepEqual(tts.spec.extra.speech.sampleRates, [24000])
|
||
assert.equal(tts.spec.extra.speech.defaultVoice, 'mimo_default')
|
||
assert.deepEqual(
|
||
tts.spec.extra.speech.voices.map((voice) => voice.id),
|
||
['mimo_default', '冰糖', '茉莉', '苏打', '白桦', 'Mia', 'Chloe', 'Milo', 'Dean'],
|
||
)
|
||
assert.deepEqual(
|
||
providerTts.extra.voices.map((voice) => voice.id),
|
||
['mimo_default', '冰糖', '茉莉', '苏打', '白桦', 'Mia', 'Chloe', 'Milo', 'Dean'],
|
||
)
|
||
assert.deepEqual(providerTts.extra.audioFormats, ['wav', 'mp3', 'pcm16le'])
|
||
assert.equal(providerTts.extra.sampleRate, 24000)
|
||
})
|
||
|
||
it('OpenRouter 2026-07-21 水位线前的缺失模型应使用当前精确规格', () => {
|
||
const openai = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'openai.json'), 'utf8'))
|
||
const google = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'google.json'), 'utf8'))
|
||
const tencent = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'tencent.json'), 'utf8'))
|
||
const xai = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'xai.json'), 'utf8'))
|
||
|
||
for (const id of ['gpt-5.6-sol-pro', 'gpt-5.6-terra-pro', 'gpt-5.6-luna-pro']) {
|
||
const modelSpec = openai.specs.find((item) => item.id === id)
|
||
assert.ok(modelSpec, `model-specs/openai.json 缺少 ${id}`)
|
||
assert.equal(modelSpec.family, id)
|
||
assert.deepEqual(modelSpec.match.exact, [id])
|
||
assert.equal('patterns' in modelSpec.match, false)
|
||
assert.equal(modelSpec.spec.contextWindow, 1050000)
|
||
assert.equal(modelSpec.spec.maxOutputTokens, 128000)
|
||
assert.equal(modelSpec.spec.supportsReasoning, true)
|
||
}
|
||
|
||
const gemini = google.specs.find((item) => item.id === 'gemini-3.1-flash-lite-image')
|
||
assert.ok(gemini, 'model-specs/google.json 缺少 gemini-3.1-flash-lite-image')
|
||
assert.deepEqual(gemini.match.exact, ['gemini-3.1-flash-lite-image'])
|
||
assert.equal(gemini.spec.contextWindow, 65536)
|
||
assert.equal(gemini.spec.maxOutputTokens, 4096)
|
||
assert.ok(gemini.spec.capabilities.includes('image_generation'))
|
||
assert.ok(gemini.spec.capabilities.includes('image_editing'))
|
||
|
||
const hy3 = tencent.specs.find((item) => item.id === 'hy3')
|
||
assert.ok(hy3, 'model-specs/tencent.json 缺少 hy3')
|
||
assert.deepEqual(hy3.match.exact, ['hy3'])
|
||
assert.equal(hy3.spec.contextWindow, 262144)
|
||
assert.equal(hy3.spec.maxOutputTokens, 128000)
|
||
|
||
const grok = xai.specs.find((item) => item.id === 'grok-4.5')
|
||
assert.ok(grok, 'model-specs/xai.json 缺少 grok-4.5')
|
||
assert.deepEqual(grok.match.exact, ['grok-4.5'])
|
||
assert.equal(grok.spec.contextWindow, 500000)
|
||
assert.equal(grok.spec.maxOutputTokens, 450000)
|
||
assert.equal(grok.spec.extra.thinkingOnly, true)
|
||
|
||
const watermark = readFileSync(join(ROOT, '.sync-watermark'), 'utf8')
|
||
assert.match(watermark, /# 人读: 2026-07-21T03:37:38Z/)
|
||
assert.equal(Number(watermark.trim().split(/\s+/).at(-1)), 1784554658)
|
||
})
|
||
|
||
it('Ox Alpha 应提供与 OpenRouter 一致的多模态推理规格', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'openrouter.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'stealth.json'), 'utf8'))
|
||
const model = provider.models.find((item) => item.modelName === 'stealth/ox-alpha')
|
||
const modelSpec = specFile.specs.find((item) => item.id === 'ox-alpha')
|
||
|
||
assert.ok(model, 'OpenRouter provider 缺少 stealth/ox-alpha')
|
||
assert.ok(modelSpec, 'model-specs 缺少 ox-alpha')
|
||
|
||
for (const item of [model, modelSpec.spec]) {
|
||
assert.equal(item.contextWindow, 1048576)
|
||
assert.equal(item.maxOutputTokens, 131072)
|
||
assert.equal(item.defaultTemperature, 1)
|
||
assert.equal(item.defaultTopP, 0.95)
|
||
assert.ok(item.capabilities.includes('reasoning'))
|
||
assert.ok(item.capabilities.includes('vision'))
|
||
assert.ok(item.capabilities.includes('video_understanding'))
|
||
}
|
||
|
||
assert.deepEqual(model.extra.reasoning.supportedEfforts, ['low', 'high', 'max'])
|
||
assert.equal(model.extra.reasoning.defaultEffort, 'max')
|
||
assert.equal(model.extra.thinking.disableSupported, false)
|
||
assert.equal(model.inputPrice, 0)
|
||
assert.equal(model.outputPrice, 0)
|
||
assert.deepEqual(modelSpec.match.exact, ['ox-alpha', 'stealth/ox-alpha'])
|
||
assert.equal(modelSpec.spec.supportsReasoning, true)
|
||
assert.equal(modelSpec.spec.releasedAt, '2026-08-21')
|
||
})
|
||
|
||
it('MiMo V2.5 仅非 Pro 型号应声明多模态能力', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'xiaomi.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'xiaomi.json'), 'utf8'))
|
||
const specFor = (modelId) => specFile.specs.find((item) => item.id === modelId)
|
||
|
||
const mimoV25 = specFor('mimo-v2.5')
|
||
assert.ok(mimoV25, 'model-specs 缺少 mimo-v2.5')
|
||
assert.ok(mimoV25.spec.capabilities.includes('vision'))
|
||
assert.ok(mimoV25.spec.serviceType.includes('vision'))
|
||
|
||
for (const modelId of ['mimo-v2.5-pro', 'mimo-v2-pro']) {
|
||
const modelSpec = specFor(modelId)
|
||
assert.ok(modelSpec, `model-specs 缺少 ${modelId}`)
|
||
assert.equal(modelSpec.spec.capabilities.includes('vision'), false)
|
||
assert.equal(modelSpec.spec.serviceType.includes('vision'), false)
|
||
}
|
||
|
||
const pro = provider.models.find((item) => item.modelName === 'mimo-v2.5-pro')
|
||
assert.ok(pro, 'provider 缺少 mimo-v2.5-pro')
|
||
assert.equal(pro.capabilities.includes('vision'), false)
|
||
assert.equal(pro.serviceType.includes('vision'), false)
|
||
})
|
||
|
||
it('MiMo-X 内测模型应声明 1M 上下文与图片识别能力', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'xiaomi.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'xiaomi.json'), 'utf8'))
|
||
const modelIds = ['mimo-x-flash-preview', 'mimo-x-pro-preview']
|
||
|
||
assert.ok(provider.services.includes('vision'))
|
||
for (const modelId of modelIds) {
|
||
const model = provider.models.find((item) => item.modelName === modelId)
|
||
const modelSpec = specFile.specs.find((item) => item.id === modelId)
|
||
|
||
assert.ok(model, `provider 缺少 ${modelId}`)
|
||
assert.ok(modelSpec, `model-specs 缺少 ${modelId}`)
|
||
assert.deepEqual(modelSpec.match.exact, [modelId])
|
||
assert.equal(model.contextWindow, 1000000)
|
||
assert.equal(modelSpec.spec.contextWindow, 1000000)
|
||
assert.ok(model.capabilities.includes('vision'))
|
||
assert.ok(model.serviceType.includes('vision'))
|
||
assert.ok(modelSpec.spec.capabilities.includes('vision'))
|
||
assert.ok(modelSpec.spec.serviceType.includes('vision'))
|
||
}
|
||
})
|
||
|
||
it('MiMo V2.6 三款官方 API 模型应保持全模态规格与接入差异', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'xiaomi.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'xiaomi.json'), 'utf8'))
|
||
const cases = [
|
||
['mimo-v2.6-pro', 3, 6, 0.025, 'flagship'],
|
||
['mimo-v2.6-flash', 1, 2, 0.02, 'balanced'],
|
||
['mimo-v2.6-pro-ultraspeed', 30, 60, 0.25, null],
|
||
]
|
||
|
||
for (const [id, inputPrice, outputPrice, cachedInputPrice, tier] of cases) {
|
||
const models = provider.models.filter((item) => item.modelName === id)
|
||
const specs = specFile.specs.filter((item) => item.id === id)
|
||
assert.equal(models.length, 1, `Provider 缺少或重复 ${id}`)
|
||
assert.equal(specs.length, 1, `model-specs 缺少或重复 ${id}`)
|
||
const model = models[0]
|
||
const modelSpec = specs[0]
|
||
assert.deepEqual(modelSpec.match.exact, [id])
|
||
assert.equal(model.contextWindow, 1000000)
|
||
assert.equal(model.maxOutputTokens, 131072)
|
||
assert.equal(modelSpec.spec.contextWindow, model.contextWindow)
|
||
assert.equal(modelSpec.spec.maxOutputTokens, model.maxOutputTokens)
|
||
assert.equal(model.inputPrice, inputPrice)
|
||
assert.equal(model.outputPrice, outputPrice)
|
||
assert.equal(model.extra.cachedInputPrice, cachedInputPrice)
|
||
for (const capability of ['vision', 'audio_understanding', 'video_understanding', 'tool_use']) {
|
||
assert.ok(model.capabilities.includes(capability), `${id} 缺少 ${capability}`)
|
||
assert.ok(modelSpec.spec.capabilities.includes(capability), `${id} 规格缺少 ${capability}`)
|
||
}
|
||
assert.equal(modelSpec.spec.extra.thinkingDefault, true)
|
||
assert.equal(modelSpec.routing?.tier ?? null, tier)
|
||
}
|
||
|
||
const previous = provider.models.find((item) => item.modelName === 'mimo-v2.5-pro')
|
||
assert.ok(previous, '尚未到官方下线日,应保留 mimo-v2.5-pro')
|
||
assert.equal(previous.maxOutputTokens, 131072)
|
||
})
|
||
|
||
it('Token Plan 应使用 Qwen3.8 正式版 ID 并淘汰 Preview 预置', () => {
|
||
const tokenPlan = JSON.parse(readFileSync(join(ROOT, 'compute', 'coding-plans', 'dashscope-token-plan.json'), 'utf8'))
|
||
const apiProvider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'dashscope.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'qwen.json'), 'utf8'))
|
||
assert.equal(tokenPlan.models.some((item) => item.modelName === 'qwen3.8-max-preview'), false)
|
||
assert.equal(apiProvider.models.some((item) => item.modelName === 'qwen3.8-max-preview'), false)
|
||
assert.ok(tokenPlan.tombstones.includes('qwen3.8-max-preview'))
|
||
assert.ok(apiProvider.tombstones.includes('qwen3.8-max-preview'))
|
||
for (const modelId of ['qwen3.8-max', 'qwen3.8-flash']) {
|
||
const model = tokenPlan.models.find((item) => item.modelName === modelId)
|
||
const modelSpec = specFile.specs.find((item) => item.id === modelId)
|
||
assert.ok(model, `Token Plan 缺少 ${modelId}`)
|
||
assert.ok(modelSpec, `model-specs 缺少 ${modelId}`)
|
||
assert.equal(model.contextWindow, 1000000)
|
||
assert.equal(model.maxOutputTokens, 131072)
|
||
assert.ok(model.capabilities.includes('vision'))
|
||
assert.deepEqual(model.extra.reasoning.supportedEfforts, ['low', 'medium', 'xhigh'])
|
||
assert.equal(model.extra.reasoning.defaultEffort, 'xhigh')
|
||
assert.equal(model.extra.thinkingOnly, undefined)
|
||
assert.equal(model.extra.preserveThinkingDefault, true)
|
||
assert.equal(modelSpec.spec.maxOutputTokens, 131072)
|
||
const apiModel = apiProvider.models.find((item) => item.modelName === modelId)
|
||
assert.deepEqual(apiModel.extra.reasoning.supportedEfforts, ['low', 'medium', 'xhigh'])
|
||
assert.equal(apiModel.extra.preserveThinkingDefault, true)
|
||
}
|
||
})
|
||
|
||
it('新增主流模型应在官方 Provider 与共享规格中保持 ID、窗口和推理配置一致', () => {
|
||
const cases = [
|
||
['openai', 'openai', 'gpt-6.1-sol', 1050000, 128000, 2, 10, 'medium'],
|
||
['anthropic', 'anthropic', 'claude-sonnet-5-5', 1000000, 128000, 2, 10, 'high'],
|
||
['openai', 'openai', 'gpt-6-sol', 1050000, 128000, 2, 10, 'medium'],
|
||
['openai', 'openai', 'gpt-6-luna', 1050000, 128000, 0.1, 0.5, 'medium'],
|
||
['anthropic', 'anthropic', 'claude-opus-5-5', 1000000, 128000, 4, 20, 'medium'],
|
||
['google', 'google', 'gemini-3.8-flash', 1048576, 65536, 0.75, 3.75, null],
|
||
['google', 'google', 'gemini-3.7-flash', 1048576, 65536, 0.75, 3.75, null],
|
||
['google', 'google', 'gemini-3.5-flash-lite', 1048576, 65536, 0.3, 2.5, null],
|
||
['dashscope', 'qwen', 'qwen3.8-max', 1000000, 131072, 12, 36, null],
|
||
['dashscope', 'qwen', 'qwen3.8-flash', 1000000, 131072, 0.8, 2.7, null],
|
||
['xai', 'xai', 'grok-4.7', 500000, undefined, 2, 6, 'high'],
|
||
]
|
||
|
||
for (const [providerName, specName, id, contextWindow, maxOutputTokens, inputPrice, outputPrice, defaultEffort] of cases) {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', `${providerName}.json`), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', `${specName}.json`), 'utf8'))
|
||
const models = provider.models.filter((item) => item.modelName === id)
|
||
const specs = specFile.specs.filter((item) => item.id === id)
|
||
assert.equal(models.length, 1, `${providerName} 缺少或重复 ${id}`)
|
||
assert.equal(specs.length, 1, `${specName} 缺少或重复 ${id}`)
|
||
assert.equal(models[0].contextWindow, contextWindow)
|
||
assert.equal(specs[0].spec.contextWindow, contextWindow)
|
||
assert.equal(models[0].maxOutputTokens, maxOutputTokens)
|
||
assert.equal(specs[0].spec.maxOutputTokens, maxOutputTokens)
|
||
assert.equal(models[0].inputPrice, inputPrice)
|
||
assert.equal(models[0].outputPrice, outputPrice)
|
||
assert.equal(specs[0].routing.reasoning.supportedModes.includes(specs[0].routing.reasoning.defaultMode), true)
|
||
if (defaultEffort) assert.equal(models[0].extra.reasoning.defaultEffort, defaultEffort)
|
||
}
|
||
|
||
const anthropic = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'anthropic.json'), 'utf8'))
|
||
const opus = anthropic.models.find((item) => item.modelName === 'claude-opus-5-5')
|
||
assert.equal(opus.extra.adaptiveThinking, true)
|
||
assert.equal(opus.extra.thinkingOnly, true)
|
||
assert.equal(opus.extra.forcedToolChoiceUnsupported, true)
|
||
assert.equal(opus.extra.cachePricing.read, 0.2)
|
||
|
||
const xai = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'xai.json'), 'utf8'))
|
||
assert.deepEqual(xai.models.find((item) => item.modelName === 'grok-4.7').extra.reasoning.supportedEfforts, ['low', 'medium', 'high', 'xhigh'])
|
||
|
||
const codex = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'openai-codex.json'), 'utf8'))
|
||
for (const id of ['gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna']) {
|
||
const model = codex.models.find((item) => item.modelName === id)
|
||
assert.ok(model, `Codex 订阅 Provider 缺少 ${id}`)
|
||
assert.equal(model.source, 'preset')
|
||
assert.equal(model.extra.reasoning.defaultEffort, 'medium')
|
||
}
|
||
})
|
||
|
||
it('GPT-6.1 与 Sonnet 5.5 的协议约束在 API、订阅与共享规格中保持一致', () => {
|
||
const read = (dir, name) => JSON.parse(readFileSync(join(ROOT, 'compute', dir, `${name}.json`), 'utf8'))
|
||
const efforts = ['low', 'medium', 'high', 'xhigh', 'max']
|
||
for (const provider of ['openai', 'openai-codex']) {
|
||
const model = read('providers', provider).models.find((item) => item.modelName === 'gpt-6.1-sol')
|
||
assert.deepEqual(model.extra.reasoning.supportedEfforts, efforts)
|
||
if (provider === 'openai') {
|
||
assert.equal(model.extra.responsesOnly, true)
|
||
assert.equal(model.extra.cachedInputPrice, 0.1)
|
||
}
|
||
}
|
||
const gpt = read('model-specs', 'openai').specs.find((item) => item.id === 'gpt-6.1-sol')
|
||
assert.deepEqual(gpt.match.exact, ['gpt-6.1-sol'])
|
||
assert.deepEqual(gpt.routing.reasoning.supportedModes, ['auto', ...efforts])
|
||
for (const provider of ['anthropic', 'anthropic-claude']) {
|
||
const model = read('providers', provider).models.find((item) => item.modelName === 'claude-sonnet-5-5')
|
||
assert.equal(model.extra.reasoning.defaultEffort, 'high')
|
||
assert.equal(model.extra.forcedToolChoiceUnsupported, true)
|
||
assert.equal(model.extra.samplingParametersDeprecated, true)
|
||
assert.equal(model.extra.thinkingOnly, undefined)
|
||
if (provider === 'anthropic') assert.equal(model.extra.promptCacheMinTokens, 512)
|
||
}
|
||
const sonnet = read('model-specs', 'anthropic').specs.find((item) => item.id === 'claude-sonnet-5-5')
|
||
assert.deepEqual(sonnet.match.exact, ['claude-sonnet-5-5'])
|
||
assert.equal(sonnet.spec.extra.forcedToolChoiceUnsupported, true)
|
||
assert.equal(sonnet.spec.extra.thinkingOnly, undefined)
|
||
assert.equal(sonnet.routing.reasoning.defaultMode, 'high')
|
||
})
|
||
|
||
it('Codex 订阅接入面不声明 none 档:ChatGPT 后端不接受关闭思考', () => {
|
||
// OpenAI 下发给 Codex 的模型元数据(models_cache.json#supported_reasoning_levels)里,
|
||
// 该接入面没有任何模型支持 none。声明了 none,客户端会在 AI 审批等关闭思考的调用上发
|
||
// reasoning.effort=none,上游 400,审批拿不到建议即自动拒绝(2026-09-26 gpt-6-sol 真机复现)。
|
||
// 不声明时客户端对这类模型不发关闭参数,改用上游默认档位。
|
||
const codex = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'openai-codex.json'), 'utf8'))
|
||
for (const model of codex.models) {
|
||
const efforts = model.extra?.reasoning?.supportedEfforts ?? []
|
||
assert.equal(efforts.includes('none'), false, `${model.modelName} 不应在 Codex 订阅接入面声明 none`)
|
||
}
|
||
})
|
||
|
||
it('GLM-5.3-FlashX 仅加入共享规格,避免假定中国区智谱 API 已提供该 ID', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'zhipu.json'), 'utf8'))
|
||
const specs = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'zhipu.json'), 'utf8')).specs
|
||
assert.equal(provider.models.some((item) => item.modelName === 'glm-5.3-flashx'), false)
|
||
const spec = specs.find((item) => item.id === 'glm-5.3-flashx')
|
||
assert.ok(spec)
|
||
assert.deepEqual(spec.match.exact, ['glm-5.3-flashx', 'z-ai/glm-5.3-flashx'])
|
||
assert.equal(spec.spec.contextWindow, 1000000)
|
||
assert.equal(spec.spec.maxOutputTokens, 128000)
|
||
assert.ok(spec.spec.capabilities.includes('vision'))
|
||
})
|
||
|
||
it('GPT Image 2.5 双型号应使用图像生成接入,并与 GPT Image 2 精确区分', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'openai.json'), 'utf8'))
|
||
const specs = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'openai.json'), 'utf8')).specs
|
||
for (const id of ['gpt-image-2.5-sunburst', 'gpt-image-2.5-flare']) {
|
||
const model = provider.models.find((item) => item.modelName === id)
|
||
const spec = specs.find((item) => item.id === id)
|
||
assert.ok(model, `OpenAI Provider 缺少 ${id}`)
|
||
assert.ok(spec, `model-specs 缺少 ${id}`)
|
||
assert.deepEqual(model.serviceType, ['image_gen'])
|
||
assert.deepEqual(spec.match.exact, [id])
|
||
assert.equal(model.inputPrice, 5)
|
||
assert.equal(model.extra.imageInputPrice, 8)
|
||
assert.equal(model.outputPrice, 30)
|
||
}
|
||
const older = specs.find((item) => item.id === 'gpt-image-2')
|
||
assert.deepEqual(older.match.exact, ['gpt-image-2'])
|
||
assert.equal('patterns' in older.match, false)
|
||
})
|
||
|
||
it('Qwen3.8 Flash 应提供完整的多模态推理规格', () => {
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'qwen.json'), 'utf8'))
|
||
const modelId = 'qwen3.8-flash'
|
||
const specs = specFile.specs.filter((item) => item.id === modelId)
|
||
|
||
assert.equal(specs.length, 1, `model-specs 中 ${modelId} 应且仅应有一条规格`)
|
||
const modelSpec = specs[0]
|
||
assert.deepEqual(modelSpec.match.exact, [modelId])
|
||
assert.equal('patterns' in modelSpec.match, false)
|
||
assert.equal(modelSpec.family, modelId)
|
||
assert.equal(modelSpec.spec.contextWindow, 1000000)
|
||
assert.equal(modelSpec.spec.maxOutputTokens, 131072)
|
||
assert.equal(modelSpec.spec.supportsReasoning, true)
|
||
assert.equal(modelSpec.spec.releasedAt, '2026-08-26')
|
||
assert.ok(modelSpec.spec.capabilities.includes('vision'))
|
||
assert.ok(modelSpec.spec.capabilities.includes('video_understanding'))
|
||
assert.ok(modelSpec.spec.capabilities.includes('fast'))
|
||
assert.deepEqual(modelSpec.spec.serviceType, ['chat', 'reasoning', 'vision'])
|
||
assert.equal(modelSpec.spec.extra.thinkingMaxTokens, 262144)
|
||
assert.equal(modelSpec.routing.tier, 'lightweight')
|
||
assert.equal(modelSpec.routing.reasoning.defaultMode, 'xhigh')
|
||
})
|
||
|
||
it('GLM-5.3-Flash 应在 Coding Plan 中提供强制思考的多模态接入', () => {
|
||
const codingPlan = JSON.parse(readFileSync(join(ROOT, 'compute', 'coding-plans', 'zhipu-coding.json'), 'utf8'))
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'zhipu.json'), 'utf8'))
|
||
const modelId = 'glm-5.3-flash'
|
||
const model = codingPlan.models.find((item) => item.modelName === modelId)
|
||
const modelSpec = specFile.specs.find((item) => item.id === modelId)
|
||
|
||
assert.ok(model, `智谱 Coding Plan 缺少 ${modelId}`)
|
||
assert.equal(model.contextWindow, 1048576)
|
||
assert.equal(model.maxOutputTokens, 131072)
|
||
assert.ok(model.capabilities.includes('vision'))
|
||
assert.ok(model.capabilities.includes('video_understanding'))
|
||
assert.deepEqual(model.serviceType, ['chat', 'reasoning', 'vision'])
|
||
assert.equal(model.extra.thinkingOnly, true)
|
||
assert.deepEqual(model.extra.reasoning.supportedEfforts, ['low', 'high', 'max'])
|
||
assert.equal(model.extra.reasoning.defaultEffort, 'max')
|
||
|
||
assert.ok(modelSpec, `model-specs 缺少 ${modelId}`)
|
||
assert.equal(modelSpec.spec.extra.thinkingOnly, true)
|
||
assert.deepEqual(modelSpec.routing.reasoning.supportedModes, ['auto', 'low', 'high', 'max'])
|
||
})
|
||
|
||
it('基础模型与后缀版本应使用独立精确规格,避免互相误匹配', () => {
|
||
const qwen = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'qwen.json'), 'utf8'))
|
||
const deepseek = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'deepseek.json'), 'utf8'))
|
||
const specFor = (file, id) => file.specs.find((item) => item.id === id)
|
||
|
||
const qwenBase = specFor(qwen, 'qwen3.8-max')
|
||
const qwenPreview = specFor(qwen, 'qwen3.8-max-preview')
|
||
const deepseekBase = specFor(deepseek, 'deepseek-v4-pro')
|
||
const deepseek0813 = specFor(deepseek, 'deepseek-v4-pro-0813')
|
||
const deepseekFlash = specFor(deepseek, 'deepseek-v4-flash')
|
||
const deepseekFlash0731 = specFor(deepseek, 'deepseek-v4-flash-0731')
|
||
|
||
for (const [id, modelSpec] of [
|
||
['qwen3.8-max', qwenBase],
|
||
['qwen3.8-max-preview', qwenPreview],
|
||
['deepseek-v4-pro', deepseekBase],
|
||
['deepseek-v4-pro-0813', deepseek0813],
|
||
['deepseek-v4-flash', deepseekFlash],
|
||
['deepseek-v4-flash-0731', deepseekFlash0731],
|
||
]) {
|
||
assert.ok(modelSpec, `model-specs 缺少 ${id}`)
|
||
assert.deepEqual(modelSpec.match.exact, [id])
|
||
assert.equal(modelSpec.family, id)
|
||
}
|
||
|
||
assert.equal('patterns' in deepseekBase.match, false)
|
||
assert.equal('patterns' in deepseek0813.match, false)
|
||
assert.equal('patterns' in deepseekFlash.match, false)
|
||
assert.equal('patterns' in deepseekFlash0731.match, false)
|
||
assert.equal(qwenBase.spec.contextWindow, qwenPreview.spec.contextWindow)
|
||
assert.equal(qwenBase.spec.maxOutputTokens, 131072)
|
||
assert.equal(qwenPreview.spec.maxOutputTokens, 65536)
|
||
assert.equal(deepseekBase.spec.contextWindow, deepseek0813.spec.contextWindow)
|
||
assert.equal(deepseekBase.spec.maxOutputTokens, deepseek0813.spec.maxOutputTokens)
|
||
assert.equal(deepseekFlash.spec.contextWindow, deepseekFlash0731.spec.contextWindow)
|
||
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)(?:$|-)|^claude-opus-5-5(?:$|-)/
|
||
// 与客户端匹配器同口径:小写、去 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,并标记始终开启思考的模型', () => {
|
||
const adaptiveSpecIds = allSpecs.map((item) => item.id).filter((id) => adaptiveGeneration.test(normalizeId(id)))
|
||
for (const id of ['claude-fable-5-1', 'claude-opus-5-5', '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('拒绝采样参数 / 强制工具选择的 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(?:$|-)|^claude-(?:opus|sonnet)-5-5(?:$|-)/
|
||
|
||
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'anthropic.json'), 'utf8'))
|
||
for (const id of ['claude-fable-5-1', 'claude-opus-5-5', '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',
|
||
'anthropic-claude': 'USD',
|
||
baichuan: 'CNY',
|
||
baidu: 'CNY',
|
||
cohere: 'USD',
|
||
dashscope: 'CNY',
|
||
deepseek: 'CNY',
|
||
'github-copilot': 'USD',
|
||
google: 'USD',
|
||
kling: 'CNY',
|
||
'local-whisper': 'USD',
|
||
minimax: 'CNY',
|
||
mistral: 'USD',
|
||
moonshot: 'CNY',
|
||
ollama: 'USD',
|
||
openai: 'USD',
|
||
'openai-codex': 'USD',
|
||
openrouter: 'USD',
|
||
perplexity: 'USD',
|
||
siliconflow: 'CNY',
|
||
stability: 'USD',
|
||
tencent: 'CNY',
|
||
volcengine: 'CNY',
|
||
xai: 'USD',
|
||
xiaomi: 'CNY',
|
||
xunfei: 'CNY',
|
||
zhipu: 'CNY',
|
||
'zhipu-embedding': 'CNY',
|
||
}
|
||
const dir = join(ROOT, 'compute', 'providers')
|
||
const actualProviders = []
|
||
|
||
for (const file of readdirSync(dir)) {
|
||
if (!file.endsWith('.json') || file === '_index.json') continue
|
||
const data = JSON.parse(readFileSync(join(dir, file), 'utf8'))
|
||
actualProviders.push(data.provider)
|
||
assert.ok(
|
||
Object.hasOwn(expectedCurrencies, data.provider),
|
||
`未给 Provider ${data.provider} 声明计价归属`,
|
||
)
|
||
assert.equal(
|
||
data.priceCurrency,
|
||
expectedCurrencies[data.provider],
|
||
`${data.label} 应按供应商归属使用 ${expectedCurrencies[data.provider]} 计价`,
|
||
)
|
||
}
|
||
|
||
assert.deepEqual(actualProviders.sort(), Object.keys(expectedCurrencies).sort())
|
||
})
|
||
|
||
it('MiniMax 应使用国内开放平台人民币价', () => {
|
||
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'minimax.json'), 'utf8'))
|
||
const model = (modelName) => provider.models.find((item) => item.modelName === modelName)
|
||
|
||
assert.equal(provider.priceCurrency, 'CNY')
|
||
assert.deepEqual(
|
||
[model('MiniMax-M3').inputPrice, model('MiniMax-M3').outputPrice],
|
||
[2.1, 8.4],
|
||
)
|
||
assert.deepEqual(
|
||
[model('MiniMax-M2.7').inputPrice, model('MiniMax-M2.7').outputPrice],
|
||
[2.1, 8.4],
|
||
)
|
||
assert.deepEqual(
|
||
[model('MiniMax-M2.7-highspeed').inputPrice, model('MiniMax-M2.7-highspeed').outputPrice],
|
||
[4.2, 16.8],
|
||
)
|
||
assert.equal(model('image-01').extra.pricePerImage, 0.025)
|
||
assert.equal(model('speech-2.8-hd').extra.pricePerMillionCharacters, 350)
|
||
assert.equal(model('speech-2.8-turbo').extra.pricePerMillionCharacters, 200)
|
||
assert.equal(model('music-2.6').extra.pricePerSongUpToFiveMinutes, 1)
|
||
})
|
||
})
|
||
|
||
describe('model-specs 智能路由 schema 反例', () => {
|
||
const validate = compile('model-spec')
|
||
|
||
it('拒绝未声明的 routing 字段,避免策略静默分叉', () => {
|
||
const data = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'openai.json'), 'utf8'))
|
||
data.specs.find((spec) => spec.routing).routing.unknownRoutingPolicy = true
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝默认 reasoning 不在模型支持集合中的策略', () => {
|
||
const data = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'openai.json'), 'utf8'))
|
||
data.specs.find((spec) => spec.routing).routing.reasoning.defaultMode = 'max'
|
||
data.specs.find((spec) => spec.routing).routing.reasoning.supportedModes = ['auto']
|
||
assert.equal(validate(data), false)
|
||
})
|
||
})
|
||
|
||
// ==================== Runtime 清单反例 ====================
|
||
|
||
describe('runtime-recommended schema 反例', () => {
|
||
const validate = compile('runtime-recommended')
|
||
|
||
function makeValidManifest() {
|
||
return JSON.parse(readFileSync(join(ROOT, 'runtimes', 'recommended.json'), 'utf8'))
|
||
}
|
||
|
||
it('拒绝缺少平台归档(archives 六平台必填)', () => {
|
||
const data = makeValidManifest()
|
||
delete data.node.archives['win32-arm64']
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝非法 sha256(长度/字符集不符)', () => {
|
||
const data = makeValidManifest()
|
||
data.python.sha256['darwin-arm64'] = 'not-a-sha'
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝未知字段(additionalProperties: false 保护老客户端)', () => {
|
||
const data = makeValidManifest()
|
||
data.unknownField = true
|
||
assert.equal(validate(data), false)
|
||
})
|
||
})
|
||
|
||
// ==================== Provider schema 反例 ====================
|
||
|
||
describe('provider schema 反例(防 PR #1 重演)', () => {
|
||
const validate = compile('provider')
|
||
|
||
function makeValidProvider() {
|
||
return {
|
||
id: 'provider-test-001',
|
||
provider: 'test',
|
||
label: 'Test',
|
||
baseUrl: 'https://api.test.com',
|
||
apiKeyRef: 'test',
|
||
apiKeyVerified: false,
|
||
enabled: false,
|
||
status: 'unconfigured',
|
||
services: ['chat'],
|
||
models: [
|
||
{
|
||
modelName: 'm1',
|
||
displayName: 'M1',
|
||
serviceType: ['chat'],
|
||
capabilities: ['chat'],
|
||
},
|
||
],
|
||
}
|
||
}
|
||
|
||
it('合法 minimal provider 通过', () => {
|
||
assert.equal(validate(makeValidProvider()), true, JSON.stringify(validate.errors))
|
||
})
|
||
|
||
it('拒绝 defaultTemperature: null(PR #1 死锁元凶)', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].defaultTemperature = null
|
||
assert.equal(validate(data), false)
|
||
assert.ok(
|
||
validate.errors.some((e) => e.instancePath.endsWith('/defaultTemperature')),
|
||
`应有 defaultTemperature 错误,实际:${JSON.stringify(validate.errors)}`,
|
||
)
|
||
})
|
||
|
||
it('拒绝 defaultTopP: null', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].defaultTopP = null
|
||
assert.equal(validate(data), false)
|
||
assert.ok(validate.errors.some((e) => e.instancePath.endsWith('/defaultTopP')))
|
||
})
|
||
|
||
it('拒绝 defaultTemperature: "0.7"(string)', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].defaultTemperature = '0.7'
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('接受省略 defaultTemperature/defaultTopP(reasoning 模型推荐做法)', () => {
|
||
const data = makeValidProvider()
|
||
// 不设这两个字段
|
||
assert.equal(validate(data), true, JSON.stringify(validate.errors))
|
||
})
|
||
|
||
it('接受 extra.reasoning 能力矩阵,同时保留 extra 其他扩展字段', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].extra = {
|
||
reasoning: {
|
||
supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
||
defaultEffort: 'medium',
|
||
},
|
||
providerSpecificFlag: true,
|
||
}
|
||
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 的 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 = {
|
||
thinkingRoundTrip: {
|
||
enabled: true,
|
||
protocol: 'anthropic-messages',
|
||
allowEmptySignature: false,
|
||
scope: 'active-tool-turn',
|
||
},
|
||
}
|
||
assert.equal(validate(valid), true, JSON.stringify(validate.errors))
|
||
|
||
for (const brokenCapability of [
|
||
{ enabled: true, protocol: 'anthropic-messages', scope: 'active-tool-turn' },
|
||
{ enabled: true, protocol: 'openai-completions', allowEmptySignature: false, scope: 'active-tool-turn' },
|
||
{ enabled: true, protocol: 'anthropic-messages', allowEmptySignature: false, scope: 'all-history' },
|
||
{ enabled: true, protocol: 'anthropic-messages', allowEmptySignature: false, scope: 'active-tool-turn', completed: true },
|
||
]) {
|
||
const invalid = makeValidProvider()
|
||
invalid.models[0].extra = { thinkingRoundTrip: brokenCapability }
|
||
assert.equal(validate(invalid), false)
|
||
}
|
||
})
|
||
|
||
it('校验 Provider model 的工具轮 thinking 校验口径', () => {
|
||
for (const value of ['requires-thinking', 'accepts-thinkless']) {
|
||
const valid = makeValidProvider()
|
||
valid.models[0].extra = {
|
||
thinkingRoundTrip: {
|
||
enabled: true,
|
||
protocol: 'anthropic-messages',
|
||
allowEmptySignature: false,
|
||
scope: 'active-tool-turn',
|
||
},
|
||
thinkingToolTurnValidation: value,
|
||
}
|
||
assert.equal(validate(valid), true, JSON.stringify(validate.errors))
|
||
}
|
||
|
||
for (const broken of ['accept', true, null]) {
|
||
const invalid = makeValidProvider()
|
||
invalid.models[0].extra = { thinkingToolTurnValidation: broken }
|
||
assert.equal(validate(invalid), false)
|
||
}
|
||
})
|
||
|
||
it('拒绝 Ultra 与重复 reasoning effort', () => {
|
||
const ultra = makeValidProvider()
|
||
ultra.models[0].extra = {
|
||
reasoning: { supportedEfforts: ['high', 'ultra'], defaultEffort: 'high' },
|
||
}
|
||
assert.equal(validate(ultra), false)
|
||
|
||
const duplicate = makeValidProvider()
|
||
duplicate.models[0].extra = {
|
||
reasoning: { supportedEfforts: ['high', 'high'], defaultEffort: 'high' },
|
||
}
|
||
assert.equal(validate(duplicate), false)
|
||
})
|
||
|
||
it('拒绝不在 supportedEfforts 中的 defaultEffort', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].extra = {
|
||
reasoning: { supportedEfforts: ['low'], defaultEffort: 'high' },
|
||
}
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('接受 defaultTemperature: 0.7(合法 number)', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].defaultTemperature = 0.7
|
||
data.models[0].defaultTopP = 0.95
|
||
assert.equal(validate(data), true, JSON.stringify(validate.errors))
|
||
})
|
||
|
||
it('拒绝 defaultTemperature 超出 [0, 2] 范围', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].defaultTemperature = 3.0
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝 model 顶层未知字段(additionalProperties: false 保护老客户端)', () => {
|
||
const data = makeValidProvider()
|
||
data.models[0].brandNewField = 'oops'
|
||
assert.equal(validate(data), false)
|
||
assert.ok(
|
||
validate.errors.some((e) => e.keyword === 'additionalProperties'),
|
||
`应有 additionalProperties 错误,实际:${JSON.stringify(validate.errors)}`,
|
||
)
|
||
})
|
||
|
||
it('拒绝 provider 顶层未知字段', () => {
|
||
const data = makeValidProvider()
|
||
data.unknownTopLevelField = 'oops'
|
||
assert.equal(validate(data), false)
|
||
assert.ok(validate.errors.some((e) => e.keyword === 'additionalProperties'))
|
||
})
|
||
|
||
it('拒绝缺少必填字段(如 model 缺 modelName)', () => {
|
||
const data = makeValidProvider()
|
||
delete data.models[0].modelName
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝非法 status enum', () => {
|
||
const data = makeValidProvider()
|
||
data.status = 'wrong-status'
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝非法 priceCurrency enum(仅允许 USD/CNY)', () => {
|
||
const data = makeValidProvider()
|
||
data.priceCurrency = 'EUR'
|
||
assert.equal(validate(data), false)
|
||
})
|
||
})
|
||
|
||
describe('model-spec schema 接入面边界', () => {
|
||
const validate = compile('model-spec')
|
||
|
||
it('拒绝在模型内在规格中声明 reasoning effort', () => {
|
||
const data = {
|
||
specs: [{
|
||
id: 'gpt-test',
|
||
spec: {
|
||
extra: {
|
||
reasoning: {
|
||
supportedEfforts: ['low', 'high'],
|
||
defaultEffort: 'low',
|
||
},
|
||
},
|
||
},
|
||
}],
|
||
}
|
||
assert.equal(validate(data), false)
|
||
})
|
||
|
||
it('拒绝在模型内在规格中声明接入面的 thinking 回放能力与工具轮校验口径', () => {
|
||
for (const extra of [
|
||
{
|
||
thinkingRoundTrip: {
|
||
enabled: true,
|
||
protocol: 'anthropic-messages',
|
||
allowEmptySignature: false,
|
||
scope: 'active-tool-turn',
|
||
},
|
||
},
|
||
{ thinkingToolTurnValidation: 'accepts-thinkless' },
|
||
]) {
|
||
assert.equal(validate({ specs: [{ id: 'claude-test', spec: { extra } }] }), false)
|
||
}
|
||
})
|
||
|
||
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('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 } } }],
|
||
}
|
||
assert.equal(validate(data), true, JSON.stringify(validate.errors))
|
||
})
|
||
|
||
it('接受 extra.modelOrigin/speech,同时保持其他扩展开放', () => {
|
||
const data = {
|
||
specs: [{
|
||
id: 'mimo-v2.5-asr',
|
||
spec: {
|
||
extra: {
|
||
modelOrigin: {
|
||
vendor: 'xiaomi',
|
||
canonicalModelId: 'mimo-v2.5-asr',
|
||
family: 'mimo-asr',
|
||
},
|
||
speech: {
|
||
protocol: 'xiaomi-mimo-chat-asr',
|
||
protocolVersion: '1',
|
||
modes: ['batch', 'streaming'],
|
||
inputFormats: ['wav', 'mp3'],
|
||
languageHints: true,
|
||
},
|
||
intrinsicBudgetHint: 8192,
|
||
},
|
||
},
|
||
}],
|
||
}
|
||
assert.equal(validate(data), true, JSON.stringify(validate.errors))
|
||
})
|
||
|
||
it('严格拒绝非法 speech 二级结构和字段越界', () => {
|
||
const invalidProfile = {
|
||
specs: [{
|
||
id: 'bad-asr',
|
||
spec: {
|
||
extra: {
|
||
speech: {
|
||
protocol: 'xiaomi-mimo-chat-asr',
|
||
protocolVersion: '1',
|
||
modes: ['batch', 'batch'],
|
||
},
|
||
},
|
||
},
|
||
}],
|
||
}
|
||
assert.equal(validate(invalidProfile), false)
|
||
|
||
const misplaced = {
|
||
specs: [{
|
||
id: 'bad-asr',
|
||
spec: {
|
||
speech: {
|
||
protocol: 'xiaomi-mimo-chat-asr',
|
||
protocolVersion: '1',
|
||
modes: ['batch'],
|
||
},
|
||
},
|
||
}],
|
||
}
|
||
assert.equal(validate(misplaced), false)
|
||
})
|
||
})
|
||
|
||
// ==================== Manifest schema ====================
|
||
|
||
describe('manifest schema', () => {
|
||
const validate = compile('manifest')
|
||
|
||
it('合法 manifest 通过', () => {
|
||
assert.equal(
|
||
validate({
|
||
version: '1.0.0',
|
||
presetDataVersion: 30,
|
||
updatedAt: '2026-04-25',
|
||
description: 'test',
|
||
}),
|
||
true,
|
||
)
|
||
})
|
||
|
||
it('拒绝 presetDataVersion 为 string', () => {
|
||
assert.equal(
|
||
validate({
|
||
version: '1.0.0',
|
||
presetDataVersion: '30',
|
||
updatedAt: '2026-04-25',
|
||
}),
|
||
false,
|
||
)
|
||
})
|
||
|
||
it('拒绝非法 updatedAt 格式', () => {
|
||
assert.equal(
|
||
validate({
|
||
version: '1.0.0',
|
||
presetDataVersion: 30,
|
||
updatedAt: '04/25/2026',
|
||
}),
|
||
false,
|
||
)
|
||
})
|
||
|
||
it('拒绝缺少 presetDataVersion', () => {
|
||
assert.equal(
|
||
validate({ version: '1.0.0', updatedAt: '2026-04-25' }),
|
||
false,
|
||
)
|
||
})
|
||
})
|
||
|
||
// ==================== Service map schema ====================
|
||
|
||
describe('service-map schema', () => {
|
||
const validate = compile('service-map')
|
||
|
||
it('合法 service-map 通过', () => {
|
||
assert.equal(
|
||
validate({
|
||
chat: { modelName: 'gpt-5-mini', providerId: 'provider-openai-001' },
|
||
}),
|
||
true,
|
||
)
|
||
})
|
||
|
||
it('拒绝缺少 providerId 的条目', () => {
|
||
assert.equal(validate({ chat: { modelName: 'gpt-5-mini' } }), false)
|
||
})
|
||
|
||
it('拒绝条目中含未知字段', () => {
|
||
assert.equal(
|
||
validate({
|
||
chat: {
|
||
modelName: 'gpt-5-mini',
|
||
providerId: 'provider-openai-001',
|
||
extra: 'oops',
|
||
},
|
||
}),
|
||
false,
|
||
)
|
||
})
|
||
})
|
||
|
||
// ==================== Providers-index schema ====================
|
||
|
||
describe('providers-index schema', () => {
|
||
const validate = compile('providers-index')
|
||
|
||
it('合法 _index.json 通过', () => {
|
||
assert.equal(validate({ description: 'x', order: ['openai', 'anthropic'] }), true)
|
||
})
|
||
|
||
it('拒绝 order 含重复元素', () => {
|
||
assert.equal(validate({ order: ['openai', 'openai'] }), false)
|
||
})
|
||
|
||
it('拒绝缺少 order', () => {
|
||
assert.equal(validate({ description: 'x' }), false)
|
||
})
|
||
})
|