@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.
- package/README.md +4 -4
- package/gui/dist/assets/{index-BNESwCzn.js → index-eBA05kYB.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/src/adapters/cursor/discovery.ts +22 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-transport.ts +26 -5
- package/src/adapters/cursor/protobuf-request.ts +35 -5
- package/src/adapters/cursor/request-builder.ts +29 -14
- package/src/adapters/cursor/types.ts +8 -1
- package/src/codex/features.ts +58 -0
- package/src/generated/compatibility-version.json +17 -13
- package/src/lib/bun-stream-caps.ts +7 -4
- package/src/providers/command-code-efforts.ts +7 -0
- package/src/providers/quota.ts +98 -25
- package/src/providers/registry.ts +90 -10
|
@@ -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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
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(
|
|
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 =
|
|
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
|
|
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 =>
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/codex/features.ts
CHANGED
|
@@ -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.
|
|
5
|
+
"bunRuntimeVersion": "1.4.0",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": "bun.lock",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "29a6cf6ad4c475b5ec0fbcc104b032fbdb293fed12eb17d803ce626481e7e2f5"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "package.json",
|
|
13
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
173
|
+
"sha256": "d712426a8ee1b3c4de4d0705b1277e1edd1d5d96ad1c264fb852c2ff426dd50e"
|
|
170
174
|
},
|
|
171
175
|
{
|
|
172
176
|
"path": "src/adapters/cursor/request-builder.ts",
|
|
173
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
2169
|
+
"sha256": "940224c87ba91f249b04e7088e71a151914d186f43cb48abcb636bc306e00d7e"
|
|
2166
2170
|
},
|
|
2167
2171
|
{
|
|
2168
2172
|
"path": "src/providers/registry.ts",
|
|
2169
|
-
"sha256": "
|
|
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).
|
|
7
|
-
* that fix
|
|
8
|
-
* "known-bad"
|
|
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 =
|
|
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
|
package/src/providers/quota.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
673
|
-
*
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
736
|
+
const value = normalizePercent(data[key]);
|
|
698
737
|
if (value !== undefined) return value;
|
|
699
|
-
const nested = asRecord(data
|
|
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 ?
|
|
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"
|
|
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")) {
|