@bitkyc08/opencodex 2.10.2 → 2.11.0-preview.20260808
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 +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-B1P60C4o.js +70 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
|
@@ -3,10 +3,13 @@ import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
|
|
|
3
3
|
import { decodeCursorArgsMap } from "./arg-codec";
|
|
4
4
|
import { normalizeArgKeys } from "./arg-normalize";
|
|
5
5
|
import {
|
|
6
|
+
CODEX_APPLY_PATCH_TOOL,
|
|
7
|
+
CURSOR_MULTI_EDIT_TOOL,
|
|
6
8
|
cursorShellBridgeArgsValid,
|
|
7
9
|
cursorShellBridgeDropError,
|
|
8
10
|
defaultShellBridgeArgNormalizeSchema,
|
|
9
11
|
isCodexShellBridgeToolName,
|
|
12
|
+
isCursorStructuredEditToolName,
|
|
10
13
|
normalizeCursorWireName,
|
|
11
14
|
OCX_RESPONSES_TOOL_PROVIDER,
|
|
12
15
|
resolveShellBridgeAliasKey,
|
|
@@ -161,14 +164,41 @@ export interface CursorProtobufEventState {
|
|
|
161
164
|
toolSchemas?: Map<string, unknown>;
|
|
162
165
|
/** Cursor wire-name → original Responses/Codex tool name for this request. */
|
|
163
166
|
cursorToolNameMap?: Map<string, string>;
|
|
167
|
+
/**
|
|
168
|
+
* Bare names WE advertised as synthetic structured-edit tools on this request.
|
|
169
|
+
* See structuredEditCallIsOurs: conversion is gated on provenance, not on the name.
|
|
170
|
+
*/
|
|
171
|
+
syntheticStructuredEditToolNames?: ReadonlySet<string>;
|
|
164
172
|
translatorBudget?: TranslatorBudget;
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Did WE advertise this bare tool name as a synthetic structured-edit tool on this request?
|
|
178
|
+
*
|
|
179
|
+
* Provenance, not a name test. `edit_file` / `multi_edit` are ordinary names a client or MCP
|
|
180
|
+
* server may legitimately expose, and `cursorStructuredEditTools` already refuses to shadow one
|
|
181
|
+
* that exists. Converting on the name alone would undo that refusal at the other end of the
|
|
182
|
+
* request: the client's own call would be silently re-emitted as `apply_patch`, or dropped with
|
|
183
|
+
* an error naming a conversion the user never asked for.
|
|
184
|
+
*
|
|
185
|
+
* Absent set = we advertised nothing, so nothing converts. Fail-closed in the safe direction:
|
|
186
|
+
* an unconverted structured call is a visible, recoverable failure; a wrongly converted one
|
|
187
|
+
* edits a file.
|
|
188
|
+
*/
|
|
189
|
+
function structuredEditCallIsOurs(
|
|
190
|
+
advertised: ReadonlySet<string> | undefined,
|
|
191
|
+
toolName: string,
|
|
192
|
+
): boolean {
|
|
193
|
+
return advertised?.has(toolName) === true;
|
|
194
|
+
}
|
|
195
|
+
|
|
167
196
|
export function createCursorProtobufEventState(options: {
|
|
168
197
|
clientToolNames?: Iterable<string>;
|
|
169
198
|
parallelToolCalls?: boolean;
|
|
170
199
|
toolSchemas?: Map<string, unknown>;
|
|
171
200
|
cursorToolNameMap?: Map<string, string>;
|
|
201
|
+
syntheticStructuredEditToolNames?: Iterable<string>;
|
|
172
202
|
contextUsage?: CursorContextUsageControls;
|
|
173
203
|
/**
|
|
174
204
|
* Request-local input estimate derived from the payload actually sent. Used only
|
|
@@ -185,6 +215,9 @@ export function createCursorProtobufEventState(options: {
|
|
|
185
215
|
openToolCalls: new Map(),
|
|
186
216
|
completedToolCalls: new Set(),
|
|
187
217
|
...(options.clientToolNames ? { clientToolNames: new Set(options.clientToolNames) } : {}),
|
|
218
|
+
...(options.syntheticStructuredEditToolNames
|
|
219
|
+
? { syntheticStructuredEditToolNames: new Set(options.syntheticStructuredEditToolNames) }
|
|
220
|
+
: {}),
|
|
188
221
|
...(options.parallelToolCalls !== undefined ? { parallelToolCalls: options.parallelToolCalls } : {}),
|
|
189
222
|
startedClientToolCalls: 0,
|
|
190
223
|
...(options.toolSchemas ? { toolSchemas: options.toolSchemas } : {}),
|
|
@@ -311,6 +344,117 @@ function resolveCompletedArgs(buffered: string, args: McpArgs | undefined, state
|
|
|
311
344
|
return "";
|
|
312
345
|
}
|
|
313
346
|
|
|
347
|
+
const PATCH_BEGIN = "*** Begin Patch";
|
|
348
|
+
const PATCH_END = "*** End Patch";
|
|
349
|
+
|
|
350
|
+
function firstStringArg(args: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
|
351
|
+
for (const key of keys) {
|
|
352
|
+
const value = args[key];
|
|
353
|
+
if (typeof value === "string") return value;
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Split a replacement into patch lines, ignoring one trailing newline (line-based patch semantics). */
|
|
359
|
+
function patchLines(text: string): string[] {
|
|
360
|
+
const lines = text.split("\n");
|
|
361
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
362
|
+
return lines;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** One `@@` hunk replacing `oldString` with `newString`. */
|
|
366
|
+
function replacementHunk(oldString: string, newString: string): { hunk: string } | { error: string } {
|
|
367
|
+
if (oldString.length === 0) {
|
|
368
|
+
return {
|
|
369
|
+
error:
|
|
370
|
+
"structured edit requires a non-empty old_string to locate the replacement; for new files or insertions without existing text, call apply_patch with an `*** Add File` / context hunk or use the shell bridge",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const oldLines = patchLines(oldString);
|
|
374
|
+
const newLines = patchLines(newString);
|
|
375
|
+
// Line-based patch semantics cannot express an edit that only adds or removes the file's
|
|
376
|
+
// final newline, and an old/new pair that normalizes to the same lines is a silent no-op —
|
|
377
|
+
// reject it rather than emitting an empty hunk that apply_patch would drop.
|
|
378
|
+
if (oldLines.length === 0 && newLines.length === 0) {
|
|
379
|
+
return { error: "structured edit requires a non-empty old_string; an empty replacement is not a valid edit" };
|
|
380
|
+
}
|
|
381
|
+
if (oldLines.length === newLines.length && oldLines.every((line, i) => line === newLines[i])) {
|
|
382
|
+
return { error: "structured edit old_string and new_string are identical after line normalization; the replacement is a no-op and was dropped" };
|
|
383
|
+
}
|
|
384
|
+
const removed = oldLines.map(line => `-${line}`);
|
|
385
|
+
const added = newLines.map(line => `+${line}`);
|
|
386
|
+
return { hunk: ["@@", ...removed, ...added].join("\n") };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Convert a completed Cursor structured edit call (`edit_file` / `multi_edit`) into a valid Codex
|
|
391
|
+
* apply_patch freeform payload (#1017). Cursor-trained models cannot emit Codex's freeform patch
|
|
392
|
+
* grammar, so the adapter advertises exact-match replacement tools and performs the grammar here.
|
|
393
|
+
* Returns `{ patch }` for a valid conversion, `{ error }` for a malformed call (which must never be
|
|
394
|
+
* relayed verbatim: Codex would reject it locally after the HTTP 200, the reported failure mode),
|
|
395
|
+
* and `undefined` for tools that are not structured edits.
|
|
396
|
+
*/
|
|
397
|
+
export type StructuredEditTranslation =
|
|
398
|
+
| { patch: string; error?: undefined }
|
|
399
|
+
| { error: string; patch?: undefined };
|
|
400
|
+
|
|
401
|
+
export function translateStructuredEditCall(
|
|
402
|
+
toolName: string,
|
|
403
|
+
argsText: string,
|
|
404
|
+
): StructuredEditTranslation | undefined {
|
|
405
|
+
if (!isCursorStructuredEditToolName(toolName)) return undefined;
|
|
406
|
+
let parsed: unknown;
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(argsText);
|
|
409
|
+
} catch {
|
|
410
|
+
return {
|
|
411
|
+
error: `${toolName} arguments were not valid JSON; the call was dropped. ${
|
|
412
|
+
toolName === CURSOR_MULTI_EDIT_TOOL
|
|
413
|
+
? "Use file_path and edits[] (each edit with old_string and new_string)."
|
|
414
|
+
: "Use file_path, old_string and new_string."
|
|
415
|
+
}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
419
|
+
return { error: `${toolName} arguments must be a JSON object; the call was dropped.` };
|
|
420
|
+
}
|
|
421
|
+
const args = parsed as Record<string, unknown>;
|
|
422
|
+
const path = firstStringArg(args, ["file_path", "filePath", "path", "filepath", "filename"]);
|
|
423
|
+
if (!path || path.trim().length === 0) {
|
|
424
|
+
return { error: `${toolName} is missing a non-empty file_path; the call was dropped.` };
|
|
425
|
+
}
|
|
426
|
+
const hunks: string[] = [];
|
|
427
|
+
const addReplacement = (record: Record<string, unknown>): StructuredEditTranslation => {
|
|
428
|
+
const oldString = firstStringArg(record, ["old_string", "oldString", "oldtext", "old_text"]);
|
|
429
|
+
const newString = firstStringArg(record, ["new_string", "newString", "newtext", "new_text"]);
|
|
430
|
+
if (oldString === undefined || newString === undefined) {
|
|
431
|
+
return { error: `${toolName} requires old_string and new_string; the call was dropped.` };
|
|
432
|
+
}
|
|
433
|
+
const hunk = replacementHunk(oldString, newString);
|
|
434
|
+
if ("error" in hunk) return { error: hunk.error };
|
|
435
|
+
return { patch: hunk.hunk as string };
|
|
436
|
+
};
|
|
437
|
+
if (toolName === CURSOR_MULTI_EDIT_TOOL) {
|
|
438
|
+
const edits = args.edits;
|
|
439
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
440
|
+
return { error: "multi_edit requires a non-empty edits array; the call was dropped." };
|
|
441
|
+
}
|
|
442
|
+
for (const edit of edits) {
|
|
443
|
+
if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
|
|
444
|
+
return { error: "multi_edit edits entries must be objects with old_string and new_string; the call was dropped." };
|
|
445
|
+
}
|
|
446
|
+
const editResult = addReplacement(edit as Record<string, unknown>);
|
|
447
|
+
if (editResult.error !== undefined) return editResult;
|
|
448
|
+
hunks.push(editResult.patch);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
const editResult = addReplacement(args);
|
|
452
|
+
if (editResult.error !== undefined) return editResult;
|
|
453
|
+
hunks.push(editResult.patch);
|
|
454
|
+
}
|
|
455
|
+
return { patch: [PATCH_BEGIN, `*** Update File: ${path}`, ...hunks, PATCH_END].join("\n") };
|
|
456
|
+
}
|
|
457
|
+
|
|
314
458
|
export function mapSyntheticMcpExecToToolEvents(
|
|
315
459
|
args: McpArgs,
|
|
316
460
|
fallbackCallId = "cursor_mcp_exec",
|
|
@@ -341,9 +485,20 @@ export function mapSyntheticMcpExecToToolEvents(
|
|
|
341
485
|
}
|
|
342
486
|
}
|
|
343
487
|
// Stateless fallback (no shared event state): emit a complete, self-contained tool call.
|
|
488
|
+
//
|
|
489
|
+
// No conversion happens here by design (#1036 review). Structured-edit translation is gated on
|
|
490
|
+
// provenance — did WE advertise this bare name on THIS request — and that record lives on the
|
|
491
|
+
// request state, which this branch does not have. Converting anyway would reinstate the exact
|
|
492
|
+
// hazard the gate exists to close: a client or MCP tool legitimately named `edit_file` would be
|
|
493
|
+
// rewritten into an apply_patch it never asked for. The live path always carries state
|
|
494
|
+
// (live-transport seeds it), so this only affects direct/unit callers.
|
|
495
|
+
const emittedName = responsesName;
|
|
496
|
+
const emittedArgs = normalizedArgs;
|
|
344
497
|
return [
|
|
345
|
-
{ type: "tool_call_start", id: callId, name:
|
|
346
|
-
...(
|
|
498
|
+
{ type: "tool_call_start", id: callId, name: emittedName },
|
|
499
|
+
...(emittedArgs.length > 2
|
|
500
|
+
? [{ type: "tool_call_delta" as const, arguments: emittedArgs }]
|
|
501
|
+
: []),
|
|
347
502
|
{ type: "tool_call_end", id: callId },
|
|
348
503
|
];
|
|
349
504
|
}
|
|
@@ -385,6 +540,13 @@ function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, to
|
|
|
385
540
|
return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
|
|
386
541
|
}
|
|
387
542
|
|
|
543
|
+
function dropStructuredEditCall(state: CursorProtobufEventState, callId: string, toolName: string, reason: string): CursorServerMessage[] {
|
|
544
|
+
state.openToolCalls.delete(callId);
|
|
545
|
+
state.translatorBudget?.closeCall(callId);
|
|
546
|
+
state.completedToolCalls.add(callId);
|
|
547
|
+
return [{ type: "error", message: `${toolName} call was not converted to apply_patch: ${reason}` }];
|
|
548
|
+
}
|
|
549
|
+
|
|
388
550
|
function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
|
|
389
551
|
const open = state.openToolCalls.get(callId);
|
|
390
552
|
if (!open) return [];
|
|
@@ -392,6 +554,14 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
|
|
|
392
554
|
if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
|
|
393
555
|
if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
|
|
394
556
|
}
|
|
557
|
+
// Structured edit calls are converted to apply_patch here so both the interactionUpdate and the
|
|
558
|
+
// native-exec mcpArgs paths emit the same valid freeform payload (#1017).
|
|
559
|
+
const translation = structuredEditCallIsOurs(state.syntheticStructuredEditToolNames, open.name)
|
|
560
|
+
? translateStructuredEditCall(open.name, finalArgs)
|
|
561
|
+
: undefined;
|
|
562
|
+
if (translation?.error !== undefined) {
|
|
563
|
+
return dropStructuredEditCall(state, callId, open.name, translation.error);
|
|
564
|
+
}
|
|
395
565
|
if (finalArgs !== open.args) {
|
|
396
566
|
const previousBytes = Buffer.byteLength(open.args);
|
|
397
567
|
const reservation = state.translatorBudget?.reserveTransient(
|
|
@@ -402,8 +572,10 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
|
|
|
402
572
|
reservation?.commitRetained();
|
|
403
573
|
state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
|
|
404
574
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
575
|
+
const emittedName = translation ? CODEX_APPLY_PATCH_TOOL : open.name;
|
|
576
|
+
const emittedArgs = translation ? JSON.stringify({ input: translation.patch }) : finalArgs;
|
|
577
|
+
const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: emittedName }];
|
|
578
|
+
if (emittedArgs.length > 0) out.push({ type: "tool_call_delta", arguments: emittedArgs });
|
|
407
579
|
out.push(...endToolCall(state, callId));
|
|
408
580
|
return out;
|
|
409
581
|
}
|
|
@@ -10,14 +10,16 @@ import type {
|
|
|
10
10
|
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
|
|
11
11
|
import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types";
|
|
12
12
|
import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
|
|
13
|
-
import { cursorEffortSuffix,
|
|
13
|
+
import { cursorEffortSuffix, cursorRequestWireModelIdWithEffort } from "./effort-map";
|
|
14
14
|
import {
|
|
15
15
|
cursorMcpToolEncodedSize,
|
|
16
16
|
cursorMcpToolsEncodedSize,
|
|
17
17
|
cursorToolAllowedByChoice,
|
|
18
18
|
cursorToolChoiceAliases,
|
|
19
|
+
cursorStructuredEditTools,
|
|
19
20
|
cursorToolWireName,
|
|
20
21
|
cursorToolsForActivePrompt,
|
|
22
|
+
isCursorStructuredEditToolName,
|
|
21
23
|
isBareCodexShellBridgeTool,
|
|
22
24
|
} from "./tool-definitions";
|
|
23
25
|
import { lookupCursorThreadConversation } from "./thread-continuity";
|
|
@@ -41,6 +43,9 @@ function toolPriority(tool: OcxTool, selectedNames: ReadonlySet<string>): number
|
|
|
41
43
|
// selected filler cannot starve the Codex execution path during truncation (#399).
|
|
42
44
|
if (isBareCodexShellBridgeTool(tool)) return 0;
|
|
43
45
|
if (!tool.namespace && tool.name === "apply_patch") return 1;
|
|
46
|
+
// Structured edit tools convert to apply_patch on the return path, so they must survive the
|
|
47
|
+
// same byte/count truncation as the freeform tool they stand in for (#1017).
|
|
48
|
+
if (!tool.namespace && isCursorStructuredEditToolName(tool.name)) return 1;
|
|
44
49
|
if (cursorToolChoiceAliases(tool).some(name => selectedNames.has(name))) return 2;
|
|
45
50
|
if (tool.loadedFromToolSearch) return 3;
|
|
46
51
|
if (!tool.namespace) return 4;
|
|
@@ -61,7 +66,11 @@ export function applyCursorToolBudget(
|
|
|
61
66
|
toolChoice: OcxToolChoice | undefined,
|
|
62
67
|
): CursorToolBudgetResult {
|
|
63
68
|
const catalog = tools ?? [];
|
|
64
|
-
const
|
|
69
|
+
const baseEligible = catalog.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog));
|
|
70
|
+
// Synthetic structured edit tools ride along with the freeform apply_patch tool (#1017). They are
|
|
71
|
+
// part of the advertised catalog, so their serialized size counts toward the byte ceiling here.
|
|
72
|
+
const synthetic = cursorStructuredEditTools(catalog, toolChoice);
|
|
73
|
+
const eligible = [...baseEligible, ...synthetic];
|
|
65
74
|
if (
|
|
66
75
|
eligible.length <= CURSOR_TOOL_COUNT_LIMIT
|
|
67
76
|
&& cursorMcpToolsEncodedSize(eligible, toolChoice) <= CURSOR_TOOL_BYTES_LIMIT
|
|
@@ -101,7 +110,9 @@ export function applyCursorToolBudget(
|
|
|
101
110
|
|
|
102
111
|
return {
|
|
103
112
|
tools: eligible.filter(tool => keptSet.has(tool)),
|
|
104
|
-
omitted
|
|
113
|
+
// Synthetic tools are pinned in phase 1 and never reported as omitted; the note counts only
|
|
114
|
+
// tools the client itself requested.
|
|
115
|
+
omitted: baseEligible.filter(tool => !keptSet.has(tool)),
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -140,7 +151,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
140
151
|
],
|
|
141
152
|
};
|
|
142
153
|
}
|
|
143
|
-
return { ...selection, modelId: suffix ?
|
|
154
|
+
return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
|
|
@@ -8,6 +8,9 @@ export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses";
|
|
|
8
8
|
export const CODEX_EXEC_COMMAND_TOOL = "exec_command";
|
|
9
9
|
export const CODEX_SHELL_COMMAND_TOOL = "shell_command";
|
|
10
10
|
export const CODEX_APPLY_PATCH_TOOL = "apply_patch";
|
|
11
|
+
export const CURSOR_EDIT_FILE_TOOL = "edit_file";
|
|
12
|
+
export const CURSOR_MULTI_EDIT_TOOL = "multi_edit";
|
|
13
|
+
export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL] as const;
|
|
11
14
|
export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
|
|
12
15
|
export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
|
|
13
16
|
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
@@ -41,6 +44,47 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
|
41
44
|
additionalProperties: false,
|
|
42
45
|
} as const;
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Structured single-replacement schema advertised to Cursor models in addition to the freeform
|
|
49
|
+
* `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native
|
|
50
|
+
* Edit shape) but cannot produce Codex's freeform patch grammar, so every file edit attempt on the
|
|
51
|
+
* Cursor route produced malformed `apply_patch` payloads that the Codex client rejected locally
|
|
52
|
+
* (#1017). Calls to this tool are converted server-side into a valid apply_patch payload.
|
|
53
|
+
*/
|
|
54
|
+
export const CURSOR_EDIT_FILE_INPUT_SCHEMA = {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
|
|
58
|
+
old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
|
|
59
|
+
new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
|
|
60
|
+
},
|
|
61
|
+
required: ["file_path", "old_string", "new_string"],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
/** Structured multi-replacement schema; mirrors Cursor's native MultiEdit shape. */
|
|
66
|
+
export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
|
|
70
|
+
edits: {
|
|
71
|
+
type: "array",
|
|
72
|
+
items: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
|
|
76
|
+
new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
|
|
77
|
+
},
|
|
78
|
+
required: ["old_string", "new_string"],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
description: "Ordered replacement edits for this file. Each old_string must match the current file content.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ["file_path", "edits"],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
|
|
46
90
|
* Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
|
|
@@ -140,6 +184,73 @@ export function cursorRequestAdvertisesApplyPatch(
|
|
|
140
184
|
return catalog.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && cursorToolAllowedByChoice(tool, toolChoice, catalog));
|
|
141
185
|
}
|
|
142
186
|
|
|
187
|
+
export function isCursorStructuredEditToolName(name: string): boolean {
|
|
188
|
+
return (CURSOR_STRUCTURED_EDIT_TOOLS as readonly string[]).includes(name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Internal provenance gate for synthetic edits after prompt filtering and catalog budgeting. */
|
|
192
|
+
export function isCursorSyntheticStructuredEditTool(
|
|
193
|
+
tool: Pick<OcxTool, "namespace" | "name" | "cursorStructuredEdit">,
|
|
194
|
+
): boolean {
|
|
195
|
+
return !tool.namespace && tool.cursorStructuredEdit === true && isCursorStructuredEditToolName(tool.name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Synthetic structured edit tools for the Cursor route (#1017).
|
|
200
|
+
*
|
|
201
|
+
* Codex exposes `apply_patch` as a freeform custom tool whose body must be the exact Codex patch
|
|
202
|
+
* grammar (`*** Begin Patch` envelope, `@@` hunks, `-`/`+` prefixes). Cursor-trained models are
|
|
203
|
+
* trained on exact-match edit tools instead and emit malformed patch text on every attempt, which
|
|
204
|
+
* the Codex client then rejects locally ("invalid hunk"). When the request advertises the freeform
|
|
205
|
+
* `apply_patch` tool, also advertise Cursor-native-shaped `edit_file` / `multi_edit` tools; the
|
|
206
|
+
* adapter converts their exact-match replacements into a valid apply_patch payload (see
|
|
207
|
+
* protobuf-events.translateStructuredEditCall).
|
|
208
|
+
*
|
|
209
|
+
* Never widened when the caller pinned an explicit tool choice: a forced `apply_patch` selection
|
|
210
|
+
* must not gain sibling tools the client did not ask for.
|
|
211
|
+
*/
|
|
212
|
+
export function cursorStructuredEditTools(
|
|
213
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
214
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
215
|
+
): OcxTool[] {
|
|
216
|
+
if (!cursorRequestAdvertisesApplyPatch(tools, toolChoice)) return [];
|
|
217
|
+
if (toolChoice && toolChoice !== "auto" && toolChoice !== "required") return [];
|
|
218
|
+
// Never shadow an already-advertised bare tool with the same name (a client catalog could
|
|
219
|
+
// legitimately expose its own `edit_file` / `multi_edit` MCP-style tools).
|
|
220
|
+
const existingBareNames = new Set(
|
|
221
|
+
(tools ?? []).filter(tool => !tool.namespace).map(tool => tool.name),
|
|
222
|
+
);
|
|
223
|
+
const candidates: OcxTool[] = [
|
|
224
|
+
{
|
|
225
|
+
name: CURSOR_EDIT_FILE_TOOL,
|
|
226
|
+
cursorStructuredEdit: true,
|
|
227
|
+
description:
|
|
228
|
+
"Replace one block of exact text in a file. OpenCodex converts the replacement into a Codex apply_patch change, which the Codex client applies with its normal approval and sandbox policy. old_string must match the current file content exactly at exactly one location (apply_patch rejects ambiguous hunks). Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
|
|
229
|
+
parameters: { ...CURSOR_EDIT_FILE_INPUT_SCHEMA },
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: CURSOR_MULTI_EDIT_TOOL,
|
|
233
|
+
cursorStructuredEdit: true,
|
|
234
|
+
description:
|
|
235
|
+
"Apply several exact-text replacements to one file. OpenCodex converts the edits into a single Codex apply_patch change, which the Codex client applies with its normal approval and sandbox policy. Each old_string must match the current file content exactly at exactly one location (apply_patch rejects ambiguous hunks). Edits are independent: every old_string is matched against the ORIGINAL file content, so a later edit must not rely on text introduced by an earlier one. Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
|
|
236
|
+
parameters: { ...CURSOR_MULTI_EDIT_INPUT_SCHEMA },
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
return candidates.filter(tool => !existingBareNames.has(tool.name));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* True when this request actually advertises the synthetic structured edit tools (`edit_file` /
|
|
244
|
+
* `multi_edit`) — i.e. a freeform `apply_patch` is advertised, no tool-choice pin blocks widening,
|
|
245
|
+
* and neither name is shadowed by an existing bare tool in the client catalog.
|
|
246
|
+
*/
|
|
247
|
+
export function cursorRequestAdvertisesStructuredEdits(
|
|
248
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
249
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
250
|
+
): boolean {
|
|
251
|
+
return cursorStructuredEditTools(tools, toolChoice).length > 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
143
254
|
export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
|
|
144
255
|
return namespacedToolName(tool.namespace, tool.name);
|
|
145
256
|
}
|
|
@@ -415,6 +526,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
415
526
|
const hasBareExec = shellBridgeNames.length > 0;
|
|
416
527
|
const shellBridgeLabel = quotedNames(shellBridgeNames.length > 0 ? shellBridgeNames : [...CODEX_SHELL_BRIDGE_TOOL_NAMES]);
|
|
417
528
|
const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
|
|
529
|
+
const structuredEditNames = tools
|
|
530
|
+
?.filter(tool => !tool.namespace && isCursorStructuredEditToolName(tool.name))
|
|
531
|
+
.map(tool => tool.name) ?? [];
|
|
418
532
|
const discoveryTools = discoveryToolLabel(wireNames);
|
|
419
533
|
const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
|
|
420
534
|
// Host-shell-neutral: the Codex client executes bridge commands, and may differ from
|
|
@@ -443,7 +557,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
443
557
|
? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.`
|
|
444
558
|
: undefined,
|
|
445
559
|
hasApplyPatch
|
|
446
|
-
?
|
|
560
|
+
? structuredEditNames.length > 0
|
|
561
|
+
? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take exact-match replacements that OpenCodex converts into Codex \`apply_patch\` changes for approval. Use \`apply_patch\` directly only when you can emit its exact freeform syntax (\`*** Begin Patch\` envelope with \`@@\` hunks and \`-\`/\`+\` line prefixes); never emit patch-like plain text as tool arguments.`
|
|
562
|
+
: "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
447
563
|
: undefined,
|
|
448
564
|
hasBareExec
|
|
449
565
|
? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
|
|
@@ -457,7 +573,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
457
573
|
: undefined,
|
|
458
574
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
459
575
|
hasBareExec
|
|
460
|
-
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
|
|
576
|
+
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
|
|
461
577
|
: undefined,
|
|
462
578
|
].filter((note): note is string => typeof note === "string");
|
|
463
579
|
return notes.join(" ");
|
package/src/adapters/google.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type TranslatorBudget,
|
|
31
31
|
} from "../lib/translator-budget";
|
|
32
32
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
33
|
-
import { mapReasoningEffort } from "../reasoning-effort";
|
|
33
|
+
import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
|
|
34
34
|
|
|
35
35
|
// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
|
|
36
36
|
// tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
|
|
@@ -340,12 +340,22 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
340
340
|
if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
|
|
341
341
|
if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
|
|
342
342
|
if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
// Effort → thinkingLevel follows the configured ladder: any model advertising reasoning
|
|
344
|
+
// efforts (registry preset or user config) sends the mapped level, so a picker-selected
|
|
345
|
+
// effort actually reaches the wire (gemini-3.1-pro-preview ships a ladder). The original
|
|
346
|
+
// gemini-3.5/3.6-flash direct-mode slice stays hardcoded so unladdered configs keep their
|
|
347
|
+
// current behavior; Vertex participates only through an explicitly configured ladder (the
|
|
348
|
+
// seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
|
|
349
|
+
// suppress the responseModalities fallback below. CCA maps effort on its envelope path.
|
|
350
|
+
const thinkingEligible = provider.googleMode !== "cloud-code-assist"
|
|
351
|
+
&& !isImageCapableModel(parsed.modelId)
|
|
352
|
+
&& (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
|
|
353
|
+
|| (provider.googleMode !== "vertex"
|
|
354
|
+
&& (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
|
|
355
|
+
const thinkingLevel = thinkingEligible
|
|
346
356
|
? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
|
|
347
357
|
: undefined;
|
|
348
|
-
if (
|
|
358
|
+
if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
|
|
349
359
|
if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
|
|
350
360
|
generationConfig.responseModalities = ["TEXT", "IMAGE"];
|
|
351
361
|
}
|
|
@@ -3,6 +3,7 @@ import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, Ocx
|
|
|
3
3
|
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
4
4
|
import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
5
5
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
6
|
+
import { sseFieldValue } from "../lib/sse-decoder";
|
|
6
7
|
import { isDebugEnabled } from "../lib/debug-settings";
|
|
7
8
|
import { isCyberPolicyCode } from "../lib/errors";
|
|
8
9
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -818,6 +819,26 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
818
819
|
if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
|
|
819
820
|
body.prompt_cache_key = parsed.options.promptCacheKey;
|
|
820
821
|
}
|
|
822
|
+
// Responses `text.format` -> chat `response_format`. json_object maps 1:1; json_schema
|
|
823
|
+
// re-nests the flattened Responses fields under `json_schema` — the exact inverse of
|
|
824
|
+
// responseFormatToText in src/chat/inbound.ts. Forwarded unconditionally (like `stop`):
|
|
825
|
+
// response_format is a first-class Chat Completions field, it is only present when the
|
|
826
|
+
// caller explicitly asked for structured output, and a backend that rejects it should
|
|
827
|
+
// fail loud rather than silently return prose the caller will try to JSON.parse.
|
|
828
|
+
const textFormat = parsed.options.textFormat;
|
|
829
|
+
if (textFormat?.type === "json_object") {
|
|
830
|
+
body.response_format = { type: "json_object" };
|
|
831
|
+
} else if (textFormat?.type === "json_schema") {
|
|
832
|
+
body.response_format = {
|
|
833
|
+
type: "json_schema",
|
|
834
|
+
json_schema: {
|
|
835
|
+
name: textFormat.name ?? "response",
|
|
836
|
+
...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
|
|
837
|
+
...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
|
|
838
|
+
...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
}
|
|
821
842
|
|
|
822
843
|
if (tools) {
|
|
823
844
|
// Default-ON for chat-completions providers (user decision 260709): the buffered
|
|
@@ -927,8 +948,9 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
927
948
|
// Yields adapter events and returns "terminate" for a terminal frame ([DONE] / error) that
|
|
928
949
|
// must end the stream, or "continue" otherwise. Mutates the closure's terminal-signal state.
|
|
929
950
|
const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
930
|
-
|
|
931
|
-
|
|
951
|
+
const rawPayload = sseFieldValue(line, "data");
|
|
952
|
+
if (rawPayload === null) return "continue";
|
|
953
|
+
const payload = rawPayload.trim();
|
|
932
954
|
if (payload === "[DONE]") {
|
|
933
955
|
yield* flushToolCalls();
|
|
934
956
|
const stopReason = stopReasonFor(finishReason);
|
|
@@ -1048,7 +1048,8 @@ function stripInputImagesDeep(value: unknown): unknown {
|
|
|
1048
1048
|
*/
|
|
1049
1049
|
function buildRoutedCompactionBody(body: unknown): unknown {
|
|
1050
1050
|
if (!isPlainObject(body)) return body;
|
|
1051
|
-
|
|
1051
|
+
// `text` goes with the tool fields: the summary must be prose, not schema-constrained JSON.
|
|
1052
|
+
const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, text: _text, ...rest } = body;
|
|
1052
1053
|
const input = Array.isArray(body.input) ? body.input : [];
|
|
1053
1054
|
const kept = input.filter(item => !isPlainObject(item)
|
|
1054
1055
|
// `additional_tools` is how Codex Desktop's responses-lite shape carries tools;
|
package/src/bridge.ts
CHANGED
|
@@ -827,8 +827,10 @@ export function bridgeToResponsesSSE(
|
|
|
827
827
|
if (currentReasoning) closeCurrentReasoning();
|
|
828
828
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
829
829
|
flushHiddenRawReasoning();
|
|
830
|
-
// Reasoning consumed by a text turn, not a tool call: no cache target.
|
|
831
|
-
|
|
830
|
+
// Reasoning consumed by a REAL text turn, not a tool call: no cache target.
|
|
831
|
+
// Empty text deltas must not wipe reasoning that precedes a tool call
|
|
832
|
+
// (chat-completions providers emit empty content deltas mid-tool-turn).
|
|
833
|
+
if (event.text.length > 0) rawReasoningForNextToolCall = "";
|
|
832
834
|
if (currentToolCall) closeCurrentToolCall();
|
|
833
835
|
// Only flush on an explicit phase change. A later delta that omits `phase` must
|
|
834
836
|
// keep appending to the current message rather than wiping the earlier phase.
|
|
@@ -880,7 +882,7 @@ export function bridgeToResponsesSSE(
|
|
|
880
882
|
if (currentMsg) closeCurrentMessage("commentary");
|
|
881
883
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
882
884
|
flushHiddenRawReasoning();
|
|
883
|
-
rawReasoningForNextToolCall = "";
|
|
885
|
+
if (event.thinking.length > 0) rawReasoningForNextToolCall = "";
|
|
884
886
|
if (currentToolCall) closeCurrentToolCall();
|
|
885
887
|
if (!currentReasoning) {
|
|
886
888
|
const itemId = `rs_${uuid()}`;
|
|
@@ -1561,7 +1563,9 @@ function buildResponseJSONWithBudget(
|
|
|
1561
1563
|
if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
|
|
1562
1564
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1563
1565
|
if (currentRawReasoning) flushRawReasoning();
|
|
1564
|
-
|
|
1566
|
+
// Empty text deltas (batch chat responses always carry content, often "") must
|
|
1567
|
+
// not wipe reasoning that precedes a tool call (#950 non-streaming path).
|
|
1568
|
+
if (e.text.length > 0) rawReasoningForNextToolCall = "";
|
|
1565
1569
|
if (currentToolCallId) flushToolCall();
|
|
1566
1570
|
// Compaction turns keep the summary out of normal message output (replay dedup — see
|
|
1567
1571
|
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
@@ -1580,7 +1584,7 @@ function buildResponseJSONWithBudget(
|
|
|
1580
1584
|
case "thinking_delta":
|
|
1581
1585
|
if (currentText) flushText("commentary");
|
|
1582
1586
|
if (currentRawReasoning) flushRawReasoning();
|
|
1583
|
-
rawReasoningForNextToolCall = "";
|
|
1587
|
+
if (e.thinking.length > 0) rawReasoningForNextToolCall = "";
|
|
1584
1588
|
if (currentToolCallId) flushToolCall();
|
|
1585
1589
|
{
|
|
1586
1590
|
({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
|
package/src/chat/outbound.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
type Rec = Record<string, unknown>;
|
|
9
9
|
|
|
10
|
-
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
10
|
+
import { decodeServerSentEvents, sseFieldValue } from "../lib/sse-decoder";
|
|
11
11
|
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
12
12
|
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
|
|
13
13
|
|
|
@@ -671,8 +671,9 @@ export async function collectChatCompletion(
|
|
|
671
671
|
const rawFrame = buffer.slice(0, sep);
|
|
672
672
|
buffer = replaceRetained(buffer, buffer.slice(sep + 2), "live_transient");
|
|
673
673
|
for (const line of rawFrame.split("\n")) {
|
|
674
|
-
|
|
675
|
-
|
|
674
|
+
const rawData = sseFieldValue(line, "data");
|
|
675
|
+
if (rawData === null) continue;
|
|
676
|
+
const data = rawData.trim();
|
|
676
677
|
if (!data || data === "[DONE]") continue;
|
|
677
678
|
let parsed: unknown;
|
|
678
679
|
try { parsed = JSON.parse(data); } catch { continue; }
|