fix(openai-codex): match codex replay identity

This commit is contained in:
Peter Steinberger
2026-05-05 00:25:49 +01:00
parent 27e467ad23
commit 15d3fd83bb
6 changed files with 25 additions and 23 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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",

View File

@@ -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,
},

View File

@@ -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",
);

View File

@@ -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<ResponseOutputMessage, "id"> & { id?: string };
type ReplayableResponseReasoningItem = Omit<ResponseReasoningItem, "id"> & { 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<Api>): 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,
},
);