pi-commandcode-provider 0.4.3 → 0.5.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
@@ -2,10 +2,154 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
2
2
  import { dirname } from "node:path"
3
3
 
4
4
  export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
5
+ export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
5
6
 
6
7
  const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
7
8
  const MODEL_CACHE_VERSION = 1
8
9
 
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
+ }
56
+
57
+ const TEXT_INPUT_ONLY = ["text"] as const
58
+
59
+ export function inputModalitiesForModel(modelId: string): readonly CommandCodeInputType[] {
60
+ return MODEL_INPUT_MODALITIES[modelId] ?? TEXT_INPUT_ONLY
61
+ }
62
+
63
+ export function modelSupportsImageInput(modelId: string): boolean {
64
+ return inputModalitiesForModel(modelId).includes("image")
65
+ }
66
+
67
+ export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
68
+
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
+ const PI_THINKING_LEVELS: readonly PiThinkingLevel[] = [
107
+ "off",
108
+ "minimal",
109
+ "low",
110
+ "medium",
111
+ "high",
112
+ "xhigh",
113
+ "max",
114
+ ]
115
+
116
+ export function thinkingLevelMapForEfforts(
117
+ efforts: readonly string[],
118
+ ): Partial<Record<PiThinkingLevel, string | null>> {
119
+ const map: Partial<Record<PiThinkingLevel, string | null>> = {}
120
+ for (const level of PI_THINKING_LEVELS) {
121
+ if (level === "off") continue
122
+ map[level] = efforts.includes(level) ? level : null
123
+ }
124
+ return map
125
+ }
126
+
127
+ export interface ThinkingMetadata {
128
+ thinkingLevelMap: Partial<Record<PiThinkingLevel, string | null>>
129
+ thinking: {
130
+ mode: "effort"
131
+ effortMap: Partial<Record<CommandCodeReasoningEffort, string>>
132
+ efforts: readonly CommandCodeReasoningEffort[]
133
+ }
134
+ }
135
+
136
+ export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
137
+ 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
+ },
146
+ }
147
+ }
148
+
149
+ function isReasoningModel(modelId: string): boolean {
150
+ return MODEL_EFFORTS[modelId] !== undefined
151
+ }
152
+
9
153
  interface ApiModel {
10
154
  id: string
11
155
  name: string
@@ -23,6 +167,8 @@ export interface CommandCodeModel {
23
167
  interface FetchCommandCodeModelsOptions {
24
168
  url?: string
25
169
  fetchImpl?: typeof fetch
170
+ signal?: AbortSignal
171
+ timeoutMs?: number
26
172
  }
27
173
 
28
174
  interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
@@ -74,10 +220,12 @@ function parseApiModel(value: unknown): ApiModel {
74
220
  function parseCachedModel(value: unknown): CommandCodeModel {
75
221
  if (!isRecord(value)) throw new Error("Expected cached model entry to be an object")
76
222
 
223
+ const id = stringField(value, "id")
224
+ booleanField(value, "reasoning")
77
225
  return {
78
- id: stringField(value, "id"),
226
+ id,
79
227
  name: stringField(value, "name"),
80
- reasoning: booleanField(value, "reasoning"),
228
+ reasoning: isReasoningModel(id),
81
229
  contextWindow: positiveNumberField(value, "contextWindow"),
82
230
  maxTokens: positiveNumberField(value, "maxTokens"),
83
231
  }
@@ -92,6 +240,85 @@ function errorMessage(error: unknown): string {
92
240
  return error instanceof Error ? error.message : String(error)
93
241
  }
94
242
 
243
+ function abortError(reason: unknown): Error {
244
+ if (reason instanceof Error) return reason
245
+ return new DOMException("The operation was aborted", "AbortError")
246
+ }
247
+
248
+ function configuredTimeoutMs(timeoutMs: number | undefined): number {
249
+ return timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0
250
+ ? timeoutMs
251
+ : DEFAULT_MODELS_TIMEOUT_MS
252
+ }
253
+
254
+ export function getModelsTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
255
+ const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS
256
+ if (!raw) return DEFAULT_MODELS_TIMEOUT_MS
257
+
258
+ const parsed = Number(raw)
259
+ return configuredTimeoutMs(parsed)
260
+ }
261
+
262
+ class ModelDiscoveryTimeoutError extends Error {
263
+ constructor(timeoutMs: number) {
264
+ super(`Command Code model discovery timed out after ${timeoutMs}ms`)
265
+ this.name = "ModelDiscoveryTimeoutError"
266
+ }
267
+ }
268
+
269
+ function runWithTimeout<T>(
270
+ operation: (signal: AbortSignal) => Promise<T>,
271
+ timeoutMs: number,
272
+ externalSignal: AbortSignal | undefined,
273
+ ): Promise<T> {
274
+ const controller = new AbortController()
275
+ let timer: ReturnType<typeof setTimeout> | undefined
276
+ let settled = false
277
+ let onExternalAbort: (() => void) | undefined
278
+
279
+ return new Promise<T>((resolve, reject) => {
280
+ const cleanup = () => {
281
+ if (timer !== undefined) clearTimeout(timer)
282
+ if (onExternalAbort && externalSignal) {
283
+ externalSignal.removeEventListener("abort", onExternalAbort)
284
+ }
285
+ }
286
+
287
+ const resolveOnce = (value: T) => {
288
+ if (settled) return
289
+ settled = true
290
+ cleanup()
291
+ resolve(value)
292
+ }
293
+
294
+ const rejectOnce = (error: unknown) => {
295
+ if (settled) return
296
+ settled = true
297
+ cleanup()
298
+ reject(error)
299
+ }
300
+
301
+ const abort = (reason: unknown) => {
302
+ const error = abortError(reason)
303
+ controller.abort(error)
304
+ rejectOnce(error)
305
+ }
306
+
307
+ if (externalSignal?.aborted) {
308
+ abort(externalSignal.reason)
309
+ return
310
+ }
311
+
312
+ onExternalAbort = () => abort(externalSignal?.reason)
313
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true })
314
+ timer = setTimeout(() => abort(new ModelDiscoveryTimeoutError(timeoutMs)), timeoutMs)
315
+
316
+ Promise.resolve()
317
+ .then(() => operation(controller.signal))
318
+ .then(resolveOnce, rejectOnce)
319
+ })
320
+ }
321
+
95
322
  export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
96
323
  if (!isRecord(value)) throw new Error("Expected models response to be an object")
97
324
  if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
@@ -102,7 +329,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
102
329
  return data.map(parseApiModel).map((model) => ({
103
330
  id: model.id,
104
331
  name: `${model.name} (CC)`,
105
- reasoning: true,
332
+ reasoning: isReasoningModel(model.id),
106
333
  contextWindow: model.contextLength,
107
334
  maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
108
335
  }))
@@ -123,19 +350,26 @@ export async function fetchCommandCodeModels(
123
350
  ): Promise<readonly CommandCodeModel[]> {
124
351
  const url = options.url ?? DEFAULT_MODELS_URL
125
352
  const fetchImpl = options.fetchImpl ?? fetch
126
- const response = await fetchImpl(url, {
127
- headers: {
128
- accept: "application/json",
129
- },
130
- })
353
+ const body: unknown = await runWithTimeout(
354
+ async (signal) => {
355
+ const response = await fetchImpl(url, {
356
+ headers: {
357
+ accept: "application/json",
358
+ },
359
+ signal,
360
+ })
131
361
 
132
- if (!response.ok) {
133
- throw new Error(
134
- `Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
135
- )
136
- }
362
+ if (!response.ok) {
363
+ throw new Error(
364
+ `Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
365
+ )
366
+ }
137
367
 
138
- const body: unknown = await response.json()
368
+ return await response.json()
369
+ },
370
+ configuredTimeoutMs(options.timeoutMs),
371
+ options.signal,
372
+ )
139
373
  return requireModels(commandCodeModelsFromApiResponse(body))
140
374
  }
141
375
 
@@ -187,6 +421,8 @@ export async function loadCommandCodeModels(
187
421
  }
188
422
  }
