pi-commandcode-provider 0.5.1 → 0.6.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.
package/src/models.ts CHANGED
@@ -1,58 +1,33 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
2
2
  import { dirname } from "node:path"
3
3
 
4
- export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
4
+ import { MODEL_EFFORT_OVERRIDES } from "./commandcode-catalog-overrides.ts"
5
+ import {
6
+ MODEL_EFFORTS as CATALOG_MODEL_EFFORTS,
7
+ MODEL_INPUT_MODALITIES,
8
+ MODEL_MAX_OUTPUT_TOKENS,
9
+ MODEL_REASONING,
10
+ type CommandCodeInputType,
11
+ type CommandCodeReasoningEffort,
12
+ } from "./commandcode-catalog.ts"
13
+
14
+ /** Upstream CLI efforts with the manual overrides merged over them. */
15
+ export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
16
+ ...CATALOG_MODEL_EFFORTS,
17
+ ...MODEL_EFFORT_OVERRIDES,
18
+ }
19
+
20
+ export { MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS, MODEL_REASONING }
21
+ export type { CommandCodeInputType }
22
+
23
+ export const DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1"
24
+ export const DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`
5
25
  export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
6
26
 
7
27
  const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
8
28
  const MODEL_CACHE_VERSION = 1
9
29
 
10
- export type CommandCodeInputType = "text" | "image"
11
-
12
- /**
13
- * Model input modalities from the command-code@1.15.1 bundled catalog.
14
- * Models omitted here remain text-only so newly discovered IDs never claim
15
- * image support without upstream evidence.
16
- */
17
- export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {
18
- "MiniMaxAI/MiniMax-M3": ["text", "image"],
19
- "Qwen/Qwen3.6-Plus": ["text", "image"],
20
- "Qwen/Qwen3.7-Flash": ["text", "image"],
21
- "Qwen/Qwen3.7-Plus": ["text", "image"],
22
- "Qwen/Qwen3.8-Max": ["text", "image"],
23
- "claude-fable-5": ["text", "image"],
24
- "claude-haiku-4-5-20251001": ["text", "image"],
25
- "claude-opus-4-7": ["text", "image"],
26
- "claude-opus-4-8": ["text", "image"],
27
- "claude-opus-5": ["text", "image"],
28
- "claude-sonnet-4-6": ["text", "image"],
29
- "claude-sonnet-5": ["text", "image"],
30
- "google/gemini-3.1-flash-lite": ["text", "image"],
31
- "google/gemini-3.5-flash": ["text", "image"],
32
- "google/gemini-3.5-flash-lite": ["text", "image"],
33
- "google/gemini-3.6-flash": ["text", "image"],
34
- "gpt-5.3-codex": ["text", "image"],
35
- "gpt-5.4": ["text", "image"],
36
- "gpt-5.4-mini": ["text", "image"],
37
- "gpt-5.5": ["text", "image"],
38
- "gpt-5.6-luna": ["text", "image"],
39
- "gpt-5.6-sol": ["text", "image"],
40
- "gpt-5.6-terra": ["text", "image"],
41
- "meta/muse-spark-1.1": ["text", "image"],
42
- "meta/muse-spark-1.2": ["text", "image"],
43
- "meta/muse-spark-1.2-contributor": ["text", "image"],
44
- "moonshotai/Kimi-K2.5": ["text", "image"],
45
- "moonshotai/Kimi-K2.6": ["text", "image"],
46
- "moonshotai/Kimi-K2.7-Code": ["text", "image"],
47
- "moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
48
- "moonshotai/Kimi-K3": ["text", "image"],
49
- "sakana/fugu-ultra": ["text", "image"],
50
- "stepfun/Step-3.7-Flash": ["text", "image"],
51
- "thinkingmachines/inkling": ["text", "image"],
52
- "thinkingmachines/inkling-small": ["text", "image"],
53
- "xai/grok-4.5": ["text", "image"],
54
- "xiaomi/mimo-v2.5": ["text", "image"],
55
- }
30
+ export type CommandCodeApi = "openai-completions" | "anthropic-messages"
56
31
 
57
32
  const TEXT_INPUT_ONLY = ["text"] as const
58
33
 
@@ -66,43 +41,6 @@ export function modelSupportsImageInput(modelId: string): boolean {
66
41
 
67
42
  export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
68
43
 
69
- type CommandCodeReasoningEffort = Exclude<PiThinkingLevel, "off">
70
-
71
- /**
72
- * Per-model reasoning efforts supported by Command Code's generate endpoint.
73
- *
74
- * The Provider API does not expose reasoning metadata. This is an exact
75
- * snapshot of `reasoningEfforts` from the command-code@1.15.1 model catalog
76
- * (`packages/shared/src/model-catalog.ts`, also published in the generated
77
- * `dist/bundled/command-code-knowledge/reference/models.md`). Models omitted
78
- * here let Command Code choose their reasoning depth, matching the CLI.
79
- */
80
- export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
81
- "Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
82
- "claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
83
- "claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
84
- "claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
85
- "claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
86
- "claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
87
- "claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
88
- "deepseek/deepseek-v4-flash": ["high", "max"],
89
- "deepseek/deepseek-v4-pro": ["high", "max"],
90
- "gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
91
- "gpt-5.4": ["low", "medium", "high", "xhigh"],
92
- "gpt-5.4-mini": ["low", "medium", "high"],
93
- "gpt-5.5": ["low", "medium", "high", "xhigh"],
94
- "gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
95
- "gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
96
- "gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
97
- "google/gemini-3.1-flash-lite": ["low", "medium", "high"],
98
- "google/gemini-3.5-flash": ["low", "medium", "high"],
99
- "google/gemini-3.5-flash-lite": ["low", "medium", "high"],
100
- "google/gemini-3.6-flash": ["low", "medium", "high"],
101
- "sakana/fugu-ultra": ["high", "xhigh"],
102
- "xai/grok-4.5": ["low", "medium", "high"],
103
- "zai-org/GLM-5.2": ["high", "max"],
104
- }
105
-
106
44
  const PI_THINKING_LEVELS: readonly PiThinkingLevel[] = [
107
45
  "off",
108
46
  "minimal",
@@ -126,7 +64,7 @@ export function thinkingLevelMapForEfforts(
126
64
 
127
65
  export interface ThinkingMetadata {
128
66
  thinkingLevelMap: Partial<Record<PiThinkingLevel, string | null>>
129
- thinking: {
67
+ thinking?: {
130
68
  mode: "effort"
131
69
  effortMap: Partial<Record<CommandCodeReasoningEffort, string>>
132
70
  efforts: readonly CommandCodeReasoningEffort[]
@@ -135,19 +73,26 @@ export interface ThinkingMetadata {
135
73
 
136
74
  export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
137
75
  const efforts = MODEL_EFFORTS[modelId]
138
- if (!efforts) return undefined
139
- return {
140
- thinkingLevelMap: thinkingLevelMapForEfforts(efforts),
141
- thinking: {
142
- mode: "effort",
143
- effortMap: Object.fromEntries(efforts.map((effort) => [effort, effort])),
144
- efforts,
145
- },
76
+ if (efforts) {
77
+ return {
78
+ thinkingLevelMap: thinkingLevelMapForEfforts(efforts),
79
+ thinking: {
80
+ mode: "effort",
81
+ effortMap: Object.fromEntries(efforts.map((effort) => [effort, effort])),
82
+ efforts,
83
+ },
84
+ }
146
85
  }
86
+ if (!isReasoningModel(modelId)) return undefined
87
+ return { thinkingLevelMap: thinkingLevelMapForEfforts([]) }
147
88
  }
148
89
 
149
90
  function isReasoningModel(modelId: string): boolean {
150
- return MODEL_EFFORTS[modelId] !== undefined
91
+ return MODEL_REASONING[modelId] === true
92
+ }
93
+
94
+ function maxOutputTokensForModel(modelId: string, contextLength: number): number {
95
+ return Math.min(contextLength, MODEL_MAX_OUTPUT_TOKENS[modelId] ?? DEFAULT_MAX_OUTPUT_TOKENS)
151
96
  }
152
97
 
153
98
  interface ApiModel {
@@ -159,11 +104,22 @@ interface ApiModel {
159
104
  export interface CommandCodeModel {
160
105
  id: string
161
106
  name: string
107
+ api: CommandCodeApi
162
108
  reasoning: boolean
163
109
  contextWindow: number
164
110
  maxTokens: number
165
111
  }
166
112
 
113
+ export function apiForModelId(id: string): CommandCodeApi {
114
+ return id.startsWith("claude-") ? "anthropic-messages" : "openai-completions"
115
+ }
116
+
117
+ export function baseUrlForModel(apiBase: string, api: CommandCodeApi): string {
118
+ const normalized = apiBase.replace(/\/+$/g, "")
119
+ if (api !== "anthropic-messages") return normalized
120
+ return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized
121
+ }
122
+
167
123
  interface FetchCommandCodeModelsOptions {
168
124
  url?: string
169
125
  fetchImpl?: typeof fetch
@@ -222,12 +178,15 @@ function parseCachedModel(value: unknown): CommandCodeModel {
222
178
 
223
179
  const id = stringField(value, "id")
224
180
  booleanField(value, "reasoning")
181
+ positiveNumberField(value, "maxTokens")
182
+ const contextWindow = positiveNumberField(value, "contextWindow")
225
183
  return {
226
184
  id,
227
185
  name: stringField(value, "name"),
186
+ api: apiForModelId(id),
228
187
  reasoning: isReasoningModel(id),
229
- contextWindow: positiveNumberField(value, "contextWindow"),
230
- maxTokens: positiveNumberField(value, "maxTokens"),
188
+ contextWindow,
189
+ maxTokens: maxOutputTokensForModel(id, contextWindow),
231
190
  }
232
191
  }
233
192
 
@@ -329,9 +288,10 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
329
288
  return data.map(parseApiModel).map((model) => ({
330
289
  id: model.id,
331
290
  name: `${model.name} (CC)`,
291
+ api: apiForModelId(model.id),
332
292
  reasoning: isReasoningModel(model.id),
333
293
  contextWindow: model.contextLength,
334
- maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
294
+ maxTokens: maxOutputTokensForModel(model.id, model.contextLength),
335
295
  }))
336
296
  }
337
297
 
@@ -379,6 +339,17 @@ async function readCommandCodeModelsCache(cachePath: string): Promise<readonly C
379
339
  return commandCodeModelsFromCache(parsed)
380
340
  }
381
341
 
342
+ /** Reads the cached catalog without touching the network; empty when missing or invalid. */
343
+ export async function loadCachedCommandCodeModels(
344
+ cachePath: string,
345
+ ): Promise<readonly CommandCodeModel[]> {
346
+ try {
347
+ return await readCommandCodeModelsCache(cachePath)
348
+ } catch {
349
+ return []
350
+ }
351
+ }
352
+
382
353
  async function writeCommandCodeModelsCache(
383
354
  cachePath: string,
384
355
  models: readonly CommandCodeModel[],
package/src/oauth.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Command Code OAuth provider for pi's /login flow.
3
3
  *
4
- * Implements a browser-assisted API key retrieval flow:
5
- * 1. Starts a local HTTP server on a Command Code CLI-compatible port
6
- * 2. Opens the Command Code Studio auth page in the browser
7
- * 3. The user authenticates on the Command Code website
8
- * 4. The website POSTs the API key back to the local server
9
- * 5. If browser transfer fails, the user can paste the API key manually
10
- * 6. The API key is stored in pi's auth.json as OAuth credentials
4
+ * Implements two API key retrieval flows:
5
+ * 1. Browser-assisted login opens Command Code Studio and waits for the
6
+ * website to POST the API key back to a local callback server.
7
+ * 2. Direct API key login prompts the user to paste a Studio API key.
8
+ *
9
+ * If browser transfer fails, the user can still paste the API key manually.
10
+ * The API key is stored in pi's auth.json as OAuth credentials.
11
11
  *
12
12
  * Since Command Code API keys don't expire, we store them as
13
13
  * OAuth credentials with a far-future expiry.
@@ -18,7 +18,8 @@ import { startAuthServer } from "./auth-server.ts"
18
18
 
19
19
  const STUDIO_BASE_URL = "https://commandcode.ai"
20
20
  const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000 // API keys don't expire
21
- const DEFAULT_AUTH_TIMEOUT_MS = 15_000
21
+ const DEFAULT_AUTH_TIMEOUT_MS = 120_000
22
+ const DEFAULT_API_BASE = "https://api.commandcode.ai"
22
23
 
23
24
  export interface OAuthLoginCallbacks {
24
25
  onAuth(params: { url: string }): void
@@ -95,22 +96,70 @@ export function sanitizeApiKey(input: string): string {
95
96
  .trim()
96
97
  }
97
98
 
99
+ export async function validateApiKey(
100
+ apiKey: string,
101
+ options: { fetchImpl?: typeof fetch; apiBase?: string } = {},
102
+ ): Promise<void> {
103
+ let response: Response
104
+ try {
105
+ response = await (options.fetchImpl ?? fetch)(
106
+ `${options.apiBase ?? DEFAULT_API_BASE}/alpha/whoami`,
107
+ {
108
+ headers: { Authorization: `Bearer ${apiKey}` },
109
+ },
110
+ )
111
+ } catch (error) {
112
+ throw new Error(
113
+ `Could not validate the Command Code API key: ${error instanceof Error ? error.message : String(error)}`,
114
+ )
115
+ }
116
+
117
+ if (response.status === 401) throw new Error("Invalid Command Code API key")
118
+ if (!response.ok) {
119
+ throw new Error(`Could not validate the Command Code API key (${response.status})`)
120
+ }
121
+ }
122
+
98
123
  async function promptForApiKey(callbacks: OAuthLoginCallbacks, message: string) {
99
124
  const apiKey = sanitizeApiKey(await callbacks.onPrompt({ message }))
100
125
  if (!apiKey) throw new Error("No Command Code API key provided")
126
+ await validateApiKey(apiKey)
101
127
  return credentialsFromApiKey(apiKey)
102
128
  }
103
129
 
104
- /**
105
- * Starts the browser-based login flow for Command Code.
106
- *
107
- * Returns OAuth credentials where access == refresh == the user's API key.
108
- * The keys don't expire, so we set a far-future expiry.
109
- */
110
- export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
130
+ type LoginChoice = { type: "browser" } | { type: "prompt" } | { type: "apiKey"; apiKey: string }
131
+
132
+ async function chooseLoginFlow(callbacks: OAuthLoginCallbacks): Promise<LoginChoice> {
133
+ const input = sanitizeApiKey(
134
+ await callbacks.onPrompt({
135
+ message:
136
+ "Command Code login: press Enter for browser login, type 'key' to paste an API key, or paste the API key directly:",
137
+ }),
138
+ )
139
+ const normalized = input.toLowerCase()
140
+
141
+ if (!input || normalized === "1" || normalized === "b" || normalized === "browser") {
142
+ return { type: "browser" }
143
+ }
144
+
145
+ if (
146
+ normalized === "2" ||
147
+ normalized === "k" ||
148
+ normalized === "key" ||
149
+ normalized === "api" ||
150
+ normalized === "paste"
151
+ ) {
152
+ return { type: "prompt" }
153
+ }
154
+
155
+ return { type: "apiKey", apiKey: input }
156
+ }
157
+
158
+ async function browserLogin(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
159
+ const stateToken = generateStateToken()
111
160
  let authServer
112
161
  try {
113
- authServer = await startAuthServer()
162
+ authServer = await startAuthServer({ expectedState: stateToken })
114
163
  } catch {
115
164
  return promptForApiKey(
116
165
  callbacks,
@@ -118,7 +167,6 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
118
167
  )
119
168
  }
120
169
 
121
- const stateToken = generateStateToken()
122
170
  const callbackUrl = `http://localhost:${authServer.port}/callback`
