fix(provider-registry): address catalog review findings

- Iterate variant/quant/date id stripping to a fixpoint (shared by canonOf
  and normalizeModelId) so canonicalization is idempotent; drop `-medium`
  from variant suffixes (it ate Mistral tier names). Pins via new
  invariants: base ids are canonOf fixpoints.
- Restore hunyuan + tencent-cloud-ti provider sources (lost in
  regeneration), delete retired cephalon (16518), restore groq
  apiFeatures.serviceTier, drop the stray duplicate bedrock
  gpt-oss-120b-1:0 override (+ invariant: duplicated keys need a self
  variant).
- Gate prefix-inherited web-search by text input+output modalities so
  TTS/transcription SKUs no longer carry it (+ generalized invariant).
- Stamp data/*.json version with a content hash instead of the generation
  date, so any content change re-runs the preset seeders (date collision
  with base made SeedRunner skip TokenHub on existing DBs).
- Fix stale src/labs + src/provider remediation text in ci.yml and
  .gitattributes.
- ppio's qwen3-235b-a22b-thinking override becomes a named standalone (its
  base row now folds into qwen3-235b-a22b).
- Regenerated data/*.json (includes upstream drift since last run).

Addresses https://github.com/CherryHQ/cherry-studio/pull/16401#pullrequestreview-4618897871

Signed-off-by: suyao <sy20010504@gmail.com>
This commit is contained in:
suyao
2026-07-03 00:54:22 +08:00
parent 04f348b019
commit 90b01d33a3
17 changed files with 524 additions and 391 deletions

View File

@@ -51,7 +51,7 @@ jobs:
- 'patches/**'
catalog-hand-edit-check:
# provider-registry data/*.json are GENERATED from src/labs + src/provider. Catch a hand-edit:
# provider-registry data/*.json are GENERATED from src/creators + src/providers. Catch a hand-edit:
# if the data changed but no source under src/ or scripts/ did, the JSON was edited directly.
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
@@ -73,7 +73,7 @@ jobs:
- name: Reject hand-edited catalog
if: steps.filter.outputs.data == 'true' && steps.filter.outputs.source == 'false'
run: |
echo "::error::packages/provider-registry/data/*.json changed but no src/ or scripts/ change — that's a hand-edit. The data files are generated: edit src/labs|src/provider and run \`pnpm --filter @cherrystudio/provider-registry generate\`, then commit. Never hand-edit data/*.json."
echo "::error::packages/provider-registry/data/*.json changed but no src/ or scripts/ change — that's a hand-edit. The data files are generated: edit src/creators|src/providers and run \`pnpm --filter @cherrystudio/provider-registry generate\`, then commit. Never hand-edit data/*.json."
exit 1
changeset-check:

View File

@@ -1,4 +1,4 @@
# Generated artifacts — produced by `pnpm generate` from src/labs + src/provider.
# Generated artifacts — produced by `pnpm generate` from src/creators + src/providers.
# Never hand-edit; collapse them in PR diffs.
data/models.json linguist-generated=true
data/providers.json linguist-generated=true

View File

@@ -2417,11 +2417,11 @@
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.08
"perMillionTokens": 0.117
},
"output": {
"currency": "USD",
"perMillionTokens": 0.5
"perMillionTokens": 0.45499999999999996
}
}
},
@@ -2448,20 +2448,20 @@
{
"capabilities": ["function-call", "reasoning", "structured-output"],
"contextWindow": 131072,
"id": "qwen3-30b-a3b-thinking",
"id": "qwen3-30b-a3b",
"inputModalities": ["text"],
"metadata": {},
"name": "qwen3-30b-a3b-thinking",
"name": "qwen3-30b-a3b",
"outputModalities": ["text"],
"ownedBy": "alibaba",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.08
"perMillionTokens": 0.13
},
"output": {
"currency": "USD",
"perMillionTokens": 0.39999999999999997
"perMillionTokens": 1.56
}
}
},
@@ -2485,46 +2485,6 @@
}
}
},
{
"capabilities": ["function-call", "reasoning", "structured-output"],
"contextWindow": 262144,
"id": "qwen3-235b-a22b-thinking",
"inputModalities": ["text"],
"metadata": {},
"name": "qwen3-235b-a22b-thinking",
"outputModalities": ["text"],
"ownedBy": "alibaba",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.14950000000000002
},
"output": {
"currency": "USD",
"perMillionTokens": 1.495
}
}
},
{
"capabilities": ["function-call", "reasoning", "structured-output"],
"contextWindow": 131072,
"id": "qwen3-30b-a3b",
"inputModalities": ["text"],
"metadata": {},
"name": "qwen3-30b-a3b",
"outputModalities": ["text"],
"ownedBy": "alibaba",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.12
},
"output": {
"currency": "USD",
"perMillionTokens": 0.5
}
}
},
{
"contextWindow": 128000,
"id": "qwen-2-5-coder-32b-instruct",
@@ -3006,6 +2966,39 @@
"supportedEfforts": ["low", "medium", "high", "max"]
}
},
{
"capabilities": ["function-call", "reasoning", "image-recognition", "structured-output", "file-input"],
"contextWindow": 1000000,
"family": "claude-fable",
"id": "claude-fable-5",
"inputModalities": ["text", "image"],
"maxOutputTokens": 128000,
"metadata": {},
"name": "Claude Fable 5",
"outputModalities": ["text"],
"ownedBy": "anthropic",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 1
},
"cacheWrite": {
"currency": "USD",
"perMillionTokens": 12.5
},
"input": {
"currency": "USD",
"perMillionTokens": 10
},
"output": {
"currency": "USD",
"perMillionTokens": 50
}
},
"reasoning": {
"supportedEfforts": ["low", "medium", "high", "max"]
}
},
{
"capabilities": [
"function-call",
@@ -3380,39 +3373,6 @@
}
}
},
{
"capabilities": ["function-call", "reasoning", "image-recognition", "structured-output", "file-input"],
"contextWindow": 1000000,
"family": "claude-fable",
"id": "claude-fable-5",
"inputModalities": ["text", "image"],
"maxOutputTokens": 128000,
"metadata": {},
"name": "Claude Fable 5",
"outputModalities": ["text"],
"ownedBy": "anthropic",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 1
},
"cacheWrite": {
"currency": "USD",
"perMillionTokens": 12.5
},
"input": {
"currency": "USD",
"perMillionTokens": 10
},
"output": {
"currency": "USD",
"perMillionTokens": 50
}
},
"reasoning": {
"supportedEfforts": ["low", "medium", "high", "max"]
}
},
{
"capabilities": ["function-call", "reasoning", "image-recognition", "file-input", "web-search"],
"contextWindow": 200000,
@@ -5802,11 +5762,11 @@
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.19999999999999998
"perMillionTokens": 0.24
},
"output": {
"currency": "USD",
"perMillionTokens": 0.77
"perMillionTokens": 0.8999999999999999
}
}
},
@@ -7015,7 +6975,7 @@
}
},
{
"capabilities": ["audio-generation", "web-search"],
"capabilities": ["audio-generation"],
"contextWindow": 8192,
"family": "gemini-flash",
"id": "gemini-2-5-flash-preview-tts",
@@ -7187,7 +7147,7 @@
}
},
{
"capabilities": ["audio-generation", "web-search"],
"capabilities": ["audio-generation"],
"contextWindow": 8192,
"family": "gemini-flash",
"id": "gemini-2-5-pro-preview-tts",
@@ -7417,7 +7377,7 @@
}
},
{
"capabilities": ["audio-generation", "web-search"],
"capabilities": ["audio-generation"],
"contextWindow": 32768,
"family": "gemini-pro",
"id": "gemini-2-5-pro-tts",
@@ -7439,7 +7399,7 @@
}
},
{
"capabilities": ["audio-generation", "web-search"],
"capabilities": ["audio-generation"],
"contextWindow": 32768,
"family": "gemini-flash",
"id": "gemini-2-5-flash-tts",
@@ -8396,10 +8356,10 @@
{
"capabilities": ["function-call", "reasoning", "structured-output"],
"contextWindow": 32768,
"id": "lfm-2-5-1-2b-thinking",
"id": "lfm-2-5-1-2b",
"inputModalities": ["text"],
"metadata": {},
"name": "lfm-2-5-1-2b-thinking",
"name": "lfm-2-5-1-2b",
"outputModalities": ["text"],
"ownedBy": "liquidai",
"pricing": {
@@ -8455,7 +8415,7 @@
"capabilities": ["reasoning"],
"contextWindow": 32768,
"family": "longcat",
"id": "longcat-flash-thinking",
"id": "longcat-flash",
"inputModalities": ["text"],
"maxOutputTokens": 32768,
"metadata": {},
@@ -9673,14 +9633,15 @@
}
},
{
"capabilities": ["function-call", "image-recognition", "file-input"],
"contextWindow": 128000,
"capabilities": ["function-call", "reasoning", "image-recognition", "structured-output", "file-input"],
"contextWindow": 262144,
"family": "mistral-medium",
"id": "mistral",
"id": "mistral-medium",
"inputModalities": ["text", "image"],
"maxOutputTokens": 64000,
"maxOutputTokens": 262144,
"metadata": {},
"name": "Mistral Medium 3.1",
"openWeights": true,
"outputModalities": ["text"],
"ownedBy": "mistral",
"pricing": {
@@ -9692,6 +9653,9 @@
"currency": "USD",
"perMillionTokens": 2
}
},
"reasoning": {
"supportedEfforts": ["none", "high"]
}
},
{
@@ -9928,7 +9892,7 @@
"capabilities": ["function-call", "reasoning"],
"contextWindow": 128000,
"family": "magistral-medium",
"id": "magistral",
"id": "magistral-medium",
"inputModalities": ["text"],
"maxOutputTokens": 16384,
"metadata": {},
@@ -10130,32 +10094,6 @@
}
}
},
{
"capabilities": ["function-call", "reasoning", "image-recognition", "structured-output", "file-input"],
"contextWindow": 262144,
"family": "mistral-medium",
"id": "mistral-medium",
"inputModalities": ["text", "image"],
"maxOutputTokens": 262144,
"metadata": {},
"name": "Mistral Medium 3.5",
"openWeights": true,
"outputModalities": ["text"],
"ownedBy": "mistral",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 1.5
},
"output": {
"currency": "USD",
"perMillionTokens": 7.5
}
},
"reasoning": {
"supportedEfforts": ["none", "high"]
}
},
{
"capabilities": ["function-call", "reasoning", "image-recognition", "file-input"],
"contextWindow": 256000,
@@ -11048,11 +10986,11 @@
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.55
"perMillionTokens": 0.66
},
"output": {
"currency": "USD",
"perMillionTokens": 3.1999999999999997
"perMillionTokens": 3.41
}
}
},
@@ -11462,26 +11400,6 @@
}
}
},
{
"capabilities": ["function-call", "reasoning", "image-recognition", "audio-recognition", "video-recognition"],
"contextWindow": 256000,
"id": "nemotron-3-nano-omni-30b-a3b-reasoning",
"inputModalities": ["text", "audio", "image", "video"],
"metadata": {},
"name": "nemotron-3-nano-omni-30b-a3b-reasoning",
"outputModalities": ["text"],
"ownedBy": "nvidia",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0
},
"output": {
"currency": "USD",
"perMillionTokens": 0
}
}
},
{
"capabilities": ["image-recognition", "image-generation", "file-input"],
"family": "gpt-image",
@@ -12040,7 +11958,7 @@
}
},
{
"capabilities": ["audio-recognition", "web-search"],
"capabilities": ["audio-recognition"],
"family": "gpt",
"id": "gpt-4o-transcribe",
"inputModalities": ["audio"],
@@ -12230,7 +12148,7 @@
}
},
{
"capabilities": ["audio-recognition", "web-search"],
"capabilities": ["audio-recognition"],
"family": "o-mini",
"id": "gpt-4o-mini-transcribe",
"inputModalities": ["audio"],
@@ -14225,31 +14143,6 @@
"supportedEfforts": ["low", "medium", "high"]
}
},
{
"capabilities": ["function-call", "reasoning"],
"contextWindow": 32768,
"id": "step-1-32k",
"inputModalities": ["text"],
"maxOutputTokens": 32768,
"metadata": {},
"name": "Step 1 (32K)",
"outputModalities": ["text"],
"ownedBy": "stepfun",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.41
},
"input": {
"currency": "USD",
"perMillionTokens": 2.05
},
"output": {
"currency": "USD",
"perMillionTokens": 9.59
}
}
},
{
"capabilities": ["function-call", "reasoning"],
"contextWindow": 16384,
@@ -14275,6 +14168,41 @@
}
}
},
{
"capabilities": ["audio-generation"],
"family": "step",
"id": "step-tts-2",
"inputModalities": ["text"],
"metadata": {},
"name": "Step TTS 2",
"outputModalities": ["audio"],
"ownedBy": "stepfun"
},
{
"capabilities": ["function-call", "reasoning"],
"contextWindow": 32768,
"id": "step-1-32k",
"inputModalities": ["text"],
"maxOutputTokens": 32768,
"metadata": {},
"name": "Step 1 (32K)",
"outputModalities": ["text"],
"ownedBy": "stepfun",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.41
},
"input": {
"currency": "USD",
"perMillionTokens": 2.05
},
"output": {
"currency": "USD",
"perMillionTokens": 9.59
}
}
},
{
"capabilities": ["reasoning"],
"contextWindow": 256000,
@@ -17238,6 +17166,26 @@
}
}
},
{
"capabilities": ["audio-generation"],
"family": "step",
"id": "stepaudio-2-5-tts",
"inputModalities": ["text"],
"metadata": {},
"name": "StepAudio 2.5 TTS",
"outputModalities": ["audio"],
"ownedBy": "stepfun"
},
{
"capabilities": ["audio-recognition"],
"family": "step",
"id": "stepaudio-2-5-asr",
"inputModalities": ["audio"],
"metadata": {},
"name": "StepAudio 2.5 ASR",
"outputModalities": ["text"],
"ownedBy": "stepfun"
},
{
"capabilities": ["video-generation"],
"family": "veo",
@@ -17474,5 +17422,5 @@
"ownedBy": "vercel"
}
],
"version": "2026.07.01"
"version": "f3aa40d6c14fa452"
}

View File

@@ -237,8 +237,7 @@
},
{
"apiModelId": "doubao-seed-1-6-thinking-250715",
"modelId": "doubao-seed-1-6-thinking",
"name": "doubao-seed-1-6-thinking-250715",
"modelId": "doubao-seed-1-6",
"pricing": {
"input": {
"currency": "USD",
@@ -2029,21 +2028,6 @@
},
"providerId": "aws-bedrock"
},
{
"apiModelId": "openai.gpt-oss-120b-1:0",
"modelId": "gpt-oss-120b",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.15
},
"output": {
"currency": "USD",
"perMillionTokens": 0.6
}
},
"providerId": "aws-bedrock"
},
{
"apiModelId": "openai.gpt-oss-20b",
"modelId": "gpt-oss-20b",
@@ -2628,6 +2612,22 @@
},
"providerId": "aws-bedrock"
},
{
"apiModelId": "gemma-4-31b",
"modelId": "gemma-4-31b",
"name": "Gemma 4 31B IT",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.99
},
"output": {
"currency": "USD",
"perMillionTokens": 1.49
}
},
"providerId": "cerebras"
},
{
"apiModelId": "zai-glm-4.7",
"modelId": "glm-4-7",
@@ -2873,6 +2873,29 @@
},
"providerId": "copilot"
},
{
"apiModelId": "claude-sonnet-5",
"modelId": "claude-sonnet-5",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.2
},
"cacheWrite": {
"currency": "USD",
"perMillionTokens": 2.5
},
"input": {
"currency": "USD",
"perMillionTokens": 2
},
"output": {
"currency": "USD",
"perMillionTokens": 10
}
},
"providerId": "copilot"
},
{
"apiModelId": "gemini-2.5-pro",
"modelId": "gemini-2-5-pro",
@@ -3120,6 +3143,45 @@
},
"providerId": "copilot"
},
{
"apiModelId": "kimi-k2.7-code",
"modelId": "kimi-k2-7-code",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.19
},
"input": {
"currency": "USD",
"perMillionTokens": 0.95
},
"output": {
"currency": "USD",
"perMillionTokens": 4
}
},
"providerId": "copilot"
},
{
"apiModelId": "mai-code-1-flash-picker",
"modelId": "mai-code-1-flash-picker",
"name": "MAI-Code-1-Flash",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.075
},
"input": {
"currency": "USD",
"perMillionTokens": 0.75
},
"output": {
"currency": "USD",
"perMillionTokens": 4.5
}
},
"providerId": "copilot"
},
{
"apiModelId": "qwen-image",
"imageGeneration": {
@@ -4663,6 +4725,29 @@
},
"providerId": "gateway"
},
{
"apiModelId": "anthropic/claude-fable-5",
"modelId": "claude-fable-5",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 1
},
"cacheWrite": {
"currency": "USD",
"perMillionTokens": 12.5
},
"input": {
"currency": "USD",
"perMillionTokens": 10
},
"output": {
"currency": "USD",
"perMillionTokens": 50
}
},
"providerId": "gateway"
},
{
"apiModelId": "anthropic/claude-haiku-4.5",
"modelId": "claude-haiku-4-5",
@@ -6952,7 +7037,7 @@
},
{
"apiModelId": "mistral/magistral-medium",
"modelId": "magistral",
"modelId": "magistral-medium",
"pricing": {
"input": {
"currency": "USD",
@@ -7315,21 +7400,6 @@
},
"providerId": "gateway"
},
{
"apiModelId": "mistral/mistral-medium",
"modelId": "mistral",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.4
},
"output": {
"currency": "USD",
"perMillionTokens": 2
}
},
"providerId": "gateway"
},
{
"apiModelId": "mistral/mistral-large-3",
"modelId": "mistral-large-3",
@@ -7345,6 +7415,21 @@
},
"providerId": "gateway"
},
{
"apiModelId": "mistral/mistral-medium",
"modelId": "mistral-medium",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.4
},
"output": {
"currency": "USD",
"perMillionTokens": 2
}
},
"providerId": "gateway"
},
{
"apiModelId": "mistral/mistral-medium-3.5",
"modelId": "mistral-medium-3-5",
@@ -9676,24 +9761,9 @@
},
"providerId": "huggingface"
},
{
"apiModelId": "Qwen/Qwen3-235B-A22B",
"modelId": "qwen3-235b-a22b",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.2
},
"output": {
"currency": "USD",
"perMillionTokens": 0.8
}
},
"providerId": "huggingface"
},
{
"apiModelId": "Qwen/Qwen3-235B-A22B-Thinking-2507",
"modelId": "qwen3-235b-a22b-thinking",
"modelId": "qwen3-235b-a22b",
"pricing": {
"input": {
"currency": "USD",
@@ -10069,6 +10139,21 @@
},
"providerId": "modelscope"
},
{
"apiModelId": "Qwen/Qwen3-235B-A22B-Thinking-2507",
"modelId": "qwen3-235b-a22b",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0
},
"output": {
"currency": "USD",
"perMillionTokens": 0
}
},
"providerId": "modelscope"
},
{
"apiModelId": "Qwen/Qwen3-235B-A22B-Instruct-2507",
"modelId": "qwen3-235b-a22b-instruct",
@@ -10086,8 +10171,8 @@
"providerId": "modelscope"
},
{
"apiModelId": "Qwen/Qwen3-235B-A22B-Thinking-2507",
"modelId": "qwen3-235b-a22b-thinking",
"apiModelId": "Qwen/Qwen3-30B-A3B-Thinking-2507",
"modelId": "qwen3-30b-a3b",
"pricing": {
"input": {
"currency": "USD",
@@ -10115,21 +10200,6 @@
},
"providerId": "modelscope"
},
{
"apiModelId": "Qwen/Qwen3-30B-A3B-Thinking-2507",
"modelId": "qwen3-30b-a3b-thinking",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0
},
"output": {
"currency": "USD",
"perMillionTokens": 0
}
},
"providerId": "modelscope"
},
{
"apiModelId": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
"modelId": "qwen3-coder-30b-a3b-instruct",
@@ -11509,6 +11579,29 @@
},
"providerId": "openrouter"
},
{
"apiModelId": "anthropic/claude-fable-5",
"modelId": "claude-fable-5",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 1
},
"cacheWrite": {
"currency": "USD",
"perMillionTokens": 12.5
},
"input": {
"currency": "USD",
"perMillionTokens": 10
},
"output": {
"currency": "USD",
"perMillionTokens": 50
}
},
"providerId": "openrouter"
},
{
"apiModelId": "~anthropic/claude-fable-latest",
"modelId": "claude-fable-latest",
@@ -12055,11 +12148,11 @@
},
"input": {
"currency": "USD",
"perMillionTokens": 0.2
"perMillionTokens": 0.24
},
"output": {
"currency": "USD",
"perMillionTokens": 0.77
"perMillionTokens": 0.9
}
},
"providerId": "openrouter"
@@ -12176,15 +12269,15 @@
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.02
"perMillionTokens": 0.018
},
"input": {
"currency": "USD",
"perMillionTokens": 0.098
"perMillionTokens": 0.089
},
"output": {
"currency": "USD",
"perMillionTokens": 0.196
"perMillionTokens": 0.18
}
},
"providerId": "openrouter"
@@ -14196,15 +14289,15 @@
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.11
"perMillionTokens": 0.14
},
"input": {
"currency": "USD",
"perMillionTokens": 0.55
"perMillionTokens": 0.66
},
"output": {
"currency": "USD",
"perMillionTokens": 3.2
"perMillionTokens": 3.41
}
},
"providerId": "openrouter"
@@ -14234,15 +14327,15 @@
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.11
"perMillionTokens": 0.14
},
"input": {
"currency": "USD",
"perMillionTokens": 0.55
"perMillionTokens": 0.66
},
"output": {
"currency": "USD",
"perMillionTokens": 3.2
"perMillionTokens": 3.41
}
},
"providerId": "openrouter"
@@ -14351,6 +14444,22 @@
},
"providerId": "openrouter"
},
{
"apiModelId": "poolside/laguna-xs-2.1:free",
"modelId": "laguna-xs-2-1",
"name": "Laguna XS 2.1 (free)",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0
},
"output": {
"currency": "USD",
"perMillionTokens": 0
}
},
"providerId": "openrouter"
},
{
"apiModelId": "liquid/lfm-2-24b-a2b",
"modelId": "lfm-2-24b-a2b",
@@ -14367,8 +14476,8 @@
"providerId": "openrouter"
},
{
"apiModelId": "liquid/lfm-2.5-1.2b-instruct:free",
"modelId": "lfm-2-5-1-2b-instruct",
"apiModelId": "liquid/lfm-2.5-1.2b-thinking:free",
"modelId": "lfm-2-5-1-2b",
"pricing": {
"input": {
"currency": "USD",
@@ -14382,8 +14491,8 @@
"providerId": "openrouter"
},
{
"apiModelId": "liquid/lfm-2.5-1.2b-thinking:free",
"modelId": "lfm-2-5-1-2b-thinking",
"apiModelId": "liquid/lfm-2.5-1.2b-instruct:free",
"modelId": "lfm-2-5-1-2b-instruct",
"pricing": {
"input": {
"currency": "USD",
@@ -14734,17 +14843,13 @@
"apiModelId": "minimax/minimax-m2",
"modelId": "minimax-m2",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.03
},
"input": {
"currency": "USD",
"perMillionTokens": 0.255
},
"output": {
"currency": "USD",
"perMillionTokens": 1
"perMillionTokens": 1.02
}
},
"providerId": "openrouter"
@@ -14759,11 +14864,11 @@
},
"input": {
"currency": "USD",
"perMillionTokens": 0.29
"perMillionTokens": 0.3
},
"output": {
"currency": "USD",
"perMillionTokens": 0.95
"perMillionTokens": 1.2
}
},
"providerId": "openrouter"
@@ -15160,7 +15265,7 @@
},
{
"apiModelId": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
"modelId": "nemotron-3-nano-omni-30b-a3b-reasoning",
"modelId": "nemotron-3-nano-omni-30b-a3b",
"pricing": {
"input": {
"currency": "USD",
@@ -15654,24 +15759,9 @@
},
"providerId": "openrouter"
},
{
"apiModelId": "qwen/qwen3-235b-a22b",
"modelId": "qwen3-235b-a22b",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.455
},
"output": {
"currency": "USD",
"perMillionTokens": 1.82
}
},
"providerId": "openrouter"
},
{
"apiModelId": "qwen/qwen3-235b-a22b-thinking-2507",
"modelId": "qwen3-235b-a22b-thinking",
"modelId": "qwen3-235b-a22b",
"pricing": {
"input": {
"currency": "USD",
@@ -15685,16 +15775,16 @@
"providerId": "openrouter"
},
{
"apiModelId": "qwen/qwen3-30b-a3b",
"apiModelId": "qwen/qwen3-30b-a3b-thinking-2507",
"modelId": "qwen3-30b-a3b",
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.12
"perMillionTokens": 0.13
},
"output": {
"currency": "USD",
"perMillionTokens": 0.5
"perMillionTokens": 1.56
}
},
"providerId": "openrouter"
@@ -15714,25 +15804,6 @@
},
"providerId": "openrouter"
},
{
"apiModelId": "qwen/qwen3-30b-a3b-thinking-2507",
"modelId": "qwen3-30b-a3b-thinking",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.08
},
"input": {
"currency": "USD",
"perMillionTokens": 0.08
},
"output": {
"currency": "USD",
"perMillionTokens": 0.4
}
},
"providerId": "openrouter"
},
{
"apiModelId": "qwen/qwen3-32b",
"modelId": "qwen3-32b",
@@ -16013,17 +16084,13 @@
"apiModelId": "qwen/qwen3-8b",
"modelId": "qwen3-8b",
"pricing": {
"cacheRead": {
"currency": "USD",
"perMillionTokens": 0.05
},
"input": {
"currency": "USD",
"perMillionTokens": 0.05
"perMillionTokens": 0.117
},
"output": {
"currency": "USD",
"perMillionTokens": 0.4
"perMillionTokens": 0.455
}
},
"providerId": "openrouter"
@@ -16276,11 +16343,11 @@
"pricing": {
"input": {
"currency": "USD",
"perMillionTokens": 0.08
"perMillionTokens": 0.117
},
"output": {
"currency": "USD",
"perMillionTokens": 0.5
"perMillionTokens": 0.455
}
},
"providerId": "openrouter"
@@ -17452,6 +17519,7 @@
},
"modelId": "qwen3-235b-a22b-thinking",
"modelVariants": ["235b"],
"name": "Qwen3 235B A22B Thinking 2507",
"pricing": {
"input": {
"currency": "CNY",
@@ -18382,7 +18450,7 @@
},
{
"apiModelId": "Qwen/Qwen3-235B-A22B-Thinking-2507",
"modelId": "qwen3-235b-a22b-thinking",
"modelId": "qwen3-235b-a22b",
"pricing": {
"input": {
"currency": "USD",
@@ -19680,5 +19748,5 @@
"providerId": "zhipu"
}
],
"version": "2026.07.01"
"version": "39112f1353bd9bbb"
}

View File

@@ -269,29 +269,6 @@
},
"name": "302.AI"
},
{
"apiFeatures": {
"arrayContent": false
},
"defaultChatEndpoint": "openai-chat-completions",
"description": "Cephalon - AI model provider",
"endpointConfigs": {
"openai-chat-completions": {
"adapterFamily": "openai-compatible",
"baseUrl": "https://cephalon.cloud/user-center/v1/model"
}
},
"id": "cephalon",
"metadata": {
"website": {
"apiKey": "https://cephalon.cloud/api",
"docs": "https://cephalon.cloud/",
"models": "https://cephalon.cloud/model",
"official": "https://cephalon.cloud/"
}
},
"name": "Cephalon"
},
{
"defaultChatEndpoint": "openai-chat-completions",
"description": "LANYUN - AI model provider",
@@ -849,6 +826,9 @@
"name": "MiniMax"
},
{
"apiFeatures": {
"serviceTier": true
},
"defaultChatEndpoint": "openai-chat-completions",
"description": "Groq - AI model provider",
"endpointConfigs": {
@@ -1081,6 +1061,46 @@
},
"name": "Xirang"
},
{
"defaultChatEndpoint": "openai-chat-completions",
"description": "hunyuan - AI model provider",
"endpointConfigs": {
"openai-chat-completions": {
"adapterFamily": "openai-compatible",
"baseUrl": "https://api.hunyuan.cloud.tencent.com"
}
},
"id": "hunyuan",
"metadata": {
"website": {
"apiKey": "https://console.cloud.tencent.com/hunyuan/api-key",
"docs": "https://cloud.tencent.com/document/product/1729/111007",
"models": "https://cloud.tencent.com/document/product/1729/104753",
"official": "https://cloud.tencent.com/product/hunyuan"
}
},
"name": "hunyuan"
},
{
"defaultChatEndpoint": "openai-chat-completions",
"description": "Tencent Cloud TI - AI model provider",
"endpointConfigs": {
"openai-chat-completions": {
"adapterFamily": "openai-compatible",
"baseUrl": "https://api.lkeap.cloud.tencent.com"
}
},
"id": "tencent-cloud-ti",
"metadata": {
"website": {
"apiKey": "https://console.cloud.tencent.com/lkeap/api",
"docs": "https://cloud.tencent.com/document/product/1772",
"models": "https://console.cloud.tencent.com/tione/v2/aimarket",
"official": "https://cloud.tencent.com/product/ti"
}
},
"name": "Tencent Cloud TI"
},
{
"defaultChatEndpoint": "openai-chat-completions",
"description": "TokenHub - AI model provider",
@@ -1369,5 +1389,5 @@
"presetProviderId": "minimax"
}
],
"version": "2026.07.01"
"version": "0544e8f799859c83"
}

View File

@@ -12,9 +12,7 @@ import {
stripAggregatorPrefixes,
stripBedrockRevision,
stripBedrockVendorPrefix,
stripDateSnapshot,
stripQuantization,
stripVariantSuffixes
stripVariantQuantDateSuffixes
} from '../src/utils/normalize'
// strip the same org/host routing prefixes the runtime resolver does (zai-org-, databricks-, …),
@@ -22,15 +20,12 @@ import {
// bedrock cross-vendor `[region.]vendor.` / `vendor-` prefix (shared with the runtime).
const base = (id: string) => stripBedrockVendorPrefix(stripAggregatorPrefixes(id.toLowerCase().split('/').pop()!))
// Minus param-size stripping — the catalog keeps `qwen3-235b` ≠ `qwen3-30b`. Order matters: strip the
// `-thinking`/`-free` variant BEFORE the date so the date ends the token.
// Minus param-size stripping — the catalog keeps `qwen3-235b` ≠ `qwen3-30b`.
export const canonOf = (id: string): string => {
let s = base(id) // split('/').pop, lowercase, strip aggregator + bedrock-vendor prefix
s = stripBedrockRevision(s) // bedrock arn revision: claude-…-v1:0 / …:0 (keeps whisper-v3)
s = expandKnownPrefixes(s) // mm- → minimax-
s = stripVariantSuffixes(s) // -free / -thinking / -tee / -low / :free / (free) …
s = stripQuantization(s) // -fp8 / -fp16 / -awq …
s = stripDateSnapshot(s) // trailing release-date stamps + @tag (shared with the runtime resolver)
s = stripVariantQuantDateSuffixes(s) // variant/quant/date suffixes, iterated to a fixpoint
s = normalizeVersionSeparators(s) // 4.6 → 4-6
return s
.replace(/[^a-z0-9-]/g, '-')

View File

@@ -9,6 +9,7 @@
* tsx scripts/generate-catalog.ts --write # write both JSON files
* tsx scripts/generate-catalog.ts --report # also dump /tmp/gen-*.txt review files
*/
import { createHash } from 'node:crypto'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -38,9 +39,19 @@ const PROVIDERS_PATH = path.join(__dirname, '../data/providers.json')
const PROVIDER_MODELS_PATH = path.join(__dirname, '../data/provider-models.json')
const WRITE = process.argv.includes('--write')
const REPORT = process.argv.includes('--report')
// Stamp generated files with the generation date (YYYY.MM.DD). Upstream (models.dev/OpenRouter) is read
// live by default; set MODELSDEV_CACHE / OPENROUTER_CACHE to a local file to cache it during dev.
const VERSION = new Date().toISOString().slice(0, 10).replace(/-/g, '.')
// Each artifact's `version` is a hash of its own (version-less, key-sorted) content: equal content ⇒
// equal version, ANY content change ⇒ new version. Seeders (`PresetProviderSeeder` via `SeedRunner`)
// skip when the journal version matches, so a date stamp would let a same-day regeneration change
// content without changing version — and the seed would silently never run. Upstream (models.dev/
// OpenRouter) is read live by default; set MODELSDEV_CACHE / OPENROUTER_CACHE to cache it during dev.
const contentVersion = (body: unknown): string =>
createHash('sha256').update(JSON.stringify(body)).digest('hex').slice(0, 16)
/** Key-sort `body`, stamp `version: contentVersion(body)`, and serialize — the single write shape. */
const stampAndSerialize = (body: Record<string, unknown>): string => {
const sorted = sortKeys(body)
return JSON.stringify(sortKeys({ ...sorted, version: contentVersion(sorted) }), null, 2) + '\n'
}
// Canonicalization (`canonOf`) + prefix matching (`prefixHit`) live in `./canonicalize` so they can be
// unit-tested / reused without running this script's generation IIFE.
@@ -214,9 +225,13 @@ function buildModels(index: Index, claimed: Map<string, string>): Map<string, an
// which of its models carry it, as DATA, via `webSearch` id-prefixes. Union onto upstream capabilities.
const creatorWebSearch = new Map(CREATORS.map((l) => [l.id, l.webSearch ?? []]))
for (const m of models.values()) {
// An image-generation model never inherits a text sibling's web search just for sharing its prefix
// (`gpt-5-image*` ride the `gpt-5` prefix). web-search is a text capability — skip the image rows.
// web-search is a TEXT-CHAT capability: a non-chat SKU never inherits it just for sharing a chat
// sibling's prefix. Skip image rows (`gpt-5-image*` ride `gpt-5`; they output text too, so the
// modality gate alone won't catch them) and any row that doesn't converse in text on both sides —
// TTS (text→audio) and transcription (audio→text). Hand-listed capabilities are unaffected.
if ((m.capabilities ?? []).includes('image-generation')) continue
if (!(m.inputModalities ?? ['text']).includes('text') || !(m.outputModalities ?? ['text']).includes('text'))
continue
if ((creatorWebSearch.get(m.ownedBy) ?? []).some((p) => prefixHit(m.id, p)))
m.capabilities = [...new Set([...(m.capabilities ?? []), 'web-search'])]
}
@@ -228,13 +243,12 @@ function buildModels(index: Index, claimed: Map<string, string>): Map<string, an
* `fetchModels` / `overrides` are dropped), with `description` templated as `"{name} - AI model provider"`.
* Array order follows PROVIDERS; `sortKeys` orders each provider's keys.
*/
function buildProviders(): { providers: ProviderEntry[]; version: string } {
function buildProviders(): ProviderEntry[] {
// oxlint-disable-next-line no-unused-vars
const providers = PROVIDERS.map(({ modelsDevProvider, fetchModels, overrides, ...conn }) => ({
return PROVIDERS.map(({ modelsDevProvider, fetchModels, overrides, ...conn }) => ({
...conn,
description: `${conn.name} - AI model provider`
}))
return { providers, version: VERSION }
}
/**
@@ -244,7 +258,7 @@ function buildProviders(): { providers: ProviderEntry[]; version: string } {
* `modelsDevProvider`, one row per served model carrying that listing's PRICING. `modelId` resolves to a
* base row or is standalone with a `name`.
*/
function buildProviderModels(md: ModelsDevApi, baseIds: Set<string>): { root: any; rows: number } {
function buildProviderModels(md: ModelsDevApi, baseIds: Set<string>): { overrides: any[] } {
const seen = new Set<string>()
const rows: any[] = []
const variantsKey = (o: any): string => (o.modelVariants ?? []).slice().sort().join(',')
@@ -283,7 +297,7 @@ function buildProviderModels(md: ModelsDevApi, baseIds: Set<string>): { root: an
}
}
rows.sort((a, b) => `${a.providerId} ${a.modelId}`.localeCompare(`${b.providerId} ${b.modelId}`))
return { root: { overrides: rows, version: VERSION }, rows: rows.length }
return { overrides: rows }
}
void (async () => {
@@ -324,14 +338,14 @@ void (async () => {
const { metadata, ...rest } = m
return { ...rest, ...(metadata ? { metadata } : {}) }
})
fs.writeFileSync(MODELS_PATH, JSON.stringify(sortKeys({ models: list, version: VERSION }), null, 2) + '\n')
fs.writeFileSync(MODELS_PATH, stampAndSerialize({ models: list }))
console.log(`\nWROTE ${MODELS_PATH} (${list.length} models).`)
const providers = buildProviders()
fs.writeFileSync(PROVIDERS_PATH, JSON.stringify(sortKeys(providers), null, 2) + '\n')
console.log(`WROTE ${PROVIDERS_PATH} (${providers.providers.length} providers).`)
fs.writeFileSync(PROVIDERS_PATH, stampAndSerialize({ providers }))
console.log(`WROTE ${PROVIDERS_PATH} (${providers.length} providers).`)
const pm = buildProviderModels(md, new Set(models.keys()))
fs.writeFileSync(PROVIDER_MODELS_PATH, JSON.stringify(sortKeys(pm.root), null, 2) + '\n')
console.log(`WROTE ${PROVIDER_MODELS_PATH} (${pm.rows} rows).`)
fs.writeFileSync(PROVIDER_MODELS_PATH, stampAndSerialize(pm))
console.log(`WROTE ${PROVIDER_MODELS_PATH} (${pm.overrides.length} rows).`)
})()

View File

@@ -12,6 +12,7 @@ import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { canonOf } from '../../scripts/canonicalize'
import { ModelListSchema } from '../schemas/model'
import { ProviderModelListSchema } from '../schemas/provider-models'
@@ -23,10 +24,13 @@ const models = modelsRaw.models as Array<{
contextWindow?: number
maxOutputTokens?: number
capabilities?: string[]
inputModalities?: string[]
outputModalities?: string[]
}>
const overrides = providerModelsRaw.overrides as Array<{
providerId: string
modelId: string
apiModelId?: string
name?: string
}>
@@ -51,6 +55,13 @@ describe('catalog invariants (data/*.json)', () => {
expect(ids.filter((id) => JUNK.test(id))).toEqual([])
})
// Canonicalization must be idempotent on the artifact itself: a base id the canonicalizer would
// still fold (id !== canonOf(id)) means a variant/date row leaked through a non-fixpoint strip pass
// and coexists with (or shadows) its true base row.
it('every base id is a canonicalization fixpoint (id === canonOf(id))', () => {
expect(ids.filter((id) => canonOf(id) !== id)).toEqual([])
})
it('every override resolves to a base row or carries a standalone name', () => {
const broken = overrides
.filter((o) => !baseIds.has(o.modelId) && !o.name)
@@ -71,6 +82,34 @@ describe('catalog invariants (data/*.json)', () => {
expect(offenders).toEqual([])
})
// web-search is a text-chat capability: a model that doesn't converse in text on both sides (TTS is
// text→audio, transcription is audio→text, embedders output vector) must never carry it. The generator
// gates prefix-inherited web-search by modality; this catches any row slipping back in.
it('no non-text-chat model carries web-search (tts / transcription / embedding)', () => {
const offenders = models
.filter((m) => m.capabilities?.includes('web-search'))
.filter(
(m) => !(m.inputModalities ?? ['text']).includes('text') || !(m.outputModalities ?? ['text']).includes('text')
)
.map((m) => m.id)
expect(offenders).toEqual([])
})
// The loader's canonical override index (`overrideByKey`) resolves a duplicated key to the self
// variant (`apiModelId === modelId`) — see registry-loader.ts `buildOverrideIndex`. A duplicated key
// with NO self variant makes the winning override file-order-dependent, so forbid it.
it('every duplicated providerId::modelId key contains a self variant', () => {
const byKey = new Map<string, Array<(typeof overrides)[number]>>()
for (const o of overrides) {
const key = `${o.providerId}::${o.modelId}`
byKey.set(key, [...(byKey.get(key) ?? []), o])
}
const orderDependent = [...byKey.entries()]
.filter(([, rows]) => rows.length > 1 && !rows.some((r) => (r.apiModelId ?? r.modelId) === r.modelId))
.map(([key]) => key)
expect(orderDependent).toEqual([])
})
it('maxOutputTokens never exceeds contextWindow', () => {
const bad = models.filter((m) => m.contextWindow && m.maxOutputTokens && m.maxOutputTokens > m.contextWindow)
expect(bad.map((m) => m.id)).toEqual([])

View File

@@ -451,14 +451,6 @@ export default defineProvider({
input: { currency: 'USD', perMillionTokens: 0.15 },
output: { currency: 'USD', perMillionTokens: 0.35 }
}
},
{
modelId: 'gpt-oss-120b',
apiModelId: 'openai.gpt-oss-120b-1:0',
pricing: {
input: { currency: 'USD', perMillionTokens: 0.15 },
output: { currency: 'USD', perMillionTokens: 0.6 }
}
}
]
})

View File

@@ -1,16 +0,0 @@
import { openaiCompatible } from './types'
export default openaiCompatible({
id: 'cephalon',
name: 'Cephalon',
baseUrl: 'https://cephalon.cloud/user-center/v1/model',
website: {
apiKey: 'https://cephalon.cloud/api',
docs: 'https://cephalon.cloud/',
models: 'https://cephalon.cloud/model',
official: 'https://cephalon.cloud/'
},
apiFeatures: {
arrayContent: false
}
})

View File

@@ -10,6 +10,9 @@ export default defineProvider({
baseUrl: 'https://api.groq.com/openai'
}
},
apiFeatures: {
serviceTier: true
},
metadata: {
website: {
apiKey: 'https://console.groq.com/keys',

View File

@@ -0,0 +1,21 @@
import { defineProvider } from './types'
export default defineProvider({
id: 'hunyuan',
name: 'hunyuan',
defaultChatEndpoint: 'openai-chat-completions',
endpointConfigs: {
'openai-chat-completions': {
adapterFamily: 'openai-compatible',
baseUrl: 'https://api.hunyuan.cloud.tencent.com'
}
},
metadata: {
website: {
apiKey: 'https://console.cloud.tencent.com/hunyuan/api-key',
docs: 'https://cloud.tencent.com/document/product/1729/111007',
models: 'https://cloud.tencent.com/document/product/1729/104753',
official: 'https://cloud.tencent.com/product/hunyuan'
}
}
})

View File

@@ -8,7 +8,6 @@ import p_azure_openai from './azure-openai'
import p_baichuan from './baichuan'
import p_baidu_cloud from './baidu-cloud'
import p_burncloud from './burncloud'
import p_cephalon from './cephalon'
import p_cerebras from './cerebras'
import p_cherryin from './cherryin'
import p_copilot from './copilot'
@@ -24,6 +23,7 @@ import p_gpustack from './gpustack'
import p_grok from './grok'
import p_groq from './groq'
import p_huggingface from './huggingface'
import p_hunyuan from './hunyuan'
import p_hyperbolic from './hyperbolic'
import p_infini from './infini'
import p_jina from './jina'
@@ -52,6 +52,7 @@ import p_qiniu from './qiniu'
import p_silicon from './silicon'
import p_sophnet from './sophnet'
import p_stepfun from './stepfun'
import p_tencent_cloud_ti from './tencent-cloud-ti'
import p_together from './together'
import p_tokenhub from './tokenhub'
import type { Provider } from './types'
@@ -76,7 +77,6 @@ export const PROVIDERS: Provider[] = [
p_aionly,
p_burncloud,
p_302ai,
p_cephalon,
p_lanyun,
p_ph8,
p_sophnet,
@@ -113,6 +113,8 @@ export const PROVIDERS: Provider[] = [
p_perplexity,
p_modelscope,
p_xirang,
p_hunyuan,
p_tencent_cloud_ti,
p_tokenhub,
p_baidu_cloud,
p_gpustack,

View File

@@ -237,6 +237,9 @@ export default openaiCompatible({
apiModelId: 'qwen/qwen3-235b-a22b-thinking-2507',
limits: { maxOutputTokens: 114688 },
modelId: 'qwen3-235b-a22b-thinking',
// The canonicalizer folds `-thinking` into the base line, so this id is no longer a base row —
// keep the distinctly-priced thinking SKU selectable as a named standalone.
name: 'Qwen3 235B A22B Thinking 2507',
modelVariants: ['235b'],
pricing: { input: { currency: 'CNY', perMillionTokens: 2 }, output: { currency: 'CNY', perMillionTokens: 20 } }
},

View File

@@ -0,0 +1,21 @@
import { defineProvider } from './types'
export default defineProvider({
id: 'tencent-cloud-ti',
name: 'Tencent Cloud TI',
defaultChatEndpoint: 'openai-chat-completions',
endpointConfigs: {
'openai-chat-completions': {
adapterFamily: 'openai-compatible',
baseUrl: 'https://api.lkeap.cloud.tencent.com'
}
},
metadata: {
website: {
apiKey: 'https://console.cloud.tencent.com/lkeap/api',
docs: 'https://cloud.tencent.com/document/product/1772',
models: 'https://console.cloud.tencent.com/tione/v2/aimarket',
official: 'https://cloud.tencent.com/product/ti'
}
}
})

View File

@@ -10,10 +10,13 @@ import { CURRENCY, objectValues } from './enums'
export const ModelIdSchema = z.string().min(1)
export const ProviderIdSchema = z.string().min(1)
/** Version string (e.g., "2026-03-09" or "2026.03.09") */
export const VersionSchema = z.string().regex(/^\d{4}[-./]\d{2}[-./]\d{2}$/, {
message: 'Version must be a date-like string (e.g., YYYY-MM-DD or YYYY.MM.DD)'
})
/**
* Opaque change-detection token for a generated artifact. The generator stamps it with a hash of the
* artifact's content, so equal content ⇒ equal version and ANY content change ⇒ new version — that's
* the contract seeders rely on (`SeedRunner` skips a seeder whose journal version matches). Older
* artifacts carried a generation date; treat the value as opaque, never parse it.
*/
export const VersionSchema = z.string().min(1)
/** ISO 8601 datetime timestamp */
export const ISOTimestampSchema = z.iso.datetime()

View File

@@ -80,7 +80,9 @@ export const HYPHEN_VARIANT_SUFFIXES = [
'-low',
'-high',
'-minimal',
'-medium',
// NOTE: `-medium` is intentionally NOT here — it's a real model-tier name (`mistral-medium`,
// `devstral-medium`), so stripping it as a reasoning-effort variant eats the tier and produces
// bogus stems (`mistral`, `devstral`).
'-nothink',
'-no-think',
'-ssvip',
@@ -251,6 +253,21 @@ export function stripDateSnapshot(modelId: string): string {
return modelId.replace(/@.*$/, '').replace(DATE_SNAPSHOT_PATTERN, '')
}
/**
* Iterate variant → quantization → date stripping to a fixpoint. A single pass is order-dependent —
* a trailing date shields an inner variant from the `endsWith` check (`…-thinking-2507` only exposes
* `-thinking` after the date is gone) — so without the loop canonicalization is not idempotent.
* SHARED by the build canonicalizer (`scripts/canonicalize.ts`) and the runtime resolver below.
*/
export function stripVariantQuantDateSuffixes(modelId: string): string {
let result = modelId
for (;;) {
const next = stripDateSnapshot(stripQuantization(stripVariantSuffixes(result)))
if (next === result) return result
result = next
}
}
export function extractParameterSize(modelId: string): string | undefined {
const match = modelId.match(PARAMETER_SIZE_PATTERN)
return match ? match[1].toLowerCase() : undefined
@@ -273,10 +290,13 @@ export function normalizeModelId(modelId: string): string {
baseName = stripBedrockVendorPrefix(baseName)
baseName = stripBedrockRevision(baseName)
baseName = expandKnownPrefixes(baseName)
baseName = stripVariantSuffixes(baseName)
baseName = stripQuantization(baseName)
baseName = stripDateSnapshot(baseName)
baseName = stripParameterSize(baseName)
// Parameter size joins the fixpoint loop too: stripping `-30b` can expose a variant suffix and
// vice versa, so iterate the whole strip stage until stable.
for (;;) {
const next = stripParameterSize(stripVariantQuantDateSuffixes(baseName))
if (next === baseName) break
baseName = next
}
baseName = normalizeVersionSeparators(baseName)
// Underscores are an interchangeable separator (HF-style `bce-embedding-base_v1`). The catalog folds
// them to `-` (every base id is dash-only), so fold here too or such ids would never resolve.