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
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ This rewrite is destination-scoped. Key-auth public/custom Responses providers a
forward gateways keep both fields unchanged; a multimodal system message is never partially folded
or silently dropped.

For canonical forward continuations, client-only `prompt_cache_breakpoint` properties are removed
recursively within bounded traversal limits. When `store: false`, `item_reference` rows are also
omitted because the destination cannot resolve an item it did not persist. Function/tool `call_id`
pairs and `reasoning.effort` are preserved.

For `key` auth, [`retryOn429`](/reference/configuration/) applies here too: a pre-stream 429
waits and replays the identical request on the same key before any other handling, exactly like
the translated `openai-chat` / Anthropic request path. Custom `runTurn` transports are not part
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/reference/proxy-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ handle only the item types they recognize, and may reject a feature their provid
On the canonical ChatGPT Codex forward route, text-only `system` input messages are folded into
top-level `instructions`, and `truncation` is removed because that destination rejects both public
Responses shapes. Other Responses destinations preserve them.
The same canonical boundary removes nested client-only `prompt_cache_breakpoint` markers and drops
`item_reference` entries only on `store: false` continuations; tool call/result pairing is unchanged.

### JSON and SSE output

Expand Down
75 changes: 75 additions & 0 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,80 @@ function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
return next;
}

const POSIT_CACHE_MARKER_MAX_DEPTH = 64;
const POSIT_CACHE_MARKER_MAX_NODES = 100_000;

type PromptCacheMarkerRewrite = {
value: unknown;
changed: boolean;
complete: boolean;
};

/**
* Remove Posit/Anthropic-style prompt-cache markers without trusting request nesting. The walk
* aborts atomically when its depth or node budget is exceeded, so a hostile extension object can
* neither overflow the stack nor receive a partially rewritten subtree.
*/
function stripPromptCacheBreakpoints(
value: unknown,
state: { nodes: number },
depth = 0,
): PromptCacheMarkerRewrite {
state.nodes += 1;
if (depth > POSIT_CACHE_MARKER_MAX_DEPTH || state.nodes > POSIT_CACHE_MARKER_MAX_NODES) {
return { value, changed: false, complete: false };
}
if (Array.isArray(value)) {
let changed = false;
const next: unknown[] = [];
for (const entry of value) {
const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
if (!rewritten.complete) return { value, changed: false, complete: false };
changed ||= rewritten.changed;
next.push(rewritten.value);
}
return { value: changed ? next : value, changed, complete: true };
}
if (!isPlainObject(value)) return { value, changed: false, complete: true };

let changed = Object.hasOwn(value, "prompt_cache_breakpoint");
const next: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
if (key === "prompt_cache_breakpoint") continue;
const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
if (!rewritten.complete) return { value, changed: false, complete: false };
changed ||= rewritten.changed;
next[key] = rewritten.value;
}
return { value: changed ? next : value, changed, complete: true };
}

/**
* Posit Assistant can replay client-only cache markers and stored-item references on a
* `store: false` continuation. The canonical ChatGPT Codex backend rejects both. Remove the
* markers recursively and drop only `item_reference` rows that cannot name persisted state;
* ordinary item ids are handled later by stripItemIdsWhenUnstored and tool call_id pairs remain.
*/
function normalizeCanonicalForwardContinuationEnvelope(body: unknown): unknown {
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
let input: unknown[] = body.input;
let changed = false;
if (body.store === false) {
const withoutReferences = input.filter(item => !isPlainObject(item) || item.type !== "item_reference");
if (withoutReferences.length !== input.length) {
input = withoutReferences;
changed = true;
}
}

const markerRewrite = stripPromptCacheBreakpoints(input, { nodes: 0 });
if (markerRewrite.complete && markerRewrite.changed) {
input = markerRewrite.value as unknown[];
changed = true;
}
return changed ? { ...body, input } : body;
}

