aigetwey 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +302 -0
  4. package/assets/logo.svg +8 -0
  5. package/assets/screenshot.png +0 -0
  6. package/assets/wordmark.svg +9 -0
  7. package/config.example.yaml +56 -0
  8. package/dashboard/.env.example +12 -0
  9. package/dashboard/next-env.d.ts +6 -0
  10. package/dashboard/next.config.ts +12 -0
  11. package/dashboard/package-lock.json +1771 -0
  12. package/dashboard/package.json +29 -0
  13. package/dashboard/postcss.config.mjs +5 -0
  14. package/dashboard/src/app/(console)/combos/page.tsx +10 -0
  15. package/dashboard/src/app/(console)/config/page.tsx +5 -0
  16. package/dashboard/src/app/(console)/console/page.tsx +92 -0
  17. package/dashboard/src/app/(console)/endpoint/page.tsx +5 -0
  18. package/dashboard/src/app/(console)/layout.tsx +17 -0
  19. package/dashboard/src/app/(console)/page.tsx +8 -0
  20. package/dashboard/src/app/(console)/providers/[id]/page.tsx +6 -0
  21. package/dashboard/src/app/(console)/providers/page.tsx +5 -0
  22. package/dashboard/src/app/(console)/quota/page.tsx +5 -0
  23. package/dashboard/src/app/(console)/tools/[id]/page.tsx +6 -0
  24. package/dashboard/src/app/(console)/tools/page.tsx +5 -0
  25. package/dashboard/src/app/(console)/usage/page.tsx +24 -0
  26. package/dashboard/src/app/api/cli-detect/[tool]/route.ts +253 -0
  27. package/dashboard/src/app/api/gw/[...path]/route.ts +89 -0
  28. package/dashboard/src/app/api/login/route.ts +30 -0
  29. package/dashboard/src/app/api/logout/route.ts +9 -0
  30. package/dashboard/src/app/api/password/route.ts +34 -0
  31. package/dashboard/src/app/globals.css +340 -0
  32. package/dashboard/src/app/icon.svg +8 -0
  33. package/dashboard/src/app/layout.tsx +28 -0
  34. package/dashboard/src/app/login/page.tsx +60 -0
  35. package/dashboard/src/components/AreaChart.tsx +115 -0
  36. package/dashboard/src/components/Badge.tsx +32 -0
  37. package/dashboard/src/components/Button.tsx +60 -0
  38. package/dashboard/src/components/CapacityBadges.tsx +40 -0
  39. package/dashboard/src/components/Checkbox.tsx +40 -0
  40. package/dashboard/src/components/CliToolConfig.tsx +63 -0
  41. package/dashboard/src/components/ConfigEditor.tsx +199 -0
  42. package/dashboard/src/components/ConfirmModal.tsx +36 -0
  43. package/dashboard/src/components/CooldownTimer.tsx +42 -0
  44. package/dashboard/src/components/EndpointView.tsx +439 -0
  45. package/dashboard/src/components/Icon.tsx +25 -0
  46. package/dashboard/src/components/KeyReveal.tsx +78 -0
  47. package/dashboard/src/components/Lamp.tsx +8 -0
  48. package/dashboard/src/components/LogTable.tsx +223 -0
  49. package/dashboard/src/components/LogoutButton.tsx +20 -0
  50. package/dashboard/src/components/ModelPicker.tsx +121 -0
  51. package/dashboard/src/components/ModelSelectModal.tsx +126 -0
  52. package/dashboard/src/components/PasswordEditor.tsx +86 -0
  53. package/dashboard/src/components/PricingEditor.tsx +171 -0
  54. package/dashboard/src/components/ProviderDetail.tsx +566 -0
  55. package/dashboard/src/components/ProviderManager.tsx +311 -0
  56. package/dashboard/src/components/QuotaView.tsx +78 -0
  57. package/dashboard/src/components/Rail.tsx +82 -0
  58. package/dashboard/src/components/RichCard.tsx +46 -0
  59. package/dashboard/src/components/RoutingView.tsx +329 -0
  60. package/dashboard/src/components/ThemeProvider.tsx +36 -0
  61. package/dashboard/src/components/ToastProvider.tsx +58 -0
  62. package/dashboard/src/components/ToolDetail.tsx +475 -0
  63. package/dashboard/src/components/TopBar.tsx +128 -0
  64. package/dashboard/src/components/UsageView.tsx +151 -0
  65. package/dashboard/src/components/ui.tsx +54 -0
  66. package/dashboard/src/lib/capabilities.ts +318 -0
  67. package/dashboard/src/lib/cliTools.ts +120 -0
  68. package/dashboard/src/lib/client.ts +190 -0
  69. package/dashboard/src/lib/gateway.ts +269 -0
  70. package/dashboard/src/lib/session.ts +71 -0
  71. package/dashboard/src/middleware.ts +37 -0
  72. package/dashboard/tsconfig.json +21 -0
  73. package/dist/adapters/anthropic.js +289 -0
  74. package/dist/adapters/anthropic.js.map +1 -0
  75. package/dist/adapters/gemini.js +268 -0
  76. package/dist/adapters/gemini.js.map +1 -0
  77. package/dist/adapters/index.js +8 -0
  78. package/dist/adapters/index.js.map +1 -0
  79. package/dist/adapters/openai.js +13 -0
  80. package/dist/adapters/openai.js.map +1 -0
  81. package/dist/cli/tray/autostart.js +152 -0
  82. package/dist/cli/tray/autostart.js.map +1 -0
  83. package/dist/cli/tray/icon.js +4 -0
  84. package/dist/cli/tray/icon.js.map +1 -0
  85. package/dist/cli/tray/tray.js +141 -0
  86. package/dist/cli/tray/tray.js.map +1 -0
  87. package/dist/cli/tray/trayRuntime.js +91 -0
  88. package/dist/cli/tray/trayRuntime.js.map +1 -0
  89. package/dist/cli.js +361 -0
  90. package/dist/cli.js.map +1 -0
  91. package/dist/config.js +728 -0
  92. package/dist/config.js.map +1 -0
  93. package/dist/core/authStore.js +78 -0
  94. package/dist/core/authStore.js.map +1 -0
  95. package/dist/core/canonical.js +9 -0
  96. package/dist/core/canonical.js.map +1 -0
  97. package/dist/core/console-buffer.js +25 -0
  98. package/dist/core/console-buffer.js.map +1 -0
  99. package/dist/core/fallback.js +62 -0
  100. package/dist/core/fallback.js.map +1 -0
  101. package/dist/core/handler.js +174 -0
  102. package/dist/core/handler.js.map +1 -0
  103. package/dist/core/keypool.js +105 -0
  104. package/dist/core/keypool.js.map +1 -0
  105. package/dist/core/quota.js +165 -0
  106. package/dist/core/quota.js.map +1 -0
  107. package/dist/core/state.js +52 -0
  108. package/dist/core/state.js.map +1 -0
  109. package/dist/db.js +193 -0
  110. package/dist/db.js.map +1 -0
  111. package/dist/headroom/compress.js +44 -0
  112. package/dist/headroom/compress.js.map +1 -0
  113. package/dist/headroom/detect.js +108 -0
  114. package/dist/headroom/detect.js.map +1 -0
  115. package/dist/headroom/process.js +158 -0
  116. package/dist/headroom/process.js.map +1 -0
  117. package/dist/inject/caveman.js +30 -0
  118. package/dist/inject/caveman.js.map +1 -0
  119. package/dist/inject/index.js +24 -0
  120. package/dist/inject/index.js.map +1 -0
  121. package/dist/inject/ponytail.js +19 -0
  122. package/dist/inject/ponytail.js.map +1 -0
  123. package/dist/middleware/auth.js +66 -0
  124. package/dist/middleware/auth.js.map +1 -0
  125. package/dist/providers/capabilities.js +246 -0
  126. package/dist/providers/capabilities.js.map +1 -0
  127. package/dist/providers/free.js +43 -0
  128. package/dist/providers/free.js.map +1 -0
  129. package/dist/providers/pricing.js +224 -0
  130. package/dist/providers/pricing.js.map +1 -0
  131. package/dist/providers/vertex.js +97 -0
  132. package/dist/providers/vertex.js.map +1 -0
  133. package/dist/routes/admin.js +622 -0
  134. package/dist/routes/admin.js.map +1 -0
  135. package/dist/routes/health.js +4 -0
  136. package/dist/routes/health.js.map +1 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/index.js.map +1 -0
  139. package/dist/routes/v1.js +75 -0
  140. package/dist/routes/v1.js.map +1 -0
  141. package/dist/rtk/detect.js +50 -0
  142. package/dist/rtk/detect.js.map +1 -0
  143. package/dist/rtk/filters.js +85 -0
  144. package/dist/rtk/filters.js.map +1 -0
  145. package/dist/rtk/index.js +39 -0
  146. package/dist/rtk/index.js.map +1 -0
  147. package/dist/server.js +100 -0
  148. package/dist/server.js.map +1 -0
  149. package/dist/stream/anthropic-stream.js +239 -0
  150. package/dist/stream/anthropic-stream.js.map +1 -0
  151. package/dist/stream/chunk.js +7 -0
  152. package/dist/stream/chunk.js.map +1 -0
  153. package/dist/stream/gemini-stream.js +135 -0
  154. package/dist/stream/gemini-stream.js.map +1 -0
  155. package/dist/stream/index.js +12 -0
  156. package/dist/stream/index.js.map +1 -0
  157. package/dist/stream/openai-stream.js +34 -0
  158. package/dist/stream/openai-stream.js.map +1 -0
  159. package/dist/stream/sse.js +64 -0
  160. package/dist/stream/sse.js.map +1 -0
  161. package/dist/translator/thinking.js +70 -0
  162. package/dist/translator/thinking.js.map +1 -0
  163. package/dist/translator/thinkingUnified.js +322 -0
  164. package/dist/translator/thinkingUnified.js.map +1 -0
  165. package/dist/upstream/client.js +120 -0
  166. package/dist/upstream/client.js.map +1 -0
  167. package/package.json +76 -0
  168. package/run.sh +27 -0
  169. package/src/adapters/anthropic.ts +377 -0
  170. package/src/adapters/gemini.ts +341 -0
  171. package/src/adapters/index.ts +17 -0
  172. package/src/adapters/openai.ts +22 -0
  173. package/src/cli/tray/autostart.ts +133 -0
  174. package/src/cli/tray/icon.ts +4 -0
  175. package/src/cli/tray/tray.ts +156 -0
  176. package/src/cli/tray/trayRuntime.ts +90 -0
  177. package/src/cli.ts +379 -0
  178. package/src/config.ts +777 -0
  179. package/src/core/authStore.ts +86 -0
  180. package/src/core/canonical.ts +93 -0
  181. package/src/core/console-buffer.ts +39 -0
  182. package/src/core/fallback.ts +116 -0
  183. package/src/core/handler.ts +236 -0
  184. package/src/core/keypool.ts +152 -0
  185. package/src/core/quota.ts +214 -0
  186. package/src/core/state.ts +65 -0
  187. package/src/db.ts +280 -0
  188. package/src/headroom/compress.ts +78 -0
  189. package/src/headroom/detect.ts +119 -0
  190. package/src/headroom/process.ts +166 -0
  191. package/src/inject/caveman.ts +35 -0
  192. package/src/inject/index.ts +46 -0
  193. package/src/inject/ponytail.ts +31 -0
  194. package/src/middleware/auth.ts +76 -0
  195. package/src/providers/capabilities.ts +297 -0
  196. package/src/providers/free.ts +53 -0
  197. package/src/providers/pricing.ts +261 -0
  198. package/src/providers/vertex.ts +117 -0
  199. package/src/routes/admin.ts +716 -0
  200. package/src/routes/health.ts +5 -0
  201. package/src/routes/index.ts +24 -0
  202. package/src/routes/v1.ts +87 -0
  203. package/src/rtk/detect.ts +55 -0
  204. package/src/rtk/filters.ts +94 -0
  205. package/src/rtk/index.ts +58 -0
  206. package/src/server.ts +108 -0
  207. package/src/stream/anthropic-stream.ts +310 -0
  208. package/src/stream/chunk.ts +46 -0
  209. package/src/stream/gemini-stream.ts +158 -0
  210. package/src/stream/index.ts +23 -0
  211. package/src/stream/openai-stream.ts +41 -0
  212. package/src/stream/sse.ts +72 -0
  213. package/src/translator/thinking.ts +64 -0
  214. package/src/translator/thinkingUnified.ts +319 -0
  215. package/src/upstream/client.ts +155 -0
  216. package/tsconfig.json +20 -0
