Files
metapi/src/server/proxy-core/cliProfiles/codexProfile.ts
T

210 lines
6.3 KiB
TypeScript

import type { CliProfileDefinition, DetectCliProfileInput } from './types.js';
type CodexOfficialClientApp = {
clientAppId: string;
clientAppName: string;
};
const CODEX_OFFICIAL_CLIENT_USER_AGENT_PREFIXES = [
'codex_cli_rs/',
'codex_vscode/',
'codex_app/',
'codex_chatgpt_desktop/',
'codex_atlas/',
'codex_exec/',
'codex_sdk_ts/',
'codex ',
];
const CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES = [
'codex_',
'codex ',
];
const CODEX_OFFICIAL_CLIENT_APP_RULES = [
{
id: 'codex_cli_rs',
name: 'Codex CLI',
userAgentPrefixes: ['codex_cli_rs/'],
originatorPrefixes: ['codex_cli_rs'],
},
{
id: 'codex_vscode',
name: 'Codex VSCode',
userAgentPrefixes: ['codex_vscode/'],
originatorPrefixes: ['codex_vscode'],
},
{
id: 'codex_app',
name: 'Codex App',
userAgentPrefixes: ['codex_app/'],
originatorPrefixes: ['codex_app'],
},
{
id: 'codex_chatgpt_desktop',
name: 'Codex Desktop',
userAgentPrefixes: ['codex_chatgpt_desktop/', 'codex desktop/'],
originatorPrefixes: ['codex_chatgpt_desktop', 'codex desktop'],
},
{
id: 'codex_atlas',
name: 'Codex Atlas',
userAgentPrefixes: ['codex_atlas/'],
originatorPrefixes: ['codex_atlas'],
},
{
id: 'codex_exec',
name: 'Codex Exec',
userAgentPrefixes: ['codex_exec/'],
originatorPrefixes: ['codex_exec'],
},
{
id: 'codex_sdk_ts',
name: 'Codex SDK TS',
userAgentPrefixes: ['codex_sdk_ts/'],
originatorPrefixes: ['codex_sdk_ts'],
},
] as const;
function headerValueToStrings(value: unknown): string[] {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? [trimmed] : [];
}
if (Array.isArray(value)) {
const values: string[] = [];
for (const item of value) {
if (typeof item !== 'string') continue;
const trimmed = item.trim();
if (trimmed) values.push(trimmed);
}
return values;
}
return [];
}
function headerValueToString(value: unknown): string | null {
return headerValueToStrings(value)[0] || null;
}
function getHeaderValue(headers: Record<string, unknown> | undefined, targetKey: string): string | null {
return getHeaderValues(headers, targetKey)[0] || null;
}
function getHeaderValues(headers: Record<string, unknown> | undefined, targetKey: string): string[] {
if (!headers) return [];
const normalizedTarget = targetKey.trim().toLowerCase();
const values: string[] = [];
for (const [rawKey, rawValue] of Object.entries(headers)) {
if (rawKey.trim().toLowerCase() !== normalizedTarget) continue;
values.push(...headerValueToStrings(rawValue));
}
return values;
}
function hasHeaderPrefix(headers: Record<string, unknown> | undefined, prefix: string): boolean {
if (!headers) return false;
const normalizedPrefix = prefix.trim().toLowerCase();
return Object.entries(headers).some(([rawKey, rawValue]) => {
const key = rawKey.trim().toLowerCase();
return key.startsWith(normalizedPrefix) && !!headerValueToString(rawValue);
});
}
function matchesHeaderPrefixes(value: string | string[] | null, prefixes: readonly string[]): boolean {
const values = Array.isArray(value)
? value.map((item) => item.trim().toLowerCase()).filter(Boolean)
: [value?.trim().toLowerCase() || ''].filter(Boolean);
if (values.length === 0) return false;
return values.some((normalizedValue) => prefixes.some((prefix) => {
const normalizedPrefix = prefix.trim().toLowerCase();
if (!normalizedPrefix) return false;
return normalizedValue.startsWith(normalizedPrefix)
|| normalizedValue.includes(normalizedPrefix);
}));
}
function isCodexPath(path: string): boolean {
const normalizedPath = path.trim().toLowerCase();
return normalizedPath.startsWith('/v1/responses')
|| normalizedPath === '/v1/chat/completions'
|| normalizedPath.startsWith('/v1/messages');
}
export function detectCodexOfficialClientApp(
headers?: Record<string, unknown>,
): CodexOfficialClientApp | null {
for (const rule of CODEX_OFFICIAL_CLIENT_APP_RULES) {
const matchesOriginator = matchesHeaderPrefixes(getHeaderValues(headers, 'originator'), rule.originatorPrefixes);
const matchesUserAgent = matchesHeaderPrefixes(getHeaderValues(headers, 'user-agent'), rule.userAgentPrefixes);
if (!matchesOriginator && !matchesUserAgent) continue;
return {
clientAppId: rule.id,
clientAppName: rule.name,
};
}
return null;
}
export function isCodexResponsesSurface(headers?: Record<string, unknown>): boolean {
return isCodexRequest({
downstreamPath: '/v1/responses',
headers,
});
}
export function getCodexSessionId(headers?: Record<string, unknown>): string | null {
return getHeaderValue(headers, 'session_id') || getHeaderValue(headers, 'session-id');
}
export function isCodexRequest(input: DetectCliProfileInput): boolean {
if (!isCodexPath(input.downstreamPath)) return false;
const headers = input.headers;
if (!headers) return false;
const originator = getHeaderValues(headers, 'originator');
if (matchesHeaderPrefixes(originator, CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES)) return true;
if (matchesHeaderPrefixes(getHeaderValues(headers, 'user-agent'), CODEX_OFFICIAL_CLIENT_USER_AGENT_PREFIXES)) return true;
if (getHeaderValue(headers, 'openai-beta')) return true;
if (hasHeaderPrefix(headers, 'x-stainless-')) return true;
if (getCodexSessionId(headers)) return true;
if (getHeaderValue(headers, 'x-codex-turn-state')) return true;
return false;
}
export const codexCliProfile: CliProfileDefinition = {
id: 'codex',
capabilities: {
supportsResponsesCompact: true,
supportsResponsesWebsocketIncremental: true,
preservesContinuation: true,
supportsCountTokens: false,
echoesTurnState: true,
},
detect(input) {
if (!isCodexRequest(input)) return null;
const sessionId = getCodexSessionId(input.headers) || undefined;
const clientApp = detectCodexOfficialClientApp(input.headers);
return {
id: 'codex',
...(sessionId ? { sessionId, traceHint: sessionId } : {}),
...(clientApp
? {
clientAppId: clientApp.clientAppId,
clientAppName: clientApp.clientAppName,
clientConfidence: 'exact' as const,
}
: {
clientAppId: 'codex',
clientAppName: 'Codex',
clientConfidence: 'heuristic' as const,
}),
};
},
};