codex-copilot-dx 0.6.13 → 0.6.15

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.
@@ -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;
@@ -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) {
@@ -445,11 +445,14 @@ export function createPmStudioRelayHandler(options = {}) {
445
445
  releaseRequest = releaseOnce(admission);
446
446
  abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
447
447
  const body = await readJsonBody(req, { admission, signal: abort.signal });
448
- const modelType = modelRouter.classify(body?.model);
449
- if (modelType === "unsupported_claude") {
448
+ const routePlan = modelRouter.plan(body?.model);
449
+ if (routePlan.disposition === "reject") {
450
450
  throw localError(400, "model_not_supported", "The requested Claude model is not enabled for PM Studio");
451
451
  }
452
- if (modelType !== "claude") {
452
+ if (routePlan.disposition !== "relay"
453
+ || routePlan.origin !== "ccdx"
454
+ || routePlan.profile !== "claude"
455
+ || routePlan.protocol !== "openai-chat-completions") {
453
456
  throw localError(
454
457
  400,
455
458
  "native_route_required",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isValidModelList,
3
- loadModelCache,
3
+ loadModelCacheEntry,
4
4
  saveModelCache,
5
5
  } from "./model-cache.mjs";
6
6
  import {
@@ -22,13 +22,48 @@ function parsedCatalog(result) {
22
22
  if (result?.status < 200 || result?.status >= 300) {
23
23
  throw new Error(`Copilot models returned HTTP ${result?.status}`);
24
24
  }
25
- const models = JSON.parse(result.body);
25
+ let models;
26
+ try {
27
+ models = JSON.parse(result.body);
28
+ } catch {
29
+ throw new Error("Copilot models response was not valid JSON");
30
+ }
26
31
  if (!isValidModelList(models)) {
27
32
  throw new Error("Copilot models response contained no valid models");
28
33
  }
29
34
  return models;
30
35
  }
31
36
 
37
+ function createModelRegistry() {
38
+ return {
39
+ modelDefs: undefined,
40
+ models: undefined,
41
+ source: "built-in",
42
+ cacheState: "none",
43
+ cacheSavedAtMs: null,
44
+ lastError: null,
45
+ lastErrorAtMs: null,
46
+ refreshInFlight: false,
47
+ generation: 0,
48
+ };
49
+ }
50
+
51
+ function sameModelDefs(left, right) {
52
+ if (left === right) return true;
53
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
54
+ return JSON.stringify(left) === JSON.stringify(right);
55
+ }
56
+
57
+ function safeRefreshError(error, { timedOut = false, timeoutMs } = {}) {
58
+ if (timedOut) return `Model refresh timed out after ${timeoutMs}ms`;
59
+ const raw = String(error?.message || error?.name || "Model refresh failed")
60
+ .replace(/[\u0000-\u001f\u007f]+/g, " ")
61
+ .replace(/\b(?:github_pat_|gh[pousr]_|copilot_)[A-Za-z0-9._-]+\b/gi, "<redacted>")
62
+ .replace(/\bBearer\s+\S+/gi, "Bearer <redacted>")
63
+ .trim();
64
+ return (raw || "Model refresh failed").slice(0, 240);
65
+ }
66
+
32
67
  export function createProfileModelRuntime({
33
68
  codexClient,
34
69
  claudeClient = codexClient,
@@ -51,11 +86,13 @@ export function createProfileModelRuntime({
51
86
  throw new Error("Isolated Claude models require an isolated Claude client");
52
87
  }
53
88
 
54
- const codexRegistry = { modelDefs: undefined, models: undefined, source: "built-in" };
89
+ const codexRegistry = createModelRegistry();
55
90
  const claudeRegistry = claudeMode === "inherited"
56
91
  ? codexRegistry
57
- : { modelDefs: undefined, models: undefined, source: "built-in" };
92
+ : createModelRegistry();
58
93
  const customClaudeDefs = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
94
+ const refreshFlights = new Map();
95
+ const refreshGenerations = new Map();
59
96
  const refreshTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
60
97
  ? timeoutMs
61
98
  : DEFAULT_TIMEOUT_MS;
@@ -85,8 +122,9 @@ export function createProfileModelRuntime({
85
122
  ? customClaudeDefs.map((model) => ({ ...model }))
86
123
  : claudeDesktopModelDefsFromCopilotModels(models);
87
124
  if (discovered.length) {
125
+ const changed = !sameModelDefs(registry.modelDefs, discovered);
88
126
  registry.modelDefs = discovered;
89
- notifyClaudeModels(discovered);
127
+ if (changed) notifyClaudeModels(discovered);
90
128
  }
91
129
  return registry.modelDefs;
92
130
  }
@@ -96,11 +134,13 @@ export function createProfileModelRuntime({
96
134
  ? String(claudeCredentialFingerprint || "").trim()
97
135
  : String(codexCredentialFingerprint || "").trim();
98
136
  if (profile === "claude" && !credentialFingerprint) return false;
99
- const cached = loadModelCache({ home, profile, credentialFingerprint });
137
+ const cached = loadModelCacheEntry({ home, profile, credentialFingerprint });
100
138
  if (!cached) return false;
101
139
  try {
102
- applyCatalog({ client, models: cached, registry, source: "cache", ownsClaudeModels });
103
- return true;
140
+ applyCatalog({ client, models: cached.models, registry, source: "cache", ownsClaudeModels });
141
+ registry.cacheState = cached.state;
142
+ registry.cacheSavedAtMs = cached.savedAtMs;
143
+ return { loaded: true, stale: cached.state === "stale" };
104
144
  } catch {
105
145
  return false;
106
146
  }
@@ -131,13 +171,22 @@ export function createProfileModelRuntime({
131
171
  }
132
172
  }
133
173
 
134
- function persist(models, profile) {
174
+ function persist(models, profile, registry, generation) {
135
175
  const credentialFingerprint = profile === "claude"
136
176
  ? String(claudeCredentialFingerprint || "").trim()
137
177
  : String(codexCredentialFingerprint || "").trim();
138
178
  if (profile === "claude" && !credentialFingerprint) return;
139
179
  try {
140
- saveModelCache(models, { home, profile, credentialFingerprint });
180
+ const saved = saveModelCache(models, {
181
+ home,
182
+ profile,
183
+ credentialFingerprint,
184
+ isCurrent: () => refreshGenerations.get(profile) === generation,
185
+ });
186
+ if (saved) {
187
+ registry.cacheState = "fresh";
188
+ registry.cacheSavedAtMs = Date.now();
189
+ }
141
190
  } catch (error) {
142
191
  const label = profile === "codex" ? "Copilot" : "Claude Copilot";
143
192
  emit("warn", `Could not persist the ${label} model cache (${error.message})`);
@@ -162,12 +211,13 @@ export function createProfileModelRuntime({
162
211
  return profile === "claude" ? "built-in model list" : "no cached model list";
163
212
  }
164
213
 
165
- async function refreshProfile({ client, profile, registry, ownsClaudeModels, checksCodex }) {
214
+ async function performRefresh({ client, profile, registry, ownsClaudeModels, checksCodex }, generation) {
166
215
  const controllerLabel = claudeMode === "inherited"
167
216
  ? ""
168
217
  : `${profile === "codex" ? "Codex" : "Claude"} `;
169
218
  try {
170
219
  const models = await requestCatalog(client);
220
+ if (refreshGenerations.get(profile) !== generation) return registry.modelDefs;
171
221
  if (checksCodex) checkCodexCatalog(models);
172
222
  const modelDefs = applyCatalog({
173
223
  client,
@@ -176,20 +226,44 @@ export function createProfileModelRuntime({
176
226
  source: "live",
177
227
  ownsClaudeModels,
178
228
  });
179
- persist(models, profile);
229
+ persist(models, profile, registry, generation);
230
+ registry.lastError = null;
231
+ registry.lastErrorAtMs = null;
180
232
  if (ownsClaudeModels) logClaudeCatalog(modelDefs);
181
233
  return modelDefs;
182
234
  } catch (error) {
235
+ if (refreshGenerations.get(profile) !== generation) return registry.modelDefs;
183
236
  const fallback = fallbackLabel(registry, profile);
184
237
  const timedOut = error?.name === "AbortError" || error?.code === "ABORT_ERR";
238
+ const diagnostic = safeRefreshError(error, { timedOut, timeoutMs: refreshTimeoutMs });
239
+ registry.lastError = diagnostic;
240
+ registry.lastErrorAtMs = Date.now();
185
241
  const message = timedOut
186
242
  ? `${controllerLabel}model refresh timed out after ${refreshTimeoutMs}ms; using ${fallback}`
187
- : `Could not refresh ${controllerLabel.toLowerCase()}model list; using ${fallback} (${error.message})`;
243
+ : `Could not refresh ${controllerLabel.toLowerCase()}model list; using ${fallback} (${diagnostic})`;
188
244
  emit("warn", message[0].toUpperCase() + message.slice(1));
189
245
  return registry.modelDefs;
190
246
  }
191
247
  }
192
248
 
249
+ function refreshProfile(options) {
250
+ const { profile, registry } = options;
251
+ const existing = refreshFlights.get(profile);
252
+ if (existing) return existing.promise;
253
+
254
+ const generation = (refreshGenerations.get(profile) || 0) + 1;
255
+ refreshGenerations.set(profile, generation);
256
+ registry.generation = generation;
257
+ registry.refreshInFlight = true;
258
+ const flight = { generation, promise: null };
259
+ refreshFlights.set(profile, flight);
260
+ flight.promise = performRefresh(options, generation).finally(() => {
261
+ if (refreshFlights.get(profile) === flight) refreshFlights.delete(profile);
262
+ if (refreshGenerations.get(profile) === generation) registry.refreshInFlight = false;
263
+ });
264
+ return flight.promise;
265
+ }
266
+
193
267
  function refreshCodex() {
194
268
  return refreshProfile({
195
269
  client: codexClient,
@@ -2,6 +2,7 @@ import {
2
2
  isClaudeCopilotCatalogEntry,
3
3
  isClaudeCopilotModel,
4
4
  } from "./models.mjs";
5
+ import { createRoutePlan } from "./route-plan.mjs";
5
6
 
6
7
  function catalogData(catalog) {
7
8
  if (Array.isArray(catalog)) return catalog;
@@ -47,14 +48,51 @@ export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {})
47
48
  return { allowed, known };
48
49
  }
49
50
 
51
+ function classify(modelId) {
52
+ const id = String(modelId || "").trim();
53
+ const { allowed, known } = availability();
54
+ if (allowed.has(id)) return "claude";
55
+ if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
56
+ return "enterprise";
57
+ }
58
+
59
+ function plan(modelId) {
60
+ const id = String(modelId || "").trim();
61
+ const modelType = classify(id);
62
+ const protocol = "openai-chat-completions";
63
+ if (modelType === "claude") {
64
+ return createRoutePlan({
65
+ disposition: "relay",
66
+ origin: "ccdx",
67
+ profile: "claude",
68
+ protocol,
69
+ model: id,
70
+ surface: "pm-studio",
71
+ });
72
+ }
73
+ if (modelType === "unsupported_claude") {
74
+ return createRoutePlan({
75
+ disposition: "reject",
76
+ origin: "ccdx",
77
+ profile: "claude",
78
+ protocol,
79
+ model: id || "unknown",
80
+ surface: "pm-studio",
81
+ });
82
+ }
83
+ return createRoutePlan({
84
+ disposition: "native",
85
+ origin: "pm-studio-native",
86
+ profile: "enterprise",
87
+ protocol,
88
+ model: id || "unknown",
89
+ surface: "pm-studio",
90
+ });
91
+ }
92
+
50
93
  return Object.freeze({
51
- classify(modelId) {
52
- const id = String(modelId || "").trim();
53
- const { allowed, known } = availability();
54
- if (allowed.has(id)) return "claude";
55
- if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
56
- return "enterprise";
57
- },
94
+ plan,
95
+ classify,
58
96
  allowedModels() {
59
97
  return [...availability().allowed.values()];
60
98
  },
@@ -202,13 +202,17 @@ export function responseHistoryMaterializedBytes(responseId) {
202
202
  return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
203
203
  }
204
204
 
205
- export function materializeResponseHistory(responseId, { assertActive } = {}) {
205
+ export function materializeResponseHistory(responseId, { assertActive, routeMetadata } = {}) {
206
206
  assertActive?.();
207
207
  const { chain, rootId } = responseHistoryChain(responseId);
208
208
  const items = [];
209
209
  for (let index = 0; index < chain.length; index += 1) {
210
210
  if ((index & 63) === 0) assertActive?.();
211
211
  const entry = chain[chain.length - index - 1];
212
+ routeMetadata?.push({
213
+ affinity: entry.routeAffinity || null,
214
+ hasOpaque: entry.hasOpaque === true,
215
+ });
212
216
  items.push(...entry.inputItems, ...entry.outputItems);
213
217
  }
214
218
  assertActive?.();
@@ -218,11 +222,22 @@ export function materializeResponseHistory(responseId, { assertActive } = {}) {
218
222
  return materialized;
219
223
  }
220
224
 
221
- export function rememberResponseHistoryNode({ id, parentId, inputItems, outputItems, takeOwnership = false }) {
225
+ export function rememberResponseHistoryNode({
226
+ id,
227
+ parentId,
228
+ inputItems,
229
+ outputItems,
230
+ hasOpaque = false,
231
+ routeAffinity = null,
232
+ takeOwnership = false,
233
+ }) {
222
234
  if (!id || !Array.isArray(inputItems)) return;
223
235
  clearResponsesToolOutputPartsCache(inputItems);
224
236
  clearResponsesToolOutputPartsCache(outputItems);
225
- const bytes = jsonByteLength([inputItems, outputItems]);
237
+ const historyValue = routeAffinity
238
+ ? [inputItems, outputItems, { routeAffinity, hasOpaque: hasOpaque === true }]
239
+ : [inputItems, outputItems];
240
+ const bytes = jsonByteLength(historyValue);
226
241
  if (bytes > maxBytes) {
227
242
  if (histories.has(id)) removeSubtree(id);
228
243
  rememberEvictedId(id);
@@ -236,6 +251,8 @@ export function rememberResponseHistoryNode({ id, parentId, inputItems, outputIt
236
251
  rootId,
237
252
  inputItems: takeOwnership ? inputItems : cloneJson(inputItems),
238
253
  outputItems: takeOwnership ? outputItems : cloneJson(outputItems),
254
+ hasOpaque: hasOpaque === true,
255
+ routeAffinity: routeAffinity ? cloneJson(routeAffinity) : null,
239
256
  bytes,
240
257
  };
241
258
  const oldRootId = existing?.rootId;
@@ -38,6 +38,23 @@ export function responsesBodyUsesCustomTools(body) {
38
38
  && body.input.some((item) => item?.type === "custom_tool_call" || item?.type === "custom_tool_call_output");
39
39
  }
40
40
 
41
+ function chatCompatibilityError(kind, itemType) {
42
+ const type = typeof itemType === "string" && itemType ? itemType : "unknown";
43
+ const message = `Responses ${kind} type "${type}" cannot be represented by Chat Completions`;
44
+ const error = new Error(message);
45
+ error.statusCode = 400;
46
+ error.code = "ccdx_responses_chat_incompatible";
47
+ error.jsonBody = {
48
+ error: {
49
+ message,
50
+ type: "invalid_request_error",
51
+ code: error.code,
52
+ item_type: type,
53
+ },
54
+ };
55
+ return error;
56
+ }
57
+
41
58
  export function responsesToChat(body) {
42
59
  const messages = [];
43
60
  if (body.instructions) messages.push({ role: "system", content: body.instructions });
@@ -65,19 +82,36 @@ export function responsesToChat(body) {
65
82
  if (typeof body.input === "string") {
66
83
  messages.push({ role: "user", content: body.input });
67
84
  } else if (Array.isArray(body.input)) {
68
- for (const item of body.input) {
69
- if (item.type === "message") {
85
+ for (let index = 0; index < body.input.length; index += 1) {
86
+ const item = body.input[index];
87
+ const isEasyMessage = item?.type === undefined
88
+ && ["user", "assistant", "system", "developer"].includes(item?.role)
89
+ && Object.prototype.hasOwnProperty.call(item, "content");
90
+ if (item?.type === "message" || isEasyMessage) {
70
91
  messages.push({ role: item.role, content: messageContent(item.content) });
71
- } else if (["input_image", "image_url", "image"].includes(item.type)) {
92
+ } else if (["input_image", "image_url", "image"].includes(item?.type)) {
72
93
  messages.push({ role: "user", content: messageContent([item]) });
73
- } else if (item.type === "function_call") {
94
+ } else if (item?.type === "function_call") {
95
+ const toolCalls = [];
96
+ while (index < body.input.length && body.input[index]?.type === "function_call") {
97
+ const call = body.input[index];
98
+ toolCalls.push({
99
+ id: call.call_id || randomUUID(),
100
+ type: "function",
101
+ function: { name: call.name, arguments: call.arguments },
102
+ });
103
+ index += 1;
104
+ }
105
+ index -= 1;
74
106
  messages.push({
75
107
  role: "assistant",
76
108
  content: null,
77
- tool_calls: [{ id: item.call_id || randomUUID(), type: "function", function: { name: item.name, arguments: item.arguments } }],
109
+ tool_calls: toolCalls,
78
110
  });
79
- } else if (item.type === "function_call_output") {
111
+ } else if (item?.type === "function_call_output") {
80
112
  messages.push({ role: "tool", tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) });
113
+ } else {
114
+ throw chatCompatibilityError("input item", item?.type);
81
115
  }
82
116
  }
83
117
  }
@@ -90,9 +124,10 @@ export function responsesToChat(body) {
90
124
  if (maxTok !== undefined) chatReq.max_completion_tokens = maxTok;
91
125
 
92
126
  if (body.tools?.length) {
127
+ const unsupportedTool = body.tools.find((tool) => tool?.type !== "function");
128
+ if (unsupportedTool) throw chatCompatibilityError("tool", unsupportedTool?.type);
93
129
  chatReq.tools = body.tools
94
130
  .map((tool) => {
95
- if (tool?.type !== "function") return null;
96
131
  if (tool.function?.name) return cloneJson(tool);
97
132
  const fn = {
98
133
  name: tool.name,
@@ -112,6 +147,8 @@ export function responsesToChat(body) {
112
147
  chatReq.tool_choice = body.tool_choice;
113
148
  } else if (body.tool_choice?.type === "function" && body.tool_choice.name) {
114
149
  chatReq.tool_choice = { type: "function", function: { name: body.tool_choice.name } };
150
+ } else {
151
+ throw chatCompatibilityError("tool choice", body.tool_choice?.type);
115
152
  }
116
153
  }
117
154
  if (body.parallel_tool_calls !== undefined) chatReq.parallel_tool_calls = body.parallel_tool_calls;
@@ -1,10 +1,6 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { httpError } from "./http-transport.mjs";
3
2
 
4
3
  const COMPACTION_TRIGGER_TYPE = "compaction_trigger";
5
- const RETAINED_MESSAGE_ROLES = new Set(["user", "assistant", "developer", "system"]);
6
- const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
7
- const APPROX_BYTES_PER_TOKEN = 4;
8
4
 
9
5
  function compactionError(message) {
10
6
  const error = httpError(message, 502);
@@ -19,47 +15,6 @@ function compactionError(message) {
19
15
  return error;
20
16
  }
21
17
 
22
- function normalizeMessageContent(content) {
23
- if (typeof content === "string") return [{ type: "input_text", text: content }];
24
- if (!Array.isArray(content)) return [];
25
- return content.map((part) => {
26
- if (!part || typeof part !== "object") return part;
27
- return part.type === "output_text"
28
- ? { ...part, type: "input_text" }
29
- : structuredClone(part);
30
- });
31
- }
32
-
33
- function approximateTextTokens(content) {
34
- return content.reduce((total, part) => {
35
- if (part?.type !== "input_text" || typeof part.text !== "string") return total;
36
- return total + Math.ceil(Buffer.byteLength(part.text) / APPROX_BYTES_PER_TOKEN);
37
- }, 0);
38
- }
39
-
40
- function retainedMessages(inputItems) {
41
- const kept = [];
42
- let usedTokens = 0;
43
- for (let index = inputItems.length - 1; index >= 0; index -= 1) {
44
- const item = inputItems[index];
45
- if (item?.type !== "message" || !RETAINED_MESSAGE_ROLES.has(item.role)) continue;
46
-
47
- const content = normalizeMessageContent(item.content);
48
- usedTokens += Math.max(approximateTextTokens(content), 1);
49
- if (usedTokens > RETAINED_MESSAGE_TOKEN_BUDGET && kept.length > 0) break;
50
-
51
- kept.push({
52
- type: "message",
53
- id: `msg_${randomUUID().replaceAll("-", "")}`,
54
- status: item.status ?? "completed",
55
- role: item.role,
56
- content,
57
- ...(item.phase !== undefined ? { phase: structuredClone(item.phase) } : {}),
58
- });
59
- }
60
- return kept.reverse();
61
- }
62
-
63
18
  export function prepareResponsesCompactionRequest(reqContext) {
64
19
  const input = Array.isArray(reqContext?.body?.input) ? reqContext.body.input : [];
65
20
  const compactionInputItems = compactionInputWithoutTrigger(input);
@@ -74,7 +29,7 @@ export function compactionInputWithoutTrigger(input) {
74
29
  : [];
75
30
  }
76
31
 
77
- export function createResponsesCompactionResult(inputItems, generated) {
32
+ export function createResponsesCompactionResult(_inputItems, generated) {
78
33
  if (!generated || typeof generated !== "object" || !generated.id || !Array.isArray(generated.output)) {
79
34
  throw compactionError("Copilot compaction returned an invalid response envelope");
80
35
  }
@@ -95,10 +50,6 @@ export function createResponsesCompactionResult(inputItems, generated) {
95
50
  return {
96
51
  ...generated,
97
52
  object: "response.compaction",
98
- output: [
99
- ...retainedMessages(Array.isArray(inputItems) ? inputItems : []),
100
- ...compactionItems,
101
- ],
102
53
  };
103
54
  }
104
55