Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/adapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface AdapterRequest {
body: string;
/** Final upstream wire names of custom tools lowered to functions while building this request. */
convertedRoutedCustomToolNames?: ReadonlySet<string>;
/** Native custom-tool wire names authorized for representation-only response repair. */
routedCustomToolRepairNames?: ReadonlySet<string>;
/** Client tool-search names actually lowered to upstream function calls for this request. */
convertedRoutedToolSearchNames?: ReadonlySet<string>;
/** Upstream-only aliases for namespace tools flattened in this request. */
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):

const forward = provider.authMode === "forward";
let convertedRoutedCustomToolNames: Set<string> | undefined;
let routedCustomToolRepairNames: Set<string> | undefined;
let convertedRoutedToolSearchNames: Set<string> | undefined;
let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string }> | undefined;
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
Expand Down Expand Up @@ -1741,6 +1742,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
);
outBody = rewritten.body;
convertedRoutedCustomToolNames = rewritten.names;
routedCustomToolRepairNames = rewritten.repairNames;
}
if (!isCanonicalOpenAiForwardProvider(provider)) {
// Run after custom-tool lowering so the search compatibility layer can choose a
Expand Down Expand Up @@ -1811,6 +1813,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
body,
releaseBodyObservation,
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
...(tierLog ? { tierLog } : {}),
Expand Down
34 changes: 19 additions & 15 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "./types";
import { coerceIntegerToolArguments } from "./lib/tool-argument-integers";
import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
import { repairFreeformToolInput } from "./responses/apply-patch-envelope";
import { encodeCompactionSummary } from "./responses/compaction";
import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
Expand Down Expand Up @@ -230,12 +231,12 @@ export function bridgeToResponsesSSE(
const replayCacheScope = options?.replayCacheScope;
const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
// function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
const freeformInput = (args: string): string => {
try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
return args;
};
// Freeform/custom tools (apply_patch, code-mode exec) carry their body in `input`; the
// model is given a function with `{input:string}`, so unwrap it here when relaying back
// as a custom_tool_call. Decorated apply_patch envelopes are repaired at this boundary.
const freeformInput = (args: string, toolName: string, namespace?: string): string => (
repairFreeformToolInput(args, toolName, namespace)
);
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
Expand Down Expand Up @@ -636,7 +637,8 @@ export function bridgeToResponsesSSE(
if (currentToolCall.freeform) {
emit("response.custom_tool_call_input.done", {
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
input: freeformInput(currentToolCall.args),
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace),
});
}
// Freeform tools serialize as custom_tool_call without extra_content; remember the
Expand All @@ -652,7 +654,8 @@ export function bridgeToResponsesSSE(
? {
type: "custom_tool_call", id: currentToolCall.itemId,
call_id: currentToolCall.callId, name: currentToolCall.name,
input: freeformInput(currentToolCall.args), status: "completed",
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace), status: "completed",
}
: {
type: "function_call", id: currentToolCall.itemId,
Expand Down Expand Up @@ -691,7 +694,8 @@ export function bridgeToResponsesSSE(
? {
type: "custom_tool_call", id: currentToolCall.itemId,
call_id: currentToolCall.callId, name: currentToolCall.name,
input: freeformInput(currentToolCall.args), status: "incomplete",
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace), status: "incomplete",
}
: {
type: "function_call", id: currentToolCall.itemId,
Expand Down Expand Up @@ -1064,7 +1068,7 @@ export function bridgeToResponsesSSE(
const item = toolSearch
? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
: freeform
? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, ...(ns ? { namespace: ns } : {}), input: "", status: "in_progress" }
: { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
emit("response.output_item.added", { output_index: outputIndex, item });
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch, providerMetadata: event.providerMetadata };
Expand Down Expand Up @@ -1567,10 +1571,9 @@ function buildResponseJSONWithBudget(
// Web-search citations awaiting the next assistant message (attached as url_citation annotations).
let pendingWebSources: { url: string; title?: string }[] = [];

const freeformInput = (args: string): string => {
try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
return args;
};
const freeformInput = (args: string, toolName: string, namespace?: string): string => (
repairFreeformToolInput(args, toolName, namespace)
);
const parseArgsObj = (args: string): Record<string, unknown> => {
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
};
Expand Down Expand Up @@ -1670,7 +1673,8 @@ function buildResponseJSONWithBudget(
pushOutput({
type: "custom_tool_call", id: `ctc_${uuid()}`,
call_id: currentToolCallId, name: realName,
input: freeformInput(currentToolCallArgs), status,
...(ns ? { namespace: ns } : {}),
input: freeformInput(currentToolCallArgs, realName, ns), status,
});
} else {
pushOutput({
Expand Down
63 changes: 63 additions & 0 deletions src/responses/apply-patch-envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Representation repair for top-level Codex apply_patch custom-tool payloads.
//
// Some routed models decorate the first and last lines as
// `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
// valid custom-tool payloads. Repair is deliberately limited to a complete,
// structurally recognizable top-level patch: arbitrary `exec` JavaScript is
// caller-authored executable input and must remain byte-identical.
//
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
// repair the one faithful reading, leave genuine patch content alone.

const PATCH_BEGIN = "*** Begin Patch";
const PATCH_END = "*** End Patch";
const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;

/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
export function unwrapFreeformToolInput(argumentsText: unknown): string {
if (typeof argumentsText !== "string") return "";
try {
const parsed: unknown = JSON.parse(argumentsText);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
const input = (parsed as { input?: unknown }).input;
if (typeof input === "string") return input;
}
} catch {
// The string is the freeform body, not nested JSON.
}
return argumentsText;
}

/**
* Strip trailing `***` only from the outer lines of one complete patch.
* Internal patch content, incomplete envelopes, and non-patch text are exact
* pass-throughs.
*/
export function normalizeApplyPatchDelimiters(text: string): string {
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
if (!match) return text;
const [, begin, beginBreak, body, endBreak, end, trailingBreak = ""] = match;
if (!PATCH_OPERATION_LINE.test(body)) return text;
if (begin === PATCH_BEGIN && end === PATCH_END) return text;
return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
}

/**
* Repair freeform input before Codex sees it.
*
* Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
* repair. Remote namespaces own their grammar; those bodies and every other
* freeform input are unwrapped and left byte-exact.
*/
export function repairFreeformToolInput(
argumentsText: unknown,
toolName = "",
namespace?: string,
): string {
const unwrapped = unwrapFreeformToolInput(argumentsText);
const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
return ownsApplyPatchGrammar && toolName === "apply_patch"
? normalizeApplyPatchDelimiters(unwrapped)
: unwrapped;
}
Loading
Loading