Files
config-center/__tests__/validate.test.mjs
Yige c46cf07d2f feat(compute): 补 GLM-5.3 系列规格并标注 Ox Alpha 真身 (#81)
* feat(compute): 补 GLM-5.3 系列规格并标注 Ox Alpha 真身

Ox Alpha 是智谱 GLM-5.3-Flash 的匿名发布别名(canonical_slug
z-ai/glm-5.3-flash-20260826,2026-08-26 揭晓后已从 OpenRouter 列表下架)。

改动:
- model-specs/zhipu.json 新增 glm-5.3 与 glm-5.3-flash 正式规格。两者均
  强制思考(z.ai 文档:thinking.type 仅接受 enabled;OpenRouter models API:
  reasoning.mandatory=true,supported_efforts=[max,high,low]),故
  routing.reasoning.supportedModes 不含 off。按 #79 的教训只用 exact 匹配,
  避免 glm-5.3* 误吞 glm-5.3-flash。
- model-specs/stealth.json 的 ox-alpha 用 spec.extra.modelOrigin 标注真身。
  条目保留:云端算力可能仍以旧别名下发该模型。
- schemas/model-spec.schema.json 为 extra.thinkingOnly / thinkingDefault
  补正式定义与 description。此前这两个键无任何说明,客户端因此从未消费,
  用户选「关闭思考」即触发上游 400(见 desirecore#2307)。
- scripts/validate.mjs 新增静默失效键名巡检:extra 是开放对象,写错键名
  既不报错也无告警。当前巡出 17 处扁平 extra.reasoningEffort。刻意只告警
  不失败——存量取值需逐个核实各自接入面实际接受哪些 effort。

* docs(schema): 写明 extra.reasoning 与 thinkingOnly 的职责边界

provider schema 的 extra.reasoning 此前只有子字段 description、对象本身没有,
维护者看不出它与 model-spec 的 thinkingOnly 分别回答什么问题——issue
desirecore#2307 的误解正源于此。补上对象级说明:本键答「接入面接受哪些 effort
值」且只能写在 provider model;thinkingOnly 答「off 能不能用」、不声明深度档位。
2026-08-27 15:21:25 +08:00

744 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, 43)
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-fable-5',
'claude-opus-5',
'claude-sonnet-5',
])
assert.deepEqual(enabled(openai), [
'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('Brave Search API 使用显式密钥声明且默认关闭', () => {
const brave = JSON.parse(readFileSync(join(ROOT, 'api-providers', 'web-search', 'brave.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.deepEqual(index.order, ['tavily', 'brave', 'serper'])
assert.equal(
validateFile(join(ROOT, 'api-providers', 'web-search', 'brave.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('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.deepEqual(specs[0].spec.serviceType, ['asr'])
assert.ok(specs[0].spec.capabilities.includes('asr'))
})
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('Qwen3.8 Max Preview 应在 Token Plan 中提供完整的推理与视觉规格', () => {
const tokenPlan = JSON.parse(readFileSync(join(ROOT, 'compute', 'coding-plans', 'dashscope-token-plan.json'), 'utf8'))
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'qwen.json'), 'utf8'))
const modelId = 'qwen3.8-max-preview'
const model = tokenPlan.models.find((item) => item.modelName === modelId)
assert.ok(model, `Token Plan 缺少 ${modelId}`)
assert.equal(model.contextWindow, 983616)
assert.equal(model.defaultTemperature, 0.6)
assert.ok(model.capabilities.includes('reasoning'))
assert.ok(model.capabilities.includes('vision'))
assert.ok(model.serviceType.includes('reasoning'))
assert.ok(model.serviceType.includes('vision'))
assert.deepEqual(model.extra.reasoning.supportedEfforts, ['low', 'high', 'xhigh'])
assert.equal(model.extra.reasoning.defaultEffort, 'xhigh')
assert.equal(model.extra.thinkingOnly, true)
assert.equal(model.extra.thinkingMaxTokens, 262144)
assert.equal(model.extra.preserveThinkingDefault, true)
assert.equal(model.extra.supportsParallelToolCalls, false)
const specs = specFile.specs.filter((item) => item.id === modelId)
assert.equal(specs.length, 1, `model-specs 中 ${modelId} 应且仅应有一条规格`)
assert.equal(specs[0].spec.contextWindow, 1000000)
assert.equal(specs[0].spec.defaultTemperature, 0.6)
assert.equal(specs[0].spec.supportsReasoning, true)
assert.ok(specs[0].spec.capabilities.includes('vision'))
})
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')
for (const [id, modelSpec] of [
['qwen3.8-max', qwenBase],
['qwen3.8-max-preview', qwenPreview],
['deepseek-v4-pro', deepseekBase],
['deepseek-v4-pro-0813', deepseek0813],
]) {
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(qwenBase.spec.contextWindow, qwenPreview.spec.contextWindow)
assert.equal(qwenBase.spec.maxOutputTokens, qwenPreview.spec.maxOutputTokens)
assert.equal(deepseekBase.spec.contextWindow, deepseek0813.spec.contextWindow)
assert.equal(deepseekBase.spec.maxOutputTokens, deepseek0813.spec.maxOutputTokens)
})
it('所有 Provider 应按供应商归属计价,模型来源不覆盖供应商币种', () => {
const expectedCurrencies = {
anthropic: 'USD',
'anthropic-claude': 'USD',
baichuan: 'CNY',
baidu: 'CNY',
cohere: 'USD',
dashscope: 'CNY',
deepseek: 'CNY',
google: 'USD',
kling: 'CNY',
lingyiwanwu: '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: nullPR #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/defaultTopPreasoning 模型推荐做法)', () => {
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('拒绝 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('仍允许模型内在扩展参数', () => {
const data = {
specs: [{ id: 'gpt-test', spec: { extra: { intrinsicBudgetHint: 8192 } } }],
}
assert.equal(validate(data), true, JSON.stringify(validate.errors))
})
})
// ==================== 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)
})
})