[codex] Add model tester fixed-channel selection (#352)

* feat: add model tester forced channel

* fix: keep codex websocket defaults and route refresh checks

* fix: scope tester-only forced channel overrides

* fix: keep tester hydration and websocket hints internal

* fix: reject gemini on legacy tester chat routes

* fix: harden forced channel selection edges
This commit is contained in:
Cita
2026-04-01 12:56:13 +08:00
committed by GitHub
parent 8c3be1a613
commit bafceb855b
27 changed files with 1331 additions and 288 deletions
@@ -10,22 +10,33 @@ function expectNoDirectModelServiceRouteRefresh(source: string): void {
expect(source).not.toMatch(/import\s*\{[^}]*\brebuildTokenRoutesFromAvailability\b[^}]*\}\s*from\s*['"][^'"]*modelService\.js['"]/m);
}
function expectImportsRouteRefreshWorkflow(source: string): void {
expect(source).toMatch(
/import\s+\*\s+as\s+routeRefreshWorkflow\s+from\s+['"][^'"]*routeRefreshWorkflow\.js['"]/m,
);
}
function expectCallsSelectProxyChannelForAttempt(source: string): void {
expect(source).toMatch(/\bselectProxyChannelForAttempt\s*\(/);
}
function expectCallsRebuildRoutesOnly(source: string): void {
expect(source).toMatch(/\brouteRefreshWorkflow\.rebuildRoutesOnly\s*\(/);
}
describe('route refresh workflow architecture boundaries', () => {
it('keeps api controllers on the shared route refresh workflow instead of modelService', () => {
const tokensSource = readSource('./tokens.ts');
const settingsSource = readSource('./settings.ts');
const statsSource = readSource('./stats.ts');
expect(tokensSource).toContain("from '../../services/routeRefreshWorkflow.js'");
expect(tokensSource).not.toContain("from '../../services/modelService.js'");
for (const source of [tokensSource, settingsSource, statsSource]) {
expectImportsRouteRefreshWorkflow(source);
expectNoDirectModelServiceRouteRefresh(source);
}
expect(settingsSource).toContain("from '../../services/routeRefreshWorkflow.js'");
expect(settingsSource).not.toContain("from '../../services/modelService.js'");
expect(statsSource).toContain("from '../../services/routeRefreshWorkflow.js'");
expectNoDirectModelServiceRouteRefresh(statsSource);
expect(tokensSource).toContain('const rebuild = await routeRefreshWorkflow.rebuildRoutesOnly();');
expect(statsSource).toContain('const rebuild = await routeRefreshWorkflow.rebuildRoutesOnly();');
expectCallsRebuildRoutesOnly(tokensSource);
expectCallsRebuildRoutesOnly(statsSource);
});
it('keeps proxy fallback refreshes and scheduler hooks on the route refresh workflow', () => {
@@ -39,6 +50,12 @@ describe('route refresh workflow architecture boundaries', () => {
const oauthServiceSource = readSource('../../services/oauth/service.ts');
const sharedSurfaceSource = readSource('../../proxy-core/surfaces/sharedSurface.ts');
const geminiSurfaceSource = readSource('../../proxy-core/surfaces/geminiSurface.ts');
const channelSelectionSource = readSource('../../proxy-core/channelSelection.ts');
for (const source of [schedulerSource, oauthServiceSource, channelSelectionSource]) {
expectImportsRouteRefreshWorkflow(source);
expectNoDirectModelServiceRouteRefresh(source);
}
for (const source of [
completionsSource,
@@ -47,13 +64,24 @@ describe('route refresh workflow architecture boundaries', () => {
modelsRouteSource,
searchSource,
videosSource,
schedulerSource,
oauthServiceSource,
sharedSurfaceSource,
geminiSurfaceSource,
]) {
expect(source).toContain('routeRefreshWorkflow');
expectNoDirectModelServiceRouteRefresh(source);
}
for (const source of [
completionsSource,
embeddingsSource,
imagesSource,
searchSource,
videosSource,
sharedSurfaceSource,
]) {
expectCallsSelectProxyChannelForAttempt(source);
}
expect(geminiSurfaceSource).toMatch(/\bselectGeminiChannel\s*\(/);
expect(geminiSurfaceSource).toMatch(/\bselectNextGeminiProbeChannel\s*\(/);
});
});
+10
View File
@@ -73,6 +73,7 @@ describe('testRoutes proxy tester transport', () => {
stream: false,
jobMode: false,
rawMode: true,
forcedChannelId: 77,
rawJsonText: JSON.stringify({
model: 'gpt-4o-mini',
include: ['reasoning.encrypted_content'],
@@ -85,6 +86,10 @@ describe('testRoutes proxy tester transport', () => {
expect(response.statusCode).toBe(200);
const [url, requestInit] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toMatch(/\/v1\/responses$/);
expect(requestInit.headers).toMatchObject({
'x-metapi-tester-request': '1',
'x-metapi-tester-forced-channel-id': '77',
});
expect(JSON.parse(String(requestInit.body))).toEqual({
model: 'gpt-4o-mini',
include: ['reasoning.encrypted_content'],
@@ -109,6 +114,7 @@ describe('testRoutes proxy tester transport', () => {
payload: {
model: 'claude-3-7-sonnet',
targetFormat: 'claude',
forcedChannelId: 55,
messages: [
{ role: 'system', content: 'be concise' },
{ role: 'user', content: 'ping' },
@@ -119,6 +125,10 @@ describe('testRoutes proxy tester transport', () => {
expect(response.statusCode).toBe(200);
const [url, requestInit] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toMatch(/\/v1\/messages$/);
expect(requestInit.headers).toMatchObject({
'x-metapi-tester-request': '1',
'x-metapi-tester-forced-channel-id': '55',
});
expect(JSON.parse(String(requestInit.body))).toEqual({
model: 'claude-3-7-sonnet',
stream: false,
+20
View File
@@ -182,4 +182,24 @@ describe('/api/test proxy tester routes', () => {
max_output_tokens: 4096,
});
});
it('rejects gemini on legacy /api/test/chat wrapper until a native mapping exists', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/test/chat',
payload: {
model: 'gemini-2.5-flash',
targetFormat: 'gemini',
messages: [
{ role: 'user', content: 'hello' },
],
},
});
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
error: 'targetFormat=gemini is not supported on legacy /api/test/chat routes; use the proxy tester Gemini path instead',
});
expect(fetchMock).not.toHaveBeenCalled();
});
});
+46 -4
View File
@@ -3,6 +3,11 @@ import { randomUUID } from 'node:crypto';
import { fetch, File as UndiciFile, FormData as UndiciFormData } from 'undici';
import { config } from '../../config.js';
import { readRuntimeResponseText } from '../../proxy-core/executors/types.js';
import {
normalizeForcedChannelId,
TESTER_FORCED_CHANNEL_HEADER,
TESTER_REQUEST_HEADER,
} from '../../proxy-core/channelSelection.js';
type UndiciRequestInit = Parameters<typeof fetch>[1];
@@ -14,6 +19,7 @@ type TestChatRequestBody = {
messages?: TestChatMessage[];
targetFormat?: TestTargetFormat;
stream?: boolean;
forcedChannelId?: number;
temperature?: number;
top_p?: number;
max_tokens?: number;
@@ -27,6 +33,7 @@ type ValidatedTestChatPayload = {
messages: TestChatMessage[];
targetFormat: TestTargetFormat;
stream?: boolean;
forcedChannelId?: number | null;
temperature?: number;
top_p?: number;
max_tokens?: number;
@@ -52,6 +59,7 @@ type ProxyTestEnvelope = {
stream?: boolean;
jobMode?: boolean;
rawMode?: boolean;
forcedChannelId?: number | null;
jsonBody?: unknown;
rawJsonText?: string;
multipartFields?: Record<string, string>;
@@ -65,6 +73,7 @@ type ValidatedProxyTestEnvelope = {
stream: boolean;
jobMode: boolean;
rawMode: boolean;
forcedChannelId?: number | null;
jsonBody?: unknown;
rawJsonText?: string;
multipartFields?: Record<string, string>;
@@ -162,19 +171,25 @@ function validateLegacyPayload(
return null;
}
if (body.targetFormat === 'gemini') {
reply.code(400).send({
error: 'targetFormat=gemini is not supported on legacy /api/test/chat routes; use the proxy tester Gemini path instead',
});
return null;
}
const targetFormat: TestTargetFormat = body.targetFormat === 'claude'
? 'claude'
: body.targetFormat === 'responses'
? 'responses'
: body.targetFormat === 'gemini'
? 'gemini'
: 'openai';
: 'openai';
return {
model: body.model,
messages: body.messages,
targetFormat,
stream: body.stream,
forcedChannelId: normalizeForcedChannelId(body.forcedChannelId),
temperature: body.temperature,
top_p: body.top_p,
max_tokens: body.max_tokens,
@@ -289,6 +304,7 @@ function convertLegacyPayloadToEnvelope(
stream: forceStream,
jobMode: false,
rawMode: false,
forcedChannelId: payload.forcedChannelId ?? null,
jsonBody: convertOpenAiPayloadToClaudeBody(payload, forceStream),
};
}
@@ -301,6 +317,7 @@ function convertLegacyPayloadToEnvelope(
stream: forceStream,
jobMode: false,
rawMode: false,
forcedChannelId: payload.forcedChannelId ?? null,
jsonBody: convertOpenAiPayloadToResponsesBody(payload, forceStream),
};
}
@@ -312,9 +329,29 @@ function convertLegacyPayloadToEnvelope(
stream: forceStream,
jobMode: false,
rawMode: false,
forcedChannelId: payload.forcedChannelId ?? null,
jsonBody: {
...payload,
model: payload.model,
messages: payload.messages,
stream: forceStream,
...(typeof payload.temperature === 'number' && Number.isFinite(payload.temperature)
? { temperature: payload.temperature }
: {}),
...(typeof payload.top_p === 'number' && Number.isFinite(payload.top_p)
? { top_p: payload.top_p }
: {}),
...(typeof payload.max_tokens === 'number' && Number.isFinite(payload.max_tokens)
? { max_tokens: payload.max_tokens }
: {}),
...(typeof payload.frequency_penalty === 'number' && Number.isFinite(payload.frequency_penalty)
? { frequency_penalty: payload.frequency_penalty }
: {}),
...(typeof payload.presence_penalty === 'number' && Number.isFinite(payload.presence_penalty)
? { presence_penalty: payload.presence_penalty }
: {}),
...(typeof payload.seed === 'number' && Number.isFinite(payload.seed)
? { seed: payload.seed }
: {}),
},
};
}
@@ -356,6 +393,7 @@ function validateProxyEnvelope(
stream: body.stream === true,
jobMode: body.jobMode === true,
rawMode: body.rawMode === true,
forcedChannelId: normalizeForcedChannelId(body.forcedChannelId),
};
if (requestKind === 'json') {
@@ -472,6 +510,10 @@ async function buildUpstreamRequestInit(
forceStream: boolean,
): Promise<UndiciRequestInit> {
const headers: Record<string, string> = createDefaultHeadersForPath(envelope.path);
headers[TESTER_REQUEST_HEADER] = '1';
if (typeof envelope.forcedChannelId === 'number' && envelope.forcedChannelId > 0) {
headers[TESTER_FORCED_CHANNEL_HEADER] = String(envelope.forcedChannelId);
}
if (envelope.requestKind === 'json') {
headers['Content-Type'] = 'application/json';
+41 -32
View File
@@ -1,10 +1,9 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage, pullSseDataEvents } from '../../services/proxyUsageParser.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js';
@@ -18,7 +17,13 @@ import { getProxyAuthContext } from '../../middleware/auth.js';
import { buildUpstreamUrl } from './upstreamUrl.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from './downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { canRetryProxyChannel, getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import {
buildForcedChannelUnavailableMessage,
canRetryChannelSelection,
getTesterForcedChannelId,
selectProxyChannelForAttempt,
} from '../../proxy-core/channelSelection.js';
export async function completionsProxyRoute(app: FastifyInstance) {
app.post('/v1/completions', async (request: FastifyRequest, reply: FastifyReply) => {
@@ -29,6 +34,10 @@ export async function completionsProxyRoute(app: FastifyInstance) {
}
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const forcedChannelId = getTesterForcedChannelId({
headers: request.headers as Record<string, unknown>,
clientIp: request.ip,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const downstreamPath = '/v1/completions';
const clientContext = detectDownstreamClientContext({
@@ -38,28 +47,28 @@ export async function completionsProxyRoute(app: FastifyInstance) {
});
const isStream = body.stream === true;
const excludeChannelIds: number[] = [];
let retryCount = 0;
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= getProxyMaxChannelRetries()) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
if (!selected) {
await reportProxyAllFailed({
model: requestedModel,
reason: 'No available channels after retries',
});
return reply.code(503).send({
error: { message: 'No available channels for this model', type: 'server_error' },
});
}
const selected = await selectProxyChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
forcedChannelId,
});
if (!selected) {
const noChannelMessage = buildForcedChannelUnavailableMessage(forcedChannelId);
await reportProxyAllFailed({
model: requestedModel,
reason: forcedChannelId ? noChannelMessage : 'No available channels after retries',
});
return reply.code(503).send({
error: { message: noChannelMessage, type: 'server_error' },
});
}
excludeChannelIds.push(selected.channel.id);
@@ -112,7 +121,7 @@ export async function completionsProxyRoute(app: FastifyInstance) {
});
}
if (shouldRetryProxyRequest(upstream.status, errText) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(upstream.status, errText) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -258,7 +267,7 @@ export async function completionsProxyRoute(app: FastifyInstance) {
downstreamPath,
);
if (shouldRetryProxyRequest(failure.status, errText) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(failure.status, errText) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount += 1;
continue;
}
@@ -341,7 +350,7 @@ export async function completionsProxyRoute(app: FastifyInstance) {
clientContext,
downstreamPath,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
+42 -33
View File
@@ -1,10 +1,9 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { parseProxyUsage } from '../../services/proxyUsageParser.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js';
@@ -17,7 +16,13 @@ import { getProxyAuthContext } from '../../middleware/auth.js';
import { buildUpstreamUrl } from './upstreamUrl.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from './downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { canRetryProxyChannel, getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import {
buildForcedChannelUnavailableMessage,
canRetryChannelSelection,
getTesterForcedChannelId,
selectProxyChannelForAttempt,
} from '../../proxy-core/channelSelection.js';
export async function embeddingsProxyRoute(app: FastifyInstance) {
app.post('/v1/embeddings', async (request: FastifyRequest, reply: FastifyReply) => {
@@ -25,10 +30,14 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
const requestedModel = body?.model;
if (!requestedModel) {
return reply.code(400).send({ error: { message: 'model is required', type: 'invalid_request_error' } });
}
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
}
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const forcedChannelId = getTesterForcedChannelId({
headers: request.headers as Record<string, unknown>,
clientIp: request.ip,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const downstreamPath = '/v1/embeddings';
const clientContext = detectDownstreamClientContext({
downstreamPath,
@@ -36,26 +45,26 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
body,
});
const excludeChannelIds: number[] = [];
let retryCount = 0;
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= getProxyMaxChannelRetries()) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
if (!selected) {
await reportProxyAllFailed({
model: requestedModel,
reason: 'No available channels after retries',
});
return reply.code(503).send({ error: { message: 'No available channels', type: 'server_error' } });
}
const selected = await selectProxyChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
forcedChannelId,
});
if (!selected) {
const noChannelMessage = buildForcedChannelUnavailableMessage(forcedChannelId);
await reportProxyAllFailed({
model: requestedModel,
reason: forcedChannelId ? noChannelMessage : 'No available channels after retries',
});
return reply.code(503).send({ error: { message: noChannelMessage, type: 'server_error' } });
}
excludeChannelIds.push(selected.channel.id);
const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/embeddings');
@@ -107,7 +116,7 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
});
}
if (shouldRetryProxyRequest(upstream.status, text) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(upstream.status, text) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -178,7 +187,7 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
clientContext,
downstreamPath,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
+58
View File
@@ -10,6 +10,7 @@ const refreshModelsAndRebuildRoutesMock = vi.fn();
const refreshOauthAccessTokenSingleflightMock = vi.fn();
const selectChannelMock = vi.fn();
const selectNextChannelMock = vi.fn();
const selectPreferredChannelMock = vi.fn();
const recordSuccessMock = vi.fn();
const recordFailureMock = vi.fn();
const explainSelectionMock = vi.fn();
@@ -68,6 +69,7 @@ vi.mock('../../services/tokenRouter.js', () => ({
tokenRouter: {
selectChannel: (...args: unknown[]) => selectChannelMock(...args),
selectNextChannel: (...args: unknown[]) => selectNextChannelMock(...args),
selectPreferredChannel: (...args: unknown[]) => selectPreferredChannelMock(...args),
recordSuccess: (...args: unknown[]) => recordSuccessMock(...args),
recordFailure: (...args: unknown[]) => recordFailureMock(...args),
explainSelection: (...args: unknown[]) => explainSelectionMock(...args),
@@ -181,6 +183,7 @@ describe('gemini native proxy routes', () => {
refreshOauthAccessTokenSingleflightMock.mockReset();
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
selectPreferredChannelMock.mockReset();
recordSuccessMock.mockReset();
recordFailureMock.mockReset();
explainSelectionMock.mockReset();
@@ -232,6 +235,7 @@ describe('gemini native proxy routes', () => {
actualModel: 'gemini-2.5-flash',
});
selectNextChannelMock.mockReturnValue(null);
selectPreferredChannelMock.mockReturnValue(null);
recordSuccessMock.mockResolvedValue(undefined);
recordFailureMock.mockResolvedValue(undefined);
resetUpstreamEndpointRuntimeState();
@@ -318,6 +322,60 @@ describe('gemini native proxy routes', () => {
expect(secondUrl).toContain('key=gemini-key-2');
});
it('pins /v1beta/models to the forced tester channel when present', async () => {
selectPreferredChannelMock
.mockReturnValueOnce(null)
.mockReturnValueOnce({
channel: { id: 77, routeId: 22 },
site: { id: 88, name: 'forced-gemini-site', url: 'https://generativelanguage.googleapis.com', platform: 'gemini' },
account: { id: 39, username: 'forced-user' },
tokenName: 'forced',
tokenValue: 'forced-gemini-key',
actualModel: 'gemini-2.0-flash',
});
fetchMock.mockResolvedValue(new Response(JSON.stringify({
models: [
{ name: 'models/gemini-2.0-flash', displayName: 'Gemini 2.0 Flash' },
],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'GET',
url: '/v1beta/models',
remoteAddress: '127.0.0.1',
headers: {
authorization: 'Bearer sk-managed-gemini',
'x-metapi-tester-request': '1',
'x-metapi-tester-forced-channel-id': '77',
},
});
expect(response.statusCode).toBe(200);
expect(selectChannelMock).not.toHaveBeenCalled();
expect(selectNextChannelMock).not.toHaveBeenCalled();
expect(selectPreferredChannelMock).toHaveBeenCalledTimes(2);
expect(selectPreferredChannelMock).toHaveBeenNthCalledWith(
1,
'gemini-2.5-flash',
77,
expect.anything(),
expect.any(Array),
);
expect(selectPreferredChannelMock).toHaveBeenNthCalledWith(
2,
'gemini-2.0-flash',
77,
expect.anything(),
expect.any(Array),
);
const [targetUrl] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(targetUrl).toContain('key=forced-gemini-key');
});
it('serves gemini-cli model list from local static catalog without upstream fetch', async () => {
selectChannelMock.mockReturnValue({
channel: { id: 21, routeId: 22 },
+41 -28
View File
@@ -1,7 +1,6 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
@@ -16,7 +15,13 @@ import { getProxyAuthContext } from '../../middleware/auth.js';
import { buildUpstreamUrl } from './upstreamUrl.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from './downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { canRetryProxyChannel, getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import {
buildForcedChannelUnavailableMessage,
canRetryChannelSelection,
getTesterForcedChannelId,
selectProxyChannelForAttempt,
} from '../../proxy-core/channelSelection.js';
export async function imagesProxyRoute(app: FastifyInstance) {
ensureMultipartBufferParser(app);
@@ -26,6 +31,10 @@ export async function imagesProxyRoute(app: FastifyInstance) {
const requestedModel = body?.model || 'gpt-image-1';
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const forcedChannelId = getTesterForcedChannelId({
headers: request.headers as Record<string, unknown>,
clientIp: request.ip,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const downstreamPath = '/v1/images/generations';
const clientContext = detectDownstreamClientContext({
@@ -37,22 +46,22 @@ export async function imagesProxyRoute(app: FastifyInstance) {
let retryCount = 0;
while (retryCount <= getProxyMaxChannelRetries()) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectProxyChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
forcedChannelId,
});
if (!selected) {
const noChannelMessage = buildForcedChannelUnavailableMessage(forcedChannelId);
await reportProxyAllFailed({
model: requestedModel,
reason: 'No available channels after retries',
reason: forcedChannelId ? noChannelMessage : 'No available channels after retries',
});
return reply.code(503).send({
error: { message: 'No available channels for this model', type: 'server_error' },
error: { message: noChannelMessage, type: 'server_error' },
});
}
@@ -101,7 +110,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
detail: `HTTP ${upstream.status}`,
});
}
if (shouldRetryProxyRequest(upstream.status, text) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(upstream.status, text) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -132,7 +141,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
downstreamPath,
clientContext,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -184,7 +193,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
downstreamPath,
clientContext,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -210,6 +219,10 @@ export async function imagesProxyRoute(app: FastifyInstance) {
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const forcedChannelId = getTesterForcedChannelId({
headers: request.headers as Record<string, unknown>,
clientIp: request.ip,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const downstreamPath = '/v1/images/edits';
const clientContext = detectDownstreamClientContext({
@@ -221,22 +234,22 @@ export async function imagesProxyRoute(app: FastifyInstance) {
let retryCount = 0;
while (retryCount <= getProxyMaxChannelRetries()) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectProxyChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
forcedChannelId,
});
if (!selected) {
const noChannelMessage = buildForcedChannelUnavailableMessage(forcedChannelId);
await reportProxyAllFailed({
model: requestedModel,
reason: 'No available channels after retries',
reason: forcedChannelId ? noChannelMessage : 'No available channels after retries',
});
return reply.code(503).send({
error: { message: 'No available channels for this model', type: 'server_error' },
error: { message: noChannelMessage, type: 'server_error' },
});
}
@@ -297,7 +310,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
detail: `HTTP ${upstream.status}`,
});
}
if (shouldRetryProxyRequest(upstream.status, text) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(upstream.status, text) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -328,7 +341,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
downstreamPath,
clientContext,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
@@ -380,7 +393,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
downstreamPath,
clientContext,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount++;
continue;
}
+23 -14
View File
@@ -1,7 +1,6 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
@@ -14,7 +13,13 @@ import { getProxyAuthContext } from '../../middleware/auth.js';
import { buildUpstreamUrl } from './upstreamUrl.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from './downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { canRetryProxyChannel, getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import {
buildForcedChannelUnavailableMessage,
canRetryChannelSelection,
getTesterForcedChannelId,
selectProxyChannelForAttempt,
} from '../../proxy-core/channelSelection.js';
const DEFAULT_SEARCH_MODEL = '__search';
const DEFAULT_MAX_RESULTS = 10;
const MAX_MAX_RESULTS = 20;
@@ -54,6 +59,10 @@ export async function searchProxyRoute(app: FastifyInstance) {
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const forcedChannelId = getTesterForcedChannelId({
headers: request.headers as Record<string, unknown>,
clientIp: request.ip,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const downstreamPath = '/v1/search';
const clientContext = detectDownstreamClientContext({
@@ -65,22 +74,22 @@ export async function searchProxyRoute(app: FastifyInstance) {
let retryCount = 0;
while (retryCount <= getProxyMaxChannelRetries()) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectProxyChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
forcedChannelId,
});
if (!selected) {
const noChannelMessage = buildForcedChannelUnavailableMessage(forcedChannelId);
await reportProxyAllFailed({
model: requestedModel,
reason: 'No available channels after retries',
reason: forcedChannelId ? noChannelMessage : 'No available channels after retries',
});
return reply.code(503).send({
error: { message: 'No available channels for this model', type: 'server_error' },
error: { message: noChannelMessage, type: 'server_error' },
});
}
@@ -131,7 +140,7 @@ export async function searchProxyRoute(app: FastifyInstance) {
detail: `HTTP ${upstream.status}`,
});
}
if (shouldRetryProxyRequest(upstream.status, text) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(upstream.status, text) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount += 1;
continue;
}
@@ -170,7 +179,7 @@ export async function searchProxyRoute(app: FastifyInstance) {
clientContext,
downstreamPath,
);
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount += 1;
continue;
}
@@ -935,6 +935,39 @@ describe('buildUpstreamEndpointRequest', () => {
expect(httpRequest.headers['x-codex-beta-features']).toBeUndefined();
});
it('uses internal websocket transport hints without forwarding internal metapi headers upstream', () => {
(config as any).codexHeaderDefaults = {
userAgent: 'codex-config-ua/1.0',
betaFeatures: 'multi_agent',
};
const request = buildUpstreamEndpointRequest({
endpoint: 'responses',
modelName: 'gpt-5.4',
stream: true,
tokenValue: 'oauth-access-token',
oauthProvider: 'codex',
sitePlatform: 'codex',
siteUrl: 'https://chatgpt.com/backend-api/codex',
openaiBody: {
model: 'gpt-5.4',
messages: [{ role: 'user', content: 'hello codex' }],
},
downstreamFormat: 'openai',
downstreamHeaders: {
'x-metapi-responses-websocket-transport': '1',
'x-metapi-tester-forced-channel-id': '77',
},
providerHeaders: {
Originator: 'codex_cli_rs',
},
} as any);
expect(request.headers['x-codex-beta-features']).toBe('multi_agent');
expect(request.headers['x-metapi-responses-websocket-transport']).toBeUndefined();
expect(request.headers['x-metapi-tester-forced-channel-id']).toBeUndefined();
});
it('applies configured payload rules before preparing codex responses requests while forcing store false', () => {
(config as any).payloadRules = {
default: [
+16 -1
View File
@@ -34,6 +34,7 @@ import {
import {
buildClaudeRuntimeHeaders,
buildGeminiCliUserAgent,
getInputHeader,
headerValueToString,
} from '../../proxy-core/providers/headerUtils.js';
export {
@@ -112,11 +113,20 @@ const BLOCKED_PASSTHROUGH_HEADERS = new Set([
'sec-websocket-version',
'sec-websocket-extensions',
]);
const METAPI_INTERNAL_HEADER_BLOCKLIST = new Set([
'x-metapi-tester-request',
'x-metapi-tester-forced-channel-id',
'x-metapi-responses-websocket-mode',
'x-metapi-responses-websocket-transport',
]);
const ANTIGRAVITY_RUNTIME_USER_AGENT = 'antigravity/1.19.6 darwin/arm64';
function shouldSkipPassthroughHeader(key: string): boolean {
return HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key);
if (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key)) return true;
if (METAPI_INTERNAL_HEADER_BLOCKLIST.has(key)) return true;
if (key.startsWith('x-metapi-')) return true;
return false;
}
function extractSafePassthroughHeaders(
@@ -911,6 +921,10 @@ export function buildUpstreamEndpointRequest(input: {
}
if (input.endpoint === 'responses') {
const responsesWebsocketTransport = getInputHeader(
input.downstreamHeaders,
'x-metapi-responses-websocket-transport',
) === '1';
const websocketMode = Object.entries(input.downstreamHeaders || {}).find(([rawKey]) => rawKey.trim().toLowerCase() === 'x-metapi-responses-websocket-mode');
const preserveWebsocketIncrementalMode = asTrimmedString(websocketMode?.[1]).toLowerCase() === 'incremental';
const responsesHeaders = input.downstreamFormat === 'responses'
@@ -963,6 +977,7 @@ export function buildUpstreamEndpointRequest(input: {
providerHeaders: input.providerHeaders,
codexSessionCacheKey: input.codexSessionCacheKey,
codexExplicitSessionId: input.codexExplicitSessionId,
responsesWebsocketTransport,
body: configuredResponsesBody,
});
}
+23 -14
View File
@@ -1,7 +1,6 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { tokenRouter } from '../../services/tokenRouter.js';
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
@@ -17,7 +16,13 @@ import {
refreshProxyVideoTaskSnapshot,
saveProxyVideoTask,
} from '../../services/proxyVideoTaskStore.js';
import { canRetryProxyChannel, getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import {
buildForcedChannelUnavailableMessage,
canRetryChannelSelection,
getTesterForcedChannelId,
selectProxyChannelForAttempt,
} from '../../proxy-core/channelSelection.js';
function rewriteVideoResponsePublicId(payload: unknown, publicId: string): unknown {
if (!payload || typeof payload !== 'object') return payload;
@@ -47,26 +52,30 @@ export async function videosProxyRoute(app: FastifyInstance) {
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const forcedChannelId = getTesterForcedChannelId({
headers: request.headers as Record<string, unknown>,
clientIp: request.ip,
});
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= getProxyMaxChannelRetries()) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
}
const selected = await selectProxyChannelForAttempt({
requestedModel,
downstreamPolicy,
excludeChannelIds,
retryCount,
forcedChannelId,
});
if (!selected) {
const noChannelMessage = buildForcedChannelUnavailableMessage(forcedChannelId);
await reportProxyAllFailed({
model: requestedModel,
reason: 'No available channels after retries',
reason: forcedChannelId ? noChannelMessage : 'No available channels after retries',
});
return reply.code(503).send({
error: { message: 'No available channels for this model', type: 'server_error' },
error: { message: noChannelMessage, type: 'server_error' },
});
}
@@ -115,7 +124,7 @@ export async function videosProxyRoute(app: FastifyInstance) {
detail: `HTTP ${upstream.status}`,
});
}
if (shouldRetryProxyRequest(upstream.status, text) && canRetryProxyChannel(retryCount)) {
if (shouldRetryProxyRequest(upstream.status, text) && canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount += 1;
continue;
}
@@ -170,7 +179,7 @@ export async function videosProxyRoute(app: FastifyInstance) {
errorText: error?.message || 'network failure',
modelName: upstreamModel,
}));
if (canRetryProxyChannel(retryCount)) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount += 1;
continue;
}