From 15d3fd83bb78df15bfb5b3423744545dce2e1ac7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 5 May 2026 00:25:49 +0100 Subject: [PATCH] fix(openai-codex): match codex replay identity --- CHANGELOG.md | 2 +- docs/reference/transcript-hygiene.md | 2 +- extensions/openai/transport-policy.test.ts | 4 ++-- extensions/openai/transport-policy.ts | 11 ----------- src/agents/openai-transport-stream.test.ts | 16 +++++++++++----- src/agents/openai-transport-stream.ts | 13 ++++++++++--- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad07570479..9607f0905e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -403,7 +403,7 @@ Docs: https://docs.openclaw.ai - Plugins/config: deduplicate identical manifest compatibility diagnostics when an explicitly configured plugin overrides another discovered candidate, so external channel plugins do not print the same missing `channelConfigs` warning repeatedly during install and enable. Thanks @vincentkoc. - Discord/status: honor explicit `messages.statusReactions.enabled: true` in tool-only guild channels so queued ack reactions can progress through thinking/done lifecycle reactions instead of stopping at the initial emoji. Thanks @Marvinthebored. - Discord/native commands: compare Discord-normalized slash-command descriptions and localized descriptions during reconcile so CJK or multiline command text no longer triggers redundant startup PATCH bursts and rate-limit 429s. Fixes #76587. Thanks @zhengsx. -- Agents/OpenAI Codex: scope ChatGPT Codex Responses request identity to each turn, strip the unsupported native Codex `prompt_cache_key`, and avoid replaying prior Responses reasoning/message/function item IDs so tool-call turns do not feed stale state into later Telegram replies. Refs #76413. +- Agents/OpenAI Codex: align ChatGPT Codex Responses replay with the Codex wire contract by preserving session cache identity while omitting prior Responses reasoning/message/function item IDs, so tool-call turns do not feed stale item identity into later Telegram replies. Refs #76413. - Agents/OpenAI: omit Chat Completions `reasoning_effort` for `gpt-5.4-mini` only when function tools are present while preserving tool-free Chat and Responses reasoning support, preventing Telegram-routed fallback runs from hanging after OpenAI rejects tool payloads. Fixes #76176. Thanks @ThisIsAdilah and @chinar-amrutkar. - Telegram: reuse the successful startup `getMe` probe for grammY polling startup and continue into `getUpdates` after recoverable `deleteWebhook` cleanup failures, reducing high-latency Bot API control-plane calls before long polling starts. Refs #76388. Thanks @jackiedepp. - Gateway/diagnostics: merge session id/key aliases in diagnostic session state and activity tracking so completed runs no longer leave stale queued work behind that keeps liveness samples at warning level. diff --git a/docs/reference/transcript-hygiene.md b/docs/reference/transcript-hygiene.md index c710ef35c7a..0233f49e2aa 100644 --- a/docs/reference/transcript-hygiene.md +++ b/docs/reference/transcript-hygiene.md @@ -117,7 +117,7 @@ inter-session user turns that only have provenance metadata. - Image sanitization only. - Drop orphaned reasoning signatures (standalone reasoning items without a following content block) for OpenAI Responses/Codex transcripts, and drop replayable OpenAI reasoning after a model route switch. - Preserve replayable OpenAI Responses reasoning item payloads, including encrypted empty-summary items, so manual/WebSocket replay keeps required `rs_*` state paired with assistant output items. -- Native ChatGPT Codex Responses is the exception: OpenClaw does not replay prior Responses reasoning/message/function item IDs or session `prompt_cache_key` to avoid stale backend replay across turns. +- Native ChatGPT Codex Responses follows Codex wire parity by replaying prior Responses reasoning/message/function payloads without prior item IDs while preserving session `prompt_cache_key`. - No tool call id sanitization. - Tool result pairing repair may move real matched outputs and synthesize Codex-style `aborted` outputs for missing tool calls. - No turn validation or reordering. diff --git a/extensions/openai/transport-policy.test.ts b/extensions/openai/transport-policy.test.ts index 2cddd1f70f7..89f9accc7f4 100644 --- a/extensions/openai/transport-policy.test.ts +++ b/extensions/openai/transport-policy.test.ts @@ -67,7 +67,7 @@ describe("openai transport policy", () => { ).toBeUndefined(); }); - it("uses turn-scoped request identity for ChatGPT Codex stream turns", () => { + it("keeps Codex request identity session-scoped while adding turn metadata", () => { expect( resolveOpenAITransportTurnState({ provider: "openai-codex", @@ -85,7 +85,7 @@ describe("openai transport policy", () => { }), ).toMatchObject({ headers: { - "x-client-request-id": "turn-123", + "x-client-request-id": "session-123", "x-openclaw-session-id": "session-123", "x-openclaw-turn-id": "turn-123", "x-openclaw-turn-attempt": "2", diff --git a/extensions/openai/transport-policy.ts b/extensions/openai/transport-policy.ts index 50f9a710943..cd69858ee77 100644 --- a/extensions/openai/transport-policy.ts +++ b/extensions/openai/transport-policy.ts @@ -46,13 +46,6 @@ function usesKnownNativeOpenAIRoute(provider: string, baseUrl?: string): boolean return false; } -function usesNativeOpenAICodexRoute(provider: string, baseUrl?: string): boolean { - const normalizedProvider = normalizeProviderId(provider); - return ( - normalizedProvider === OPENAI_CODEX_PROVIDER_ID && (!baseUrl || isOpenAICodexBaseUrl(baseUrl)) - ); -} - function resolveSessionHeaders(params: { provider: string; baseUrl?: string; @@ -85,14 +78,10 @@ export function resolveOpenAITransportTurnState( const turnId = normalizeIdentityValue(ctx.turnId); const attempt = String(Math.max(1, ctx.attempt)); - const requestId = usesNativeOpenAICodexRoute(ctx.provider, ctx.model?.baseUrl) - ? turnId || `${sessionHeaders["x-openclaw-session-id"] ?? "session"}:${attempt}` - : sessionHeaders["x-client-request-id"]; return { headers: { ...sessionHeaders, - "x-client-request-id": requestId, "x-openclaw-turn-id": turnId, "x-openclaw-turn-attempt": attempt, }, diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index cde72f1e6be..8ab0b6afe13 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -1020,7 +1020,7 @@ describe("openai transport stream", () => { expect(params.max_output_tokens).toBe(65_536); }); - it("uses top-level instructions for Codex responses and strips unsupported ChatGPT params", () => { + it("uses top-level instructions for Codex responses and preserves prompt cache identity", () => { const params = buildOpenAIResponsesParams( { id: "gpt-5.4", @@ -1059,7 +1059,7 @@ describe("openai transport stream", () => { expect(params.input?.some((item) => item.role === "system" || item.role === "developer")).toBe( false, ); - expect(params).not.toHaveProperty("prompt_cache_key"); + expect(params.prompt_cache_key).toBe("session-123"); expect(params.store).toBe(false); expect(params).not.toHaveProperty("metadata"); expect(params).not.toHaveProperty("max_output_tokens"); @@ -1068,7 +1068,7 @@ describe("openai transport stream", () => { expect(params).not.toHaveProperty("temperature"); }); - it("sanitizes Codex responses params after payload hooks mutate them", () => { + it("sanitizes Codex responses params after payload hooks mutate them without stripping cache identity", () => { const payload = { model: "gpt-5.4", input: [], @@ -1097,7 +1097,7 @@ describe("openai transport stream", () => { payload, ); - expect(sanitized).not.toHaveProperty("prompt_cache_key"); + expect(sanitized.prompt_cache_key).toBe("session-123"); expect(sanitized).not.toHaveProperty("metadata"); expect(sanitized).not.toHaveProperty("max_output_tokens"); expect(sanitized).not.toHaveProperty("prompt_cache_retention"); @@ -1257,10 +1257,16 @@ describe("openai transport stream", () => { id?: string; call_id?: string; phase?: string; + encrypted_content?: string; }>; }; - expect(params.input?.some((item) => item.type === "reasoning")).toBe(false); + const reasoningItem = params.input?.find((item) => item.type === "reasoning"); + expect(reasoningItem).toMatchObject({ + type: "reasoning", + encrypted_content: "ciphertext", + }); + expect(reasoningItem?.id).toBeUndefined(); const assistantMessage = params.input?.find( (item) => item.type === "message" && item.role === "assistant", ); diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 7c8acb12cb4..f83fdb36bec 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -20,6 +20,7 @@ import type { ResponseInputItem, ResponseInputMessageContentList, ResponseOutputMessage, + ResponseReasoningItem, } from "openai/resources/responses/responses.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -60,6 +61,7 @@ const OPENAI_CODEX_RESPONSES_EMPTY_INPUT_TEXT = " "; const log = createSubsystemLogger("openai-transport"); type ReplayableResponseOutputMessage = Omit & { id?: string }; +type ReplayableResponseReasoningItem = Omit & { id?: string }; type BaseStreamOptions = { temperature?: number; @@ -299,7 +301,13 @@ function convertResponsesMessages( for (const block of msg.content) { if (block.type === "thinking") { if (shouldReplayReasoningItems && block.thinkingSignature) { - output.push(JSON.parse(block.thinkingSignature)); + const reasoningItem = JSON.parse( + block.thinkingSignature, + ) as ReplayableResponseReasoningItem; + if (!shouldReplayResponsesItemIds) { + delete reasoningItem.id; + } + output.push(reasoningItem as ResponseInputItem); } } else if (block.type === "text") { const textSignature = parseTextSignature(block.textSignature); @@ -927,7 +935,6 @@ function usesNativeOpenAICodexResponsesBackend(model: Model): boolean { const OPENAI_CODEX_RESPONSES_UNSUPPORTED_PARAMS = [ "max_output_tokens", "metadata", - "prompt_cache_key", "prompt_cache_retention", "service_tier", "temperature", @@ -987,7 +994,7 @@ export function buildOpenAIResponsesParams( { includeSystemPrompt: !isCodexResponses, supportsDeveloperRole, - replayReasoningItems: !isNativeCodexResponses, + replayReasoningItems: true, replayResponsesItemIds: !isNativeCodexResponses, }, );