[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';