diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 82792df1af..5f143a03b1 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -45,6 +45,7 @@ export interface RegistryModel { supportsVision?: boolean; supportsXHighEffort?: boolean; targetFormat?: string; + strip?: readonly string[]; unsupportedParams?: readonly string[]; /** Maximum context window in tokens */ contextLength?: number; diff --git a/open-sse/services/contextManager.ts b/open-sse/services/contextManager.ts index 6b09d4dd19..ec155b4f66 100644 --- a/open-sse/services/contextManager.ts +++ b/open-sse/services/contextManager.ts @@ -323,7 +323,7 @@ function fixToolPairs(messages: Record[]) { if (Array.isArray(newMsg.tool_calls)) { const filteredToolCalls = newMsg.tool_calls.filter( - (tc: any) => !tc.id || toolResultIds.has(tc.id) + (tc: Record) => !tc.id || toolResultIds.has(tc.id) ); if (filteredToolCalls.length !== newMsg.tool_calls.length) { newMsg.tool_calls = filteredToolCalls; @@ -333,7 +333,8 @@ function fixToolPairs(messages: Record[]) { if (Array.isArray(newMsg.content)) { const filteredContent = newMsg.content.filter( - (block: any) => block.type !== "tool_use" || !block.id || toolResultIds.has(block.id) + (block: Record) => + block.type !== "tool_use" || !block.id || toolResultIds.has(block.id) ); if (filteredContent.length !== newMsg.content.length) { newMsg.content = filteredContent; @@ -374,7 +375,7 @@ function fixToolPairs(messages: Record[]) { if (msg.role === "user" && Array.isArray(msg.content)) { const filteredContent = msg.content.filter( - (block: any) => + (block: Record) => block.type !== "tool_result" || !block.tool_use_id || toolCallIds.has(block.tool_use_id) ); if (filteredContent.length !== msg.content.length) { diff --git a/src/app/api/internal/codex-responses-ws/route.ts b/src/app/api/internal/codex-responses-ws/route.ts index eda29b1382..078cee518b 100644 --- a/src/app/api/internal/codex-responses-ws/route.ts +++ b/src/app/api/internal/codex-responses-ws/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { createHash, timingSafeEqual } from "node:crypto"; +import { z } from "zod"; import { CodexExecutor } from "@omniroute/open-sse/executors/codex.ts"; import { getApiKeyMetadata } from "@/lib/db/apiKeys"; import { authorizeWebSocketHandshake, extractWsTokenFromRequest } from "@/lib/ws/handshake"; @@ -12,6 +13,15 @@ const executor = new CodexExecutor(); type JsonRecord = Record; +const bridgePayloadSchema = z + .object({ + action: z.string().optional(), + requestUrl: z.string().optional(), + headers: z.record(z.string(), z.unknown()).optional(), + response: z.record(z.string(), z.unknown()).optional(), + }) + .passthrough(); + function isRecord(value: unknown): value is JsonRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -179,8 +189,11 @@ export async function POST(request: Request) { let body: JsonRecord; try { - const parsed = await request.json(); - body = isRecord(parsed) ? parsed : {}; + const parsed = bridgePayloadSchema.safeParse(await request.json()); + if (!parsed.success) { + return jsonError(400, "invalid_json", "Request body must be a JSON object"); + } + body = parsed.data as JsonRecord; } catch { return jsonError(400, "invalid_json", "Request body must be JSON"); } diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 3dbddf3deb..16d7b177f3 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -403,3 +403,25 @@ test("Codex internal websocket bridge secret comparison handles mismatched lengt assert.equal(bridgeSecretMatches("bridge-secret", "bridge-secret-extra"), false); assert.equal(bridgeSecretMatches("bridge-secret", ""), false); }); + +test("Codex internal websocket bridge rejects non-object JSON payloads", async () => { + await withEnv({ OMNIROUTE_WS_BRIDGE_SECRET: "bridge-secret" }, async () => { + const { POST } = await import("../../src/app/api/internal/codex-responses-ws/route.ts"); + + const response = await POST( + new Request("http://omniroute.local/api/internal/codex-responses-ws", { + method: "POST", + headers: { + "content-type": "application/json", + "x-omniroute-ws-bridge-secret": "bridge-secret", + }, + body: JSON.stringify(["invalid"]), + }) + ); + const body = await response.json(); + + assert.equal(response.status, 400); + assert.equal(body.error.code, "invalid_json"); + assert.match(body.error.message, /JSON object/); + }); +});