@bitkyc08/opencodex 2.7.34 → 2.7.35
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 +1 -0
- package/gui/dist/assets/index-BunUANVE.js +52 -0
- package/gui/dist/index.html +1 -1
- package/package.json +5 -2
- package/src/adapters/anthropic.ts +22 -37
- package/src/adapters/cursor/cursor-errors.ts +11 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +5 -0
- package/src/adapters/cursor/protobuf-events.ts +14 -0
- package/src/adapters/cursor/protobuf-request.ts +11 -1
- package/src/adapters/cursor/request-builder.ts +20 -3
- package/src/adapters/cursor.ts +69 -22
- package/src/adapters/google-http.ts +23 -4
- package/src/adapters/google-tool-schema.ts +143 -68
- package/src/adapters/google-wire-compiler.ts +228 -0
- package/src/adapters/google.ts +27 -18
- package/src/adapters/kiro.ts +25 -1
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +565 -0
- package/src/claude/agents-inject.ts +4 -2
- package/src/codex/catalog.ts +31 -2
- package/src/codex/home.ts +1 -1
- package/src/codex/quota.ts +43 -13
- package/src/index.ts +1 -0
- package/src/lib/sse-decoder.ts +84 -0
- package/src/providers/antigravity-models.ts +9 -3
- package/src/server/chat-completions.ts +258 -0
- package/src/server/gui-static.ts +1 -0
- package/src/server/index.ts +24 -0
- package/src/server/management-api.ts +9 -0
- package/src/server/responses.ts +28 -2
- package/src/types.ts +6 -3
- package/gui/dist/assets/index-BkmJJgg6.js +0 -52
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BunUANVE.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-Sg-7L_oZ.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.35",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"dev:proxy": "bun run src/cli/index.ts start",
|
|
37
37
|
"dev:gui": "cd gui && bun run dev",
|
|
38
38
|
"start": "bun run src/cli/index.ts start",
|
|
39
|
-
"test": "bun test
|
|
39
|
+
"test": "bun scripts/test.ts",
|
|
40
40
|
"typecheck": "bun x tsc --noEmit",
|
|
41
41
|
"privacy:scan": "bun scripts/privacy-scan.ts",
|
|
42
42
|
"generate:jawcode-metadata": "bun scripts/generate-jawcode-metadata.ts",
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
"@types/bun": "1.3.14",
|
|
64
64
|
"typescript": "5.9.3"
|
|
65
65
|
},
|
|
66
|
+
"overrides": {
|
|
67
|
+
"fast-uri": "^3.1.4"
|
|
68
|
+
},
|
|
66
69
|
"keywords": [
|
|
67
70
|
"codex",
|
|
68
71
|
"openai",
|
|
@@ -21,6 +21,7 @@ import { normalizeAnthropicImages } from "./anthropic-image-normalize";
|
|
|
21
21
|
import { neutralizeIdentity } from "./identity";
|
|
22
22
|
import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
|
|
23
23
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
24
|
+
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
24
25
|
|
|
25
26
|
/** Map a user content part to an Anthropic content block (text or image source). */
|
|
26
27
|
function toAnthropicContentPart(p: OcxContentPart): unknown {
|
|
@@ -719,9 +720,6 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
719
720
|
return;
|
|
720
721
|
}
|
|
721
722
|
|
|
722
|
-
const reader = response.body.getReader();
|
|
723
|
-
const decoder = new TextDecoder();
|
|
724
|
-
let buffer = "";
|
|
725
723
|
let currentBlockType = "";
|
|
726
724
|
let currentToolCallId = "";
|
|
727
725
|
let currentToolCallName = "";
|
|
@@ -739,34 +737,19 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
739
737
|
};
|
|
740
738
|
};
|
|
741
739
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (done) break;
|
|
746
|
-
buffer += decoder.decode(value, { stream: true });
|
|
740
|
+
for await (const record of decodeServerSentEvents(response.body)) {
|
|
741
|
+
const payload = record.data.trim();
|
|
742
|
+
if (!payload) continue;
|
|
747
743
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
if (!line.startsWith("data: ")) continue;
|
|
758
|
-
const payload = line.slice(6).trim();
|
|
759
|
-
if (!payload) continue;
|
|
760
|
-
|
|
761
|
-
let data: Record<string, unknown>;
|
|
762
|
-
try {
|
|
763
|
-
data = JSON.parse(payload) as Record<string, unknown>;
|
|
764
|
-
} catch {
|
|
765
|
-
debugDroppedFrame("anthropic", payload);
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
744
|
+
let data: Record<string, unknown>;
|
|
745
|
+
try {
|
|
746
|
+
data = JSON.parse(payload) as Record<string, unknown>;
|
|
747
|
+
} catch {
|
|
748
|
+
debugDroppedFrame("anthropic", payload);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
768
751
|
|
|
769
|
-
|
|
752
|
+
switch (record.event || data.type) {
|
|
770
753
|
case "message_start": {
|
|
771
754
|
const message = data.message as { usage?: Record<string, number> } | undefined;
|
|
772
755
|
pendingUsage = mergeAnthropicUsage(pendingUsage, message?.usage);
|
|
@@ -794,7 +777,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
794
777
|
yield { type: "text_delta", text: delta.text };
|
|
795
778
|
} else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
796
779
|
yield { type: "thinking_delta", thinking: delta.thinking };
|
|
797
|
-
} else if (delta.type === "
|
|
780
|
+
} else if (delta.type === "reasoning_delta" && typeof delta.reasoning === "string") {
|
|
781
|
+
// Some Anthropic-compatible reasoning models use `reasoning` names for the
|
|
782
|
+
// otherwise equivalent thinking block. Preserve it as raw reasoning and keep
|
|
783
|
+
// later text blocks independent.
|
|
784
|
+
yield { type: "thinking_delta", thinking: delta.reasoning };
|
|
785
|
+
} else if (delta.type === "signature_delta" && typeof delta.signature === "string" && (currentBlockType === "thinking" || currentBlockType === "reasoning")) {
|
|
798
786
|
// Arrives once, just before the thinking block's content_block_stop; block-scoped
|
|
799
787
|
// so a stray signature on a non-thinking block can never be captured.
|
|
800
788
|
yield { type: "thinking_signature", signature: delta.signature };
|
|
@@ -827,20 +815,15 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
827
815
|
yield { type: "error", message: err?.message ?? "Anthropic error" };
|
|
828
816
|
return;
|
|
829
817
|
}
|
|
830
|
-
}
|
|
831
|
-
currentEventType = "";
|
|
832
|
-
}
|
|
833
818
|
}
|
|
834
|
-
if (pendingUsage && !emittedDone) yield* emitDone();
|
|
835
|
-
} finally {
|
|
836
|
-
reader.releaseLock();
|
|
837
819
|
}
|
|
820
|
+
if (pendingUsage && !emittedDone) yield* emitDone();
|
|
838
821
|
},
|
|
839
822
|
|
|
840
823
|
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
841
824
|
const json = await response.json() as Record<string, unknown>;
|
|
842
825
|
const events: AdapterEvent[] = [];
|
|
843
|
-
const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; signature?: string; data?: string }[] | undefined;
|
|
826
|
+
const content = json.content as { type: string; text?: string; id?: string; name?: string; input?: unknown; thinking?: string; reasoning?: string; signature?: string; data?: string }[] | undefined;
|
|
844
827
|
if (content) {
|
|
845
828
|
for (const block of content) {
|
|
846
829
|
if (block.type === "text" && block.text) {
|
|
@@ -850,6 +833,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
850
833
|
if (typeof block.signature === "string" && block.signature) {
|
|
851
834
|
events.push({ type: "thinking_signature", signature: block.signature });
|
|
852
835
|
}
|
|
836
|
+
} else if (block.type === "reasoning" && typeof block.reasoning === "string") {
|
|
837
|
+
events.push({ type: "thinking_delta", thinking: block.reasoning });
|
|
853
838
|
} else if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
854
839
|
events.push({ type: "redacted_thinking", data: block.data });
|
|
855
840
|
} else if (block.type === "tool_use") {
|
|
@@ -36,6 +36,17 @@ export function isCursorBenignCancelError(value: unknown): boolean {
|
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* True when Cursor Connect rejected the turn with invalid_argument.
|
|
41
|
+
* Seen after stepCompleted on brittle external-model continuations.
|
|
42
|
+
*/
|
|
43
|
+
export function isCursorInvalidArgumentError(value: unknown): boolean {
|
|
44
|
+
const code = errorCode(value).toLowerCase();
|
|
45
|
+
if (code === "invalid_argument") return true;
|
|
46
|
+
const message = errorMessage(value).toLowerCase();
|
|
47
|
+
return message.includes("invalid_argument");
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/**
|
|
40
51
|
* Classify a Cursor transport/Connect/gRPC error message into an actionable category.
|
|
41
52
|
* The returned prefix string is recognized by `src/lib/errors.ts` `classifyError` keywords,
|
|
@@ -120,6 +120,31 @@ export function cursorCodexToWireModelId(modelId: string): string {
|
|
|
120
120
|
return cursorWireModelSelection(modelId).modelId;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Cursor-native wire models keep server-side conversation state reliably.
|
|
125
|
+
* External models (gpt/claude/gemini/grok families and similar) are more brittle on resumeAction.
|
|
126
|
+
*/
|
|
127
|
+
export function isCursorNativeWireModel(modelId: string): boolean {
|
|
128
|
+
const wire = cursorCodexToWireModelId(modelId).trim().toLowerCase();
|
|
129
|
+
const bare = stripCursorEffortSuffix(wire);
|
|
130
|
+
if (bare === CURSOR_AUTO_WIRE_MODEL_ID || bare === CURSOR_AUTO_MODEL_ID) return true;
|
|
131
|
+
return bare.startsWith("composer-");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Inverse of {@link isCursorNativeWireModel}. */
|
|
135
|
+
export function isCursorExternalWireModel(modelId: string): boolean {
|
|
136
|
+
return !isCursorNativeWireModel(modelId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stripCursorEffortSuffix(wireModelId: string): string {
|
|
140
|
+
const suffixes = [...CANONICAL_EFFORT_SUFFIXES].sort((a, b) => b.length - a.length);
|
|
141
|
+
for (const suffix of suffixes) {
|
|
142
|
+
const marker = `-${suffix}`;
|
|
143
|
+
if (wireModelId.endsWith(marker)) return wireModelId.slice(0, -marker.length);
|
|
144
|
+
}
|
|
145
|
+
return wireModelId;
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
export function isCursorRouterModelId(modelId: string): boolean {
|
|
124
149
|
return (CURSOR_ROUTER_MODEL_IDS as readonly string[]).includes(modelId);
|
|
125
150
|
}
|
|
@@ -70,6 +70,11 @@ const GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS = 1_800;
|
|
|
70
70
|
const GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS = 125;
|
|
71
71
|
const cursorContextUsageTracker = createCursorContextUsageTracker();
|
|
72
72
|
|
|
73
|
+
/** Carry context-usage totals across conversation-id rotation for external-model replay. */
|
|
74
|
+
export function rekeyCursorContextUsage(fromConversationId: string, toConversationId: string): void {
|
|
75
|
+
cursorContextUsageTracker.rekey(fromConversationId, toConversationId);
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
export class CursorMissingCredentialError extends Error {
|
|
74
79
|
readonly code = "cursor_missing_credential";
|
|
75
80
|
|
|
@@ -22,6 +22,8 @@ export interface CursorContextUsageTracker {
|
|
|
22
22
|
controlsForConversation(conversationId: string, options?: { clearPrior?: boolean; storeCheckpoints?: boolean }): CursorContextUsageControls;
|
|
23
23
|
get(conversationId: string): number | undefined;
|
|
24
24
|
record(conversationId: string, tokens: number): void;
|
|
25
|
+
/** Copy numeric carry-forward totals when a conversation id is rotated for replay. */
|
|
26
|
+
rekey(fromConversationId: string, toConversationId: string): void;
|
|
25
27
|
clear(conversationId: string): void;
|
|
26
28
|
clearAll(): void;
|
|
27
29
|
}
|
|
@@ -91,6 +93,18 @@ export function createCursorContextUsageTracker(options: { maxEntries?: number;
|
|
|
91
93
|
},
|
|
92
94
|
get,
|
|
93
95
|
record,
|
|
96
|
+
rekey(fromConversationId, toConversationId) {
|
|
97
|
+
if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return;
|
|
98
|
+
prune();
|
|
99
|
+
const from = entries.get(fromConversationId);
|
|
100
|
+
if (!from) return;
|
|
101
|
+
const to = entries.get(toConversationId);
|
|
102
|
+
const tokens = Math.max(from.tokens, to?.tokens ?? 0);
|
|
103
|
+
entries.delete(fromConversationId);
|
|
104
|
+
entries.delete(toConversationId);
|
|
105
|
+
entries.set(toConversationId, { tokens, updatedAt: now() });
|
|
106
|
+
prune();
|
|
107
|
+
},
|
|
94
108
|
clear(conversationId) {
|
|
95
109
|
entries.delete(conversationId);
|
|
96
110
|
},
|
|
@@ -4,6 +4,8 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
|
4
4
|
import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
|
|
5
5
|
import { namespacedToolName } from "../../types";
|
|
6
6
|
import type { CursorRunRequest } from "./types";
|
|
7
|
+
import { isCursorExternalWireModel } from "./discovery";
|
|
8
|
+
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
7
9
|
import { storeCursorBlob } from "./native-exec";
|
|
8
10
|
import {
|
|
9
11
|
AgentClientMessageSchema,
|
|
@@ -326,8 +328,9 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
|
|
|
326
328
|
// would pollute the model input and double-deliver the result. Use ResumeAction so Cursor picks up
|
|
327
329
|
// from the history we provided.
|
|
328
330
|
const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
|
|
331
|
+
const actionCase = !lastRawIsToolResult && text.trim().length > 0 ? "userMessageAction" : "resumeAction";
|
|
329
332
|
const action = create(ConversationActionSchema, {
|
|
330
|
-
action:
|
|
333
|
+
action: actionCase === "userMessageAction"
|
|
331
334
|
? {
|
|
332
335
|
case: "userMessageAction",
|
|
333
336
|
value: create(UserMessageActionSchema, {
|
|
@@ -345,6 +348,13 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
|
|
|
345
348
|
}),
|
|
346
349
|
},
|
|
347
350
|
});
|
|
351
|
+
debugProviderDiagnostic("cursor", "run-request", {
|
|
352
|
+
wireModel: request.modelId,
|
|
353
|
+
action: actionCase,
|
|
354
|
+
conversationId: request.conversationId,
|
|
355
|
+
turnType: lastRawIsToolResult ? "tool-continuation" : "initial",
|
|
356
|
+
externalModel: isCursorExternalWireModel(request.modelId),
|
|
357
|
+
});
|
|
348
358
|
|
|
349
359
|
const runRequest = create(AgentRunRequestSchema, {
|
|
350
360
|
conversationId: request.conversationId,
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "../../types";
|
|
9
9
|
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
|
|
10
10
|
import type { CursorRequestMessage, CursorRunRequest } from "./types";
|
|
11
|
-
import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
|
|
11
|
+
import { cursorWireModelSelection, isCursorExternalWireModel, type CursorRoutingLevel } from "./discovery";
|
|
12
12
|
import { cursorEffortSuffix } from "./effort-map";
|
|
13
13
|
import {
|
|
14
14
|
cursorMcpToolEncodedSize,
|
|
@@ -159,7 +159,15 @@ export function generatedCursorConversationId(): string {
|
|
|
159
159
|
return `cursor_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export
|
|
162
|
+
export interface CreateCursorRequestOptions {
|
|
163
|
+
/** Force a brand-new Cursor conversation id even when remembered state exists. */
|
|
164
|
+
forceFreshConversation?: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createCursorRequest(
|
|
168
|
+
parsed: OcxParsedRequest,
|
|
169
|
+
options: CreateCursorRequestOptions = {},
|
|
170
|
+
): CursorRunRequest {
|
|
163
171
|
const messages = parsed.context.messages
|
|
164
172
|
.map(requestMessage)
|
|
165
173
|
.filter((message): message is CursorRequestMessage => !!message && message.content.length > 0);
|
|
@@ -168,6 +176,13 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
|
|
|
168
176
|
const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
|
|
169
177
|
const limitNote = catalogLimitNote(budget.tools, budget.omitted);
|
|
170
178
|
const model = normalizeCursorModelId(parsed.modelId, parsed.options.reasoning);
|
|
179
|
+
const lastRaw = parsed.context.messages.at(-1);
|
|
180
|
+
// External Cursor models (e.g. gpt-5.6-sol) can corrupt server-side conversation state across
|
|
181
|
+
// tool-result continuations when ResumeAction reuses the same conversationId. Force a fresh id
|
|
182
|
+
// so the full history is replayed without depending on that state.
|
|
183
|
+
const forceFreshConversation =
|
|
184
|
+
options.forceFreshConversation === true
|
|
185
|
+
|| (lastRaw?.role === "toolResult" && isCursorExternalWireModel(model.modelId));
|
|
171
186
|
return {
|
|
172
187
|
modelId: model.modelId,
|
|
173
188
|
...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
|
|
@@ -175,7 +190,9 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
|
|
|
175
190
|
// back to the OpenAI Responses previous_response_id (resp_*): that is a Responses-chain id in a
|
|
176
191
|
// different namespace and would start an unrelated Cursor conversation, breaking tool-result
|
|
177
192
|
// continuation. If we have no remembered Cursor conversation, start a fresh one.
|
|
178
|
-
conversationId:
|
|
193
|
+
conversationId: forceFreshConversation
|
|
194
|
+
? generatedCursorConversationId()
|
|
195
|
+
: (parsed._cursorConversationId ?? generatedCursorConversationId()),
|
|
179
196
|
system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
|
|
180
197
|
messages,
|
|
181
198
|
rawMessages: parsed.context.messages,
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxProviderConfig } from "../types";
|
|
2
2
|
import type { ProviderAdapter } from "./base";
|
|
3
3
|
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
|
|
4
|
-
import { isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor/cursor-errors";
|
|
4
|
+
import { isCursorBenignCancelError, isCursorInvalidArgumentError, safeCursorErrorMessage } from "./cursor/cursor-errors";
|
|
5
|
+
import { isCursorExternalWireModel } from "./cursor/discovery";
|
|
5
6
|
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
6
7
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
7
8
|
import { createCursorRequest, generatedCursorConversationId } from "./cursor/request-builder";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createLiveCursorTransport,
|
|
11
|
+
CursorMissingCredentialError,
|
|
12
|
+
rekeyCursorContextUsage,
|
|
13
|
+
} from "./cursor/live-transport";
|
|
9
14
|
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
10
15
|
import {
|
|
11
16
|
createDisabledCursorTransport,
|
|
@@ -70,26 +75,68 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
70
75
|
const makeTransport = deps.createTransport ?? createLiveCursorTransport;
|
|
71
76
|
const kv = deps.kv ?? createCursorKvStore();
|
|
72
77
|
_parsed._cursorConversationId ??= generatedCursorConversationId();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
const previousConversationId = _parsed._cursorConversationId;
|
|
79
|
+
let request = createCursorRequest(_parsed);
|
|
80
|
+
// Keep remembered conversation id in sync when the request builder mints a fresh id
|
|
81
|
+
// for external-model tool-result continuations (stateless replay).
|
|
82
|
+
if (request.conversationId !== previousConversationId) {
|
|
83
|
+
rekeyCursorContextUsage(previousConversationId, request.conversationId);
|
|
84
|
+
}
|
|
85
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
86
|
+
let emittedOutput = false;
|
|
87
|
+
const lastRawIsToolResult = _parsed.context.messages.at(-1)?.role === "toolResult";
|
|
88
|
+
|
|
89
|
+
const runOnce = async (activeRequest: ReturnType<typeof createCursorRequest>) => {
|
|
90
|
+
await runCursorTurnWithRetry(
|
|
91
|
+
makeTransport,
|
|
92
|
+
{
|
|
93
|
+
provider,
|
|
94
|
+
headers: incoming.headers,
|
|
95
|
+
requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(activeRequest),
|
|
96
|
+
},
|
|
97
|
+
activeRequest,
|
|
98
|
+
incoming.abortSignal,
|
|
99
|
+
(message, activeTransport) => {
|
|
100
|
+
if (incoming.abortSignal?.aborted) {
|
|
101
|
+
emit({ type: "error", message: "Cursor turn was aborted." });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const events = mapCursorServerMessage(message, {
|
|
105
|
+
kv,
|
|
106
|
+
writeClient: clientMessage => {
|
|
107
|
+
void activeTransport.writeClient(clientMessage);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
for (const event of events) {
|
|
111
|
+
if (event.type !== "heartbeat") emittedOutput = true;
|
|
112
|
+
emit(event);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await runOnce(request);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// One-shot fallback: only for external-model tool-result continuations that fail
|
|
122
|
+
// with Connect invalid_argument before any non-heartbeat output was forwarded.
|
|
123
|
+
// Replaying after text/tool events would duplicate output.
|
|
124
|
+
if (
|
|
125
|
+
!isCursorInvalidArgumentError(err)
|
|
126
|
+
|| !isCursorExternalWireModel(request.modelId)
|
|
127
|
+
|| !lastRawIsToolResult
|
|
128
|
+
|| emittedOutput
|
|
129
|
+
|| incoming.abortSignal?.aborted
|
|
130
|
+
) {
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
const failedConversationId = request.conversationId;
|
|
134
|
+
_parsed._cursorConversationId = undefined;
|
|
135
|
+
request = createCursorRequest(_parsed, { forceFreshConversation: true });
|
|
136
|
+
rekeyCursorContextUsage(failedConversationId, request.conversationId);
|
|
137
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
138
|
+
await runOnce(request);
|
|
139
|
+
}
|
|
93
140
|
} catch (err) {
|
|
94
141
|
if (isCursorBenignCancelError(err)) return;
|
|
95
142
|
const partialUsage = (err as { partialUsage?: import("../types").OcxUsage }).partialUsage;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
2
2
|
import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
|
|
3
|
+
import { repairGoogleInvalidRequestBody } from "./google-wire-compiler";
|
|
3
4
|
import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
|
|
4
5
|
import {
|
|
5
6
|
abortError,
|
|
@@ -29,14 +30,32 @@ async function normalizeFinalGoogleError(label: string, res: Response, signal?:
|
|
|
29
30
|
export async function fetchGoogleWithRetry(label: string, request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
30
31
|
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
31
32
|
let lastError: unknown;
|
|
33
|
+
let activeRequest = request;
|
|
34
|
+
let compatibilityReplayUsed = false;
|
|
32
35
|
for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
|
|
33
36
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
34
37
|
try {
|
|
35
|
-
const res = await fetchWithAttemptDeadline(
|
|
36
|
-
method:
|
|
37
|
-
headers:
|
|
38
|
-
body:
|
|
38
|
+
const res = await fetchWithAttemptDeadline(activeRequest.url, {
|
|
39
|
+
method: activeRequest.method,
|
|
40
|
+
headers: activeRequest.headers,
|
|
41
|
+
body: activeRequest.body,
|
|
39
42
|
}, timeoutMs, ctx.abortSignal, ctx.stream);
|
|
43
|
+
if (res.status === 400 && !compatibilityReplayUsed) {
|
|
44
|
+
let payloadText = "";
|
|
45
|
+
try {
|
|
46
|
+
payloadText = await readDisplaySafeErrorPayloadText(res.clone(), ctx.abortSignal);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (ctx.abortSignal?.aborted) throw error;
|
|
49
|
+
}
|
|
50
|
+
const repairedBody = repairGoogleInvalidRequestBody(activeRequest.body, payloadText);
|
|
51
|
+
if (repairedBody !== undefined) {
|
|
52
|
+
compatibilityReplayUsed = true;
|
|
53
|
+
activeRequest = { ...activeRequest, body: repairedBody };
|
|
54
|
+
cancelResponseBodyBestEffort(res);
|
|
55
|
+
attempt--; // The changed-request replay is separate from transient retry accounting.
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
40
59
|
if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
|
|
41
60
|
return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
42
61
|
}
|