pi-commandcode-provider 0.4.3 → 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 +19 -0
- package/CONTRIBUTING.md +18 -0
- package/README.md +100 -72
- package/index.ts +89 -92
- 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 +194 -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
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function stringValue(value: unknown): string | undefined {
|
|
6
|
+
return typeof value === "string" ? value : undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
10
|
+
return typeof value === "boolean" ? value : undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type JsonSchemaValue = boolean | Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
const JSON_SCHEMA_TYPES = new Set([
|
|
16
|
+
"array",
|
|
17
|
+
"boolean",
|
|
18
|
+
"integer",
|
|
19
|
+
"null",
|
|
20
|
+
"number",
|
|
21
|
+
"object",
|
|
22
|
+
"string",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const LEGACY_KINDS = new Set([
|
|
26
|
+
"any",
|
|
27
|
+
"array",
|
|
28
|
+
"boolean",
|
|
29
|
+
"enum",
|
|
30
|
+
"integer",
|
|
31
|
+
"intersect",
|
|
32
|
+
"intersection",
|
|
33
|
+
"literal",
|
|
34
|
+
"never",
|
|
35
|
+
"null",
|
|
36
|
+
"nullable",
|
|
37
|
+
"number",
|
|
38
|
+
"object",
|
|
39
|
+
"optional",
|
|
40
|
+
"string",
|
|
41
|
+
"undefined",
|
|
42
|
+
"union",
|
|
43
|
+
"unknown",
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const LEGACY_FIELDS = new Set([
|
|
47
|
+
"element",
|
|
48
|
+
"kind",
|
|
49
|
+
"inner",
|
|
50
|
+
"optional",
|
|
51
|
+
"value",
|
|
52
|
+
"values",
|
|
53
|
+
"variants",
|
|
54
|
+
"wrapped",
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
const SCHEMA_MAP_FIELDS = new Set([
|
|
58
|
+
"$defs",
|
|
59
|
+
"definitions",
|
|
60
|
+
"dependentSchemas",
|
|
61
|
+
"patternProperties",
|
|
62
|
+
"properties",
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
const SCHEMA_ARRAY_FIELDS = new Set(["allOf", "anyOf", "oneOf", "prefixItems"])
|
|
66
|
+
|
|
67
|
+
const SCHEMA_VALUE_FIELDS = new Set([
|
|
68
|
+
"additionalItems",
|
|
69
|
+
"additionalProperties",
|
|
70
|
+
"contains",
|
|
71
|
+
"contentSchema",
|
|
72
|
+
"else",
|
|
73
|
+
"if",
|
|
74
|
+
"items",
|
|
75
|
+
"not",
|
|
76
|
+
"propertyNames",
|
|
77
|
+
"then",
|
|
78
|
+
"unevaluatedItems",
|
|
79
|
+
"unevaluatedProperties",
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const SCHEMA_KEYWORDS = new Set([
|
|
83
|
+
"$anchor",
|
|
84
|
+
"$comment",
|
|
85
|
+
"$defs",
|
|
86
|
+
"$dynamicAnchor",
|
|
87
|
+
"$dynamicRef",
|
|
88
|
+
"$id",
|
|
89
|
+
"$ref",
|
|
90
|
+
"$schema",
|
|
91
|
+
"$vocabulary",
|
|
92
|
+
"additionalItems",
|
|
93
|
+
"additionalProperties",
|
|
94
|
+
"allOf",
|
|
95
|
+
"anyOf",
|
|
96
|
+
"const",
|
|
97
|
+
"contains",
|
|
98
|
+
"contentEncoding",
|
|
99
|
+
"contentMediaType",
|
|
100
|
+
"contentSchema",
|
|
101
|
+
"default",
|
|
102
|
+
"definitions",
|
|
103
|
+
"dependentRequired",
|
|
104
|
+
"dependentSchemas",
|
|
105
|
+
"description",
|
|
106
|
+
"else",
|
|
107
|
+
"enum",
|
|
108
|
+
"examples",
|
|
109
|
+
"exclusiveMaximum",
|
|
110
|
+
"exclusiveMinimum",
|
|
111
|
+
"format",
|
|
112
|
+
"if",
|
|
113
|
+
"items",
|
|
114
|
+
"maxContains",
|
|
115
|
+
"maxItems",
|
|
116
|
+
"maxLength",
|
|
117
|
+
"maxProperties",
|
|
118
|
+
"maximum",
|
|
119
|
+
"minContains",
|
|
120
|
+
"minItems",
|
|
121
|
+
"minLength",
|
|
122
|
+
"minProperties",
|
|
123
|
+
"minimum",
|
|
124
|
+
"multipleOf",
|
|
125
|
+
"not",
|
|
126
|
+
"oneOf",
|
|
127
|
+
"pattern",
|
|
128
|
+
"patternProperties",
|
|
129
|
+
"prefixItems",
|
|
130
|
+
"properties",
|
|
131
|
+
"propertyNames",
|
|
132
|
+
"readOnly",
|
|
133
|
+
"required",
|
|
134
|
+
"title",
|
|
135
|
+
"type",
|
|
136
|
+
"unevaluatedItems",
|
|
137
|
+
"unevaluatedProperties",
|
|
138
|
+
"uniqueItems",
|
|
139
|
+
"writeOnly",
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
143
|
+
if (!Array.isArray(value)) return undefined
|
|
144
|
+
const values = value.filter((item): item is string => typeof item === "string")
|
|
145
|
+
return values.length === value.length ? values : undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validSchemaType(value: unknown): boolean {
|
|
149
|
+
if (typeof value === "string") return JSON_SCHEMA_TYPES.has(value)
|
|
150
|
+
if (!Array.isArray(value) || value.length === 0) return false
|
|
151
|
+
return value.every((item) => typeof item === "string" && JSON_SCHEMA_TYPES.has(item))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function legacyKind(schema: Record<string, unknown>): string | undefined {
|
|
155
|
+
const explicitKind = stringValue(schema.kind)?.toLowerCase()
|
|
156
|
+
if (explicitKind && LEGACY_KINDS.has(explicitKind)) return explicitKind
|
|
157
|
+
|
|
158
|
+
const type = stringValue(schema.type)
|
|
159
|
+
const normalized = type?.toLowerCase()
|
|
160
|
+
if (!normalized || !LEGACY_KINDS.has(normalized)) return undefined
|
|
161
|
+
if (!validSchemaType(type) || Object.keys(schema).some((key) => LEGACY_FIELDS.has(key))) {
|
|
162
|
+
return normalized
|
|
163
|
+
}
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function looksLikeJsonSchema(schema: Record<string, unknown>): boolean {
|
|
168
|
+
if (Object.keys(schema).length === 0) return true
|
|
169
|
+
if (schema.type !== undefined && !validSchemaType(schema.type)) return false
|
|
170
|
+
return Object.keys(schema).some((key) => SCHEMA_KEYWORDS.has(key))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isOptionalSchema(schema: unknown): boolean {
|
|
174
|
+
if (!isRecord(schema)) return false
|
|
175
|
+
if (booleanValue(schema.optional) === true) return true
|
|
176
|
+
|
|
177
|
+
const kind = legacyKind(schema)
|
|
178
|
+
if (kind === "optional") return true
|
|
179
|
+
if (kind !== "union") return false
|
|
180
|
+
|
|
181
|
+
const variants = Array.isArray(schema.variants)
|
|
182
|
+
? schema.variants
|
|
183
|
+
: Array.isArray(schema.anyOf)
|
|
184
|
+
? schema.anyOf
|
|
185
|
+
: []
|
|
186
|
+
return variants.some((variant) => legacyKind(isRecord(variant) ? variant : {}) === "undefined")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function schemaValue(value: unknown, seen: WeakSet<object>): JsonSchemaValue {
|
|
190
|
+
if (typeof value === "boolean") return value
|
|
191
|
+
if (!isRecord(value)) return {}
|
|
192
|
+
return convertSchema(value, seen)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function setSchemaProperty(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
196
|
+
Object.defineProperty(target, key, {
|
|
197
|
+
configurable: true,
|
|
198
|
+
enumerable: true,
|
|
199
|
+
value,
|
|
200
|
+
writable: true,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function schemaMap(value: unknown, seen: WeakSet<object>): Record<string, unknown> {
|
|
205
|
+
if (!isRecord(value)) return {}
|
|
206
|
+
const out: Record<string, unknown> = {}
|
|
207
|
+
for (const [key, item] of Object.entries(value)) {
|
|
208
|
+
setSchemaProperty(out, key, schemaValue(item, seen))
|
|
209
|
+
}
|
|
210
|
+
return out
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function schemaArray(value: unknown, seen: WeakSet<object>): unknown[] {
|
|
214
|
+
if (!Array.isArray(value)) return []
|
|
215
|
+
return value.map((item) => schemaValue(item, seen))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isSchemaValue(value: unknown): value is JsonSchemaValue {
|
|
219
|
+
return typeof value === "boolean" || isRecord(value)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function copySchemaObject(
|
|
223
|
+
source: Record<string, unknown>,
|
|
224
|
+
seen: WeakSet<object>,
|
|
225
|
+
legacy: boolean,
|
|
226
|
+
forcedType?: string,
|
|
227
|
+
): JsonSchemaValue {
|
|
228
|
+
const out: Record<string, unknown> = {}
|
|
229
|
+
|
|
230
|
+
for (const [key, value] of Object.entries(source)) {
|
|
231
|
+
if (legacy && LEGACY_FIELDS.has(key)) continue
|
|
232
|
+
if (key === "nullable" || (forcedType !== undefined && key === "type")) continue
|
|
233
|
+
|
|
234
|
+
if (key === "required") {
|
|
235
|
+
const required = stringArray(value)
|
|
236
|
+
if (required) out.required = required
|
|
237
|
+
} else if (SCHEMA_MAP_FIELDS.has(key)) {
|
|
238
|
+
out[key] = schemaMap(value, seen)
|
|
239
|
+
} else if (SCHEMA_ARRAY_FIELDS.has(key)) {
|
|
240
|
+
out[key] = schemaArray(value, seen)
|
|
241
|
+
} else if (SCHEMA_VALUE_FIELDS.has(key)) {
|
|
242
|
+
out[key] =
|
|
243
|
+
Array.isArray(value) && key === "items"
|
|
244
|
+
? schemaArray(value, seen)
|
|
245
|
+
: schemaValue(value, seen)
|
|
246
|
+
} else {
|
|
247
|
+
out[key] = value
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (forcedType !== undefined) out.type = forcedType
|
|
252
|
+
if (booleanValue(source.nullable) === true) return makeNullable(out)
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function makeNullable(schema: Record<string, unknown>): Record<string, unknown> {
|
|
257
|
+
const type = schema.type
|
|
258
|
+
if (typeof type === "string") {
|
|
259
|
+
if (type === "null") return schema
|
|
260
|
+
return { ...schema, type: [type, "null"] }
|
|
261
|
+
}
|
|
262
|
+
if (Array.isArray(type) && !type.includes("null")) {
|
|
263
|
+
return { ...schema, type: [...type, "null"] }
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(schema.anyOf)) {
|
|
266
|
+
return { ...schema, anyOf: [...schema.anyOf, { type: "null" }] }
|
|
267
|
+
}
|
|
268
|
+
return { anyOf: [schema, { type: "null" }] }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function legacyVariants(schema: Record<string, unknown>): unknown[] {
|
|
272
|
+
if (Array.isArray(schema.variants)) return schema.variants
|
|
273
|
+
if (Array.isArray(schema.anyOf)) return schema.anyOf
|
|
274
|
+
return []
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function convertLegacySchema(
|
|
278
|
+
source: Record<string, unknown>,
|
|
279
|
+
kind: string,
|
|
280
|
+
seen: WeakSet<object>,
|
|
281
|
+
): JsonSchemaValue {
|
|
282
|
+
if (kind === "optional") return schemaValue(source.wrapped ?? source.inner, seen)
|
|
283
|
+
if (kind === "nullable") {
|
|
284
|
+
const wrapped = schemaValue(source.wrapped ?? source.inner, seen)
|
|
285
|
+
return typeof wrapped === "boolean" ? wrapped : makeNullable(wrapped)
|
|
286
|
+
}
|
|
287
|
+
if (kind === "undefined" || kind === "never" || kind === "any" || kind === "unknown") return {}
|
|
288
|
+
|
|
289
|
+
if (kind === "union" || kind === "intersect" || kind === "intersection") {
|
|
290
|
+
const variants = legacyVariants(source)
|
|
291
|
+
.map((variant) => schemaValue(variant, seen))
|
|
292
|
+
.filter(
|
|
293
|
+
(variant) =>
|
|
294
|
+
isSchemaValue(variant) &&
|
|
295
|
+
(typeof variant === "boolean" || Object.keys(variant).length > 0),
|
|
296
|
+
)
|
|
297
|
+
if (variants.length === 0) return copySchemaObject(source, seen, true)
|
|
298
|
+
if (variants.length === 1) return variants[0] ?? {}
|
|
299
|
+
|
|
300
|
+
const out = copySchemaObject(source, seen, true)
|
|
301
|
+
if (typeof out !== "boolean") out[kind === "union" ? "anyOf" : "allOf"] = variants
|
|
302
|
+
return out
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (kind === "object") {
|
|
306
|
+
const converted = copySchemaObject(source, seen, true, "object")
|
|
307
|
+
if (typeof converted === "boolean") return converted
|
|
308
|
+
const out = converted
|
|
309
|
+
const sourceProperties = isRecord(source.properties) ? source.properties : undefined
|
|
310
|
+
if (!sourceProperties) return out
|
|
311
|
+
|
|
312
|
+
const properties: Record<string, unknown> = {}
|
|
313
|
+
const optional = stringArray(source.optional) ?? []
|
|
314
|
+
for (const [key, value] of Object.entries(sourceProperties)) {
|
|
315
|
+
setSchemaProperty(properties, key, schemaValue(value, seen))
|
|
316
|
+
}
|
|
317
|
+
out.properties = properties
|
|
318
|
+
|
|
319
|
+
const explicitRequired = stringArray(source.required)
|
|
320
|
+
const required =
|
|
321
|
+
explicitRequired ??
|
|
322
|
+
Object.entries(sourceProperties)
|
|
323
|
+
.filter(([key, value]) => !optional.includes(key) && !isOptionalSchema(value))
|
|
324
|
+
.map(([key]) => key)
|
|
325
|
+
if (required.length > 0) out.required = required
|
|
326
|
+
else delete out.required
|
|
327
|
+
return out
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (kind === "array") {
|
|
331
|
+
const converted = copySchemaObject(source, seen, true, "array")
|
|
332
|
+
if (typeof converted === "boolean") return converted
|
|
333
|
+
const out = converted
|
|
334
|
+
if (!("items" in source) && "element" in source) out.items = schemaValue(source.element, seen)
|
|
335
|
+
return out
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (kind === "enum") {
|
|
339
|
+
const converted = copySchemaObject(source, seen, true)
|
|
340
|
+
if (typeof converted === "boolean") return converted
|
|
341
|
+
const out = converted
|
|
342
|
+
if (!("enum" in out) && Array.isArray(source.values)) out.enum = source.values
|
|
343
|
+
return out
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (kind === "literal") {
|
|
347
|
+
const converted = copySchemaObject(source, seen, true)
|
|
348
|
+
if (typeof converted === "boolean") return converted
|
|
349
|
+
const out = converted
|
|
350
|
+
if (!("const" in out) && "value" in source) out.const = source.value
|
|
351
|
+
return out
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const scalarType =
|
|
355
|
+
kind === "string" ||
|
|
356
|
+
kind === "number" ||
|
|
357
|
+
kind === "boolean" ||
|
|
358
|
+
kind === "integer" ||
|
|
359
|
+
kind === "null"
|
|
360
|
+
? kind
|
|
361
|
+
: undefined
|
|
362
|
+
return scalarType ? copySchemaObject(source, seen, true, scalarType) : {}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function convertSchema(source: Record<string, unknown>, seen: WeakSet<object>): JsonSchemaValue {
|
|
366
|
+
if (seen.has(source)) return {}
|
|
367
|
+
seen.add(source)
|
|
368
|
+
try {
|
|
369
|
+
const kind = legacyKind(source)
|
|
370
|
+
if (kind) return convertLegacySchema(source, kind, seen)
|
|
371
|
+
if (!looksLikeJsonSchema(source)) return {}
|
|
372
|
+
return copySchemaObject(source, seen, false)
|
|
373
|
+
} finally {
|
|
374
|
+
seen.delete(source)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function toJsonSchema(schema: unknown): unknown {
|
|
379
|
+
if (typeof schema === "boolean") return schema
|
|
380
|
+
if (!isRecord(schema)) return {}
|
|
381
|
+
return convertSchema(schema, new WeakSet<object>())
|
|
382
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -2,10 +2,97 @@ 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 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
|
+
}
|
|
95
|
+
|
|
9
96
|
interface ApiModel {
|
|
10
97
|
id: string
|
|
11
98
|
name: string
|
|
@@ -23,6 +110,8 @@ export interface CommandCodeModel {
|
|
|
23
110
|
interface FetchCommandCodeModelsOptions {
|
|
24
111
|
url?: string
|
|
25
112
|
fetchImpl?: typeof fetch
|
|
113
|
+
signal?: AbortSignal
|
|
114
|
+
timeoutMs?: number
|
|
26
115
|
}
|
|
27
116
|
|
|
28
117
|
interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
|
|
@@ -74,10 +163,12 @@ function parseApiModel(value: unknown): ApiModel {
|
|
|
74
163
|
function parseCachedModel(value: unknown): CommandCodeModel {
|
|
75
164
|
if (!isRecord(value)) throw new Error("Expected cached model entry to be an object")
|
|
76
165
|
|
|
166
|
+
const id = stringField(value, "id")
|
|
167
|
+
booleanField(value, "reasoning")
|
|
77
168
|
return {
|
|
78
|
-
id
|
|
169
|
+
id,
|
|
79
170
|
name: stringField(value, "name"),
|
|
80
|
-
reasoning:
|
|
171
|
+
reasoning: isReasoningModel(id),
|
|
81
172
|
contextWindow: positiveNumberField(value, "contextWindow"),
|
|
82
173
|
maxTokens: positiveNumberField(value, "maxTokens"),
|
|
83
174
|
}
|
|
@@ -92,6 +183,85 @@ function errorMessage(error: unknown): string {
|
|
|
92
183
|
return error instanceof Error ? error.message : String(error)
|
|
93
184
|
}
|
|
94
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
|
+
|
|
95
265
|
export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
|
|
96
266
|
if (!isRecord(value)) throw new Error("Expected models response to be an object")
|
|
97
267
|
if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
|
|
@@ -102,7 +272,7 @@ export function commandCodeModelsFromApiResponse(value: unknown): readonly Comma
|
|
|
102
272
|
return data.map(parseApiModel).map((model) => ({
|
|
103
273
|
id: model.id,
|
|
104
274
|
name: `${model.name} (CC)`,
|
|
105
|
-
reasoning:
|
|
275
|
+
reasoning: isReasoningModel(model.id),
|
|
106
276
|
contextWindow: model.contextLength,
|
|
107
277
|
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
|
|
108
278
|
}))
|
|
@@ -123,19 +293,26 @@ export async function fetchCommandCodeModels(
|
|
|
123
293
|
): Promise<readonly CommandCodeModel[]> {
|
|
124
294
|
const url = options.url ?? DEFAULT_MODELS_URL
|
|
125
295
|
const fetchImpl = options.fetchImpl ?? fetch
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
})
|
|
131
304
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
137
310
|
|
|
138
|
-
|
|
311
|
+
return await response.json()
|
|
312
|
+
},
|
|
313
|
+
configuredTimeoutMs(options.timeoutMs),
|
|
314
|
+
options.signal,
|
|
315
|
+
)
|
|
139
316
|
return requireModels(commandCodeModelsFromApiResponse(body))
|
|
140
317
|
}
|
|
141
318
|
|
|
@@ -187,6 +364,8 @@ export async function loadCommandCodeModels(
|
|
|
187
364
|
}
|
|
188
365
|
}
|
|
189
366
|
} catch (liveError) {
|
|
367
|
+
if (options.signal?.aborted) throw abortError(options.signal.reason ?? liveError)
|
|
368
|
+
|
|
190
369
|
try {
|
|
191
370
|
const models = await readCommandCodeModelsCache(cachePath)
|
|
192
371
|
return {
|
|
@@ -198,7 +377,7 @@ export async function loadCommandCodeModels(
|
|
|
198
377
|
return {
|
|
199
378
|
models: [],
|
|
200
379
|
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 /
|
|
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.`,
|
|
202
381
|
}
|
|
203
382
|
}
|
|
204
383
|
}
|