Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion open-sse/config/codexClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const DEFAULT_CODEX_CLIENT_VERSION = "0.125.0";
const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26100";
const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26200";
const DEFAULT_CODEX_USER_AGENT_ARCH = "x64";
const CODEX_VERSION_OVERRIDE_ENV = "CODEX_CLIENT_VERSION";
const CODEX_USER_AGENT_OVERRIDE_ENV = "CODEX_USER_AGENT";
const SAFE_HEADER_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
const SAFE_HEADER_VALUE_PATTERN = /^[\x20-\x7E]{1,200}$/;
const SAFE_CODEX_SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;

function getSafeEnvValue(name: string, pattern: RegExp): string | null {
const raw = process.env[name];
Expand Down Expand Up @@ -40,3 +41,9 @@ export function getCodexDefaultHeaders(): Record<string, string> {
"User-Agent": getCodexUserAgent(),
};
}

export function normalizeCodexSessionId(value: unknown): string | null {
if (typeof value !== "string") return null;
const normalized = value.trim();
return SAFE_CODEX_SESSION_ID_PATTERN.test(normalized) ? normalized : null;
}
83 changes: 83 additions & 0 deletions open-sse/config/codexIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createHash, randomUUID } from "node:crypto";

import { normalizeCodexSessionId } from "./codexClient.ts";

const CODEX_INSTALLATION_SALT = "omniroute-codex-installation";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

export type CodexClientIdentity = {
sessionId: string;
turnId: string;
windowId: string;
installationId: string;
};

function normalizeUuid(value: unknown): string | null {
return typeof value === "string" && UUID_PATTERN.test(value.trim()) ? value.trim() : null;
}

function uuidFromStableValue(value: string): string {
const hash = createHash("sha256").update(value).digest("hex");
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
}

export function getCodexInstallationId(providerSpecificData?: Record<string, unknown> | null): string {
const explicit = normalizeUuid(providerSpecificData?.codexInstallationId);
if (explicit) return explicit;

const stableSource =
typeof providerSpecificData?.workspaceId === "string" && providerSpecificData.workspaceId.trim()
? providerSpecificData.workspaceId.trim()
: typeof providerSpecificData?.accountId === "string" && providerSpecificData.accountId.trim()
? providerSpecificData.accountId.trim()
: typeof providerSpecificData?.email === "string" && providerSpecificData.email.trim()
? providerSpecificData.email.trim()
: "default";

return uuidFromStableValue(`${CODEX_INSTALLATION_SALT}:${stableSource}`);
}

export function createCodexClientIdentity(
sessionId: string | null,
providerSpecificData?: Record<string, unknown> | null
): CodexClientIdentity | null {
const normalizedSessionId = normalizeCodexSessionId(sessionId);
if (!normalizedSessionId) return null;
return {
sessionId: normalizedSessionId,
turnId: randomUUID(),
windowId: `${normalizedSessionId}:0`,
installationId: getCodexInstallationId(providerSpecificData),
};
}

export function applyCodexClientIdentityHeaders(
headers: Record<string, string>,
identity?: CodexClientIdentity | null
): void {
if (!identity) return;
headers["session_id"] = identity.sessionId;
headers["x-client-request-id"] = identity.sessionId;
headers["x-codex-window-id"] = identity.windowId;
headers["x-codex-turn-metadata"] = JSON.stringify({
session_id: identity.sessionId,
thread_source: "user",
turn_id: identity.turnId,
sandbox: "none",
});
}

export function applyCodexClientMetadata(
body: Record<string, unknown>,
identity?: CodexClientIdentity | null
): void {
if (!identity) return;
const existing =
body.client_metadata && typeof body.client_metadata === "object" && !Array.isArray(body.client_metadata)
? (body.client_metadata as Record<string, unknown>)
: {};
body.client_metadata = {
...existing,
"x-codex-installation-id": identity.installationId,
};
}
121 changes: 48 additions & 73 deletions open-sse/executors/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import {
CODEX_DEFAULT_INSTRUCTIONS,
} from "../config/codexInstructions.ts";
import { PROVIDERS } from "../config/constants.ts";
import { getCodexClientVersion, getCodexUserAgent } from "../config/codexClient.ts";
import {
getCodexClientVersion,
getCodexUserAgent,
normalizeCodexSessionId,
} from "../config/codexClient.ts";
import {
applyCodexClientIdentityHeaders,
applyCodexClientMetadata,
createCodexClientIdentity,
} from "../config/codexIdentity.ts";
import { getAccessToken } from "../services/tokenRefresh.ts";
import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
import { CORS_HEADERS } from "../utils/cors.ts";
Expand Down Expand Up @@ -253,68 +262,11 @@ export function getCodexUpstreamModel(model: unknown): string {
return splitCodexReasoningSuffix(model).baseModel;
}

function stringifyCodexInstructionContent(content: unknown): string {
if (typeof content === "string") {
return content.trim();
}

if (Array.isArray(content)) {
return content
.map((part) => {
if (typeof part === "string") return part.trim();
if (!part || typeof part !== "object") return "";
const record = part as Record<string, unknown>;
if (typeof record.text === "string") return record.text.trim();
if (typeof record.content === "string") return record.content.trim();
return "";
})
.filter(Boolean)
.join("\n")
.trim();
}

return "";
}

