refactor: consolidate single-source routing and protocol helpers

This commit is contained in:
Cita
2026-03-22 21:27:09 +08:00
parent 96e7614835
commit 9290e51ecc
64 changed files with 3571 additions and 2681 deletions
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { detectCodexOfficialClientApp } from './codexProfile.js';
describe('detectCodexOfficialClientApp', () => {
it('detects official Codex clients from originator prefixes', () => {
expect(detectCodexOfficialClientApp({
originator: 'codex_exec',
})).toEqual({
clientAppId: 'codex_exec',
clientAppName: 'Codex Exec',
});
});
it('detects official Codex clients from user-agent prefixes', () => {
expect(detectCodexOfficialClientApp({
'user-agent': 'Mozilla/5.0 codex_chatgpt_desktop/1.2.3',
})).toEqual({
clientAppId: 'codex_chatgpt_desktop',
clientAppName: 'Codex Desktop',
});
});
it('returns null for non-official Codex clients', () => {
expect(detectCodexOfficialClientApp({
'user-agent': 'OpenClaw/1.0',
})).toBe(null);
});
});
@@ -1,5 +1,10 @@
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/',
@@ -16,6 +21,51 @@ const CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES = [
'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 headerValueToString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
@@ -73,6 +123,23 @@ function isCodexPath(path: string): boolean {
|| normalizedPath.startsWith('/v1/messages');
}
export function detectCodexOfficialClientApp(
headers?: Record<string, unknown>,
): CodexOfficialClientApp | null {
for (const rule of CODEX_OFFICIAL_CLIENT_APP_RULES) {
const matchesOriginator = Array.isArray(rule.originatorPrefixes)
&& matchesHeaderPrefixes(getHeaderValue(headers, 'originator'), [...rule.originatorPrefixes]);
const matchesUserAgent = Array.isArray(rule.userAgentPrefixes)
&& matchesHeaderPrefixes(getHeaderValue(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',
@@ -1,97 +1,5 @@
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderProfile } from './types.js';
const CLAUDE_DEFAULT_USER_AGENT = 'claude-cli/2.1.63 (external, cli)';
const CLAUDE_DEFAULT_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
function headerValueToString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || null;
}
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item !== 'string') continue;
const trimmed = item.trim();
if (trimmed) return trimmed;
}
}
return null;
}
function getInputHeader(
headers: Record<string, unknown> | Record<string, string> | undefined,
key: string,
): string | null {
if (!headers) return null;
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
return headerValueToString(candidateValue);
}
return null;
}
function mergeClaudeBetaHeader(
explicitValue: string | null,
extraBetas: string[] = [],
): string {
const source = explicitValue || CLAUDE_DEFAULT_BETA_HEADER;
const seen = new Set<string>();
const merged: string[] = [];
for (const entry of source.split(',')) {
const normalized = entry.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
merged.push(normalized);
}
if (!explicitValue) {
for (const entry of extraBetas) {
const normalized = entry.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
merged.push(normalized);
}
}
return merged.join(',');
}
function buildClaudeRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
claudeHeaders: Record<string, string>;
anthropicVersion: string;
stream: boolean;
isClaudeOauthUpstream: boolean;
tokenValue: string;
}): Record<string, string> {
const anthropicBeta = mergeClaudeBetaHeader(
getInputHeader(input.claudeHeaders, 'anthropic-beta'),
);
const headers: Record<string, string> = {
...input.baseHeaders,
...input.claudeHeaders,
'anthropic-version': input.anthropicVersion,
...(anthropicBeta ? { 'anthropic-beta': anthropicBeta } : {}),
'Anthropic-Dangerous-Direct-Browser-Access': 'true',
'X-App': 'cli',
'X-Stainless-Retry-Count': getInputHeader(input.claudeHeaders, 'x-stainless-retry-count') || '0',
'X-Stainless-Runtime-Version': getInputHeader(input.claudeHeaders, 'x-stainless-runtime-version') || 'v24.3.0',
'X-Stainless-Package-Version': getInputHeader(input.claudeHeaders, 'x-stainless-package-version') || '0.74.0',
'X-Stainless-Runtime': getInputHeader(input.claudeHeaders, 'x-stainless-runtime') || 'node',
'X-Stainless-Lang': getInputHeader(input.claudeHeaders, 'x-stainless-lang') || 'js',
'X-Stainless-Arch': getInputHeader(input.claudeHeaders, 'x-stainless-arch') || 'x64',
'X-Stainless-Os': getInputHeader(input.claudeHeaders, 'x-stainless-os') || 'Windows',
'X-Stainless-Timeout': getInputHeader(input.claudeHeaders, 'x-stainless-timeout') || '600',
'User-Agent': getInputHeader(input.claudeHeaders, 'user-agent') || CLAUDE_DEFAULT_USER_AGENT,
Connection: 'keep-alive',
Accept: input.stream ? 'text/event-stream' : 'application/json',
'Accept-Encoding': 'gzip, deflate, br, zstd',
};
if (input.isClaudeOauthUpstream) {
headers.Authorization = `Bearer ${input.tokenValue}`;
} else {
headers['x-api-key'] = input.tokenValue;
}
return headers;
}
import { buildClaudeRuntimeHeaders } from './headerUtils.js';
export const claudeProviderProfile: ProviderProfile = {
id: 'claude',
@@ -1,130 +1,34 @@
import { createHash, randomUUID } from 'node:crypto';
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderProfile } from './types.js';
import { config } from '../../config.js';
const CODEX_CLIENT_VERSION = '0.101.0';
const CODEX_DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
import { buildCodexRuntimeHeaders, getInputHeader } from './headerUtils.js';
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function headerValueToString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || null;
}
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item !== 'string') continue;
const trimmed = item.trim();
if (trimmed) return trimmed;
}
}
return null;
}
function getInputHeader(
headers: Record<string, unknown> | Record<string, string> | undefined,
key: string,
): string | null {
if (!headers) return null;
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
return headerValueToString(candidateValue);
}
return null;
}
function uuidFromSeed(seed: string): string {
const hash = createHash('sha1').update(seed).digest();
const bytes = new Uint8Array(hash.subarray(0, 16));
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join('-');
}
function buildCodexRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
oauthProvider?: string;
explicitSessionId?: string | null;
continuityKey?: string | null;
}): Record<string, string> {
const authorization = (
getInputHeader(input.baseHeaders, 'authorization')
|| getInputHeader(input.baseHeaders, 'Authorization')
|| ''
);
const originator = getInputHeader(input.providerHeaders, 'originator') || 'codex_cli_rs';
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
const version = getInputHeader(input.baseHeaders, 'version') || CODEX_CLIENT_VERSION;
const isCodexOauth = asTrimmedString(input.oauthProvider).toLowerCase() === 'codex';
const configuredUserAgent = isCodexOauth ? asTrimmedString(config.codexHeaderDefaults.userAgent) : '';
const websocketTransport = getInputHeader(input.baseHeaders, 'x-metapi-responses-websocket-transport') === '1';
const configuredBetaFeatures = (
isCodexOauth && websocketTransport
? asTrimmedString(config.codexHeaderDefaults.betaFeatures)
: ''
);
const userAgent = configuredUserAgent || getInputHeader(input.baseHeaders, 'user-agent') || CODEX_DEFAULT_USER_AGENT;
const codexBetaFeatures = getInputHeader(input.baseHeaders, 'x-codex-beta-features') || configuredBetaFeatures;
const codexTurnState = getInputHeader(input.baseHeaders, 'x-codex-turn-state');
const codexTurnMetadata = getInputHeader(input.baseHeaders, 'x-codex-turn-metadata');
const timingMetrics = getInputHeader(input.baseHeaders, 'x-responsesapi-include-timing-metrics');
const openAiBeta = getInputHeader(input.baseHeaders, 'openai-beta')
|| (websocketTransport ? asTrimmedString(config.codexResponsesWebsocketBeta) : null);
const explicitSessionId = asTrimmedString(input.explicitSessionId);
const continuityKey = asTrimmedString(input.continuityKey);
const sessionId = (
getInputHeader(input.baseHeaders, 'session_id')
|| getInputHeader(input.baseHeaders, 'session-id')
|| explicitSessionId
|| (continuityKey ? uuidFromSeed(`metapi:codex:${continuityKey}`) : null)
|| randomUUID()
);
const conversationId = (
getInputHeader(input.baseHeaders, 'conversation_id')
|| getInputHeader(input.baseHeaders, 'conversation-id')
|| explicitSessionId
|| (continuityKey ? sessionId : null)
);
return {
Authorization: authorization,
'Content-Type': 'application/json',
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
Originator: originator,
Version: version,
...(codexBetaFeatures ? { 'x-codex-beta-features': codexBetaFeatures } : {}),
...(codexTurnState ? { 'x-codex-turn-state': codexTurnState } : {}),
...(codexTurnMetadata ? { 'x-codex-turn-metadata': codexTurnMetadata } : {}),
...(timingMetrics ? { 'x-responsesapi-include-timing-metrics': timingMetrics } : {}),
...(openAiBeta ? { 'OpenAI-Beta': openAiBeta } : {}),
Session_id: sessionId,
...(conversationId ? { Conversation_id: conversationId } : {}),
'User-Agent': userAgent,
Accept: 'text/event-stream',
Connection: 'Keep-Alive',
};
}
export const codexProviderProfile: ProviderProfile = {
id: 'codex',
prepareRequest(input: PrepareProviderRequestInput): PreparedProviderRequest {
const isCodexOauth = asTrimmedString(input.oauthProvider).toLowerCase() === 'codex';
const websocketTransport = getInputHeader(input.baseHeaders, 'x-metapi-responses-websocket-transport') === '1';
const configuredUserAgent = isCodexOauth ? asTrimmedString(config.codexHeaderDefaults.userAgent) : '';
const configuredBetaFeatures = (
isCodexOauth && websocketTransport
? asTrimmedString(config.codexHeaderDefaults.betaFeatures)
: ''
);
const headers = buildCodexRuntimeHeaders({
baseHeaders: input.baseHeaders,
providerHeaders: input.providerHeaders,
oauthProvider: input.oauthProvider,
explicitSessionId: asTrimmedString(input.codexExplicitSessionId) || null,
continuityKey: asTrimmedString(input.codexSessionCacheKey) || null,
userAgentOverride: configuredUserAgent || null,
codexBetaFeatures: getInputHeader(input.baseHeaders, 'x-codex-beta-features') || configuredBetaFeatures,
codexTurnState: getInputHeader(input.baseHeaders, 'x-codex-turn-state'),
codexTurnMetadata: getInputHeader(input.baseHeaders, 'x-codex-turn-metadata'),
timingMetrics: getInputHeader(input.baseHeaders, 'x-responsesapi-include-timing-metrics'),
openAiBeta: getInputHeader(input.baseHeaders, 'openai-beta')
|| (websocketTransport ? asTrimmedString(config.codexResponsesWebsocketBeta) : null),
});
const codexSessionId = getInputHeader(headers, 'session_id') || getInputHeader(headers, 'session-id');
const shouldInjectDerivedPromptCacheKey = !!codexSessionId
@@ -1,60 +1,10 @@
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderAction, ProviderProfile } from './types.js';
import { buildGeminiCliRuntimeHeaders } from './headerUtils.js';
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function headerValueToString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || null;
}
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item !== 'string') continue;
const trimmed = item.trim();
if (trimmed) return trimmed;
}
}
return null;
}
function getInputHeader(
headers: Record<string, unknown> | Record<string, string> | undefined,
key: string,
): string | null {
if (!headers) return null;
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
return headerValueToString(candidateValue);
}
return null;
}
function parseGeminiCliUserAgentRuntime(userAgent: string | null): {
version: string;
platform: string;
arch: string;
} | null {
if (!userAgent) return null;
const match = /^GeminiCLI\/([^/]+)\/[^ ]+ \(([^;]+); ([^)]+)\)$/i.exec(userAgent.trim());
if (!match) return null;
return {
version: match[1] || '0.31.0',
platform: match[2] || 'win32',
arch: match[3] || 'x64',
};
}
function buildGeminiCLIUserAgent(modelName: string, existingUserAgent?: string | null): string {
const parsed = parseGeminiCliUserAgentRuntime(existingUserAgent ?? null);
const version = parsed?.version || '0.31.0';
const platform = parsed?.platform || 'win32';
const arch = parsed?.arch || 'x64';
const effectiveModel = asTrimmedString(modelName) || 'unknown';
return `GeminiCLI/${version}/${effectiveModel} (${platform}; ${arch})`;
}
function resolveAction(action: ProviderAction | undefined, stream: boolean): ProviderAction {
if (action) return action;
return stream ? 'streamGenerateContent' : 'generateContent';
@@ -74,25 +24,12 @@ export const geminiCliProviderProfile: ProviderProfile = {
throw new Error('gemini-cli oauth project id missing');
}
const action = resolveAction(input.action, input.stream);
const apiClient = (
getInputHeader(input.providerHeaders, 'x-goog-api-client')
|| getInputHeader(input.baseHeaders, 'x-goog-api-client')
);
const userAgent = buildGeminiCLIUserAgent(
input.modelName,
getInputHeader(input.providerHeaders, 'user-agent') || getInputHeader(input.baseHeaders, 'user-agent'),
);
const headers: Record<string, string> = {
Authorization: input.baseHeaders.Authorization,
'Content-Type': 'application/json',
'User-Agent': userAgent,
};
if (apiClient) {
headers['X-Goog-Api-Client'] = apiClient;
}
if (action === 'streamGenerateContent') {
headers.Accept = 'text/event-stream';
}
const headers = buildGeminiCliRuntimeHeaders({
baseHeaders: input.baseHeaders,
providerHeaders: input.providerHeaders,
modelName: input.modelName,
stream: action === 'streamGenerateContent',
});
return {
path: resolvePath(action),
headers,
@@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest';
describe('provider header utils', () => {
it('coerces header values and performs case-insensitive lookups', async () => {
const { headerValueToString, getInputHeader } = await import('./headerUtils.js');
expect(headerValueToString(' value ')).toBe('value');
expect(headerValueToString(['', ' first ', 'second'])).toBe('first');
expect(getInputHeader({ Authorization: 'Bearer test' }, 'authorization')).toBe('Bearer test');
});
it('derives stable uuids from seeds', async () => {
const { uuidFromSeed } = await import('./headerUtils.js');
expect(uuidFromSeed('seed-1')).toBe(uuidFromSeed('seed-1'));
expect(uuidFromSeed('seed-1')).not.toBe(uuidFromSeed('seed-2'));
expect(uuidFromSeed('seed-1')).toMatch(/^[0-9a-f-]{36}$/);
});
it('merges claude beta headers without duplicating entries', async () => {
const { mergeClaudeBetaHeader } = await import('./headerUtils.js');
expect(
mergeClaudeBetaHeader(null, 'beta-a,beta-b', ['beta-b', 'beta-c']),
).toBe('beta-a,beta-b,beta-c');
expect(
mergeClaudeBetaHeader('custom-a,custom-b', 'beta-a,beta-b', ['beta-c']),
).toBe('custom-a,custom-b');
});
it('preserves gemini cli runtime metadata when rebuilding user agents', async () => {
const {
buildGeminiCliUserAgent,
parseGeminiCliUserAgentRuntime,
} = await import('./headerUtils.js');
expect(
parseGeminiCliUserAgentRuntime('GeminiCLI/0.55.0/gemini-2.5-pro (darwin; arm64)'),
).toEqual({
version: '0.55.0',
platform: 'darwin',
arch: 'arm64',
});
expect(
buildGeminiCliUserAgent(
'gemini-2.5-flash',
'GeminiCLI/0.55.0/gemini-2.5-pro (darwin; arm64)',
),
).toBe('GeminiCLI/0.55.0/gemini-2.5-flash (darwin; arm64)');
});
it('builds codex runtime headers with continuity-derived session identifiers', async () => {
const { buildCodexRuntimeHeaders } = await import('./headerUtils.js');
const headers = buildCodexRuntimeHeaders({
baseHeaders: {
authorization: 'Bearer test',
version: '0.101.0',
},
providerHeaders: {
originator: 'codex_cli_rs',
'chatgpt-account-id': 'acct-1',
},
continuityKey: 'cache-key-1',
explicitSessionId: null,
});
expect(headers.Authorization).toBe('Bearer test');
expect(headers.Originator).toBe('codex_cli_rs');
expect(headers['Chatgpt-Account-Id']).toBe('acct-1');
expect(headers.Session_id).toMatch(/^[0-9a-f-]{36}$/);
expect(headers.Conversation_id).toBe(headers.Session_id);
expect(headers.Accept).toBe('text/event-stream');
});
it('builds claude runtime headers with merged betas and oauth bearer auth', async () => {
const { buildClaudeRuntimeHeaders } = await import('./headerUtils.js');
const headers = buildClaudeRuntimeHeaders({
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders: {
'anthropic-beta': 'custom-beta',
},
anthropicVersion: '2023-06-01',
stream: true,
isClaudeOauthUpstream: true,
tokenValue: 'oauth-token',
});
expect(headers['anthropic-version']).toBe('2023-06-01');
expect(headers['anthropic-beta']).toBe('custom-beta');
expect(headers.Authorization).toBe('Bearer oauth-token');
expect(headers['x-api-key']).toBeUndefined();
expect(headers.Accept).toBe('text/event-stream');
});
it('builds gemini cli runtime headers with preserved api client metadata', async () => {
const { buildGeminiCliRuntimeHeaders } = await import('./headerUtils.js');
const headers = buildGeminiCliRuntimeHeaders({
baseHeaders: {
Authorization: 'Bearer test',
},
providerHeaders: {
'x-goog-api-client': 'gl-node/22 gccl/0.31.0',
'user-agent': 'GeminiCLI/0.55.0/gemini-2.5-pro (darwin; arm64)',
},
modelName: 'gemini-2.5-flash',
stream: true,
});
expect(headers.Authorization).toBe('Bearer test');
expect(headers['X-Goog-Api-Client']).toBe('gl-node/22 gccl/0.31.0');
expect(headers['User-Agent']).toBe('GeminiCLI/0.55.0/gemini-2.5-flash (darwin; arm64)');
expect(headers.Accept).toBe('text/event-stream');
});
});
@@ -0,0 +1,245 @@
import { createHash, randomUUID } from 'node:crypto';
const CODEX_CLIENT_VERSION = '0.101.0';
const CODEX_DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
const CLAUDE_DEFAULT_USER_AGENT = 'claude-cli/2.1.63 (external, cli)';
const CLAUDE_DEFAULT_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
export function headerValueToString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || null;
}
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item !== 'string') continue;
const trimmed = item.trim();
if (trimmed) return trimmed;
}
}
return null;
}
export function getInputHeader(
headers: Record<string, unknown> | Record<string, string> | undefined,
key: string,
): string | null {
if (!headers) return null;
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
return headerValueToString(candidateValue);
}
return null;
}
export function uuidFromSeed(seed: string): string {
const hash = createHash('sha1').update(seed).digest();
const bytes = new Uint8Array(hash.subarray(0, 16));
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join('-');
}
export function mergeClaudeBetaHeader(
explicitValue: string | null,
defaultValue: string,
extraBetas: string[] = [],
): string {
const source = explicitValue || defaultValue;
const seen = new Set<string>();
const merged: string[] = [];
for (const entry of source.split(',')) {
const normalized = entry.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
merged.push(normalized);
}
if (!explicitValue) {
for (const entry of extraBetas) {
const normalized = entry.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
merged.push(normalized);
}
}
return merged.join(',');
}
export function parseGeminiCliUserAgentRuntime(userAgent: string | null): {
version: string;
platform: string;
arch: string;
} | null {
if (!userAgent) return null;
const match = /^GeminiCLI\/([^/]+)\/[^ ]+ \(([^;]+); ([^)]+)\)$/i.exec(userAgent.trim());
if (!match) return null;
return {
version: match[1] || '0.31.0',
platform: match[2] || 'win32',
arch: match[3] || 'x64',
};
}
export function buildGeminiCliUserAgent(modelName: string, existingUserAgent?: string | null): string {
const parsed = parseGeminiCliUserAgentRuntime(existingUserAgent ?? null);
const version = parsed?.version || '0.31.0';
const platform = parsed?.platform || 'win32';
const arch = parsed?.arch || 'x64';
const effectiveModel = typeof modelName === 'string' ? modelName.trim() : '';
return `GeminiCLI/${version}/${effectiveModel || 'unknown'} (${platform}; ${arch})`;
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
export function buildCodexRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
explicitSessionId?: string | null;
continuityKey?: string | null;
versionDefault?: string;
userAgentDefault?: string;
userAgentOverride?: string | null;
originatorDefault?: string;
codexBetaFeatures?: string | null;
codexTurnState?: string | null;
codexTurnMetadata?: string | null;
timingMetrics?: string | null;
openAiBeta?: string | null;
}): Record<string, string> {
const authorization = (
getInputHeader(input.baseHeaders, 'authorization')
|| getInputHeader(input.baseHeaders, 'Authorization')
|| ''
);
const originator = getInputHeader(input.providerHeaders, 'originator')
|| input.originatorDefault
|| 'codex_cli_rs';
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
const version = getInputHeader(input.baseHeaders, 'version')
|| input.versionDefault
|| CODEX_CLIENT_VERSION;
const userAgent = input.userAgentOverride
|| getInputHeader(input.baseHeaders, 'user-agent')
|| input.userAgentDefault
|| CODEX_DEFAULT_USER_AGENT;
const codexBetaFeatures = input.codexBetaFeatures || null;
const codexTurnState = input.codexTurnState || null;
const codexTurnMetadata = input.codexTurnMetadata || null;
const timingMetrics = input.timingMetrics || null;
const openAiBeta = input.openAiBeta || null;
const explicitSessionId = asTrimmedString(input.explicitSessionId);
const continuityKey = asTrimmedString(input.continuityKey);
const sessionId = (
getInputHeader(input.baseHeaders, 'session_id')
|| getInputHeader(input.baseHeaders, 'session-id')
|| explicitSessionId
|| (continuityKey ? uuidFromSeed(`metapi:codex:${continuityKey}`) : null)
|| randomUUID()
);
const conversationId = (
getInputHeader(input.baseHeaders, 'conversation_id')
|| getInputHeader(input.baseHeaders, 'conversation-id')
|| explicitSessionId
|| (continuityKey ? sessionId : null)
);
return {
Authorization: authorization,
'Content-Type': 'application/json',
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
Originator: originator,
Version: version,
...(codexBetaFeatures ? { 'x-codex-beta-features': codexBetaFeatures } : {}),
...(codexTurnState ? { 'x-codex-turn-state': codexTurnState } : {}),
...(codexTurnMetadata ? { 'x-codex-turn-metadata': codexTurnMetadata } : {}),
...(timingMetrics ? { 'x-responsesapi-include-timing-metrics': timingMetrics } : {}),
...(openAiBeta ? { 'OpenAI-Beta': openAiBeta } : {}),
Session_id: sessionId,
...(conversationId ? { Conversation_id: conversationId } : {}),
'User-Agent': userAgent,
Accept: 'text/event-stream',
Connection: 'Keep-Alive',
};
}
export function buildClaudeRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
claudeHeaders: Record<string, string>;
anthropicVersion: string;
stream: boolean;
isClaudeOauthUpstream: boolean;
tokenValue: string;
extraBetas?: string[];
defaultBetaHeader?: string;
defaultUserAgent?: string;
}): Record<string, string> {
const anthropicBeta = mergeClaudeBetaHeader(
getInputHeader(input.claudeHeaders, 'anthropic-beta'),
input.defaultBetaHeader || CLAUDE_DEFAULT_BETA_HEADER,
input.extraBetas,
);
const headers: Record<string, string> = {
...input.baseHeaders,
...input.claudeHeaders,
'anthropic-version': input.anthropicVersion,
...(anthropicBeta ? { 'anthropic-beta': anthropicBeta } : {}),
'Anthropic-Dangerous-Direct-Browser-Access': 'true',
'X-App': 'cli',
'X-Stainless-Retry-Count': getInputHeader(input.claudeHeaders, 'x-stainless-retry-count') || '0',
'X-Stainless-Runtime-Version': getInputHeader(input.claudeHeaders, 'x-stainless-runtime-version') || 'v24.3.0',
'X-Stainless-Package-Version': getInputHeader(input.claudeHeaders, 'x-stainless-package-version') || '0.74.0',
'X-Stainless-Runtime': getInputHeader(input.claudeHeaders, 'x-stainless-runtime') || 'node',
'X-Stainless-Lang': getInputHeader(input.claudeHeaders, 'x-stainless-lang') || 'js',
'X-Stainless-Arch': getInputHeader(input.claudeHeaders, 'x-stainless-arch') || 'x64',
'X-Stainless-Os': getInputHeader(input.claudeHeaders, 'x-stainless-os') || 'Windows',
'X-Stainless-Timeout': getInputHeader(input.claudeHeaders, 'x-stainless-timeout') || '600',
'User-Agent': getInputHeader(input.claudeHeaders, 'user-agent') || input.defaultUserAgent || CLAUDE_DEFAULT_USER_AGENT,
Connection: 'keep-alive',
Accept: input.stream ? 'text/event-stream' : 'application/json',
'Accept-Encoding': 'gzip, deflate, br, zstd',
};
if (input.isClaudeOauthUpstream) {
headers.Authorization = `Bearer ${input.tokenValue}`;
} else {
headers['x-api-key'] = input.tokenValue;
}
return headers;
}
export function buildGeminiCliRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
modelName: string;
stream: boolean;
}): Record<string, string> {
const apiClient = (
getInputHeader(input.providerHeaders, 'x-goog-api-client')
|| getInputHeader(input.baseHeaders, 'x-goog-api-client')
);
const userAgent = buildGeminiCliUserAgent(
input.modelName,
getInputHeader(input.providerHeaders, 'user-agent') || getInputHeader(input.baseHeaders, 'user-agent'),
);
const headers: Record<string, string> = {
Authorization: input.baseHeaders.Authorization,
'Content-Type': 'application/json',
'User-Agent': userAgent,
};
if (apiClient) {
headers['X-Goog-Api-Client'] = apiClient;
}
if (input.stream) {
headers.Accept = 'text/event-stream';
}
return headers;
}
+169 -432
View File
@@ -1,13 +1,9 @@
import { TextDecoder } from 'node:util';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { reportProxyAllFailed } from '../../services/alertService.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { type DownstreamFormat } from '../../transformers/shared/normalized.js';
import {
buildClaudeCountTokensUpstreamRequest,
@@ -21,11 +17,9 @@ import {
getDownstreamRoutingPolicy,
recordDownstreamCostUsage,
} from '../../routes/proxy/downstreamPolicy.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { buildUpstreamUrl } from '../../routes/proxy/upstreamUrl.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveProxyLogBilling } from '../../routes/proxy/proxyBilling.js';
import { openAiChatTransformer } from '../../transformers/openai/chat/index.js';
import { anthropicMessagesTransformer } from '../../transformers/anthropic/messages/index.js';
@@ -37,8 +31,7 @@ import {
import {
buildOauthProviderHeaders,
} from '../../services/oauth/service.js';
import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js';
import { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
import { getOauthInfoFromAccount } from '../../services/oauth/oauthAccount.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import {
collectResponsesFinalPayloadFromSse,
@@ -50,10 +43,13 @@ import {
createGeminiCliStreamReader,
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';
import { summarizeConversationFileInputsInOpenAiBody } from '../capabilities/conversationFileCapabilities.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import {
createSurfaceFailureToolkit,
createSurfaceDispatchRequest,
selectSurfaceChannelForAttempt,
} from './sharedSurface.js';
const MAX_RETRIES = 2;
@@ -114,19 +110,24 @@ export async function handleChatSurfaceRequest(
proxyToken: getProxyAuthContext(request)?.token || null,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const failureToolkit = createSurfaceFailureToolkit({
warningScope: 'chat',
downstreamPath,
maxRetries: MAX_RETRIES,
clientContext,
downstreamApiKeyId,
});
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= MAX_RETRIES) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectSurfaceChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
});
if (!selected) {
await reportProxyAllFailed({
@@ -141,7 +142,7 @@ export async function handleChatSurfaceRequest(
excludeChannelIds.push(selected.channel.id);
const modelName = selected.actualModel || requestedModel;
const oauth = getOauthInfoFromExtraConfig(selected.account.extraConfig);
const oauth = getOauthInfoFromAccount(selected.account);
const isCodexSite = String(selected.site.platform || '').trim().toLowerCase() === 'codex';
const endpointCandidates = [
...await resolveUpstreamEndpointCandidates(
@@ -170,7 +171,7 @@ export async function handleChatSurfaceRequest(
};
const buildProviderHeaders = () => (
buildOauthProviderHeaders({
extraConfig: typeof selected.account.extraConfig === 'string' ? selected.account.extraConfig : null,
account: selected.account,
downstreamHeaders: request.headers as Record<string, unknown>,
})
);
@@ -204,22 +205,10 @@ export async function handleChatSurfaceRequest(
runtime: endpointRequest.runtime,
};
};
const channelProxyUrl = resolveChannelProxyUrl(selected.site, selected.account.extraConfig);
const dispatchRequest = (
compatibilityRequest: BuiltEndpointRequest,
targetUrl?: string,
) => (
dispatchRuntimeRequest({
siteUrl: selected.site.url,
targetUrl,
request: compatibilityRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}, channelProxyUrl),
})
);
const dispatchRequest = createSurfaceDispatchRequest({
site: selected.site,
accountExtraConfig: selected.account.extraConfig,
});
const endpointStrategy = downstreamTransformer.compatibility.createEndpointStrategy({
downstreamFormat,
endpointCandidates,
@@ -286,82 +275,34 @@ export async function handleChatSurfaceRequest(
},
shouldDowngrade: endpointStrategy.shouldDowngrade,
onDowngrade: (ctx) => {
logProxy(
selected,
requestedModel,
'failed',
ctx.response.status,
Date.now() - startTime,
ctx.errText,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
},
});
return failureToolkit.log({
selected,
modelRequested: requestedModel,
status: 'failed',
httpStatus: ctx.response.status,
latencyMs: Date.now() - startTime,
errorMessage: ctx.errText,
retryCount,
});
},
});
if (!endpointResult.ok) {
const status = endpointResult.status || 502;
const errText = endpointResult.errText || 'unknown error';
const rawErrText = endpointResult.rawErrText || errText;
tokenRouter.recordFailure(selected.channel.id, {
status,
errorText: rawErrText,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleUpstreamFailure({
selected,
requestedModel,
'failed',
status,
Date.now() - startTime,
errText,
modelName,
status: endpointResult.status || 502,
errText: endpointResult.errText || 'unknown error',
rawErrText: endpointResult.rawErrText,
latencyMs: Date.now() - startTime,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
await recordOauthQuotaResetHint({
accountId: selected.account.id,
statusCode: status,
errorText: rawErrText,
});
if (isTokenExpiredError({ status, message: errText })) {
await reportTokenExpired({
accountId: selected.account.id,
username: selected.account.username,
siteName: selected.site.name,
detail: `HTTP ${status}`,
});
}
if (shouldRetryProxyRequest(status, errText) && retryCount < MAX_RETRIES) {
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: `upstream returned HTTP ${status}`,
});
return reply.code(status).send({
error: { message: errText, type: 'upstream_error' },
});
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
const upstream = endpointResult.upstream;
@@ -418,25 +359,18 @@ export async function handleChatSurfaceRequest(
);
const latency = Date.now() - startTime;
if (streamResult.status === 'failed') {
tokenRouter.recordFailure(selected.channel.id, modelName);
logProxy(
await failureToolkit.recordStreamFailure({
selected,
requestedModel,
'failed',
200,
latency,
streamResult.errorMessage,
modelName,
errorMessage: streamResult.errorMessage,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
});
return;
}
return;
@@ -454,71 +388,41 @@ export async function handleChatSurfaceRequest(
const latency = Date.now() - startTime;
const failure = detectProxyFailure({ rawText, usage: parsedUsage });
if (failure) {
tokenRouter.recordFailure(selected.channel.id, {
status: failure.status,
errorText: failure.reason,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleDetectedFailure({
selected,
requestedModel,
'failed',
failure.status,
latency,
failure.reason,
modelName,
failure,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: failure.reason,
});
return reply.code(failure.status).send({
error: { message: failure.reason, type: 'upstream_error' },
});
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
startSseResponse();
const streamResult = streamSession.consumeUpstreamFinalPayload(fallbackData, fallbackText, reply.raw);
if (streamResult.status === 'failed') {
tokenRouter.recordFailure(selected.channel.id, {
status: 502,
errorText: streamResult.errorMessage,
modelName,
});
logProxy(
await failureToolkit.recordStreamFailure({
selected,
requestedModel,
'failed',
200,
latency,
streamResult.errorMessage,
modelName,
errorMessage: streamResult.errorMessage,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
runtimeFailureStatus: 502,
});
return;
}
} else {
@@ -550,29 +454,19 @@ export async function handleChatSurfaceRequest(
const latency = Date.now() - startTime;
if (streamResult.status === 'failed') {
tokenRouter.recordFailure(selected.channel.id, {
status: 502,
errorText: streamResult.errorMessage,
modelName,
});
logProxy(
await failureToolkit.recordStreamFailure({
selected,
requestedModel,
'failed',
200,
latency,
streamResult.errorMessage,
modelName,
errorMessage: streamResult.errorMessage,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
runtimeFailureStatus: 502,
});
return;
}
@@ -609,24 +503,21 @@ export async function handleChatSurfaceRequest(
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
await failureToolkit.log({
selected,
requestedModel,
'success',
200,
latency,
null,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
latencyMs: latency,
errorMessage: null,
retryCount,
downstreamPath,
resolvedUsage.promptTokens,
resolvedUsage.completionTokens,
resolvedUsage.totalTokens,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
upstreamPath: successfulUpstreamPath,
});
return;
}
@@ -658,43 +549,23 @@ export async function handleChatSurfaceRequest(
const parsedUsage = parseProxyUsage(upstreamData);
const failure = detectProxyFailure({ rawText, usage: parsedUsage });
if (failure) {
tokenRouter.recordFailure(selected.channel.id, {
status: failure.status,
errorText: failure.reason,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleDetectedFailure({
selected,
requestedModel,
'failed',
failure.status,
latency,
failure.reason,
modelName,
failure,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: failure.reason,
});
return reply.code(failure.status).send({
error: { message: failure.reason, type: 'upstream_error' },
});
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
const normalizedFinal = downstreamTransformer.transformFinalResponse(upstreamData, modelName, rawText);
const downstreamResponse = downstreamTransformer.serializeFinalResponse(normalizedFinal, parsedUsage);
@@ -725,66 +596,37 @@ export async function handleChatSurfaceRequest(
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
await failureToolkit.log({
selected,
requestedModel,
'success',
200,
latency,
null,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
latencyMs: latency,
errorMessage: null,
retryCount,
downstreamPath,
resolvedUsage.promptTokens,
resolvedUsage.completionTokens,
resolvedUsage.totalTokens,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
upstreamPath: successfulUpstreamPath,
});
return reply.send(downstreamResponse);
} catch (err: any) {
tokenRouter.recordFailure(selected.channel.id, {
errorText: err?.message,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleExecutionError({
selected,
requestedModel,
'failed',
0,
Date.now() - startTime,
err?.message || 'network error',
modelName,
errorMessage: err?.message || 'network failure',
latencyMs: Date.now() - startTime,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
if (retryCount < MAX_RETRIES) {
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: err?.message || 'network failure',
});
return reply.code(502).send({
error: {
message: `Upstream error: ${err?.message || 'network failure'}`,
type: 'upstream_error',
},
});
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
}
}
@@ -845,18 +687,23 @@ export async function handleClaudeCountTokensSurfaceRequest(
});
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const failureToolkit = createSurfaceFailureToolkit({
warningScope: 'chat',
downstreamPath,
maxRetries: MAX_RETRIES,
clientContext,
downstreamApiKeyId,
});
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= MAX_RETRIES) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectSurfaceChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
});
if (!selected) {
await reportProxyAllFailed({
@@ -891,7 +738,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
},
});
}
const oauth = getOauthInfoFromExtraConfig(selected.account.extraConfig);
const oauth = getOauthInfoFromAccount(selected.account);
const startTime = Date.now();
const buildRequest = () => {
@@ -914,15 +761,11 @@ export async function handleClaudeCountTokensSurfaceRequest(
try {
let upstreamRequest = buildRequest();
let upstream = await dispatchRuntimeRequest({
siteUrl: selected.site.url,
request: upstreamRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}, resolveChannelProxyUrl(selected.site, selected.account.extraConfig)),
const dispatchRequest = createSurfaceDispatchRequest({
site: selected.site,
accountExtraConfig: selected.account.extraConfig,
});
let upstream = await dispatchRequest(upstreamRequest);
if ((upstream.status === 401 || upstream.status === 403) && oauth) {
try {
@@ -934,15 +777,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
extraConfig: refreshed.extraConfig ?? selected.account.extraConfig,
};
upstreamRequest = buildRequest();
upstream = await dispatchRuntimeRequest({
siteUrl: selected.site.url,
request: upstreamRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}, resolveChannelProxyUrl(selected.site, selected.account.extraConfig)),
});
upstream = await dispatchRequest(upstreamRequest);
} catch {
// Fall through to the regular upstream error handling below.
}
@@ -959,148 +794,50 @@ export async function handleClaudeCountTokensSurfaceRequest(
}
if (!upstream.ok) {
tokenRouter.recordFailure(selected.channel.id, {
status: upstream.status,
errorText: typeof payload === 'string' ? payload : text,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleUpstreamFailure({
selected,
requestedModel,
'failed',
upstream.status,
latency,
typeof payload === 'string' ? payload : JSON.stringify(payload),
modelName,
status: upstream.status,
errText: typeof payload === 'string' ? payload : JSON.stringify(payload),
rawErrText: typeof payload === 'string' ? payload : text,
latencyMs: latency,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
upstreamRequest.path,
clientContext,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(upstream.status, typeof payload === 'string' ? payload : text) && retryCount < MAX_RETRIES) {
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
return reply.code(upstream.status).type(contentType).send(payload);
return reply.code(failureOutcome.status).type(contentType).send(payload);
}
tokenRouter.recordSuccess(selected.channel.id, latency, 0, modelName);
recordDownstreamCostUsage(request, 0);
logProxy(
await failureToolkit.log({
selected,
requestedModel,
'success',
upstream.status,
latency,
null,
modelRequested: requestedModel,
status: 'success',
httpStatus: upstream.status,
latencyMs: latency,
errorMessage: null,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
upstreamRequest.path,
clientContext,
downstreamApiKeyId,
);
upstreamPath: upstreamRequest.path,
});
return reply.code(upstream.status).type(contentType).send(payload);
} catch (error: any) {
tokenRouter.recordFailure(selected.channel.id, {
errorText: error?.message,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleExecutionError({
selected,
requestedModel,
'failed',
0,
Date.now() - startTime,
error?.message || 'network error',
modelName,
errorMessage: error?.message || 'network failure',
latencyMs: Date.now() - startTime,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
if (retryCount < MAX_RETRIES) {
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
return reply.code(502).send({
error: {
message: `Upstream error: ${error?.message || 'network failure'}`,
type: 'upstream_error',
},
});
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
}
}
async function logProxy(
selected: any,
modelRequested: string,
status: string,
httpStatus: number,
latencyMs: number,
errorMessage: string | null,
retryCount: number,
downstreamPath: string,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
billingDetails: unknown = null,
upstreamPath: string | null = null,
clientContext: DownstreamClientContext | null = null,
downstreamApiKeyId: number | null = null,
) {
try {
const createdAt = formatUtcSqlDateTime(new Date());
const normalizedErrorMessage = composeProxyLogMessage({
clientKind: clientContext?.clientKind && clientContext.clientKind !== 'generic'
? clientContext.clientKind
: null,
sessionId: clientContext?.sessionId || null,
traceHint: clientContext?.traceHint || null,
downstreamPath,
upstreamPath,
errorMessage,
});
await insertProxyLog({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
downstreamApiKeyId,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens,
completionTokens,
totalTokens,
estimatedCost,
billingDetails,
clientFamily: clientContext?.clientKind || null,
clientAppId: clientContext?.clientAppId || null,
clientAppName: clientContext?.clientAppName || null,
clientConfidence: clientContext?.clientConfidence || null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
});
} catch (error) {
console.warn('[proxy/chat] failed to write proxy log', error);
}
}
@@ -8,7 +8,7 @@ import { parseProxyUsage } from '../../services/proxyUsageParser.js';
import { isModelAllowedByPolicyOrAllowedRoutes } from '../../services/downstreamApiKeyService.js';
import { tokenRouter } from '../../services/tokenRouter.js';
import { buildOauthProviderHeaders } from '../../services/oauth/service.js';
import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js';
import { getOauthInfoFromAccount } from '../../services/oauth/oauthAccount.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
@@ -407,7 +407,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
isGeminiCliDownstream ? omitGeminiCliModelField(request.body) : (request.body || {}),
actualModel,
);
let oauth = getOauthInfoFromExtraConfig(selected.account.extraConfig);
let oauth = getOauthInfoFromAccount(selected.account);
const isGeminiCli = isGeminiCliPlatform(selected.site.platform);
const isInternalGemini = isInternalGeminiPlatform(selected.site.platform);
const isDirectGeminiFamily = isDirectGeminiFamilyPlatform(selected.site.platform);
@@ -463,7 +463,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
...(isStreamAction ? { Accept: 'text/event-stream' } : {}),
Authorization: `Bearer ${selected.tokenValue}`,
...buildOauthProviderHeaders({
extraConfig: typeof selected.account.extraConfig === 'string' ? selected.account.extraConfig : null,
account: selected.account,
downstreamHeaders: request.headers as Record<string, unknown>,
}),
}
@@ -523,7 +523,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
accessToken: refreshed.accessToken,
extraConfig: refreshed.extraConfig ?? selected.account.extraConfig,
};
oauth = getOauthInfoFromExtraConfig(selected.account.extraConfig);
oauth = getOauthInfoFromAccount(selected.account);
upstream = await dispatchSelectedRequest();
contentType = upstream.headers.get('content-type') || 'application/json';
} catch {
@@ -758,7 +758,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
forceNormalizeClaudeBody: requestOptions.forceNormalizeClaudeBody,
downstreamHeaders: request.headers as Record<string, unknown>,
providerHeaders: buildOauthProviderHeaders({
extraConfig: typeof selected.account.extraConfig === 'string' ? selected.account.extraConfig : null,
account: selected.account,
downstreamHeaders: request.headers as Record<string, unknown>,
}),
});
@@ -1,114 +1,4 @@
import type { ProxyResourceOwner } from '../../middleware/auth.js';
import { getProxyFileByPublicIdForOwner } from '../../services/proxyFileStore.js';
import {
inferInputFileMimeType,
normalizeInputFileBlock,
toOpenAiChatFileBlock,
toResponsesInputFileBlock,
type NormalizedInputFile,
} from '../../transformers/shared/inputFile.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function isSupportedFileMimeType(mimeType: string): boolean {
return mimeType === 'application/pdf'
|| mimeType === 'text/plain'
|| mimeType === 'text/markdown'
|| mimeType === 'application/json'
|| mimeType.startsWith('image/')
|| mimeType.startsWith('audio/');
}
export class ProxyInputFileResolutionError extends Error {
statusCode: number;
payload: unknown;
constructor(statusCode: number, payload: unknown) {
super(typeof payload === 'object' && payload && 'error' in (payload as Record<string, unknown>)
? String(((payload as { error?: { message?: unknown } }).error?.message) || 'input file resolution failed')
: 'input file resolution failed');
this.name = 'ProxyInputFileResolutionError';
this.statusCode = statusCode;
this.payload = payload;
}
}
function buildInvalidRequest(message: string): ProxyInputFileResolutionError {
return new ProxyInputFileResolutionError(400, {
error: {
message,
type: 'invalid_request_error',
},
});
}
function buildNotFound(message: string): ProxyInputFileResolutionError {
return new ProxyInputFileResolutionError(404, {
error: {
message,
type: 'not_found_error',
},
});
}
async function resolveInputFileBlock(
item: Record<string, unknown>,
owner: ProxyResourceOwner,
): Promise<Record<string, unknown>> {
const normalized = normalizeInputFileBlock(item);
if (!normalized) return item;
let resolved: NormalizedInputFile = { ...normalized };
if (!resolved.fileData && resolved.fileId?.startsWith('file-metapi-')) {
const stored = await getProxyFileByPublicIdForOwner(resolved.fileId, owner);
if (!stored) {
throw buildNotFound(`file not found: ${resolved.fileId}`);
}
resolved = {
filename: stored.filename,
mimeType: stored.mimeType,
fileData: stored.contentBase64,
};
}
const inferredMimeType = inferInputFileMimeType(resolved);
if (resolved.fileData && (!inferredMimeType || !isSupportedFileMimeType(inferredMimeType))) {
throw buildInvalidRequest(`unsupported file mime type: ${inferredMimeType || 'application/octet-stream'}`);
}
if (inferredMimeType) {
resolved.mimeType = inferredMimeType;
}
const type = typeof item.type === 'string' ? item.type : '';
if (type === 'input_file') {
return toResponsesInputFileBlock(resolved);
}
if (type === 'file') {
return toOpenAiChatFileBlock(resolved);
}
return item;
}
export async function inlineLocalInputFileReferences(
value: unknown,
owner: ProxyResourceOwner,
): Promise<unknown> {
if (Array.isArray(value)) {
return Promise.all(value.map((item) => inlineLocalInputFileReferences(item, owner)));
}
if (!isRecord(value)) return value;
const type = typeof value.type === 'string' ? value.type : '';
if (type === 'input_file' || type === 'file') {
return resolveInputFileBlock(value, owner);
}
const entries = await Promise.all(
Object.entries(value).map(async ([key, entryValue]) => [key, await inlineLocalInputFileReferences(entryValue, owner)] as const),
);
return Object.fromEntries(entries);
}
export {
inlineLocalInputFileReferences,
ProxyInputFileResolutionError,
} from '../../services/proxyInputFileResolver.js';
@@ -1,13 +1,9 @@
import { TextDecoder } from 'node:util';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { reportProxyAllFailed } from '../../services/alertService.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js';
import {
buildUpstreamEndpointRequest,
@@ -16,14 +12,11 @@ import {
resolveUpstreamEndpointCandidates,
} from '../../routes/proxy/upstreamEndpoint.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from '../../routes/proxy/downstreamPolicy.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { buildUpstreamUrl } from '../../routes/proxy/upstreamUrl.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveProxyLogBilling } from '../../routes/proxy/proxyBilling.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';
import { normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
import {
ProxyInputFileResolutionError,
@@ -32,8 +25,7 @@ import {
import {
buildOauthProviderHeaders,
} from '../../services/oauth/service.js';
import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js';
import { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
import { getOauthInfoFromAccount } from '../../services/oauth/oauthAccount.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import {
collectResponsesFinalPayloadFromSse,
@@ -50,8 +42,12 @@ import {
summarizeConversationFileInputsInOpenAiBody,
summarizeConversationFileInputsInResponsesBody,
} from '../capabilities/conversationFileCapabilities.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import {
createSurfaceFailureToolkit,
createSurfaceDispatchRequest,
selectSurfaceChannelForAttempt,
} from './sharedSurface.js';
const MAX_RETRIES = 2;
@@ -185,18 +181,23 @@ export async function handleOpenAiResponsesSurfaceRequest(
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const failureToolkit = createSurfaceFailureToolkit({
warningScope: 'responses',
downstreamPath,
maxRetries: MAX_RETRIES,
clientContext,
downstreamApiKeyId,
});
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= MAX_RETRIES) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectSurfaceChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
});
if (!selected) {
await reportProxyAllFailed({
@@ -211,7 +212,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
excludeChannelIds.push(selected.channel.id);
const modelName = selected.actualModel || requestedModel;
const oauth = getOauthInfoFromExtraConfig(selected.account.extraConfig);
const oauth = getOauthInfoFromAccount(selected.account);
const isCodexSite = String(selected.site.platform || '').trim().toLowerCase() === 'codex';
const owner = getProxyResourceOwner(request);
let normalizedResponsesBody: Record<string, unknown> = {
@@ -274,7 +275,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
};
const buildProviderHeaders = () => (
buildOauthProviderHeaders({
extraConfig: typeof selected.account.extraConfig === 'string' ? selected.account.extraConfig : null,
account: selected.account,
downstreamHeaders: request.headers as Record<string, unknown>,
})
);
@@ -308,19 +309,10 @@ export async function handleOpenAiResponsesSurfaceRequest(
runtime: endpointRequest.runtime,
};
};
const channelProxyUrl = resolveChannelProxyUrl(selected.site, selected.account.extraConfig);
const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => (
dispatchRuntimeRequest({
siteUrl: selected.site.url,
targetUrl,
request: compatibilityRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}, channelProxyUrl),
})
);
const dispatchRequest = createSurfaceDispatchRequest({
site: selected.site,
accountExtraConfig: selected.account.extraConfig,
});
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
isStream: isStream || isCodexSite,
requiresNativeResponsesFileUrl,
@@ -385,79 +377,34 @@ export async function handleOpenAiResponsesSurfaceRequest(
},
shouldDowngrade: endpointStrategy.shouldDowngrade,
onDowngrade: (ctx) => {
logProxy(
return failureToolkit.log({
selected,
requestedModel,
'failed',
ctx.response.status,
Date.now() - startTime,
ctx.errText,
modelRequested: requestedModel,
status: 'failed',
httpStatus: ctx.response.status,
latencyMs: Date.now() - startTime,
errorMessage: ctx.errText,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
});
},
});
if (!endpointResult.ok) {
const status = endpointResult.status || 502;
const errText = endpointResult.errText || 'unknown error';
const rawErrText = endpointResult.rawErrText || errText;
tokenRouter.recordFailure(selected.channel.id, {
status,
errorText: rawErrText,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleUpstreamFailure({
selected,
requestedModel,
'failed',
status,
Date.now() - startTime,
errText,
modelName,
status: endpointResult.status || 502,
errText: endpointResult.errText || 'unknown error',
rawErrText: endpointResult.rawErrText,
latencyMs: Date.now() - startTime,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
await recordOauthQuotaResetHint({
accountId: selected.account.id,
statusCode: status,
errorText: rawErrText,
});
if (isTokenExpiredError({ status, message: errText })) {
await reportTokenExpired({
accountId: selected.account.id,
username: selected.account.username,
siteName: selected.site.name,
detail: `HTTP ${status}`,
});
}
if (shouldRetryProxyRequest(status, errText) && retryCount < MAX_RETRIES) {
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: `upstream returned HTTP ${status}`,
});
return reply.code(status).send({ error: { message: errText, type: 'upstream_error' } });
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
const upstream = endpointResult.upstream;
@@ -508,13 +455,21 @@ export async function handleOpenAiResponsesSurfaceRequest(
try {
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', 200, latency, null, retryCount, downstreamPath,
usageForLog.promptTokens, usageForLog.completionTokens, usageForLog.totalTokens, estimatedCost, billingDetails,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
await failureToolkit.log({
selected,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
latencyMs: latency,
errorMessage: null,
retryCount,
promptTokens: usageForLog.promptTokens,
completionTokens: usageForLog.completionTokens,
totalTokens: usageForLog.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: successfulUpstreamPath,
});
} catch (error) {
console.error('[responses] post-stream success logging failed:', error);
}
@@ -569,25 +524,18 @@ export async function handleOpenAiResponsesSurfaceRequest(
);
const latency = Date.now() - startTime;
if (streamResult.status === 'failed') {
tokenRouter.recordFailure(selected.channel.id, modelName);
logProxy(
await failureToolkit.recordStreamFailure({
selected,
requestedModel,
'failed',
200,
latency,
streamResult.errorMessage,
modelName,
errorMessage: streamResult.errorMessage,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
});
return;
}
@@ -608,68 +556,41 @@ export async function handleOpenAiResponsesSurfaceRequest(
const latency = Date.now() - startTime;
const failure = detectProxyFailure({ rawText, usage: parsedUsage });
if (failure) {
tokenRouter.recordFailure(selected.channel.id, {
status: failure.status,
errorText: failure.reason,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleDetectedFailure({
selected,
requestedModel,
'failed',
failure.status,
latency,
failure.reason,
modelName,
failure,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: failure.reason,
});
return reply.code(failure.status).send({ error: { message: failure.reason, type: 'upstream_error' } });
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
startSseResponse();
const streamResult = streamSession.consumeUpstreamFinalPayload(upstreamData, rawText, reply.raw);
if (streamResult.status === 'failed') {
tokenRouter.recordFailure(selected.channel.id, {
status: 502,
errorText: streamResult.errorMessage,
modelName,
});
logProxy(
await failureToolkit.recordStreamFailure({
selected,
requestedModel,
'failed',
200,
latency,
streamResult.errorMessage,
modelName,
errorMessage: streamResult.errorMessage,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
runtimeFailureStatus: 502,
});
return;
}
@@ -707,29 +628,19 @@ export async function handleOpenAiResponsesSurfaceRequest(
const latency = Date.now() - startTime;
if (streamResult.status === 'failed') {
tokenRouter.recordFailure(selected.channel.id, {
status: 502,
errorText: streamResult.errorMessage,
modelName,
});
logProxy(
await failureToolkit.recordStreamFailure({
selected,
requestedModel,
'failed',
200,
latency,
streamResult.errorMessage,
modelName,
errorMessage: streamResult.errorMessage,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
runtimeFailureStatus: 502,
});
return;
}
@@ -775,39 +686,23 @@ export async function handleOpenAiResponsesSurfaceRequest(
const parsedUsage = parseProxyUsage(upstreamData);
const failure = detectProxyFailure({ rawText, usage: parsedUsage });
if (failure) {
tokenRouter.recordFailure(selected.channel.id, {
status: failure.status,
errorText: failure.reason,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleDetectedFailure({
selected,
requestedModel,
'failed',
failure.status,
latency,
failure.reason,
modelName,
failure,
latencyMs: latency,
retryCount,
downstreamPath,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
0,
null,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
upstreamPath: successfulUpstreamPath,
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: failure.reason,
});
return reply.code(failure.status).send({ error: { message: failure.reason, type: 'upstream_error' } });
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
const normalized = openAiResponsesTransformer.transformFinalResponse(
upstreamData,
@@ -845,106 +740,36 @@ export async function handleOpenAiResponsesSurfaceRequest(
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', 200, latency, null, retryCount, downstreamPath,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
successfulUpstreamPath,
clientContext,
downstreamApiKeyId,
);
await failureToolkit.log({
selected,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
latencyMs: latency,
errorMessage: null,
retryCount,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: successfulUpstreamPath,
});
return reply.send(downstreamData);
} catch (err: any) {
tokenRouter.recordFailure(selected.channel.id, {
errorText: err?.message,
modelName,
});
logProxy(
const failureOutcome = await failureToolkit.handleExecutionError({
selected,
requestedModel,
'failed',
0,
Date.now() - startTime,
err.message,
modelName,
errorMessage: err?.message || 'network failure',
latencyMs: Date.now() - startTime,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
downstreamApiKeyId,
);
if (retryCount < MAX_RETRIES) {
});
if (failureOutcome.action === 'retry') {
retryCount += 1;
continue;
}
await reportProxyAllFailed({
model: requestedModel,
reason: err.message || 'network failure',
});
return reply.code(502).send({
error: { message: `Upstream error: ${err.message}`, type: 'upstream_error' },
});
return reply.code(failureOutcome.status).send(failureOutcome.payload);
}
}
}
async function logProxy(
selected: any,
modelRequested: string,
status: string,
httpStatus: number,
latencyMs: number,
errorMessage: string | null,
retryCount: number,
downstreamPath: string,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
billingDetails: unknown = null,
upstreamPath: string | null = null,
clientContext: DownstreamClientContext | null = null,
downstreamApiKeyId: number | null = null,
) {
try {
const createdAt = formatUtcSqlDateTime(new Date());
const normalizedErrorMessage = composeProxyLogMessage({
clientKind: clientContext?.clientKind && clientContext.clientKind !== 'generic'
? clientContext.clientKind
: null,
sessionId: clientContext?.sessionId || null,
traceHint: clientContext?.traceHint || null,
downstreamPath,
upstreamPath,
errorMessage,
});
await insertProxyLog({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
downstreamApiKeyId,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens,
completionTokens,
totalTokens,
estimatedCost,
billingDetails,
clientFamily: clientContext?.clientKind || null,
clientAppId: clientContext?.clientAppId || null,
clientAppName: clientContext?.clientAppName || null,
clientConfidence: clientContext?.clientConfidence || null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
});
} catch (error) {
console.warn('[proxy/responses] failed to write proxy log', error);
}
}
@@ -0,0 +1,471 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EMPTY_DOWNSTREAM_ROUTING_POLICY } from '../../services/downstreamPolicyTypes.js';
const selectChannelMock = vi.fn();
const selectNextChannelMock = vi.fn();
const recordFailureMock = vi.fn();
const refreshModelsAndRebuildRoutesMock = vi.fn();
const composeProxyLogMessageMock = vi.fn();
const formatUtcSqlDateTimeMock = vi.fn();
const insertProxyLogMock = vi.fn();
const resolveChannelProxyUrlMock = vi.fn();
const withSiteRecordProxyRequestInitMock = vi.fn();
const dispatchRuntimeRequestMock = vi.fn();
const reportProxyAllFailedMock = vi.fn();
const reportTokenExpiredMock = vi.fn();
const isTokenExpiredErrorMock = vi.fn();
const shouldRetryProxyRequestMock = vi.fn();
const recordOauthQuotaResetHintMock = vi.fn();
vi.mock('../../services/tokenRouter.js', () => ({
tokenRouter: {
selectChannel: (...args: unknown[]) => selectChannelMock(...args),
selectNextChannel: (...args: unknown[]) => selectNextChannelMock(...args),
recordFailure: (...args: unknown[]) => recordFailureMock(...args),
},
}));
vi.mock('../../services/modelService.js', () => ({
refreshModelsAndRebuildRoutes: (...args: unknown[]) => refreshModelsAndRebuildRoutesMock(...args),
}));
vi.mock('../../routes/proxy/logPathMeta.js', () => ({
composeProxyLogMessage: (...args: unknown[]) => composeProxyLogMessageMock(...args),
}));
vi.mock('../../services/localTimeService.js', () => ({
formatUtcSqlDateTime: (...args: unknown[]) => formatUtcSqlDateTimeMock(...args),
}));
vi.mock('../../services/proxyLogStore.js', () => ({
insertProxyLog: (...args: unknown[]) => insertProxyLogMock(...args),
}));
vi.mock('../../services/siteProxy.js', () => ({
resolveChannelProxyUrl: (...args: unknown[]) => resolveChannelProxyUrlMock(...args),
withSiteRecordProxyRequestInit: (...args: unknown[]) => withSiteRecordProxyRequestInitMock(...args),
}));
vi.mock('../../routes/proxy/runtimeExecutor.js', () => ({
dispatchRuntimeRequest: (...args: unknown[]) => dispatchRuntimeRequestMock(...args),
}));
vi.mock('../../services/alertService.js', () => ({
reportProxyAllFailed: (...args: unknown[]) => reportProxyAllFailedMock(...args),
reportTokenExpired: (...args: unknown[]) => reportTokenExpiredMock(...args),
}));
vi.mock('../../services/alertRules.js', () => ({
isTokenExpiredError: (...args: unknown[]) => isTokenExpiredErrorMock(...args),
}));
vi.mock('../../services/proxyRetryPolicy.js', () => ({
shouldRetryProxyRequest: (...args: unknown[]) => shouldRetryProxyRequestMock(...args),
}));
vi.mock('../../services/oauth/quota.js', () => ({
recordOauthQuotaResetHint: (...args: unknown[]) => recordOauthQuotaResetHintMock(...args),
}));
describe('selectSurfaceChannelForAttempt', () => {
beforeEach(() => {
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
recordFailureMock.mockReset();
refreshModelsAndRebuildRoutesMock.mockReset();
composeProxyLogMessageMock.mockReset();
formatUtcSqlDateTimeMock.mockReset();
insertProxyLogMock.mockReset();
resolveChannelProxyUrlMock.mockReset();
withSiteRecordProxyRequestInitMock.mockReset();
dispatchRuntimeRequestMock.mockReset();
reportProxyAllFailedMock.mockReset();
reportTokenExpiredMock.mockReset();
isTokenExpiredErrorMock.mockReset();
shouldRetryProxyRequestMock.mockReset();
recordOauthQuotaResetHintMock.mockReset();
});
it('refreshes models and retries selectChannel on the first attempt when no channel is available', async () => {
const selected = { channel: { id: 11 } };
selectChannelMock
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(selected);
const { selectSurfaceChannelForAttempt } = await import('./sharedSurface.js');
const result = await selectSurfaceChannelForAttempt({
requestedModel: 'gpt-5.2',
downstreamPolicy: EMPTY_DOWNSTREAM_ROUTING_POLICY,
excludeChannelIds: [],
retryCount: 0,
});
expect(result).toBe(selected);
expect(selectChannelMock).toHaveBeenCalledTimes(2);
expect(selectNextChannelMock).not.toHaveBeenCalled();
expect(refreshModelsAndRebuildRoutesMock).toHaveBeenCalledTimes(1);
});
it('uses selectNextChannel on retry attempts without refreshing models', async () => {
const selected = { channel: { id: 22 } };
selectNextChannelMock.mockResolvedValueOnce(selected);
const { selectSurfaceChannelForAttempt } = await import('./sharedSurface.js');
const result = await selectSurfaceChannelForAttempt({
requestedModel: 'gpt-5.2',
downstreamPolicy: EMPTY_DOWNSTREAM_ROUTING_POLICY,
excludeChannelIds: [11],
retryCount: 1,
});
expect(result).toBe(selected);
expect(selectChannelMock).not.toHaveBeenCalled();
expect(selectNextChannelMock).toHaveBeenCalledWith(
'gpt-5.2',
[11],
EMPTY_DOWNSTREAM_ROUTING_POLICY,
);
expect(refreshModelsAndRebuildRoutesMock).not.toHaveBeenCalled();
});
it('writes proxy logs through the shared log formatter and store', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
const { writeSurfaceProxyLog } = await import('./sharedSurface.js');
await writeSurfaceProxyLog({
warningScope: 'chat',
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33 },
actualModel: 'upstream-model',
},
modelRequested: 'gpt-5.2',
status: 'failed',
httpStatus: 502,
latencyMs: 1200,
errorMessage: 'upstream failed',
retryCount: 1,
downstreamPath: '/v1/chat/completions',
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
estimatedCost: 0.42,
billingDetails: { source: 'test' },
upstreamPath: '/v1/responses',
clientContext: {
clientKind: 'codex',
clientAppId: 'app-id',
clientAppName: 'App',
clientConfidence: 'high',
sessionId: 'sess-1',
traceHint: 'trace-1',
},
downstreamApiKeyId: 44,
});
expect(composeProxyLogMessageMock).toHaveBeenCalledWith({
clientKind: 'codex',
sessionId: 'sess-1',
traceHint: 'trace-1',
downstreamPath: '/v1/chat/completions',
upstreamPath: '/v1/responses',
errorMessage: 'upstream failed',
});
expect(insertProxyLogMock).toHaveBeenCalledWith({
routeId: 22,
channelId: 11,
accountId: 33,
downstreamApiKeyId: 44,
modelRequested: 'gpt-5.2',
modelActual: 'upstream-model',
status: 'failed',
httpStatus: 502,
latencyMs: 1200,
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
estimatedCost: 0.42,
billingDetails: { source: 'test' },
clientFamily: 'codex',
clientAppId: 'app-id',
clientAppName: 'App',
clientConfidence: 'high',
errorMessage: 'normalized error',
retryCount: 1,
createdAt: '2026-03-21 22:00:00',
});
});
it('builds runtime dispatch requests with site proxy initialization', async () => {
const site = { url: 'https://upstream.example.com' };
const request = {
endpoint: 'responses',
path: '/v1/responses',
headers: { authorization: 'Bearer test' },
body: { model: 'gpt-5.2', input: 'hello' },
runtime: { executor: 'default' },
};
resolveChannelProxyUrlMock.mockReturnValue('http://proxy.example.com');
withSiteRecordProxyRequestInitMock.mockImplementation(async (_site, init, proxyUrl) => ({
...init,
proxyUrl,
}));
dispatchRuntimeRequestMock.mockResolvedValue('ok');
const { createSurfaceDispatchRequest } = await import('./sharedSurface.js');
const dispatchRequest = createSurfaceDispatchRequest({
site,
accountExtraConfig: '{"proxyUrl":"http://proxy.example.com"}',
});
const result = await dispatchRequest(request, 'https://target.example.com/v1/responses');
expect(result).toBe('ok');
expect(resolveChannelProxyUrlMock).toHaveBeenCalledWith(
site,
'{"proxyUrl":"http://proxy.example.com"}',
);
expect(dispatchRuntimeRequestMock).toHaveBeenCalledTimes(1);
const dispatchArg = dispatchRuntimeRequestMock.mock.calls[0]?.[0];
expect(dispatchArg.siteUrl).toBe('https://upstream.example.com');
expect(dispatchArg.targetUrl).toBe('https://target.example.com/v1/responses');
expect(dispatchArg.request).toBe(request);
return dispatchArg.buildInit('https://target.example.com/v1/responses', {
headers: { authorization: 'Bearer test' },
body: { model: 'gpt-5.2', input: 'hello' },
}).then((init: Record<string, unknown>) => {
expect(withSiteRecordProxyRequestInitMock).toHaveBeenCalledWith(site, {
method: 'POST',
headers: { authorization: 'Bearer test' },
body: JSON.stringify({ model: 'gpt-5.2', input: 'hello' }),
}, 'http://proxy.example.com');
expect(init).toEqual({
method: 'POST',
headers: { authorization: 'Bearer test' },
body: JSON.stringify({ model: 'gpt-5.2', input: 'hello' }),
proxyUrl: 'http://proxy.example.com',
});
});
});
it('retries retryable upstream HTTP failures through the shared failure toolkit', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
shouldRetryProxyRequestMock.mockReturnValue(true);
isTokenExpiredErrorMock.mockReturnValue(false);
recordOauthQuotaResetHintMock.mockResolvedValue(null);
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'chat',
downstreamPath: '/v1/chat/completions',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: 44,
});
const result = await toolkit.handleUpstreamFailure({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
status: 429,
errText: 'quota exceeded',
rawErrText: '{"error":"quota exceeded"}',
latencyMs: 1200,
retryCount: 0,
});
expect(result).toEqual({ action: 'retry' });
expect(recordFailureMock).toHaveBeenCalledWith(11, {
status: 429,
errorText: '{"error":"quota exceeded"}',
modelName: 'upstream-model',
});
expect(recordOauthQuotaResetHintMock).toHaveBeenCalledWith({
accountId: 33,
statusCode: 429,
errorText: '{"error":"quota exceeded"}',
});
expect(reportProxyAllFailedMock).not.toHaveBeenCalled();
expect(insertProxyLogMock).toHaveBeenCalledWith(expect.objectContaining({
channelId: 11,
accountId: 33,
downstreamApiKeyId: 44,
modelRequested: 'gpt-5.2',
modelActual: 'upstream-model',
status: 'failed',
httpStatus: 429,
latencyMs: 1200,
errorMessage: 'normalized error',
retryCount: 0,
}));
});
it('returns a terminal upstream error response and reports token expiration when retries stop', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
shouldRetryProxyRequestMock.mockReturnValue(false);
isTokenExpiredErrorMock.mockReturnValue(true);
recordOauthQuotaResetHintMock.mockResolvedValue(null);
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'responses',
downstreamPath: '/v1/responses',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: null,
});
const result = await toolkit.handleUpstreamFailure({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
status: 401,
errText: 'expired token',
rawErrText: 'expired token',
latencyMs: 900,
retryCount: 2,
});
expect(result).toEqual({
action: 'respond',
status: 401,
payload: {
error: {
message: 'expired token',
type: 'upstream_error',
},
},
});
expect(reportTokenExpiredMock).toHaveBeenCalledWith({
accountId: 33,
username: 'oauth-user',
siteName: 'Codex OAuth',
detail: 'HTTP 401',
});
expect(reportProxyAllFailedMock).toHaveBeenCalledWith({
model: 'gpt-5.2',
reason: 'upstream returned HTTP 401',
});
});
it('handles detected proxy failures through the shared failure toolkit', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
shouldRetryProxyRequestMock.mockReturnValue(false);
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'chat',
downstreamPath: '/v1/chat/completions',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: null,
});
const result = await toolkit.handleDetectedFailure({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
failure: {
status: 500,
reason: 'upstream failure',
},
latencyMs: 700,
retryCount: 2,
promptTokens: 12,
completionTokens: 4,
totalTokens: 16,
upstreamPath: '/v1/responses',
});
expect(result).toEqual({
action: 'respond',
status: 500,
payload: {
error: {
message: 'upstream failure',
type: 'upstream_error',
},
},
});
expect(recordFailureMock).toHaveBeenCalledWith(11, {
status: 500,
errorText: 'upstream failure',
modelName: 'upstream-model',
});
expect(reportProxyAllFailedMock).toHaveBeenCalledWith({
model: 'gpt-5.2',
reason: 'upstream failure',
});
expect(recordOauthQuotaResetHintMock).not.toHaveBeenCalled();
});
it('returns a terminal 502 for exhausted network failures through the shared failure toolkit', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'responses',
downstreamPath: '/v1/responses',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: null,
});
const result = await toolkit.handleExecutionError({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
errorMessage: 'socket hang up',
latencyMs: 650,
retryCount: 2,
});
expect(result).toEqual({
action: 'respond',
status: 502,
payload: {
error: {
message: 'Upstream error: socket hang up',
type: 'upstream_error',
},
},
});
expect(recordFailureMock).toHaveBeenCalledWith(11, {
errorText: 'socket hang up',
modelName: 'upstream-model',
});
expect(reportProxyAllFailedMock).toHaveBeenCalledWith({
model: 'gpt-5.2',
reason: 'socket hang up',
});
});
});
@@ -0,0 +1,394 @@
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import type { SiteProxyConfigLike } from '../../services/siteProxy.js';
import { tokenRouter } from '../../services/tokenRouter.js';
import type { DownstreamRoutingPolicy } from '../../services/downstreamPolicyTypes.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import type { DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';
import type { BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
type SelectedChannel = Awaited<ReturnType<typeof tokenRouter.selectChannel>>;
type SurfaceWarningScope = 'chat' | 'responses';
type SurfaceSelectedChannel = {
channel: { routeId: number | null; id: number };
account: { id: number; username?: string | null };
site: { name?: string | null };
actualModel?: string | null;
};
type SurfaceFailureResponse = {
action: 'respond';
status: number;
payload: {
error: {
message: string;
type: 'upstream_error';
};
};
};
type SurfaceFailureOutcome =
| { action: 'retry' }
| SurfaceFailureResponse;
export async function selectSurfaceChannelForAttempt(input: {
requestedModel: string;
downstreamPolicy: DownstreamRoutingPolicy;
excludeChannelIds: number[];
retryCount: number;
}): Promise<SelectedChannel> {
let selected = input.retryCount === 0
? await tokenRouter.selectChannel(input.requestedModel, input.downstreamPolicy)
: await tokenRouter.selectNextChannel(
input.requestedModel,
input.excludeChannelIds,
input.downstreamPolicy,
);
if (!selected && input.retryCount === 0) {
await refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(input.requestedModel, input.downstreamPolicy);
}
return selected;
}
export async function writeSurfaceProxyLog(input: {
warningScope: string;
selected: {
channel: { routeId: number | null; id: number | null };
account: { id: number | null };
actualModel?: string | null;
};
modelRequested: string;
status: string;
httpStatus: number;
latencyMs: number;
errorMessage: string | null;
retryCount: number;
downstreamPath: string;
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
estimatedCost?: number;
billingDetails?: unknown;
upstreamPath?: string | null;
clientContext?: DownstreamClientContext | null;
downstreamApiKeyId?: number | null;
}): Promise<void> {
try {
const createdAt = formatUtcSqlDateTime(new Date());
const normalizedErrorMessage = composeProxyLogMessage({
clientKind: input.clientContext?.clientKind && input.clientContext.clientKind !== 'generic'
? input.clientContext.clientKind
: null,
sessionId: input.clientContext?.sessionId || null,
traceHint: input.clientContext?.traceHint || null,
downstreamPath: input.downstreamPath,
upstreamPath: input.upstreamPath || null,
errorMessage: input.errorMessage,
});
await insertProxyLog({
routeId: input.selected.channel.routeId,
channelId: input.selected.channel.id,
accountId: input.selected.account.id,
downstreamApiKeyId: input.downstreamApiKeyId ?? null,
modelRequested: input.modelRequested,
modelActual: input.selected.actualModel ?? null,
status: input.status,
httpStatus: input.httpStatus,
latencyMs: input.latencyMs,
promptTokens: input.promptTokens ?? 0,
completionTokens: input.completionTokens ?? 0,
totalTokens: input.totalTokens ?? 0,
estimatedCost: input.estimatedCost ?? 0,
billingDetails: input.billingDetails ?? null,
clientFamily: input.clientContext?.clientKind || null,
clientAppId: input.clientContext?.clientAppId || null,
clientAppName: input.clientContext?.clientAppName || null,
clientConfidence: input.clientContext?.clientConfidence || null,
errorMessage: normalizedErrorMessage,
retryCount: input.retryCount,
createdAt,
});
} catch (error) {
console.warn(`[proxy/${input.warningScope}] failed to write proxy log`, error);
}
}
export function createSurfaceDispatchRequest(input: {
site: SiteProxyConfigLike & { url: string };
accountExtraConfig?: string | null;
}) {
const channelProxyUrl = resolveChannelProxyUrl(input.site, input.accountExtraConfig);
return (
request: BuiltEndpointRequest,
targetUrl?: string,
) => (
dispatchRuntimeRequest({
siteUrl: input.site.url,
targetUrl,
request,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(input.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}, channelProxyUrl),
})
);
}
export function createSurfaceFailureToolkit(input: {
warningScope: SurfaceWarningScope;
downstreamPath: string;
maxRetries: number;
clientContext?: DownstreamClientContext | null;
downstreamApiKeyId?: number | null;
}) {
const log = async (args: {
selected: SurfaceSelectedChannel;
modelRequested: string;
status: string;
httpStatus: number;
latencyMs: number;
errorMessage: string | null;
retryCount: number;
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
estimatedCost?: number;
billingDetails?: unknown;
upstreamPath?: string | null;
}) => {
await writeSurfaceProxyLog({
warningScope: input.warningScope,
selected: args.selected,
modelRequested: args.modelRequested,
status: args.status,
httpStatus: args.httpStatus,
latencyMs: args.latencyMs,
errorMessage: args.errorMessage,
retryCount: args.retryCount,
downstreamPath: input.downstreamPath,
promptTokens: args.promptTokens,
completionTokens: args.completionTokens,
totalTokens: args.totalTokens,
estimatedCost: args.estimatedCost,
billingDetails: args.billingDetails,
upstreamPath: args.upstreamPath,
clientContext: input.clientContext,
downstreamApiKeyId: input.downstreamApiKeyId,
});
};
const maybeRetry = (retryCount: number) => retryCount < input.maxRetries
? { action: 'retry' as const }
: null;
return {
log,
async handleUpstreamFailure(args: {
selected: SurfaceSelectedChannel;
requestedModel: string;
modelName: string;
status: number;
errText: string;
rawErrText?: string | null;
latencyMs: number;
retryCount: number;
}): Promise<SurfaceFailureOutcome> {
const rawErrText = args.rawErrText || args.errText;
tokenRouter.recordFailure(args.selected.channel.id, {
status: args.status,
errorText: rawErrText,
modelName: args.modelName,
});
await log({
selected: args.selected,
modelRequested: args.requestedModel,
status: 'failed',
httpStatus: args.status,
latencyMs: args.latencyMs,
errorMessage: args.errText,
retryCount: args.retryCount,
});
await recordOauthQuotaResetHint({
accountId: args.selected.account.id,
statusCode: args.status,
errorText: rawErrText,
});
if (isTokenExpiredError({ status: args.status, message: args.errText })) {
await reportTokenExpired({
accountId: args.selected.account.id,
username: args.selected.account.username,
siteName: args.selected.site.name,
detail: `HTTP ${args.status}`,
});
}
if (shouldRetryProxyRequest(args.status, args.errText)) {
const retry = maybeRetry(args.retryCount);
if (retry) return retry;
}
await reportProxyAllFailed({
model: args.requestedModel,
reason: `upstream returned HTTP ${args.status}`,
});
return {
action: 'respond',
status: args.status,
payload: {
error: {
message: args.errText,
type: 'upstream_error',
},
},
};
},
async handleDetectedFailure(args: {
selected: SurfaceSelectedChannel;
requestedModel: string;
modelName: string;
failure: { status: number; reason: string };
latencyMs: number;
retryCount: number;
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
upstreamPath?: string | null;
}): Promise<SurfaceFailureOutcome> {
tokenRouter.recordFailure(args.selected.channel.id, {
status: args.failure.status,
errorText: args.failure.reason,
modelName: args.modelName,
});
await log({
selected: args.selected,
modelRequested: args.requestedModel,
status: 'failed',
httpStatus: args.failure.status,
latencyMs: args.latencyMs,
errorMessage: args.failure.reason,
retryCount: args.retryCount,
promptTokens: args.promptTokens,
completionTokens: args.completionTokens,
totalTokens: args.totalTokens,
upstreamPath: args.upstreamPath,
});
if (shouldRetryProxyRequest(args.failure.status, args.failure.reason)) {
const retry = maybeRetry(args.retryCount);
if (retry) return retry;
}
await reportProxyAllFailed({
model: args.requestedModel,
reason: args.failure.reason,
});
return {
action: 'respond',
status: args.failure.status,
payload: {
error: {
message: args.failure.reason,
type: 'upstream_error',
},
},
};
},
async handleExecutionError(args: {
selected: SurfaceSelectedChannel;
requestedModel: string;
modelName: string;
errorMessage: string;
latencyMs: number;
retryCount: number;
}): Promise<SurfaceFailureOutcome> {
tokenRouter.recordFailure(args.selected.channel.id, {
errorText: args.errorMessage,
modelName: args.modelName,
});
await log({
selected: args.selected,
modelRequested: args.requestedModel,
status: 'failed',
httpStatus: 0,
latencyMs: args.latencyMs,
errorMessage: args.errorMessage,
retryCount: args.retryCount,
});
const retry = maybeRetry(args.retryCount);
if (retry) return retry;
await reportProxyAllFailed({
model: args.requestedModel,
reason: args.errorMessage || 'network failure',
});
return {
action: 'respond',
status: 502,
payload: {
error: {
message: `Upstream error: ${args.errorMessage || 'network failure'}`,
type: 'upstream_error',
},
},
};
},
async recordStreamFailure(args: {
selected: SurfaceSelectedChannel;
requestedModel: string;
modelName: string;
errorMessage: string | null;
latencyMs: number;
retryCount: number;
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
upstreamPath?: string | null;
httpStatus?: number;
runtimeFailureStatus?: number | null;
}) {
const errorMessage = args.errorMessage || 'stream processing failed';
if (typeof args.runtimeFailureStatus === 'number') {
tokenRouter.recordFailure(args.selected.channel.id, {
status: args.runtimeFailureStatus,
errorText: errorMessage,
modelName: args.modelName,
});
} else {
tokenRouter.recordFailure(args.selected.channel.id, args.modelName);
}
await log({
selected: args.selected,
modelRequested: args.requestedModel,
status: 'failed',
httpStatus: args.httpStatus ?? 200,
latencyMs: args.latencyMs,
errorMessage,
retryCount: args.retryCount,
promptTokens: args.promptTokens,
completionTokens: args.completionTokens,
totalTokens: args.totalTokens,
upstreamPath: args.upstreamPath,
});
},
};
}