pi-commandcode-provider 0.4.2 → 0.5.0
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 +25 -0
- package/CONTRIBUTING.md +18 -0
- package/README.md +101 -71
- package/index.ts +90 -85
- package/package.json +15 -6
- package/scripts/pi-authenticated.mjs +49 -0
- package/scripts/pi-isolated.mjs +78 -0
- package/src/converters.ts +27 -83
- package/src/core.ts +109 -29
- package/src/cost.ts +16 -4
- package/src/json-schema.ts +382 -0
- package/src/models.ts +314 -15
- package/src/overflow.ts +120 -0
- package/src/pricing.ts +226 -0
- package/src/runtime.ts +279 -0
- package/src/types.ts +19 -1
package/src/models.ts
CHANGED
|
@@ -1,6 +1,97 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
|
|
1
4
|
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
|
|
5
|
+
export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
|
|
2
6
|
|
|
3
7
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
|
|
8
|
+
const MODEL_CACHE_VERSION = 1
|
|
9
|
+
|
|
10
|
+
export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
|
|
11
|
+
|
|
12
|
+
type CommandCodeReasoningEffort = Exclude<PiThinkingLevel, "off">
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-model reasoning efforts supported by Command Code's generate endpoint.
|
|
16
|
+
*
|
|
17
|
+
* The Provider API does not expose reasoning metadata. This is an exact
|
|
18
|
+
* snapshot of `reasoningEfforts` from the command-code@1.14.1 model catalog
|
|
19
|
+
* (`packages/shared/src/model-catalog.ts`, also published in the generated
|
|
20
|
+
* `dist/bundled/command-code-knowledge/reference/models.md`). Models omitted
|
|
21
|
+
* here let Command Code choose their reasoning depth, matching the CLI.
|
|
22
|
+
*/
|
|
23
|
+
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
|
24
|
+
"Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
|
|
25
|
+
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
26
|
+
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
27
|
+
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
28
|
+
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
29
|
+
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
|
|
30
|
+
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
31
|
+
"deepseek/deepseek-v4-flash": ["high", "max"],
|
|
32
|
+
"deepseek/deepseek-v4-pro": ["high", "max"],
|
|
33
|
+
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
|
|
34
|
+
"gpt-5.4": ["low", "medium", "high", "xhigh"],
|
|
35
|
+
"gpt-5.4-mini": ["low", "medium", "high"],
|
|
36
|
+
"gpt-5.5": ["low", "medium", "high", "xhigh"],
|
|
37
|
+
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
|
38
|
+
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
39
|
+
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
|
40
|
+
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
|
|
41
|
+
"google/gemini-3.5-flash": ["low", "medium", "high"],
|
|
42
|
+
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
|
|
43
|
+
"google/gemini-3.6-flash": ["low", "medium", "high"],
|
|
44
|
+
"sakana/fugu-ultra": ["high", "xhigh"],
|
|
45
|
+
"xai/grok-4.5": ["low", "medium", "high"],
|
|
46
|
+
"zai-org/GLM-5.2": ["high", "max"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PI_THINKING_LEVELS: readonly PiThinkingLevel[] = [
|
|
50
|
+
"off",
|
|
51
|
+
"minimal",
|
|
52
|
+
"low",
|
|
53
|
+
"medium",
|
|
54
|
+
"high",
|
|
55
|
+
"xhigh",
|
|
56
|
+
"max",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export function thinkingLevelMapForEfforts(
|
|
60
|
+
efforts: readonly string[],
|
|
61
|
+
): Partial<Record<PiThinkingLevel, string | null>> {
|
|
62
|
+
const map: Partial<Record<PiThinkingLevel, string | null>> = {}
|
|
63
|
+
for (const level of PI_THINKING_LEVELS) {
|
|
64
|
+
if (level === "off") continue
|
|
65
|
+
map[level] = efforts.includes(level) ? level : null
|
|
66
|
+
}
|
|
67
|
+
return map
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ThinkingMetadata {
|
|
71
|
+
thinkingLevelMap: Partial<Record<PiThinkingLevel, string | null>>
|
|
72
|
+
thinking: {
|
|
73
|
+
mode: "effort"
|
|
74
|
+
effortMap: Partial<Record<CommandCodeReasoningEffort, string>>
|
|
75
|
+
efforts: readonly CommandCodeReasoningEffort[]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
|
|
80
|
+
const efforts = MODEL_EFFORTS[modelId]
|
|
81
|
+
if (!efforts) return undefined
|
|
82
|
+
return {
|
|
83
|
+
thinkingLevelMap: thinkingLevelMapForEfforts(efforts),
|
|
84
|
+
thinking: {
|
|
85
|
+
mode: "effort",
|
|
86
|
+
effortMap: Object.fromEntries(efforts.map((effort) => [effort, effort])),
|
|
87
|
+
efforts,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isReasoningModel(modelId: string): boolean {
|
|
93
|
+
return MODEL_EFFORTS[modelId] !== undefined
|
|
94
|
+
}
|
|
4
95
|
|
|
5
96
|
interface ApiModel {
|
|
6
97
|
id: string
|
|
@@ -19,21 +110,43 @@ export interface CommandCodeModel {
|
|
|
19
110
|
interface FetchCommandCodeModelsOptions {
|
|
20
111
|
url?: string
|
|
21
112
|
fetchImpl?: typeof fetch
|
|
113
|
+
signal?: AbortSignal
|
|
114
|
+
timeoutMs?: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
|
|
118
|
+
cachePath: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface LoadCommandCodeModelsResult {
|
|
122
|
+
models: readonly CommandCodeModel[]
|
|
123
|
+
source: "live" | "cache" | "empty"
|
|
124
|
+
warning?: string
|
|
22
125
|
}
|
|
23
126
|
|
|
24
127
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
-
return typeof value === "object" && value !== null
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
26
129
|
}
|
|
27
130
|
|
|
28
131
|
function stringField(record: Record<string, unknown>, key: string): string {
|
|
29
132
|
const value = record[key]
|
|
30
|
-
if (typeof value !== "string"
|
|
133
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
134
|
+
throw new Error(`Expected ${key} to be a non-empty string`)
|
|
135
|
+
}
|
|
136
|
+
return value
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function booleanField(record: Record<string, unknown>, key: string): boolean {
|
|
140
|
+
const value = record[key]
|
|
141
|
+
if (typeof value !== "boolean") throw new Error(`Expected ${key} to be a boolean`)
|
|
31
142
|
return value
|
|
32
143
|
}
|
|
33
144
|
|
|
34
|
-
function
|
|
145
|
+
function positiveNumberField(record: Record<string, unknown>, key: string): number {
|
|
35
146
|
const value = record[key]
|
|
36
|
-
if (typeof value !== "number"
|
|
147
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
148
|
+
throw new Error(`Expected ${key} to be a positive number`)
|
|
149
|
+
}
|
|
37
150
|
return value
|
|
38
151
|
}
|
|
39
152
|
|
|
@@ -43,10 +156,112 @@ function parseApiModel(value: unknown): ApiModel {
|
|
|
43
156
|
return {
|
|
44
157
|
id: stringField(value, "id"),
|
|
45
158
|
name: stringField(value, "name"),
|
|
46
|
-
contextLength:
|
|
159
|
+
contextLength: positiveNumberField(value, "context_length"),
|
|
47
160
|
}
|
|
48
161
|
}
|
|
49
162
|
|
|
163
|
+
function parseCachedModel(value: unknown): CommandCodeModel {
|
|
164
|
+
if (!isRecord(value)) throw new Error("Expected cached model entry to be an object")
|
|
165
|
+
|
|
166
|
+
const id = stringField(value, "id")
|
|
167
|
+
booleanField(value, "reasoning")
|
|
168
|
+
return {
|
|
169
|
+
id,
|
|
170
|
+
name: stringField(value, "name"),
|
|
171
|
+
reasoning: isReasoningModel(id),
|
|
172
|
+
contextWindow: positiveNumberField(value, "contextWindow"),
|
|
173
|
+
maxTokens: positiveNumberField(value, "maxTokens"),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requireModels(models: readonly CommandCodeModel[]): readonly CommandCodeModel[] {
|
|
178
|
+
if (models.length === 0) throw new Error("Command Code returned an empty model catalog")
|
|
179
|
+
return models
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function errorMessage(error: unknown): string {
|
|
183
|
+
return error instanceof Error ? error.message : String(error)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function abortError(reason: unknown): Error {
|
|
187
|
+
if (reason instanceof Error) return reason
|
|
188
|
+
return new DOMException("The operation was aborted", "AbortError")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function configuredTimeoutMs(timeoutMs: number | undefined): number {
|
|
192
|
+
return timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
193
|
+
? timeoutMs
|
|
194
|
+
: DEFAULT_MODELS_TIMEOUT_MS
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getModelsTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
198
|
+
const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS
|
|
199
|
+
if (!raw) return DEFAULT_MODELS_TIMEOUT_MS
|
|
200
|
+
|
|
201
|
+
const parsed = Number(raw)
|
|
202
|
+
return configuredTimeoutMs(parsed)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class ModelDiscoveryTimeoutError extends Error {
|
|
206
|
+
constructor(timeoutMs: number) {
|
|
207
|
+
super(`Command Code model discovery timed out after ${timeoutMs}ms`)
|
|
208
|
+
this.name = "ModelDiscoveryTimeoutError"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function runWithTimeout<T>(
|
|
213
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
214
|
+
timeoutMs: number,
|
|
215
|
+
externalSignal: AbortSignal | undefined,
|
|
216
|
+
): Promise<T> {
|
|
217
|
+
const controller = new AbortController()
|
|
218
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
219
|
+
let settled = false
|
|
220
|
+
let onExternalAbort: (() => void) | undefined
|
|
221
|
+
|
|
222
|
+
return new Promise<T>((resolve, reject) => {
|
|
223
|
+
const cleanup = () => {
|
|
224
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
225
|
+
if (onExternalAbort && externalSignal) {
|
|
226
|
+
externalSignal.removeEventListener("abort", onExternalAbort)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const resolveOnce = (value: T) => {
|
|
231
|
+
if (settled) return
|
|
232
|
+
settled = true
|
|
233
|
+
cleanup()
|
|
234
|
+
resolve(value)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const rejectOnce = (error: unknown) => {
|
|
238
|
+
if (settled) return
|
|
239
|
+
settled = true
|
|
240
|
+
cleanup()
|
|
241
|
+
reject(error)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const abort = (reason: unknown) => {
|
|
245
|
+
const error = abortError(reason)
|
|
246
|
+
controller.abort(error)
|
|
247
|
+
rejectOnce(error)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (externalSignal?.aborted) {
|
|
251
|
+
abort(externalSignal.reason)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
onExternalAbort = () => abort(externalSignal?.reason)
|
|
256
|
+
externalSignal?.addEventListener("abort", onExternalAbort, { once: true })
|
|
257
|
+
timer = setTimeout(() => abort(new ModelDiscoveryTimeoutError(timeoutMs)), timeoutMs)
|
|
258
|
+
|
|
259
|
+
Promise.resolve()
|
|
260
|
+
.then(() => operation(controller.signal))
|
|
261
|
+
.then(resolveOnce, rejectOnce)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
50
265
|
export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
|
|
51
266
|
if (!isRecord(value)) throw new Error("Expected models response to be an object")
|
|
52
267
|
if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
|
|
@@ -57,29 +272,113 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
|
|
|
57
272
|
return data.map(parseApiModel).map((model) => ({
|
|
58
273
|
id: model.id,
|
|
59
274
|
name: `${model.name} (CC)`,
|
|
60
|
-
reasoning:
|
|
275
|
+
reasoning: isReasoningModel(model.id),
|
|
61
276
|
contextWindow: model.contextLength,
|
|
62
277
|
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
|
63
278
|
}))
|
|
64
279
|
}
|
|
65
280
|
|
|
281
|
+
export function commandCodeModelsFromCache(value: unknown): readonly CommandCodeModel[] {
|
|
282
|
+
if (!isRecord(value)) throw new Error("Expected model cache to be an object")
|
|
283
|
+
if (value.version !== MODEL_CACHE_VERSION) {
|
|
284
|
+
throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`)
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(value.models)) throw new Error("Expected cached models to be an array")
|
|
287
|
+
|
|
288
|
+
return requireModels(value.models.map(parseCachedModel))
|
|
289
|
+
}
|
|
290
|
+
|
|
66
291
|
export async function fetchCommandCodeModels(
|
|
67
292
|
options: FetchCommandCodeModelsOptions = {},
|
|
68
293
|
): Promise<readonly CommandCodeModel[]> {
|
|
69
294
|
const url = options.url ?? DEFAULT_MODELS_URL
|
|
70
295
|
const fetchImpl = options.fetchImpl ?? fetch
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
296
|
+
const body: unknown = await runWithTimeout(
|
|
297
|
+
async (signal) => {
|
|
298
|
+
const response = await fetchImpl(url, {
|
|
299
|
+
headers: {
|
|
300
|
+
accept: "application/json",
|
|
301
|
+
},
|
|
302
|
+
signal,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return await response.json()
|
|
74
312
|
},
|
|
75
|
-
|
|
313
|
+
configuredTimeoutMs(options.timeoutMs),
|
|
314
|
+
options.signal,
|
|
315
|
+
)
|
|
316
|
+
return requireModels(commandCodeModelsFromApiResponse(body))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function readCommandCodeModelsCache(cachePath: string): Promise<readonly CommandCodeModel[]> {
|
|
320
|
+
const contents = await readFile(cachePath, "utf-8")
|
|
321
|
+
const parsed: unknown = JSON.parse(contents)
|
|
322
|
+
return commandCodeModelsFromCache(parsed)
|
|
323
|
+
}
|
|
76
324
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
325
|
+
async function writeCommandCodeModelsCache(
|
|
326
|
+
cachePath: string,
|
|
327
|
+
models: readonly CommandCodeModel[],
|
|
328
|
+
): Promise<void> {
|
|
329
|
+
await mkdir(dirname(cachePath), { recursive: true })
|
|
330
|
+
const temporaryPath = `${cachePath}.${process.pid}.tmp`
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await writeFile(
|
|
334
|
+
temporaryPath,
|
|
335
|
+
`${JSON.stringify({ version: MODEL_CACHE_VERSION, models }, null, 2)}\n`,
|
|
336
|
+
{ encoding: "utf-8", mode: 0o600 },
|
|
80
337
|
)
|
|
338
|
+
await rename(temporaryPath, cachePath)
|
|
339
|
+
} finally {
|
|
340
|
+
try {
|
|
341
|
+
await rm(temporaryPath, { force: true })
|
|
342
|
+
} catch {
|
|
343
|
+
// Best-effort cleanup must not hide the original cache write error.
|
|
344
|
+
}
|
|
81
345
|
}
|
|
346
|
+
}
|
|
82
347
|
|
|
83
|
-
|
|
84
|
-
|
|
348
|
+
export async function loadCommandCodeModels(
|
|
349
|
+
options: LoadCommandCodeModelsOptions,
|
|
350
|
+
): Promise<LoadCommandCodeModelsResult> {
|
|
351
|
+
const cachePath = options.cachePath
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const models = await fetchCommandCodeModels(options)
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await writeCommandCodeModelsCache(cachePath, models)
|
|
358
|
+
return { models, source: "live" }
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return {
|
|
361
|
+
models,
|
|
362
|
+
source: "live",
|
|
363
|
+
warning: `Loaded the live Command Code model catalog but could not update ${cachePath}: ${errorMessage(error)}`,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch (liveError) {
|
|
367
|
+
if (options.signal?.aborted) throw abortError(options.signal.reason ?? liveError)
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const models = await readCommandCodeModelsCache(cachePath)
|
|
371
|
+
return {
|
|
372
|
+
models,
|
|
373
|
+
source: "cache",
|
|
374
|
+
warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}). Using the cached catalog from ${cachePath}.`,
|
|
375
|
+
}
|
|
376
|
+
} catch (cacheError) {
|
|
377
|
+
return {
|
|
378
|
+
models: [],
|
|
379
|
+
source: "empty",
|
|
380
|
+
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.`,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
85
384
|
}
|
package/src/overflow.ts
ADDED
|
@@ -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
|
+
}
|