const IMAGE_GEN_NAMESPACE = "image_gen";
const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
Expand Down Expand Up @@ -1858,6 +1932,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
if (isCanonicalOpenAiForwardProvider(provider)) {
outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
outBody = normalizeCanonicalForwardPromptEnvelope(outBody);
outBody = normalizeCanonicalForwardContinuationEnvelope(outBody);
}
} else {
outBody = preferConfiguredHostedTools(
Expand Down
18 changes: 17 additions & 1 deletion src/compatibility/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
schemaVersion: 1,
id: "openai.codex-forward.gpt-5-6-sol.responses",
version: "1.1.0",
version: "1.2.0",
subject: {
providerId: "openai",
baseUrl: "https://chatgpt.com/backend-api/codex",
Expand Down Expand Up @@ -85,6 +85,22 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }],
},
{
id: "prompt-cache-breakpoint",
feature: "request.prompt_cache_breakpoint",
disposition: "unsupported",
summary: "Client-only prompt cache breakpoint markers do not reach the forward backend.",
limitation: "Markers are removed recursively from input within bounded depth and node budgets.",
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-breakpoint-removed"] }],
},
{
id: "unstored-item-reference",
feature: "continuation.item_reference",
disposition: "degraded",
summary: "Unpersisted item references are omitted from store-false continuations.",
limitation: "The referenced item cannot exist at the backend when store is false; tool call_id pairs remain unchanged.",
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["unstored-item-reference-removed"] }],
},
{
id: "prompt-cache-retention",
feature: "request.prompt_cache_retention",
Expand Down
21 changes: 21 additions & 0 deletions structure/11_compatibility-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ Compatibility manifests are passive data. The Responses request path, router, an
not import them. A future `ocx compatibility explain` or GUI reader may load the catalog on demand,
but adding a manifest must not activate Compatibility Lab or alter dispatch behavior.

## Canonical forward continuation extensions

The canonical ChatGPT Codex forward boundary removes client-only
`prompt_cache_breakpoint` properties from `input` recursively. The traversal is bounded by depth
and node count; exceeding either bound leaves the marker-bearing input unchanged instead of
publishing a partially transformed continuation. When the request explicitly sets `store: false`,
top-level `item_reference` input rows are omitted because the destination cannot resolve state that
it did not persist. Function and tool-result `call_id` pairs and `reasoning.effort` remain intact.

This is destination-scoped compatibility behavior. Key-auth public Responses providers and custom
forward gateways keep both extensions unchanged because their contracts may accept or interpret
them independently.

[Decision Log]
- 목적과 의도: Preserve Posit Assistant tool continuation semantics while preventing canonical ChatGPT Codex forwarding from sending client-only cache markers or unresolvable stored-item references.
- 기존 구현 및 제약 조건: The existing `store: false` sanitizer removed item ids but left `item_reference` shells, and no bounded pass recognized markers nested inside content; tool `call_id` pairing and reasoning effort are continuation-critical.
- 검토한 주요 대안: Strip the extensions for every Responses destination; delete only reference ids; expand references from local state; or normalize only the canonical forward destination with bounded recursive marker removal.
- 선택한 방식: Apply the bounded marker pass only to canonical forward `input`, and omit `item_reference` rows only when `store` is exactly `false`.
- 다른 대안 대신 이 방식을 선택한 이유: Public and custom gateways may implement these extensions, while id-only deletion creates an invalid reference shell and local expansion would invent unavailable persistence authority.
- 장점, 단점 및 영향: Posit continuations retain tool pairing and reasoning controls without widening public-provider behavior; hostile nesting fails closed to the original input, so an over-limit request may still be rejected upstream rather than partially rewritten.

[Decision Log]
- 목적과 의도: Make provider compatibility explicit and machine-readable before larger routing or Responses refactors.
- 기존 구현 및 제약 조건: Adapter-wide conformance tests already protect tool translation, and Compatibility Lab owns broader protocol evidence, but neither publishes an exact provider/destination/auth/model claim table. Lab must remain outside the ordinary request import graph.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@
{
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "ping" }]
"content": [{
"type": "input_text",
"text": "ping",
"prompt_cache_breakpoint": { "type": "ephemeral" }
}]
},
{
"type": "item_reference",
"id": "rs_unpersisted"
}
],
"instructions": "Fixture instructions",
Expand Down Expand Up @@ -79,6 +87,8 @@
}
},
{ "id": "truncation-removed", "operator": "absent", "path": "/body/truncation" },
{ "id": "prompt-cache-breakpoint-removed", "operator": "absent", "path": "/body/input/0/content/0/prompt_cache_breakpoint" },
{ "id": "unstored-item-reference-removed", "operator": "absent", "path": "/body/input/1" },
{ "id": "previous-response-id-removed", "operator": "absent", "path": "/body/previous_response_id" },
{ "id": "prompt-cache-key-preserved", "operator": "equals", "path": "/body/prompt_cache_key", "expected": "project-cache-v1" },
{ "id": "prompt-cache-retention-removed", "operator": "absent", "path": "/body/prompt_cache_retention" }
Expand Down
134 changes: 134 additions & 0 deletions tests/responses-forward-posit-continuation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, expect, test } from "bun:test";
import { createResponsesPassthroughAdapter as createProductionAdapter } from "../src/adapters/openai-responses";
import { parseRequest } from "../src/responses/parser";
import type { OcxProviderConfig } from "../src/types";
import { withTestTranslatorBudget } from "./helpers/translator-budget";

