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:
8 files changed
+9629
-2411
No files matched your search
+8886
-2146
File diff suppressed because it is too large.
Load diff
+592
@@ -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:
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
requests
|
|
||||||
ruamel.yaml
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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());
|
||||||
|
|||||||
Reference in new issue
Block a user