@bitkyc08/opencodex 2.13.0 → 2.14.0
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/gui/dist/assets/index-Co12XTT-.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +5 -1
- package/src/adapters/cursor/request-builder.ts +3 -3
- package/src/adapters/google.ts +25 -5
- package/src/adapters/openai-chat.ts +182 -6
- package/src/adapters/openai-responses.ts +17 -7
- package/src/codex/catalog/bundled.ts +16 -0
- package/src/codex/catalog/metadata.ts +180 -5
- package/src/codex/catalog/parsing.ts +7 -6
- package/src/codex/catalog/sync.ts +73 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +20 -0
- package/src/codex/prompt-journal.ts +50 -13
- package/src/codex/prompt-layers.ts +1 -1
- package/src/config.ts +57 -0
- package/src/generated/compatibility-version.json +64 -48
- package/src/generated/model-metadata.ts +3 -3
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/oauth/login-cli.ts +52 -26
- package/src/providers/derive.ts +2 -0
- package/src/providers/openai-sidecar.ts +9 -2
- package/src/providers/quota.ts +57 -0
- package/src/providers/registry.ts +53 -25
- package/src/responses/state.ts +22 -0
- package/src/router.ts +1 -0
- package/src/server/claude-messages.ts +57 -11
- package/src/server/direct-local-http.ts +7 -3
- package/src/server/images.ts +6 -0
- package/src/server/index.ts +51 -11
- package/src/server/live.ts +117 -13
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/config-routes.ts +20 -3
- package/src/server/management/logs-usage-routes.ts +28 -0
- package/src/server/management/model-routes.ts +11 -3
- package/src/server/management/model-rows.ts +18 -3
- package/src/server/management/provider-routes.ts +107 -3
- package/src/server/management-auth.ts +65 -1
- package/src/server/proxy-liveness.ts +1 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +460 -0
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core.ts +142 -6
- package/src/server/responses/encrypted-payload.ts +4 -1
- package/src/server/search.ts +4 -0
- package/src/types.ts +27 -0
- package/src/usage/expected-prices.ts +11 -0
- package/src/vision/describe.ts +4 -0
- package/src/web-search/anthropic-executor.ts +5 -1
- package/src/web-search/executor.ts +9 -1
- package/src/web-search/index.ts +5 -0
- package/src/web-search/loop.ts +42 -5
- package/gui/dist/assets/index-BHldBl6_.js +0 -76
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-Co12XTT-.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-BNVYzdn0.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ export function inferCursorContextWindow(modelId: string): number {
|
|
|
29
29
|
if (id === "glm-5.2") return CONTEXT_1M;
|
|
30
30
|
if (id.startsWith("gpt-5.6-")) return CONTEXT_1M;
|
|
31
31
|
if (id.startsWith("gpt-5") || id === "gpt-5-codex") return CONTEXT_272K;
|
|
32
|
-
if (id.startsWith("grok-4.5")) return 500_000;
|
|
32
|
+
if (id.startsWith("grok-4.5") || id.startsWith("grok-4.6")) return 500_000;
|
|
33
33
|
if (id.startsWith("grok-")) return CONTEXT_256K;
|
|
34
34
|
if (id.includes("claude")) return CONTEXT_200K;
|
|
35
35
|
return CURSOR_DEFAULT_CONTEXT_WINDOW;
|
|
@@ -240,6 +240,9 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
240
240
|
|
|
241
241
|
{ id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
242
242
|
{ id: "grok-4.5-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
243
|
+
// 260813 preemptive: grok-4.6 seeded ahead of Cursor's lineup update (mirrors grok-4.5).
|
|
244
|
+
{ id: "grok-4.6", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
245
|
+
{ id: "grok-4.6-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
243
246
|
]);
|
|
244
247
|
|
|
245
248
|
export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
|
|
@@ -35,6 +35,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
35
35
|
// cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found.
|
|
36
36
|
"grok-4.5": ["low", "medium", "high"],
|
|
37
37
|
"grok-4.5-fast": ["low", "medium", "high"],
|
|
38
|
+
// 260813 preemptive: grok-4.6 tiers mirrored from grok-4.5 ahead of Cursor's lineup update,
|
|
39
|
+
// so the suffix/wire handling is already correct the day the slugs appear.
|
|
40
|
+
"grok-4.6": ["low", "medium", "high"],
|
|
41
|
+
"grok-4.6-fast": ["low", "medium", "high"],
|
|
38
42
|
"gpt-5.1": ["low", "high"],
|
|
39
43
|
"gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"],
|
|
40
44
|
"gpt-5.1-codex-mini": ["low", "high"],
|
|
@@ -135,5 +139,5 @@ export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: s
|
|
|
135
139
|
*/
|
|
136
140
|
export function cursorRequestWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
|
|
137
141
|
const flattened = cursorWireModelIdWithEffort(baseModelId, effortSuffix);
|
|
138
|
-
return baseModelId === "grok-4.5" ? `cursor-${flattened}` : flattened;
|
|
142
|
+
return baseModelId === "grok-4.5" || baseModelId === "grok-4.6" ? `cursor-${flattened}` : flattened;
|
|
139
143
|
}
|
|
@@ -129,7 +129,7 @@ function catalogLimitNote(kept: readonly OcxTool[], omitted: readonly OcxTool[])
|
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Resolve a `cursor/<model>` selection + Codex reasoning effort to Cursor's requested model shape.
|
|
132
|
-
* Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok
|
|
132
|
+
* Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok Fast is parameterized
|
|
133
133
|
* instead: current Cursor clients send the `grok-4.5` base id plus `effort` and `fast` parameters.
|
|
134
134
|
* A fully-qualified id (one that is not a known effort base) passes through unchanged.
|
|
135
135
|
*/
|
|
@@ -141,10 +141,10 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
141
141
|
const selection = cursorWireModelSelection(modelId);
|
|
142
142
|
const id = selection.modelId;
|
|
143
143
|
const suffix = cursorEffortSuffix(id, reasoning);
|
|
144
|
-
if (id === "grok-4.5-fast" && suffix) {
|
|
144
|
+
if ((id === "grok-4.5-fast" || id === "grok-4.6-fast") && suffix) {
|
|
145
145
|
return {
|
|
146
146
|
...selection,
|
|
147
|
-
modelId: "
|
|
147
|
+
modelId: id.slice(0, -"-fast".length),
|
|
148
148
|
requestedModelParameters: [
|
|
149
149
|
{ id: "effort", value: suffix },
|
|
150
150
|
{ id: "fast", value: "true" },
|
package/src/adapters/google.ts
CHANGED
|
@@ -304,6 +304,24 @@ function artifactMarkdownUrl(filePath: string): string {
|
|
|
304
304
|
return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
interface GoogleResponsePart {
|
|
308
|
+
text?: string;
|
|
309
|
+
thought?: boolean;
|
|
310
|
+
functionCall?: { name: string; args: unknown };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Google marks model-internal reasoning as a normal text-bearing part plus `thought: true`.
|
|
315
|
+
* Keep that provider visibility bit authoritative here so the streaming and buffered parsers
|
|
316
|
+
* cannot accidentally expose the same hidden reasoning through different event types.
|
|
317
|
+
*/
|
|
318
|
+
function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
|
|
319
|
+
if (!part.text) return undefined;
|
|
320
|
+
return part.thought === true
|
|
321
|
+
? { type: "reasoning_raw_delta", text: part.text }
|
|
322
|
+
: { type: "text_delta", text: part.text };
|
|
323
|
+
}
|
|
324
|
+
|
|
307
325
|
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
308
326
|
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
309
327
|
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
@@ -602,7 +620,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
602
620
|
sawTerminalSignal = true;
|
|
603
621
|
}
|
|
604
622
|
|
|
605
|
-
const parts = candidate.content?.parts as
|
|
623
|
+
const parts = candidate.content?.parts as GoogleResponsePart[] | undefined;
|
|
606
624
|
// Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
|
|
607
625
|
// Antigravity use separate model namespaces so opaque provider state cannot cross routes.
|
|
608
626
|
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
@@ -613,9 +631,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
613
631
|
}
|
|
614
632
|
if (parts) {
|
|
615
633
|
for (const part of parts) {
|
|
616
|
-
|
|
634
|
+
const textEvent = googlePartTextEvent(part);
|
|
635
|
+
if (textEvent) {
|
|
617
636
|
emittedContentEvent = true;
|
|
618
|
-
yield
|
|
637
|
+
yield textEvent;
|
|
619
638
|
}
|
|
620
639
|
const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
|
|
621
640
|
if (inline && typeof inline.data === "string") {
|
|
@@ -817,7 +836,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
817
836
|
}
|
|
818
837
|
const events: AdapterEvent[] = [];
|
|
819
838
|
|
|
820
|
-
const candidates = json.candidates as { content?: { parts?:
|
|
839
|
+
const candidates = json.candidates as { content?: { parts?: GoogleResponsePart[] }; finishReason?: string }[] | undefined;
|
|
821
840
|
if (!candidates?.length) {
|
|
822
841
|
return finish([{ type: "error", message: "google response contained no candidates" }]);
|
|
823
842
|
}
|
|
@@ -833,7 +852,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
833
852
|
observeAntigravityReplay(replayModel, replaySession, candidates[0].content.parts as unknown[]);
|
|
834
853
|
}
|
|
835
854
|
for (const part of candidates[0].content.parts) {
|
|
836
|
-
|
|
855
|
+
const textEvent = googlePartTextEvent(part);
|
|
856
|
+
if (textEvent) events.push(textEvent);
|
|
837
857
|
const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
|
|
838
858
|
if (inline && typeof inline.data === "string") {
|
|
839
859
|
if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
|
|
@@ -178,10 +178,125 @@ function invalidToolCallsEvent(usage?: OcxUsage): Extract<AdapterEvent, { type:
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* A streamed tool call is only dispatchable once the upstream has named the function.
|
|
183
|
+
*
|
|
184
|
+
* The OpenAI streaming convention puts `function.name` in the first chunk for a tool-call
|
|
185
|
+
* index and leaves later chunks carrying only `arguments` deltas, so a stream that never
|
|
186
|
+
* sends a name is non-conforming for every provider rather than quirky for one. The
|
|
187
|
+
* reference implementations accumulate such a call with an empty name and let the caller
|
|
188
|
+
* fail; we sit at the boundary where it would become a Codex tool-call contract event, so
|
|
189
|
+
* the equivalent is to refuse to emit it.
|
|
190
|
+
*
|
|
191
|
+
* Failing closed rather than dropping is deliberate, and matches #1325: a claimed tool call
|
|
192
|
+
* that silently disappears can leave the matching result orphaned on the next turn. Naming
|
|
193
|
+
* it ourselves is worse still — the id is synthesizable because it is an opaque correlation
|
|
194
|
+
* handle, but a function name is a guess at intent.
|
|
195
|
+
*/
|
|
196
|
+
function unnamedToolCallEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
197
|
+
return {
|
|
198
|
+
type: "error",
|
|
199
|
+
message: "upstream streamed a tool call without a function name — cannot dispatch",
|
|
200
|
+
...(usage !== undefined ? { usage } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
181
204
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
182
205
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
183
206
|
}
|
|
184
207
|
|
|
208
|
+
type InvalidToolCallReason =
|
|
209
|
+
| "tool_calls_not_array"
|
|
210
|
+
| "tool_call_not_object"
|
|
211
|
+
| "tool_call_id_invalid"
|
|
212
|
+
| "tool_call_function_not_object"
|
|
213
|
+
| "tool_call_function_name_invalid"
|
|
214
|
+
| "tool_call_function_name_blank"
|
|
215
|
+
| "tool_call_function_arguments_invalid";
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Explain only the rejected wire shape, never its values. This diagnostic exists so provider
|
|
219
|
+
* compatibility can be tightened from evidence without retaining tool arguments or credentials.
|
|
220
|
+
*/
|
|
221
|
+
function diagnoseInvalidToolCalls(
|
|
222
|
+
rawToolCalls: unknown,
|
|
223
|
+
mode: "stream" | "response",
|
|
224
|
+
): { reason: InvalidToolCallReason; callIndex?: number; valueType: string } | undefined {
|
|
225
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
226
|
+
return { reason: "tool_calls_not_array", valueType: rawToolCalls === null ? "null" : typeof rawToolCalls };
|
|
227
|
+
}
|
|
228
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
229
|
+
const rawToolCall = rawToolCalls[callIndex];
|
|
230
|
+
if (!isRecord(rawToolCall)) {
|
|
231
|
+
return {
|
|
232
|
+
reason: "tool_call_not_object",
|
|
233
|
+
callIndex,
|
|
234
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (mode === "stream") {
|
|
238
|
+
// The streamed path validates the pieces it is about to store (#1531): a present
|
|
239
|
+
// `function` must be a record, and a present `name`/`arguments`/`id` must be a string.
|
|
240
|
+
// Blank names are caught later at flush, not here, so they are not diagnosed on this
|
|
241
|
+
// branch. Describe exactly that boundary rather than tightening compatibility in a
|
|
242
|
+
// diagnostic change.
|
|
243
|
+
const streamFunction = (rawToolCall as { function?: unknown }).function;
|
|
244
|
+
if (streamFunction !== undefined && streamFunction !== null) {
|
|
245
|
+
if (!isRecord(streamFunction)) {
|
|
246
|
+
return {
|
|
247
|
+
reason: "tool_call_function_not_object",
|
|
248
|
+
callIndex,
|
|
249
|
+
valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (streamFunction.name !== undefined && typeof streamFunction.name !== "string") {
|
|
253
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
|
|
254
|
+
}
|
|
255
|
+
if (streamFunction.arguments !== undefined && typeof streamFunction.arguments !== "string") {
|
|
256
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (rawToolCall.id !== undefined && typeof rawToolCall.id !== "string") {
|
|
260
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Precedence must mirror the buffered validator below, or a payload with more than one
|
|
265
|
+
// problem is reported under the wrong reason and sends compatibility work after the wrong
|
|
266
|
+
// shape. That validator checks the `function` container first (`!isRecord(rawToolCall) ||
|
|
267
|
+
// !isRecord(rawToolCall.function)`), then id/name/arguments types together, and only then
|
|
268
|
+
// the blank name.
|
|
269
|
+
if (!isRecord(rawToolCall.function)) {
|
|
270
|
+
return {
|
|
271
|
+
reason: "tool_call_function_not_object",
|
|
272
|
+
callIndex,
|
|
273
|
+
valueType: rawToolCall.function === null ? "null" : Array.isArray(rawToolCall.function) ? "array" : typeof rawToolCall.function,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (typeof rawToolCall.id !== "string") {
|
|
277
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
278
|
+
}
|
|
279
|
+
if (typeof rawToolCall.function.name !== "string") {
|
|
280
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawToolCall.function.name };
|
|
281
|
+
}
|
|
282
|
+
if (typeof rawToolCall.function.arguments !== "string") {
|
|
283
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawToolCall.function.arguments };
|
|
284
|
+
}
|
|
285
|
+
// Last, matching the validator: #1531 also rejects a blank or whitespace-only name here,
|
|
286
|
+
// because such a call cannot select a dispatch target. Reporting it as `name_invalid`
|
|
287
|
+
// would claim a type problem for a correctly-typed value, so it gets its own code.
|
|
288
|
+
if (rawToolCall.function.name.trim().length === 0) {
|
|
289
|
+
return { reason: "tool_call_function_name_blank", callIndex, valueType: "string" };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function logInvalidToolCalls(mode: "stream" | "response", rawToolCalls: unknown): void {
|
|
296
|
+
const diagnostic = diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
297
|
+
if (diagnostic) debugProviderDiagnostic("openai-chat", "invalid-tool-calls", { mode, ...diagnostic });
|
|
298
|
+
}
|
|
299
|
+
|
|
185
300
|
function developerSystemText(message: OcxMessage): string | undefined {
|
|
186
301
|
if (message.role !== "developer") return undefined;
|
|
187
302
|
if (typeof message.content === "string") return message.content;
|
|
@@ -721,6 +836,18 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
721
836
|
messages,
|
|
722
837
|
stream: parsed.stream,
|
|
723
838
|
};
|
|
839
|
+
// Preserve a caller-selected service tier for OpenAI-compatible chat gateways. The
|
|
840
|
+
// request pipeline deliberately does not inject fast mode for this adapter, but dropping
|
|
841
|
+
// an explicit value here makes the Responses parser's serviceTier projection ineffective.
|
|
842
|
+
//
|
|
843
|
+
// Opt-in, like `prompt_cache_key` directly below: `service_tier` is an OpenAI-specific
|
|
844
|
+
// extension and 66 registry providers share this adapter, several of which reject
|
|
845
|
+
// unknown body fields. Forwarding unconditionally would turn a caller-supplied
|
|
846
|
+
// `service_tier` into an upstream 400 on those routes. `supportsServiceTier` is the
|
|
847
|
+
// Responses-wire flag (applyServiceTierGate) and deliberately does not gate this path.
|
|
848
|
+
if (provider.chatServiceTier && parsed.options.serviceTier !== undefined) {
|
|
849
|
+
body.service_tier = parsed.options.serviceTier;
|
|
850
|
+
}
|
|
724
851
|
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
|
|
725
852
|
const maxTokens = resolveMaxTokens(provider, parsed);
|
|
726
853
|
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
|
|
@@ -897,13 +1024,27 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
897
1024
|
pendingToolCalls.length = 0;
|
|
898
1025
|
return calls;
|
|
899
1026
|
};
|
|
900
|
-
|
|
1027
|
+
// Returns "terminate" when a pending call cannot be dispatched, so every flush site
|
|
1028
|
+
// stops the turn instead of emitting an unusable call. `closeToolCalls()` runs first,
|
|
1029
|
+
// so budget reservations are released for every pending call even on the early return.
|
|
1030
|
+
const flushToolCalls = function* (): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
901
1031
|
for (const call of closeToolCalls()) {
|
|
1032
|
+
// Ingest already proved `name` is a string; the typeof guard keeps this branch
|
|
1033
|
+
// total so a future ingest change cannot turn a malformed name into a throw.
|
|
1034
|
+
if (typeof call.name !== "string" || call.name.trim().length === 0) {
|
|
1035
|
+
debugProviderDiagnostic("openai-chat", "tool-call-unnamed", {
|
|
1036
|
+
hadId: call.id.length > 0,
|
|
1037
|
+
argsBytes: call.argsBytes,
|
|
1038
|
+
});
|
|
1039
|
+
yield unnamedToolCallEvent(pendingUsage);
|
|
1040
|
+
return "terminate";
|
|
1041
|
+
}
|
|
902
1042
|
if (!call.id) call.id = `call_${++toolCallSeq}`;
|
|
903
1043
|
yield { type: "tool_call_start", id: call.id, name: call.name };
|
|
904
1044
|
if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
|
|
905
1045
|
yield { type: "tool_call_end" };
|
|
906
1046
|
}
|
|
1047
|
+
return "continue";
|
|
907
1048
|
};
|
|
908
1049
|
const terminateWithError = function* (
|
|
909
1050
|
event: Extract<AdapterEvent, { type: "error" }>,
|
|
@@ -922,7 +1063,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
922
1063
|
const payload = rawPayload.trim();
|
|
923
1064
|
if (payload.length === 0) return "continue";
|
|
924
1065
|
if (payload === "[DONE]") {
|
|
925
|
-
yield* flushToolCalls();
|
|
1066
|
+
if ((yield* flushToolCalls()) === "terminate") return "terminate";
|
|
926
1067
|
const stopReason = stopReasonFor(finishReason);
|
|
927
1068
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
928
1069
|
return "terminate";
|
|
@@ -981,10 +1122,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
981
1122
|
// through the adapter error channel instead of escaping as TypeError (#1325). Null is
|
|
982
1123
|
// tolerated as absent because OpenAI-compatible providers may emit it as stream padding.
|
|
983
1124
|
if (!Array.isArray(rawToolCalls)) {
|
|
1125
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
984
1126
|
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
985
1127
|
}
|
|
986
1128
|
for (const rawToolCall of rawToolCalls) {
|
|
987
1129
|
if (!isRecord(rawToolCall)) {
|
|
1130
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
988
1131
|
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
989
1132
|
}
|
|
990
1133
|
const tc = rawToolCall as {
|
|
@@ -992,6 +1135,28 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
992
1135
|
id?: string;
|
|
993
1136
|
function?: { name?: string; arguments?: string };
|
|
994
1137
|
};
|
|
1138
|
+
// That cast is a TypeScript convenience, not a runtime guarantee: this is
|
|
1139
|
+
// upstream JSON. Validate the fields before they are stored, so a non-string
|
|
1140
|
+
// name or arguments value fails closed through the #1325 channel here rather
|
|
1141
|
+
// than escaping later as a TypeError from string handling at flush time.
|
|
1142
|
+
const rawFunction = (rawToolCall as { function?: unknown }).function;
|
|
1143
|
+
if (rawFunction !== undefined && rawFunction !== null) {
|
|
1144
|
+
if (!isRecord(rawFunction)) {
|
|
1145
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
1146
|
+
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
1147
|
+
}
|
|
1148
|
+
const rawName = rawFunction.name;
|
|
1149
|
+
const rawArguments = rawFunction.arguments;
|
|
1150
|
+
if ((rawName !== undefined && typeof rawName !== "string")
|
|
1151
|
+
|| (rawArguments !== undefined && typeof rawArguments !== "string")) {
|
|
1152
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
1153
|
+
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (tc.id !== undefined && typeof tc.id !== "string") {
|
|
1157
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
1158
|
+
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
1159
|
+
}
|
|
995
1160
|
const key = typeof tc.index === "number"
|
|
996
1161
|
? `i:${tc.index}`
|
|
997
1162
|
: tc.id
|
|
@@ -1025,7 +1190,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1025
1190
|
}
|
|
1026
1191
|
}
|
|
1027
1192
|
|
|
1028
|
-
if (typeof choice.finish_reason === "string" && choice.finish_reason)
|
|
1193
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) {
|
|
1194
|
+
if ((yield* flushToolCalls()) === "terminate") return "terminate";
|
|
1195
|
+
}
|
|
1029
1196
|
return "continue";
|
|
1030
1197
|
};
|
|
1031
1198
|
|
|
@@ -1085,7 +1252,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1085
1252
|
yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
|
|
1086
1253
|
return;
|
|
1087
1254
|
}
|
|
1088
|
-
yield* flushToolCalls();
|
|
1255
|
+
if ((yield* flushToolCalls()) === "terminate") return;
|
|
1089
1256
|
const stopReason = stopReasonFor(finishReason);
|
|
1090
1257
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
1091
1258
|
} catch (error) {
|
|
@@ -1148,15 +1315,24 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1148
1315
|
if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content });
|
|
1149
1316
|
const rawToolCalls = msg.tool_calls;
|
|
1150
1317
|
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
1151
|
-
if (!Array.isArray(rawToolCalls))
|
|
1318
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
1319
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
1320
|
+
return [invalidToolCallsEvent(usage)];
|
|
1321
|
+
}
|
|
1152
1322
|
for (const rawToolCall of rawToolCalls) {
|
|
1153
1323
|
if (!isRecord(rawToolCall) || !isRecord(rawToolCall.function)) {
|
|
1324
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
1154
1325
|
return [invalidToolCallsEvent(usage)];
|
|
1155
1326
|
}
|
|
1156
1327
|
const id = rawToolCall.id;
|
|
1157
1328
|
const name = rawToolCall.function.name;
|
|
1158
1329
|
const args = rawToolCall.function.arguments;
|
|
1159
|
-
|
|
1330
|
+
// A blank name is as undispatchable as a missing one, so it fails closed here
|
|
1331
|
+
// for the same reason the streamed path refuses it. Trimmed length, not `!name`:
|
|
1332
|
+
// a whitespace-only function name is not a legitimate tool-call shape either.
|
|
1333
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof args !== "string"
|
|
1334
|
+
|| name.trim().length === 0) {
|
|
1335
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
1160
1336
|
return [invalidToolCallsEvent(usage)];
|
|
1161
1337
|
}
|
|
1162
1338
|
events.push({ type: "tool_call_start", id, name });
|
|
@@ -6,7 +6,7 @@ import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../resp
|
|
|
6
6
|
import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
7
7
|
import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
|
|
8
8
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
9
|
-
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
9
|
+
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
10
10
|
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
11
11
|
import { modelRecordValue } from "../reasoning-effort";
|
|
12
12
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
@@ -1213,22 +1213,32 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1213
1213
|
let url: string;
|
|
1214
1214
|
|
|
1215
1215
|
if (provider.authMode === "forward") {
|
|
1216
|
+
const mayForwardCallerCredentials = isCanonicalOpenAiForwardProvider(provider);
|
|
1216
1217
|
// OAuth passthrough: ChatGPT backend path is `${baseUrl}/responses` (no /v1).
|
|
1217
|
-
|
|
1218
|
+
const baseUrl = mayForwardCallerCredentials
|
|
1219
|
+
? CODEX_FORWARD_BASE_URL
|
|
1220
|
+
: provider.baseUrl.replace(/\/+$/, "");
|
|
1221
|
+
url = `${baseUrl}/responses`;
|
|
1218
1222
|
if (provider.headers) Object.assign(headers, provider.headers); // static headers first…
|
|
1219
1223
|
const runtimeProvider = provider as {
|
|
1220
1224
|
_codexAccountOverride?: { accessToken: string; chatgptAccountId: string };
|
|
1221
1225
|
_codexAccountRequired?: boolean;
|
|
1222
1226
|
};
|
|
1223
|
-
if (
|
|
1227
|
+
if (
|
|
1228
|
+
mayForwardCallerCredentials
|
|
1229
|
+
&& runtimeProvider._codexAccountRequired
|
|
1230
|
+
&& !runtimeProvider._codexAccountOverride
|
|
1231
|
+
) {
|
|
1224
1232
|
throw new Error("Codex pool account auth is required but unavailable");
|
|
1225
1233
|
}
|
|
1226
|
-
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1234
|
+
if (mayForwardCallerCredentials) {
|
|
1235
|
+
for (const h of FORWARD_HEADERS) {
|
|
1236
|
+
const v = incoming?.headers.get(h);
|
|
1237
|
+
if (v) headers[h] = v; // …so forwarded auth always wins.
|
|
1238
|
+
}
|
|
1229
1239
|
}
|
|
1230
1240
|
const override = runtimeProvider._codexAccountOverride;
|
|
1231
|
-
if (override) {
|
|
1241
|
+
if (override && mayForwardCallerCredentials) {
|
|
1232
1242
|
headers["authorization"] = `Bearer ${override.accessToken}`;
|
|
1233
1243
|
headers["chatgpt-account-id"] = override.chatgptAccountId;
|
|
1234
1244
|
}
|
|
@@ -522,6 +522,22 @@ export function readCurrentCatalogOrCache(): RawCatalog | null {
|
|
|
522
522
|
return readCatalog(path) ?? readCatalog(activeCodexModelsCachePath());
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Read the user-owned Codex catalog surfaces without substituting the bundled catalog.
|
|
527
|
+
*
|
|
528
|
+
* The bundled catalog is intentionally the authority for static native metadata on the default
|
|
529
|
+
* path. Account-qualified discovery needs the opposite view: an exact model id that Codex has
|
|
530
|
+
* observed in the user's catalog/cache may be account-scoped even when this release does not know
|
|
531
|
+
* it statically yet.
|
|
532
|
+
*/
|
|
533
|
+
export function readCurrentCodexCatalog(): RawCatalog | null {
|
|
534
|
+
return readCatalog(readCodexCatalogPath());
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function readCurrentCodexModelsCache(): RawCatalog | null {
|
|
538
|
+
return readCatalog(activeCodexModelsCachePath());
|
|
539
|
+
}
|
|
540
|
+
|
|
525
541
|
export function loadCatalogTemplate(): RawEntry | null {
|
|
526
542
|
const catalogPath = readCodexCatalogPath();
|
|
527
543
|
const bundled = loadBundledCodexCatalog();
|