const createAdapter = (provider: OcxProviderConfig) =>
withTestTranslatorBudget(createProductionAdapter(provider));

const canonicalForward: OcxProviderConfig = {
adapter: "openai-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
authMode: "forward",
};

function outboundBody(provider: OcxProviderConfig, rawBody: Record<string, unknown>): Record<string, unknown> {
const request = createAdapter(provider).buildRequest(
parseRequest(structuredClone(rawBody)),
{ headers: new Headers({ authorization: "Bearer test-token" }) },
);
try {
return JSON.parse(request.body) as Record<string, unknown>;
} finally {
request.releaseBodyObservation?.();
}
}

function positContinuation(store: boolean): Record<string, unknown> {
return {
model: "gpt-5.6-luna",
store,
stream: true,
reasoning: { effort: "high" },
input: [
{
type: "message",
role: "user",
content: [{
type: "input_text",
text: "continue",
prompt_cache_breakpoint: { type: "ephemeral" },
}],
},
{
type: "item_reference",
id: "rs_unpersisted",
},
{
type: "function_call",
id: "fc_pair",
call_id: "call_pair",
name: "list_files",
arguments: "{}",
},
{
type: "function_call_output",
call_id: "call_pair",
output: [{
type: "input_text",
text: "file.R",
metadata: {
nested: {
prompt_cache_breakpoint: true,
keep: "visible",
},
},
}],
},
],
};
}

describe("canonical ChatGPT forward Posit continuation normalization", () => {
test("removes cache markers and unstored references without changing call_id or reasoning", () => {
const body = outboundBody(canonicalForward, positContinuation(false));
const input = body.input as Array<Record<string, unknown>>;

expect(JSON.stringify(input)).not.toContain("prompt_cache_breakpoint");
expect(input.some(item => item.type === "item_reference")).toBe(false);
expect(input.find(item => item.type === "function_call")?.call_id).toBe("call_pair");
expect(input.find(item => item.type === "function_call_output")?.call_id).toBe("call_pair");
expect(body.reasoning).toEqual({ effort: "high" });
expect(JSON.stringify(input)).toContain('"keep":"visible"');
});

test("keeps item_reference when storage is enabled but still removes client-only markers", () => {
const body = outboundBody(canonicalForward, positContinuation(true));
const input = body.input as Array<Record<string, unknown>>;

expect(input.find(item => item.type === "item_reference")).toEqual({
type: "item_reference",
id: "rs_unpersisted",
});
expect(JSON.stringify(input)).not.toContain("prompt_cache_breakpoint");
});

test.each([
{
name: "key-auth public Responses provider",
provider: {
adapter: "openai-responses",
baseUrl: "https://api.openai.com/v1",
authMode: "key" as const,
apiKey: "test-key",
},
},
{
name: "noncanonical forward gateway",
provider: {
adapter: "openai-responses",
baseUrl: "https://gateway.example/v1",
authMode: "forward" as const,
},
},
])("preserves the continuation extensions for $name", ({ provider }) => {
const body = outboundBody(provider, positContinuation(false));
const serialized = JSON.stringify(body.input);

expect(serialized).toContain("prompt_cache_breakpoint");
expect((body.input as Array<Record<string, unknown>>).some(item => item.type === "item_reference")).toBe(true);
});

test("an over-depth marker subtree is kept atomically instead of partially rewritten", () => {
let nested: Record<string, unknown> = { prompt_cache_breakpoint: true, keep: "deep" };
for (let depth = 0; depth < 70; depth += 1) nested = { nested };
const body = outboundBody(canonicalForward, {
model: "gpt-5.6-luna",
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi", nested }] }],
});

expect(JSON.stringify(body.input)).toContain("prompt_cache_breakpoint");
expect(JSON.stringify(body.input)).toContain('"keep":"deep"');
});
});
Loading