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.
@@ -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: stringField(value, "id"),
169
+ id,
79
170
  name: stringField(value, "name"),
80
- reasoning: booleanField(value, "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: true,
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 response = await fetchImpl(url, {
127
- headers: {
128
- accept: "application/json",
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
- if (!response.ok) {
133
- throw new Error(
134
- `Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
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
- const body: unknown = await response.json()
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 /reload succeeds.`,
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
  }