1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
use once_cell::sync::Lazy;
use std::collections::HashMap;
static MODEL_ALIASES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
let mut m = HashMap::new();
// Stealth preview shorthands for the August 2026 Z.AI GLM-5.3-Flash free
// preview. Sessions record the bare `ox-alpha` (and the router-qualified
// `stealth/ox-alpha` gateways emit), while upstream models.dev tracks the
// canonical free incarnations `opencode-go/ox-alpha-free` and
// `opencode/x-preview-f-free` at $0.00 (both deprecated). Pin the
// shorthand spellings to those canonical keys -- the same "canonical
// first-party key" pattern as `minimax-m3` above -- so the live upstream
// $0 row prices them instead of an unverified reseller guess. The
// qualified `stealth/` form must be explicit because provider-prefix
// stripping does not run alias resolution a second time.
// Sources (accessed 2026-09-03):
// https://openrouter.ai/stealth/ox-alpha ("free to use", ZAI reveal)
// https://docs.z.ai/guides/vlm/glm-5.3-flash ("tested anonymously as ox-alpha")
// https://models.dev/api.json (providers.opencode.models.x-preview-f-free
// and providers.opencode-go.models.ox-alpha-free at input = 0,
// output = 0, cache_read = 0, both deprecated)
m.insert("ox-alpha", "opencode-go/ox-alpha-free");
m.insert("stealth/ox-alpha", "opencode-go/ox-alpha-free");
m.insert("x-preview-f-free", "opencode/x-preview-f-free");
m.insert("big-pickle", "glm-4.7");
m.insert("big pickle", "glm-4.7");
m.insert("bigpickle", "glm-4.7");
m.insert("k2p5", "kimi-k2-thinking");
m.insert("k2-p5", "kimi-k2-thinking");
m.insert("k2p6", "kimi-k2.6");
m.insert("k2-p6", "kimi-k2.6");
m.insert("kimi-k2p6", "kimi-k2.6");
m.insert("kimi-k2.5-thinking", "kimi-k2-thinking");
m.insert("kimi-for-coding", "kimi-k2.7-code");
m.insert("kimi-for-coding-highspeed", "kimi-k2.7-code-highspeed");
m.insert("k3", "kimi-k3");
m.insert("k3-256k", "kimi-k3");
// Kimi Work (desktop agent mode) reports its own ids.
m.insert("k2d6-agent", "kimi-k2.6");
m.insert("k3-agent", "kimi-k3");
m.insert("k3-agent-swarm", "kimi-k3");
m.insert("minimax-m3", "minimax/MiniMax-M3");
m.insert("model_placeholder_m26", "claude-opus-4-6");
m.insert("model_placeholder_m35", "claude-sonnet-4-6");
m.insert("model_placeholder_m36", "gemini-3.1-pro");
m.insert("model_placeholder_m37", "gemini-3.1-pro");
// Antigravity uses opaque placeholder IDs in IDE metadata and shorter
// responseModel aliases in CLI conversation protobufs. The evidence has
// two distinct roles:
//
// - Antigravity Manager is a third-party account/quota manager. Its quota
// client documents the server-side metadata source and response shape:
// model IDs and display names come from Google Cloud Code Assist's
// fetchAvailableModels API.
// https://github.com/lbjlaq/Antigravity-Manager/blob/dfe876548d572237da92fe4c3e070a9db33c0910/src-tauri/src/modules/quota.rs
// - The concrete placeholder and responseModel mappings below come from
// Antigravity Context Window Monitor's GetUserStatus/session registry.
// https://github.com/AGI-is-going-to-arrive/Antigravity-Context-Window-Monitor/blob/603e3ea00a0ee94f1beecc162cf47a4ed68d3a6f/src/models.ts
//
// Keep these as machine-ID aliases. Do not use server-provided display
// labels as pricing keys because labels may be renamed or localized.
//
// M133/`gemini-3-flash-b`, `gemini-3-flash-a`, and M187/raw
// `gemini-3.5-flash-low` are cases where the obvious mapping is wrong,
// verified against the pinned Antigravity Context Window Monitor SHA
// above (models.ts@603e3ea):
//
// - M133 was renamed from "Gemini 3 Flash" to "Gemini 3.5 Flash (High)"
// ("MODEL_PLACEHOLDER_M133": 'Gemini 3.5 Flash (High)', // gemini-3-flash-agent
// (renamed from "Gemini 3 Flash")"), and `responseModelAliases` maps
// BOTH `gemini-3-flash-agent` and `gemini-3-flash-b` to M133. So M133
// and `gemini-3-flash-b` must resolve identically to `gemini-3-flash-agent`
// (gemini-3.5-flash-high), not to the retired gemini-3-flash-preview tier.
// - `responseModelAliases['gemini-3-flash-a'] = 'MODEL_PLACEHOLDER_M132'`
// ("legacy responseModel for 3.5 Flash"), and
// `STATIC_MODEL_NAME_FALLBACKS['MODEL_PLACEHOLDER_M132'] =
// 'Gemini 3.5 Flash (High)' // retired predecessor of M133`. So
// `gemini-3-flash-a` prices as the retired-predecessor High tier
// (gemini-3.5-flash-high) — the same catalog entry as M133/M132/
// `gemini-3-flash-b` — not as the unrelated gemini-3-flash-preview
// family (M18/M84), which is a different, older backend command model.
// - M20's `activeModelSpecs` entry has `modelId: 'gemini-3.5-flash-low'`
// with `displayName: 'Gemini 3.5 Flash (Medium)'` — the wire string
// says "low" but the tier is actually Medium. M187 is a distinct
// placeholder whose own `activeModelSpecs` entry has
// `modelId: 'gemini-3.5-flash-extra-low'` and
// `displayName: 'Gemini 3.5 Flash (Low)'` — the true Low tier. M187
// and M20/raw `gemini-3.5-flash-low` must NOT collapse to the same
// canonical alias target: M187 maps to `gemini-3.5-flash-extra-low`
// (its own machine ID), while M20 and the raw wire string map to
// `gemini-3.5-flash-medium`.
m.insert("model_placeholder_m16", "gemini-3.1-pro");
m.insert("model_placeholder_m18", "gemini-3-flash-preview");
m.insert("model_placeholder_m84", "gemini-3-flash-preview");
m.insert("model_placeholder_m132", "gemini-3.5-flash-high");
m.insert("model_placeholder_m133", "gemini-3.5-flash-high");
m.insert("model_placeholder_m187", "gemini-3.5-flash-extra-low");
m.insert("model_placeholder_m20", "gemini-3.5-flash-medium");
m.insert("gemini-pro-default", "gemini-3.1-pro");
m.insert("gemini-pro-agent", "gemini-3.1-pro");
m.insert("gemini-3-flash-agent", "gemini-3.5-flash-high");
m.insert("gemini-3-flash-b", "gemini-3.5-flash-high");
m.insert("gemini-3.5-flash-low", "gemini-3.5-flash-medium");
m.insert("model_placeholder_m47", "gemini-3-flash-preview");
m.insert("model_openai_gpt_oss_120b_medium", "gpt-oss-120b-medium");
m.insert("claude-opus-4-6-thinking", "claude-opus-4-6");
m.insert("claude-sonnet-4-6-thinking", "claude-sonnet-4-6");
m.insert("claude-opus-4.6-thinking", "claude-opus-4-6");
m.insert("claude-sonnet-4.6-thinking", "claude-sonnet-4-6");
m.insert("claude-opus-4-6", "claude-opus-4-6");
m.insert("claude-sonnet-4-6", "claude-sonnet-4-6");
m.insert("claude-haiku-4-6", "claude-haiku-4-6");
m.insert("claude-opus-4.6", "claude-opus-4-6");
m.insert("claude-sonnet-4.6", "claude-sonnet-4-6");
m.insert("claude-haiku-4.6", "claude-haiku-4-6");
// Anthropic's "-0" suffix is their documented moving alias for the latest
// snapshot of a model line (claude-opus-4-0 -> newest Opus 4). Datasets
// publish the dated key instead, so the alias form resolved to nothing and
// real first-party usage was excluded from submission as unpriced.
m.insert("claude-opus-4-0", "claude-opus-4");
m.insert("claude-sonnet-4-0", "claude-sonnet-4");
// GitHub Copilot reports Claude 4.1 without the separator. Copilot usage is
// priced at the underlying model's rates (its own $0.00 subscription rows
// are filtered out by EXCLUDED_LITELLM_PREFIXES), so this must resolve the
// same way github_copilot/gpt-4o already resolves to gpt-4o.
// Deliberately opus-only: `claude-sonnet-4-1` currently resolves to
// `databricks/databricks-claude-sonnet-4-1` via a cross-vendor fuzzy match
// (#1062), so aliasing the Copilot spelling onto it would route Sonnet 4.1
// usage to Databricks rates. Add it once #1062 makes that target safe.
m.insert("claude-opus-41", "claude-opus-4-1");
m.insert("anthropic/claude-4-5-opus", "claude-opus-4-5");
m.insert("anthropic/claude-4-5-sonnet", "claude-sonnet-4-5");
m.insert("anthropic/claude-4-5-haiku", "claude-haiku-4-5");
m.insert("anthropic/claude-4-6-opus", "claude-opus-4-6");
m.insert("anthropic/claude-4-6-sonnet", "claude-sonnet-4-6");
m.insert("anthropic/claude-4-6-haiku", "claude-haiku-4-6");
m.insert("gemini-3.1-pro-high", "gemini-3.1-pro");
m.insert("gemini-3.1-pro-low", "gemini-3.1-pro");
m.insert("gemini-3-pro-high", "gemini-3-pro");
m.insert("gemini-3-pro-low", "gemini-3-pro");
m.insert("gemini-3-flash", "gemini-3-flash-preview");
m.insert("gemini-3-flash-c", "gemini-3-flash-preview");
m.insert("gemini-3-flash-a", "gemini-3.5-flash-high");
// OpenAI documents the API spelling below as a moving alias for
// `gpt-5.6-sol`; Codex records the same alias with its `gpt-` prefix.
// Keep the API, Codex, and provider-qualified spellings pinned to the
// currently documented target so the upstream GPT-5.6 Sol row supplies
// all token-bucket rates. The qualified form must be explicit because
// provider-prefix stripping does not run alias resolution a second time.
m.insert("daybreak-blue-latest", "gpt-5.6-sol");
m.insert("gpt-daybreak-blue-latest", "gpt-5.6-sol");
m.insert("openai/gpt-daybreak-blue-latest", "gpt-5.6-sol");
m.insert("openai/daybreak-blue-latest", "gpt-5.6-sol");
m.insert("grok-composer-2.5", "composer-2.5");
m.insert("grok-composer-2.5-fast", "composer-2.5-fast");
// Synthetic model variants (only where resolver needs help)
m.insert("kimi-k2.5-nvfp4", "kimi-k2.5"); // Quantization variant → base model pricing
m.insert("kimi-k2-instruct-0905", "kimi-k2.5"); // Specific version → base (avoids reseller)
m
});
pub fn resolve_alias(model_id: &str) -> Option<&'static str> {
MODEL_ALIASES.get(model_id.to_lowercase().as_str()).copied()
}
#[cfg(test)]
mod tests {
use super::resolve_alias;
use std::collections::HashMap;
#[test]
fn resolves_antigravity_placeholders() {
let cases = [
("MODEL_PLACEHOLDER_M26", "claude-opus-4-6"),
("model_placeholder_m37", "gemini-3.1-pro"),
("model_placeholder_m16", "gemini-3.1-pro"),
("model_placeholder_m18", "gemini-3-flash-preview"),
("MODEL_PLACEHOLDER_M84", "gemini-3-flash-preview"),
("model_placeholder_m132", "gemini-3.5-flash-high"),
("model_placeholder_m133", "gemini-3.5-flash-high"),
("model_placeholder_m187", "gemini-3.5-flash-extra-low"),
("model_placeholder_m20", "gemini-3.5-flash-medium"),
("gemini-pro-default", "gemini-3.1-pro"),
("gemini-pro-agent", "gemini-3.1-pro"),
("gemini-3-flash-agent", "gemini-3.5-flash-high"),
("gemini-3-flash-b", "gemini-3.5-flash-high"),
("gemini-3.5-flash-low", "gemini-3.5-flash-medium"),
("MODEL_OPENAI_GPT_OSS_120B_MEDIUM", "gpt-oss-120b-medium"),
("gemini-3-flash-c", "gemini-3-flash-preview"),
("gemini-3-flash-a", "gemini-3.5-flash-high"),
("claude-opus-4.6-thinking", "claude-opus-4-6"),
("anthropic/claude-4-5-haiku", "claude-haiku-4-5"),
("anthropic/claude-4-6-sonnet", "claude-sonnet-4-6"),
];
for (raw, expected) in cases {
assert_eq!(resolve_alias(raw), Some(expected), "raw model: {raw}");
}
}
#[test]
fn resolves_kimi_k2p6_aliases_without_regressing_k2p5() {
assert_eq!(resolve_alias("k2p6"), Some("kimi-k2.6"));
assert_eq!(resolve_alias("k2-p6"), Some("kimi-k2.6"));
assert_eq!(resolve_alias("kimi-k2p6"), Some("kimi-k2.6"));
assert_eq!(resolve_alias("KIMI-K2P6"), Some("kimi-k2.6"));
assert_eq!(resolve_alias("k2p5"), Some("kimi-k2-thinking"));
assert_eq!(resolve_alias("k2-p5"), Some("kimi-k2-thinking"));
}
#[test]
fn resolves_kimi_coding_plan_ids_to_underlying_models() {
// kimi-code writes `kimi-code/<id>`; the parser strips the prefix, so
// pricing sees the bare id. Without these, models.dev matches them under
// its `kimi-for-coding/*` subscription namespace at $0.00.
assert_eq!(
resolve_alias("kimi-for-coding-highspeed"),
Some("kimi-k2.7-code-highspeed")
);
assert_eq!(resolve_alias("k3"), Some("kimi-k3"));
}
#[test]
fn resolves_grok_composer_aliases_to_cursor_composer_prices() {
assert_eq!(resolve_alias("grok-composer-2.5"), Some("composer-2.5"));
assert_eq!(
resolve_alias("GROK-COMPOSER-2.5-FAST"),
Some("composer-2.5-fast")
);
}
#[test]
fn m187_and_m20_resolve_to_distinct_tiers_but_both_still_price() {
// M187 (true Low tier, machine id `gemini-3.5-flash-extra-low`) and
// M20/raw CLI `gemini-3.5-flash-low` (actually the Medium tier) must
// NOT collapse to the same canonical alias target — that would
// silently merge two different-priced tiers into one cost bucket.
// Verified against the pinned Antigravity Context Window Monitor SHA
// (models.ts@603e3ea): M187's own `activeModelSpecs` entry has
// `modelId: 'gemini-3.5-flash-extra-low'`, distinct from M20's
// `modelId: 'gemini-3.5-flash-low'`.
let m187_canonical = resolve_alias("model_placeholder_m187").unwrap();
let m20_canonical = resolve_alias("model_placeholder_m20").unwrap();
let cli_low_canonical = resolve_alias("gemini-3.5-flash-low").unwrap();
assert_eq!(m187_canonical, "gemini-3.5-flash-extra-low");
assert_eq!(m20_canonical, "gemini-3.5-flash-medium");
assert_ne!(
m187_canonical, m20_canonical,
"M187 (Low) and M20 (Medium) must not resolve to the same tier"
);
// The raw CLI wire string tracks M20 (Medium), not M187 (Low).
assert_eq!(cli_low_canonical, m20_canonical);
// Both tiers must still reach a priced catalog entry: the pricing
// dataset only carries one generic `google/gemini-3.5-flash` entry,
// and the lookup's suffix-stripping normalization must land both the
// `-extra-low` and `-medium` canonical ids on it.
let mut models_dev = HashMap::new();
models_dev.insert(
"google/gemini-3.5-flash".to_string(),
super::super::litellm::ModelPricing {
input_cost_per_token: Some(0.0000015),
output_cost_per_token: Some(0.000009),
..Default::default()
},
);
let lookup = super::super::lookup::PricingLookup::new_with_models_dev(
HashMap::new(),
HashMap::new(),
HashMap::new(),
HashMap::new(),
models_dev,
);
let m187_result = lookup
.lookup(m187_canonical)
.expect("M187 target must still price via lookup normalization");
let m20_result = lookup
.lookup(m20_canonical)
.expect("M20 target must still price via lookup normalization");
assert_eq!(m187_result.matched_key, "google/gemini-3.5-flash");
assert_eq!(m20_result.matched_key, "google/gemini-3.5-flash");
}
#[test]
fn resolves_openai_daybreak_blue_aliases_to_gpt_5_6_sol() {
assert_eq!(resolve_alias("daybreak-blue-latest"), Some("gpt-5.6-sol"));
assert_eq!(
resolve_alias("gpt-daybreak-blue-latest"),
Some("gpt-5.6-sol")
);
assert_eq!(
resolve_alias("GPT-DAYBREAK-BLUE-LATEST"),
Some("gpt-5.6-sol")
);
assert_eq!(
resolve_alias("openai/daybreak-blue-latest"),
Some("gpt-5.6-sol")
);
assert_eq!(
resolve_alias("openai/gpt-daybreak-blue-latest"),
Some("gpt-5.6-sol")
);
}
#[test]
fn anthropic_moving_aliases_and_copilot_spelling_resolve() {
assert_eq!(resolve_alias("claude-opus-4-0"), Some("claude-opus-4"));
assert_eq!(resolve_alias("claude-sonnet-4-0"), Some("claude-sonnet-4"));
assert_eq!(resolve_alias("claude-opus-41"), Some("claude-opus-4-1"));
assert_eq!(resolve_alias("Claude-Opus-4-0"), Some("claude-opus-4"));
assert_eq!(resolve_alias("claude-sonnet-41"), None);
}
}