189
423
  } catch (liveError) {
424
+ if (options.signal?.aborted) throw abortError(options.signal.reason ?? liveError)
425
+
190
426
  try {
191
427
  const models = await readCommandCodeModelsCache(cachePath)
192
428
  return {
@@ -198,7 +434,7 @@ export async function loadCommandCodeModels(
198
434
  return {
199
435
  models: [],
200
436
  source: "empty",
201
- warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}), and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Command Code models will remain unavailable until /reload succeeds.`,
437
+ warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}), and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Command Code models will remain unavailable until /commandcode-refresh succeeds.`,
202
438
  }
203
439
  }
204
440
  }
@@ -0,0 +1,120 @@
1
+ const COMMAND_CODE_PROVIDER = "commandcode"
2
+ const CONTEXT_OVERFLOW_PREFIX = "context_length_exceeded:"
3
+
4
+ const COMMAND_CODE_OVERFLOW_PATTERNS = [
5
+ /\b(?:context[_\s-]*(?:length|window)|model[_\s-]*context[_\s-]*window)[_\s-]*(?:exceeded|overflow(?:ed)?|too[_\s-]*(?:large|long))\b/i,
6
+ /\b(?:context|prompt|input)[_\s-]*(?:length|window|size|tokens?|limit|maximum)\b[\s\S]{0,120}\b(?:exceed(?:ed|s)?|overflow(?:ed|s)?|too\s+(?:large|long)|(?:maximum|limit)\s+(?:reached|exceeded|hit))\b/i,
7
+ /\b(?:exceed(?:ed|s)?|overflow(?:ed|s)?|too\s+(?:large|long))\b[\s\S]{0,120}\b(?:context|prompt|input)[_\s-]*(?:length|window|size|tokens?|limit|maximum)\b/i,
8
+ /\b(?:prompt|input|context)\b[\s\S]{0,32}\btoo\s+(?:large|long)\b/i,
9
+ /\b(?:prompt|input)[_\s-]*too[_\s-]*(?:large|long)\b/i,
10
+ /\b(?:prompt|input)[_\s-]*tokens?[_\s-]*(?:limit|maximum|max)[_\s-]*(?:exceeded|reached)\b/i,
11
+ /\b(?:prompt|input)[_\s-]*(?:tokens?|length|size)\b[\s\S]{0,120}\b(?:limit|maximum)\b[\s\S]{0,40}\b(?:exceed(?:ed|s)?|reached|hit)\b/i,
12
+ /\b(?:maximum|limit)[_\s-]+(?:allowed[_\s-]+)?(?:context|prompt|input)[_\s-]*(?:length|window|size|tokens?)\b/i,
13
+ ]
14
+
15
+ const NON_OVERFLOW_PATTERNS = [
16
+ /\brate[_\s-]*limit\b/i,
17
+ /\btoo\s+many\s+requests\b/i,
18
+ /\b(?:capacity|quota|throttl(?:e|ed|ing)?|concurren(?:cy|t)|overloaded)\b/i,
19
+ /\b(?:service|temporarily)\s+unavailable\b/i,
20
+ /\bstatus(?:[_\s-]*code)?\s*[:=]\s*429\b/i,
21
+ ]
22
+
23
+ const CONTEXT_OVERFLOW_PREFIX_PATTERN = /context_length_exceeded/i
24
+
25
+ const HTTP_RATE_LIMIT_STATUS_PATTERNS = [
26
+ /\b(?:api\s+error|http|status(?:[_\s-]*code)?|status[_\s-]*code)\s*[:(]?\s*429\b/i,
27
+ /["']?(?:status|status[_\s-]*code)["']?\s*:\s*429\b/i,
28
+ ]
29
+
30
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi
31
+ const CREDENTIAL_PATTERN =
32
+ /\b(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*[=:]\s*[^\s,;)]+/gi
33
+ const USER_TOKEN_PATTERN = /\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi
34
+ const QUERY_SECRET_PATTERN =
35
+ /([?&](?:api[-_ ]?key|apikey|access_token|refresh_token|token|secret|password)=)[^&#\s]+/gi
36
+ const STANDALONE_SECRET_PATTERN =
37
+ /\b(?:sk|rk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{16,}\b|\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
38
+
39
+ export function redactCommandCodeErrorText(value: string): string {
40
+ return value
41
+ .replace(BEARER_PATTERN, "Bearer [redacted]")
42
+ .replace(CREDENTIAL_PATTERN, (match) => {
43
+ const separatorIndex = match.search(/[=:]/)
44
+ return separatorIndex < 0 ? "[redacted]" : `${match.slice(0, separatorIndex + 1)}[redacted]`
45
+ })
46
+ .replace(USER_TOKEN_PATTERN, "[redacted]")
47
+ .replace(QUERY_SECRET_PATTERN, "$1[redacted]")
48
+ .replace(STANDALONE_SECRET_PATTERN, "[redacted]")
49
+ }
50
+
51
+ function isRecord(value: unknown): value is Record<string, unknown> {
52
+ return typeof value === "object" && value !== null
53
+ }
54
+
55
+ export interface CommandCodeMessageLike {
56
+ role: string
57
+ provider: string
58
+ stopReason: string
59
+ errorMessage?: string
60
+ }
61
+
62
+ export function commandCodeErrorMessage(value: unknown): string | undefined {
63
+ if (typeof value === "string") return value
64
+ if (!isRecord(value)) return undefined
65
+
66
+ const record = value
67
+ const parts: string[] = []
68
+ for (const key of [
69
+ "message",
70
+ "errorMessage",
71
+ "error",
72
+ "detail",
73
+ "details",
74
+ "code",
75
+ "type",
76
+ "reason",
77
+ ]) {
78
+ const part = commandCodeErrorMessage(record[key])
79
+ if (part && !parts.includes(part)) parts.push(part)
80
+ }
81
+
82
+ for (const key of ["status", "statusCode", "httpStatus"]) {
83
+ const status = record[key]
84
+ if (typeof status === "string" || typeof status === "number") {
85
+ const statusPart = `status: ${status}`
86
+ if (!parts.includes(statusPart)) parts.push(statusPart)
87
+ }
88
+ }
89
+
90
+ return parts.length > 0 ? redactCommandCodeErrorText(parts.join(": ")) : undefined
91
+ }
92
+
93
+ export function normalizeCommandCodeErrorMessage(
94
+ errorMessage: string | undefined,
95
+ ): string | undefined {
96
+ if (!errorMessage) return undefined
97
+ if (CONTEXT_OVERFLOW_PREFIX_PATTERN.test(errorMessage)) return undefined
98
+ if (NON_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage))) return undefined
99
+ if (HTTP_RATE_LIMIT_STATUS_PATTERNS.some((pattern) => pattern.test(errorMessage)))
100
+ return undefined
101
+ if (!COMMAND_CODE_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage)))
102
+ return undefined
103
+
104
+ return `${CONTEXT_OVERFLOW_PREFIX} ${errorMessage}`
105
+ }
106
+
107
+ export function normalizeCommandCodeMessage<T extends CommandCodeMessageLike>(
108
+ message: T,
109
+ modelProvider?: string,
110
+ ): { message: T & { errorMessage: string } } | undefined {
111
+ if (message.role !== "assistant" || message.stopReason !== "error") return undefined
112
+ if (message.provider !== COMMAND_CODE_PROVIDER && modelProvider !== COMMAND_CODE_PROVIDER) {
113
+ return undefined
114
+ }
115
+
116
+ const errorMessage = normalizeCommandCodeErrorMessage(message.errorMessage)
117
+ if (!errorMessage) return undefined
118
+
119
+ return { message: { ...message, errorMessage } }
120
+ }
package/src/pricing.ts ADDED
@@ -0,0 +1,226 @@
1
+ export interface CommandCodeModelCostRates {
2
+ input: number
3
+ output: number
4
+ cacheRead: number
5
+ cacheWrite: number
6
+ }
7
+
8
+ export interface CommandCodeModelCostTier extends CommandCodeModelCostRates {
9
+ inputTokensAbove: number
10
+ }
11
+
12
+ export interface CommandCodeModelCost extends CommandCodeModelCostRates {
13
+ tiers?: readonly CommandCodeModelCostTier[]
14
+ }
15
+
16
+ export interface TemporaryPricing {
17
+ models: readonly string[]
18
+ expiresOn: string
19
+ description: string
20
+ }
21
+
22
+ export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits"
23
+ export const PRICING_LAST_VERIFIED = "2026-08-04"
24
+
25
+ export const ZERO_MODEL_COST: CommandCodeModelCost = {
26
+ input: 0,
27
+ output: 0,
28
+ cacheRead: 0,
29
+ cacheWrite: 0,
30
+ }
31
+
32
+ /**
33
+ * Display prices in USD per million tokens.
34
+ *
35
+ * Context-dependent rates use pi's request-wide input pricing tiers. The
36
+ * highest threshold exceeded by input + cache reads + cache writes applies to
37
+ * the full request. The Command Code usage page remains authoritative for the
38
+ * amount billed for an individual request.
39
+ */
40
+ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
41
+ // Free models
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
+
45
+ // Open and open-weight models
46
+ "tencent/hy3-paid": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
47
+ "moonshotai/Kimi-K3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
48
+ "moonshotai/Kimi-K2.7-Code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
49
+ "moonshotai/Kimi-K2.7-Code-Highspeed": {
50
+ input: 1.9,
51
+ output: 8,
52
+ cacheRead: 0.38,
53
+ cacheWrite: 0,
54
+ },
55
+ "moonshotai/Kimi-K2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
56
+ "moonshotai/Kimi-K2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
57
+ "zai-org/GLM-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
58
+ "zai-org/GLM-5.2-Fast": { input: 3, output: 10.25, cacheRead: 0.5, cacheWrite: 0 },
59
+ "zai-org/GLM-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
60
+ "zai-org/GLM-5": { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 },
61
+ "MiniMaxAI/MiniMax-M3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
62
+ "MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
63
+ "MiniMaxAI/MiniMax-M2.5": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
64
+ // Permanent 75% discount.
65
+ "deepseek/deepseek-v4-pro": {
66
+ input: 0.435,
67
+ output: 0.87,
68
+ cacheRead: 0.003625,
69
+ cacheWrite: 0,
70
+ },
71
+ "deepseek/deepseek-v4-flash": {
72
+ input: 0.14,
73
+ output: 0.28,
74
+ cacheRead: 0.0028,
75
+ cacheWrite: 0,
76
+ },
77
+ "Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
78
+ "Qwen/Qwen3.7-Max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.13 },
79
+ "Qwen/Qwen3.7-Plus": {
80
+ input: 0.4,
81
+ output: 1.6,
82
+ cacheRead: 0.08,
83
+ cacheWrite: 0.5,
84
+ tiers: [
85
+ {
86
+ inputTokensAbove: 256_000,
87
+ input: 1.2,
88
+ output: 4.8,
89
+ cacheRead: 0.24,
90
+ cacheWrite: 1.5,
91
+ },
92
+ ],
93
+ },
94
+ "Qwen/Qwen3.7-Flash": {
95
+ input: 0.03,
96
+ output: 0.13,
97
+ cacheRead: 0.006,
98
+ cacheWrite: 0.038,
99
+ tiers: [
100
+ {
101
+ inputTokensAbove: 32_000,
102
+ input: 0.1,
103
+ output: 0.4,
104
+ cacheRead: 0.02,
105
+ cacheWrite: 0.125,
106
+ },
107
+ {
108
+ inputTokensAbove: 256_000,
109
+ input: 0.2,
110
+ output: 0.8,
111
+ cacheRead: 0.04,
112
+ cacheWrite: 0.25,
113
+ },
114
+ ],
115
+ },
116
+ "Qwen/Qwen3.6-Max-Preview": {
117
+ input: 1.3,
118
+ output: 7.8,
119
+ cacheRead: 0.26,
120
+ cacheWrite: 1.63,
121
+ },
122
+ "Qwen/Qwen3.6-Plus": { input: 0.5, output: 3, cacheRead: 0.1, cacheWrite: 0 },
123
+ "stepfun/Step-3.7-Flash": { input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0 },
124
+ "stepfun/Step-3.5-Flash": { input: 0.1, output: 0.3, cacheRead: 0.02, cacheWrite: 0 },
125
+ // Permanent discounted rates.
126
+ "xiaomi/mimo-v2.5-pro": { input: 0.435, output: 0.87, cacheRead: 0.0036, cacheWrite: 0 },
127
+ "xiaomi/mimo-v2.5": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
128
+ "nvidia/nemotron-3-ultra-550b-a55b": {
129
+ input: 0.6,
130
+ output: 2.4,
131
+ cacheRead: 0.12,
132
+ cacheWrite: 0,
133
+ },
134
+ "sakana/fugu-ultra": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
135
+ "thinkingmachines/inkling": { input: 1, output: 4.05, cacheRead: 0.17, cacheWrite: 0 },
136
+ "thinkingmachines/inkling-small": {
137
+ input: 0.5,
138
+ output: 1.2,
139
+ cacheRead: 0.1,
140
+ cacheWrite: 0,
141
+ },
142
+ "meta/muse-spark-1.1": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
143
+
144
+ // Anthropic
145
+ // Introductory pricing through 2026-08-31.
146
+ "claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
147
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
148
+ "claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
149
+ "claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
150
+ "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
151
+ "claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
152
+ "claude-haiku-4-5-20251001": {
153
+ input: 1,
154
+ output: 5,
155
+ cacheRead: 0.1,
156
+ cacheWrite: 1.25,
157
+ },
158
+
159
+ // OpenAI
160
+ "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
+ },
192
+ "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
193
+ "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
194
+ "gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
195
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
196
+
197
+ // Google and xAI
198
+ "google/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
199
+ "google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
200
+ "google/gemini-3.5-flash-lite": {
201
+ input: 0.3,
202
+ output: 2.5,
203
+ cacheRead: 0.03,
204
+ cacheWrite: 0,
205
+ },
206
+ "google/gemini-3.1-flash-lite": {
207
+ input: 0.25,
208
+ output: 1.5,
209
+ cacheRead: 0.03,
210
+ cacheWrite: 0,
211
+ },
212
+ "xai/grok-4.5": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
213
+ }
214
+
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
+ ]