Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ Verifier: pure standalone synthetic SSE-block imports, compare outputs at each l
## Audit amendment

Executable repair is limited to authorized code-mode exec and recognized helper aliases; unrelated same-name native custom tools keep raw input byte-for-byte. Explicit negative: render_diagram input JSON string {"input":"literal"} is not unwrapped. Separate scenarios cover missing input.done, terminal-only completion, failed/incomplete after held deltas, and disposal. Authoritative completion wins over previews. Failure never synthesizes successful completion. All retained buffers release. One simulated execution means choose the client-consumed completed item once, not execute every redundant lifecycle representation.

## P revalidation

Consume 020 ff388977a with isolated route/writer proof and remote75+19 tests. 030 remains scoped to the two patch lifecycle gaps. Append040 for independently confirmed ordinary function and dotted-namespace parity gaps; all terminal CI/merge obligations move there unchanged. Main owns production030; a disjoint worker may add tests only in tests/responses/responses-custom-tool-repair.test.ts. Native code-mode exec previews may be held until final when they could be complete raw/wrapped envelopes; unrelated native custom JSON bodies remain raw.

## Implementation audit synthesis

A fragmented pretty JSON wrapper beginning with brace-newline escaped the compact-prefix guard, so preview bytes could contradict compiled completion. The completion parser accepts arbitrary whitespace, escaped property names and property order; native exec now conservatively holds all object-leading inputs to completion. Ordinary JavaScript stays byte-exact, though a block-leading program waits for completion. Added per-character pretty-wrapper and escaped-key regressions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 040 Native function and namespace parity

Depends on 030 custom-call restoration. Class C3 spec-satisfaction repair. User requested all Chat-era tool repairs be checked. Independent source/probe inventory finds native ordinary calls retain integer-as-float and numeric-as-string mismatches, completed empty arguments, and dotted namespace names that bridge already repairs. These are in scope; assistant output filtering is not.

NEW src/responses/function-call-compat.ts: collect original current-turn ordinary function declarations using collectResponsesToolGroups, preserving namespace/kind and original parameter schema. Lookup exact declared identity, including reserved functions children as bare and authorized canonical namespace aliases. Do not consume historical-only declarations or provider-normalized schemas. Pure completed-item transform calls coerceIntegerToolArguments(raw||"{}", original.parameters, original.namespace ? undefined : original.name). Only explicit completed empty payload becomes {}; unknown/missing, malformed nonempty, fractions, numeric unions, unsafe integers, custom/hosted/helper calls remain unchanged.
NEW src/server/responses-function-tool-repair.ts: SseBlockRewrite tracks item identity by item_id/output_index and uses same pure completion transform at arguments.done, item.done and terminal snapshots. Preserve in-progress placeholders. Budget any buffered data and release on done/terminal/dispose. Original declaration is authority; attempt wire alias is transport spelling only. Final representations must agree and failed/incomplete never synthesize successful executable input. Revalidate whether delta holding is necessary against existing bridge closeCurrentToolCall (which already repairs authoritative final arguments after streamed numeric previews); use one compatible completion contract rather than introducing arbitrary JSON rewriting.
MODIFY src/server/responses/core.ts: derive ordinary schemas from currentTurnWireToolCatalogBody before lowering; compose native function repair after namespace/custom restores and before undeclared guard. Apply pure repair in JSON, SSE final snapshots, bounded JSON-to-SSE, and rememberPassthroughResponseChecked so client and replay state agree. Canonical forward auth remains byte-pass-through. Rebuild only attempt-specific aliases on retries.
MODIFY src/responses/namespace-tool-compat.ts and, if needed, responses-undeclared-tool-guard.ts: reuse existing collectAmbiguousDottedAliases ownership algorithm rather than duplicate. Add unambiguous dotted aliases after canonical authorization, collisions computed from whole original current-turn catalog including bare spellings before selection. Never reinterpret explicit conflicting namespaces or different kinds; canonical identities retain precedence.
MODIFY existing native Responses repair and namespace tests, or register new domain tests in both layout manifests: integer/string and no-arg scenarios at JSON/each SSE completion/replay, namespace wait exception boundaries, same-inner-name schemas, forbidden selectors/replay-only names, early/interleaved events, terminal/dispose cleanup, dotted collision order independence, unchanged030 code/patch semantics.
UPDATE structure/11_compatibility-contracts.md and guides/codex-integration.md with completion parity boundary and inventory.

