@bitkyc08/opencodex 2.29.0 → 2.30.0-preview.20260821

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.
@@ -13,6 +13,8 @@ import {
13
13
  type TranslatorBudget,
14
14
  } from "../../lib/translator-budget";
15
15
  import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
16
+ import { prepareCursorRawMessages, resolveActiveCursorImages } from "./images";
17
+ import { cursorRequestMessagesFromRaw } from "./request-builder";
16
18
  import {
17
19
  createCursorContextUsageTracker,
18
20
  createCursorProtobufEventState,
@@ -569,10 +571,29 @@ class LiveCursorTransport implements CursorTransport {
569
571
 
570
572
  // Advertise MCP tools before the stream opens — the server only calls tools it was told about.
571
573
  await this.prepareMcp();
572
- const activeText = activePromptText(request);
573
- this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(request, this.clientToolFinalizeGraceMs);
574
- const cursorVisibleTools = cursorToolsForActivePrompt(request.tools, activeText, request.toolChoice);
575
- const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, request.toolChoice);
574
+ // JPEG soft-cap rewrite for active-turn data: images before encode. Rebuild text
575
+ // messages from the prepared raw channel so omission markers replace stale
576
+ // pre-rewrite content that activePromptText and the tool filter would otherwise see.
577
+ const preparedRaw = await prepareCursorRawMessages(request.rawMessages, signal);
578
+ const preparedRawMessages = preparedRaw.messages;
579
+ const selectedImages = await resolveActiveCursorImages(
580
+ preparedRawMessages,
581
+ signal,
582
+ preparedRaw.images,
583
+ );
584
+ const preparedMessages = preparedRawMessages === request.rawMessages
585
+ ? request.messages
586
+ : cursorRequestMessagesFromRaw(preparedRawMessages);
587
+ const activeRequest: CursorRunRequest = {
588
+ ...request,
589
+ messages: preparedMessages,
590
+ rawMessages: preparedRawMessages,
591
+ selectedImages,
592
+ };
593
+ const activeText = activePromptText(activeRequest);
594
+ this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(activeRequest, this.clientToolFinalizeGraceMs);
595
+ const cursorVisibleTools = cursorToolsForActivePrompt(activeRequest.tools, activeText, activeRequest.toolChoice);
596
+ const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, activeRequest.toolChoice);
576
597
  // `request.tools` is the catalog already filtered and budgeted by request-builder. Derive
577
598
  // conversion provenance only from tagged synthetic tools that also survive this final prompt
578
599
  // filter; a client tool with the same wire name can never opt into conversion by collision.
@@ -608,7 +629,7 @@ class LiveCursorTransport implements CursorTransport {
608
629
  });
609
630
  // Build the payload once. The estimate is only worth deriving when there is no
610
631
  // carry-forward to fall back on — with a carry present it would never be used (#373).
