feat(server): add proactive channel probes and load-aware stable routing / 服务端新增主动探活与负载感知稳定路由 (#330)

* wip: preserve local changes before upstream merge

* Improve proxy routing stability and add stable-first strategy

* Sync explicit-group routing strategy to eligible source routes

* Fix proxy retry and runtime health review issues

* fix: make proxy channel retry attempts configurable

* fix: harden proxy retry recovery and bookkeeping

* fix: address pr 257 review comments

* feat(proxy): add sticky session channel coordination

* feat(server): add proactive channel probes and load-aware stable routing

* test(server): stabilize session load routing selection assertion

* fix(proxy): harden codex forwarding stability

* fix(router): rotate stable-first channels by healthy order

* fix(server): address probe review feedback

* fix(server): address probe review follow-ups

* fix(ci): address CodeQL and core test failures

* feat(server): refine stable-first routing recovery and observation

* Finish PR 330 settings and review fixes

* Fix chat site endpoint CI mock

* Address latest PR 330 review comments

---------

Co-authored-by: Cita <juricek.chen@gmail.com>
This commit is contained in:
Bainily
2026-04-02 09:49:04 +08:00
committed by GitHub
parent 1133846998
commit 596d1b93cb
75 changed files with 6365 additions and 507 deletions
@@ -0,0 +1,233 @@
import { fetch } from 'undici';
import { readRuntimeResponseText } from '../executors/types.js';
import { withSiteProxyRequestInit } from '../../services/siteProxy.js';
import {
buildUpstreamUrl,
summarizeUpstreamError,
type UpstreamEndpoint,
} from './upstreamRequest.js';
export type BuiltEndpointRequest = {
endpoint: UpstreamEndpoint;
path: string;
headers: Record<string, string>;
body: Record<string, unknown>;
runtime?: {
executor: 'default' | 'codex' | 'gemini-cli' | 'antigravity' | 'claude';
modelName?: string;
stream?: boolean;
oauthProjectId?: string | null;
action?: 'generateContent' | 'streamGenerateContent' | 'countTokens';
};
};
export type EndpointAttemptContext = {
endpointIndex: number;
endpointCount: number;
request: BuiltEndpointRequest;
targetUrl: string;
response: Awaited<ReturnType<typeof fetch>>;
rawErrText: string;
recoverApplied?: boolean;
};
export type EndpointAttemptSuccessContext = {
endpointIndex: number;
endpointCount: number;
request: BuiltEndpointRequest;
targetUrl: string;
response: Awaited<ReturnType<typeof fetch>>;
recoverApplied?: boolean;
};
export type EndpointRecoverResult = {
upstream: Awaited<ReturnType<typeof fetch>>;
upstreamPath: string;
request?: BuiltEndpointRequest;
targetUrl?: string;
} | null;
export type EndpointFlowResult =
| {
ok: true;
upstream: Awaited<ReturnType<typeof fetch>>;
upstreamPath: string;
}
| {
ok: false;
status: number;
errText: string;
rawErrText?: string;
};
export type ExecuteEndpointFlowInput = {
siteUrl: string;
proxyUrl?: string | null;
disableCrossProtocolFallback?: boolean;
endpointCandidates: UpstreamEndpoint[];
buildRequest: (endpoint: UpstreamEndpoint, endpointIndex: number) => BuiltEndpointRequest;
dispatchRequest?: (
request: BuiltEndpointRequest,
targetUrl: string,
) => Promise<Awaited<ReturnType<typeof fetch>>>;
tryRecover?: (ctx: EndpointAttemptContext) => Promise<EndpointRecoverResult>;
shouldDowngrade?: (ctx: EndpointAttemptContext) => boolean;
shouldAbortRemainingEndpoints?: (ctx: EndpointAttemptContext & { errText: string }) => boolean;
onDowngrade?: (ctx: EndpointAttemptContext & { errText: string }) => void | Promise<void>;
onAttemptFailure?: (ctx: EndpointAttemptContext & { errText: string }) => void | Promise<void>;
onAttemptSuccess?: (ctx: EndpointAttemptSuccessContext) => void | Promise<void>;
};
export function withUpstreamPath(path: string, message: string): string {
return `[upstream:${path}] ${message}`;
}
async function runEndpointFlowHook<T>(
hook: ((ctx: T) => void | Promise<void>) | undefined,
ctx: T,
hookName: string,
): Promise<void> {
if (!hook) return;
try {
await hook(ctx);
} catch (error) {
console.error(`endpointFlow ${hookName} hook failed`, error);
}
}
export async function executeEndpointFlow(input: ExecuteEndpointFlowInput): Promise<EndpointFlowResult> {
const endpointCount = input.endpointCandidates.length;
if (endpointCount <= 0) {
return {
ok: false,
status: 502,
errText: 'Upstream request failed',
};
}
let finalStatus = 0;
let finalErrText = 'unknown error';
let finalRawErrText: string | undefined;
for (let endpointIndex = 0; endpointIndex < endpointCount; endpointIndex += 1) {
const endpoint = input.endpointCandidates[endpointIndex] as UpstreamEndpoint;
const request = input.buildRequest(endpoint, endpointIndex);
const defaultTarget = buildUpstreamUrl(input.siteUrl, request.path);
const targetUrl = input.proxyUrl
? buildUpstreamUrl(input.proxyUrl, request.path)
: defaultTarget;
let response = input.dispatchRequest
? await input.dispatchRequest(request, targetUrl)
: await fetch(targetUrl, await withSiteProxyRequestInit(targetUrl, {
method: 'POST',
headers: request.headers,
body: JSON.stringify(request.body),
}));
if (response.ok) {
await runEndpointFlowHook(input.onAttemptSuccess, {
endpointIndex,
endpointCount,
request,
targetUrl,
response,
recoverApplied: false,
}, 'onAttemptSuccess');
return {
ok: true,
upstream: response,
upstreamPath: request.path,
};
}
let rawErrText = await readRuntimeResponseText(response).catch(() => 'unknown error');
const baseContext: EndpointAttemptContext = {
endpointIndex,
endpointCount,
request,
targetUrl,
response,
rawErrText,
recoverApplied: false,
};
if (input.tryRecover) {
const recovered = await input.tryRecover(baseContext);
baseContext.recoverApplied = recovered !== null
|| baseContext.request !== request
|| baseContext.response !== response
|| baseContext.rawErrText !== rawErrText;
if (recovered?.upstream?.ok) {
const recoveredRequest = recovered.request ?? baseContext.request;
const recoveredTargetUrl = recovered.targetUrl ?? (
input.proxyUrl
? buildUpstreamUrl(input.proxyUrl, recovered.upstreamPath)
: buildUpstreamUrl(input.siteUrl, recovered.upstreamPath)
);
await runEndpointFlowHook(input.onAttemptSuccess, {
endpointIndex,
endpointCount,
request: recoveredRequest,
targetUrl: recoveredTargetUrl,
response: recovered.upstream,
recoverApplied: true,
}, 'onAttemptSuccess');
return {
ok: true,
upstream: recovered.upstream,
upstreamPath: recovered.upstreamPath,
};
}
}
rawErrText = baseContext.rawErrText;
response = baseContext.response;
const errText = withUpstreamPath(
baseContext.request.path,
summarizeUpstreamError(response.status, rawErrText),
);
await runEndpointFlowHook(input.onAttemptFailure, {
...baseContext,
errText,
}, 'onAttemptFailure');
const isLastEndpoint = endpointIndex >= endpointCount - 1;
if (input.disableCrossProtocolFallback && !isLastEndpoint) {
finalStatus = response.status;
finalErrText = errText;
finalRawErrText = rawErrText;
break;
}
const shouldAbortRemainingEndpoints = !isLastEndpoint && !!input.shouldAbortRemainingEndpoints?.({
...baseContext,
errText,
});
if (shouldAbortRemainingEndpoints) {
finalStatus = response.status;
finalErrText = errText;
finalRawErrText = rawErrText;
break;
}
const shouldDowngrade = !isLastEndpoint && !!input.shouldDowngrade?.(baseContext);
if (shouldDowngrade) {
await runEndpointFlowHook(input.onDowngrade, {
...baseContext,
errText,
}, 'onDowngrade');
continue;
}
finalStatus = response.status;
finalErrText = errText;
finalRawErrText = rawErrText;
break;
}
return {
ok: false,
status: finalStatus || 502,
errText: finalErrText || 'unknown error',
rawErrText: finalRawErrText,
};
}
@@ -0,0 +1,138 @@
export type UpstreamEndpoint = 'chat' | 'messages' | 'responses';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function collapseWhitespace(text: string): string {
return text.replace(/\s+/g, ' ').trim();
}
function extractJsonErrorMessage(rawText: string): string {
try {
const parsed = JSON.parse(rawText) as unknown;
const root = isRecord(parsed) ? parsed : null;
const error = (root && isRecord(root.error)) ? root.error : root;
if (!error) return '';
const message = typeof error.message === 'string' ? collapseWhitespace(error.message) : '';
if (message) return message;
const code = typeof error.code === 'string' ? collapseWhitespace(error.code) : '';
const type = typeof error.type === 'string' ? collapseWhitespace(error.type) : '';
return [type, code].filter((part) => part.length > 0).join('/');
} catch {
return '';
}
}
function extractHtmlTitle(rawText: string): string {
const match = rawText.match(/<title[^>]*>([^<>]*)<\/title>/i);
if (!match?.[1]) return '';
return collapseWhitespace(match[1]);
}
function extractCloudflareHtmlSummary(rawText: string, status: number): string {
if (!/cloudflare/i.test(rawText)) return '';
const title = extractHtmlTitle(rawText);
const codeMatch = (
title.match(/\b(\d{3,4})\s*:\s*([^\|<]+)/i)
|| rawText.match(/Error code\s*(\d{3,4})/i)
);
const code = codeMatch?.[1] || (status > 0 ? String(status) : '');
const reason = collapseWhitespace(
(typeof codeMatch?.[2] === 'string' ? codeMatch[2] : '')
|| (status >= 500 ? 'origin host error' : 'request blocked')
);
if (code) return `Cloudflare ${code}: ${reason}`;
return `Cloudflare: ${reason}`;
}
function extractHtmlSummary(rawText: string, status: number): string {
if (!/(<!doctype|<html)/i.test(rawText)) return '';
const cloudflareSummary = extractCloudflareHtmlSummary(rawText, status);
if (cloudflareSummary) return cloudflareSummary;
const title = extractHtmlTitle(rawText);
if (title) return title;
const heading = rawText.match(/<h1[^>]*>([^<>]*)<\/h1>/i)?.[1] || '';
return collapseWhitespace(heading);
}
function formatUrlOrigin(url: URL): string {
const username = url.username ? encodeURIComponent(url.username) : '';
const password = url.password ? encodeURIComponent(url.password) : '';
const auth = username
? `${username}${password ? `:${password}` : ''}@`
: '';
return `${url.protocol}//${auth}${url.host}`;
}
function joinPath(basePath: string, requestPath: string): string {
const base = basePath.replace(/\/+$/, '');
const path = requestPath.startsWith('/') ? requestPath : `/${requestPath}`;
if (!base || base === '/') return path || '/';
if (!path || path === '/') return base;
return `${base}${path}`;
}
export function summarizeUpstreamError(status: number, rawErrorText: string): string {
const statusPrefix = status > 0
? `Upstream returned HTTP ${status}`
: 'Upstream request failed';
const raw = typeof rawErrorText === 'string' ? rawErrorText.trim() : '';
if (!raw) return statusPrefix;
const jsonMessage = extractJsonErrorMessage(raw);
if (jsonMessage) return `${statusPrefix}: ${jsonMessage}`;
const htmlMessage = extractHtmlSummary(raw, status);
if (htmlMessage) return `${statusPrefix}: ${htmlMessage}`;
const compact = collapseWhitespace(raw);
if (!compact) return statusPrefix;
if (compact.length <= 400) return `${statusPrefix}: ${compact}`;
return `${statusPrefix}: ${compact.slice(0, 400)}...(truncated)`;
}
export function buildUpstreamUrl(siteUrl: string, requestPath: string): string {
const baseRaw = typeof siteUrl === 'string' ? siteUrl.trim() : '';
const pathRaw = typeof requestPath === 'string' ? requestPath.trim() : '';
const fallbackBase = baseRaw.replace(/\/+$/, '');
let path = pathRaw.startsWith('/') ? pathRaw : `/${pathRaw}`;
if (!fallbackBase) return path || '/';
if (!path || path === '/') return fallbackBase;
try {
const parsed = new URL(baseRaw);
const basePath = parsed.pathname.replace(/\/+$/, '');
const baseHasVersionSuffix = /\/(?:api\/)?v1$/i.test(basePath);
if (baseHasVersionSuffix) {
if (path === '/v1') {
path = '/';
} else if (path.startsWith('/v1/')) {
path = path.slice('/v1'.length) || '/';
}
}
const joinedPath = joinPath(basePath, path);
return `${formatUrlOrigin(parsed)}${joinedPath}${parsed.search}${parsed.hash}`;
} catch {
const baseHasVersionSuffix = /\/(?:api\/)?v1$/i.test(fallbackBase);
if (baseHasVersionSuffix) {
if (path === '/v1') {
path = '/';
} else if (path.startsWith('/v1/')) {
path = path.slice('/v1'.length) || '/';
}
}
return `${fallbackBase}${path}`;
}
}
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import {
buildCodexSessionResponseStoreKey,
getCodexSessionResponseId,
resetCodexSessionResponseStore,
setCodexSessionResponseId,
} from './codexSessionResponseStore.js';
describe('codexSessionResponseStore', () => {
it('evicts the oldest session id when the store exceeds the cap', () => {
resetCodexSessionResponseStore();
for (let index = 0; index <= 10_000; index += 1) {
setCodexSessionResponseId(`session-${index}`, `resp-${index}`);
}
expect(getCodexSessionResponseId('session-0')).toBeNull();
expect(getCodexSessionResponseId('session-1')).toBe('resp-1');
expect(getCodexSessionResponseId('session-10000')).toBe('resp-10000');
resetCodexSessionResponseStore();
});
it('namespaces identical downstream session ids by channel scope', () => {
resetCodexSessionResponseStore();
const keyA = buildCodexSessionResponseStoreKey({
sessionId: 'session-1',
siteId: 10,
accountId: 20,
channelId: 30,
});
const keyB = buildCodexSessionResponseStoreKey({
sessionId: 'session-1',
siteId: 10,
accountId: 21,
channelId: 31,
});
setCodexSessionResponseId(keyA, 'resp-a');
setCodexSessionResponseId(keyB, 'resp-b');
expect(getCodexSessionResponseId(keyA)).toBe('resp-a');
expect(getCodexSessionResponseId(keyB)).toBe('resp-b');
resetCodexSessionResponseStore();
});
});
@@ -0,0 +1,55 @@
const MAX_CODEX_SESSION_RESPONSE_IDS = 10_000;
const codexSessionResponseIds = new Map<string, string>();
function normalizeSessionId(sessionId: string): string {
return sessionId.trim();
}
export function buildCodexSessionResponseStoreKey(input: {
sessionId: string;
siteId?: number | null;
accountId?: number | null;
channelId?: number | null;
}): string {
const normalizedSessionId = normalizeSessionId(input.sessionId);
if (!normalizedSessionId) return '';
const parts = [
Number.isFinite(input.siteId as number) && Number(input.siteId) > 0 ? `site:${Math.trunc(Number(input.siteId))}` : '',
Number.isFinite(input.accountId as number) && Number(input.accountId) > 0 ? `account:${Math.trunc(Number(input.accountId))}` : '',
Number.isFinite(input.channelId as number) && Number(input.channelId) > 0 ? `channel:${Math.trunc(Number(input.channelId))}` : '',
`session:${normalizedSessionId}`,
].filter(Boolean);
return parts.join('|');
}
export function getCodexSessionResponseId(sessionId: string): string | null {
const normalized = normalizeSessionId(sessionId);
if (!normalized) return null;
return codexSessionResponseIds.get(normalized) ?? null;
}
export function setCodexSessionResponseId(sessionId: string, responseId: string): void {
const normalizedSessionId = normalizeSessionId(sessionId);
const normalizedResponseId = responseId.trim();
if (!normalizedSessionId || !normalizedResponseId) return;
if (codexSessionResponseIds.has(normalizedSessionId)) {
codexSessionResponseIds.delete(normalizedSessionId);
}
codexSessionResponseIds.set(normalizedSessionId, normalizedResponseId);
while (codexSessionResponseIds.size > MAX_CODEX_SESSION_RESPONSE_IDS) {
const oldestKey = codexSessionResponseIds.keys().next().value;
if (!oldestKey) break;
codexSessionResponseIds.delete(oldestKey);
}
}
export function clearCodexSessionResponseId(sessionId: string): void {
const normalized = normalizeSessionId(sessionId);
if (!normalized) return;
codexSessionResponseIds.delete(normalized);
}
export function resetCodexSessionResponseStore(): void {
codexSessionResponseIds.clear();
}
@@ -1,6 +1,7 @@
import { AddressInfo } from 'node:net';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { WebSocketServer } from 'ws';
import { resetCodexSessionResponseStore } from './codexSessionResponseStore.js';
describe('codexWebsocketRuntime', () => {
let upstreamServer: WebSocketServer;
@@ -25,6 +26,7 @@ describe('codexWebsocketRuntime', () => {
});
beforeEach(() => {
resetCodexSessionResponseStore();
upstreamConnectionCount = 0;
upstreamRequests = [];
upstreamMessageHandler = (socket, parsed, requestIndex) => {
@@ -1,10 +1,22 @@
import type { IncomingMessage } from 'node:http';
import WebSocket from 'ws';
import {
extractResponsesTerminalResponseId,
isResponsesPreviousResponseNotFoundError,
shouldInferResponsesPreviousResponseId,
stripResponsesPreviousResponseId,
withResponsesPreviousResponseId,
} from '../../transformers/openai/responses/continuation.js';
import {
buildCodexWebsocketHandshakeHeaders,
buildCodexWebsocketRequestBody,
toCodexWebsocketUrl,
} from './codexWebsocketHeaders.js';
import {
clearCodexSessionResponseId,
getCodexSessionResponseId,
setCodexSessionResponseId,
} from './codexSessionResponseStore.js';
import { createCodexWebsocketSessionStore } from './codexWebsocketSessionStore.js';
import type {
CodexWebsocketRuntimeResult,
@@ -190,6 +202,23 @@ function clearSessionSocket(session: CodexWebsocketSession, socket: WebSocket):
session.socketUrl = null;
}
function buildContinuationAwareRuntimeBody(
sessionId: string,
body: Record<string, unknown>,
): Record<string, unknown> {
const rememberedResponseId = getCodexSessionResponseId(sessionId);
if (!shouldInferResponsesPreviousResponseId(body, rememberedResponseId)) {
return body;
}
return withResponsesPreviousResponseId(body, rememberedResponseId);
}
function rememberSessionResponseId(sessionId: string, payload: unknown): void {
const responseId = extractResponsesTerminalResponseId(payload);
if (!responseId) return;
setCodexSessionResponseId(sessionId, responseId);
}
async function ensureSessionSocket(
session: CodexWebsocketSession,
input: CodexWebsocketRuntimeSendInput,
@@ -232,9 +261,11 @@ async function ensureSessionSocket(
};
}
async function sendSessionRequest(
async function sendSessionRequestAttempt(
session: CodexWebsocketSession,
input: CodexWebsocketRuntimeSendInput,
input: CodexWebsocketRuntimeSendInput & {
body: Record<string, unknown>;
},
): Promise<CodexWebsocketRuntimeResult> {
const { socket, reusedSession } = await ensureSessionSocket(session, input);
const events: Array<Record<string, unknown>> = [];
@@ -262,7 +293,13 @@ async function sendSessionRequest(
events.push(parsed);
if (!isTerminalEvent(parsed)) return;
if (settled) return;
if (isRuntimeErrorEvent(parsed)) {
if (
isRuntimeErrorEvent(parsed)
|| isResponsesPreviousResponseNotFoundError({
payload: parsed,
rawErrText: extractTerminalErrorMessage(parsed),
})
) {
settled = true;
cleanup();
clearSessionSocket(session, socket);
@@ -274,6 +311,7 @@ async function sendSessionRequest(
}));
return;
}
rememberSessionResponseId(session.sessionId, parsed);
settled = true;
cleanup();
resolve({
@@ -307,6 +345,43 @@ async function sendSessionRequest(
});
}
async function sendSessionRequest(
session: CodexWebsocketSession,
input: CodexWebsocketRuntimeSendInput,
): Promise<CodexWebsocketRuntimeResult> {
let currentBody = buildContinuationAwareRuntimeBody(session.sessionId, input.body);
let previousResponseRecoveryTried = false;
for (;;) {
try {
return await sendSessionRequestAttempt(session, {
...input,
body: currentBody,
});
} catch (error) {
if (
previousResponseRecoveryTried
|| !(error instanceof CodexWebsocketRuntimeError)
|| !isResponsesPreviousResponseNotFoundError({
payload: error.payload ?? error.events[error.events.length - 1],
rawErrText: error.message,
})
) {
throw error;
}
const previousResponseRecovery = stripResponsesPreviousResponseId(currentBody);
if (!previousResponseRecovery.removed) {
throw error;
}
previousResponseRecoveryTried = true;
clearCodexSessionResponseId(session.sessionId);
currentBody = previousResponseRecovery.body;
}
}
}
export function createCodexWebsocketRuntime(input?: {
sessionStore?: CodexWebsocketSessionStore;
}) {
@@ -334,6 +409,7 @@ export function createCodexWebsocketRuntime(input?: {
await closeSocket(session.socket);
session.socket = null;
session.socketUrl = null;
clearCodexSessionResponseId(sessionId);
},
async closeAllSessions(): Promise<void> {
+15 -2
View File
@@ -9,7 +9,7 @@ import {
buildClaudeCountTokensUpstreamRequest,
buildUpstreamEndpointRequest,
resolveUpstreamEndpointCandidates,
} from '../../routes/proxy/upstreamEndpoint.js';
} from '../../services/upstreamEndpointRuntime.js';
import {
getUpstreamEndpointRuntimeStateSnapshot,
recordUpstreamEndpointFailure,
@@ -20,10 +20,11 @@ import {
getDownstreamRoutingPolicy,
recordDownstreamCostUsage,
} from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { openAiChatTransformer } from '../../transformers/openai/chat/index.js';
import { anthropicMessagesTransformer } from '../../transformers/anthropic/messages/index.js';
import { shouldPreferResponsesForAnthropicContinuation } from '../../transformers/anthropic/messages/compatibility.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
import {
ProxyInputFileResolutionError,
@@ -47,6 +48,7 @@ import { summarizeConversationFileInputsInOpenAiBody } from '../capabilities/con
import { getRuntimeResponseReader, readRuntimeResponseText } from '../executors/types.js';
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { shouldAbortSameSiteEndpointFallback } from '../../services/proxyRetryPolicy.js';
import {
acquireSurfaceChannelLease,
bindSurfaceStickyChannel,
@@ -170,6 +172,10 @@ export async function handleChatSurfaceRequest(
}
const conversationFileSummary = summarizeConversationFileInputsInOpenAiBody(resolvedOpenAiBody);
const hasNonImageFileInput = conversationFileSummary.hasDocument;
const wantsContinuationAwareResponses = (
downstreamFormat === 'claude'
&& shouldPreferResponsesForAnthropicContinuation(claudeOriginalBody)
);
const codexSessionCacheKey = deriveCodexSessionCacheKey({
downstreamFormat,
body: downstreamFormat === 'claude' ? claudeOriginalBody : request.body,
@@ -283,6 +289,7 @@ export async function handleChatSurfaceRequest(
{
hasNonImageFileInput,
conversationFileSummary,
wantsContinuationAwareResponses,
},
),
];
@@ -297,6 +304,7 @@ export async function handleChatSurfaceRequest(
requestCapabilities: {
hasNonImageFileInput,
conversationFileSummary,
wantsContinuationAwareResponses,
},
};
await safeUpdateSurfaceProxyDebugCandidates(debugTrace, {
@@ -309,6 +317,7 @@ export async function handleChatSurfaceRequest(
stickyPreferredChannelId,
oauthProvider: oauth?.provider || null,
isCodexSite,
wantsContinuationAwareResponses,
},
});
const buildProviderHeaders = () => (
@@ -389,6 +398,10 @@ export async function handleChatSurfaceRequest(
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
tryRecover,
shouldAbortRemainingEndpoints: (ctx) => shouldAbortSameSiteEndpointFallback(
ctx.response.status,
ctx.rawErrText || ctx.errText,
),
onAttemptFailure: async (ctx) => {
const memoryWrite = recordUpstreamEndpointFailure({
...endpointRuntimeContext,
@@ -14,12 +14,12 @@ import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refres
import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { getDownstreamRoutingPolicy } from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import {
buildUpstreamEndpointRequest,
resolveUpstreamEndpointCandidates,
} from '../../routes/proxy/upstreamEndpoint.js';
} from '../../services/upstreamEndpointRuntime.js';
import {
getUpstreamEndpointRuntimeStateSnapshot,
recordUpstreamEndpointFailure,
@@ -35,12 +35,13 @@ import {
unwrapGeminiCliPayload,
wrapGeminiCliRequest,
} from '../../routes/proxy/geminiCliCompat.js';
import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';
import { dispatchRuntimeRequest } from '../../services/runtimeDispatch.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { summarizeConversationFileInputsInOpenAiBody } from '../capabilities/conversationFileCapabilities.js';
import { getRuntimeResponseReader, readRuntimeResponseText } from '../executors/types.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { shouldAbortSameSiteEndpointFallback } from '../../services/proxyRetryPolicy.js';
import {
buildSurfaceProxyDebugResponseHeaders,
captureSurfaceProxyDebugSuccessResponseBody,
@@ -1224,6 +1225,10 @@ export async function geminiProxyRoute(app: FastifyInstance) {
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
tryRecover: endpointStrategy.tryRecover,
shouldAbortRemainingEndpoints: (ctx) => shouldAbortSameSiteEndpointFallback(
ctx.response.status,
ctx.rawErrText || ctx.errText,
),
onAttemptFailure: async (ctx) => {
const memoryWrite = recordUpstreamEndpointFailure({
...endpointRuntimeContext,
@@ -4,17 +4,24 @@ import { config } from '../../config.js';
import { reportProxyAllFailed } from '../../services/alertService.js';
import { hasProxyUsagePayload, mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js';
import {
extractResponsesTerminalResponseId,
isResponsesPreviousResponseNotFoundError,
shouldInferResponsesPreviousResponseId,
stripResponsesPreviousResponseId,
withResponsesPreviousResponseId,
} from '../../transformers/openai/responses/continuation.js';
import {
buildUpstreamEndpointRequest,
resolveUpstreamEndpointCandidates,
} from '../../routes/proxy/upstreamEndpoint.js';
} from '../../services/upstreamEndpointRuntime.js';
import {
getUpstreamEndpointRuntimeStateSnapshot,
recordUpstreamEndpointFailure,
recordUpstreamEndpointSuccess,
} from '../../services/upstreamEndpointRuntimeMemory.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
import { normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
@@ -39,12 +46,19 @@ import {
import { isCodexResponsesSurface } from '../cliProfiles/codexProfile.js';
import { getRuntimeResponseReader, readRuntimeResponseText } from '../executors/types.js';
import { runCodexHttpSessionTask } from '../runtime/codexHttpSessionQueue.js';
import {
buildCodexSessionResponseStoreKey,
clearCodexSessionResponseId,
getCodexSessionResponseId,
setCodexSessionResponseId,
} from '../runtime/codexSessionResponseStore.js';
import {
summarizeConversationFileInputsInOpenAiBody,
summarizeConversationFileInputsInResponsesBody,
} from '../capabilities/conversationFileCapabilities.js';
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { shouldAbortSameSiteEndpointFallback } from '../../services/proxyRetryPolicy.js';
import {
acquireSurfaceChannelLease,
bindSurfaceStickyChannel,
@@ -96,6 +110,12 @@ function isResponsesWebsocketTransportRequest(headers: Record<string, unknown>):
&& String(rawValue).trim() === '1');
}
function rememberCodexSessionResponseId(sessionId: string, payload: unknown): void {
const responseId = extractResponsesTerminalResponseId(payload);
if (!responseId) return;
setCodexSessionResponseId(sessionId, responseId);
}
function normalizeIncludeList(value: unknown): string[] {
if (typeof value === 'string') {
const trimmed = value.trim();
@@ -345,6 +365,20 @@ export async function handleOpenAiResponsesSurfaceRequest(
const modelName = selected.actualModel || requestedModel;
const oauth = getOauthInfoFromAccount(selected.account);
const isCodexSite = String(selected.site.platform || '').trim().toLowerCase() === 'codex';
const codexSessionId = isCodexSite
? getCodexSessionHeaderValue(request.headers as Record<string, string>)
: '';
const codexSessionStoreKey = (
isCodexSite
&& codexSessionId
)
? buildCodexSessionResponseStoreKey({
sessionId: codexSessionId,
siteId: selected.site.id,
accountId: selected.account.id,
channelId: selected.channel.id,
})
: '';
const owner = getProxyResourceOwner(request);
let normalizedResponsesBody: Record<string, unknown> = {
...requestEnvelope.parsed.normalizedBody,
@@ -427,6 +461,20 @@ export async function handleOpenAiResponsesSurfaceRequest(
const executeEndpointResultForSiteApiBaseUrl = async (siteApiBaseUrl: string) => {
const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => {
const upstreamStream = isStream || (isCodexSite && endpoint === 'responses');
const responsesOriginalBody = (
endpoint === 'responses'
&& isCodexSite
&& codexSessionStoreKey
&& shouldInferResponsesPreviousResponseId(
normalizedResponsesBody,
getCodexSessionResponseId(codexSessionStoreKey),
)
)
? withResponsesPreviousResponseId(
normalizedResponsesBody,
getCodexSessionResponseId(codexSessionStoreKey)!,
)
: normalizedResponsesBody;
const endpointRequest = buildUpstreamEndpointRequest({
endpoint,
modelName,
@@ -438,7 +486,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
siteUrl: siteApiBaseUrl,
openaiBody: openAiBody,
downstreamFormat: 'responses',
responsesOriginalBody: normalizedResponsesBody,
responsesOriginalBody,
downstreamHeaders: request.headers as Record<string, unknown>,
providerHeaders: buildProviderHeaders(),
});
@@ -469,7 +517,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
}
const sessionId = getCodexSessionHeaderValue(endpointRequest.headers);
return runCodexHttpSessionTask(
sessionId,
codexSessionStoreKey || sessionId,
() => baseDispatchRequest(endpointRequest, targetUrl),
);
};
@@ -496,6 +544,35 @@ export async function handleOpenAiResponsesSurfaceRequest(
return recovered;
}
}
if (
ctx.request.endpoint === 'responses'
&& isResponsesPreviousResponseNotFoundError({
rawErrText: ctx.rawErrText,
})
) {
if (codexSessionStoreKey) {
clearCodexSessionResponseId(codexSessionStoreKey);
}
const previousResponseRecovery = stripResponsesPreviousResponseId(ctx.request.body);
if (previousResponseRecovery.removed) {
const recoveredRequest = {
...ctx.request,
body: previousResponseRecovery.body,
};
const recoveredResponse = await dispatchRequest(recoveredRequest, ctx.targetUrl);
if (recoveredResponse.ok) {
return {
upstream: recoveredResponse,
upstreamPath: recoveredRequest.path,
request: recoveredRequest,
targetUrl: ctx.targetUrl,
};
}
ctx.request = recoveredRequest;
ctx.response = recoveredResponse;
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
}
}
return endpointStrategy.tryRecover(ctx);
};
@@ -507,6 +584,10 @@ export async function handleOpenAiResponsesSurfaceRequest(
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
tryRecover,
shouldAbortRemainingEndpoints: (ctx) => shouldAbortSameSiteEndpointFallback(
ctx.response.status,
ctx.rawErrText || ctx.errText,
),
onAttemptFailure: async (ctx) => {
const memoryWrite = recordUpstreamEndpointFailure({
...endpointRuntimeContext,
@@ -699,6 +780,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
if (payload && typeof payload === 'object') {
upstreamUsagePresent = upstreamUsagePresent || hasProxyUsagePayload(payload);
parsedUsage = mergeProxyUsage(parsedUsage, parseProxyUsage(payload));
if (codexSessionStoreKey) {
rememberCodexSessionResponseId(codexSessionStoreKey, payload);
}
}
},
writeLines,
@@ -762,6 +846,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
if (String(selected.site.platform || '').trim().toLowerCase() === 'gemini-cli') {
upstreamData = unwrapGeminiCliPayload(upstreamData);
}
if (codexSessionStoreKey) {
rememberCodexSessionResponseId(codexSessionStoreKey, upstreamData);
}
parsedUsage = parseProxyUsage(upstreamData);
upstreamUsagePresent = upstreamUsagePresent || hasProxyUsagePayload(upstreamData);
@@ -867,6 +954,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
`event: ${terminalEventType}\ndata: ${JSON.stringify({ type: terminalEventType, response: collectedPayload })}\n\n`,
'data: [DONE]\n\n',
]);
if (codexSessionStoreKey) {
rememberCodexSessionResponseId(codexSessionStoreKey, collectedPayload);
}
reply.raw.end();
const latency = Date.now() - startTime;
await finalizeStreamSuccess(
@@ -1025,6 +1115,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
if (String(selected.site.platform || '').trim().toLowerCase() === 'gemini-cli') {
upstreamData = unwrapGeminiCliPayload(upstreamData);
}
if (codexSessionStoreKey) {
rememberCodexSessionResponseId(codexSessionStoreKey, upstreamData);
}
const latency = Date.now() - startTime;
const parsedUsage = parseProxyUsage(upstreamData);
const upstreamUsagePresent = hasProxyUsagePayload(upstreamData);
@@ -85,6 +85,7 @@ vi.mock('../../services/alertRules.js', () => ({
vi.mock('../../services/proxyRetryPolicy.js', () => ({
shouldRetryProxyRequest: (...args: unknown[]) => shouldRetryProxyRequestMock(...args),
shouldAbortSameSiteEndpointFallback: () => false,
}));
vi.mock('../../services/oauth/quota.js', () => ({
@@ -11,9 +11,9 @@ import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import { resolveProxyLogBilling } from '../../routes/proxy/proxyBilling.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 { buildUpstreamUrl } from '../../routes/proxy/upstreamUrl.js';
import { dispatchRuntimeRequest } from '../../services/runtimeDispatch.js';
import type { BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { buildUpstreamUrl } from '../orchestration/upstreamRequest.js';
import { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import { proxyChannelCoordinator } from '../../services/proxyChannelCoordinator.js';