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:
@@ -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> {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user