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/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +10 -0
- package/README.md +41 -13
- package/index.ts +106 -38
- package/package.json +13 -4
- package/scripts/live-e2e-profile.mjs +87 -0
- package/scripts/pi-authenticated.mjs +1 -0
- package/scripts/pi-isolated.mjs +1 -0
- package/src/api-key.ts +69 -0
- package/src/auth-server.ts +7 -0
- package/src/commandcode-catalog-overrides.ts +23 -0
- package/src/commandcode-catalog.ts +152 -0
- package/src/converters.ts +78 -17
- package/src/core.ts +117 -23
- package/src/models.ts +68 -97
- package/src/oauth.ts +84 -22
- package/src/pricing.ts +58 -53
- package/src/quota-command.ts +66 -0
- package/src/quota-format.ts +143 -0
- package/src/quota-types.ts +47 -0
- package/src/quota.ts +342 -0
- package/src/runtime.ts +49 -8
- package/src/transport.ts +140 -0
- package/src/types.ts +9 -0
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
|
-
|
|
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
|
|
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 (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
|
230
|
-
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:
|
|
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
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 =
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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-
|
|
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
|
-
//
|
|
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.
|
|
67
|
-
output:
|
|
68
|
-
cacheRead: 0.
|
|
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.
|
|
73
|
-
output: 0.
|
|
74
|
-
cacheRead: 0.
|
|
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
|
-
|
|
162
|
-
"gpt-5.6-
|
|
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
|
+
}
|