@@ -0,0 +1,297 @@
1
+ // Model capabilities — what each model can read/do beyond plain text.
2
+ //
3
+ // Fallback order (first match wins), result merged over DEFAULT_CAPABILITIES:
4
+ // 1. PROVIDER_CAPABILITIES[provider][model] — provider-specific override
5
+ // 2. MODEL_CAPABILITIES[model] — canonical exact id (handles exceptions)
6
+ // 3. PATTERN_CAPABILITIES — glob match, ordered specific -> generic
7
+ // 4. DEFAULT_CAPABILITIES — safe floor (always returned)
8
+ //
9
+ // ── HOW TO ADD / UPDATE A MODEL ──────────────────────────────────────
10
+ // Authoritative data source: https://models.dev/api.json (145 providers, 4000+
11
+ // models, MIT). Each model exposes the exact fields we map below:
12
+ // modalities.input ["text","image","pdf","audio","video"] -> vision / pdf / audioInput / videoInput
13
+ // modalities.output ["text","image","audio"] -> imageOutput / audioOutput
14
+ // reasoning -> reasoning tool_call -> tools
15
+ // limit.context -> contextWindow limit.output -> maxOutput
16
+ // Look up the model id, then:
17
+ // • If a PATTERN below already covers it correctly -> nothing to do.
18
+ // • If it is an exception (pattern would mis-match) -> add an exact entry to
19
+ // MODEL_CAPABILITIES (only the fields that differ from DEFAULT).
20
+ // • If a whole new family -> add an ordered PATTERN (specific before generic).
21
+ // NOTE: models.dev has NO "search" flag (web search is a runtime tool, not a
22
+ // model spec); set `search` from vendor docs (Claude 4.x+, GPT-5.x/4o, Gemini
23
+ // 2.0+, Grok, Perplexity). Verify with: curl -s https://models.dev/api.json
24
+
25
+
26
+ /**
27
+ * Safe floor — every resolved result is merged over this so consumers
28
+ * never need null-checks. Most modern LLMs meet these limits.
29
+ */
30
+ export interface Caps {
31
+ vision: boolean;
32
+ pdf: boolean;
33
+ audioInput: boolean;
34
+ videoInput: boolean;
35
+ imageOutput: boolean;
36
+ audioOutput: boolean;
37
+ search: boolean;
38
+ tools: boolean;
39
+ reasoning: boolean;
40
+ thinkingFormat: string | null;
41
+ thinkingCanDisable: boolean;
42
+ thinkingRange: { min: number; max: number } | null;
43
+ contextWindow: number;
44
+ maxOutput: number;
45
+ }
46
+
47
+ /**
48
+ * Glob (* = wildcard) match, anchored + case-insensitive. aigetwey's
49
+ * pricing.matchPattern so capabilities resolve identically.
50
+ */
51
+ export function matchPattern(pattern: string, model: string): boolean {
52
+ const regex = new RegExp(
53
+ "^" + pattern.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$",
54
+ "i",
55
+ );
56
+ return regex.test(model);
57
+ }
58
+
59
+ export const DEFAULT_CAPABILITIES: Caps = {
60
+ // input modalities
61
+ vision: false, // read images
62
+ pdf: false, // read PDF / documents
63
+ audioInput: false, // read audio
64
+ videoInput: false, // read video
65
+ // output modalities
66
+ imageOutput: false, // generate images
67
+ audioOutput: false, // generate audio
68
+ // features
69
+ search: false, // built-in web search tool / grounding
70
+ tools: true, // function / tool calling
71
+ reasoning: false, // thinking / reasoning
72
+ // thinking wire format (only meaningful when reasoning:true). null → derive from transport.format.
73
+ // enum: openai|claude-adaptive|claude-budget|gemini-level|gemini-budget|zai|qwen|deepseek|kimi|minimax|hunyuan|step
74
+ thinkingFormat: null,
75
+ thinkingCanDisable: true, // false → model cannot turn thinking off (clamp to min instead of disable)
76
+ thinkingRange: null, // { min, max } for budget formats; null = no clamp
77
+ // limits (tokens)
78
+ contextWindow: 200000,
79
+ maxOutput: 64000,
80
+ };
81
+
82
+ // User-added model metadata can carry dashboard service kinds instead of the
83
+ // runtime capability names used here. Map those typed model kinds into input /
84
+ // output capabilities so custom vision models are not treated as text-only.
85
+ const SERVICE_KIND_CAPABILITIES: Record<string, Partial<Caps>> = {
86
+ imageToText: { vision: true },
87
+ image: { imageOutput: true },
88
+ stt: { audioInput: true },
89
+ tts: { audioOutput: true },
90
+ embedding: { tools: false },
91
+ };
92
+
93
+ export function capabilitiesFromServiceKind(kind: string): Partial<Caps> | null {
94
+ return SERVICE_KIND_CAPABILITIES[kind] || null;
95
+ }
96
+
97
+ /**
98
+ * Canonical exact-id overrides — used for exceptions that patterns would
99
+ * otherwise mis-match. Only declare deltas vs DEFAULT.
100
+ */
101
+ export const MODEL_CAPABILITIES: Record<string, Partial<Caps>> = {
102
+ // Claude 4.6/4.7 have 1M context + adaptive thinking (override generic claude pattern)
103
+ "claude-opus-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
104
+ "claude-opus-4.7": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
105
+ "claude-opus-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 128000 },
106
+ "claude-sonnet-4.6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 },
107
+ "claude-sonnet-4-6": { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive", contextWindow: 1000000, maxOutput: 64000 },
108
+
109
+ // Gemini image-gen / OpenAI image / xai image variants
110
+ "gpt-image-1": { imageOutput: true, tools: false },
111
+
112
+ // GLM vision variant (text GLM has no vision)
113
+ "glm-4.6v": { vision: true, reasoning: true, thinkingFormat: "zai", contextWindow: 128000 },
114
+
115
+ // Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
116
+ "vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
117
+ "coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
118
+ };
119
+
120
+ /**
121
+ * Provider-specific capability overrides. Keyed by provider alias/id.
122
+ */
123
+ export const PROVIDER_CAPABILITIES: Record<string, Record<string, Partial<Caps>>> = {
124
+ // CodeBuddy.cn — authoritative per-model metadata from the gateway's model
125
+ // config (contextWindow=maxInputTokens, maxOutput=maxOutputTokens, vision=
126
+ // supportsImages). Every model reasons via OpenAI-style reasoning_effort
127
+ // (see registry thinkingFormat). `onlyReasoning` models can't turn thinking
128
+ // off → thinkingCanDisable:false (clamped to minimal instead of disabled).
129
+ "codebuddy-cn": {
130
+ "glm-5.2": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 48000 },
131
+ "glm-5.1": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
132
+ "glm-5.0": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
133
+ "glm-5.0-turbo": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
134
+ "glm-5v-turbo": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 38000 },
135
+ "glm-4.7": { reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 48000 },
136
+ "minimax-m3": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 512000, maxOutput: 48000 },
137
+ "minimax-m2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 48000 },
138
+ "kimi-k2.7": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
139
+ "kimi-k2.6": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 256000, maxOutput: 32000 },
140
+ "kimi-k2.5": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 164000, maxOutput: 32000 },
141
+ "hy3-preview": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 192000, maxOutput: 64000 },
142
+ "deepseek-v4-pro": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
143
+ "deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 },
144
+ "deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 },
145
+ },
146
+ };
147
+
148
+ /**
149
+ * Pattern fallback — glob (* = wildcard), matched case-insensitively and
150
+ * anchored (^...$) so a pattern must match the full model id. ORDER MATTERS:
151
+ * vision/specific variants first, text-only/generic families last, to avoid
152
+ * a broad family pattern swallowing an exception (e.g. glm-4.6v vs glm-5).
153
+ */
154
+ export const PATTERN_CAPABILITIES: Array<{ pattern: string; caps: Partial<Caps> }> = [
155
+ // ── Claude (4.6+ = adaptive thinking; older/haiku = budget) ──────
156
+ { pattern: "*claude*opus-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
157
+ { pattern: "*claude*opus-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
158
+ { pattern: "*claude*opus-4.8*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
159
+ { pattern: "*claude*sonnet-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
160
+ { pattern: "*claude*sonnet-4.7*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-adaptive" } },
161
+ { pattern: "*claude*haiku*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
162
+ { pattern: "*claude*opus*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
163
+ { pattern: "*claude*sonnet*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
164
+ { pattern: "*claude*fable*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } },
165
+ { pattern: "*claude*mythos*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget", contextWindow: 1000000, maxOutput: 128000 } },
166
+ { pattern: "*claude-3*", caps: { vision: true } },
167
+ { pattern: "*claude*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "claude-budget" } },
168
+
169
+ // ── Gemini (all 2.0+ multimodal + google_search grounding, 1M ctx) ─
170
+ { pattern: "*gemini*image*", caps: { vision: true, imageOutput: true, contextWindow: 1048576 } },
171
+ { pattern: "*gemini-3*pro*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65535 } },
172
+ { pattern: "*gemini-3*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-level", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 65536 } },
173
+ { pattern: "*gemini-2.5*", caps: { vision: true, audioInput: true, videoInput: true, reasoning: true, search: true, thinkingFormat: "gemini-budget", thinkingRange: { min: 0, max: 24576 }, contextWindow: 1048576, maxOutput: 65536 } },
174
+ { pattern: "*gemini-2*", caps: { vision: true, audioInput: true, videoInput: true, search: true, contextWindow: 1048576, maxOutput: 65536 } },
175
+ { pattern: "*gemini*", caps: { vision: true, search: true, contextWindow: 1048576 } },
176
+ { pattern: "*gemma*", caps: { vision: true, contextWindow: 128000 } },
177
+ { pattern: "*nanobanana*", caps: { vision: true, imageOutput: true } },
178
+
179
+ // ── OpenAI GPT-5.x (vision + thinking + web search) ──────────────
180
+ { pattern: "*gpt-5*image*", caps: { imageOutput: true } },
181
+ { pattern: "*gpt-5*codex*", caps: { reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } },
182
+ { pattern: "*gpt-5*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 400000, maxOutput: 128000 } },
183
+ { pattern: "*gpt-4o*", caps: { vision: true, search: true, contextWindow: 128000, maxOutput: 16384 } },
184
+ { pattern: "*gpt-4.1*", caps: { vision: true, contextWindow: 1000000, maxOutput: 32768 } },
185
+ { pattern: "*gpt-4-turbo*", caps: { vision: true, contextWindow: 128000 } },
186
+ { pattern: "*gpt-4*", caps: { contextWindow: 128000 } },
187
+ { pattern: "*gpt-3.5*", caps: { contextWindow: 16385, maxOutput: 4096 } },
188
+ { pattern: "*gpt-oss*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } },
189
+
190
+ // ── OpenAI o-series (reasoning, vision) ──────────────────────────
191
+ { pattern: "*o1-mini*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 128000 } },
192
+ { pattern: "*o1*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
193
+ { pattern: "*o3*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
194
+ { pattern: "*o4*", caps: { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 200000, maxOutput: 100000 } },
195
+
196
+ // ── Grok (vision + Live Search) ──────────────────────────────────
197
+ { pattern: "*grok*image*", caps: { imageOutput: true } },
198
+ { pattern: "*grok-code*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 256000 } },
199
+ { pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
200
+ { pattern: "*grok-3*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 131072 } },
201
+ { pattern: "*grok*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
202
+
203
+ // ── Qwen (enable_thinking + thinking_budget; QwQ = thinking-only) ─
204
+ { pattern: "*qwen*vl*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
205
+ { pattern: "*qwen*max*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } },
206
+ { pattern: "*qwen*plus*", caps: { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000, maxOutput: 65536 } },
207
+ { pattern: "*qwen*235b*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
208
+ { pattern: "*qwen*coder*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 } },
209
+ { pattern: "*qwq*", caps: { reasoning: true, thinkingFormat: "qwen", thinkingCanDisable: false, contextWindow: 131072 } },
210
+ { pattern: "*qwen*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
211
+
212
+ // ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ─────
213
+ { pattern: "*kimi*k2.7*code*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 262144 } },
214
+ { pattern: "*kimi*k2*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", contextWindow: 262144, maxOutput: 262144 } },
215
+ { pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },
216
+
217
+ // ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─
218
+ { pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
219
+ { pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
220
+ { pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
221
+ { pattern: "*glm*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
222
+
223
+ // ── DeepSeek (thinking.enabled + reasoning_effort; r1 = thinking-only) ─
224
+ { pattern: "*deepseek-v4*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 } },
225
+ { pattern: "*reasoner*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
226
+ { pattern: "*deepseek-r*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
227
+ { pattern: "*deepseek-chat*", caps: { contextWindow: 128000 } },
228
+ { pattern: "*deepseek*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 128000 } },
229
+
230
+ // ── MiniMax (M3 = adaptive; M2.x cannot disable) ─────────────────
231
+ { pattern: "*minimax*image*", caps: { imageOutput: true } },
232
+ { pattern: "*minimax-m3*", caps: { vision: true, reasoning: true, thinkingFormat: "minimax", contextWindow: 1048576, maxOutput: 512000 } },
233
+ { pattern: "*minimax-m2.7*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 204800, maxOutput: 131072 } },
234
+ { pattern: "*minimax*", caps: { reasoning: true, thinkingFormat: "minimax", thinkingCanDisable: false, contextWindow: 200000, maxOutput: 131072 } },
235
+
236
+ // ── Xiaomi MiMo (vision, 1M / 262K ctx) ──────────────────────────
237
+ { pattern: "*mimo*v2.5*", caps: { vision: true, contextWindow: 1048576, maxOutput: 131072 } },
238
+ { pattern: "*mimo*omni*", caps: { vision: true, audioInput: true, contextWindow: 262144, maxOutput: 131072 } },
239
+ { pattern: "*mimo*", caps: { vision: true, contextWindow: 262144, maxOutput: 131072 } },
240
+
241
+ // ── Llama (4 = vision/1M; 3.x = text-only/128K) ──────────────────
242
+ { pattern: "*llama-4*", caps: { vision: true, contextWindow: 1000000 } },
243
+ { pattern: "*llama*", caps: { contextWindow: 128000 } },
244
+
245
+ // ── Mistral (Large 3 = vision/256K; codestral text) ──────────────
246
+ { pattern: "*codestral*", caps: { contextWindow: 256000 } },
247
+ { pattern: "*mistral-large*", caps: { vision: true, contextWindow: 256000 } },
248
+ { pattern: "*mistral*", caps: { contextWindow: 128000 } },
249
+
250
+ // ── Cohere (Command A Vision = vision; others text) ──────────────
251
+ { pattern: "*command-a-vision*", caps: { vision: true, contextWindow: 128000 } },
252
+ { pattern: "*command*", caps: { contextWindow: 128000 } },
253
+
254
+ // ── Perplexity (web search native) ───────────────────────────────
255
+ { pattern: "*sonar*", caps: { search: true, contextWindow: 128000 } },
256
+ { pattern: "*pplx*", caps: { search: true, contextWindow: 128000 } },
257
+ { pattern: "*perplexity*", caps: { search: true, contextWindow: 128000 } },
258
+
259
+ // ── Others ───────────────────────────────────────────────────────
260
+ { pattern: "*hunyuan*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
261
+ { pattern: "hy3*", caps: { reasoning: true, thinkingFormat: "hunyuan", contextWindow: 262144, maxOutput: 262144 } },
262
+ { pattern: "*step-*", caps: { reasoning: true, thinkingFormat: "step", contextWindow: 128000 } },
263
+ { pattern: "*nemotron*", caps: { reasoning: true, contextWindow: 128000 } },
264
+ { pattern: "*ling-*", caps: { reasoning: true, contextWindow: 128000 } },
265
+ ];
266
+
267
+ /**
268
+ * Resolve capabilities for a model using the 4-step fallback chain,
269
+ * merged over DEFAULT_CAPABILITIES so the result is always complete.
270
+ *
271
+ * @param {string} provider
272
+ * @param {string} model
273
+ * @returns {object} full capabilities object
274
+ */
275
+ export function getCapabilitiesForModel(provider: string | null, model: string): Caps {
276
+ if (!model) return { ...DEFAULT_CAPABILITIES };
277
+
278
+ // 1. Provider-specific override
279
+ if (provider && PROVIDER_CAPABILITIES[provider]?.[model]) {
280
+ return { ...DEFAULT_CAPABILITIES, ...PROVIDER_CAPABILITIES[provider][model] };
281
+ }
282
+
283
+ // 2. Canonical exact (strip vendor prefix: "anthropic/claude-opus-4.7" -> "claude-opus-4.7")
284
+ const baseModel = (model.includes("/") ? model.split("/").pop() : model) ?? model;
285
+ if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
286
+ if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
287
+
288
+ // 3. Pattern match (first match wins)
289
+ for (const { pattern, caps } of PATTERN_CAPABILITIES) {
290
+ if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
291
+ return { ...DEFAULT_CAPABILITIES, ...caps };
292
+ }
293
+ }
294
+
295
+ // 4. Floor
296
+ return { ...DEFAULT_CAPABILITIES };
297
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Free passthrough providers (e.g. OpenCode Free). These speak OpenAI format,
3
+ * need no upstream auth, and expose their catalog at `{base_url}/models`. The
4
+ * gateway already routes them through the normal pipeline (the key pool hands
5
+ * out an empty key, the client omits the auth header); this module only adds the
6
+ * one extra capability they need — fetching the model list at runtime so it
7
+ * doesn't have to be hand-maintained in config.
8
+ */
9
+ import { request } from "undici";
10
+ import type { Provider } from "../config.js";
11
+
12
+ export interface FetchedModel {
13
+ id: string;
14
+ }
15
+
16
+ export interface ModelFetchResult {
17
+ ok: boolean;
18
+ models: FetchedModel[];
19
+ error?: string;
20
+ }
21
+
22
+ /**
23
+ * Fetch a provider's model catalog from `{base_url}/models` (OpenAI shape:
24
+ * `{ data: [{ id }] }`). Never throws — returns a structured result so the
25
+ * dashboard's "Connect OpenCode Free" button can surface failures inline.
26
+ */
27
+ export async function fetchModels(provider: Provider): Promise<ModelFetchResult> {
28
+ const base = provider.base_url.replace(/\/$/, "");
29
+ const headers: Record<string, string> = { ...(provider.headers ?? {}) };
30
+ // a free provider has no key; a keyed one still gets its bearer for /models.
31
+ const key = provider.api_keys?.[0] ?? provider.api_key;
32
+ if (key && !provider.free) headers["authorization"] = `Bearer ${key}`;
33
+
34
+ try {
35
+ const res = await request(`${base}/models`, {
36
+ method: "GET",
37
+ headers,
38
+ headersTimeout: 15_000,
39
+ bodyTimeout: 15_000,
40
+ });
41
+ if (res.statusCode >= 400) {
42
+ await res.body.dump();
43
+ return { ok: false, models: [], error: `models endpoint returned ${res.statusCode}` };
44
+ }
45
+ const body = (await res.body.json()) as { data?: Array<{ id?: unknown }> };
46
+ const models = (body.data ?? [])
47
+ .map((m) => (typeof m.id === "string" ? { id: m.id } : null))
48
+ .filter((m): m is FetchedModel => m !== null);
49
+ return { ok: true, models };
50
+ } catch (e) {
51
+ return { ok: false, models: [], error: (e as Error).message };
52
+ }
53
+ }
@@ -0,0 +1,261 @@
1
+ // Pricing rates for AI models — all rates in $/1M tokens
2
+ //
3
+ // Fallback order (first match wins):
4
+ // 1. PROVIDER_PRICING[provider][model] — provider-specific override
5
+ // 2. MODEL_PRICING[model] — canonical model price (provider-agnostic)
6
+ // 3. PATTERN_PRICING — glob pattern match (e.g. "codex-*")
7
+
8
+ /**
9
+ * All rates in $/1M tokens. Implemented for aigetwey so cost auto-resolves per model the same way. See
10
+ *
11
+ */
12
+ export interface Pricing {
13
+ input: number;
14
+ output: number;
15
+ cached?: number;
16
+ reasoning?: number;
17
+ cache_creation?: number;
18
+ }
19
+
20
+ /**
21
+ * Canonical model pricing — provider-agnostic.
22
+ * Cover all known models; deduplicated across providers.
23
+ */
24
+ export const MODEL_PRICING: Record<string, Pricing> = {
25
+ // === Anthropic / Claude ===
26
+ "claude-opus-4-6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 },
27
+ "claude-opus-4-5-20251101": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 },
28
+ "claude-sonnet-4-6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 },
29
+ "claude-sonnet-4-5-20250929": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 },
30
+ "claude-haiku-4-5-20251001": { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 },
31
+ "claude-sonnet-4-20250514": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 },
32
+ "claude-opus-4-20250514": { input: 15.00, output: 25.00, cached: 7.50, reasoning: 112.50, cache_creation: 15.00 },
33
+ "claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 },
34
+ "claude-haiku-4.5": { input: 0.50, output: 2.50, cached: 0.05, reasoning: 3.75, cache_creation: 0.50 },
35
+ "claude-opus-4.1": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
36
+ "claude-opus-4.5": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
37
+ "claude-opus-4.6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
38
+ "claude-sonnet-4": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
39
+ "claude-sonnet-4.5": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
40
+ "claude-sonnet-4.6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 },
41
+ "claude-opus-4-5-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
42
+ "claude-opus-4-6-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 },
43
+
44
+ // === OpenAI / GPT ===
45
+ "gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 },
46
+ "gpt-4": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
47
+ "gpt-4-turbo": { input: 10.00, output: 30.00, cached: 5.00, reasoning: 45.00, cache_creation: 10.00 },
48
+ "gpt-4o": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
49
+ "gpt-4o-mini": { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 },
50
+ "gpt-4.1": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 },
51
+ "gpt-5": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
52
+ "gpt-5-mini": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
53
+ "gpt-5-codex": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
54
+ "gpt-5.1": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
55
+ "gpt-5.1-codex": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
56
+ "gpt-5.1-codex-mini": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
57
+ "gpt-5.1-codex-mini-high": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 },
58
+ "gpt-5.1-codex-max": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 },
59
+ "gpt-5.2": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 },
60
+ "gpt-5.2-codex": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 },
61
+ "gpt-5.3-codex": { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 },
62
+ "gpt-5.3-codex-xhigh": { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 },
63
+ "gpt-5.3-codex-high": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 },
64
+ "gpt-5.3-codex-low": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 },
65
+ "gpt-5.3-codex-none": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
66
+ "gpt-5.3-codex-spark": { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 },
67
+ "o1": { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 },
68
+ "o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 },
69
+
70
+ // === Gemini ===
71
+ "gemini-3-flash-preview": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
72
+ "gemini-3-pro-preview": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
73
+ "gemini-3.1-pro-low": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
74
+ "gemini-3.1-pro-high": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 },
75
+ "gemini-pro-agent": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 },
76
+ "gemini-3-flash-agent": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
77
+ "gemini-3.5-flash-low": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
78
+ "gemini-3.5-flash-extra-low": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
79
+ "gemini-3-flash": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 },
80
+ "gemini-2.5-pro": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 },
81
+ "gemini-2.5-flash": { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 },
82
+ "gemini-2.5-flash-lite": { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 },
83
+
84
+ // === Qwen ===
85
+ "qwen3-coder-plus": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
86
+ "qwen3-coder-flash": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
87
+
88
+ // === Kimi ===
89
+ "kimi-k2": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
90
+ "kimi-k2-thinking": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
91
+ "kimi-k2.5": { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 },
92
+ "kimi-k2.5-thinking": { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 },
93
+ "kimi-latest": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
94
+
95
+ // === DeepSeek ===
96
+ "deepseek-chat": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
97
+ "deepseek-reasoner": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
98
+ "deepseek-r1": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
99
+ "deepseek-v3.2-chat": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
100
+ "deepseek-v3.2-reasoner": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
101
+ "deepseek-v4-flash": { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 },
102
+ "deepseek-v4-pro": { input: 0.435, output: 0.87, cached: 0.003625, reasoning: 0.87, cache_creation: 0.435 },
103
+
104
+ // === GLM ===
105
+ "glm-4.6": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
106
+ "glm-4.6v": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
107
+ "glm-4.7": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 },
108
+ "glm-5": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
109
+
110
+ // === MiniMax ===
111
+ "MiniMax-M3": { input: 0.30, output: 1.20, cached: 0.06, reasoning: 1.80, cache_creation: 0.30 },
112
+ "MiniMax-M2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
113
+ "MiniMax-M2.5": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
114
+ "MiniMax-M2.7": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
115
+ "minimax-m2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
116
+ "minimax-m2.5": { input: 0.60, output: 2.40, cached: 0.30, reasoning: 3.60, cache_creation: 0.60 },
117
+
118
+ // === Grok ===
119
+ "grok-code-fast-1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
120
+
121
+ // === OpenRouter fallback ===
122
+ "auto": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 },
123
+
124
+ // === Misc ===
125
+ "oswe-vscode-prime": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
126
+ "gpt-oss-120b-medium": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
127
+ "vision-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
128
+ "coder-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
129
+ };
130
+
131
+ /**
132
+ * Provider-specific pricing overrides.
133
+ * Only include entries where price DIFFERS from MODEL_PRICING.
134
+ * Keyed by provider alias (cc, cx, gc, gh, ...) or provider id (openai, anthropic, ...).
135
+ */
136
+ export const PROVIDER_PRICING: Record<string, Record<string, Pricing>> = {
137
+ // GitHub Copilot (gh) — gpt-5.3-codex has different rate than canonical
138
+ gh: {
139
+ "gpt-5.3-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 },
140
+ },
141
+ };
142
+
143
+ /**
144
+ * Pattern-based pricing fallback — matched when no exact model entry found.
145
+ * Patterns use simple glob: "*" matches any substring.
146
+ * First match wins — order matters.
147
+ */
148
+ export const PATTERN_PRICING: Array<{ pattern: string; pricing: Pricing }> = [
149
+ // --- Codex variants ---
150
+ { pattern: "*-codex-xhigh", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } },
151
+ { pattern: "*-codex-high", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } },
152
+ { pattern: "*-codex-max", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } },
153
+ { pattern: "*-codex-mini-*", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } },
154
+ { pattern: "*-codex-mini", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } },
155
+ { pattern: "*-codex-low", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } },
156
+ { pattern: "*-codex-none", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
157
+ { pattern: "*-codex-spark", pricing: { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 } },
158
+ { pattern: "codex-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
159
+ { pattern: "*-codex", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
160
+
161
+ // --- Claude ---
162
+ { pattern: "claude-opus-*", pricing: { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 } },
163
+ { pattern: "claude-sonnet-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } },
164
+ { pattern: "claude-haiku-*", pricing: { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 } },
165
+ { pattern: "claude-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } },
166
+
167
+ // --- Gemini (specific first, generic last) ---
168
+ { pattern: "gemini-*-flash-lite", pricing: { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 } },
169
+ { pattern: "gemini-*-flash", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } },
170
+ { pattern: "gemini-*-pro", pricing: { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 } },
171
+ { pattern: "gemini-3-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } },
172
+ { pattern: "gemini-2.5-*", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } },
173
+ { pattern: "gemini-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } },
174
+
175
+ // --- GPT (specific first, generic last) ---
176
+ { pattern: "gpt-5.3-*", pricing: { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 } },
177
+ { pattern: "gpt-5.2-*", pricing: { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 } },
178
+ { pattern: "gpt-5.1-*", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } },
179
+ { pattern: "gpt-5-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
180
+ { pattern: "gpt-5*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
181
+ { pattern: "gpt-4o-*", pricing: { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 } },
182
+ { pattern: "gpt-4o", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } },
183
+ { pattern: "gpt-4*", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } },
184
+
185
+ // --- o1 / o-series ---
186
+ { pattern: "o1-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } },
187
+ { pattern: "o1", pricing: { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 } },
188
+ { pattern: "o3-*", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } },
189
+ { pattern: "o4-*", pricing: { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 } },
190
+
191
+ // --- Qwen ---
192
+ { pattern: "qwen3-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
193
+ { pattern: "qwen*-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
194
+ { pattern: "qwen*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
195
+
196
+ // --- Kimi ---
197
+ { pattern: "kimi-*-thinking", pricing: { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 } },
198
+ { pattern: "kimi-k2*", pricing: { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 } },
199
+ { pattern: "kimi-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
200
+
201
+ // --- DeepSeek ---
202
+ { pattern: "deepseek-*reasoner*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
203
+ { pattern: "deepseek-r*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
204
+ { pattern: "deepseek-v*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
205
+ { pattern: "deepseek-*", pricing: { input: 0.14, output: 0.28, cached: 0.0028, reasoning: 0.28, cache_creation: 0.14 } },
206
+
207
+ // --- GLM ---
208
+ { pattern: "glm-5*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
209
+ { pattern: "glm-4*", pricing: { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 } },
210
+ { pattern: "glm-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
211
+
212
+ // --- MiniMax ---
213
+ { pattern: "MiniMax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
214
+ { pattern: "minimax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
215
+
216
+ // --- Grok ---
217
+ { pattern: "grok-code-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
218
+ { pattern: "grok-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } },
219
+ ];
220
+
221
+ /**
222
+ * Match a model ID against a glob pattern (* = wildcard). Case-insensitive:
223
+ * registry ids mix casing (e.g. "MiniMax-M2.5" vs "minimax-m2.5").
224
+ */
225
+ export function matchPattern(pattern: string, model: string): boolean {
226
+ const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$", "i");
227
+ return regex.test(model);
228
+ }
229
+
230
+ /**
231
+ * Resolve pricing for a model using the 3-step fallback chain:
232
+ * 1. PROVIDER_PRICING[provider][model]
233
+ * 2. MODEL_PRICING[model]
234
+ * 3. PATTERN_PRICING (glob match)
235
+ *
236
+ * @param {string} provider
237
+ * @param {string} model
238
+ * @returns {object|null}
239
+ */
240
+ export function getPricingForModel(provider: string | null, model: string): Pricing | null {
241
+ if (!model) return null;
242
+
243
+ // 1. Provider-specific override
244
+ if (provider && PROVIDER_PRICING[provider]?.[model]) {
245
+ return PROVIDER_PRICING[provider][model];
246
+ }
247
+
248
+ // 2. Canonical model pricing (strip vendor prefix if needed: "deepseek/deepseek-chat" → "deepseek-chat")
249
+ const baseModel = (model.includes("/") ? model.split("/").pop() : model) ?? model;
250
+ if (MODEL_PRICING[baseModel]) return MODEL_PRICING[baseModel];
251
+ if (MODEL_PRICING[model]) return MODEL_PRICING[model];
252
+
253
+ // 3. Pattern match
254
+ for (const { pattern, pricing } of PATTERN_PRICING) {
255
+ if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
256
+ return pricing;
257
+ }
258
+ }
259
+
260
+ return null;
261
+ }