fix: finish review-driven single-source cleanup

This commit is contained in:
Cita
2026-03-23 11:26:01 +08:00
parent 29c9fcbd29
commit 328b492ab3
21 changed files with 926 additions and 320 deletions
@@ -25,7 +25,7 @@ describe('provider header utils', () => {
).toBe('beta-a,beta-b,beta-c');
expect(
mergeClaudeBetaHeader('custom-a,custom-b', 'beta-a,beta-b', ['beta-c']),
).toBe('custom-a,custom-b');
).toBe('beta-a,beta-b,custom-a,custom-b,beta-c');
});
it('preserves gemini cli runtime metadata when rebuilding user agents', async () => {
@@ -79,9 +79,12 @@ describe('provider header utils', () => {
const headers = buildClaudeRuntimeHeaders({
baseHeaders: {
'Content-Type': 'application/json',
authorization: 'Bearer stale-token',
},
claudeHeaders: {
'anthropic-beta': 'custom-beta',
'x-api-key': 'stale-api-key',
'user-agent': 'custom-agent',
},
anthropicVersion: '2023-06-01',
stream: true,
@@ -90,9 +93,13 @@ describe('provider header utils', () => {
});
expect(headers['anthropic-version']).toBe('2023-06-01');
expect(headers['anthropic-beta']).toBe('custom-beta');
expect(headers['anthropic-beta']).toContain('claude-code-20250219');
expect(headers['anthropic-beta']).toContain('custom-beta');
expect(headers.Authorization).toBe('Bearer oauth-token');
expect(headers.authorization).toBeUndefined();
expect(headers['x-api-key']).toBeUndefined();
expect(headers['user-agent']).toBeUndefined();
expect(headers['User-Agent']).toBe('custom-agent');
expect(headers.Accept).toBe('text/event-stream');
});
+36 -10
View File
@@ -32,6 +32,24 @@ export function getInputHeader(
return null;
}
function normalizeLowerCaseHeaderMap(
sources: Array<Record<string, unknown> | Record<string, string> | undefined>,
shouldSkip: (key: string) => boolean,
): Record<string, string> {
const normalized: Record<string, string> = {};
for (const source of sources) {
if (!source) continue;
for (const [key, value] of Object.entries(source)) {
const normalizedValue = headerValueToString(value);
if (!normalizedValue) continue;
const normalizedKey = key.toLowerCase();
if (shouldSkip(normalizedKey)) continue;
normalized[normalizedKey] = normalizedValue;
}
}
return normalized;
}
export function uuidFromSeed(seed: string): string {
const derived = pbkdf2Sync(seed, 'metapi-runtime-header-seed', 10_000, 16, 'sha256');
const bytes = new Uint8Array(derived);
@@ -52,22 +70,15 @@ export function mergeClaudeBetaHeader(
defaultValue: string,
extraBetas: string[] = [],
): string {
const source = explicitValue || defaultValue;
const seen = new Set<string>();
const merged: string[] = [];
for (const source of [defaultValue, explicitValue ?? '', ...extraBetas]) {
for (const entry of source.split(',')) {
const normalized = entry.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
merged.push(normalized);
}
if (!explicitValue) {
for (const entry of extraBetas) {
const normalized = entry.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
merged.push(normalized);
}
}
return merged.join(',');
}
@@ -187,9 +198,24 @@ export function buildClaudeRuntimeHeaders(input: {
input.defaultBetaHeader || CLAUDE_DEFAULT_BETA_HEADER,
input.extraBetas,
);
const passthroughHeaders = normalizeLowerCaseHeaderMap(
[input.baseHeaders, input.claudeHeaders],
(key) => (
key === 'accept'
|| key === 'accept-encoding'
|| key === 'anthropic-beta'
|| key === 'anthropic-dangerous-direct-browser-access'
|| key === 'anthropic-version'
|| key === 'authorization'
|| key === 'connection'
|| key === 'user-agent'
|| key === 'x-api-key'
|| key === 'x-app'
|| key.startsWith('x-stainless-')
),
);
const headers: Record<string, string> = {
...input.baseHeaders,
...input.claudeHeaders,
...passthroughHeaders,
'anthropic-version': input.anthropicVersion,
...(anthropicBeta ? { 'anthropic-beta': anthropicBeta } : {}),
'Anthropic-Dangerous-Direct-Browser-Access': 'true',
+44 -106
View File
@@ -2,7 +2,6 @@ import { TextDecoder } from 'node:util';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { tokenRouter } from '../../services/tokenRouter.js';
import { reportProxyAllFailed } from '../../services/alertService.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { type DownstreamFormat } from '../../transformers/shared/normalized.js';
import {
@@ -19,8 +18,6 @@ import {
} from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { buildUpstreamUrl } from '../../routes/proxy/upstreamUrl.js';
import { resolveProxyLogBilling } from '../../routes/proxy/proxyBilling.js';
import { openAiChatTransformer } from '../../transformers/openai/chat/index.js';
import { anthropicMessagesTransformer } from '../../transformers/anthropic/messages/index.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
@@ -32,7 +29,6 @@ import {
buildOauthProviderHeaders,
} from '../../services/oauth/service.js';
import { getOauthInfoFromAccount } from '../../services/oauth/oauthAccount.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import {
collectResponsesFinalPayloadFromSse,
collectResponsesFinalPayloadFromSseText,
@@ -48,7 +44,9 @@ import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClie
import {
createSurfaceFailureToolkit,
createSurfaceDispatchRequest,
recordSurfaceSuccess,
selectSurfaceChannelForAttempt,
trySurfaceOauthRefreshRecovery,
} from './sharedSurface.js';
const MAX_RETRIES = 2;
@@ -224,28 +222,15 @@ export async function handleChatSurfaceRequest(
});
const tryRecover = async (ctx: Parameters<NonNullable<typeof endpointStrategy.tryRecover>>[0]) => {
if ((ctx.response.status === 401 || ctx.response.status === 403) && oauth) {
try {
const refreshed = await refreshOauthAccessTokenSingleflight(selected.account.id);
selected.tokenValue = refreshed.accessToken;
selected.account = {
...selected.account,
accessToken: refreshed.accessToken,
extraConfig: refreshed.extraConfig ?? selected.account.extraConfig,
};
const refreshedRequest = buildEndpointRequest(ctx.request.endpoint);
const refreshedTargetUrl = buildUpstreamUrl(selected.site.url, refreshedRequest.path);
const refreshedResponse = await dispatchRequest(refreshedRequest, refreshedTargetUrl);
if (refreshedResponse.ok) {
return {
upstream: refreshedResponse,
upstreamPath: refreshedRequest.path,
};
}
ctx.request = refreshedRequest;
ctx.response = refreshedResponse;
ctx.rawErrText = await refreshedResponse.text().catch(() => 'unknown error');
} catch {
return endpointStrategy.tryRecover(ctx);
const recovered = await trySurfaceOauthRefreshRecovery({
ctx,
selected,
siteUrl: selected.site.url,
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
});
if (recovered?.upstream?.ok) {
return recovered;
}
}
return endpointStrategy.tryRecover(ctx);
@@ -477,46 +462,19 @@ export async function handleChatSurfaceRequest(
}
const latency = Date.now() - startTime;
const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({
site: selected.site,
account: selected.account,
tokenValue: selected.tokenValue,
tokenName: selected.tokenName,
modelName,
requestStartedAtMs: startTime,
requestEndedAtMs: startTime + latency,
localLatencyMs: latency,
usage: {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
},
});
const { estimatedCost, billingDetails } = await resolveProxyLogBilling({
site: selected.site,
account: selected.account,
await recordSurfaceSuccess({
selected,
requestedModel,
modelName,
parsedUsage,
resolvedUsage,
});
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
await failureToolkit.log({
selected,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
requestStartedAtMs: startTime,
latencyMs: latency,
errorMessage: null,
retryCount,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: successfulUpstreamPath,
logSuccess: failureToolkit.log,
recordDownstreamCost: (estimatedCost) => {
recordDownstreamCostUsage(request, estimatedCost);
},
});
return;
}
@@ -570,46 +528,19 @@ export async function handleChatSurfaceRequest(
const normalizedFinal = downstreamTransformer.transformFinalResponse(upstreamData, modelName, rawText);
const downstreamResponse = downstreamTransformer.serializeFinalResponse(normalizedFinal, parsedUsage);
const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({
site: selected.site,
account: selected.account,
tokenValue: selected.tokenValue,
tokenName: selected.tokenName,
modelName,
requestStartedAtMs: startTime,
requestEndedAtMs: startTime + latency,
localLatencyMs: latency,
usage: {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
},
});
const { estimatedCost, billingDetails } = await resolveProxyLogBilling({
site: selected.site,
account: selected.account,
await recordSurfaceSuccess({
selected,
requestedModel,
modelName,
parsedUsage,
resolvedUsage,
});
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
await failureToolkit.log({
selected,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
requestStartedAtMs: startTime,
latencyMs: latency,
errorMessage: null,
retryCount,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: successfulUpstreamPath,
logSuccess: failureToolkit.log,
recordDownstreamCost: (estimatedCost) => {
recordDownstreamCostUsage(request, estimatedCost);
},
});
return reply.send(downstreamResponse);
@@ -768,18 +699,25 @@ export async function handleClaudeCountTokensSurfaceRequest(
let upstream = await dispatchRequest(upstreamRequest);
if ((upstream.status === 401 || upstream.status === 403) && oauth) {
try {
const refreshed = await refreshOauthAccessTokenSingleflight(selected.account.id);
selected.tokenValue = refreshed.accessToken;
selected.account = {
...selected.account,
accessToken: refreshed.accessToken,
extraConfig: refreshed.extraConfig ?? selected.account.extraConfig,
const recoverContext = {
request: upstreamRequest,
response: upstream,
rawErrText: '',
};
const recovered = await trySurfaceOauthRefreshRecovery({
ctx: recoverContext,
selected,
siteUrl: selected.site.url,
buildRequest: () => buildRequest(),
dispatchRequest,
captureFailureBody: false,
});
if (recovered?.upstream?.ok) {
upstreamRequest = buildRequest();
upstream = await dispatchRequest(upstreamRequest);
} catch {
// Fall through to the regular upstream error handling below.
upstream = recovered.upstream;
} else {
upstreamRequest = recoverContext.request;
upstream = recoverContext.response;
}
}
@@ -1,8 +1,6 @@
import { TextDecoder } from 'node:util';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { tokenRouter } from '../../services/tokenRouter.js';
import { reportProxyAllFailed } from '../../services/alertService.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js';
import {
@@ -14,8 +12,6 @@ import {
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { buildUpstreamUrl } from '../../routes/proxy/upstreamUrl.js';
import { resolveProxyLogBilling } from '../../routes/proxy/proxyBilling.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
import { normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
import {
@@ -26,7 +22,6 @@ import {
buildOauthProviderHeaders,
} from '../../services/oauth/service.js';
import { getOauthInfoFromAccount } from '../../services/oauth/oauthAccount.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import {
collectResponsesFinalPayloadFromSse,
collectResponsesFinalPayloadFromSseText,
@@ -46,7 +41,9 @@ import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClie
import {
createSurfaceFailureToolkit,
createSurfaceDispatchRequest,
recordSurfaceSuccess,
selectSurfaceChannelForAttempt,
trySurfaceOauthRefreshRecovery,
} from './sharedSurface.js';
const MAX_RETRIES = 2;
@@ -325,28 +322,15 @@ export async function handleOpenAiResponsesSurfaceRequest(
response: ctx.response,
rawErrText: ctx.rawErrText || '',
})) {
try {
const refreshed = await refreshOauthAccessTokenSingleflight(selected.account.id);
selected.tokenValue = refreshed.accessToken;
selected.account = {
...selected.account,
accessToken: refreshed.accessToken,
extraConfig: refreshed.extraConfig ?? selected.account.extraConfig,
};
const refreshedRequest = buildEndpointRequest(ctx.request.endpoint);
const refreshedTargetUrl = buildUpstreamUrl(selected.site.url, refreshedRequest.path);
const refreshedResponse = await dispatchRequest(refreshedRequest, refreshedTargetUrl);
if (refreshedResponse.ok) {
return {
upstream: refreshedResponse,
upstreamPath: refreshedRequest.path,
};
}
ctx.request = refreshedRequest;
ctx.response = refreshedResponse;
ctx.rawErrText = await refreshedResponse.text().catch(() => 'unknown error');
} catch {
return endpointStrategy.tryRecover(ctx);
const recovered = await trySurfaceOauthRefreshRecovery({
ctx,
selected,
siteUrl: selected.site.url,
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
});
if (recovered?.upstream?.ok) {
return recovered;
}
}
return endpointStrategy.tryRecover(ctx);
@@ -410,65 +394,23 @@ export async function handleOpenAiResponsesSurfaceRequest(
const upstream = endpointResult.upstream;
const successfulUpstreamPath = endpointResult.upstreamPath;
const finalizeStreamSuccess = async (parsedUsage: UsageSummary, latency: number) => {
let usageForLog = {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
};
let estimatedCost = 0;
let billingDetails: unknown = null;
try {
const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({
site: selected.site,
account: selected.account,
tokenValue: selected.tokenValue,
tokenName: selected.tokenName,
modelName: selected.actualModel || requestedModel,
requestStartedAtMs: startTime,
requestEndedAtMs: startTime + latency,
localLatencyMs: latency,
usage: {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
},
});
usageForLog = {
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
};
const billing = await resolveProxyLogBilling({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
parsedUsage,
resolvedUsage,
});
estimatedCost = billing.estimatedCost;
billingDetails = billing.billingDetails;
} catch (error) {
console.error('[responses] post-stream bookkeeping failed:', error);
}
try {
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
await failureToolkit.log({
await recordSurfaceSuccess({
selected,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
requestedModel,
modelName,
parsedUsage,
requestStartedAtMs: startTime,
latencyMs: latency,
errorMessage: null,
retryCount,
promptTokens: usageForLog.promptTokens,
completionTokens: usageForLog.completionTokens,
totalTokens: usageForLog.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: successfulUpstreamPath,
logSuccess: failureToolkit.log,
recordDownstreamCost: (estimatedCost) => {
recordDownstreamCostUsage(request, estimatedCost);
},
bestEffortMetrics: {
errorLabel: '[responses] post-stream bookkeeping failed:',
},
});
} catch (error) {
console.error('[responses] post-stream success logging failed:', error);
@@ -716,48 +658,25 @@ export async function handleOpenAiResponsesSurfaceRequest(
serializationMode: isCompactRequest ? 'compact' : 'response',
});
try {
const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({
site: selected.site,
account: selected.account,
tokenValue: selected.tokenValue,
tokenName: selected.tokenName,
modelName: selected.actualModel || requestedModel,
await recordSurfaceSuccess({
selected,
requestedModel,
modelName,
parsedUsage,
requestStartedAtMs: startTime,
requestEndedAtMs: startTime + latency,
localLatencyMs: latency,
usage: {
promptTokens: parsedUsage.promptTokens,
completionTokens: parsedUsage.completionTokens,
totalTokens: parsedUsage.totalTokens,
latencyMs: latency,
retryCount,
upstreamPath: successfulUpstreamPath,
logSuccess: failureToolkit.log,
recordDownstreamCost: (estimatedCost) => {
recordDownstreamCostUsage(request, estimatedCost);
},
bestEffortMetrics: {
errorLabel: '[responses] post-response bookkeeping failed:',
},
});
const { estimatedCost, billingDetails } = await resolveProxyLogBilling({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
parsedUsage,
resolvedUsage,
});
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost, modelName);
recordDownstreamCostUsage(request, estimatedCost);
await failureToolkit.log({
selected,
modelRequested: requestedModel,
status: 'success',
httpStatus: 200,
latencyMs: latency,
errorMessage: null,
retryCount,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: successfulUpstreamPath,
});
} catch (error) {
console.error('[responses] post-response bookkeeping failed:', error);
console.error('[responses] post-response success logging failed:', error);
}
return reply.send(downstreamData);
} catch (err: any) {
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { EMPTY_DOWNSTREAM_ROUTING_POLICY } from '../../services/downstreamPolicyTypes.js';
const selectChannelMock = vi.fn();
@@ -16,12 +16,18 @@ const reportTokenExpiredMock = vi.fn();
const isTokenExpiredErrorMock = vi.fn();
const shouldRetryProxyRequestMock = vi.fn();
const recordOauthQuotaResetHintMock = vi.fn();
const recordSuccessMock = vi.fn();
const resolveProxyUsageWithSelfLogFallbackMock = vi.fn();
const resolveProxyLogBillingMock = vi.fn();
const refreshOauthAccessTokenSingleflightMock = vi.fn();
const consoleWarnMock = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.mock('../../services/tokenRouter.js', () => ({
tokenRouter: {
selectChannel: (...args: unknown[]) => selectChannelMock(...args),
selectNextChannel: (...args: unknown[]) => selectNextChannelMock(...args),
recordFailure: (...args: unknown[]) => recordFailureMock(...args),
recordSuccess: (...args: unknown[]) => recordSuccessMock(...args),
},
}));
@@ -67,7 +73,23 @@ vi.mock('../../services/oauth/quota.js', () => ({
recordOauthQuotaResetHint: (...args: unknown[]) => recordOauthQuotaResetHintMock(...args),
}));
vi.mock('../../services/proxyUsageFallbackService.js', () => ({
resolveProxyUsageWithSelfLogFallback: (...args: unknown[]) => resolveProxyUsageWithSelfLogFallbackMock(...args),
}));
vi.mock('../../routes/proxy/proxyBilling.js', () => ({
resolveProxyLogBilling: (...args: unknown[]) => resolveProxyLogBillingMock(...args),
}));
vi.mock('../../services/oauth/refreshSingleflight.js', () => ({
refreshOauthAccessTokenSingleflight: (...args: unknown[]) => refreshOauthAccessTokenSingleflightMock(...args),
}));
describe('selectSurfaceChannelForAttempt', () => {
afterAll(() => {
consoleWarnMock.mockRestore();
});
beforeEach(() => {
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
@@ -84,6 +106,11 @@ describe('selectSurfaceChannelForAttempt', () => {
isTokenExpiredErrorMock.mockReset();
shouldRetryProxyRequestMock.mockReset();
recordOauthQuotaResetHintMock.mockReset();
recordSuccessMock.mockReset();
resolveProxyUsageWithSelfLogFallbackMock.mockReset();
resolveProxyLogBillingMock.mockReset();
refreshOauthAccessTokenSingleflightMock.mockReset();
consoleWarnMock.mockClear();
});
it('refreshes models and retries selectChannel on the first attempt when no channel is available', async () => {
@@ -308,6 +335,40 @@ describe('selectSurfaceChannelForAttempt', () => {
}));
});
it('keeps retryable failures on the retry path even when quota hint recording fails', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
shouldRetryProxyRequestMock.mockReturnValue(true);
isTokenExpiredErrorMock.mockReturnValue(false);
recordOauthQuotaResetHintMock.mockRejectedValue(new Error('hint failed'));
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'chat',
downstreamPath: '/v1/chat/completions',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: null,
});
await expect(toolkit.handleUpstreamFailure({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
status: 429,
errText: 'quota exceeded',
rawErrText: '{"error":"quota exceeded"}',
latencyMs: 1200,
retryCount: 0,
})).resolves.toEqual({ action: 'retry' });
});
it('returns a terminal upstream error response and reports token expiration when retries stop', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
@@ -363,6 +424,50 @@ describe('selectSurfaceChannelForAttempt', () => {
});
});
it('returns terminal failures even when final alerting throws', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
shouldRetryProxyRequestMock.mockReturnValue(false);
isTokenExpiredErrorMock.mockReturnValue(true);
recordOauthQuotaResetHintMock.mockResolvedValue(null);
reportTokenExpiredMock.mockRejectedValue(new Error('token alert failed'));
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'responses',
downstreamPath: '/v1/responses',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: null,
});
await expect(toolkit.handleUpstreamFailure({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
status: 401,
errText: 'expired token',
rawErrText: 'expired token',
latencyMs: 900,
retryCount: 2,
})).resolves.toEqual({
action: 'respond',
status: 401,
payload: {
error: {
message: 'expired token',
type: 'upstream_error',
},
},
});
});
it('handles detected proxy failures through the shared failure toolkit', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
@@ -468,4 +573,255 @@ describe('selectSurfaceChannelForAttempt', () => {
reason: 'socket hang up',
});
});
it('records stream failures with error text even without a runtime status code', async () => {
composeProxyLogMessageMock.mockReturnValue('normalized error');
formatUtcSqlDateTimeMock.mockReturnValue('2026-03-21 22:00:00');
insertProxyLogMock.mockResolvedValue(undefined);
const { createSurfaceFailureToolkit } = await import('./sharedSurface.js');
const toolkit = createSurfaceFailureToolkit({
warningScope: 'responses',
downstreamPath: '/v1/responses',
maxRetries: 2,
clientContext: null,
downstreamApiKeyId: null,
});
await toolkit.recordStreamFailure({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { name: 'Codex OAuth' },
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
errorMessage: 'stream exploded',
latencyMs: 450,
retryCount: 1,
});
expect(recordFailureMock).toHaveBeenCalledWith(11, {
errorText: 'stream exploded',
modelName: 'upstream-model',
});
});
it('refreshes oauth tokens through the shared recover helper and retries the rebuilt request', async () => {
const refreshedResponse = {
ok: true,
status: 200,
text: vi.fn(),
};
const selected = {
account: {
id: 33,
accessToken: 'old-access-token',
extraConfig: '{"oauth":{"refreshToken":"refresh"}}',
},
tokenValue: 'old-access-token',
};
const ctx = {
request: {
endpoint: 'responses' as const,
path: '/v1/responses',
headers: { authorization: 'Bearer old-access-token' },
body: { model: 'gpt-5.2' },
},
response: {
ok: false,
status: 401,
text: vi.fn(),
},
rawErrText: 'expired token',
};
refreshOauthAccessTokenSingleflightMock.mockResolvedValue({
accessToken: 'new-access-token',
extraConfig: '{"oauth":{"refreshToken":"refresh-next"}}',
});
const dispatchRequest = vi.fn().mockResolvedValue(refreshedResponse);
const { trySurfaceOauthRefreshRecovery } = await import('./sharedSurface.js');
const result = await trySurfaceOauthRefreshRecovery({
ctx,
selected,
siteUrl: 'https://upstream.example.com',
buildRequest: () => ({
endpoint: 'responses',
path: '/v1/responses',
headers: { authorization: `Bearer ${selected.tokenValue}` },
body: { model: 'gpt-5.2' },
}),
dispatchRequest,
});
expect(refreshOauthAccessTokenSingleflightMock).toHaveBeenCalledWith(33);
expect(selected.tokenValue).toBe('new-access-token');
expect(selected.account.accessToken).toBe('new-access-token');
expect(selected.account.extraConfig).toBe('{"oauth":{"refreshToken":"refresh-next"}}');
expect(dispatchRequest).toHaveBeenCalledWith(expect.objectContaining({
headers: { authorization: 'Bearer new-access-token' },
}), 'https://upstream.example.com/v1/responses');
expect(result).toEqual({
upstream: refreshedResponse,
upstreamPath: '/v1/responses',
});
});
it('updates the recover context with the refreshed failure response when oauth refresh retry still fails', async () => {
const refreshedResponse = {
ok: false,
status: 403,
text: vi.fn().mockResolvedValue('account mismatch'),
};
const ctx = {
request: {
endpoint: 'responses' as const,
path: '/v1/responses',
headers: { authorization: 'Bearer old-access-token' },
body: { model: 'gpt-5.2' },
},
response: {
ok: false,
status: 401,
text: vi.fn(),
},
rawErrText: 'expired token',
};
const selected = {
account: {
id: 33,
accessToken: 'old-access-token',
extraConfig: '{"oauth":{"refreshToken":"refresh"}}',
},
tokenValue: 'old-access-token',
};
refreshOauthAccessTokenSingleflightMock.mockResolvedValue({
accessToken: 'new-access-token',
extraConfig: '{"oauth":{"refreshToken":"refresh-next"}}',
});
const { trySurfaceOauthRefreshRecovery } = await import('./sharedSurface.js');
const result = await trySurfaceOauthRefreshRecovery({
ctx,
selected,
siteUrl: 'https://upstream.example.com',
buildRequest: () => ({
endpoint: 'responses',
path: '/v1/responses',
headers: { authorization: `Bearer ${selected.tokenValue}` },
body: { model: 'gpt-5.2' },
}),
dispatchRequest: vi.fn().mockResolvedValue(refreshedResponse),
});
expect(result).toBeNull();
expect(ctx.request.headers).toEqual({ authorization: 'Bearer new-access-token' });
expect(ctx.response).toBe(refreshedResponse);
expect(ctx.rawErrText).toBe('account mismatch');
});
it('records shared success bookkeeping with usage fallback, billing, and success logging', async () => {
resolveProxyUsageWithSelfLogFallbackMock.mockResolvedValue({
promptTokens: 20,
completionTokens: 8,
totalTokens: 28,
});
resolveProxyLogBillingMock.mockResolvedValue({
estimatedCost: 0.42,
billingDetails: { source: 'pricing-test' },
});
const logSuccess = vi.fn().mockResolvedValue(undefined);
const recordDownstreamCost = vi.fn();
const { recordSurfaceSuccess } = await import('./sharedSurface.js');
const result = await recordSurfaceSuccess({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { id: 44, url: 'https://upstream.example.com', name: 'Codex OAuth' },
tokenValue: 'live-token',
tokenName: 'default',
actualModel: 'upstream-model',
},
requestedModel: 'gpt-5.2',
modelName: 'upstream-model',
parsedUsage: {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
},
requestStartedAtMs: 1000,
latencyMs: 250,
retryCount: 1,
upstreamPath: '/v1/responses',
logSuccess,
recordDownstreamCost,
});
expect(resolveProxyUsageWithSelfLogFallbackMock).toHaveBeenCalledWith({
site: { id: 44, url: 'https://upstream.example.com', name: 'Codex OAuth' },
account: { id: 33, username: 'oauth-user' },
tokenValue: 'live-token',
tokenName: 'default',
modelName: 'upstream-model',
requestStartedAtMs: 1000,
requestEndedAtMs: 1250,
localLatencyMs: 250,
usage: {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
},
});
expect(resolveProxyLogBillingMock).toHaveBeenCalledWith({
site: { id: 44, url: 'https://upstream.example.com', name: 'Codex OAuth' },
account: { id: 33, username: 'oauth-user' },
modelName: 'upstream-model',
parsedUsage: {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
},
resolvedUsage: {
promptTokens: 20,
completionTokens: 8,
totalTokens: 28,
},
});
expect(recordSuccessMock).toHaveBeenCalledWith(11, 250, 0.42, 'upstream-model');
expect(recordDownstreamCost).toHaveBeenCalledWith(0.42);
expect(logSuccess).toHaveBeenCalledWith({
selected: {
channel: { id: 11, routeId: 22 },
account: { id: 33, username: 'oauth-user' },
site: { id: 44, url: 'https://upstream.example.com', name: 'Codex OAuth' },
tokenValue: 'live-token',
tokenName: 'default',
actualModel: 'upstream-model',
},
modelRequested: 'gpt-5.2',
status: 'success',
httpStatus: 200,
latencyMs: 250,
errorMessage: null,
retryCount: 1,
promptTokens: 20,
completionTokens: 8,
totalTokens: 28,
estimatedCost: 0.42,
billingDetails: { source: 'pricing-test' },
upstreamPath: '/v1/responses',
});
expect(result).toEqual({
resolvedUsage: {
promptTokens: 20,
completionTokens: 8,
totalTokens: 28,
},
estimatedCost: 0.42,
billingDetails: { source: 'pricing-test' },
});
});
});
+198 -11
View File
@@ -3,16 +3,20 @@ import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import type { SiteProxyConfigLike } from '../../services/siteProxy.js';
import { tokenRouter } from '../../services/tokenRouter.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import type { DownstreamRoutingPolicy } from '../../services/downstreamPolicyTypes.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import { 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 { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
type SelectedChannel = Awaited<ReturnType<typeof tokenRouter.selectChannel>>;
type SurfaceWarningScope = 'chat' | 'responses';
@@ -39,6 +43,34 @@ type SurfaceFailureOutcome =
| { action: 'retry' }
| SurfaceFailureResponse;
type SurfaceOauthRefreshSelectedChannel = {
account: {
id: number;
accessToken?: string | null;
extraConfig?: string | null;
};
tokenValue: string;
};
type SurfaceOauthRefreshContext<TRequest extends BuiltEndpointRequest> = {
request: TRequest;
response: Awaited<ReturnType<typeof dispatchRuntimeRequest>>;
rawErrText: string;
};
type SurfaceSuccessSelectedChannel = SurfaceSelectedChannel & {
account: Record<string, unknown> & { id: number };
site: Record<string, unknown>;
tokenValue: string;
tokenName?: string | null;
};
type SurfaceUsageSummary = {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
export async function selectSurfaceChannelForAttempt(input: {
requestedModel: string;
downstreamPolicy: DownstreamRoutingPolicy;
@@ -146,6 +178,150 @@ export function createSurfaceDispatchRequest(input: {
);
}
export async function trySurfaceOauthRefreshRecovery<TRequest extends BuiltEndpointRequest>(input: {
ctx: SurfaceOauthRefreshContext<TRequest>;
selected: SurfaceOauthRefreshSelectedChannel;
siteUrl: string;
buildRequest: (endpoint: TRequest['endpoint']) => TRequest;
dispatchRequest: (
request: TRequest,
targetUrl: string,
) => Promise<Awaited<ReturnType<typeof dispatchRuntimeRequest>>>;
captureFailureBody?: boolean;
}): Promise<{
upstream: Awaited<ReturnType<typeof dispatchRuntimeRequest>>;
upstreamPath: string;
} | null> {
try {
const refreshed = await refreshOauthAccessTokenSingleflight(input.selected.account.id);
input.selected.tokenValue = refreshed.accessToken;
input.selected.account = {
...input.selected.account,
accessToken: refreshed.accessToken,
extraConfig: refreshed.extraConfig ?? input.selected.account.extraConfig,
};
const refreshedRequest = input.buildRequest(input.ctx.request.endpoint);
const refreshedTargetUrl = buildUpstreamUrl(input.siteUrl, refreshedRequest.path);
const refreshedResponse = await input.dispatchRequest(refreshedRequest, refreshedTargetUrl);
if (refreshedResponse.ok) {
return {
upstream: refreshedResponse,
upstreamPath: refreshedRequest.path,
};
}
input.ctx.request = refreshedRequest;
input.ctx.response = refreshedResponse;
if (input.captureFailureBody !== false) {
input.ctx.rawErrText = await refreshedResponse.text().catch(() => 'unknown error');
}
} catch {
return null;
}
return null;
}
export async function recordSurfaceSuccess(input: {
selected: SurfaceSuccessSelectedChannel;
requestedModel: string;
modelName: string;
parsedUsage: SurfaceUsageSummary;
requestStartedAtMs: number;
latencyMs: number;
retryCount: number;
upstreamPath?: string | null;
logSuccess: (args: {
selected: SurfaceSelectedChannel;
modelRequested: string;
status: string;
httpStatus: number;
latencyMs: number;
errorMessage: string | null;
retryCount: number;
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
estimatedCost?: number;
billingDetails?: unknown;
upstreamPath?: string | null;
}) => Promise<void>;
recordDownstreamCost?: (estimatedCost: number) => void;
bestEffortMetrics?: {
errorLabel: string;
};
}) {
let resolvedUsage = {
promptTokens: input.parsedUsage.promptTokens,
completionTokens: input.parsedUsage.completionTokens,
totalTokens: input.parsedUsage.totalTokens,
};
let estimatedCost = 0;
let billingDetails: unknown = null;
try {
resolvedUsage = await resolveProxyUsageWithSelfLogFallback({
site: input.selected.site,
account: input.selected.account,
tokenValue: input.selected.tokenValue,
tokenName: input.selected.tokenName,
modelName: input.modelName,
requestStartedAtMs: input.requestStartedAtMs,
requestEndedAtMs: input.requestStartedAtMs + input.latencyMs,
localLatencyMs: input.latencyMs,
usage: {
promptTokens: input.parsedUsage.promptTokens,
completionTokens: input.parsedUsage.completionTokens,
totalTokens: input.parsedUsage.totalTokens,
},
});
const billing = await resolveProxyLogBilling({
site: input.selected.site,
account: input.selected.account,
modelName: input.modelName,
parsedUsage: input.parsedUsage,
resolvedUsage,
});
estimatedCost = billing.estimatedCost;
billingDetails = billing.billingDetails;
} catch (error) {
if (!input.bestEffortMetrics) {
throw error;
}
console.error(input.bestEffortMetrics.errorLabel, error);
}
tokenRouter.recordSuccess(
input.selected.channel.id,
input.latencyMs,
estimatedCost,
input.modelName,
);
input.recordDownstreamCost?.(estimatedCost);
await input.logSuccess({
selected: input.selected,
modelRequested: input.requestedModel,
status: 'success',
httpStatus: 200,
latencyMs: input.latencyMs,
errorMessage: null,
retryCount: input.retryCount,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
upstreamPath: input.upstreamPath,
});
return {
resolvedUsage,
estimatedCost,
billingDetails,
};
}
export function createSurfaceFailureToolkit(input: {
warningScope: SurfaceWarningScope;
downstreamPath: string;
@@ -193,6 +369,14 @@ export function createSurfaceFailureToolkit(input: {
? { action: 'retry' as const }
: null;
const runBestEffort = (label: string, fn: () => Promise<unknown>) => {
void Promise.resolve()
.then(fn)
.catch((error) => {
console.warn(`[proxy/${input.warningScope}] failed to ${label}`, error);
});
};
return {
log,
async handleUpstreamFailure(args: {
@@ -220,19 +404,19 @@ export function createSurfaceFailureToolkit(input: {
errorMessage: args.errText,
retryCount: args.retryCount,
});
await recordOauthQuotaResetHint({
runBestEffort('record oauth quota reset hint', () => recordOauthQuotaResetHint({
accountId: args.selected.account.id,
statusCode: args.status,
errorText: rawErrText,
});
}));
if (isTokenExpiredError({ status: args.status, message: args.errText })) {
await reportTokenExpired({
runBestEffort('report token expired', () => reportTokenExpired({
accountId: args.selected.account.id,
username: args.selected.account.username,
siteName: args.selected.site.name,
detail: `HTTP ${args.status}`,
});
}));
}
if (shouldRetryProxyRequest(args.status, args.errText)) {
@@ -240,10 +424,10 @@ export function createSurfaceFailureToolkit(input: {
if (retry) return retry;
}
await reportProxyAllFailed({
runBestEffort('report proxy all failed', () => reportProxyAllFailed({
model: args.requestedModel,
reason: `upstream returned HTTP ${args.status}`,
});
}));
return {
action: 'respond',
@@ -293,10 +477,10 @@ export function createSurfaceFailureToolkit(input: {
if (retry) return retry;
}
await reportProxyAllFailed({
runBestEffort('report proxy all failed', () => reportProxyAllFailed({
model: args.requestedModel,
reason: args.failure.reason,
});
}));
return {
action: 'respond',
@@ -335,10 +519,10 @@ export function createSurfaceFailureToolkit(input: {
const retry = maybeRetry(args.retryCount);
if (retry) return retry;
await reportProxyAllFailed({
runBestEffort('report proxy all failed', () => reportProxyAllFailed({
model: args.requestedModel,
reason: args.errorMessage || 'network failure',
});
}));
return {
action: 'respond',
@@ -374,7 +558,10 @@ export function createSurfaceFailureToolkit(input: {
modelName: args.modelName,
});
} else {
await tokenRouter.recordFailure(args.selected.channel.id, args.modelName);
await tokenRouter.recordFailure(args.selected.channel.id, {
errorText: errorMessage,
modelName: args.modelName,
});
}
await log({
selected: args.selected,
@@ -0,0 +1,13 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
function readSource(relativePath: string): string {
return readFileSync(new URL(relativePath, import.meta.url), 'utf8');
}
describe('account token route architecture boundaries', () => {
it('keeps legacy token repair from forcing route rebuilds inline', () => {
const source = readSource('./accountTokens.ts');
expect(source).not.toContain('rebuildRoutes: true');
});
});
-1
View File
@@ -233,7 +233,6 @@ async function executeAccountTokenSync(row: AccountWithSiteRow): Promise<SyncExe
accountId,
preferredApiToken: row.accounts.apiToken,
defaultTokenSource: 'legacy',
rebuildRoutes: true,
});
if (convergence.defaultTokenId != null) {
return {
+7 -12
View File
@@ -3,8 +3,10 @@ import { db, schema, runtimeDbDialect } from '../../db/index.js';
import { and, eq, gte, lt, sql } from 'drizzle-orm';
import { refreshBalance } from '../../services/balanceService.js';
import { getAdapter } from '../../services/platforms/index.js';
import { rebuildTokenRoutesFromAvailability } from '../../services/modelService.js';
import { convergeAccountMutation } from '../../services/accountMutationWorkflow.js';
import {
convergeAccountMutation,
rebuildRoutesBestEffort,
} from '../../services/accountMutationWorkflow.js';
import {
getCredentialModeFromExtraConfig,
getProxyUrlFromExtraConfig,
@@ -1330,9 +1332,7 @@ export async function accountsRoutes(app: FastifyInstance) {
app.delete<{ Params: { id: string } }>('/api/accounts/:id', async (request) => {
const id = parseInt(request.params.id);
await db.delete(schema.accounts).where(eq(schema.accounts.id, id)).run();
try {
await rebuildTokenRoutesFromAvailability();
} catch { }
await rebuildRoutesBestEffort();
return { success: true };
});
@@ -1386,9 +1386,7 @@ export async function accountsRoutes(app: FastifyInstance) {
}
if (shouldRebuildRoutes) {
try {
await rebuildTokenRoutesFromAvailability();
} catch { }
await rebuildRoutesBestEffort();
}
return {
@@ -1596,10 +1594,7 @@ export async function accountsRoutes(app: FastifyInstance) {
}
}
});
try {
await rebuildTokenRoutesFromAvailability();
} catch { }
await rebuildRoutesBestEffort();
return { success: true };
} catch (err: any) {
@@ -92,6 +92,12 @@ describe('proxy route architecture boundaries', () => {
expect(source).not.toContain('normalizeContentText(');
});
it('keeps codex runtime header and prompt-cache derivation inside provider profiles', () => {
const source = readSource('./upstreamEndpoint.ts');
expect(source).not.toContain('buildCodexRuntimeHeaders(');
expect(source).not.toContain('shouldInjectDerivedPromptCacheKey');
});
it('keeps gemini runtime closure in transformer-owned helpers', () => {
const source = readSource('./gemini.ts');
const surfaceSource = readSource('../../proxy-core/surfaces/geminiSurface.ts');
@@ -139,6 +145,23 @@ describe('proxy route architecture boundaries', () => {
expect(surfaceSource).toContain('openAiResponsesTransformer.proxyStream.createSession(');
});
it('keeps oauth refresh recovery and success bookkeeping behind shared surface helpers', () => {
const chatSurfaceSource = readSource('../../proxy-core/surfaces/chatSurface.ts');
const responsesSurfaceSource = readSource('../../proxy-core/surfaces/openAiResponsesSurface.ts');
expect(chatSurfaceSource).toContain('trySurfaceOauthRefreshRecovery(');
expect(chatSurfaceSource).toContain('recordSurfaceSuccess(');
expect(chatSurfaceSource).not.toContain('refreshOauthAccessTokenSingleflight(');
expect(chatSurfaceSource).not.toContain('resolveProxyUsageWithSelfLogFallback(');
expect(chatSurfaceSource).not.toContain('resolveProxyLogBilling(');
expect(responsesSurfaceSource).toContain('trySurfaceOauthRefreshRecovery(');
expect(responsesSurfaceSource).toContain('recordSurfaceSuccess(');
expect(responsesSurfaceSource).not.toContain('refreshOauthAccessTokenSingleflight(');
expect(responsesSurfaceSource).not.toContain('resolveProxyUsageWithSelfLogFallback(');
expect(responsesSurfaceSource).not.toContain('resolveProxyLogBilling(');
});
it('keeps canonical transformer contracts imported from the transformer boundary only', () => {
const chatSource = readSource('./chat.ts');
const responsesSource = readSource('./responses.ts');
+6 -31
View File
@@ -29,10 +29,7 @@ import {
} from '../../transformers/shared/endpointCompatibility.js';
import {
buildClaudeRuntimeHeaders,
buildCodexRuntimeHeaders,
buildGeminiCliRuntimeHeaders,
buildGeminiCliUserAgent,
getInputHeader,
headerValueToString,
} from '../../proxy-core/providers/headerUtils.js';
export {
@@ -1234,7 +1231,10 @@ export function buildUpstreamEndpointRequest(input: {
);
const configuredResponsesBody = applyConfiguredPayloadRules(body);
if (providerProfile?.id === 'codex') {
if (sitePlatform === 'codex') {
if (providerProfile?.id !== 'codex') {
throw new Error(`missing codex provider profile for platform: ${sitePlatform}`);
}
return providerProfile.prepareRequest({
endpoint: 'responses',
modelName: input.modelName,
@@ -1254,39 +1254,14 @@ export function buildUpstreamEndpointRequest(input: {
});
}
const headers = sitePlatform === 'codex'
? buildCodexRuntimeHeaders({
baseHeaders: {
...commonHeaders,
...responsesHeaders,
},
providerHeaders: input.providerHeaders,
explicitSessionId: asTrimmedString(input.codexExplicitSessionId) || null,
continuityKey: asTrimmedString(input.codexSessionCacheKey) || null,
})
: ensureStreamAcceptHeader({
const headers = ensureStreamAcceptHeader({
...commonHeaders,
...responsesHeaders,
}, input.stream);
const codexSessionId = sitePlatform === 'codex'
? (getInputHeader(headers, 'session_id') || getInputHeader(headers, 'session-id'))
: null;
const shouldInjectDerivedPromptCacheKey = sitePlatform === 'codex'
&& !!codexSessionId
&& !asTrimmedString((configuredResponsesBody as Record<string, unknown>).prompt_cache_key)
&& !asTrimmedString(input.codexExplicitSessionId)
&& !!asTrimmedString(input.codexSessionCacheKey);
const runtimeBody = shouldInjectDerivedPromptCacheKey
? {
...configuredResponsesBody,
prompt_cache_key: codexSessionId,
}
: configuredResponsesBody;
return {
path: resolveEndpointPath('responses'),
headers,
body: runtimeBody,
body: configuredResponsesBody,
runtime,
};
}
@@ -107,6 +107,19 @@ describe('accountMutationWorkflow', () => {
expect(rebuildTokenRoutesFromAvailabilityMock).toHaveBeenCalledTimes(1);
});
it('only marks refreshedModels when the refresh result explicitly says it refreshed', async () => {
refreshModelsForAccountMock.mockResolvedValue({ accountId: 4, refreshed: false, status: 'skipped' });
const { convergeAccountMutation } = await import('./accountMutationWorkflow.js');
const result = await convergeAccountMutation({
accountId: 4,
refreshModels: true,
});
expect(result.modelRefreshResult).toEqual({ accountId: 4, refreshed: false, status: 'skipped' });
expect(result.refreshedModels).toBe(false);
});
it('refreshes model coverage in batches and maps failures', async () => {
refreshModelsForAccountMock
.mockResolvedValueOnce({ accountId: 1, refreshed: true, status: 'success' })
@@ -129,4 +142,26 @@ describe('accountMutationWorkflow', () => {
result: { createdRoutes: 2 },
});
});
it('rejects invalid batch sizes before starting the batch loop', async () => {
const { refreshAccountCoverageBatch } = await import('./accountMutationWorkflow.js');
await expect(refreshAccountCoverageBatch({
accountIds: [1, 2],
batchSize: 1.5,
mapFailure: (accountId, errorMessage) => ({ accountId, errorMessage }),
})).rejects.toThrow('batchSize must be a positive integer');
expect(refreshModelsForAccountMock).not.toHaveBeenCalled();
});
it('exposes a best-effort route rebuild helper for controller callers', async () => {
rebuildTokenRoutesFromAvailabilityMock.mockResolvedValueOnce({ createdRoutes: 1 });
const { rebuildRoutesBestEffort } = await import('./accountMutationWorkflow.js');
await expect(rebuildRoutesBestEffort()).resolves.toBe(true);
rebuildTokenRoutesFromAvailabilityMock.mockRejectedValueOnce(new Error('rebuild failed'));
await expect(rebuildRoutesBestEffort()).resolves.toBe(false);
});
});
+17 -3
View File
@@ -20,6 +20,15 @@ export type CoverageBatchRebuildResult =
| { success: true; result: Awaited<ReturnType<typeof rebuildTokenRoutesFromAvailability>> }
| { success: false; error: string };
export async function rebuildRoutesBestEffort(): Promise<boolean> {
try {
await rebuildTokenRoutesFromAvailability();
return true;
} catch {
return false;
}
}
export async function convergeAccountMutation(input: {
accountId: number;
preferredApiToken?: string | null;
@@ -110,7 +119,7 @@ export async function convergeAccountMutation(input: {
const modelRefreshResult = await runStep(() => refreshModelsForAccount(input.accountId));
if (modelRefreshResult) {
result.modelRefreshResult = modelRefreshResult;
result.refreshedModels = true;
result.refreshedModels = modelRefreshResult.refreshed === true;
}
}
@@ -133,6 +142,11 @@ export async function refreshAccountCoverageBatch<TFailure>(input: {
refresh: Array<ModelRefreshResult | TFailure>;
rebuild: CoverageBatchRebuildResult | null;
}> {
if (!Number.isInteger(input.batchSize) || input.batchSize <= 0) {
throw new Error('batchSize must be a positive integer');
}
const batchSize = input.batchSize;
const uniqueAccountIds = Array.from(new Set(
input.accountIds.filter((id) => Number.isFinite(id) && id > 0),
));
@@ -142,8 +156,8 @@ export async function refreshAccountCoverageBatch<TFailure>(input: {
}
const refresh: Array<ModelRefreshResult | TFailure> = [];
for (let offset = 0; offset < uniqueAccountIds.length; offset += input.batchSize) {
const batch = uniqueAccountIds.slice(offset, offset + input.batchSize);
for (let offset = 0; offset < uniqueAccountIds.length; offset += batchSize) {
const batch = uniqueAccountIds.slice(offset, offset + batchSize);
const settled = await Promise.allSettled(
batch.map(async (accountId) => refreshModelsForAccount(accountId)),
);
@@ -7,6 +7,7 @@ import {
class TestStandardApiProviderAdapter extends StandardApiProviderAdapterBase {
readonly platformName = 'test-standard';
fetchJsonImpl = async () => ({ data: [] as Array<{ id: string }> });
async detect(_url: string): Promise<boolean> {
return false;
@@ -15,6 +16,14 @@ class TestStandardApiProviderAdapter extends StandardApiProviderAdapterBase {
async getModels(_baseUrl: string, _token: string): Promise<string[]> {
return [];
}
protected override async fetchJson<T>(url: string, options?: Parameters<StandardApiProviderAdapterBase['fetchJson']>[1]): Promise<T> {
return this.fetchJsonImpl(url, options) as Promise<T>;
}
async fetchModelsForTest(options: Parameters<StandardApiProviderAdapterBase['fetchModelsFromStandardEndpoint']>[0]) {
return this.fetchModelsFromStandardEndpoint(options);
}
}
describe('standardApiProvider helpers', () => {
@@ -43,4 +52,33 @@ describe('standardApiProvider helpers', () => {
quota: 0,
});
});
it('does not swallow mapper bugs while still returning empty lists for network failures', async () => {
const adapter = new TestStandardApiProviderAdapter();
adapter.fetchJsonImpl = async () => ({ data: [{ id: 'gpt-5' }] });
await expect(adapter.fetchModelsForTest({
baseUrl: 'https://api.example.com',
mapResponse: () => {
throw new Error('mapper exploded');
},
})).rejects.toThrow('mapper exploded');
adapter.fetchJsonImpl = async () => {
throw new Error('network failed');
};
await expect(adapter.fetchModelsForTest({
baseUrl: 'https://api.example.com',
})).resolves.toEqual([]);
});
it('rejects invalid payload shapes instead of silently treating them as no models', async () => {
const adapter = new TestStandardApiProviderAdapter();
adapter.fetchJsonImpl = async () => ({ data: 'not-an-array' });
await expect(adapter.fetchModelsForTest({
baseUrl: 'https://api.example.com',
})).rejects.toThrow('invalid standard models payload');
});
});
@@ -60,18 +60,27 @@ export abstract class StandardApiProviderAdapterBase extends BasePlatformAdapter
? options.resolveUrl(normalizedBaseUrl)
: resolveVersionedModelsUrl(normalizedBaseUrl);
let payload: any;
try {
const payload = await this.fetchJson<any>(url, {
payload = await this.fetchJson<any>(url, {
headers: options.headers,
});
const rows = options.mapResponse
? options.mapResponse(payload)
: (payload?.data || []).map((item: any) => item?.id);
return rows
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item) => item.length > 0);
} catch {
return [];
}
const rows = options.mapResponse
? options.mapResponse(payload)
: Array.isArray(payload?.data)
? payload.data.map((item: any) => item?.id)
: null;
if (!Array.isArray(rows)) {
throw new Error('invalid standard models payload');
}
return rows
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item) => item.length > 0);
}
}
@@ -34,6 +34,47 @@ describe('responses conversion single source of truth', () => {
expect(convertOpenAiBodyToResponsesBodyViaCompatibility).toBe(convertOpenAiBodyToResponsesBody);
expect(sanitizeResponsesBodyForProxyViaCompatibility).toBe(sanitizeResponsesBodyForProxy);
});
it('preserves extra properties when normalizing role-only message items', () => {
expect(normalizeResponsesInputForCompatibility([
{
role: 'assistant',
content: 'done',
id: 'msg_1',
status: 'completed',
},
])).toEqual([
{
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'done' }],
id: 'msg_1',
status: 'completed',
},
]);
});
it('filters whitespace-only string entries from normalized responses input arrays', () => {
expect(normalizeResponsesInputForCompatibility([
'hello',
' ',
{
role: 'user',
content: 'world',
},
])).toEqual([
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'hello' }],
},
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'world' }],
},
]);
});
});
describe('sanitizeResponsesBodyForProxy', () => {
@@ -136,6 +136,7 @@ export function normalizeResponsesMessageItem(item: Record<string, unknown>): Re
if (asTrimmedString(item.role)) {
return {
...item,
type: 'message',
role,
content: normalizedContent,
@@ -158,13 +159,13 @@ export function normalizeResponsesInputForCompatibility(input: unknown): unknown
}
if (Array.isArray(input)) {
return input.map((item) => {
return input.flatMap((item) => {
if (typeof item === 'string') {
const normalized = item.trim();
return normalized ? toResponsesInputMessageFromText(normalized) : item;
return normalized ? [toResponsesInputMessageFromText(normalized)] : [];
}
if (!isRecord(item)) return item;
return normalizeResponsesMessageItem(item);
if (!isRecord(item)) return [item];
return [normalizeResponsesMessageItem(item)];
});
}
+10 -4
View File
@@ -17,6 +17,12 @@ function normalizeValue(value) {
return (value || '').trim().toLowerCase();
}
function normalizeMimeType(value) {
const normalized = normalizeValue(value);
const [essence] = normalized.split(';');
return (essence || '').trim();
}
export function inferConversationFileMimeType(filename) {
const normalized = normalizeValue(filename);
if (normalized.endsWith('.pdf')) return 'application/pdf';
@@ -41,7 +47,7 @@ export function inferConversationFileMimeType(filename) {
}
export function resolveConversationFileMimeType(mimeType, filename) {
const normalizedMimeType = normalizeValue(mimeType);
const normalizedMimeType = normalizeMimeType(mimeType);
if (normalizedMimeType && !GENERIC_MIME_TYPES.has(normalizedMimeType)) {
return normalizedMimeType;
}
@@ -49,14 +55,14 @@ export function resolveConversationFileMimeType(mimeType, filename) {
}
export function classifyConversationFileMimeType(mimeType) {
const normalized = normalizeValue(mimeType);
const normalized = normalizeMimeType(mimeType);
if (normalized.startsWith('image/')) return 'image';
if (normalized.startsWith('audio/')) return 'audio';
return 'document';
}
export function detectConversationFileKind(file) {
const mimeType = normalizeValue(file?.mimeType);
const mimeType = normalizeMimeType(file?.mimeType);
if (mimeType) {
if (mimeType.startsWith('image/')) return 'image';
if (mimeType.startsWith('audio/')) return 'audio';
@@ -73,7 +79,7 @@ export function detectConversationFileKind(file) {
}
export function isSupportedConversationFileMimeType(mimeType) {
const normalized = normalizeValue(mimeType);
const normalized = normalizeMimeType(mimeType);
return DOCUMENT_MIME_TYPES.has(normalized)
|| normalized.startsWith('image/')
|| normalized.startsWith('audio/');
+10
View File
@@ -42,4 +42,14 @@ describe('conversationFileTypes', () => {
mimeType: 'application/octet-stream',
})).toBe('document');
});
it('strips MIME parameters before matching support and kind rules', () => {
expect(resolveConversationFileMimeType('text/plain; charset=utf-8', 'notes.txt')).toBe('text/plain');
expect(classifyConversationFileMimeType('text/plain; charset=utf-8')).toBe('document');
expect(detectConversationFileKind({
filename: 'notes.txt',
mimeType: 'text/plain; charset=utf-8',
})).toBe('document');
expect(isSupportedConversationFileMimeType('text/plain; charset=utf-8')).toBe(true);
});
});
+6 -4
View File
@@ -86,6 +86,11 @@ function isSafeRegexPatternBody(body) {
continue;
}
if (ch === '\\') {
const next = body[index + 1];
if (!next) return false;
if (/[a-z]/i.test(next) && next !== 'd') {
return false;
}
escaped = true;
continue;
}
@@ -99,10 +104,7 @@ function isSafeRegexPatternBody(body) {
}
if (ch === '(') {
if (body[index + 1] === '?') {
if (body[index + 2] !== ':') return false;
groupStack.push({ hasInnerQuantifier: false, hasAlternation: false });
index += 2;
continue;
return false;
}
groupStack.push({ hasInnerQuantifier: false, hasAlternation: false });
continue;
+12
View File
@@ -21,6 +21,18 @@ describe('token route pattern helpers', () => {
expect(matchesTokenRouteModelPattern('claude-sonnet-4-6', 're:(?=claude)')).toBe(false);
});
it('rejects regex syntax that the lightweight parser does not implement', async () => {
const {
matchesTokenRouteModelPattern,
parseTokenRouteRegexPattern,
} = await import('./tokenRoutePatterns.js');
expect(parseTokenRouteRegexPattern('re:^(?:gpt|claude)-5$').regex).toBeNull();
expect(matchesTokenRouteModelPattern('gpt-5', 're:^(?:gpt|claude)-5$')).toBe(false);
expect(parseTokenRouteRegexPattern('re:^gpt-\\s+$').regex).toBeNull();
expect(matchesTokenRouteModelPattern('gpt- ', 're:^gpt-\\s+$')).toBe(false);
});
it('supports exact, glob, and safe regex route matches', async () => {
const { matchesTokenRouteModelPattern } = await import('./tokenRoutePatterns.js');