Skip to content
Closed
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/reference/proxy-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ the same serialized request and records the recovery in Logs. Other 400 errors,
custom destinations, cancellations and repeated upload rejections remain failures.
This does not retry filtered model responses or interrupted streams.

## Codex-native ordinary hosted-search history

For ordinary requests to canonical ChatGPT Codex `/responses`, all models (including Sol, Luna and Astra) project unreferenced remaining `web_search_call` history into labeled assistant reference notes. It applies to new threads/forks, cross-model continuations and cache misses/restarts, without diagnostic settings. Only a valid same-scope bridge memo proves an original function call/result that can be restored first; an ID prefix, model name or missing memo cannot determine the issuer. A hosted cell exactly referenced by a same-input `item_reference.id` is retained. `store:false` removes references earlier, and its remaining hosted history can be projected. Tool declarations/Lite `additional_tools`, choices, client call/result pairs, reasoning/native ciphertext, non-hosted order and caller raw history retain their existing contracts.

This is an explicit **lossy history policy**, not a claim that hosted calls are invalid schema or that all upstream errors are fixed. Notes carry untrusted status/action metadata and allowlisted URL/title/type from both action and top-level sources, never instructions or fetched page bodies: 2,048 UTF-16 units per string, 20 entries per list, 65,536 serialized bytes for all projected notes, then an omission note. IDs and unknown fields are dropped from converted cells. HTTP/SSE and WS share the final ordinary builder.

Public OpenAI API and other destinations do not gain ordinary or native-compact projection. Native `/responses/compact` remains its existing endpoint/account-scrub/official opaque-output path and retains structural hosted history. Portable/routed summaries reuse their existing projection trigger and default reference behavior; the shared renderer additionally retains allowlisted top-level sources there. No new retry, source classification or compaction summarizer is introduced.

## Empty search answers

After hosted search, a clean but empty forced-answer pass receives one additional answer
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/proxy-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,11 @@ Anthropic 来源的失败会以 Anthropic 的错误封装呈现,因此该方
重放的推理项携带的 `encrypted_content` 只有生成它的 provider 和凭据才能读取。如果 opencodex 知道该对话上一次由另一个 provider 处理,它会在发送前移除这个 blob,并保留该项的摘要。如果那个 provider 还使用了不同的 endpoint 或凭据,该项的 `rs_…` id 也会被移除,因为它指向新目标无法查到的项。如果 opencodex 无从得知,例如代理重启之后,新目标会拒绝这个 blob:OpenAI 和 Azure OpenAI 返回 `400 invalid_encrypted_content`。此时 opencodex 会去掉上一个 provider 的推理状态(blob 和 `rs_…` id)后只重发一次请求;保留 id 会导致 `Item with id 'rs_…' not found`。

这项恢复适用于所有使用 Responses 协议的 adapter,因此 `openai-responses` 和 `azure-openai` 的行为相同。恢复成功后,该对话在同一目标上的后续轮次会在接下来五分钟内于首次发送前移除这些状态。重发计入请求的常规发送预算。普通的 400 和 429 不会以这种方式重发,5xx 也不会,只有一个狭窄的例外:对于携带加密工具输出的请求,响应体恰好是该解密拒绝的 502 会得到同样的一次重发。第二次拒绝会原样返回给客户端。遇到这种情况,请在目标 provider 上开始新的对话。

## Codex 原生普通请求的搜索历史

发往标准 ChatGPT Codex `/responses` 的普通请求中,所有模型(包括 Sol、Luna、Astra)都会把未被同输入引用的剩余 `web_search_call` 历史转为带标签的 assistant 参考说明。新 thread、fork、跨模型续聊、缓存未命中或重启后都适用,不依赖诊断设置。只有有效同 scope bridge memo 能证明并先恢复原函数调用及结果;ID 前缀、模型名或 memo 缺失不能判断来源。若同输入的 `item_reference.id` 精确指向某 hosted cell,该 cell 保留;`store:false` 会先移除引用,剩余 hosted 历史可投影。工具声明、Lite `additional_tools`、选择、call/result 配对、推理/原生密文、非 hosted 顺序及调用方 raw 历史保持原契约。

