feat: generate the model catalog from live API data

Add a hidden --generate-models flag that rebuilds models.yaml from
models.dev and OpenRouter merged with the hand-owned quirks overlay,
regenerate the committed catalog with it (393 -> 1090 models), and
retire the Python updater. GitHub becomes fully quirks-owned since
upstream only tracks Copilot ids, legacy ids upstream dropped are
preserved as hand-owned quirks entries, and non-chat model families
(image/tts/audio/realtime) are excluded from generation.
This commit is contained in:
Dark-Alex-17 committed 2026-09-02 13:38:56 -06:00
1 parent 61c4037033
commit 46c8561173
8 files changed
+9629 -2411

No files matched your search

+8886 -2146
View File
File diff suppressed because it is too large. Load diff
+592
View File
@@ -9,6 +9,11 @@
# entries are grouped under a `# legacy — pruning candidates` comment. # entries are grouped under a `# legacy — pruning candidates` comment.
- provider: openai - provider: openai
exclude:
- "text-embedding-*"
- "gpt-image-*"
- "chatgpt-image-*"
- "gpt-realtime*"
rules: rules:
- match: "gpt-5.6-*" - match: "gpt-5.6-*"
default_reasoning_effort: medium default_reasoning_effort: medium
@@ -118,8 +123,39 @@
max_tokens: null max_tokens: null
temperature: null temperature: null
top_p: null top_p: null
- name: chat-latest
max_input_tokens: 400000
max_output_tokens: 128000
input_price: 5
output_price: 30
supports_vision: true
supports_function_calling: true
- name: gpt-5.1-chat-latest
max_input_tokens: 400000
max_output_tokens: 128000
input_price: 1.25
output_price: 10
supports_vision: true
supports_function_calling: true
reasoning_levels: [none, low, medium, high]
default_reasoning_effort: none
- name: gpt-5-chat-latest
max_input_tokens: 400000
max_output_tokens: 128000
input_price: 1.25
output_price: 10
supports_vision: true
supports_function_calling: true
reasoning_levels: [minimal, low, medium, high]
default_reasoning_effort: medium
- provider: gemini - provider: gemini
exclude:
- "gemini-embedding-*"
- "veo-*"
- "*-tts"
- "*-tts-preview"
- "*-image-preview"
rules: rules:
- match: "gemini-3.6-flash" - match: "gemini-3.6-flash"
default_reasoning_effort: medium default_reasoning_effort: medium
@@ -135,6 +171,7 @@
default_reasoning_effort: high default_reasoning_effort: high
- match: "gemini-2.5-pro" - match: "gemini-2.5-pro"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high]
models: models:
- name: text-embedding-004 - name: text-embedding-004
type: embedding type: embedding
@@ -142,6 +179,18 @@
max_tokens_per_chunk: 2048 max_tokens_per_chunk: 2048
default_chunk_size: 1500 default_chunk_size: 1500
max_batch_size: 100 max_batch_size: 100
# legacy — pruning candidates
- name: gemini-3-pro-preview
max_input_tokens: 1048576
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, high]
default_reasoning_effort: high
- name: gemma-3-27b-it
max_input_tokens: 131072
max_output_tokens: 8192
input_price: 0
output_price: 0
- provider: claude - provider: claude
rules: rules:
@@ -246,8 +295,28 @@
thinking: thinking:
type: enabled type: enabled
budget_tokens: 16000 budget_tokens: 16000
models:
# legacy — pruning candidates
- name: claude-opus-4-1-20250805
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
input_price: 15
output_price: 75
supports_vision: true
supports_function_calling: true
- name: claude-opus-4-20250514
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
input_price: 15
output_price: 75
supports_vision: true
supports_function_calling: true
- provider: mistral - provider: mistral
exclude:
- "voxtral-*"
models: models:
- name: mistral-embed - name: mistral-embed
type: embedding type: embedding
@@ -255,6 +324,21 @@
input_price: 0.1 input_price: 0.1
max_tokens_per_chunk: 8092 max_tokens_per_chunk: 8092
default_chunk_size: 2000 default_chunk_size: 2000
# legacy — pruning candidates
- name: magistral-small-latest
max_input_tokens: 131072
input_price: 0.5
output_price: 1.5
- name: devstral-small-latest
max_input_tokens: 262144
input_price: 0.1
output_price: 0.3
supports_function_calling: true
- name: ministral-14b-latest
max_input_tokens: 262144
input_price: 0.2
output_price: 0.2
supports_function_calling: true
- provider: ai21 - provider: ai21
models: models:
@@ -300,6 +384,8 @@
max_input_tokens: 4096 max_input_tokens: 4096
- provider: xai - provider: xai
exclude:
- "grok-imagine-*"
oauth: oauth:
client_id: b1a00492-073a-47ea-816f-4c329264a828 client_id: b1a00492-073a-47ea-816f-4c329264a828
authorize_url: https://auth.x.ai/oauth2/authorize authorize_url: https://auth.x.ai/oauth2/authorize
@@ -337,8 +423,71 @@
output_price: 2.5 output_price: 2.5
max_input_tokens: 1000000 max_input_tokens: 1000000
supports_function_calling: true supports_function_calling: true
- name: grok-4-1-fast-non-reasoning
max_input_tokens: 1000000
input_price: 0.2
output_price: 0.5
supports_function_calling: true
- name: grok-4-1-fast-reasoning
max_input_tokens: 1000000
input_price: 0.2
output_price: 0.5
supports_function_calling: true
- name: grok-code-fast-1
max_input_tokens: 256000
input_price: 0.2
output_price: 1.5
supports_function_calling: true
- name: grok-3
max_input_tokens: 131072
input_price: 3
output_price: 15
supports_function_calling: true
- name: grok-3-mini
max_input_tokens: 131072
input_price: 0.3
output_price: 0.5
supports_function_calling: true
- provider: perplexity
models:
# legacy — pruning candidates
- name: sonar-reasoning
max_input_tokens: 128000
input_price: 1
output_price: 5
- provider: groq
exclude:
- "whisper-*"
models:
# legacy — pruning candidates
- name: meta-llama/llama-4-maverick-17b-128e-instruct
max_input_tokens: 131072
input_price: 0
output_price: 0
supports_vision: true
supports_function_calling: true
- name: meta-llama/llama-4-scout-17b-16e-instruct
max_input_tokens: 131072
input_price: 0
output_price: 0
supports_vision: true
supports_function_calling: true
- name: moonshotai/kimi-k2-instruct-0905
max_input_tokens: 262144
input_price: 0
output_price: 0
supports_function_calling: true
- name: qwen/qwen3-32b
max_input_tokens: 131072
input_price: 0
output_price: 0
- provider: vertexai - provider: vertexai
exclude:
- "gemini-embedding-*"
- "*-tts"
rules: rules:
- match: "claude-*" - match: "claude-*"
require_max_tokens: true require_max_tokens: true
@@ -354,8 +503,10 @@
default_reasoning_effort: high default_reasoning_effort: high
- match: "gemini-2.5-flash" - match: "gemini-2.5-flash"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [low, medium, high]
- match: "gemini-2.5-pro" - match: "gemini-2.5-pro"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high]
- match: "claude-opus-5" - match: "claude-opus-5"
default_reasoning_effort: high default_reasoning_effort: high
- match: "claude-fable-*" - match: "claude-fable-*"
@@ -449,6 +600,109 @@
max_tokens_per_chunk: 2048 max_tokens_per_chunk: 2048
default_chunk_size: 1500 default_chunk_size: 1500
max_batch_size: 5 max_batch_size: 5
# legacy — pruning candidates
- name: gemini-3-pro-preview
max_input_tokens: 1048576
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, high]
default_reasoning_effort: high
- name: claude-opus-5
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 5
output_price: 25
supports_function_calling: true
supports_vision: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: claude-fable-5-1
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 10
output_price: 50
supports_function_calling: true
supports_vision: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: claude-fable-5
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 10
output_price: 50
supports_function_calling: true
supports_vision: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: claude-opus-4-8
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 5
output_price: 25
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: claude-opus-4-7
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 5
output_price: 25
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: claude-opus-4-6
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
input_price: 5
output_price: 25
supports_vision: true
supports_function_calling: true
- name: claude-sonnet-5
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 3
output_price: 15
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: claude-sonnet-4-6
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
input_price: 3
output_price: 15
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, medium, high, max]
default_reasoning_effort: high
- name: claude-3-5-haiku@20241022
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
input_price: 0.8
output_price: 4
supports_vision: true
supports_function_calling: true
- name: mistral-small-2503
max_input_tokens: 32000
input_price: 0.1
output_price: 0.3
supports_function_calling: true
- name: codestral-2501
max_input_tokens: 256000
input_price: 0.3
output_price: 0.9
supports_function_calling: true
- provider: bedrock - provider: bedrock
rules: rules:
@@ -559,6 +813,36 @@
max_tokens_per_chunk: 512 max_tokens_per_chunk: 512
default_chunk_size: 1000 default_chunk_size: 1000
max_batch_size: 96 max_batch_size: 96
# legacy — pruning candidates
- name: us.meta.llama3-3-70b-instruct-v1:0
max_input_tokens: 131072
max_output_tokens: 8192
require_max_tokens: true
input_price: 0.72
output_price: 0.72
supports_function_calling: true
- name: us.amazon.nova-premier-v1:0
max_input_tokens: 300000
max_output_tokens: 5120
input_price: 2.5
output_price: 12.5
- name: us.amazon.nova-pro-v1:0
max_input_tokens: 300000
max_output_tokens: 5120
input_price: 0.8
output_price: 3.2
supports_vision: true
- name: us.amazon.nova-lite-v1:0
max_input_tokens: 300000
max_output_tokens: 5120
input_price: 0.06
output_price: 0.24
supports_vision: true
- name: us.amazon.nova-micro-v1:0
max_input_tokens: 128000
max_output_tokens: 5120
input_price: 0.035
output_price: 0.14
- provider: cloudflare - provider: cloudflare
rules: rules:
@@ -571,6 +855,13 @@
max_tokens_per_chunk: 512 max_tokens_per_chunk: 512
default_chunk_size: 1000 default_chunk_size: 1000
max_batch_size: 100 max_batch_size: 100
# legacy — pruning candidates
- name: '@cf/google/gemma-3-12b-it'
max_input_tokens: 131072
max_output_tokens: 2048
require_max_tokens: true
input_price: 0
output_price: 0
- provider: ernie - provider: ernie
models: models:
@@ -609,6 +900,8 @@
input_price: 0.07 input_price: 0.07
- provider: qianwen - provider: qianwen
exclude:
- "*-realtime"
rules: rules:
- match: "qwen3.5-plus" - match: "qwen3.5-plus"
patch: patch:
@@ -646,6 +939,40 @@
max_tokens_per_chunk: 8192 max_tokens_per_chunk: 8192
default_chunk_size: 2000 default_chunk_size: 2000
max_batch_size: 10 max_batch_size: 10
# legacy — pruning candidates
- name: qwen3-vl-flash
max_input_tokens: 262144
supports_vision: true
- name: qwen3-235b-a22b-instruct-2507
max_input_tokens: 131072
input_price: 0.28
output_price: 1.12
supports_function_calling: true
- name: qwen3-235b-a22b-thinking-2507
max_input_tokens: 131072
input_price: 0.28
output_price: 2.8
- name: qwen3-30b-a3b-instruct-2507
max_input_tokens: 131072
input_price: 0.105
output_price: 0.42
supports_function_calling: true
- name: qwen3-30b-a3b-thinking-2507
max_input_tokens: 131072
input_price: 0.105
output_price: 1.05
- name: qwen3-vl-32b-instruct
max_input_tokens: 131072
input_price: 0.28
output_price: 1.12
supports_vision: true
- name: qwen3-vl-8b-instruct
max_input_tokens: 131072
input_price: 0.07
output_price: 0.28
supports_vision: true
- name: qwen3-coder-next
max_input_tokens: 262144
- provider: hunyuan - provider: hunyuan
models: models:
@@ -671,6 +998,21 @@
default_chunk_size: 1000 default_chunk_size: 1000
max_batch_size: 100 max_batch_size: 100
- provider: deepseek
models:
# legacy — pruning candidates
- name: deepseek-chat
max_input_tokens: 64000
max_output_tokens: 8192
input_price: 0.56
output_price: 1.68
supports_function_calling: true
- name: deepseek-reasoner
max_input_tokens: 64000
max_output_tokens: 32768
input_price: 0.56
output_price: 1.68
- provider: zhipuai - provider: zhipuai
variants: variants:
- match: "glm-5" - match: "glm-5"
@@ -698,6 +1040,46 @@
type: reranker type: reranker
max_input_tokens: 4096 max_input_tokens: 4096
input_price: 0.112 input_price: 0.112
# legacy — pruning candidates
- name: glm-4.6v-flash
max_input_tokens: 65536
input_price: 0
output_price: 0
supports_vision: true
- provider: minimax
models:
# legacy — pruning candidates
- name: minimax-m3
max_input_tokens: 1000000
input_price: 4.2
output_price: 16.8
supports_function_calling: true
- name: minimax-m2.7
max_input_tokens: 204800
input_price: 0.294
output_price: 1.176
supports_function_calling: true
- name: minimax-m2.5
max_input_tokens: 204800
input_price: 0.294
output_price: 1.176
supports_function_calling: true
- name: minimax-m2.5-highspeed
max_input_tokens: 204800
input_price: 0.588
output_price: 2.352
supports_function_calling: true
- name: minimax-m2.1
max_input_tokens: 204800
input_price: 0.294
output_price: 1.176
supports_function_calling: true
- name: minimax-m2.1-highspeed
max_input_tokens: 204800
input_price: 0.588
output_price: 2.352
supports_function_calling: true
- provider: openrouter - provider: openrouter
rules: rules:
@@ -705,54 +1087,76 @@
require_max_tokens: true require_max_tokens: true
- match: "openai/gpt-5.6-*" - match: "openai/gpt-5.6-*"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [none, low, medium, high, xhigh, max]
- match: "openai/gpt-5.5" - match: "openai/gpt-5.5"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [none, low, medium, high, xhigh]
- match: "openai/gpt-5.5-pro" - match: "openai/gpt-5.5-pro"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [medium, high, xhigh]
- match: "openai/gpt-5.4" - match: "openai/gpt-5.4"
default_reasoning_effort: none default_reasoning_effort: none
reasoning_levels: [none, low, medium, high, xhigh]
- match: "openai/gpt-5.4-pro" - match: "openai/gpt-5.4-pro"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [medium, high, xhigh]
- match: "openai/gpt-5.4-mini" - match: "openai/gpt-5.4-mini"
default_reasoning_effort: none default_reasoning_effort: none
reasoning_levels: [none, low, medium, high, xhigh]
- match: "openai/gpt-5.4-nano" - match: "openai/gpt-5.4-nano"
default_reasoning_effort: none default_reasoning_effort: none
reasoning_levels: [none, low, medium, high, xhigh]
- match: "openai/gpt-5.3-codex" - match: "openai/gpt-5.3-codex"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [low, medium, high, xhigh]
- match: "openai/gpt-5.2" - match: "openai/gpt-5.2"
default_reasoning_effort: none default_reasoning_effort: none
reasoning_levels: [none, low, medium, high, xhigh]
- match: "openai/gpt-5.2-pro" - match: "openai/gpt-5.2-pro"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [medium, high, xhigh]
- match: "openai/gpt-5" - match: "openai/gpt-5"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [minimal, low, medium, high]
- match: "google/gemini-3.5-flash" - match: "google/gemini-3.5-flash"
default_reasoning_effort: medium default_reasoning_effort: medium
reasoning_levels: [minimal, low, medium, high]
- match: "google/gemini-3-flash-preview" - match: "google/gemini-3-flash-preview"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [minimal, low, medium, high]
- match: "google/gemini-3.1-flash-lite" - match: "google/gemini-3.1-flash-lite"
default_reasoning_effort: minimal default_reasoning_effort: minimal
reasoning_levels: [minimal, low, medium, high]
- match: "google/gemini-3.1-pro-preview" - match: "google/gemini-3.1-pro-preview"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high]
- match: "google/gemini-3-pro-preview" - match: "google/gemini-3-pro-preview"
default_reasoning_effort: high default_reasoning_effort: high
- match: "google/gemini-2.5-flash" - match: "google/gemini-2.5-flash"
default_reasoning_effort: low default_reasoning_effort: low
reasoning_levels: [low, medium, high]
- match: "google/gemini-2.5-pro" - match: "google/gemini-2.5-pro"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high]
- match: "anthropic/claude-opus-5" - match: "anthropic/claude-opus-5"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high, xhigh, max]
- match: "anthropic/claude-fable-*" - match: "anthropic/claude-fable-*"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high, xhigh, max]
- match: "anthropic/claude-opus-4-8" - match: "anthropic/claude-opus-4-8"
default_reasoning_effort: high default_reasoning_effort: high
- match: "anthropic/claude-opus-4-7" - match: "anthropic/claude-opus-4-7"
default_reasoning_effort: high default_reasoning_effort: high
- match: "anthropic/claude-opus-4.6" - match: "anthropic/claude-opus-4.6"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high, max]
- match: "anthropic/claude-sonnet-5" - match: "anthropic/claude-sonnet-5"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high, xhigh, max]
- match: "anthropic/claude-sonnet-4.6" - match: "anthropic/claude-sonnet-4.6"
default_reasoning_effort: high default_reasoning_effort: high
reasoning_levels: [low, medium, high, max]
- match: "perplexity/sonar-reasoning-pro" - match: "perplexity/sonar-reasoning-pro"
patch: patch:
body: body:
@@ -761,9 +1165,105 @@
patch: patch:
body: body:
include_reasoning: true include_reasoning: true
models:
# legacy — pruning candidates
- name: google/gemini-3-pro-preview
max_input_tokens: 1048576
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, high]
default_reasoning_effort: high
- name: google/gemini-2.0-flash-001
max_input_tokens: 1000000
input_price: 0.15
output_price: 0.6
supports_vision: true
supports_function_calling: true
- name: google/gemini-2.0-flash-lite-001
max_input_tokens: 1048576
input_price: 0.075
output_price: 0.3
supports_vision: true
supports_function_calling: true
- name: anthropic/claude-fable-5-1
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 10
output_price: 50
supports_function_calling: true
supports_vision: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: anthropic/claude-opus-4-8
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 5
output_price: 25
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: anthropic/claude-opus-4-7
max_input_tokens: 1000000
max_output_tokens: 128000
require_max_tokens: true
input_price: 5
output_price: 25
supports_vision: true
supports_function_calling: true
reasoning_levels: [low, medium, high, xhigh, max]
default_reasoning_effort: high
- name: mistralai/devstral-small
max_input_tokens: 131072
input_price: 0.07
output_price: 0.28
supports_function_calling: true
- name: ai21/jamba-large-1.7
max_input_tokens: 256000
input_price: 2
output_price: 8
supports_function_calling: true
- name: x-ai/grok-4.1-fast
max_input_tokens: 2000000
input_price: 0.2
output_price: 0.5
supports_function_calling: true
- name: x-ai/grok-code-fast-1
max_input_tokens: 256000
input_price: 0.2
output_price: 1.5
supports_function_calling: true
# Fully hand-owned: upstream catalogs track GitHub Copilot ids, which do not
# match the GitHub Models API ids coyote uses.
- provider: github - provider: github
models: models:
- name: gpt-5
max_input_tokens: 400000
max_output_tokens: 128000
supports_vision: true
supports_function_calling: true
- name: gpt-5-mini
max_input_tokens: 400000
max_output_tokens: 128000
supports_vision: true
supports_function_calling: true
- name: gpt-5-nano
max_input_tokens: 400000
max_output_tokens: 128000
supports_vision: true
supports_function_calling: true
- name: gpt-4.1
max_input_tokens: 1047576
max_output_tokens: 32768
supports_vision: true
supports_function_calling: true
- name: gpt-4o
max_input_tokens: 128000
max_output_tokens: 16384
supports_function_calling: true
- name: text-embedding-3-large - name: text-embedding-3-large
type: embedding type: embedding
max_tokens_per_chunk: 8191 max_tokens_per_chunk: 8191
@@ -774,6 +1274,23 @@
max_tokens_per_chunk: 8191 max_tokens_per_chunk: 8191
default_chunk_size: 2000 default_chunk_size: 2000
max_batch_size: 100 max_batch_size: 100
- name: llama-4-maverick-17b-128e-instruct-fp8
max_input_tokens: 1048576
supports_vision: true
- name: llama-4-scout-17b-16e-instruct
max_input_tokens: 327680
supports_vision: true
- name: llama-3.3-70b-instruct
max_input_tokens: 131072
- name: mistral-medium-2505
max_input_tokens: 131072
supports_function_calling: true
- name: mistral-small-2503
max_input_tokens: 131072
supports_function_calling: true
- name: codestral-2501
max_input_tokens: 256000
supports_function_calling: true
- name: cohere-embed-v3-english - name: cohere-embed-v3-english
type: embedding type: embedding
max_tokens_per_chunk: 512 max_tokens_per_chunk: 512
@@ -784,6 +1301,24 @@
max_tokens_per_chunk: 512 max_tokens_per_chunk: 512
default_chunk_size: 1000 default_chunk_size: 1000
max_batch_size: 96 max_batch_size: 96
- name: deepseek-r1-0528
max_input_tokens: 163840
- name: deepseek-v3-0324
max_input_tokens: 163840
- name: mai-ds-r1
max_input_tokens: 163840
- name: phi-4
max_input_tokens: 16384
- name: phi-4-mini-instruct
max_input_tokens: 131072
- name: phi-4-reasoning
max_input_tokens: 33792
- name: phi-4-mini-reasoning
max_input_tokens: 131072
- name: grok-3
max_input_tokens: 131072
- name: grok-3-mini
max_input_tokens: 131072
- provider: deepinfra - provider: deepinfra
models: models:
@@ -817,6 +1352,63 @@
max_tokens_per_chunk: 512 max_tokens_per_chunk: 512
default_chunk_size: 1000 default_chunk_size: 1000
max_batch_size: 100 max_batch_size: 100
# legacy — pruning candidates
- name: Qwen/Qwen3-Max-Thinking
max_input_tokens: 262144
input_price: 1.2
output_price: 6
supports_function_calling: true
- name: Qwen/Qwen3-Next-80B-A3B-Thinking
max_input_tokens: 262144
input_price: 0.14
output_price: 1.4
- name: Qwen/Qwen3-235B-A22B-Thinking-2507
max_input_tokens: 131072
input_price: 0.13
output_price: 0.6
- name: Qwen/Qwen3-Coder-480B-A35B-Instruct
max_input_tokens: 131072
input_price: 0.4
output_price: 1.6
supports_function_calling: true
- name: Qwen/Qwen3-Coder-30B-A3B-Instruct
max_input_tokens: 262144
input_price: 0.07
output_price: 0.27
supports_function_calling: true
- name: Qwen/Qwen3-VL-8B-Instruct
max_input_tokens: 262144
input_price: 0.18
output_price: 0.69
supports_vision: true
- name: google/gemma-3-27b-it
max_input_tokens: 131072
input_price: 0.1
output_price: 0.2
- name: mistralai/Mistral-Small-3.2-24B-Instruct-2506
max_input_tokens: 32768
input_price: 0.06
output_price: 0.12
- name: moonshotai/Kimi-K2-Instruct-0905
max_input_tokens: 262144
input_price: 0.5
output_price: 2.0
supports_function_calling: true
- name: moonshotai/Kimi-K2-Thinking
max_input_tokens: 262144
input_price: 0.55
output_price: 2.5
supports_function_calling: true
- name: MiniMaxAI/MiniMax-M2.1
max_input_tokens: 196608
input_price: 0.27
output_price: 0.95
supports_function_calling: true
- name: zai-org/GLM-4.6V
max_input_tokens: 131072
input_price: 0.3
output_price: 0.9
supports_vision: true
- provider: jina - provider: jina
models: models:
-2
View File
@@ -1,2 +0,0 @@
requests
ruamel.yaml
-255
View File
@@ -1,255 +0,0 @@
import requests
import sys
import re
import json
# Provider mapping from models.yaml to OpenRouter prefixes
PROVIDER_MAPPING = {
"openai": "openai",
"claude": "anthropic",
"gemini": "google",
"mistral": "mistralai",
"cohere": "cohere",
"perplexity": "perplexity",
"xai": "x-ai",
"openrouter": "openrouter",
"ai21": "ai21",
"deepseek": "deepseek",
"moonshot": "moonshotai",
"qianwen": "qwen",
"zhipuai": "zhipuai",
"minimax": "minimax",
"vertexai": "google",
"groq": "groq",
"bedrock": "amazon",
"hunyuan": "tencent",
"ernie": "baidu",
"github": "github",
}
def fetch_openrouter_models():
print("Fetching models from OpenRouter...")
try:
response = requests.get("https://openrouter.ai/api/v1/models")
response.raise_for_status()
data = response.json()["data"]
print(f"Fetched {len(data)} models.")
return data
except Exception as e:
print(f"Error fetching models: {e}")
sys.exit(1)
def get_openrouter_model(models_data, provider_prefix, model_name, is_openrouter_provider=False):
if is_openrouter_provider:
# For openrouter provider, the model_name in yaml is usually the full ID
for model in models_data:
if model["id"] == model_name:
return model
return None
expected_id = f"{provider_prefix}/{model_name}"
# 1. Try exact match on ID
for model in models_data:
if model["id"] == expected_id:
return model
# 2. Try match by suffix
for model in models_data:
if model["id"].split("/")[-1] == model_name:
if model["id"].startswith(f"{provider_prefix}/"):
return model
return None
def format_price(price_per_token):
if price_per_token is None:
return None
try:
price_per_1m = float(price_per_token) * 1_000_000
if price_per_1m.is_integer():
return str(int(price_per_1m))
else:
return str(round(price_per_1m, 4))
except:
return None
def get_indentation(line):
return len(line) - len(line.lstrip())
def process_model_block(block_lines, current_provider, or_models):
if not block_lines:
return []
# 1. Identify model name and indentation
name_line = block_lines[0]
name_match = re.match(r"^(\s*)-\s*name:\s*(.+)$", name_line)
if not name_match:
return block_lines
name_indent_str = name_match.group(1)
model_name = name_match.group(2).strip()
# 2. Find OpenRouter model
or_prefix = PROVIDER_MAPPING.get(current_provider)
is_openrouter_provider = (current_provider == "openrouter")
if not or_prefix and not is_openrouter_provider:
return block_lines
or_model = get_openrouter_model(or_models, or_prefix, model_name, is_openrouter_provider)
if not or_model:
return block_lines
print(f" Updating {model_name}...")
# 3. Prepare updates
updates = {}
# Pricing
pricing = or_model.get("pricing", {})
p_in = format_price(pricing.get("prompt"))
p_out = format_price(pricing.get("completion"))
if p_in: updates["input_price"] = p_in
if p_out: updates["output_price"] = p_out
# Context
ctx = or_model.get("context_length")
if ctx: updates["max_input_tokens"] = str(ctx)
max_out = None
if "top_provider" in or_model and or_model["top_provider"]:
max_out = or_model["top_provider"].get("max_completion_tokens")
if max_out: updates["max_output_tokens"] = str(max_out)
# Capabilities
arch = or_model.get("architecture", {})
modality = arch.get("modality", "")
if "image" in modality:
updates["supports_vision"] = "true"
# 4. Detect field indentation
field_indent_str = None
existing_fields = {} # key -> line_index
for i, line in enumerate(block_lines):
if i == 0: continue # Skip name line
# Skip comments
if line.strip().startswith("#"):
continue
# Look for "key: value"
m = re.match(r"^(\s*)([\w_-]+):", line)
if m:
indent = m.group(1)
key = m.group(2)
# Must be deeper than name line
if len(indent) > len(name_indent_str):
if field_indent_str is None:
field_indent_str = indent
existing_fields[key] = i
if field_indent_str is None:
field_indent_str = name_indent_str + " "
# 5. Apply updates
new_block = list(block_lines)
# Update existing fields
for key, value in updates.items():
if key in existing_fields:
idx = existing_fields[key]
# Preserve original key indentation exactly
original_line = new_block[idx]
m = re.match(r"^(\s*)([\w_-]+):", original_line)
if m:
current_indent = m.group(1)
new_block[idx] = f"{current_indent}{key}: {value}\n"
# Insert missing fields
# Insert after the name line
insertion_idx = 1
for key, value in updates.items():
if key not in existing_fields:
new_line = f"{field_indent_str}{key}: {value}\n"
new_block.insert(insertion_idx, new_line)
insertion_idx += 1
return new_block
def main():
or_models = fetch_openrouter_models()
print("Reading models.yaml...")
with open("models.yaml", "r") as f:
lines = f.readlines()
new_lines = []
current_provider = None
i = 0
while i < len(lines):
line = lines[i]
# Check for provider
# - provider: name
p_match = re.match(r"^\s*-?\s*provider:\s*(.+)$", line)
if p_match:
current_provider = p_match.group(1).strip()
new_lines.append(line)
i += 1
continue
# Check for model start
# - name: ...
m_match = re.match(r"^(\s*)-\s*name:\s*.+$", line)
if m_match:
# Start of a model block
start_indent = len(m_match.group(1))
# Collect block lines
block_lines = [line]
j = i + 1
while j < len(lines):
next_line = lines[j]
stripped = next_line.strip()
# If empty or comment, include it
if not stripped or stripped.startswith("#"):
block_lines.append(next_line)
j += 1
continue
# Check indentation
next_indent = get_indentation(next_line)
# If indentation is greater, it's part of the block (property)
if next_indent > start_indent:
block_lines.append(next_line)
j += 1
continue
# If indentation is equal or less, it's the end of the block
break
# Process the block
processed_block = process_model_block(block_lines, current_provider, or_models)
new_lines.extend(processed_block)
# Advance i
i = j
continue
# Otherwise, just a regular line
new_lines.append(line)
i += 1
print("Saving models.yaml...")
with open("models.yaml", "w") as f:
f.writelines(new_lines)
print("Done.")
if __name__ == "__main__":
main()
+24 -1
View File
@@ -12,6 +12,7 @@ use clap_complete::ArgValueCompleter;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::{Read, stdin}; use std::io::{Read, stdin};
use std::path::PathBuf;
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] #[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpTransportArg { pub enum McpTransportArg {
@@ -53,7 +54,7 @@ pub enum McpScopeArg {
"install_builtins", "sync_models", "list_models", "list_roles", "install_builtins", "sync_models", "list_models", "list_roles",
"list_sessions", "list_agents", "list_rags", "list_macros", "list_sessions", "list_agents", "list_rags", "list_macros",
"list_skills", "list_bundles", "skill", "tail_logs", "completions", "list_skills", "list_bundles", "skill", "tail_logs", "completions",
"update", "update_bundle", "uninstall", "update", "update_bundle", "uninstall", "generate_models",
]) ])
), ),
group( group(
@@ -253,6 +254,15 @@ pub struct Cli {
/// Sync models updates /// Sync models updates
#[arg(long, help_heading = "Installation & Updates")] #[arg(long, help_heading = "Installation & Updates")]
pub sync_models: bool, pub sync_models: bool,
/// Regenerate the models catalog file from live API data
#[arg(
long,
hide = true,
value_name = "PATH",
num_args = 0..=1,
default_missing_value = "models.yaml"
)]
pub generate_models: Option<PathBuf>,
/// Update Coyote to the latest release, or to a specific version /// Update Coyote to the latest release, or to a specific version
#[arg(long, value_name = "VERSION", help_heading = "Installation & Updates")] #[arg(long, value_name = "VERSION", help_heading = "Installation & Updates")]
pub update: Option<Option<String>>, pub update: Option<Option<String>>,
@@ -810,6 +820,19 @@ mod tests {
assert!(cli.sync_models); assert!(cli.sync_models);
} }
#[test]
fn parse_generate_models_flag() {
assert_eq!(parse(&[]).generate_models, None);
assert_eq!(
parse(&["--generate-models"]).generate_models,
Some(PathBuf::from("models.yaml"))
);
assert_eq!(
parse(&["--generate-models", "out.yaml"]).generate_models,
Some(PathBuf::from("out.yaml"))
);
}
#[test] #[test]
fn parse_model_with_role() { fn parse_model_with_role() {
let cli = parse(&["-m", "gpt-4o", "-r", "coder"]); let cli = parse(&["-m", "gpt-4o", "-r", "coder"]);
+120 -5
View File
@@ -13,7 +13,6 @@ use std::sync::LazyLock;
pub const QUIRKS_YAML: &str = include_str!("../../quirks.yaml"); pub const QUIRKS_YAML: &str = include_str!("../../quirks.yaml");
#[allow(dead_code)]
pub static ALL_QUIRKS: LazyLock<Vec<ProviderQuirks>> = pub static ALL_QUIRKS: LazyLock<Vec<ProviderQuirks>> =
LazyLock::new(|| serde_yaml::from_str(QUIRKS_YAML).expect("quirks.yaml must be valid")); LazyLock::new(|| serde_yaml::from_str(QUIRKS_YAML).expect("quirks.yaml must be valid"));
@@ -133,8 +132,8 @@ const PROVIDER_SOURCES: [SourceIds; 24] = [
}, },
SourceIds { SourceIds {
provider: "github", provider: "github",
modelsdev: Some("github-copilot"), modelsdev: None,
openrouter: Some("github"), openrouter: None,
}, },
SourceIds { SourceIds {
provider: "deepinfra", provider: "deepinfra",
@@ -162,7 +161,6 @@ pub struct FetchedSources {
/// Fetches both upstream catalogs concurrently. A failed fetch is logged and /// Fetches both upstream catalogs concurrently. A failed fetch is logged and
/// surfaces as `None` rather than an error. /// surfaces as `None` rather than an error.
#[allow(dead_code)]
pub async fn fetch_sources() -> FetchedSources { pub async fn fetch_sources() -> FetchedSources {
let (modelsdev, openrouter) = tokio::join!(fetch(MODELSDEV_API_URL), fetch(OPENROUTER_API_URL)); let (modelsdev, openrouter) = tokio::join!(fetch(MODELSDEV_API_URL), fetch(OPENROUTER_API_URL));
FetchedSources { FetchedSources {
@@ -478,7 +476,6 @@ pub fn build_catalog(sources: &FetchedSources, quirks: &[ProviderQuirks]) -> Cat
/// Like [`build_catalog`], but errors unless every provider was assembled /// Like [`build_catalog`], but errors unless every provider was assembled
/// from live API data (or is quirks-owned by design). /// from live API data (or is quirks-owned by design).
#[allow(dead_code)]
pub fn build_catalog_strict( pub fn build_catalog_strict(
sources: &FetchedSources, sources: &FetchedSources,
quirks: &[ProviderQuirks], quirks: &[ProviderQuirks],
@@ -494,6 +491,32 @@ pub fn build_catalog_strict(
Ok(build.providers) Ok(build.providers)
} }
const GENERATED_HEADER: &str = "# GENERATED by 'coyote --generate-models' — DO NOT hand-edit model data.\n\
# Hand-owned behavior lives in quirks.yaml; this file is the embedded offline fallback.\n";
/// Builds the catalog from live API data and writes it to `path`. Both
/// upstream sources must respond and every provider must fully resolve;
/// identical source data always produces identical bytes.
pub async fn generate_models_file(path: &std::path::Path) -> Result<()> {
let sources = fetch_sources().await;
if sources.modelsdev.is_none() || sources.openrouter.is_none() {
bail!(
"generating the models file requires both catalog sources; see the log for the failed fetch"
);
}
let providers = build_catalog_strict(&sources, &ALL_QUIRKS)?;
let model_count: usize = providers.iter().map(|provider| provider.models.len()).sum();
let yaml = serde_yaml::to_string(&providers)?;
std::fs::write(path, format!("{GENERATED_HEADER}{yaml}"))?;
println!(
"Wrote {} models across {} providers to {}",
model_count,
providers.len(),
path.display()
);
Ok(())
}
/// Fraction of `old` names that also appear in `new`; empty `old` yields 1.0. /// Fraction of `old` names that also appear in `new`; empty `old` yields 1.0.
/// Used to guard against upstream id-shape drift: a low overlap means the /// Used to guard against upstream id-shape drift: a low overlap means the
/// upstream catalog renamed its models out from under us. /// upstream catalog renamed its models out from under us.
@@ -1333,6 +1356,8 @@ rules:
models: [{name: e}] models: [{name: e}]
- provider: hunyuan - provider: hunyuan
models: [{name: h}] models: [{name: h}]
- provider: github
models: [{name: g}]
- provider: jina - provider: jina
models: [{name: j}] models: [{name: j}]
- provider: voyageai - provider: voyageai
@@ -1358,6 +1383,96 @@ rules:
assert_eq!(name_overlap(&old, &[]), 0.0); assert_eq!(name_overlap(&old, &[]), 0.0);
} }
fn embedded_catalog() -> Vec<ProviderModels> {
serde_yaml::from_str(crate::client::MODELS_YAML).unwrap()
}
fn embedded_provider<'a>(providers: &'a [ProviderModels], name: &str) -> &'a ProviderModels {
providers
.iter()
.find(|provider| provider.provider == name)
.unwrap_or_else(|| panic!("provider '{name}' missing from models.yaml"))
}
#[test]
fn embedded_catalog_invariants() {
let providers = embedded_catalog();
assert!(providers.len() >= 24);
let order: Vec<&str> = providers
.iter()
.map(|provider| provider.provider.as_str())
.collect();
assert_eq!(order, KNOWN_PROVIDERS);
assert!(embedded_provider(&providers, "xai").oauth.is_some());
for name in ["jina", "voyageai"] {
assert!(
embedded_provider(&providers, name)
.models
.iter()
.any(|model| model.model_type == "embedding"),
"{name} lost its embedding models"
);
}
assert!(
embedded_provider(&providers, "cohere")
.models
.iter()
.any(|model| model.model_type == "reranker"),
"cohere lost its reranker models"
);
for provider in &providers {
for model in &provider.models {
// A :thinking model without a patch must at least alias a real
// wire name (qianwen twins invert a base-model patch instead).
if model.name.ends_with(":thinking") {
assert!(
model.patch.is_some() || model.real_name.is_some(),
"{}:{} has neither patch nor real_name",
provider.provider,
model.name
);
}
if model.model_type == "chat" {
let serialized = serde_yaml::to_value(model).unwrap();
assert!(
serialized
.as_mapping()
.unwrap()
.keys()
.any(|key| !matches!(key.as_str(), Some("name" | "type"))),
"{}:{} is an all-default chat entry",
provider.provider,
model.name
);
}
}
}
for model in &embedded_provider(&providers, "claude").models {
if model.model_type == "chat" {
assert!(
model.require_max_tokens,
"claude:{} is missing require_max_tokens",
model.name
);
}
}
}
#[test]
fn embedded_catalog_roundtrip() {
let providers = embedded_catalog();
let yaml = serde_yaml::to_string(&providers).unwrap();
let reparsed: Vec<ProviderModels> = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(reparsed.len(), providers.len());
for (reparsed, original) in reparsed.iter().zip(&providers) {
assert_eq!(reparsed.provider, original.provider);
assert_eq!(reparsed.models.len(), original.models.len());
}
}
/// Guards against upstream id-shape drift by comparing live API model /// Guards against upstream id-shape drift by comparing live API model
/// names against the embedded models.yaml. Run manually: /// names against the embedded models.yaml. Run manually:
/// `cargo test idshape_guard_live -- --ignored --nocapture` /// `cargo test idshape_guard_live -- --ignored --nocapture`
+1 -1
View File
@@ -358,7 +358,7 @@ impl ModelData {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderModels { pub struct ProviderModels {
pub provider: String, pub provider: String,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub oauth: Option<OAuthConfig>, pub oauth: Option<OAuthConfig>,
pub models: Vec<ModelData>, pub models: Vec<ModelData>,
} }
+6 -1
View File
@@ -20,7 +20,7 @@ extern crate log;
use crate::cli::Cli; use crate::cli::Cli;
use crate::client::{ use crate::client::{
ModelType, call_chat_completions, call_chat_completions_streaming, list_models, oauth, ModelType, call_chat_completions, call_chat_completions_streaming, catalog, list_models, oauth,
}; };
use crate::config::instructions::WORKSPACE_INSTRUCTIONS_FILE_NAME; use crate::config::instructions::WORKSPACE_INSTRUCTIONS_FILE_NAME;
use crate::config::{ use crate::config::{
@@ -101,6 +101,7 @@ async fn main() -> Result<()> {
let info_flag = cli.info let info_flag = cli.info
|| cli.sync_models || cli.sync_models
|| cli.generate_models.is_some()
|| cli.list_models || cli.list_models
|| cli.list_roles || cli.list_roles
|| cli.list_agents || cli.list_agents
@@ -295,6 +296,10 @@ async fn run(
return sync_models(&url, abort_signal.clone()).await; return sync_models(&url, abort_signal.clone()).await;
} }
if let Some(path) = &cli.generate_models {
return catalog::generate_models_file(path).await;
}
if cli.list_models { if cli.list_models {
for model in list_models(ctx.app.config.as_ref(), ModelType::Chat) { for model in list_models(ctx.app.config.as_ref(), ModelType::Chat) {
println!("{}", model.id()); println!("{}", model.id());