611
- const prepared = prepareCursorRunRequest(request, {
632
+ const prepared = prepareCursorRunRequest(activeRequest, {
612
633
  estimateInputTokens: contextUsage.carryForwardTokens === undefined,
613
634
  });
614
635
  this.blobRequestScope = prepared.blobRequestScope;
@@ -15,6 +15,7 @@ import {
15
15
  storeCursorBlob,
16
16
  type CursorBlobRequestScopeToken,
17
17
  } from "./native-exec";
18
+ import { buildSelectedContext, CURSOR_VISION_IMAGE_HISTORY_MARKER } from "./images";
18
19
  import { estimateTokens } from "../../lib/token-estimate";
19
20
  import { parseDataUrl } from "../image";
20
21
  import {
@@ -219,7 +220,7 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
219
220
  const message = messages[i];
220
221
  if (!message) continue;
221
222
  if (message.role === "user" || message.role === "developer") {
222
- const text = contentText(message).trim();
223
+ const text = historyContentText(message).trim();
223
224
  // Cursor root replay expects OpenAI-style content parts for historical user messages.
224
225
  // A bare string survives blob hydration but external workers reject the completed replay
225
226
  // before tokenization (`usedTokens: 0`, then invalid_argument).
@@ -336,7 +337,7 @@ function contentText(message: OcxMessage): string {
336
337
  .map(part => {
337
338
  if (part.type === "text") return part.text;
338
339
  if (part.type === "thinking") return part.thinking;
339
- if (part.type === "image") return `[image produced by this tool, omitted from Cursor text replay: ${part.detail ?? "auto"}]`;
340
+ if (part.type === "image") return undefined;
340
341
  return undefined;
341
342
  })
342
343
  .filter((value): value is string => typeof value === "string" && value.length > 0)
@@ -346,7 +347,26 @@ function contentText(message: OcxMessage): string {
346
347
  function contentToText(content: OcxToolResultMessage["content"]): string {
347
348
  if (typeof content === "string") return content;
348
349
  return content
349
- .map(part => part.type === "text" ? part.text : `[image produced by this tool, omitted from Cursor text replay: ${part.detail ?? "auto"}]`)
350
+ .map(part => {
351
+ if (part.type === "text") return part.text;
352
+ if (part.type === "image") return CURSOR_VISION_IMAGE_HISTORY_MARKER;
353
+ return undefined;
354
+ })
355
+ .filter((value): value is string => typeof value === "string" && value.length > 0)
356
+ .join("\n");
357
+ }
358
+
359
+ /** History serializer. Replayed turns are text-only; never embed image bytes. */
360
+ function historyContentText(message: OcxMessage): string {
361
+ if (message.role === "toolResult" || typeof message.content === "string") return contentText(message);
362
+ return message.content
363
+ .map(part => {
364
+ if (part.type === "text") return part.text;
365
+ if (part.type === "thinking") return part.thinking;
366
+ if (part.type === "image") return CURSOR_VISION_IMAGE_HISTORY_MARKER;
367
+ return undefined;
368
+ })
369
+ .filter((value): value is string => typeof value === "string" && value.length > 0)
350
370
  .join("\n");
351
371
  }
352
372
 
@@ -721,8 +741,10 @@ function conversationTurns(
721
741
  flush();
722
742
  current = {
723
743
  userMessage: storeCursorBlob(toBinary(UserMessageSchema, create(UserMessageSchema, {
724
- text: contentText(message),
744
+ text: historyContentText(message),
725
745
  messageId: crypto.randomUUID(),
746
+ selectedContext: buildSelectedContext([], requestScope),
747
+ mode: 1,
726
748
  })), requestScope),
727
749
  steps: [],
728
750
  };
@@ -792,6 +814,7 @@ function buildPreparedCursorRunRequest(
792
814
  ? appendCursorGenericToolUseHint(request.tools, rawText)
793
815
  : rawText;
794
816
  const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
817
+ const selectedImages = request.selectedImages ?? [];
795
818
  // Native models resume the remembered Cursor conversation. External wire
796
819
  // models continue as userMessageAction so history-blob tool results stay
797
820
  // visible without a ResumeAction. Some native composer ids are also routed
@@ -799,7 +822,11 @@ function buildPreparedCursorRunRequest(
799
822
  // because a bare resumeAction makes them continue exploring with native tools
800
823
  // instead of answering (observed on composer-2.5; see discovery.ts).
801
824
  const externalToolContinuation = lastRawIsToolResult && cursorNeedsExternalToolContinuation(request.modelId);
802
- const actionCase = (externalToolContinuation || (!lastRawIsToolResult && text.trim().length > 0))
825
+ // Image-only active turns (including soft-omitted images) stay userMessageAction.
826
+ const actionCase = (
827
+ externalToolContinuation
828
+ || (!lastRawIsToolResult && (text.trim().length > 0 || selectedImages.length > 0))
829
+ )
803
830
  ? "userMessageAction"
804
831
  : "resumeAction";
805
832
  const actionText = externalToolContinuation
@@ -813,6 +840,9 @@ function buildPreparedCursorRunRequest(
813
840
  userMessage: create(UserMessageSchema, {
814
841
  text: actionText,
815
842
  messageId: crypto.randomUUID(),
843
+ selectedContext: buildSelectedContext(selectedImages, requestScope),
844
+ // OmniRoute / cursor-agent always send mode=1 on UserMessage.
845
+ mode: 1,
816
846
  }),
817
847
  requestContext: buildRequestContext(),
818
848
  }),
@@ -31,6 +31,7 @@ import {
31
31
  type CursorCheckpointInvalidationReason,
32
32
  type CursorCheckpointSnapshot,
33
33
  } from "./checkpoint-store";
34
+ import { extractCursorImageUrls } from "./images";
34
35
 
35
36
  /** Probe-verified Cursor Connect boundaries, with byte headroom for the enclosing field. */
36
37
  export const CURSOR_TOOL_COUNT_LIMIT = 330;
@@ -211,15 +212,8 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
211
212
  case "thinking":
212
213
  return part.thinking;
213
214
  case "image":
214
- // User-message images are still flattened here: this path builds the plain-text prompt, and
215
- // the schema slot that could carry them (UserMessage.selectedContext.selectedImages) is not
216
- // populated by this adapter. The tool-result ENCODER does build real McpImageContent
217
- // (see protobuf-request.ts), so the old "unsupported by Cursor adapter" wording is no
218
- // longer true of the encoder — but note that nothing reaches Cursor today either way:
219
- // every Cursor model is in noVisionModels (providers/registry.ts), so the vision sidecar
220
- // describes or strips images before this adapter runs. Kept the same length to avoid
221
- // shifting any byte-budgeted prompt path.
222
- return `[image omitted from this Cursor text prompt: ${part.detail ?? "auto"}]`;
215
+ // Images ride UserMessage.selected_context (SelectedImage) instead of text.
216
+ return undefined;
223
217
  case "toolCall":
224
218
  // Cursor does not accept OpenAI Responses assistant tool-call parts as native history here.
225
219
  // Rendering them as visible "[tool_call]" text leaks synthetic protocol markers back into
@@ -252,9 +246,19 @@ function requestMessage(message: OcxMessage): CursorRequestMessage | undefined {
252
246
  switch (message.role) {
253
247
  case "user":
254
248
  case "developer":
255
- return { role: message.role, content: contentToText(message.content) };
249
+ {
250
+ const content = contentToText(message.content);
251
+ // Image-only turns survive as empty content; the encoder keeps them userMessageAction.
252
+ if (content.length === 0 && extractCursorImageUrls(message.content).length === 0) {
253
+ return undefined;
254
+ }
255
+ return { role: message.role, content };
256
+ }
256
257
  case "assistant":
257
- return { role: "assistant", content: contentToText(message.content) };
258
+ {
259
+ const content = contentToText(message.content);
260
+ return content.length > 0 ? { role: "assistant", content } : undefined;
261
+ }
258
262
  case "toolResult":
259
263
  return {
260
264
  role: "tool",
@@ -263,6 +267,19 @@ function requestMessage(message: OcxMessage): CursorRequestMessage | undefined {
263
267
  }
264
268
  }
265
269
 
270
+ /**
271
+ * Rebuild the text `messages` channel from prepared `rawMessages` so omission markers
272
+ * and JPEG-rewritten parts stay visible to activePromptText after image preparation.
273
+ */
274
+ export function cursorRequestMessagesFromRaw(
275
+ messages: readonly OcxMessage[] | undefined,
276
+ ): CursorRequestMessage[] {
277
+ if (!messages?.length) return [];
278
+ return messages
279
+ .map(requestMessage)
280
+ .filter((message): message is CursorRequestMessage => !!message);
281
+ }
282
+
266
283
  export function generatedCursorConversationId(): string {
267
284
  return `cursor_${crypto.randomUUID().replace(/-/g, "")}`;
268
285
  }
@@ -409,9 +426,7 @@ export function createCursorRequest(
409
426
  parsed: OcxParsedRequest,
410
427
  options: CreateCursorRequestOptions = {},
411
428
  ): CursorRunRequest {
412
- const messages = parsed.context.messages
413
- .map(requestMessage)
414
- .filter((message): message is CursorRequestMessage => !!message && message.content.length > 0);
429
+ const messages = cursorRequestMessagesFromRaw(parsed.context.messages);
415
430
  const activeText = [...messages].reverse().find(message => message.role === "user" || message.role === "developer")?.content ?? "";
416
431
  const visibleTools = cursorToolsForActivePrompt(parsed.context.tools, activeText, parsed.options.toolChoice);
417
432
  const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
@@ -2,6 +2,7 @@ import type { OcxUsage } from "../../types";
2
2
  import type { OcxMessage, OcxRequestOptions, OcxTool } from "../../types";
3
3
  import type { CursorRoutingLevel } from "./discovery";
4
4
  import type { CursorCheckpointInvalidationReason } from "./checkpoint-store";
5
+ import type { ResolvedCursorImage } from "./images";
5
6
 
6
7
  export interface CursorRequestedModelParameter {
7
8
  id: string;
@@ -17,7 +18,13 @@ export interface CursorRunRequest {
17
18
  conversationId: string;
18
19
  system: string[];
19
20
  messages: CursorRequestMessage[];
20
- rawMessages?: OcxMessage[];
21
+ rawMessages?: readonly OcxMessage[];
22
+ /**
23
+ * Images for the active user/developer turn. Encoded as SelectedImage blobIdWithData refs under
24
+ * UserMessage.selected_context (bytes live in the request-scoped KV store for getBlobArgs
25
+ * hydration). History stays text-only. data: URLs only in this slice.
26
+ */
27
+ selectedImages?: readonly ResolvedCursorImage[];
21
28
  tools?: OcxTool[];
22
29
  toolChoice?: OcxRequestOptions["toolChoice"];
23
30
  parallelToolCalls?: boolean;
@@ -155,6 +155,29 @@ export function multiAgentV2EnabledFromConfigText(content: string | null): boole
155
155
  return false;
156
156
  }
157
157
 
158
+ // Bun 1.4 enforces TOML's "value must begin on the assignment line" rule that
159
+ // 1.3.14 did not, so `hint =` followed by `[` on the next line now fails the
160
+ // real parse and reaches the line-based fallback below — which reads that `[`
161
+ // as a table header and truncates the table before `enabled`. Codex's own
162
+ // parser accepts the document, so answering "disabled" would report a parser
163
+ // disagreement as a feature state (#1295, #1691).
164
+ //
165
+ // Joining a dangling `=` to the line that follows is the smallest repair that
166
+ // keeps the scanner untouched: widening `tomlTableBody` to be string-aware is
167
+ // what previously broke `getAgentsEnabled`, `getAgentsMaxDepth`, and
168
+ // `getMaxConcurrentThreads` (see its comment). If the joined document parses,
169
+ // that answer is authoritative; if it does not, nothing is lost.
170
+ const joined = joinDanglingTomlAssignments(content);
171
+ if (joined !== content) {
172
+ const reparsed = parsedTomlTable(joined, "features");
173
+ if (reparsed !== null) {
174
+ const table = plainTomlRecord(reparsed.multi_agent_v2);
175
+ if (table !== null) return table.enabled === true;
176
+ if (typeof reparsed.multi_agent_v2 === "boolean") return reparsed.multi_agent_v2;
177
+ return false;
178
+ }
179
+ }
180
+
158
181
  const table = tomlTableBody(content, "features.multi_agent_v2");
159
182
  if (table !== null) {
160
183
  const enabled = tomlBoolInBody(table, "enabled");
@@ -184,6 +207,41 @@ function plainTomlRecord(value: unknown): Record<string, unknown> | null {
184
207
  : null;
185
208
  }
186
209
 
210
+ /**
211
+ * Join `key =` to the following line when the value was written on the next
212
+ * line, so a parser enforcing TOML's same-line rule can read the document.
213
+ *
214
+ * Bun 1.3.14 accepted this shape; Bun 1.4 rejects it, correctly — TOML requires
215
+ * the value to begin on the assignment line. Codex's parser still accepts it, so
216
+ * this exists to keep the two readers agreeing rather than to endorse the shape.
217
+ *
218
+ * Deliberately narrow: it only acts on a line whose LAST non-comment character
219
+ * is `=`, which cannot occur in a valid assignment. Lines inside multi-line
220
+ * strings are left alone — a `"""` body line ending in `=` would be rewritten,
221
+ * but the result is only used when it PARSES, and the unmodified document is
222
+ * always tried first, so a wrong join cannot displace a correct read.
223
+ */
224
+ function joinDanglingTomlAssignments(content: string): string {
225
+ const lines = content.split("\n");
226
+ const out: string[] = [];
227
+ for (let i = 0; i < lines.length; i++) {
228
+ const line = lines[i]!;
229
+ // A dangling assignment: trailing `=` with nothing after it on this line.
230
+ if (/^[^#]*[^=!<>]=\s*$/.test(line) && i + 1 < lines.length) {
231
+ let j = i + 1;
232
+ // Skip blank and comment-only lines between the `=` and its value.
233
+ while (j < lines.length && /^\s*(?:#.*)?$/.test(lines[j]!)) j++;
234
+ if (j < lines.length) {
235
+ out.push(`${line.replace(/\s*$/, "")} ${lines[j]!.replace(/^\s*/, "")}`);
236
+ i = j;
237
+ continue;
238
+ }
239
+ }
240
+ out.push(line);
241
+ }
242
+ return out.join("\n");
243
+ }
244
+
187
245
  /**
188
246
  * A top-level table from a full TOML parse, or null when the document does not
189
247
  * parse. A parsed document with no such table yields `{}` rather than null: that
@@ -2,15 +2,15 @@
2
2
  "schemaVersion": 1,
3
3
  "assertionDslVersion": "1.0.0",
4
4
  "evidenceSchemaVersion": "1.0.0",
5
- "bunRuntimeVersion": "1.3.14",
5
+ "bunRuntimeVersion": "1.4.0",
6
6
  "files": [
7
7
  {
8
8
  "path": "bun.lock",
9
- "sha256": "dbbd8ffc7f0fc893dbf15cb96552b9ba044a4ffaf0447ac5f6c1817b9ec48ca2"
9
+ "sha256": "29a6cf6ad4c475b5ec0fbcc104b032fbdb293fed12eb17d803ce626481e7e2f5"
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "fd4c288d8c09df6271ed0e94759eba0c897c31a3a66431ba5c658225046c3745"
13
+ "sha256": "e93619c60e2ad2e56d45c02fa28e0f3df982f60821532d3211d764c7f4e199b3"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -78,7 +78,7 @@
78
78
  },
79
79
  {
80
80
  "path": "src/adapters/cursor/discovery.ts",
81
- "sha256": "07916522a2df0c9bb15c4676cfb7c24d3bb39e7cfa857f6e0ac97421ef695264"
81
+ "sha256": "cdebbff19a9d7e3a51ec0444c21a2d09aaf30e49858c4a791b28176096aa2607"
82
82
  },
83
83
  {
84
84
  "path": "src/adapters/cursor/effort-map.ts",
@@ -100,6 +100,10 @@
100
100
  "path": "src/adapters/cursor/http1-bidi.ts",
101
101
  "sha256": "351a05fa5bab54709c5fcb1bbbaaae2974f041ea6033283b854812d97df53c7d"
102
102
  },
103
+ {
104
+ "path": "src/adapters/cursor/images.ts",
105
+ "sha256": "3fe4ef5a79540055ec57b896fe5df73c3b0a532cc9d9031e29e1921d52969c89"
106
+ },
103
107
  {
104
108
  "path": "src/adapters/cursor/kv-store.ts",
105
109
  "sha256": "9699c3b4bf5c61f42f2a6653755dc46d662513b728e0e284ca2b46a1006c57ed"
@@ -114,7 +118,7 @@
114
118
  },
115
119
  {
116
120
  "path": "src/adapters/cursor/live-transport.ts",
117
- "sha256": "f44b3fad927a7559f4ddf231f02216c0f732570c4a7765338ee7ab0a2431283b"
121
+ "sha256": "f7ec1630801fb7fd240052807e0cbd1b32207e0b13e4c95874757f0686cf57c8"
118
122
  },
119
123
  {
120
124
  "path": "src/adapters/cursor/mcp-config.ts",
@@ -166,11 +170,11 @@
166
170
  },
167
171
  {
168
172
  "path": "src/adapters/cursor/protobuf-request.ts",
169
- "sha256": "43a9745dec2007f819e30b6ee8cc1221040bf7138228f5a3dce5cb2f97fc382c"
173
+ "sha256": "d712426a8ee1b3c4de4d0705b1277e1edd1d5d96ad1c264fb852c2ff426dd50e"
170
174
  },
171
175
  {
172
176
  "path": "src/adapters/cursor/request-builder.ts",
173
- "sha256": "d05a75fc2f051bbd6ca53ab085196ecc545d366d967dc31dc45be34c202307ba"
177
+ "sha256": "0d73c142a173ee52aefb90038f99667915c8def79e009bcf5044571d4f44cb0b"
174
178
  },
175
179
  {
176
180
  "path": "src/adapters/cursor/thread-continuity.ts",
@@ -194,7 +198,7 @@
194
198
  },
195
199
  {
196
200
  "path": "src/adapters/cursor/types.ts",
197
- "sha256": "18b53a305b3006ead774026dd91b7b15667e106c56b2e9e3f69b3e81ab33447e"
201
+ "sha256": "afbcb4770747dd3287c3f9069805c4f9a2cb675f3e124bad8295b672a678b3ad"
198
202
  },
199
203
  {
200
204
  "path": "src/adapters/google-antigravity-replay.ts",
@@ -766,7 +770,7 @@
766
770
  },
767
771
  {
768
772
  "path": "src/codex/features.ts",
769
- "sha256": "f6e64aa07c4ac8330df679a2b613fac84029d2fe1b8f0d5e8ace27ea664dda16"
773
+ "sha256": "a42c3891a7978d27b974d68a3b5abecc7c3650e7d4d6f17c9e96ff52536f0a1e"
770
774
  },
771
775
  {
772
776
  "path": "src/codex/generation.ts",
@@ -1702,7 +1706,7 @@
1702
1706
  },
1703
1707
  {
1704
1708
  "path": "src/lib/bun-stream-caps.ts",
1705
- "sha256": "e1523443bb2aefcb59cf2759e9f41b5e5e3ca54b8e5cef0d421a22e8e35ac35c"
1709
+ "sha256": "d2255a860bc9ea451abc7913cca76779c41656d35965d80ce10c63ad94e032aa"
1706
1710
  },
1707
1711
  {
1708
1712
  "path": "src/lib/codex-restart-contract.ts",
@@ -2078,7 +2082,7 @@
2078
2082
  },
2079
2083
  {
2080
2084
  "path": "src/providers/command-code-efforts.ts",
2081
- "sha256": "3aaec189b3d2661bb27f9aa1487c9efa9d8ba180d05de86e90c8e87a865d8d2f"
2085
+ "sha256": "1f1e616bc6179d729f582142d854406be626d3900a978383c7fdc33a291476d8"
2082
2086
  },
2083
2087
  {
2084
2088
  "path": "src/providers/context-cap.ts",
@@ -2162,11 +2166,11 @@
2162
2166
  },
2163
2167
  {
2164
2168
  "path": "src/providers/quota.ts",
2165
- "sha256": "d74e47b5106a03d447532f283bdbfe96f632dee1ef662014bbfb049ddb857c8d"
2169
+ "sha256": "940224c87ba91f249b04e7088e71a151914d186f43cb48abcb636bc306e00d7e"
2166
2170
  },
2167
2171
  {
2168
2172
  "path": "src/providers/registry.ts",
2169
- "sha256": "ec1ad142e7ad3e2d49cf60fa59281f0d5fd995d94d61d3a9611acc6c8d2b327b"
2173
+ "sha256": "68af5ea5f483ace1f33d64d23b03ac778668c180e6b896b2e0f7043028217e05"
2170
2174
  },
2171
2175
  {
2172
2176
  "path": "src/providers/request-pacing.ts",
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * The eager bounded relay (src/server/relay-eager.ts) uses a JS async producer
5
5
  * loop — the exact shape of the Bun#32111 use-after-free (fixed upstream by Bun
6
- * PR #32120, merged 2026-06-21). No RELEASED Bun version is proven to carry
7
- * that fix yet, so `MIN_FIXED_BUN_VERSION` is null: every runtime is
8
- * "known-bad" until a bundle-bump commit sets it. Windows no-rewrite traffic
6
+ * PR #32120, merged 2026-06-21). Bun 1.4.0 is the first RELEASED version proven
7
+ * to carry that fix, so `MIN_FIXED_BUN_VERSION` is "1.4.0": older runtimes stay
8
+ * "known-bad". Windows no-rewrite traffic
9
9
  * follows this runtime/config decision, preserving the explicit legacy-tee
10
10
  * safety pin. Darwin no-rewrite traffic stays on tee
11
11
  * for `auto` regardless of runtime capability and reaches eager relay only via
@@ -21,8 +21,11 @@
21
21
  /**
22
22
  * Bump in the SAME commit that bumps package.json's bundled Bun to a version
23
23
  * verified to include Bun PR #32120. null = no released version is known-fixed.
24
+ * Bun 1.4.0 (npm stable, bundled by this package.json) carries the fix: PR
25
+ * #32120 merged 2026-06-21, well before the 1.4.0 cut, and the full suite ran
26
+ * green under the 1.4 line on every supported OS (devlog/260814_bun14-preview-dev).
24
27
  */
25
- export const MIN_FIXED_BUN_VERSION: string | null = null;
28
+ export const MIN_FIXED_BUN_VERSION: string | null = "1.4.0";
26
29
 
27
30
  export type StreamMode = "auto" | "legacy-tee" | "eager-relay";
28
31
 
@@ -9,6 +9,13 @@ const COMMAND_CODE_MODEL_EFFORTS = {
9
9
  efforts: ["high", "max"],
10
10
  profileUrl: "https://commandcode.ai/models/deepseek-v4-flash",
11
11
  },
12
+ // Ox Alpha (stealth preview, added in Command Code v1.31.0): free 1M-context
13
+ // reasoning model on every plan. The profile does not publish an effort ladder,
14
+ // so mirror the OpenRouter contract (reasoning mandatory; max/high/low).
15
+ "stealth/ox-alpha": {
16
+ efforts: ["low", "high", "max"],
17
+ profileUrl: "https://commandcode.ai/models/ox-alpha",
18
+ },
12
19
  // Keys must match the EXACT upstream /provider/v1/models ids (GLM ships as
13
20
  // `zai-org/GLM-5.3`, not `zai-org/glm-5.3`). The table doubles as the router's
14
21
  // known-ids decode source (via `knownModelIdsForProvider`), so a case mismatch
@@ -50,6 +50,7 @@ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
50
50
  const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
51
51
  const CLINE_BASE_URL = "https://api.cline.bot";
52
52
  const ZAI_BASE_URL = "https://api.z.ai";
53
+ const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
53
54
  const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
54
55
  const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
55
56
  const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
@@ -343,7 +344,12 @@ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
343
344
 
344
345
  function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
345
346
  const normalized = normalizedBaseUrl(baseUrl);
346
- return normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`;
347
+ return normalized === ZAI_BASE_URL
348
+ || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
349
+ || normalized === ZAI_CN_BASE_URL
350
+ || normalized === `${ZAI_CN_BASE_URL}/api/coding/paas/v4`
351
+ // BigModel serves the same GLM Coding Plan on the OpenAI Responses wire at /api/v1.
352
+ || normalized === `${ZAI_CN_BASE_URL}/api/v1`;
347
353
  }
348
354
 
349
355
  function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean {
@@ -669,34 +675,67 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
669
675
 
670
676
  /**
671
677
  * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
672
- * subscription's 5-hour token cycle, weekly quota, and monthly MCP usage.
673
- * Authenticates with the API key as a Bearer token per Z.AI's API reference.
678
+ * limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
679
+ * same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
680
+ * rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
681
+ * the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
682
+ * window). `TIME_LIMIT` rows are the monthly MCP tool budget (Web Search / Web
683
+ * Reader / Zread). Every row's `percentage` is the consumed share (falling
684
+ * back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
685
+ * the window reset.
674
686
  */
675
- async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
676
- if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
677
- const apiKey = resolveEnvValue(config.apiKey)?.trim();
678
- if (!apiKey) return null;
679
- const response = await fetch(`${ZAI_BASE_URL}/api/monitor/usage/quota/limit`, {
680
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
681
- redirect: "error",
682
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
683
- });
684
- if (!response.ok) {
685
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
686
- ? TERMINAL_QUOTA_FAILURE
687
- : null;
687
+ export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
688
+ const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
689
+ if (!limits) return null;
690
+ const quota: ProviderQuota = { updatedAt: Date.now() };
691
+ let windows = 0;
692
+ for (const raw of limits) {
693
+ const row = asRecord(raw);
694
+ if (!row) continue;
695
+ const resetAt = normalizeResetAt(row.nextResetTime);
696
+ let percent = normalizePercent(row.percentage);
697
+ if (percent === undefined) {
698
+ const used = toFiniteNumber(row.currentValue);
699
+ const total = toFiniteNumber(row.usage);
700
+ if (used !== undefined && total !== undefined && total > 0) {
701
+ percent = normalizePercent((used / total) * 100);
702
+ }
703
+ }
704
+ if (percent === undefined) continue;
705
+ if (row.type === "TOKENS_LIMIT" || row.type === "CREDIT_LIMIT") {
706
+ const unit = toFiniteNumber(row.unit);
707
+ const number = toFiniteNumber(row.number);
708
+ if (unit === 3 && number === 5) {
709
+ quota.fiveHourPercent = percent;
710
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
711
+ windows += 1;
712
+ } else if (unit === 6 && number === 1) {
713
+ quota.weeklyPercent = percent;
714
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
715
+ windows += 1;
716
+ }
717
+ } else if (row.type === "TIME_LIMIT") {
718
+ quota.monthlyPercent = percent;
719
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
720
+ windows += 1;
721
+ }
688
722
  }
689
- const body = asRecord(await readQuotaJson(response));
690
- if (!body || body.success === false) return null;
691
- const data = asRecord(body.data) ?? body;
692
- // The plugin renders a 5h token window, a weekly window, and a monthly MCP
693
- // window. Look for percent fields with window identifiers.
723
+ return windows > 0 ? quota : null;
724
+ }
725
+
726
+ /**
727
+ * Legacy Z.AI payload shape: percent fields with window identifiers directly on
728
+ * the data object (optionally nested under `quota`). Kept as a fallback so
729
+ * older responses keep rendering when the `limits` array is absent.
730
+ */
731
+ function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): ProviderQuota | null {
732
+ if (!data) return null;
694
733
  const quota: ProviderQuota = { updatedAt: Date.now() };
695
734
  let windows = 0;
696
735
  const percentAt = (key: string): number | undefined => {
697
- const value = normalizePercent(data?.[key]);
736
+ const value = normalizePercent(data[key]);
698
737
  if (value !== undefined) return value;
699
- const nested = asRecord(data?.quota);
738
+ const nested = asRecord(data.quota);
700
739
  return nested ? normalizePercent(nested[key]) : undefined;
701
740
  };
702
741
  const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed");
@@ -714,7 +753,40 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
714
753
  quota.monthlyPercent = monthly;
715
754
  windows += 1;
716
755
  }
717
- return windows > 0 ? report(provider, "zai:quota-limit", quota) : null;
756
+ return windows > 0 ? quota : null;
757
+ }
758
+
759
+ /**
760
+ * Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
761
+ * points at (api.z.ai or open.bigmodel.cn). Authenticates with the API key as
762
+ * a Bearer token per Z.AI's API reference. The `limits` array shape is
763
+ * preferred; older field-name payloads fall back to the legacy parser.
764
+ */
765
+ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
766
+ if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
767
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
768
+ if (!apiKey) return null;
769
+ const normalized = normalizedBaseUrl(config.baseUrl);
770
+ const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
771
+ ? ZAI_BASE_URL
772
+ : ZAI_CN_BASE_URL;
773
+ const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
774
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
775
+ redirect: "error",
776
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
777
+ });
778
+ if (!response.ok) {
779
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
780
+ ? TERMINAL_QUOTA_FAILURE
781
+ : null;
782
+ }
783
+ const body = asRecord(await readQuotaJson(response));
784
+ if (!body || body.success === false) return null;
785
+ const data = asRecord(body.data) ?? body;
786
+ const quota = Array.isArray(data?.limits)
787
+ ? parseZaiQuotaLimits(data)
788
+ : parseZaiQuotaLegacyFields(data);
789
+ return quota ? report(provider, "zai:quota-limit", quota) : null;
718
790
  }
719
791
 
720
792
  /**
@@ -2106,7 +2178,8 @@ async function maybeFetchProviderQuota(
2106
2178
  if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
2107
2179
  return fetchClineQuota(name, provider);
2108
2180
  }
2109
- if ((provider.authMode ?? "key") === "key" && name === "zai") {
2181
+ if ((provider.authMode ?? "key") === "key"
2182
+ && (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
2110
2183
  return fetchZaiQuota(name, provider);
2111
2184
  }
2112
2185
  if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) {