mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 12:22:14 +03:00
fix(copilot): stabilize responses configuration (#2579)
Integrated into release/v3.8.2
This commit is contained in:
@@ -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` |
|
||||
|
||||
@@ -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<string, unknown>): void {
|
||||
}
|
||||
}
|
||||
|
||||
function buildRecoveredToolContextMessage(
|
||||
droppedItems: Array<Record<string, unknown>>
|
||||
): Record<string, unknown> {
|
||||
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<string, unknown>): 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<string>();
|
||||
const inputFunctionCallOutputIds = new Set<string>();
|
||||
|
||||
for (const item of Array.isArray(body.input) ? body.input : []) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const record = item as Record<string, unknown>;
|
||||
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<string>();
|
||||
const finalFunctionCallOutputIds = new Set<string>();
|
||||
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<string, unknown>;
|
||||
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<Record<string, unknown>> = [];
|
||||
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<string, unknown>;
|
||||
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.
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
import { sanitizeResponsesInputItems } from "./responsesInputSanitizer.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
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<string, RememberedResponseToolState>();
|
||||
const rememberedFunctionCallsById = new Map<string, RememberedFunctionCallByIdState>();
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<string, unknown> {
|
||||
test.afterEach(() => {
|
||||
setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
|
||||
__setCodexWebSocketTransportForTesting(undefined);
|
||||
clearRememberedResponseFunctionCallsForTesting();
|
||||
});
|
||||
|
||||
async function withEnv<T>(entries: Record<string, string | undefined>, fn: () => T | Promise<T>) {
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user