123
171
  const authUrl = `${STUDIO_BASE_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(stateToken)}`
124
172
 
@@ -142,13 +190,27 @@ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCreden
142
190
  throw error
143
191
  }
144
192
 
145
- // Validate state token to prevent CSRF.
146
- if (callback.state !== stateToken) {
147
- authServer.server.close()
148
- throw new Error("State token mismatch. Authentication may have been tampered with.")
193
+ return credentialsFromApiKey(callback.apiKey)
194
+ }
195
+
196
+ /**
197
+ * Starts the login flow for Command Code.
198
+ *
199
+ * Returns OAuth credentials where access == refresh == the user's API key.
200
+ * The keys don't expire, so we set a far-future expiry.
201
+ */
202
+ export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
203
+ const choice = await chooseLoginFlow(callbacks)
204
+
205
+ if (choice.type === "apiKey") {
206
+ await validateApiKey(choice.apiKey)
207
+ return credentialsFromApiKey(choice.apiKey)
208
+ }
209
+ if (choice.type === "prompt") {
210
+ return promptForApiKey(callbacks, "Paste your Command Code API key:")
149
211
  }
150
212
 
151
- return credentialsFromApiKey(callback.apiKey)
213
+ return browserLogin(callbacks)
152
214
  }
153
215
 
154
216
  /**
package/src/pricing.ts CHANGED
@@ -20,7 +20,7 @@ export interface TemporaryPricing {
20
20
  }
21
21
 
22
22
  export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits"
23
- export const PRICING_LAST_VERIFIED = "2026-08-04"
23
+ export const PRICING_LAST_VERIFIED = "2026-09-01"
24
24
 
25
25
  export const ZERO_MODEL_COST: CommandCodeModelCost = {
26
26
  input: 0,
@@ -40,10 +40,10 @@ export const ZERO_MODEL_COST: CommandCodeModelCost = {
40
40
  export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
41
41
  // Free models
42
42
  "poolside/laguna-s-2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
43
- "inclusionai/ling-3.0-flash-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
44
43
 
45
44
  // Open and open-weight models
46
45
  "tencent/hy3-paid": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
46
+ "tencent/hy4-preview": { input: 0.834, output: 2.501, cacheRead: 0.042, cacheWrite: 0 },
47
47
  "moonshotai/Kimi-K3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
48
48
  "moonshotai/Kimi-K2.7-Code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
49
49
  "moonshotai/Kimi-K2.7-Code-Highspeed": {
@@ -54,6 +54,8 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
54
54
  },
55
55
  "moonshotai/Kimi-K2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
56
56
  "moonshotai/Kimi-K2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
57
+ "z-ai/glm-5.3-flash": { input: 0.15, output: 0.5, cacheRead: 0.03, cacheWrite: 0 },
58
+ "zai-org/GLM-5.3": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
57
59
  "zai-org/GLM-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
58
60
  "zai-org/GLM-5.2-Fast": { input: 3, output: 10.25, cacheRead: 0.5, cacheWrite: 0 },
59
61
  "zai-org/GLM-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
@@ -61,20 +63,35 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
61
63
  "MiniMaxAI/MiniMax-M3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
62
64
  "MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
63
65
  "MiniMaxAI/MiniMax-M2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
64
- // Permanent 75% discount.
66
+ // DeepSeek V4 uses time-dependent rates. Display the documented off-peak
67
+ // rates, which apply for 17 hours per day; the Usage page remains authoritative.
65
68
  "deepseek/deepseek-v4-pro": {
66
- input: 0.435,
67
- output: 0.87,
68
- cacheRead: 0.003625,
69
+ input: 0.66,
70
+ output: 1.98,
71
+ cacheRead: 0.022,
69
72
  cacheWrite: 0,
70
73
  },
71
74
  "deepseek/deepseek-v4-flash": {
72
- input: 0.14,
73
- output: 0.28,
74
- cacheRead: 0.0028,
75
+ input: 0.22,
76
+ output: 0.66,
77
+ cacheRead: 0.007,
78
+ cacheWrite: 0,
79
+ },
80
+ "deepseek/deepseek-v4-flash-vision-exp": {
81
+ input: 0.22,
82
+ output: 0.66,
83
+ cacheRead: 0.007,
84
+ cacheWrite: 0,
85
+ },
86
+ "deepseek/deepseek-v4-flash-fast": {
87
+ input: 0.28,
88
+ output: 0.56,
89
+ cacheRead: 0.07,
75
90
  cacheWrite: 0,
76
91
  },
77
92
  "Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
93
+ "Qwen/Qwen3.8-27B": { input: 0.4, output: 3, cacheRead: 0.04, cacheWrite: 0 },
94
+ "Qwen/Qwen3.8-Flash": { input: 0.16, output: 0.47, cacheRead: 0.016, cacheWrite: 0 },
78
95
  "Qwen/Qwen3.7-Max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.13 },
79
96
  "Qwen/Qwen3.7-Plus": {
80
97
  input: 0.4,
@@ -140,11 +157,18 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
140
157
  cacheWrite: 0,
141
158
  },
142
159
  "meta/muse-spark-1.1": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
160
+ "meta/muse-spark-1.2": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
161
+ "meta/muse-spark-1.2-contributor": {
162
+ input: 0.1,
163
+ output: 0.2,
164
+ cacheRead: 0.002,
165
+ cacheWrite: 0,
166
+ },
143
167
 
144
168
  // Anthropic
145
- // Introductory pricing through 2026-08-31.
146
169
  "claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
147
170
  "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
171
+ "claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
148
172
  "claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
149
173
  "claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
150
174
  "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
@@ -158,43 +182,20 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
158
182
 
159
183
  // OpenAI
160
184
  "gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
161
- // Discounted rates through 2026-08-14.
162
- "gpt-5.6-terra": {
163
- input: 1,
164
- output: 6,
165
- cacheRead: 0.1,
166
- cacheWrite: 1.25,
167
- tiers: [
168
- {
169
- inputTokensAbove: 272_000,
170
- input: 2,
171
- output: 9,
172
- cacheRead: 0.2,
173
- cacheWrite: 2.5,
174
- },
175
- ],
176
- },
177
- "gpt-5.6-luna": {
178
- input: 0.1,
179
- output: 0.6,
180
- cacheRead: 0.01,
181
- cacheWrite: 0.125,
182
- tiers: [
183
- {
184
- inputTokensAbove: 272_000,
185
- input: 0.2,
186
- output: 0.9,
187
- cacheRead: 0.02,
188
- cacheWrite: 0.25,
189
- },
190
- ],
191
- },
185
+ "gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
186
+ "gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
192
187
  "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
193
188
  "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
194
189
  "gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
195
190
  "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
196
191
 
197
192
  // Google and xAI
193
+ "google/gemini-3.7-flash": {
194
+ input: 1.5,
195
+ output: 7.5,
196
+ cacheRead: 0.15,
197
+ cacheWrite: 0.08334,
198
+ },
198
199
  "google/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
199
200
  "google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
200
201
  "google/gemini-3.5-flash-lite": {
@@ -210,17 +211,21 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
210
211
  cacheWrite: 0,
211
212
  },
212
213
  "xai/grok-4.5": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
214
+ "xai/grok-4.6": {
215
+ input: 2,
216
+ output: 6,
217
+ cacheRead: 0.5,
218
+ cacheWrite: 0,
219
+ tiers: [
220
+ {
221
+ inputTokensAbove: 200_000,
222
+ input: 4,
223
+ output: 12,
224
+ cacheRead: 1,
225
+ cacheWrite: 0,
226
+ },
227
+ ],
228
+ },
213
229
  }
214
230
 
215
- export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
216
- {
217
- models: ["gpt-5.6-terra", "gpt-5.6-luna"],
218
- expiresOn: "2026-08-14",
219
- description: "50% promotional rates",
220
- },
221
- {
222
- models: ["claude-sonnet-5"],
223
- expiresOn: "2026-08-31",
224
- description: "introductory pricing",
225
- },
226
- ]
231
+ export const TEMPORARY_PRICING: readonly TemporaryPricing[] = []
@@ -0,0 +1,66 @@
1
+ import { getConfiguredApiKey } from "./api-key.ts"
2
+ import { pickCommandCodeApiKey } from "./converters.ts"
3
+ import { fetchCommandCodeQuota, redactValue } from "./quota.ts"
4
+ import { formatQuota } from "./quota-format.ts"
5
+
6
+ export interface QuotaCommandContext {
7
+ waitForIdle?: () => Promise<void>
8
+ modelRegistry?: {
9
+ getApiKeyForProvider?: (provider: string) => Promise<string | undefined>
10
+ }
11
+ ui: {
12
+ notify(message: string, type?: "info" | "warning" | "error"): void
13
+ }
14
+ }
15
+
16
+ interface QuotaCommandApi {
17
+ registerCommand(
18
+ name: string,
19
+ options: {
20
+ description: string
21
+ handler: (args: string, ctx: QuotaCommandContext) => Promise<void>
22
+ },
23
+ ): void
24
+ }
25
+
26
+ interface RegisterQuotaCommandOptions {
27
+ apiBase: string
28
+ headers?: Record<string, string>
29
+ getConfiguredKey?: () => string | undefined
30
+ fetchQuota?: typeof fetchCommandCodeQuota
31
+ }
32
+
33
+ export function registerCommandCodeQuota(
34
+ pi: QuotaCommandApi,
35
+ options: RegisterQuotaCommandOptions,
36
+ ): void {
37
+ const getConfiguredKey = options.getConfiguredKey ?? getConfiguredApiKey
38
+ const fetchQuota = options.fetchQuota ?? fetchCommandCodeQuota
39
+
40
+ pi.registerCommand("commandcode-quota", {
41
+ description: "Show Command Code account usage and quota",
42
+ handler: async (_args, ctx) => {
43
+ await ctx.waitForIdle?.()
44
+ const registryKey = await ctx.modelRegistry?.getApiKeyForProvider?.("commandcode")
45
+ const apiKey = pickCommandCodeApiKey(registryKey, getConfiguredKey())
46
+ if (!apiKey) {
47
+ ctx.ui.notify(
48
+ "Command Code quota requires an API key. Run /login and select Command Code, or set COMMAND_CODE_API_KEY.",
49
+ "warning",
50
+ )
51
+ return
52
+ }
53
+
54
+ const result = await fetchQuota({
55
+ apiKey,
56
+ baseUrl: options.apiBase,
57
+ extraHeaders: options.headers,
58
+ })
59
+ if (!result.ok) {
60
+ ctx.ui.notify(redactValue(result.error.message), "error")
61
+ return
62
+ }
63
+ ctx.ui.notify(formatQuota(result.quota), "info")
64
+ },
65
+ })
66
+ }