这是用户明确选择的**有损历史策略**,不是 hosted schema 非法或所有上游错误已修复的断言。说明标为不可信参考数据,复制 status/action 及 action 或顶层 sources 中允许的 URL/title/type,不是指令或网页正文;每字符串最多 2,048 个 UTF-16 单元、每列表 20 项、全部已投影说明共 65,536 字节,超出的 action 用省略说明替代,转换项不复制 ID 或未知字段。HTTP/SSE 与 WS 共用普通请求的最终 builder。

公开 OpenAI API 及其他目的地不新增普通或 native compact 投影。原生 `/responses/compact` 保持既有端点、账户清理及官方 opaque 输出路径,保留结构化 hosted 历史。portable/routed 摘要仍沿用原有触发条件与默认引用行为;共享 renderer 在这些既有路径中也补充顶层 sources 白名单保留。本次不增加重试、来源分类或新的 compaction summarizer。
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"root": "tests",
"explicit": {
"explicit": { "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses",
"messages-request-id-headers.test.ts": "claude-integration",
"messages-request-id-endpoint.test.ts": "claude-integration",
"anthropic-revoked-token.test.ts": "adapters/anthropic",
Expand Down
21 changes: 14 additions & 7 deletions src/adapters/openai-responses/compaction-search-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const MAX_SEARCH_NOTE_FIELD_CHARS = 2048;
/** Most `queries` or `sources` entries copied from one hosted call. */
export const MAX_SEARCH_NOTE_LIST_ENTRIES = 20;
/**
* Serialized bytes all notes of one summary request may use, the omission note included. Request
* Serialized bytes all projected notes of one request may use, the omission note included. Request
* body limits are off by default, so the projection carries its own bound.
*/
export const MAX_PROJECTED_SEARCH_NOTE_BYTES = 65_536;
Expand Down Expand Up @@ -49,14 +49,19 @@ function noteBytes(note: unknown): number {
}

function omissionNote(count: number): Record<string, unknown> {
return referenceNote(`${count} further hosted web search actions omitted from this summary request.`);
return referenceNote(`${count} further hosted web search actions omitted from this request.`);
}

/** Request-local projection of one hosted call for a summarizer with no hosted tool declarations. */
/** Request-local metadata projection for native Codex replay and portable summaries. */
export function renderCompactionSearchHistory(item: unknown): unknown {
if (!isPlainObject(item) || item.type !== "web_search_call") return item;
// Allowlist action metadata, not opaque state, IDs, or unknown provider fields.
const metadata: Record<string, unknown> = stringFields(item, ["status"]);
if (Array.isArray(item.sources)) {
metadata.sources = firstEntries(item.sources, source => isPlainObject(source) && typeof source.url === "string"
? stringFields(source, ["type", "url", "title"])
: undefined);
}
if (isPlainObject(item.action)) {
const action: Record<string, unknown> = stringFields(item.action, ["type", "query", "url", "pattern"]);
if (Array.isArray(item.action.queries)) {
Expand All @@ -73,21 +78,23 @@ export function renderCompactionSearchHistory(item: unknown): unknown {
}

/**
* Project every hosted call in a summary request's input, in place order, within
* Project every hosted call in the selected input, in place order, within
* MAX_PROJECTED_SEARCH_NOTE_BYTES. Once the next note would cross the budget less the reserved
* omission note, that call and every later hosted call are dropped, and one omission note stands at
* the first dropped position. Items other than hosted calls pass through untouched.
*/
export function renderCompactionSearchHistoryItems(items: readonly unknown[]): unknown[] {
const hostedCount = items.filter(item => isPlainObject(item) && item.type === "web_search_call").length;
export function renderCompactionSearchHistoryItems(items: readonly unknown[], referencedHostedIds?: ReadonlySet<string>): unknown[] {
const shouldProject = (item: unknown) => isPlainObject(item) && item.type === "web_search_call"
&& !(typeof item.id === "string" && referencedHostedIds?.has(item.id));
const hostedCount = items.filter(shouldProject).length;
if (hostedCount === 0) return [...items];
const reserve = noteBytes(omissionNote(hostedCount));
const out: unknown[] = [];
let used = 0;
let omitted = 0;
let omissionIndex = -1;
for (const item of items) {
if (!isPlainObject(item) || item.type !== "web_search_call") {
if (!shouldProject(item)) {
out.push(item);
continue;
}
Expand Down
10 changes: 10 additions & 0 deletions src/adapters/openai-responses/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
// here, on the serialized body, not on the parsed selector. One place covers both the
// HTTP and the WebSocket outbound, because the WS path transports this same request
// instead of rebuilding it.
// Codex-native history policy: replay remaining hosted actions as untrusted metadata,
// after bridge restoration/tool validation. Tool declarations and other input stay intact.
if (url === `${CODEX_FORWARD_BASE_URL}/responses`
&& !parsed._compactionRequest && !parsed._portableCompaction
&& isPlainObject(finalBody) && Array.isArray(finalBody.input)
&& finalBody.input.some(item => isPlainObject(item) && item.type === "web_search_call")) {
const referencedIds = new Set(finalBody.input.flatMap(item => isPlainObject(item)
&& item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
finalBody = { ...finalBody, input: renderCompactionSearchHistoryItems(finalBody.input, referencedIds) };
}
observeOutbound(parsed._rawBody, finalBody, headers);
if (!isCanonicalOpenAiForwardProvider(provider)) {
validateFinalCustomToolCompatibility(finalBody, provider.supportsResponsesCustomTools);
Expand Down
2 changes: 2 additions & 0 deletions structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ The shared Responses path follows the [bounded multipart recovery contract](suba

## Hosted-search continuation binding

The Codex-native ordinary-history policy in `src/adapters/openai-responses/passthrough.ts` projects unreferenced remaining `web_search_call` cells through `src/adapters/openai-responses/compaction-search-history.ts` after bridge restoration, query backfill and schema normalization. Exact canonical ChatGPT `/responses` opts in for every model and thread, independent of diagnostic controls; only a same-scope bridge memo proves a restorable call/result, never an id prefix, model name or cache miss. Same-input `item_reference.id` matches retain their hosted cell; `store:false` removes references earlier. Notes are untrusted metadata, not instructions/page content: action and top-level `sources` copy only url/title/type, strings cap at 2,048 UTF-16 units, lists at 20, projected notes at 65,536 bytes with an omission note. Tool declarations/Lite wrappers, choices, non-hosted order, call/results, reasoning and caller raw input keep their contracts. Native `/responses/compact` stays unchanged, including account scrubbing and opaque output; portable/routed summaries keep their existing triggers/default projection (now preserving allowlisted top-level sources), not the ordinary reference exemption. `tests/adapters/openai/openai-native-hosted-search-history.test.ts` and `tests/responses/responses-native-compact-search-history.test.ts` pin these boundaries. This is an explicit lossy policy, not issuer inference, retry, or a claim that Codex rejects valid hosted schema.

The opt-in key-auth Responses hosted-search bridge in `src/server/responses/passthrough-delivery.ts` captures the
request binding that served the first leg, after any permitted initial reselection. Before every
continuation dispatch, after provider pacing, that binding must remain an API-key selection matching
Expand Down
2 changes: 1 addition & 1 deletion structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ converted. A retained previous-response ID does not override an explicit custom-
> Decision record: [ADR-0052](../decisions/ADR-0052-reasoning-and-tool-result-compatibility.md)

## Declared hosted-tool denials

The all-model Codex ordinary unreferenced history projection follows [hosted-search continuation binding](../providers-and-adapters.md#hosted-search-continuation-binding): it changes historical cells, not hosted tool declarations or action capability.
A gateway that speaks the Responses API does not necessarily accept everything OpenAI accepts.
`unsupportedHostedTools` is how such a destination says so: it names the hosted tool declarations
this provider rejects, and `stripUnsupportedHostedTools` in
Expand Down
2 changes: 2 additions & 0 deletions structure/transports/byte-accounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,5 @@ Dashboard Fast-row persistence and client refresh follow the [Fast selector rows
The [compaction routing override](responses-failover.md#compaction-routing-overrides) changes model and effort scalars on the already-read request body, before parsing, within the existing body-reader budget.

`src/lib/sse-decoder.ts` recognizes CR, LF and CRLF line endings, including CRLF split between fetch chunks. Delimiters are consumed before field retention; event/comment ordering, EOF dispatch and translator-budget release remain shared across all three forms. The delimiter search keeps native `indexOf` cursors for the next CR and LF in each decoded chunk, so scanning stays linear for every framing.

Canonical Codex ordinary unreferenced history notes reuse `src/adapters/openai-responses/compaction-search-history.ts` and its 65,536-byte projection cap, included in final passthrough serialization accounting; original raw history and all non-hosted input remain untouched. The budget counts only actually projected cells; same-input referenced hosted cells do not consume the ordinary notes budget. Top-level and action sources share the existing per-field/list allowlists and total bound.
2 changes: 1 addition & 1 deletion structure/transports/responses-failover.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ skips `/responses/compact`, and `request-prepare.ts` sets `parsed._portableCompa
`request-sidecar-auth.ts` (`routedCompaction`) and the passthrough adapter's compaction body
build both honor for canonical ChatGPT destinations. Native ciphertext is replayable only by the backend that minted it; the conversation model would otherwise resume with an omission marker in place of its history.

The portable Responses summarizer in `src/adapters/openai-responses/passthrough.ts` uses `src/adapters/openai-responses/compaction-search-history.ts` to render remaining top-level `web_search_call` items, after bridge restoration, as assistant reference notes labeled untrusted historical metadata (not instructions or fetched page content). The allowlist keeps string status and action fields (type, query/queries, URL, pattern, source URL/title/type); opaque state, IDs, unknown fields and malformed values are omitted. Strings are cut at 2048 code units, lists at 20 entries, and one request carries at most 64 KiB of notes; later hosted cells collapse into a single omission note reserved inside that budget. Tool declarations stay absent, bridge-restored call/result pairs and existing messages/citations stay intact, and only the summary request changes, never stored history, ordinary turns or native compaction. A failed summary publishes no replacement history. Lite requests retain `parallel_tool_calls=false`, which the upstream requires even without tool declarations.
The portable Responses summarizer in `src/adapters/openai-responses/passthrough.ts` uses `src/adapters/openai-responses/compaction-search-history.ts` to render remaining top-level `web_search_call` items, after bridge restoration, as assistant reference notes labeled untrusted historical metadata (not instructions or fetched page content). The allowlist keeps string status and action fields (type, query/queries, URL, pattern, source URL/title/type); opaque state, IDs, unknown fields and malformed values are omitted. Strings are cut at 2048 code units, lists at 20 entries, and one request carries at most 64 KiB of notes; later hosted cells collapse into a single omission note reserved inside that budget. Tool declarations stay absent, bridge-restored call/result pairs and existing messages/citations stay intact, and this portable path changes only compact summary input, never stored history or native compaction. Ordinary Codex continuation policy is separate: see [hosted-search-continuation](../providers-and-adapters.md#hosted-search-continuation-binding). A failed summary publishes no replacement history. Lite requests retain `parallel_tool_calls=false`, which the upstream requires even without tool declarations.

`tests/responses/responses-compaction-override.test.ts` covers source filtering, trigger selection, config
validation, native and routed handlers, credential retention, portable summaries and replay, and combo failover.
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses-wire-shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ does not set `modelResponsesUpstreamStreaming`: client `stream: true` remains re
streaming until a current-runtime reproduction justifies a separate bounded-JSON compatibility
policy.

Go's non-forward Responses request path moves valid `additional_tools` wrappers into top-level
All-model Codex ordinary unreferenced hosted-history lowering follows [the hosted-search continuation boundary](../providers-and-adapters.md#hosted-search-continuation-binding); native compact and public API retain structural history; portable compaction keeps its existing trigger and default projection. Go's non-forward Responses request path moves valid `additional_tools` wrappers into top-level
`tools` through `src/adapters/opencode-go-additional-tools.ts`. Placement runs after existing
custom/search/namespace lowering and before code-mode, compaction and final hosted-tool pruning.
It does not recalculate wire identities or response aliases. The matcher reads the constructed
Expand Down
Loading
Loading