codex-copilot-dx 0.6.14 → 0.6.18

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,78 @@
1
+ function cloneJson(value) {
2
+ return value === undefined ? undefined : structuredClone(value);
3
+ }
4
+
5
+ export function isEncryptedContentVerificationError(statusCode, text) {
6
+ if (statusCode < 400 || !text) return false;
7
+ const lower = String(text).toLowerCase();
8
+ const reasoningFailure = lower.includes("encrypted content")
9
+ && lower.includes("could not be verified")
10
+ && (lower.includes("could not be decrypted") || lower.includes("could not be parsed"));
11
+ const functionOutputFailure = lower.includes("encrypted function output content")
12
+ && lower.includes("could not be decrypted or decoded");
13
+ const missingEncryptedContent = statusCode < 500
14
+ && /missing required parameter:\s*(['"]?)input\[\d+\](?:\.[a-z0-9_]+|\[\d+\])*\.encrypted_content\1(?=\.?(?:\s|$|["},\]]))/.test(lower);
15
+ return reasoningFailure || functionOutputFailure || missingEncryptedContent;
16
+ }
17
+
18
+ export function isImageNamespaceCollisionError(statusCode, text) {
19
+ if (statusCode < 400 || !text) return false;
20
+ const lower = String(text).toLowerCase();
21
+ return lower.includes("namespace")
22
+ && lower.includes("image_gen")
23
+ && lower.includes("collid");
24
+ }
25
+
26
+ export function isCopilotImageNamespaceTool(tool, { collisionFallback = false } = {}) {
27
+ if (!tool || typeof tool !== "object") return false;
28
+ const type = String(tool.type || "").toLowerCase();
29
+ if (["image_gen", "image_generation"].includes(type)) return true;
30
+ if (!collisionFallback) return false;
31
+ const name = String(tool.name || tool.function?.name || "").toLowerCase();
32
+ const namespace = String(tool.namespace || "").toLowerCase();
33
+ if (["image_gen", "image_generation"].includes(name)) return true;
34
+ if (namespace === "image_gen" || namespace === "image_generation") return true;
35
+ return [type, name, namespace].some((value) => value.startsWith("image_gen"));
36
+ }
37
+
38
+ function toolName(tool) {
39
+ return String(tool?.name || tool?.function?.name || "").trim();
40
+ }
41
+
42
+ // Copilot currently rejects the public image-generation tool. Keep this
43
+ // provider-owned policy paired with tool_choice so request invariants survive
44
+ // the removal. Revalidate against Copilot before deleting or broadening it.
45
+ // Reference: https://github.com/Menci/Floway/blob/ac2bbb04352033a7d9d74574a9d465d40395ef06/packages/provider-copilot/src/interceptors/openai-responses/strip-image-generation.ts
46
+ export function applyCopilotResponsesRequestPolicies(body, options = {}) {
47
+ if (!Array.isArray(body?.tools)) return false;
48
+ const removedNames = new Set();
49
+ const filtered = body.tools.filter((tool) => {
50
+ const removed = isCopilotImageNamespaceTool(tool, options);
51
+ if (removed) {
52
+ const name = toolName(tool);
53
+ if (name) removedNames.add(name);
54
+ }
55
+ return !removed;
56
+ });
57
+ if (filtered.length === body.tools.length) return false;
58
+ if (filtered.length) body.tools = filtered;
59
+ else delete body.tools;
60
+
61
+ const choiceName = toolName(body.tool_choice);
62
+ const choiceTargetsRemovedTool = choiceName
63
+ && removedNames.has(choiceName)
64
+ && !filtered.some((tool) => toolName(tool) === choiceName);
65
+ if (isCopilotImageNamespaceTool(body.tool_choice, options)
66
+ || choiceTargetsRemovedTool
67
+ || (body.tool_choice === "required" && !body.tools)) {
68
+ delete body.tool_choice;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ export function sanitizeImageNamespaceCollisionRequest(reqContext) {
74
+ if (!Array.isArray(reqContext?.body?.tools)) return null;
75
+ const body = cloneJson(reqContext.body);
76
+ if (!applyCopilotResponsesRequestPolicies(body, { collisionFallback: true })) return null;
77
+ return { ...reqContext, body };
78
+ }
package/src/copilot.mjs CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { status } from "./status.mjs";
3
3
  import { debugLog } from "./log.mjs";
4
4
  import { createCopilotClientRuntime } from "./copilot-client.mjs";
5
+ import { discardBoundedResponseBody } from "./http-transport.mjs";
5
6
 
6
7
  export {
7
8
  optimizeImageDataUrl,
@@ -156,7 +157,7 @@ export async function fetchCopilotUpstream(
156
157
  const resp = await fetchImpl(url, init);
157
158
  debugLog(`upstream ${method} ${target} status=${resp.status} attempt=${attempt + 1}/${retryCount + 1} attempt_ms=${Date.now() - attemptStart} total_ms=${Date.now() - totalStart}`);
158
159
  if (attempt < retryCount && isRetryableUpstreamStatus(resp.status, method)) {
159
- await resp.arrayBuffer?.().catch(() => {});
160
+ await discardBoundedResponseBody(resp);
160
161
  const delay = upstreamRetryDelay(attempt, baseDelay);
161
162
  console.warn(status("warn", `upstream ${target} returned ${resp.status}; retry ${attempt + 1}/${retryCount} in ${delay}ms`));
162
163
  await sleep(delay, { signal });
@@ -17,12 +17,103 @@ const MAX_INFLIGHT_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxInflightBodyBytes;
17
17
  const MAX_QUEUED_REQUESTS = HTTP_RUNTIME_CONFIG.maxQueuedRequests;
18
18
  const REQUEST_QUEUE_TIMEOUT_MS = HTTP_RUNTIME_CONFIG.requestQueueTimeoutMs;
19
19
 
20
+ export const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
21
+ export const MAX_UPSTREAM_MODEL_CATALOG_BYTES = 8 * 1024 * 1024;
22
+ export const MAX_UPSTREAM_RETRY_DRAIN_BYTES = 64 * 1024;
23
+
20
24
  export function httpError(message, statusCode) {
21
25
  const err = new Error(message);
22
26
  err.statusCode = statusCode;
23
27
  return err;
24
28
  }
25
29
 
30
+ function upstreamBodyTooLarge(label, maxBytes) {
31
+ const error = httpError(`${label} exceeds ${maxBytes} bytes`, 502);
32
+ error.code = "ccdx_upstream_response_too_large";
33
+ error.jsonBody = {
34
+ error: {
35
+ message: error.message,
36
+ type: "upstream_error",
37
+ code: error.code,
38
+ },
39
+ };
40
+ return error;
41
+ }
42
+
43
+ function unreadableUpstreamBody(label) {
44
+ const error = httpError(`${label} is not a readable web stream`, 502);
45
+ error.code = "ccdx_upstream_response_unreadable";
46
+ error.jsonBody = {
47
+ error: {
48
+ message: error.message,
49
+ type: "upstream_error",
50
+ code: error.code,
51
+ },
52
+ };
53
+ return error;
54
+ }
55
+
56
+ async function cancelResponseBody(response) {
57
+ try { await response?.body?.cancel?.(); } catch {}
58
+ }
59
+
60
+ function responseContentLength(response) {
61
+ const raw = response?.headers?.get?.("content-length")
62
+ ?? response?.headers?.["content-length"]
63
+ ?? response?.headers?.["Content-Length"];
64
+ const parsed = Number.parseInt(raw || "", 10);
65
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
66
+ }
67
+
68
+ export async function readBoundedResponseBuffer(response, {
69
+ maxBytes,
70
+ label = "Upstream response body",
71
+ } = {}) {
72
+ const limit = Number.isFinite(maxBytes) && maxBytes >= 0 ? Math.floor(maxBytes) : 0;
73
+ if (responseContentLength(response) > limit) {
74
+ await cancelResponseBody(response);
75
+ throw upstreamBodyTooLarge(label, limit);
76
+ }
77
+ if (!response?.body) {
78
+ if ((responseContentLength(response) || 0) > 0) throw unreadableUpstreamBody(label);
79
+ return Buffer.alloc(0);
80
+ }
81
+ const reader = response?.body?.getReader?.();
82
+ if (!reader) throw unreadableUpstreamBody(label);
83
+ const chunks = [];
84
+ let total = 0;
85
+ try {
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+ if (done) break;
89
+ const chunk = Buffer.from(value);
90
+ total += chunk.length;
91
+ if (total > limit) {
92
+ await reader.cancel().catch(() => {});
93
+ throw upstreamBodyTooLarge(label, limit);
94
+ }
95
+ chunks.push(chunk);
96
+ }
97
+ return Buffer.concat(chunks, total);
98
+ } finally {
99
+ reader.releaseLock();
100
+ }
101
+ }
102
+
103
+ export async function readBoundedResponseText(response, options) {
104
+ return new TextDecoder().decode(await readBoundedResponseBuffer(response, options));
105
+ }
106
+
107
+ export async function discardBoundedResponseBody(response, maxBytes = MAX_UPSTREAM_RETRY_DRAIN_BYTES) {
108
+ try {
109
+ await readBoundedResponseBuffer(response, { maxBytes, label: "Upstream retry response body" });
110
+ return true;
111
+ } catch {
112
+ await cancelResponseBody(response);
113
+ return false;
114
+ }
115
+ }
116
+
26
117
  function payloadTooLarge(kind, maxBytes) {
27
118
  return httpError(`${kind} request body exceeds ${maxBytes} bytes`, 413);
28
119
  }
package/src/lock.mjs CHANGED
@@ -32,23 +32,171 @@ function processIsAlive(pid) {
32
32
  }
33
33
  }
34
34
 
35
+ function readStaleLockSnapshot(lockPath, io) {
36
+ const before = io.lstatSync(lockPath);
37
+ if (!before.isFile() || before.isSymbolicLink()) return { safe: false };
38
+
39
+ const noFollow = io.constants?.O_NOFOLLOW || 0;
40
+ let fd;
41
+ try {
42
+ fd = io.openSync(lockPath, io.constants.O_RDONLY | noFollow);
43
+ const opened = io.fstatSync(fd);
44
+ if (!opened.isFile()
45
+ || opened.dev !== before.dev
46
+ || opened.ino !== before.ino
47
+ || opened.size !== before.size) {
48
+ return { safe: false };
49
+ }
50
+ const text = io.readFileSync(fd, "utf8");
51
+ const after = io.fstatSync(fd);
52
+ if (after.dev !== opened.dev
53
+ || after.ino !== opened.ino
54
+ || after.size !== opened.size
55
+ || after.mtimeMs !== opened.mtimeMs
56
+ || after.ctimeMs !== opened.ctimeMs) {
57
+ return { safe: false };
58
+ }
59
+ let record = {};
60
+ try {
61
+ record = JSON.parse(text);
62
+ } catch {}
63
+ return {
64
+ safe: true,
65
+ dev: after.dev,
66
+ ino: after.ino,
67
+ mtimeMs: after.mtimeMs,
68
+ size: after.size,
69
+ text,
70
+ pid: Number(record?.pid),
71
+ owner: typeof record?.owner === "string" ? record.owner : "",
72
+ };
73
+ } finally {
74
+ if (fd !== undefined) io.closeSync(fd);
75
+ }
76
+ }
77
+
35
78
  function sameLock(left, right) {
36
79
  return left.dev === right.dev
37
80
  && left.ino === right.ino
38
81
  && left.mtimeMs === right.mtimeMs
39
82
  && left.size === right.size
40
- && left.owner === right.owner;
83
+ && left.owner === right.owner
84
+ && left.text === right.text;
41
85
  }
42
86
 
43
- function maybeRemoveStaleLock(lockPath, staleMs, nowMs) {
44
- if (!Number.isFinite(staleMs) || staleMs <= 0) return false;
87
+ function lockPathExists(lockPath, io) {
88
+ try {
89
+ io.lstatSync(lockPath);
90
+ return true;
91
+ } catch (error) {
92
+ if (error?.code === "ENOENT") return false;
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ function writeLockRecord(lockPath, record, io) {
98
+ const fd = io.openSync(lockPath, "wx", 0o600);
99
+ try {
100
+ io.writeFileSync(fd, JSON.stringify(record));
101
+ } catch (error) {
102
+ io.closeSync(fd);
103
+ try { io.unlinkSync(lockPath); } catch {}
104
+ throw error;
105
+ }
106
+ return fd;
107
+ }
108
+
109
+ function removeOwnedLock(lockPath, owner, io) {
110
+ try {
111
+ const snapshot = readStaleLockSnapshot(lockPath, io);
112
+ if (snapshot.safe && snapshot.owner === owner) io.unlinkSync(lockPath);
113
+ } catch (error) {
114
+ if (error?.code !== "ENOENT") throw error;
115
+ }
116
+ }
117
+
118
+ function reclaimGuardPath(lockPath) {
119
+ return `${lockPath}.reclaim`;
120
+ }
121
+
122
+ function acquireReclaimGuard(lockPath, io) {
123
+ const guardPath = reclaimGuardPath(lockPath);
124
+ const owner = randomUUID();
125
+ let fd;
126
+ try {
127
+ fd = writeLockRecord(guardPath, {
128
+ pid: process.pid,
129
+ owner,
130
+ created_at: new Date().toISOString(),
131
+ }, io);
132
+ io.closeSync(fd);
133
+ return { guardPath, owner };
134
+ } catch (error) {
135
+ if (fd !== undefined) try { io.closeSync(fd); } catch {}
136
+ if (error?.code === "EEXIST") return null;
137
+ throw error;
138
+ }
139
+ }
140
+
141
+ function maybeRemoveAbandonedGuard(guardPath, staleMs, nowMs, io, processAlive) {
45
142
  try {
46
- const first = readLockSnapshot(lockPath);
47
- if (nowMs - first.mtimeMs <= staleMs || processIsAlive(first.pid)) return false;
48
- const current = readLockSnapshot(lockPath);
49
- if (!sameLock(first, current)) return false;
50
- fs.unlinkSync(lockPath);
143
+ const snapshot = readStaleLockSnapshot(guardPath, io);
144
+ if (!snapshot.safe || nowMs - snapshot.mtimeMs <= staleMs || processAlive(snapshot.pid)) return false;
145
+ const quarantinePath = `${guardPath}.stale-${randomUUID()}`;
146
+ io.renameSync(guardPath, quarantinePath);
147
+ const moved = readStaleLockSnapshot(quarantinePath, io);
148
+ if (!moved.safe || !sameLock(snapshot, moved)) {
149
+ restoreQuarantinedLock(guardPath, quarantinePath, io);
150
+ return false;
151
+ }
152
+ io.unlinkSync(quarantinePath);
51
153
  return true;
154
+ } catch (error) {
155
+ return error?.code === "ENOENT";
156
+ }
157
+ }
158
+
159
+ function restoreQuarantinedLock(lockPath, quarantinePath, io) {
160
+ try {
161
+ io.linkSync(quarantinePath, lockPath);
162
+ } catch {
163
+ return;
164
+ }
165
+ try {
166
+ io.unlinkSync(quarantinePath);
167
+ } catch {}
168
+ }
169
+
170
+ function maybeRemoveStaleLock(lockPath, staleMs, nowMs, io, processAlive) {
171
+ if (!Number.isFinite(staleMs) || staleMs <= 0) return false;
172
+ try {
173
+ const first = readStaleLockSnapshot(lockPath, io);
174
+ if (!first.safe || nowMs - first.mtimeMs <= staleMs || processAlive(first.pid)) return false;
175
+ const guard = acquireReclaimGuard(lockPath, io);
176
+ if (!guard) return false;
177
+ try {
178
+ const current = readStaleLockSnapshot(lockPath, io);
179
+ if (!sameLock(first, current)
180
+ || nowMs - current.mtimeMs <= staleMs
181
+ || processAlive(current.pid)) {
182
+ return false;
183
+ }
184
+ const quarantinePath = `${lockPath}.stale-${randomUUID()}`;
185
+ try {
186
+ io.renameSync(lockPath, quarantinePath);
187
+ } catch (e) {
188
+ return e?.code === "ENOENT";
189
+ }
190
+ const moved = readStaleLockSnapshot(quarantinePath, io);
191
+ if (!moved.safe || !sameLock(current, moved)) {
192
+ restoreQuarantinedLock(lockPath, quarantinePath, io);
193
+ return false;
194
+ }
195
+ io.unlinkSync(quarantinePath);
196
+ return true;
197
+ } finally {
198
+ removeOwnedLock(guard.guardPath, guard.owner, io);
199
+ }
52
200
  } catch (e) {
53
201
  if (e?.code === "ENOENT") return true;
54
202
  return false;
@@ -60,28 +208,42 @@ export async function withFileLock(lockPath, fn, {
60
208
  staleMs = 15 * 60 * 1000,
61
209
  pollMs = 50,
62
210
  now = Date.now,
211
+ fsImpl = fs,
212
+ processAlive = processIsAlive,
63
213
  } = {}) {
64
214
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
65
215
  const started = now();
66
216
  const owner = randomUUID();
67
217
  let fd = null;
218
+ const guardPath = reclaimGuardPath(lockPath);
68
219
 
69
220
  while (fd === null) {
221
+ const beforeAcquire = now();
222
+ if (lockPathExists(guardPath, fsImpl)) {
223
+ maybeRemoveAbandonedGuard(guardPath, staleMs, beforeAcquire, fsImpl, processAlive);
224
+ if (lockPathExists(guardPath, fsImpl)) {
225
+ if (beforeAcquire - started >= timeoutMs) throw new Error(`Timed out waiting for lock: ${lockPath}`);
226
+ await sleep(Math.min(pollMs, Math.max(1, timeoutMs - (beforeAcquire - started))));
227
+ continue;
228
+ }
229
+ }
70
230
  try {
71
- const candidateFd = fs.openSync(lockPath, "wx", 0o600);
72
- try {
73
- fs.writeFileSync(candidateFd, JSON.stringify({ pid: process.pid, owner, created_at: new Date().toISOString() }));
74
- fd = candidateFd;
75
- } catch (e) {
76
- fs.closeSync(candidateFd);
77
- try { fs.unlinkSync(lockPath); } catch {}
78
- throw e;
231
+ fd = writeLockRecord(lockPath, {
232
+ pid: process.pid,
233
+ owner,
234
+ created_at: new Date().toISOString(),
235
+ }, fs);
236
+ if (lockPathExists(guardPath, fsImpl)) {
237
+ fs.closeSync(fd);
238
+ fd = null;
239
+ removeOwnedLock(lockPath, owner, fs);
240
+ continue;
79
241
  }
80
242
  break;
81
243
  } catch (e) {
82
244
  if (e?.code !== "EEXIST") throw e;
83
245
  const nowMs = now();
84
- if (maybeRemoveStaleLock(lockPath, staleMs, nowMs)) continue;
246
+ if (maybeRemoveStaleLock(lockPath, staleMs, nowMs, fsImpl, processAlive)) continue;
85
247
  if (nowMs - started >= timeoutMs) {
86
248
  throw new Error(`Timed out waiting for lock: ${lockPath}`);
87
249
  }
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
5
+ export const MODEL_CACHE_SOFT_TTL_MS = 2 * 60 * 60 * 1000;
6
+ export const MODEL_CACHE_HARD_TTL_MS = 7 * 24 * 60 * 60 * 1000;
6
7
  const MODEL_CACHE_PROFILES = new Set(["codex", "claude"]);
7
8
 
8
9
  export function modelCachePath(home = os.homedir(), profile = "codex") {
@@ -23,40 +24,69 @@ export function isValidModelList(models) {
23
24
  return modelData(models) !== null;
24
25
  }
25
26
 
26
- export function loadModelCache({
27
+ function normalizedTtl(value, fallback) {
28
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
29
+ }
30
+
31
+ export function loadModelCacheEntry({
27
32
  home = os.homedir(),
28
33
  profile = "codex",
29
34
  credentialFingerprint = "",
30
- maxAgeMs = DEFAULT_MAX_AGE_MS,
35
+ softTtlMs = MODEL_CACHE_SOFT_TTL_MS,
36
+ hardTtlMs,
37
+ maxAgeMs,
31
38
  now = Date.now,
32
39
  } = {}) {
33
40
  try {
34
41
  const parsed = JSON.parse(fs.readFileSync(modelCachePath(home, profile), "utf8"));
35
42
  const savedAt = Date.parse(parsed.saved_at);
36
- if (!Number.isFinite(savedAt) || now() - savedAt > maxAgeMs) return null;
43
+ if (!Number.isFinite(savedAt)) return null;
37
44
  const expectedFingerprint = String(credentialFingerprint || "").trim();
38
45
  if (expectedFingerprint && parsed.credential_fingerprint !== expectedFingerprint) return null;
39
46
  if (!isValidModelList(parsed.models)) return null;
40
- return parsed.models;
47
+ const resolvedHardTtlMs = normalizedTtl(
48
+ hardTtlMs,
49
+ normalizedTtl(maxAgeMs, MODEL_CACHE_HARD_TTL_MS),
50
+ );
51
+ const resolvedSoftTtlMs = Math.min(
52
+ normalizedTtl(softTtlMs, MODEL_CACHE_SOFT_TTL_MS),
53
+ resolvedHardTtlMs,
54
+ );
55
+ const ageMs = Math.max(0, now() - savedAt);
56
+ if (ageMs > resolvedHardTtlMs) return null;
57
+ return {
58
+ models: parsed.models,
59
+ savedAtMs: savedAt,
60
+ ageMs,
61
+ state: ageMs > resolvedSoftTtlMs ? "stale" : "fresh",
62
+ };
41
63
  } catch {
42
64
  return null;
43
65
  }
44
66
  }
45
67
 
68
+ export function loadModelCache(options = {}) {
69
+ return loadModelCacheEntry(options)?.models || null;
70
+ }
71
+
46
72
  export function saveModelCache(models, {
47
73
  home = os.homedir(),
48
74
  profile = "codex",
49
75
  credentialFingerprint = "",
76
+ isCurrent = () => true,
77
+ now = Date.now,
50
78
  } = {}) {
51
79
  if (!isValidModelList(models)) return false;
80
+ if (typeof isCurrent === "function" && !isCurrent()) return false;
52
81
  const filePath = modelCachePath(home, profile);
53
82
  const tempPath = `${filePath}.${process.pid}.tmp`;
54
- const payload = { saved_at: new Date().toISOString(), models };
83
+ const payload = { saved_at: new Date(now()).toISOString(), models };
55
84
  const fingerprint = String(credentialFingerprint || "").trim();
56
85
  if (fingerprint) payload.credential_fingerprint = fingerprint;
57
86
  fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
58
87
  try {
59
88
  fs.writeFileSync(tempPath, `${JSON.stringify(payload)}\n`, { mode: 0o600 });
89
+ if (typeof isCurrent === "function" && !isCurrent()) return false;
60
90
  fs.renameSync(tempPath, filePath);
61
91
  fs.chmodSync(filePath, 0o600);
62
92
  return true;
package/src/models.mjs CHANGED
@@ -117,6 +117,23 @@ export function modelIsResponsesOnly(model) {
117
117
  return hasResponses && !endpoints.includes("/chat/completions");
118
118
  }
119
119
 
120
+ export function resolveCopilotPriorityTierModel(model, serviceTier, models) {
121
+ const baseModel = String(model || "").trim();
122
+ if (baseModel !== "gpt-5.6-sol" || serviceTier !== "priority") return null;
123
+ const fastModelId = "gpt-5.6-sol-fast";
124
+ const fastModel = copilotModelData(models)
125
+ .find((entry) => String(entry?.id || "").trim() === fastModelId);
126
+ const vendor = String(fastModel?.vendor || fastModel?.owned_by || "").trim().toLowerCase();
127
+ const policy = String(fastModel?.policy?.state || "").trim().toLowerCase();
128
+ if (vendor !== "openai"
129
+ || policy !== "enabled"
130
+ || fastModel?.model_picker_enabled !== true
131
+ || !modelIsResponsesOnly(fastModel)) {
132
+ return null;
133
+ }
134
+ return fastModelId;
135
+ }
136
+
120
137
  function copilotModelData(models) {
121
138
  const data = Array.isArray(models) ? models : models?.data;
122
139
  return Array.isArray(data) ? data : [];
@@ -116,11 +116,23 @@ function modelRegistryStatus(modelRegistry) {
116
116
  const modelData = Array.isArray(modelRegistry?.models)
117
117
  ? modelRegistry.models
118
118
  : modelRegistry?.models?.data;
119
- return {
119
+ const result = {
120
120
  source: String(modelRegistry?.source || "built-in"),
121
121
  models: Array.isArray(modelData) ? modelData.length : 0,
122
122
  claude_models: Array.isArray(modelRegistry?.modelDefs) ? modelRegistry.modelDefs.length : 0,
123
123
  };
124
+ if (typeof modelRegistry?.cacheState === "string") {
125
+ const savedAtMs = Number(modelRegistry.cacheSavedAtMs);
126
+ result.cache_state = modelRegistry.cacheState;
127
+ result.cache_age_ms = Number.isFinite(savedAtMs) ? Math.max(0, Date.now() - savedAtMs) : null;
128
+ result.refresh_in_flight = modelRegistry.refreshInFlight === true;
129
+ result.generation = finiteRuntimeNumber(modelRegistry.generation);
130
+ result.last_error = typeof modelRegistry.lastError === "string" ? modelRegistry.lastError : null;
131
+ result.last_error_at_ms = Number.isFinite(modelRegistry.lastErrorAtMs)
132
+ ? modelRegistry.lastErrorAtMs
133
+ : null;
134
+ }
135
+ return result;
124
136
  }
125
137
 
126
138
  function finiteRuntimeNumber(value, fallback = 0) {