From 8f0a4c1adeac71d47d227fcdcd2b90d424df040e Mon Sep 17 00:00:00 2001 From: ivan-mezentsev Date: Sat, 23 May 2026 00:15:31 +0300 Subject: [PATCH] fix(copilot): stabilize responses configuration (#2579) Integrated into release/v3.8.2 --- docs/architecture/CODEBASE_DOCUMENTATION.md | 2 +- open-sse/executors/codex.ts | 173 --------- open-sse/services/responsesToolCallState.ts | 228 ----------- open-sse/utils/stream.ts | 23 -- .../cli-tools/components/CopilotToolCard.tsx | 21 +- tests/unit/codex-stream-false.test.ts | 77 ---- tests/unit/executor-codex.test.ts | 367 +----------------- 7 files changed, 26 insertions(+), 865 deletions(-) delete mode 100644 open-sse/services/responsesToolCallState.ts diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md index 130e6ea14a..76adff538f 100644 --- a/docs/architecture/CODEBASE_DOCUMENTATION.md +++ b/docs/architecture/CODEBASE_DOCUMENTATION.md @@ -493,7 +493,7 @@ Highlights (full list under `open-sse/services/`): | Routing intelligence | `intentClassifier.ts`, `taskAwareRouter.ts`, `backgroundTaskDetector.ts`, `volumeDetector.ts`, `wildcardRouter.ts`, `workflowFSM.ts`, `specificityDetector.ts`, `specificityRules.ts`, `specificityTypes.ts` | | Model handling | `modelCapabilities.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`, `modelStrip.ts`, `model.ts`, `provider.ts`, `providerRequestDefaults.ts`, `providerCostData.ts`, `payloadRules.ts` | | Compression | `compression/` — full compression engine wiring | -| Token + session | `tokenRefresh.ts`, `sessionManager.ts`, `apiKeyRotator.ts`, `contextManager.ts`, `contextHandoff.ts`, `systemPrompt.ts`, `roleNormalizer.ts`, `responsesInputSanitizer.ts`, `responsesToolCallState.ts`, `toolSchemaSanitizer.ts`, `toolLimitDetector.ts`, `thinkingBudget.ts` | +| Token + session | `tokenRefresh.ts`, `sessionManager.ts`, `apiKeyRotator.ts`, `contextManager.ts`, `contextHandoff.ts`, `systemPrompt.ts`, `roleNormalizer.ts`, `responsesInputSanitizer.ts`, `toolSchemaSanitizer.ts`, `toolLimitDetector.ts`, `thinkingBudget.ts` | | Tier / manifest | `tierResolver.ts`, `tierConfig.ts`, `tierDefaults.json`, `tierTypes.ts`, `manifestAdapter.ts` | | IP / network | `ipFilter.ts`, `webSearchFallback.ts` | | Batches | `batchProcessor.ts` | diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 8ca45d32eb..70c596d55d 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -24,11 +24,6 @@ import { type CodexClientIdentity, } from "../config/codexIdentity.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; -import { - getRememberedFunctionCallsByIds, - getRememberedResponseConversationItems, - getRememberedResponseFunctionCalls, -} from "../services/responsesToolCallState.ts"; import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts"; import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts"; import { CORS_HEADERS } from "../utils/cors.ts"; @@ -310,23 +305,6 @@ function convertSystemToDeveloperRole(body: Record): void { } } -function buildRecoveredToolContextMessage( - droppedItems: Array> -): Record { - return { - type: "message", - role: "user", - content: [ - { - type: "input_text", - text: - "Recovered tool context from the previous turn. Continue using this context instead of calling the same tools again unless you must.\n" + - JSON.stringify(droppedItems), - }, - ], - }; -} - /** * Strip server-generated item IDs from the input array. * @@ -343,156 +321,8 @@ function buildRecoveredToolContextMessage( * 3. Strips the "id" field from any object in input whose id matches a * server-generated prefix (rs_, fc_, resp_, msg_) — so the content is * preserved but the backend won't try to look it up - * 4. Expands locally remembered conversation snapshots for stateful follow-ups - * when the upstream backend rejects previous_response_id - * 5. Falls back to rehydrating missing function_call items if only the older - * tool-call state is available - * 6. Filters orphaned function_call/function_call_output items when one side - * of the tool exchange is still missing after local replay/fallback repair */ function stripStoredItemReferences(body: Record): void { - const hasInput = Array.isArray(body.input) && body.input.length > 0; - const inputItems = Array.isArray(body.input) ? body.input : []; - const previousResponseId = - typeof body.previous_response_id === "string" ? body.previous_response_id : ""; - const rememberedConversationItems = - hasInput && previousResponseId - ? getRememberedResponseConversationItems(previousResponseId) - : []; - - if (rememberedConversationItems.length > 0) { - body.input = [...rememberedConversationItems, ...inputItems]; - } - const inputFunctionCallIds = new Set(); - const inputFunctionCallOutputIds = new Set(); - - for (const item of Array.isArray(body.input) ? body.input : []) { - if (!item || typeof item !== "object" || Array.isArray(item)) continue; - const record = item as Record; - const type = typeof record.type === "string" ? record.type : ""; - const callId = typeof record.call_id === "string" ? record.call_id : ""; - if (!callId) continue; - if (type === "function_call") { - inputFunctionCallIds.add(callId); - continue; - } - if (type === "function_call_output") { - inputFunctionCallOutputIds.add(callId); - } - } - - const missingFunctionCallIds = [...inputFunctionCallOutputIds].filter( - (callId) => !inputFunctionCallIds.has(callId) - ); - - if (hasInput && previousResponseId && missingFunctionCallIds.length > 0) { - const rememberedFunctionCalls = getRememberedResponseFunctionCalls(previousResponseId); - const globallyRememberedFunctionCalls = getRememberedFunctionCallsByIds(missingFunctionCallIds); - const injectedFunctionCalls = [...rememberedFunctionCalls, ...globallyRememberedFunctionCalls] - .filter((functionCall) => missingFunctionCallIds.includes(functionCall.call_id)) - .filter((functionCall) => !inputFunctionCallIds.has(functionCall.call_id)) - .filter( - (functionCall, index, allFunctionCalls) => - allFunctionCalls.findIndex((candidate) => candidate.call_id === functionCall.call_id) === - index - ) - .map((functionCall) => ({ - type: "function_call", - call_id: functionCall.call_id, - name: - typeof functionCall.name === "string" - ? functionCall.name.slice(0, 128) - : functionCall.name, - arguments: functionCall.arguments, - })); - - if (injectedFunctionCalls.length > 0) { - body.input = [...injectedFunctionCalls, ...inputItems]; - for (const functionCall of injectedFunctionCalls) { - inputFunctionCallIds.add(functionCall.call_id); - } - } - } - - const finalFunctionCallIds = new Set(); - const finalFunctionCallOutputIds = new Set(); - if (Array.isArray(body.input)) { - for (const item of body.input) { - if (!item || typeof item !== "object" || Array.isArray(item)) continue; - const record = item as Record; - const type = typeof record.type === "string" ? record.type : ""; - const callId = typeof record.call_id === "string" ? record.call_id : ""; - if (!callId) continue; - if (type === "function_call") { - finalFunctionCallIds.add(callId); - continue; - } - if (type === "function_call_output") { - finalFunctionCallOutputIds.add(callId); - } - } - } - - const droppedOrphanFunctionCallIds: string[] = []; - const droppedOrphanFunctionCallOutputIds: string[] = []; - const droppedOrphanItems: Array> = []; - if (Array.isArray(body.input)) { - body.input = body.input.filter((item) => { - if (!item || typeof item !== "object" || Array.isArray(item)) { - return true; - } - - const record = item as Record; - const callId = typeof record.call_id === "string" ? record.call_id : ""; - if (!callId) { - return true; - } - - if (record.type === "function_call") { - if (finalFunctionCallOutputIds.has(callId)) { - return true; - } - - droppedOrphanFunctionCallIds.push(callId); - droppedOrphanItems.push({ ...record }); - return false; - } - - if (record.type === "function_call_output") { - if (finalFunctionCallIds.has(callId)) { - return true; - } - - droppedOrphanFunctionCallOutputIds.push(callId); - droppedOrphanItems.push({ ...record }); - return false; - } - - return true; - }); - } - - if (droppedOrphanFunctionCallIds.length > 0) { - console.warn( - `[Codex] stripStoredItemReferences: dropped ${droppedOrphanFunctionCallIds.length} orphan function_call item(s): ${droppedOrphanFunctionCallIds.join(", ")}` - ); - } - - if (droppedOrphanFunctionCallOutputIds.length > 0) { - console.warn( - `[Codex] stripStoredItemReferences: dropped ${droppedOrphanFunctionCallOutputIds.length} orphan function_call_output item(s): ${droppedOrphanFunctionCallOutputIds.join(", ")}` - ); - } - - if (Array.isArray(body.input) && body.input.length === 0 && droppedOrphanItems.length > 0) { - body.input = [buildRecoveredToolContextMessage(droppedOrphanItems)]; - console.warn( - `[Codex] stripStoredItemReferences: synthesized recovery message from ${droppedOrphanItems.length} dropped orphan tool item(s)` - ); - } - - // Codex rejects previous_response_id for passthrough requests. - delete body.previous_response_id; if (Array.isArray(body.input) && body.input.length === 0) { body.input = [ { @@ -1413,9 +1243,6 @@ export class CodexExecutor extends BaseExecutor { } delete body.reasoning_effort; - // previous_response_id is expanded into a self-contained local replay when - // input is present because Codex rejects that parameter upstream. - // Remove unsupported token limit parameters BEFORE the passthrough return. // Codex API rejects both max_tokens and max_output_tokens regardless of // whether the request came via native passthrough or translation. diff --git a/open-sse/services/responsesToolCallState.ts b/open-sse/services/responsesToolCallState.ts deleted file mode 100644 index 9ded2ecefe..0000000000 --- a/open-sse/services/responsesToolCallState.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { sanitizeResponsesInputItems } from "./responsesInputSanitizer.ts"; - -type JsonRecord = Record; - -type RememberedFunctionCall = { - call_id: string; - name: string; - arguments: string; -}; - -type RememberedResponseToolState = { - functionCalls: RememberedFunctionCall[]; - conversationItems: unknown[]; - expiresAt: number; - updatedAt: number; -}; - -type RememberedFunctionCallByIdState = RememberedFunctionCall & { - expiresAt: number; - updatedAt: number; -}; - -const RESPONSE_TOOL_CALL_TTL_MS = 30 * 60 * 1000; -const RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES = 512; - -const rememberedResponseToolCalls = new Map(); -const rememberedFunctionCallsById = new Map(); - -function toRecord(value: unknown): JsonRecord | null { - return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null; -} - -function sanitizeRememberedConversationItems(items: readonly unknown[]): unknown[] { - return sanitizeResponsesInputItems(items); -} - -function cleanupRememberedResponseToolCalls(now: number = Date.now()) { - for (const [responseId, entry] of rememberedResponseToolCalls.entries()) { - if (entry.expiresAt <= now) { - rememberedResponseToolCalls.delete(responseId); - } - } - - for (const [callId, entry] of rememberedFunctionCallsById.entries()) { - if (entry.expiresAt <= now) { - rememberedFunctionCallsById.delete(callId); - } - } - - if (rememberedResponseToolCalls.size <= RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES) { - if (rememberedFunctionCallsById.size <= RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES) { - return; - } - } - - if (rememberedResponseToolCalls.size > RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES) { - const oldestEntries = [...rememberedResponseToolCalls.entries()].sort( - (a, b) => a[1].updatedAt - b[1].updatedAt - ); - - while (rememberedResponseToolCalls.size > RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES) { - const oldest = oldestEntries.shift(); - if (!oldest) break; - rememberedResponseToolCalls.delete(oldest[0]); - } - } - - if (rememberedFunctionCallsById.size > RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES) { - const oldestCallEntries = [...rememberedFunctionCallsById.entries()].sort( - (a, b) => a[1].updatedAt - b[1].updatedAt - ); - - while (rememberedFunctionCallsById.size > RESPONSE_TOOL_CALL_CACHE_MAX_ENTRIES) { - const oldest = oldestCallEntries.shift(); - if (!oldest) break; - rememberedFunctionCallsById.delete(oldest[0]); - } - } -} - -export function rememberResponseFunctionCalls( - responseId: unknown, - outputItems: readonly unknown[] -) { - const normalizedResponseId = typeof responseId === "string" ? responseId.trim() : ""; - if (!normalizedResponseId || !Array.isArray(outputItems) || outputItems.length === 0) { - return; - } - - const existingEntry = rememberedResponseToolCalls.get(normalizedResponseId); - - const functionCalls: RememberedFunctionCall[] = []; - - for (const item of outputItems) { - const record = toRecord(item); - if (!record || record.type !== "function_call") continue; - - const callId = typeof record.call_id === "string" ? record.call_id.trim() : ""; - const name = typeof record.name === "string" ? record.name.trim() : ""; - const argumentsValue = - typeof record.arguments === "string" - ? record.arguments - : JSON.stringify(record.arguments ?? {}); - - if (!callId || !name) continue; - - functionCalls.push({ - call_id: callId, - name, - arguments: argumentsValue, - }); - } - - if (functionCalls.length === 0) { - return; - } - - cleanupRememberedResponseToolCalls(); - - const now = Date.now(); - for (const functionCall of functionCalls) { - rememberedFunctionCallsById.set(functionCall.call_id, { - ...functionCall, - updatedAt: now, - expiresAt: now + RESPONSE_TOOL_CALL_TTL_MS, - }); - } - - rememberedResponseToolCalls.set(normalizedResponseId, { - functionCalls, - conversationItems: sanitizeRememberedConversationItems(existingEntry?.conversationItems || []), - updatedAt: now, - expiresAt: now + RESPONSE_TOOL_CALL_TTL_MS, - }); -} - -export function rememberResponseConversationState( - responseId: unknown, - requestInput: readonly unknown[], - outputItems: readonly unknown[] -) { - const normalizedResponseId = typeof responseId === "string" ? responseId.trim() : ""; - if (!normalizedResponseId) { - return; - } - - const normalizedRequestInput = Array.isArray(requestInput) ? requestInput : []; - const normalizedOutputItems = Array.isArray(outputItems) ? outputItems : []; - const conversationItems = sanitizeRememberedConversationItems([ - ...normalizedRequestInput, - ...normalizedOutputItems, - ]); - if (conversationItems.length === 0) { - return; - } - - cleanupRememberedResponseToolCalls(); - - const existingEntry = rememberedResponseToolCalls.get(normalizedResponseId); - rememberedResponseToolCalls.set(normalizedResponseId, { - functionCalls: existingEntry?.functionCalls?.map((functionCall) => ({ ...functionCall })) || [], - conversationItems, - updatedAt: Date.now(), - expiresAt: Date.now() + RESPONSE_TOOL_CALL_TTL_MS, - }); -} - -export function getRememberedResponseFunctionCalls(responseId: unknown): RememberedFunctionCall[] { - cleanupRememberedResponseToolCalls(); - - const normalizedResponseId = typeof responseId === "string" ? responseId.trim() : ""; - if (!normalizedResponseId) { - return []; - } - - const entry = rememberedResponseToolCalls.get(normalizedResponseId); - if (!entry) { - return []; - } - - return entry.functionCalls.map((functionCall) => ({ ...functionCall })); -} - -export function getRememberedResponseConversationItems(responseId: unknown): unknown[] { - cleanupRememberedResponseToolCalls(); - - const normalizedResponseId = typeof responseId === "string" ? responseId.trim() : ""; - if (!normalizedResponseId) { - return []; - } - - const entry = rememberedResponseToolCalls.get(normalizedResponseId); - if (!entry) { - return []; - } - - return sanitizeRememberedConversationItems(entry.conversationItems); -} - -export function getRememberedFunctionCallsByIds( - callIds: readonly string[] -): RememberedFunctionCall[] { - cleanupRememberedResponseToolCalls(); - - if (!Array.isArray(callIds) || callIds.length === 0) { - return []; - } - - const remembered: RememberedFunctionCall[] = []; - for (const rawCallId of callIds) { - const callId = typeof rawCallId === "string" ? rawCallId.trim() : ""; - if (!callId) continue; - const entry = rememberedFunctionCallsById.get(callId); - if (!entry) continue; - remembered.push({ - call_id: entry.call_id, - name: entry.name, - arguments: entry.arguments, - }); - } - - return remembered; -} - -export function clearRememberedResponseFunctionCallsForTesting() { - rememberedResponseToolCalls.clear(); - rememberedFunctionCallsById.clear(); -} diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index cda5a84a4d..062cb808bb 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -28,10 +28,6 @@ import { sanitizeStreamingChunk, extractThinkingFromContent, } from "../handlers/responseSanitizer.ts"; -import { - rememberResponseConversationState, - rememberResponseFunctionCalls, -} from "../services/responsesToolCallState.ts"; import { buildErrorBody } from "./error.ts"; /** @@ -1586,25 +1582,6 @@ export function createSSEStream(options: StreamOptions = {}) { } clearPendingPassthroughEvent(); - if (passthroughResponsesId) { - const requestInput = - body && typeof body === "object" && Array.isArray((body as JsonRecord).input) - ? ((body as JsonRecord).input as unknown[]) - : []; - rememberResponseConversationState( - passthroughResponsesId, - requestInput, - passthroughResponsesOutputItems - ); - } - - if (passthroughResponsesId && passthroughResponsesOutputItems.length > 0) { - rememberResponseFunctionCalls( - passthroughResponsesId, - passthroughResponsesOutputItems - ); - } - // Estimate usage if provider didn't return valid usage if (!hasValidUsage(usage) && totalContentLength > 0) { usage = estimateUsage(body, totalContentLength, sourceFormat || FORMATS.OPENAI); diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.tsx index 605537a188..f4740005bb 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.tsx @@ -128,6 +128,18 @@ export default function CopilotToolCard({ maxOutputTokens, })); + const responseModels = [...selectedModels].map((modelId) => ({ + id: modelId, + name: modelId, + url: `${baseUrl}/v1/responses#models.ai.azure.com`, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh"], + zeroDataRetentionEnabled: true, + toolCalling, + vision, + maxInputTokens, + maxOutputTokens, + })); + const config = { name: "OmniRoute", vendor: "azure", @@ -135,7 +147,14 @@ export default function CopilotToolCard({ models, }; - return JSON.stringify(config, null, 2); + const responsesConfig = { + name: "OmniRoute-responses", + vendor: "azure", + apiKey: `\${input:chat.lm.secret.omniroute}`, + models: responseModels, + }; + + return [config, responsesConfig].map((entry) => JSON.stringify(entry, null, 2)).join(",\n"); }; const handleCopy = async (text: string, field: string) => { diff --git a/tests/unit/codex-stream-false.test.ts b/tests/unit/codex-stream-false.test.ts index 8cb8ed214a..50d2988d1b 100644 --- a/tests/unit/codex-stream-false.test.ts +++ b/tests/unit/codex-stream-false.test.ts @@ -11,11 +11,6 @@ const core = await import("../../src/lib/db/core.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); const { handleComboChat } = await import("../../open-sse/services/combo.ts"); const { CodexExecutor } = await import("../../open-sse/executors/codex.ts"); -const { - clearRememberedResponseFunctionCallsForTesting, - getRememberedResponseConversationItems, - rememberResponseFunctionCalls, -} = await import("../../open-sse/services/responsesToolCallState.ts"); const originalFetch = globalThis.fetch; @@ -192,87 +187,15 @@ async function invokeChatCore({ test.beforeEach(async () => { globalThis.fetch = originalFetch; - clearRememberedResponseFunctionCallsForTesting(); await resetStorage(); }); test.after(async () => { globalThis.fetch = originalFetch; - clearRememberedResponseFunctionCallsForTesting(); await resetStorage(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -test("chatCore remembers Codex Responses conversation state from transformed body, not raw client input", async () => { - rememberResponseFunctionCalls("resp_prev_tool_123", [ - { - type: "function_call", - call_id: "call_tool_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }, - ]); - - const { result } = await invokeChatCore({ - endpoint: "/v1/responses", - accept: "text/event-stream", - provider: "codex", - model: "gpt-5.5-low", - body: { - model: "gpt-5.5-low", - previous_response_id: "resp_prev_tool_123", - input: [ - { - type: "function_call_output", - call_id: "call_tool_123", - output: '{"ok":true}', - }, - ], - }, - responseFactory: () => - new Response( - [ - "event: response.created", - 'data: {"type":"response.created","response":{"id":"resp_current_456","model":"gpt-5.5-low","status":"in_progress","output":[]}}', - "", - "event: response.completed", - 'data: {"type":"response.completed","response":{"id":"resp_current_456","object":"response","model":"gpt-5.5-low","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"done"}]}],"usage":{"input_tokens":6,"output_tokens":1}}}', - "", - "data: [DONE]", - "", - ].join("\n"), - { - status: 200, - headers: { "Content-Type": "text/event-stream" }, - } - ), - }); - - assert.equal(result.success, true); - await result.response.text(); - await waitForAsyncSideEffects(); - - const rememberedItems = getRememberedResponseConversationItems("resp_current_456"); - const rememberedFunctionCall = rememberedItems.find( - (item) => item && typeof item === "object" && item.type === "function_call" - ); - const rememberedFunctionCallOutput = rememberedItems.find( - (item) => item && typeof item === "object" && item.type === "function_call_output" - ); - - assert.deepEqual(rememberedFunctionCall, { - type: "function_call", - call_id: "call_tool_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }); - assert.deepEqual(rememberedFunctionCallOutput, { - type: "function_call_output", - call_id: "call_tool_123", - output: '{"ok":true}', - }); -}); - test("CodexExecutor.transformRequest clones the request body before forcing stream=true", () => { const executor = new CodexExecutor(); const body = { diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 1bbd7ce015..975cd1902a 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -12,11 +12,6 @@ import { isCodexResponsesWebSocketRequired, parseCodexQuotaHeaders, } from "../../open-sse/executors/codex.ts"; -import { - clearRememberedResponseFunctionCallsForTesting, - rememberResponseConversationState, - rememberResponseFunctionCalls, -} from "../../open-sse/services/responsesToolCallState.ts"; import { DEFAULT_THINKING_CONFIG, setThinkingBudgetConfig, @@ -42,7 +37,6 @@ function getRecord(value: unknown): Record { test.afterEach(() => { setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); __setCodexWebSocketTransportForTesting(undefined); - clearRememberedResponseFunctionCallsForTesting(); }); async function withEnv(entries: Record, fn: () => T | Promise) { @@ -294,7 +288,7 @@ test("CodexExecutor.transformRequest preserves compact requests and native passt assert.equal(result.instructions, "keep this"); }); -test("CodexExecutor.transformRequest preserves store-enabled responses state when explicitly enabled", () => { +test("CodexExecutor.transformRequest preserves previous_response_id without local handling", () => { const executor = new CodexExecutor(); const body = { _nativeCodexPassthrough: true, @@ -314,7 +308,7 @@ test("CodexExecutor.transformRequest preserves store-enabled responses state whe assert.equal(result._omnirouteResponsesStore, undefined); assert.equal(result.store, true); - assert.equal(result.previous_response_id, undefined); + assert.equal(result.previous_response_id, "resp_prev_123"); }); test("CodexExecutor.transformRequest strips store from compact requests even when store is enabled", () => { const executor = new CodexExecutor(); @@ -340,185 +334,6 @@ test("CodexExecutor.transformRequest strips store from compact requests even whe assert.equal(result.instructions, "keep this"); }); -test("CodexExecutor.transformRequest expands remembered conversation state for stateful tool outputs", () => { - const executor = new CodexExecutor(); - rememberResponseConversationState( - "resp_prev_tool_123", - [ - { - type: "message", - role: "user", - content: [ - { - type: "input_text", - text: "Read README.md and summarize it.", - }, - ], - }, - ], - [ - { - type: "function_call", - call_id: "call_tool_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }, - ] - ); - const body = { - _nativeCodexPassthrough: true, - previous_response_id: "resp_prev_tool_123", - input: [ - { - type: "function_call_output", - call_id: "call_tool_123", - output: '{"ok":true}', - }, - ], - stream: false, - }; - - const result = executor.transformRequest("gpt-5.5-low", body, false, { - requestEndpointPath: "/responses", - }); - - assert.equal(result.previous_response_id, undefined); - assert.equal(result.store, false); - assert.equal(result.input.length, 3); - assert.deepEqual(result.input[0], { - type: "message", - role: "user", - content: [ - { - type: "input_text", - text: "Read README.md and summarize it.", - }, - ], - }); - assert.deepEqual(result.input[1], { - type: "function_call", - call_id: "call_tool_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }); - assert.deepEqual(result.input[2], { - type: "function_call_output", - call_id: "call_tool_123", - output: '{"ok":true}', - }); -}); - -test("CodexExecutor.transformRequest does not replay internal assistant commentary", () => { - const executor = new CodexExecutor(); - rememberResponseConversationState( - "resp_prev_commentary_123", - [ - { - type: "message", - role: "user", - content: [{ type: "input_text", text: "Summarize the previous result." }], - }, - { - role: "assistant", - phase: "commentary", - content: [{ type: "output_text", text: "Need inspect raw tool output first." }], - }, - { - type: "message", - role: "assistant", - content: [{ type: "output_text", text: "Visible assistant answer." }], - }, - ], - [ - { - type: "function_call", - call_id: "call_safe_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }, - ] - ); - - const body = { - _nativeCodexPassthrough: true, - previous_response_id: "resp_prev_commentary_123", - input: [ - { - type: "function_call_output", - call_id: "call_safe_123", - output: '{"ok":true}', - }, - ], - stream: false, - }; - - const result = executor.transformRequest("gpt-5.5-low", body, false, { - requestEndpointPath: "/responses", - }); - - assert.equal(result.previous_response_id, undefined); - assert.equal(result.input.length, 4); - assert.equal( - result.input.some((item) => JSON.stringify(item).includes("Need inspect raw tool output")), - false - ); - assert.equal(result.input[0].role, "user"); - assert.equal(result.input[1].role, "assistant"); - assert.equal(result.input[2].type, "function_call"); - assert.equal(result.input[3].type, "function_call_output"); -}); - -test("CodexExecutor.transformRequest preserves replayed assistant final_answer messages", () => { - const executor = new CodexExecutor(); - rememberResponseConversationState( - "resp_prev_final_answer_123", - [ - { - type: "message", - role: "user", - content: [{ type: "input_text", text: "9+10?" }], - }, - { - type: "message", - role: "assistant", - phase: "final_answer", - content: [{ type: "output_text", text: "19" }], - }, - ], - [] - ); - - const result = executor.transformRequest( - "gpt-5.5-low", - { - _nativeCodexPassthrough: true, - previous_response_id: "resp_prev_final_answer_123", - input: [ - { - type: "message", - role: "user", - content: [{ type: "input_text", text: "did you answered?" }], - }, - ], - stream: false, - }, - false, - { requestEndpointPath: "/responses" } - ); - - assert.equal( - result.input.some((item) => JSON.stringify(item).includes('"text":"19"')), - true - ); - assert.equal( - result.input.some((item) => { - if (!item || typeof item !== "object" || Array.isArray(item)) return false; - return item.role === "assistant" && item.phase === "final_answer"; - }), - true - ); -}); - test("CodexExecutor.transformRequest strips raw internal assistant commentary without dropping useful Responses items", () => { const executor = new CodexExecutor(); const body = { @@ -645,16 +460,8 @@ test("CodexExecutor.transformRequest strips internal assistant commentary before assert.equal(result.messages, undefined); }); -test("CodexExecutor.transformRequest rehydrates missing function_call items for stateful tool outputs", () => { +test("CodexExecutor.transformRequest does not locally replay previous_response_id tool follow-ups", () => { const executor = new CodexExecutor(); - rememberResponseFunctionCalls("resp_prev_tool_123", [ - { - type: "function_call", - call_id: "call_tool_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }, - ]); const body = { _nativeCodexPassthrough: true, previous_response_id: "resp_prev_tool_123", @@ -672,179 +479,15 @@ test("CodexExecutor.transformRequest rehydrates missing function_call items for requestEndpointPath: "/responses", }); - assert.equal(result.previous_response_id, undefined); + assert.equal(result.previous_response_id, "resp_prev_tool_123"); assert.equal(result.store, false); + assert.equal(result.input.length, 1); assert.deepEqual(result.input[0], { - type: "function_call", - call_id: "call_tool_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }); - assert.deepEqual(result.input[1], { type: "function_call_output", call_id: "call_tool_123", output: '{"ok":true}', }); }); - -test("CodexExecutor.transformRequest filters orphaned function_call_output items after replay repair", () => { - const executor = new CodexExecutor(); - const body = { - _nativeCodexPassthrough: true, - previous_response_id: "resp_prev_orphan_123", - input: [ - { - type: "function_call", - call_id: "call_valid_123", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }, - { - type: "function_call_output", - call_id: "call_valid_123", - output: '{"ok":true}', - }, - { - type: "function_call_output", - call_id: "call_orphan_123", - output: '{"stale":true}', - }, - ], - stream: false, - }; - - const result = executor.transformRequest("gpt-5.5-low", body, false, { - requestEndpointPath: "/responses", - }); - - assert.equal(result.previous_response_id, undefined); - assert.equal(result.input.filter((item) => item.type === "function_call_output").length, 1); - assert.equal( - result.input.find((item) => item.type === "function_call_output")?.call_id, - "call_valid_123" - ); -}); - -test("CodexExecutor.transformRequest filters orphaned function_call items after replay repair", () => { - const executor = new CodexExecutor(); - const body = { - _nativeCodexPassthrough: true, - previous_response_id: "resp_prev_orphan_call_123", - input: [ - { - type: "function_call", - call_id: "call_valid_456", - name: "workspace_read_file", - arguments: '{"path":"README.md"}', - }, - { - type: "function_call_output", - call_id: "call_valid_456", - output: '{"ok":true}', - }, - { - type: "function_call", - call_id: "call_orphan_456", - name: "workspace_search", - arguments: '{"query":"Authorization"}', - }, - ], - stream: false, - }; - - const result = executor.transformRequest("gpt-5.5-low", body, false, { - requestEndpointPath: "/responses", - }); - - assert.equal(result.previous_response_id, undefined); - assert.equal(result.input.filter((item) => item.type === "function_call").length, 1); - assert.equal( - result.input.find((item) => item.type === "function_call")?.call_id, - "call_valid_456" - ); -}); - -test("CodexExecutor.transformRequest synthesizes a recovery message when orphan cleanup empties input", () => { - const executor = new CodexExecutor(); - const body = { - _nativeCodexPassthrough: true, - conversation_id: "conv_empty_after_cleanup", - session_id: "sess_empty_after_cleanup", - previous_response_id: "resp_prev_empty_after_cleanup", - input: [ - { - type: "function_call", - call_id: "call_orphan_only_1", - name: "workspace_search", - arguments: '{"query":"auth"}', - }, - { - type: "function_call_output", - call_id: "call_orphan_only_2", - output: '{"matches":1}', - }, - ], - stream: false, - }; - - const result = executor.transformRequest("gpt-5.5-low", body, false, { - requestEndpointPath: "/responses", - }); - - assert.equal(result.previous_response_id, undefined); - assert.equal(result.conversation_id, undefined); - assert.equal(result.session_id, undefined); - assert.equal(result.prompt_cache_key, "sess_empty_after_cleanup"); - assert.equal(Array.isArray(result.input), true); - assert.equal(result.input.length, 1); - assert.deepEqual(result.input[0].type, "message"); - assert.deepEqual(result.input[0].role, "user"); - assert.match(result.input[0].content[0].text, /Recovered tool context from the previous turn/); - assert.match(result.input[0].content[0].text, /call_orphan_only_1/); - assert.match(result.input[0].content[0].text, /call_orphan_only_2/); -}); - -test("CodexExecutor.transformRequest repairs orphan function_call_output from global remembered call_id cache", () => { - const executor = new CodexExecutor(); - rememberResponseFunctionCalls("resp_older_tool_123", [ - { - type: "function_call", - call_id: "call_old_123", - name: "workspace_search", - arguments: '{"query":"Authorization"}', - }, - ]); - - const body = { - _nativeCodexPassthrough: true, - previous_response_id: "resp_prev_without_that_call", - input: [ - { - type: "function_call_output", - call_id: "call_old_123", - output: '{"matches":1}', - }, - ], - stream: false, - }; - - const result = executor.transformRequest("gpt-5.5-low", body, false, { - requestEndpointPath: "/responses", - }); - - assert.equal(result.previous_response_id, undefined); - assert.deepEqual(result.input[0], { - type: "function_call", - call_id: "call_old_123", - name: "workspace_search", - arguments: '{"query":"Authorization"}', - }); - assert.deepEqual(result.input[1], { - type: "function_call_output", - call_id: "call_old_123", - output: '{"matches":1}', - }); -}); test("CodexExecutor.transformRequest applies per-connection reasoning and service tier defaults", () => { const executor = new CodexExecutor(); const result = executor.transformRequest(