function hoistSystemMessagesToInstructions(body: Record<string, unknown>): void {
if (!Array.isArray(body.input)) return;

const systemChunks: string[] = [];
const filteredInput = body.input.filter((itemValue) => {
if (!itemValue || typeof itemValue !== "object" || Array.isArray(itemValue)) {
return true;
}

const item = itemValue as Record<string, unknown>;
const role = typeof item.role === "string" ? item.role : "";
const type = typeof item.type === "string" ? item.type : "";
const isSystemMessage = role === "system" && (!type || type === "message");
if (!isSystemMessage) {
return true;
}

const text = stringifyCodexInstructionContent(item.content);
if (text) {
systemChunks.push(text);
}
return false;
});

if (systemChunks.length === 0) return;

const existingInstructions =
typeof body.instructions === "string" ? body.instructions.trim() : "";
body.instructions = existingInstructions
? `${systemChunks.join("\n\n")}\n\n${existingInstructions}`
: systemChunks.join("\n\n");
body.input = filteredInput;
}

/**
* Convert role=system messages in `input` to role=developer.
*
* GPT-5 models support the `developer` role in input, but reject `system`.
* Unlike hoistSystemMessagesToInstructions(), this keeps the content inside
* This keeps the content inside
* the `input` array where it benefits from OpenAI's automatic prompt caching.
*
* OpenAI's prompt caching matches on the serialized prefix of the `input` array
Expand Down Expand Up @@ -711,21 +663,37 @@ export class CodexExecutor extends BaseExecutor {
}

async execute(input: ExecuteInput) {
if (!isCodexResponsesWebSocketRequired(input.model, input.credentials)) {
return super.execute(input);
const sessionId = this.getPromptCacheSessionId(
input.credentials,
input.body as Record<string, unknown> | null
);
const identity = createCodexClientIdentity(sessionId, input.credentials?.providerSpecificData ?? null);
const credentials = identity
? {
...input.credentials,
providerSpecificData: {
...(input.credentials?.providerSpecificData || {}),
codexClientIdentity: identity,
},
}
: input.credentials;
const nextInput = { ...input, credentials };

if (!isCodexResponsesWebSocketRequired(nextInput.model, nextInput.credentials)) {
return super.execute(nextInput);
}

const url = CODEX_RESPONSES_WS_URL;
const headers = normalizeCodexWsHeaders(this.buildHeaders(input.credentials, true));
mergeUpstreamExtraHeaders(headers, input.upstreamExtraHeaders);
const headers = normalizeCodexWsHeaders(this.buildHeaders(nextInput.credentials, true));
mergeUpstreamExtraHeaders(headers, nextInput.upstreamExtraHeaders);

const transformedBody = (await this.transformRequest(
input.model,
input.body,
nextInput.model,
nextInput.body,
true,
input.credentials
nextInput.credentials
)) as Record<string, unknown>;
transformedBody.model = getCodexUpstreamModel(transformedBody.model || input.model);
transformedBody.model = getCodexUpstreamModel(transformedBody.model || nextInput.model);
delete transformedBody.stream;
delete transformedBody.stream_options;

Expand Down Expand Up @@ -760,7 +728,7 @@ export class CodexExecutor extends BaseExecutor {
let abortHandler: (() => void) | null = null;
const removeAbortListener = () => {
if (!abortHandler) return;
input.signal?.removeEventListener("abort", abortHandler);
nextInput.signal?.removeEventListener("abort", abortHandler);
abortHandler = null;
};

Expand Down Expand Up @@ -821,7 +789,7 @@ export class CodexExecutor extends BaseExecutor {
abortHandler = () => {
finishStream({ reason: "client_aborted" });
};
input.signal?.addEventListener("abort", abortHandler, { once: true });
nextInput.signal?.addEventListener("abort", abortHandler, { once: true });

try {
ws = await websocketFn(toWebSocketUrl(url), {
Expand All @@ -830,7 +798,7 @@ export class CodexExecutor extends BaseExecutor {
headers,
});
if (closed) return;
if (input.signal?.aborted) {
if (nextInput.signal?.aborted) {
finishStream({ reason: "client_aborted" });
return;
}
Expand Down Expand Up @@ -928,6 +896,7 @@ export class CodexExecutor extends BaseExecutor {
if (workspaceId) {
headers["chatgpt-account-id"] = workspaceId;
}
const clientIdentity = credentials?.providerSpecificData?.codexClientIdentity;

// Originator header — identifies the client type to the Codex backend.
// Ref: openai/codex login/src/auth/default_client.rs DEFAULT_ORIGINATOR = "codex_cli_rs"
Expand All @@ -940,6 +909,7 @@ export class CodexExecutor extends BaseExecutor {
if (cacheSessionId) {
headers["session_id"] = cacheSessionId;
}
applyCodexClientIdentityHeaders(headers, clientIdentity);

return headers;
}
Expand All @@ -956,13 +926,17 @@ export class CodexExecutor extends BaseExecutor {
credentials,
body: Record<string, unknown> | null
): string | null {
const promptCacheKey = normalizeCodexSessionId(body?.prompt_cache_key);
if (promptCacheKey) return promptCacheKey;

// Prefer per-session identifiers from the client request body
const sessionId = body?.session_id ?? body?.conversation_id;
if (typeof sessionId === "string" && sessionId.length > 0) {
return sessionId;
const normalizedSessionId = normalizeCodexSessionId(sessionId);
if (normalizedSessionId) {
return normalizedSessionId;
}
// Fall back to workspaceId (account-wide) — better than nothing
return credentials?.providerSpecificData?.workspaceId || null;
return normalizeCodexSessionId(credentials?.providerSpecificData?.workspaceId) || null;
}

/**
Expand Down Expand Up @@ -1146,6 +1120,7 @@ export class CodexExecutor extends BaseExecutor {
body.prompt_cache_key = cacheSessionId;
}
}
applyCodexClientMetadata(body, credentials?.providerSpecificData?.codexClientIdentity);

// Delete session_id and conversation_id from the body.
// These are often injected by OmniRoute's fallback logic for store=true,
Expand Down
Loading