C: standalone synthetic imports with stub tools (no real execution) and remote focused tests/typecheck; all stack PR exact-head hosted CI must pass before merge. Keep original pi-filter/owned-refresh/responses-patch terminal criteria unchanged and satisfy them at final D with PR heads/CI/merge ancestry. Register native GitHub stack, merge approved prefix using async REST and SHA guard, wait for actual merged status, fetch dev and prove all merged SHAs ancestors. No release/deploy/local suites. Stop only verified DONE or actual external blocking evidence. Resource bounds inherited from000.
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ encodes that declaration and its history as an upstream function tool, then rest
function-call lifecycle to `custom_tool_call` before Codex sees it. Native OpenAI forward routing
and the supported `apply_patch` custom tool stay unchanged.

If a routed model sends a complete patch as the entire code-mode `exec` input, opencodex
converts it to the nested `tools.apply_patch` call before the tool-completion events reach
Codex. Native custom calls and converted function calls use the same completion rule;
patch previews are held while their executable form is unresolved. JavaScript that merely
contains patch text and unrelated native custom payloads stay unchanged.

The selected provider must support function/tool calling. A text-only provider without tool-call
support cannot use `exec`, Browser, or Computer Use. Native OpenAI rows keep their upstream tool
mode unchanged.
Expand Down
10 changes: 10 additions & 0 deletions src/responses/custom-tool-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ export function restoreRoutedCustomCalls(
const helper = aliased && sourceInput !== ""
? item.name
: resolveCodeModeHelperName(undefined, targetName, sourceInput, itemNamespace, declaredNames);
// Native custom input is already the tool's raw grammar. Only a recognized
// helper/envelope may reinterpret it; a JSON-looking native body is not a wrapper.
if (item.type === "custom_tool_call" && !aliased && !helper) {
const input = repairNames.has(wireName) && typeof sourceInput === "string"
? normalizeApplyPatchDelimiters(sourceInput)
: sourceInput;
return input !== sourceInput
? { value: { ...item, input }, changed: true }
: { value: item, changed: false };
}
const restored: Record<string, unknown> = {
...item,
type: "custom_tool_call",
Expand Down
38 changes: 29 additions & 9 deletions src/server/responses-custom-tool-repair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export function createRoutedCustomToolRestoreBlockRewrite(
declaredNames?: ReadonlySet<string>,
): SseBlockRewrite {
const itemNames = new Map<string, { name: string; aliased: boolean; namespace?: string }>();
const customAliasItemNames = new Map<string, string>();
// Native helper aliases and genuine bare code-mode exec calls share completion repair.
const customExecItemNames = new Map<string, string>();
const repairItemNames = new Map<string, string>();
const ordinaryItemIds = new Set<string>();
const openCalls = new Map<string, OpenCustomCall>();
Expand All @@ -119,7 +120,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
}
pendingArguments = [];
itemNames.clear();
customAliasItemNames.clear();
customExecItemNames.clear();
repairItemNames.clear();
ordinaryItemIds.clear();
};
Expand Down Expand Up @@ -195,15 +196,17 @@ export function createRoutedCustomToolRestoreBlockRewrite(
const wireName = routedCustomToolWireName(parsed.item);
const targetName = routedCustomToolTargetName(parsed.item, names, declaredNames);
const aliased = targetName !== undefined && targetName !== wireName;
if (upstreamItemId && aliased) {
customAliasItemNames.set(upstreamItemId, parsed.item.name);
const codeModeExec = targetName === "exec" && parsed.item.name === "exec"
&& parsed.item.namespace === undefined && declaresCodeModeExec(declaredNames);
if (upstreamItemId && (aliased || codeModeExec)) {
customExecItemNames.set(upstreamItemId, parsed.item.name);
if (type === "response.output_item.added") {
openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
}
}
const repairable = wireName !== undefined && repairNames.has(wireName);
if (upstreamItemId && repairable) repairItemNames.set(upstreamItemId, parsed.item.name);
const restored = repairable || aliased
const restored = repairable || aliased || codeModeExec
? restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames)
: { value: parsed, changed: false };
if (type === "response.output_item.done" && upstreamItemId) releaseCall(upstreamItemId);
Expand Down Expand Up @@ -259,7 +262,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
if (
type === "response.custom_tool_call_input.delta"
&& upstreamItemId
&& customAliasItemNames.has(upstreamItemId)
&& customExecItemNames.has(upstreamItemId)
) {
const open = openCalls.get(upstreamItemId) ?? { argumentsText: "", emittedInput: "", retainedBytes: 0 };
const delta = typeof parsed.delta === "string" ? parsed.delta : "";
Expand All @@ -268,19 +271,34 @@ export function createRoutedCustomToolRestoreBlockRewrite(
open.argumentsText += delta;
open.retainedBytes += deltaBytes;
openCalls.set(upstreamItemId, open);
return [];
if (customExecItemNames.get(upstreamItemId) !== "exec"
|| mayBecomePatchEnvelope(open.argumentsText)
// JSON.parse accepts whitespace, escaped keys and arbitrary property order.
// Any object prefix may still wrap a patch; keep it until authoritative completion.
|| open.argumentsText.trimStart() === ""
|| open.argumentsText.trimStart().startsWith("{")) return [];
// If a held prefix turns out to be ordinary JavaScript, release the entire
// un-emitted suffix. Native custom input remains byte-exact.
const inputDelta = open.argumentsText.slice(open.emittedInput.length);
open.emittedInput = open.argumentsText;
return inputDelta ? [replaceSseDataPayload(block, JSON.stringify({ ...parsed, delta: inputDelta }))] : [];
}
if (
type === "response.custom_tool_call_input.done"
&& upstreamItemId
&& customAliasItemNames.has(upstreamItemId)
&& customExecItemNames.has(upstreamItemId)
) {
const source = typeof parsed.input === "string"
? parsed.input
: openCalls.get(upstreamItemId)?.argumentsText ?? "";
const name = customExecItemNames.get(upstreamItemId)!;
const helper = name === "exec"
? resolveCodeModeHelperName(undefined, name, source, undefined, declaredNames)
: name;
releaseCall(upstreamItemId);
return [replaceSseDataPayload(block, JSON.stringify({
...parsed,
input: compileCodeModeHelperInput(source, customAliasItemNames.get(upstreamItemId)!),
input: helper ? compileCodeModeHelperInput(source, helper) : source,
}))];
}
if (
Expand Down Expand Up @@ -312,6 +330,8 @@ export function createRoutedCustomToolRestoreBlockRewrite(
open.argumentsText += delta;
open.retainedBytes += deltaBytes;
openCalls.set(upstreamItemId, open);
// A helper alias will become JavaScript at completion, never raw patch/JSON.
if (itemNames.get(upstreamItemId)?.aliased) return [];
// Still accumulating toward the compact wrapper, or an unrecognized shape:
// suppress progressive emission and let the done event carry input.
if (FREEFORM_WRAP_PREFIX.startsWith(open.argumentsText)) return [];
Expand Down
8 changes: 8 additions & 0 deletions structure/11_compatibility-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,11 @@ them independently.
- 선택한 방식: Add a passive versioned schema and one exact `openai`/canonical Codex URL/forward/`gpt-5.6-sol` manifest whose claims reference assertion-level fixtures executed against the production adapter.
- 다른 대안 대신 이 방식을 선택한 이유: Registry flags do not capture transformations such as local continuation expansion or orphan-output degradation. A broad first matrix would turn unverified assumptions into public promises.
- 장점, 단점 및 영향: The first contract is small but trustworthy and can feed future CLI/GUI surfaces. Coverage expands only as fixtures are added; no request behavior changes in this slice.

## Routed code-mode patch completion

Native Responses custom exec and function helper aliases apply the same complete-envelope
resolver at input.done, output_item.done and terminal snapshots. Potential raw/wrapped patch
previews are withheld before compilation; ordinary native custom payloads retain their raw
grammar. A string merely containing patch markers remains executable caller input and is
never rewritten. Completion and disposal release retained preview buffers.
Loading
Loading