1
0
forked from boos/BoosAPI

feat: extend proxy runtime parity

This commit is contained in:
ciat-777
2026-03-18 12:01:05 +08:00
parent 20ef75e76d
commit cd37b11722
21 changed files with 2756 additions and 112 deletions
@@ -0,0 +1,157 @@
import Fastify, { type FastifyInstance } from 'fastify';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
const fetchMock = vi.fn();
const selectChannelMock = vi.fn();
const selectNextChannelMock = vi.fn();
const recordSuccessMock = vi.fn();
const recordFailureMock = vi.fn();
const refreshModelsAndRebuildRoutesMock = vi.fn();
const reportProxyAllFailedMock = vi.fn();
const reportTokenExpiredMock = vi.fn();
const resolveProxyUsageWithSelfLogFallbackMock = vi.fn(async ({ usage }: any) => ({
...usage,
estimatedCostFromQuota: 0,
recoveredFromSelfLog: false,
}));
const dbInsertMock = vi.fn((_arg?: any) => ({
values: () => ({
run: () => undefined,
}),
}));
vi.mock('undici', () => ({
fetch: (...args: unknown[]) => fetchMock(...args),
}));
vi.mock('../../services/tokenRouter.js', () => ({
tokenRouter: {
selectChannel: (...args: unknown[]) => selectChannelMock(...args),
selectNextChannel: (...args: unknown[]) => selectNextChannelMock(...args),
recordSuccess: (...args: unknown[]) => recordSuccessMock(...args),
recordFailure: (...args: unknown[]) => recordFailureMock(...args),
},
}));
vi.mock('../../services/modelService.js', () => ({
refreshModelsAndRebuildRoutes: (...args: unknown[]) => refreshModelsAndRebuildRoutesMock(...args),
}));
vi.mock('../../services/alertService.js', () => ({
reportProxyAllFailed: (...args: unknown[]) => reportProxyAllFailedMock(...args),
reportTokenExpired: (...args: unknown[]) => reportTokenExpiredMock(...args),
}));
vi.mock('../../services/alertRules.js', () => ({
isTokenExpiredError: () => false,
}));
vi.mock('../../services/modelPricingService.js', () => ({
estimateProxyCost: async () => 0,
buildProxyBillingDetails: async () => null,
fetchModelPricingCatalog: async () => null,
}));
vi.mock('../../services/proxyRetryPolicy.js', () => ({
shouldRetryProxyRequest: () => false,
}));
vi.mock('../../services/proxyUsageFallbackService.js', () => ({
resolveProxyUsageWithSelfLogFallback: (arg: any) => resolveProxyUsageWithSelfLogFallbackMock(arg),
}));
vi.mock('../../services/oauth/quota.js', () => ({
recordOauthQuotaResetHint: async () => undefined,
}));
vi.mock('../../db/index.js', () => ({
db: {
insert: (arg: any) => dbInsertMock(arg),
},
hasProxyLogDownstreamApiKeyIdColumn: async () => false,
schema: {
proxyLogs: {},
},
}));
describe('claude count_tokens proxy route', () => {
let app: FastifyInstance;
beforeAll(async () => {
const { claudeMessagesProxyRoute } = await import('./chat.js');
app = Fastify();
await app.register(claudeMessagesProxyRoute);
});
beforeEach(() => {
fetchMock.mockReset();
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
recordSuccessMock.mockReset();
recordFailureMock.mockReset();
refreshModelsAndRebuildRoutesMock.mockReset();
reportProxyAllFailedMock.mockReset();
reportTokenExpiredMock.mockReset();
resolveProxyUsageWithSelfLogFallbackMock.mockClear();
dbInsertMock.mockClear();
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
site: { name: 'claude-site', url: 'https://api.anthropic.com', platform: 'claude' },
account: { id: 33, username: 'claude-user@example.com' },
tokenName: 'default',
tokenValue: 'sk-claude',
actualModel: 'claude-opus-4-6',
});
selectNextChannelMock.mockReturnValue(null);
});
afterAll(async () => {
await app.close();
});
it('forwards /v1/messages/count_tokens to the claude count_tokens upstream path', async () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify({
input_tokens: 42,
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/messages/count_tokens',
payload: {
model: 'claude-opus-4-6',
tools: [
{ name: 'lookup', input_schema: { type: 'object' } },
],
messages: [
{
role: 'user',
content: [{ type: 'text', text: 'count these tokens' }],
},
],
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ input_tokens: 42 });
expect(fetchMock).toHaveBeenCalledTimes(1);
const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toBe('https://api.anthropic.com/v1/messages/count_tokens?beta=true');
expect(options.headers['anthropic-version']).toBe('2023-06-01');
expect(options.headers['anthropic-beta']).toContain('claude-code-20250219');
expect(options.headers['Accept-Encoding']).toBe('gzip, deflate, br, zstd');
const forwardedBody = JSON.parse(String(options.body));
expect(forwardedBody.model).toBe('claude-opus-4-6');
expect(forwardedBody.messages).toEqual([
{
role: 'user',
content: [{ type: 'text', text: 'count these tokens', cache_control: { type: 'ephemeral' } }],
},
]);
});
});
+301 -28
View File
@@ -1,7 +1,6 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { tokenRouter } from '../../services/tokenRouter.js';
import { db, hasProxyLogDownstreamApiKeyIdColumn, schema } from '../../db/index.js';
import { fetch } from 'undici';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
@@ -11,6 +10,7 @@ import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParse
import { resolveProxyUrlForSite, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { type DownstreamFormat } from '../../transformers/shared/normalized.js';
import {
buildClaudeCountTokensUpstreamRequest,
buildUpstreamEndpointRequest,
resolveUpstreamEndpointCandidates,
} from './upstreamEndpoint.js';
@@ -20,7 +20,7 @@ import {
recordDownstreamCostUsage,
} from './downstreamPolicy.js';
import { composeProxyLogMessage } from './logPathMeta.js';
import { executeEndpointFlow } from './endpointFlow.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from './endpointFlow.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveProxyLogBilling } from './proxyBilling.js';
import { openAiChatTransformer } from '../../transformers/openai/chat/index.js';
@@ -36,15 +36,25 @@ import {
buildOauthProviderHeaders,
} from '../../services/oauth/service.js';
import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js';
import { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import { collectResponsesFinalPayloadFromSse } from './responsesSseFinal.js';
import {
createGeminiCliStreamReader,
unwrapGeminiCliPayload,
} from './geminiCliCompat.js';
import { dispatchRuntimeRequest } from './runtimeExecutor.js';
const MAX_RETRIES = 2;
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
export async function chatProxyRoute(app: FastifyInstance) {
app.post('/v1/chat/completions', async (request: FastifyRequest, reply: FastifyReply) =>
handleChatProxyRequest(request, reply, 'openai'));
@@ -53,6 +63,8 @@ export async function chatProxyRoute(app: FastifyInstance) {
export async function claudeMessagesProxyRoute(app: FastifyInstance) {
app.post('/v1/messages', async (request: FastifyRequest, reply: FastifyReply) =>
handleChatProxyRequest(request, reply, 'claude'));
app.post('/v1/messages/count_tokens', async (request: FastifyRequest, reply: FastifyReply) =>
handleClaudeCountTokensRequest(request, reply));
}
async function handleChatProxyRequest(
@@ -96,6 +108,12 @@ async function handleChatProxyRequest(
}
}
const hasNonImageFileInput = hasNonImageFileInputInOpenAiBody(resolvedOpenAiBody);
const codexSessionCacheKey = deriveCodexSessionCacheKey({
downstreamFormat,
body: downstreamFormat === 'claude' ? claudeOriginalBody : request.body,
requestedModel,
proxyToken: getProxyAuthContext(request)?.token || null,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const logDownstreamApiKeyId = downstreamApiKeyId !== null
&& await hasProxyLogDownstreamApiKeyIdColumn();
@@ -153,29 +171,46 @@ async function handleChatProxyRequest(
options: { forceNormalizeClaudeBody?: boolean } = {},
) => {
const upstreamStream = isStream || (isCodexSite && endpoint === 'responses');
const endpointRequest = buildUpstreamEndpointRequest({
endpoint,
modelName,
stream: upstreamStream,
tokenValue: selected.tokenValue,
oauthProvider: oauth?.provider,
oauthProjectId: oauth?.projectId,
sitePlatform: selected.site.platform,
siteUrl: selected.site.url,
openaiBody: resolvedOpenAiBody,
const endpointRequest = buildUpstreamEndpointRequest({
endpoint,
modelName,
stream: upstreamStream,
tokenValue: selected.tokenValue,
oauthProvider: oauth?.provider,
oauthProjectId: oauth?.projectId,
sitePlatform: selected.site.platform,
siteUrl: selected.site.url,
openaiBody: resolvedOpenAiBody,
downstreamFormat,
claudeOriginalBody,
forceNormalizeClaudeBody: options.forceNormalizeClaudeBody,
downstreamHeaders: request.headers as Record<string, unknown>,
providerHeaders: buildProviderHeaders(),
codexSessionCacheKey,
});
return {
endpoint,
path: endpointRequest.path,
headers: endpointRequest.headers,
body: endpointRequest.body as Record<string, unknown>,
runtime: endpointRequest.runtime,
};
};
const dispatchRequest = (
compatibilityRequest: BuiltEndpointRequest,
targetUrl?: string,
) => (
dispatchRuntimeRequest({
siteUrl: selected.site.url,
targetUrl,
request: compatibilityRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}),
})
);
const endpointStrategy = downstreamTransformer.compatibility.createEndpointStrategy({
downstreamFormat,
endpointCandidates,
@@ -187,14 +222,7 @@ async function handleChatProxyRequest(
endpoint,
{ forceNormalizeClaudeBody },
),
dispatchRequest: (compatibilityRequest, targetUrl) => fetch(
targetUrl ?? `${selected.site.url}${compatibilityRequest.path}`,
withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: compatibilityRequest.headers,
body: JSON.stringify(compatibilityRequest.body),
}),
),
dispatchRequest,
});
const tryRecover = async (ctx: Parameters<NonNullable<typeof endpointStrategy.tryRecover>>[0]) => {
if (ctx.response.status === 401 && oauth) {
@@ -208,14 +236,7 @@ async function handleChatProxyRequest(
};
const refreshedRequest = buildEndpointRequest(ctx.request.endpoint);
const refreshedTargetUrl = `${selected.site.url}${refreshedRequest.path}`;
const refreshedResponse = await fetch(
refreshedTargetUrl,
withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: refreshedRequest.headers,
body: JSON.stringify(refreshedRequest.body),
}),
);
const refreshedResponse = await dispatchRequest(refreshedRequest, refreshedTargetUrl);
if (refreshedResponse.ok) {
return {
upstream: refreshedResponse,
@@ -239,6 +260,7 @@ async function handleChatProxyRequest(
proxyUrl: resolveProxyUrlForSite(selected.site),
endpointCandidates,
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
tryRecover,
shouldDowngrade: endpointStrategy.shouldDowngrade,
onDowngrade: (ctx) => {
@@ -266,6 +288,7 @@ async function handleChatProxyRequest(
if (!endpointResult.ok) {
const status = endpointResult.status || 502;
const errText = endpointResult.errText || 'unknown error';
const rawErrText = endpointResult.rawErrText || errText;
tokenRouter.recordFailure(selected.channel.id);
logProxy(
selected,
@@ -285,6 +308,11 @@ async function handleChatProxyRequest(
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
);
await recordOauthQuotaResetHint({
accountId: selected.account.id,
statusCode: status,
errorText: rawErrText,
});
if (isTokenExpiredError({ status, message: errText })) {
await reportTokenExpired({
@@ -577,6 +605,251 @@ async function handleChatProxyRequest(
}
}
function deriveCodexSessionCacheKey(input: {
downstreamFormat: DownstreamFormat | 'responses';
body: unknown;
requestedModel: string;
proxyToken: string | null;
}): string | null {
if (isRecord(input.body)) {
if (input.downstreamFormat === 'claude' && isRecord(input.body.metadata)) {
const userId = asTrimmedString(input.body.metadata.user_id);
if (userId) return `${input.requestedModel}:claude:${userId}`;
}
const promptCacheKey = asTrimmedString(input.body.prompt_cache_key);
if (promptCacheKey) return `${input.requestedModel}:responses:${promptCacheKey}`;
}
const proxyToken = asTrimmedString(input.proxyToken);
if (proxyToken) {
return `${input.requestedModel}:proxy:${proxyToken}`;
}
return null;
}
async function handleClaudeCountTokensRequest(
request: FastifyRequest,
reply: FastifyReply,
) {
const rawBody = isRecord(request.body) ? { ...request.body } : null;
if (!rawBody) {
return reply.code(400).send({
error: {
message: 'Request body must be a JSON object',
type: 'invalid_request_error',
},
});
}
const requestedModel = asTrimmedString(rawBody.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 downstreamPath = '/v1/messages/count_tokens';
const clientContext = detectDownstreamClientContext({
downstreamPath,
headers: request.headers as Record<string, unknown>,
body: rawBody,
});
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const logDownstreamApiKeyId = downstreamApiKeyId !== null
&& await hasProxyLogDownstreamApiKeyIdColumn();
const excludeChannelIds: number[] = [];
let retryCount = 0;
while (retryCount <= MAX_RETRIES) {
let selected = retryCount === 0
? await tokenRouter.selectChannel(requestedModel, downstreamPolicy)
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
if (!selected && retryCount === 0) {
await 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' },
});
}
excludeChannelIds.push(selected.channel.id);
if (String(selected.site.platform || '').trim().toLowerCase() !== 'claude') {
if (retryCount < MAX_RETRIES) {
retryCount += 1;
continue;
}
return reply.code(501).send({
error: {
message: 'Claude count_tokens compatibility is not implemented for this upstream',
type: 'invalid_request_error',
},
});
}
const modelName = selected.actualModel || requestedModel;
const oauth = getOauthInfoFromExtraConfig(selected.account.extraConfig);
const startTime = Date.now();
const buildRequest = () => {
const upstreamRequest = buildClaudeCountTokensUpstreamRequest({
modelName,
tokenValue: selected.tokenValue,
oauthProvider: oauth?.provider,
sitePlatform: selected.site.platform,
claudeBody: rawBody,
downstreamHeaders: request.headers as Record<string, unknown>,
});
return {
endpoint: 'messages' as const,
path: upstreamRequest.path,
headers: upstreamRequest.headers,
body: upstreamRequest.body,
runtime: upstreamRequest.runtime,
};
};
try {
let upstreamRequest = buildRequest();
let upstream = await dispatchRuntimeRequest({
siteUrl: selected.site.url,
request: upstreamRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}),
});
if (upstream.status === 401 && 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,
};
upstreamRequest = buildRequest();
upstream = await dispatchRuntimeRequest({
siteUrl: selected.site.url,
request: upstreamRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}),
});
} catch {
// Fall through to the regular upstream error handling below.
}
}
const latency = Date.now() - startTime;
const contentType = upstream.headers.get('content-type') || 'application/json';
const text = await upstream.text();
let payload: unknown = text;
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
if (!upstream.ok) {
tokenRouter.recordFailure(selected.channel.id);
logProxy(
selected,
requestedModel,
'failed',
upstream.status,
latency,
typeof payload === 'string' ? payload : JSON.stringify(payload),
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
upstreamRequest.path,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
);
if (shouldRetryProxyRequest(upstream.status, typeof payload === 'string' ? payload : text) && retryCount < MAX_RETRIES) {
retryCount += 1;
continue;
}
return reply.code(upstream.status).type(contentType).send(payload);
}
tokenRouter.recordSuccess(selected.channel.id, latency, 0);
recordDownstreamCostUsage(request, 0);
logProxy(
selected,
requestedModel,
'success',
upstream.status,
latency,
null,
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
upstreamRequest.path,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
);
return reply.code(upstream.status).type(contentType).send(payload);
} catch (error: any) {
tokenRouter.recordFailure(selected.channel.id);
logProxy(
selected,
requestedModel,
'failed',
0,
Date.now() - startTime,
error?.message || 'network error',
retryCount,
downstreamPath,
0,
0,
0,
0,
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
);
if (retryCount < MAX_RETRIES) {
retryCount += 1;
continue;
}
return reply.code(502).send({
error: {
message: `Upstream error: ${error?.message || 'network failure'}`,
type: 'upstream_error',
},
});
}
}
}
async function logProxy(
selected: any,
modelRequested: string,
@@ -58,6 +58,24 @@ describe('executeEndpointFlow', () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('uses the injected dispatchRequest hook instead of the default fetch path', async () => {
const dispatchRequest = vi.fn(async () => toUndiciResponse(new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
})));
const result = await executeEndpointFlow({
siteUrl: 'https://example.com',
endpointCandidates: ['responses'],
buildRequest: () => requestFor('/v1/responses'),
dispatchRequest,
});
expect(result.ok).toBe(true);
expect(dispatchRequest).toHaveBeenCalledTimes(1);
expect(fetchMock).not.toHaveBeenCalled();
});
it('avoids duplicated /v1 when base url already ends with /v1', async () => {
fetchMock.mockResolvedValueOnce(toUndiciResponse(new Response(JSON.stringify({ ok: true }), {
status: 200,
+22 -5
View File
@@ -9,6 +9,13 @@ export type BuiltEndpointRequest = {
path: string;
headers: Record<string, string>;
body: Record<string, unknown>;
runtime?: {
executor: 'default' | 'codex' | 'gemini-cli' | 'antigravity' | 'claude';
modelName?: string;
stream?: boolean;
oauthProjectId?: string | null;
action?: 'generateContent' | 'streamGenerateContent' | 'countTokens';
};
};
export type EndpointAttemptContext = {
@@ -35,6 +42,7 @@ export type EndpointFlowResult =
ok: false;
status: number;
errText: string;
rawErrText?: string;
};
export function withUpstreamPath(path: string, message: string): string {
@@ -46,6 +54,10 @@ type ExecuteEndpointFlowInput = {
proxyUrl?: string | null;
endpointCandidates: UpstreamEndpoint[];
buildRequest: (endpoint: UpstreamEndpoint, endpointIndex: number) => BuiltEndpointRequest;
dispatchRequest?: (
request: BuiltEndpointRequest,
targetUrl: string,
) => Promise<Awaited<ReturnType<typeof fetch>>>;
tryRecover?: (ctx: EndpointAttemptContext) => Promise<EndpointRecoverResult>;
shouldDowngrade?: (ctx: EndpointAttemptContext) => boolean;
onDowngrade?: (ctx: EndpointAttemptContext & { errText: string }) => void | Promise<void>;
@@ -63,17 +75,20 @@ export async function executeEndpointFlow(input: ExecuteEndpointFlowInput): Prom
let finalStatus = 0;
let finalErrText = 'unknown error';
let finalRawErrText: string | undefined;
for (let endpointIndex = 0; endpointIndex < endpointCount; endpointIndex += 1) {
const endpoint = input.endpointCandidates[endpointIndex] as UpstreamEndpoint;
const request = input.buildRequest(endpoint, endpointIndex);
const targetUrl = buildUpstreamUrl(input.siteUrl, request.path);
let response = await fetch(targetUrl, await withSiteProxyRequestInit(targetUrl, {
method: 'POST',
headers: request.headers,
body: JSON.stringify(request.body),
}));
let response = input.dispatchRequest
? await input.dispatchRequest(request, targetUrl)
: await fetch(targetUrl, await withSiteProxyRequestInit(targetUrl, {
method: 'POST',
headers: request.headers,
body: JSON.stringify(request.body),
}));
if (response.ok) {
return {
@@ -124,6 +139,7 @@ export async function executeEndpointFlow(input: ExecuteEndpointFlowInput): Prom
finalStatus = response.status;
finalErrText = errText;
finalRawErrText = rawErrText;
break;
}
@@ -131,5 +147,6 @@ export async function executeEndpointFlow(input: ExecuteEndpointFlowInput): Prom
ok: false,
status: finalStatus || 502,
errText: finalErrText || 'unknown error',
rawErrText: finalRawErrText,
};
}
+10 -2
View File
@@ -742,15 +742,21 @@ describe('gemini native proxy routes', () => {
expect(response.statusCode).toBe(200);
const [targetUrl, requestInit] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(targetUrl).toBe('https://cloudcode-pa.googleapis.com/v1internal:generateContent');
expect(targetUrl).toBe('https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent');
expect(requestInit.headers).toMatchObject({
Authorization: 'Bearer antigravity-access-token',
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent': 'antigravity/1.19.6 darwin/arm64',
});
expect(JSON.parse(String(requestInit.body))).toEqual({
const upstreamBody = JSON.parse(String(requestInit.body));
expect(upstreamBody).toMatchObject({
project: 'project-demo',
model: 'gemini-3-pro-preview',
userAgent: 'antigravity',
requestType: 'agent',
request: {
sessionId: expect.any(String),
contents: [
{
role: 'user',
@@ -759,6 +765,8 @@ describe('gemini native proxy routes', () => {
],
},
});
expect(upstreamBody.requestId).toMatch(/^agent-[0-9a-f-]{36}$/i);
expect(String(upstreamBody.request.sessionId)).toMatch(/^-\d+$/);
expect(response.json()).toEqual({
responseId: 'antigravity-response-1',
modelVersion: 'gemini-3-pro-preview',
+47 -14
View File
@@ -12,7 +12,7 @@ import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.j
import { withSiteProxyRequestInit } from '../../services/siteProxy.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { getDownstreamRoutingPolicy } from './downstreamPolicy.js';
import { executeEndpointFlow } from './endpointFlow.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from './endpointFlow.js';
import { composeProxyLogMessage } from './logPathMeta.js';
import {
buildUpstreamEndpointRequest,
@@ -28,6 +28,7 @@ import {
unwrapGeminiCliPayload,
wrapGeminiCliRequest,
} from './geminiCliCompat.js';
import { dispatchRuntimeRequest } from './runtimeExecutor.js';
const MAX_RETRIES = 2;
const GEMINI_MODEL_PROBES = [
@@ -442,11 +443,36 @@ export async function geminiProxyRoute(app: FastifyInstance) {
selected.tokenValue,
query,
);
const upstream = await fetch(targetUrl, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(requestBody),
});
const upstream = isInternalGemini
? await dispatchRuntimeRequest({
siteUrl: selected.site.url,
targetUrl,
request: {
endpoint: 'chat',
path: upstreamPath,
headers: requestHeaders,
body: requestBody as Record<string, unknown>,
runtime: {
executor: isGeminiCli ? 'gemini-cli' : 'antigravity',
modelName: actualModel,
stream: isStreamAction,
oauthProjectId: oauth?.projectId || null,
action: isCountTokensAction
? 'countTokens'
: (isStreamAction ? 'streamGenerateContent' : 'generateContent'),
},
},
buildInit: async (requestUrl, requestForFetch) => withSiteProxyRequestInit(requestUrl, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}),
})
: await fetch(targetUrl, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(requestBody),
});
const contentType = upstream.headers.get('content-type') || 'application/json';
if (!upstream.ok) {
lastStatus = upstream.status;
@@ -661,8 +687,21 @@ export async function geminiProxyRoute(app: FastifyInstance) {
path: endpointRequest.path,
headers: endpointRequest.headers,
body: endpointRequest.body as Record<string, unknown>,
runtime: endpointRequest.runtime,
};
};
const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => (
dispatchRuntimeRequest({
siteUrl: selected.site.url,
targetUrl,
request: compatibilityRequest,
buildInit: async (requestUrl, requestForFetch) => withSiteProxyRequestInit(requestUrl, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}),
})
);
const endpointStrategy = createChatEndpointStrategy({
downstreamFormat: 'openai',
endpointCandidates,
@@ -674,19 +713,13 @@ export async function geminiProxyRoute(app: FastifyInstance) {
endpoint,
{ forceNormalizeClaudeBody },
),
dispatchRequest: async (compatibilityRequest, targetUrl) => fetch(
targetUrl ?? `${selected.site.url}${compatibilityRequest.path}`,
await withSiteProxyRequestInit(selected.site.url, {
method: 'POST',
headers: compatibilityRequest.headers,
body: JSON.stringify(compatibilityRequest.body),
}),
),
dispatchRequest,
});
const endpointResult = await executeEndpointFlow({
siteUrl: selected.site.url,
endpointCandidates,
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
tryRecover: endpointStrategy.tryRecover,
shouldDowngrade: endpointStrategy.shouldDowngrade,
});
@@ -15,6 +15,7 @@ const resolveProxyUsageWithSelfLogFallbackMock = vi.fn(async ({ usage }: any) =>
recoveredFromSelfLog: false,
}));
const refreshOauthAccessTokenSingleflightMock = vi.fn();
const recordOauthQuotaResetHintMock = vi.fn();
const dbInsertMock = vi.fn((_arg?: any) => ({
values: () => ({
run: () => undefined,
@@ -65,6 +66,10 @@ vi.mock('../../services/oauth/refreshSingleflight.js', () => ({
refreshOauthAccessTokenSingleflight: (...args: unknown[]) => refreshOauthAccessTokenSingleflightMock(...args),
}));
vi.mock('../../services/oauth/quota.js', () => ({
recordOauthQuotaResetHint: (...args: unknown[]) => recordOauthQuotaResetHintMock(...args),
}));
vi.mock('../../db/index.js', () => ({
db: {
insert: (arg: any) => dbInsertMock(arg),
@@ -110,6 +115,7 @@ describe('responses proxy codex oauth refresh', () => {
reportTokenExpiredMock.mockReset();
resolveProxyUsageWithSelfLogFallbackMock.mockClear();
refreshOauthAccessTokenSingleflightMock.mockReset();
recordOauthQuotaResetHintMock.mockReset();
dbInsertMock.mockClear();
selectChannelMock.mockReturnValue({
@@ -185,6 +191,14 @@ describe('responses proxy codex oauth refresh', () => {
expect(secondOptions.headers.Authorization).toBe('Bearer fresh-access-token');
expect(secondOptions.headers.Originator || secondOptions.headers.originator).toBe('codex_cli_rs');
expect(secondOptions.headers['Chatgpt-Account-Id'] || secondOptions.headers['chatgpt-account-id']).toBe('chatgpt-account-123');
expect(secondOptions.headers.Version || secondOptions.headers.version).toBe('0.101.0');
expect(String(secondOptions.headers.Session_id || secondOptions.headers.session_id || '')).toMatch(/^[0-9a-f-]{36}$/i);
expect(secondOptions.headers.Conversation_id || secondOptions.headers.conversation_id).toBe(
secondOptions.headers.Session_id || secondOptions.headers.session_id,
);
expect(secondOptions.headers['User-Agent'] || secondOptions.headers['user-agent']).toBe('codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464');
expect(secondOptions.headers.Accept || secondOptions.headers.accept).toBe('text/event-stream');
expect(secondOptions.headers.Connection || secondOptions.headers.connection).toBe('Keep-Alive');
expect(response.json()?.output_text).toContain('ok after codex token refresh');
});
@@ -216,6 +230,9 @@ describe('responses proxy codex oauth refresh', () => {
const [, options] = fetchMock.mock.calls[0] as [string, any];
const forwardedBody = JSON.parse(options.body);
expect(forwardedBody.instructions).toBe('');
expect(forwardedBody.prompt_cache_key).toBe(
options.headers.Session_id || options.headers.session_id,
);
expect(forwardedBody.stream).toBe(true);
expect(forwardedBody.input).toEqual([
{
@@ -226,6 +243,41 @@ describe('responses proxy codex oauth refresh', () => {
]);
});
it('records codex usage_limit_reached reset hints on upstream 429 failures', async () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify({
error: {
type: 'usage_limit_reached',
resets_at: 1773800400,
message: 'quota exceeded',
},
}), {
status: 429,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/responses',
payload: {
model: 'gpt-5.2-codex',
input: 'hello codex',
},
});
expect(response.statusCode).toBe(429);
expect(recordOauthQuotaResetHintMock).toHaveBeenCalledWith({
accountId: 33,
statusCode: 429,
errorText: JSON.stringify({
error: {
type: 'usage_limit_reached',
resets_at: 1773800400,
message: 'quota exceeded',
},
}),
});
});
it('forces codex upstream responses requests to stream and aggregates the SSE payload for non-stream downstream callers', async () => {
fetchMock.mockResolvedValue(createSseResponse([
'event: response.created\n',
+60 -18
View File
@@ -1,5 +1,4 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fetch } from 'undici';
import { db, hasProxyLogDownstreamApiKeyIdColumn, schema } from '../../db/index.js';
import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
@@ -16,11 +15,12 @@ import {
} from './upstreamEndpoint.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js';
import { composeProxyLogMessage } from './logPathMeta.js';
import { executeEndpointFlow, withUpstreamPath } from './endpointFlow.js';
import { executeEndpointFlow, withUpstreamPath, type BuiltEndpointRequest } from './endpointFlow.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveProxyLogBilling } from './proxyBilling.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
import { detectDownstreamClientContext, isCodexResponsesSurface, type DownstreamClientContext } from './downstreamClientContext.js';
import { dispatchRuntimeRequest } from './runtimeExecutor.js';
import { normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
import {
ProxyInputFileResolutionError,
@@ -31,12 +31,14 @@ import {
buildOauthProviderHeaders,
} from '../../services/oauth/service.js';
import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js';
import { recordOauthQuotaResetHint } from '../../services/oauth/quota.js';
import { refreshOauthAccessTokenSingleflight } from '../../services/oauth/refreshSingleflight.js';
import { collectResponsesFinalPayloadFromSse } from './responsesSseFinal.js';
import {
createGeminiCliStreamReader,
unwrapGeminiCliPayload,
} from './geminiCliCompat.js';
import { ensureResponsesWebsocketTransport } from './responsesWebsocket.js';
const MAX_RETRIES = 2;
@@ -122,7 +124,28 @@ function carriesResponsesFileUrlInput(value: unknown): boolean {
type UsageSummary = ReturnType<typeof parseProxyUsage>;
function deriveCodexSessionCacheKey(input: {
body: Record<string, unknown>;
requestedModel: string;
proxyToken: string | null;
}): string | null {
const promptCacheKey = typeof input.body.prompt_cache_key === 'string'
? input.body.prompt_cache_key.trim()
: '';
if (promptCacheKey) {
return `${input.requestedModel}:responses:${promptCacheKey}`;
}
const proxyToken = typeof input.proxyToken === 'string' ? input.proxyToken.trim() : '';
if (proxyToken) {
return `${input.requestedModel}:proxy:${proxyToken}`;
}
return null;
}
export async function responsesProxyRoute(app: FastifyInstance) {
ensureResponsesWebsocketTransport(app);
const handleResponsesRequest = async (
request: FastifyRequest,
reply: FastifyReply,
@@ -248,6 +271,11 @@ export async function responsesProxyRoute(app: FastifyInstance) {
);
const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => {
const upstreamStream = isStream || (isCodexSite && endpoint === 'responses');
const codexSessionCacheKey = deriveCodexSessionCacheKey({
body: normalizedResponsesBody,
requestedModel,
proxyToken: getProxyAuthContext(request)?.token || null,
});
const endpointRequest = buildUpstreamEndpointRequest({
endpoint,
modelName,
@@ -262,6 +290,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
responsesOriginalBody: normalizedResponsesBody,
downstreamHeaders: request.headers as Record<string, unknown>,
providerHeaders: buildProviderHeaders(),
codexSessionCacheKey,
});
const upstreamPath = (
isCompactRequest && endpoint === 'responses'
@@ -273,19 +302,25 @@ export async function responsesProxyRoute(app: FastifyInstance) {
path: upstreamPath,
headers: endpointRequest.headers,
body: endpointRequest.body as Record<string, unknown>,
runtime: endpointRequest.runtime,
};
};
const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => (
dispatchRuntimeRequest({
siteUrl: selected.site.url,
targetUrl,
request: compatibilityRequest,
buildInit: (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body),
}),
})
);
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
isStream: isStream || isCodexSite,
requiresNativeResponsesFileUrl,
dispatchRequest: (compatibilityRequest, targetUrl) => fetch(
targetUrl ?? `${selected.site.url}${compatibilityRequest.path}`,
withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: compatibilityRequest.headers,
body: JSON.stringify(compatibilityRequest.body),
}),
),
dispatchRequest,
});
const tryRecover = async (ctx: Parameters<NonNullable<typeof endpointStrategy.tryRecover>>[0]) => {
if (ctx.response.status === 401 && oauth) {
@@ -299,14 +334,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
};
const refreshedRequest = buildEndpointRequest(ctx.request.endpoint);
const refreshedTargetUrl = `${selected.site.url}${refreshedRequest.path}`;
const refreshedResponse = await fetch(
refreshedTargetUrl,
withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: refreshedRequest.headers,
body: JSON.stringify(refreshedRequest.body),
}),
);
const refreshedResponse = await dispatchRequest(refreshedRequest, refreshedTargetUrl);
if (refreshedResponse.ok) {
return {
upstream: refreshedResponse,
@@ -331,6 +359,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
proxyUrl: resolveProxyUrlForSite(selected.site),
endpointCandidates,
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest,
tryRecover,
shouldDowngrade: endpointStrategy.shouldDowngrade,
onDowngrade: (ctx) => {
@@ -358,6 +387,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
if (!endpointResult.ok) {
const status = endpointResult.status || 502;
const errText = endpointResult.errText || 'unknown error';
const rawErrText = endpointResult.rawErrText || errText;
tokenRouter.recordFailure(selected.channel.id);
logProxy(
selected,
@@ -377,6 +407,11 @@ export async function responsesProxyRoute(app: FastifyInstance) {
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
);
await recordOauthQuotaResetHint({
accountId: selected.account.id,
statusCode: status,
errorText: rawErrText,
});
if (isTokenExpiredError({ status, message: errText })) {
await reportTokenExpired({
@@ -578,6 +613,13 @@ export async function responsesProxyRoute(app: FastifyInstance) {
app.post('/v1/responses', async (request: FastifyRequest, reply: FastifyReply) =>
handleResponsesRequest(request, reply, '/v1/responses'));
app.get('/v1/responses', async (_request: FastifyRequest, reply: FastifyReply) =>
reply.code(426).send({
error: {
message: 'WebSocket upgrade required for GET /v1/responses',
type: 'invalid_request_error',
},
}));
app.post('/v1/responses/compact', async (request: FastifyRequest, reply: FastifyReply) =>
handleResponsesRequest(request, reply, '/v1/responses/compact'));
}
@@ -0,0 +1,209 @@
import Fastify, { type FastifyInstance } from 'fastify';
import { AddressInfo } from 'node:net';
import WebSocket from 'ws';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
const fetchMock = vi.fn();
const selectChannelMock = vi.fn();
const selectNextChannelMock = vi.fn();
const recordSuccessMock = vi.fn();
const recordFailureMock = vi.fn();
const refreshModelsAndRebuildRoutesMock = vi.fn();
const reportProxyAllFailedMock = vi.fn();
const reportTokenExpiredMock = vi.fn();
const resolveProxyUsageWithSelfLogFallbackMock = vi.fn(async ({ usage }: any) => ({
...usage,
estimatedCostFromQuota: 0,
recoveredFromSelfLog: false,
}));
const dbInsertMock = vi.fn((_arg?: any) => ({
values: () => ({
run: () => undefined,
}),
}));
vi.mock('undici', () => ({
fetch: (...args: unknown[]) => fetchMock(...args),
}));
vi.mock('../../services/tokenRouter.js', () => ({
tokenRouter: {
selectChannel: (...args: unknown[]) => selectChannelMock(...args),
selectNextChannel: (...args: unknown[]) => selectNextChannelMock(...args),
recordSuccess: (...args: unknown[]) => recordSuccessMock(...args),
recordFailure: (...args: unknown[]) => recordFailureMock(...args),
},
}));
vi.mock('../../services/modelService.js', () => ({
refreshModelsAndRebuildRoutes: (...args: unknown[]) => refreshModelsAndRebuildRoutesMock(...args),
}));
vi.mock('../../services/alertService.js', () => ({
reportProxyAllFailed: (...args: unknown[]) => reportProxyAllFailedMock(...args),
reportTokenExpired: (...args: unknown[]) => reportTokenExpiredMock(...args),
}));
vi.mock('../../services/alertRules.js', () => ({
isTokenExpiredError: () => false,
}));
vi.mock('../../services/modelPricingService.js', () => ({
estimateProxyCost: async () => 0,
buildProxyBillingDetails: async () => null,
fetchModelPricingCatalog: async () => null,
}));
vi.mock('../../services/proxyRetryPolicy.js', () => ({
shouldRetryProxyRequest: () => false,
}));
vi.mock('../../services/proxyUsageFallbackService.js', () => ({
resolveProxyUsageWithSelfLogFallback: (arg: any) => resolveProxyUsageWithSelfLogFallbackMock(arg),
}));
vi.mock('../../services/oauth/quota.js', () => ({
recordOauthQuotaResetHint: async () => undefined,
}));
vi.mock('../../db/index.js', () => ({
db: {
insert: (arg: any) => dbInsertMock(arg),
},
hasProxyLogDownstreamApiKeyIdColumn: async () => false,
schema: {
proxyLogs: {},
},
}));
function createSseResponse(chunks: string[], status = 200) {
const encoder = new TextEncoder();
return new Response(new ReadableStream<Uint8Array>({
start(controller) {
for (const chunk of chunks) {
controller.enqueue(encoder.encode(chunk));
}
controller.close();
},
}), {
status,
headers: { 'content-type': 'text/event-stream; charset=utf-8' },
});
}
function waitForSocketOpen(socket: WebSocket) {
return new Promise<void>((resolve, reject) => {
socket.once('open', () => resolve());
socket.once('error', reject);
});
}
function waitForSocketMessages(socket: WebSocket, count: number) {
return new Promise<any[]>((resolve, reject) => {
const messages: any[] = [];
const onMessage = (payload: WebSocket.RawData) => {
messages.push(JSON.parse(String(payload)));
if (messages.length >= count) {
socket.off('message', onMessage);
socket.off('error', reject);
resolve(messages);
}
};
socket.on('message', onMessage);
socket.once('error', reject);
});
}
describe('responses websocket transport', () => {
let app: FastifyInstance;
let baseUrl: string;
beforeAll(async () => {
const { responsesProxyRoute } = await import('./responses.js');
app = Fastify();
await app.register(responsesProxyRoute);
await app.listen({ port: 0, host: '127.0.0.1' });
const address = app.server.address() as AddressInfo;
baseUrl = `ws://127.0.0.1:${address.port}`;
});
beforeEach(() => {
fetchMock.mockReset();
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
recordSuccessMock.mockReset();
recordFailureMock.mockReset();
refreshModelsAndRebuildRoutesMock.mockReset();
reportProxyAllFailedMock.mockReset();
reportTokenExpiredMock.mockReset();
resolveProxyUsageWithSelfLogFallbackMock.mockClear();
dbInsertMock.mockClear();
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
site: { name: 'codex-site', url: 'https://chatgpt.com/backend-api/codex', platform: 'codex' },
account: {
id: 33,
username: 'codex-user@example.com',
extraConfig: JSON.stringify({
credentialMode: 'session',
oauth: {
provider: 'codex',
accountId: 'chatgpt-account-123',
email: 'codex-user@example.com',
},
}),
},
tokenName: 'default',
tokenValue: 'oauth-access-token',
actualModel: 'gpt-5.4',
});
selectNextChannelMock.mockReturnValue(null);
});
afterAll(async () => {
await app.close();
});
it('accepts response.create over GET /v1/responses websocket and forwards streamed responses events', async () => {
fetchMock.mockResolvedValue(createSseResponse([
'event: response.created\n',
'data: {"type":"response.created","response":{"id":"resp_ws","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}\n\n',
'event: response.output_item.added\n',
'data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_ws","type":"message","role":"assistant","status":"in_progress","content":[]}}\n\n',
'event: response.output_text.delta\n',
'data: {"type":"response.output_text.delta","output_index":0,"item_id":"msg_ws","delta":"pong"}\n\n',
'event: response.completed\n',
'data: {"type":"response.completed","response":{"id":"resp_ws","model":"gpt-5.4","status":"completed","output":[{"id":"msg_ws","type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"pong"}]}],"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}\n\n',
'data: [DONE]\n\n',
]));
const socket = new WebSocket(`${baseUrl}/v1/responses`);
await waitForSocketOpen(socket);
const messagesPromise = waitForSocketMessages(socket, 4);
socket.send(JSON.stringify({
type: 'response.create',
model: 'gpt-5.4',
input: [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'hello websocket' }],
},
],
}));
const messages = await messagesPromise;
socket.close();
expect(messages.map((message) => message.type)).toEqual([
'response.created',
'response.output_item.added',
'response.output_text.delta',
'response.completed',
]);
expect(messages[3]?.response?.output?.[0]?.content?.[0]?.text).toBe('pong');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,349 @@
import { randomUUID } from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import type { IncomingMessage } from 'node:http';
import { WebSocketServer, type RawData, type WebSocket } from 'ws';
import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js';
const installedApps = new WeakSet<FastifyInstance>();
type NormalizedResponsesWebsocketRequest =
| {
ok: true;
request: Record<string, unknown>;
nextRequestSnapshot: Record<string, unknown>;
}
| {
ok: false;
status: number;
message: string;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function parseJsonObject(raw: RawData): Record<string, unknown> | null {
try {
const parsed = JSON.parse(String(raw));
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
function cloneJsonObject<T>(value: T): T {
return structuredClone(value);
}
function toResponseInputArray(value: unknown): unknown[] {
return Array.isArray(value) ? cloneJsonObject(value) : [];
}
function normalizeResponsesWebsocketRequest(
raw: RawData,
lastRequest: Record<string, unknown> | null,
lastResponseOutput: unknown[],
): NormalizedResponsesWebsocketRequest {
const parsed = parseJsonObject(raw);
if (!parsed) {
return {
ok: false,
status: 400,
message: 'Invalid websocket JSON payload',
};
}
const requestType = asTrimmedString(parsed.type);
if (requestType !== 'response.create' && requestType !== 'response.append') {
return {
ok: false,
status: 400,
message: `unsupported websocket request type: ${requestType || 'unknown'}`,
};
}
if (requestType === 'response.create' && parsed.generate === false) {
const next = cloneJsonObject(parsed);
delete next.type;
delete next.generate;
next.stream = true;
if (!Array.isArray(next.input)) next.input = [];
const modelName = asTrimmedString(next.model);
if (!modelName) {
return {
ok: false,
status: 400,
message: 'missing model in response.create request',
};
}
return {
ok: true,
request: next,
nextRequestSnapshot: cloneJsonObject(next),
};
}
if (!lastRequest) {
if (requestType !== 'response.create') {
return {
ok: false,
status: 400,
message: 'websocket request received before response.create',
};
}
const next = cloneJsonObject(parsed);
delete next.type;
next.stream = true;
if (!Array.isArray(next.input)) next.input = [];
const modelName = asTrimmedString(next.model);
if (!modelName) {
return {
ok: false,
status: 400,
message: 'missing model in response.create request',
};
}
return {
ok: true,
request: next,
nextRequestSnapshot: cloneJsonObject(next),
};
}
if (!Array.isArray(parsed.input)) {
return {
ok: false,
status: 400,
message: 'websocket request requires array field: input',
};
}
const mergedInput = [
...toResponseInputArray(lastRequest.input),
...cloneJsonObject(lastResponseOutput),
...cloneJsonObject(parsed.input),
];
const next = cloneJsonObject(parsed);
delete next.type;
delete next.previous_response_id;
next.input = mergedInput;
next.stream = true;
if (!('model' in next) && typeof lastRequest.model === 'string') {
next.model = lastRequest.model;
}
if (!('instructions' in next) && lastRequest.instructions !== undefined) {
next.instructions = cloneJsonObject(lastRequest.instructions);
}
return {
ok: true,
request: next,
nextRequestSnapshot: cloneJsonObject(next),
};
}
function writeResponsesWebsocketError(
socket: WebSocket,
status: number,
message: string,
errorPayload?: unknown,
) {
socket.send(JSON.stringify({
type: 'error',
status,
error: isRecord(errorPayload) && isRecord(errorPayload.error)
? errorPayload.error
: {
type: status >= 500 ? 'server_error' : 'invalid_request_error',
message,
},
}));
}
function synthesizePrewarmResponsePayloads(request: Record<string, unknown>) {
const responseId = `resp_prewarm_${randomUUID()}`;
const modelName = asTrimmedString(request.model) || 'unknown';
const createdAt = Math.floor(Date.now() / 1000);
return [
{
type: 'response.created',
response: {
id: responseId,
object: 'response',
created_at: createdAt,
status: 'in_progress',
model: modelName,
output: [],
},
},
{
type: 'response.completed',
response: {
id: responseId,
object: 'response',
created_at: createdAt,
status: 'completed',
model: modelName,
output: [],
usage: {
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
},
},
},
];
}
function collectResponsesOutput(payloads: unknown[]): unknown[] {
const outputByIndex = new Map<number, unknown>();
let completedOutput: unknown[] | null = null;
for (const payload of payloads) {
if (!isRecord(payload)) continue;
const type = asTrimmedString(payload.type);
if ((type === 'response.output_item.added' || type === 'response.output_item.done')
&& Number.isInteger(payload.output_index)
&& payload.item !== undefined) {
outputByIndex.set(Number(payload.output_index), cloneJsonObject(payload.item));
continue;
}
if (type === 'response.completed' && isRecord(payload.response) && Array.isArray(payload.response.output)) {
completedOutput = cloneJsonObject(payload.response.output);
}
}
if (completedOutput) return completedOutput;
return [...outputByIndex.entries()]
.sort((left, right) => left[0] - right[0])
.map(([, value]) => value);
}
function buildInjectHeaders(request: IncomingMessage): Record<string, string | string[]> {
const headers: Record<string, string | string[]> = {};
for (const [rawKey, rawValue] of Object.entries(request.headers)) {
const key = rawKey.toLowerCase();
if (!rawValue) continue;
if (
key === 'host'
|| key === 'connection'
|| key === 'upgrade'
|| key === 'sec-websocket-key'
|| key === 'sec-websocket-version'
|| key === 'sec-websocket-extensions'
|| key === 'sec-websocket-protocol'
) {
continue;
}
headers[rawKey] = rawValue as string | string[];
}
return headers;
}
async function handleResponsesWebsocketConnection(
app: FastifyInstance,
socket: WebSocket,
request: IncomingMessage,
) {
let lastRequest: Record<string, unknown> | null = null;
let lastResponseOutput: unknown[] = [];
socket.on('message', async (raw) => {
const parsed = parseJsonObject(raw);
if (!parsed) {
writeResponsesWebsocketError(socket, 400, 'Invalid websocket JSON payload');
return;
}
const normalized = normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput);
if (!normalized.ok) {
writeResponsesWebsocketError(socket, normalized.status, normalized.message);
return;
}
lastRequest = normalized.nextRequestSnapshot;
if (parsed.generate === false && asTrimmedString(parsed.type) === 'response.create') {
lastResponseOutput = [];
for (const payload of synthesizePrewarmResponsePayloads(normalized.request)) {
socket.send(JSON.stringify(payload));
}
return;
}
const response = await app.inject({
method: 'POST',
url: '/v1/responses',
headers: buildInjectHeaders(request),
payload: normalized.request,
});
if (response.statusCode < 200 || response.statusCode >= 300) {
let payload: unknown = null;
try {
payload = JSON.parse(response.body);
} catch {
payload = null;
}
writeResponsesWebsocketError(
socket,
response.statusCode,
response.statusMessage || 'Upstream error',
payload,
);
return;
}
const contentType = String(response.headers['content-type'] || '').toLowerCase();
if (!contentType.includes('text/event-stream')) {
try {
const payload = JSON.parse(response.body);
socket.send(JSON.stringify(payload));
lastResponseOutput = isRecord(payload?.response) && Array.isArray(payload.response.output)
? cloneJsonObject(payload.response.output)
: [];
} catch {
writeResponsesWebsocketError(socket, 502, 'Unexpected non-JSON websocket proxy response');
}
return;
}
const pulled = openAiResponsesTransformer.pullSseEvents(response.body);
const forwardedPayloads: unknown[] = [];
for (const event of pulled.events) {
if (event.data === '[DONE]') continue;
try {
const payload = JSON.parse(event.data);
forwardedPayloads.push(payload);
socket.send(JSON.stringify(payload));
} catch {
// Ignore malformed SSE frames; the HTTP route already normalizes them.
}
}
lastResponseOutput = collectResponsesOutput(forwardedPayloads);
});
}
export function ensureResponsesWebsocketTransport(app: FastifyInstance) {
if (installedApps.has(app)) return;
installedApps.add(app);
const websocketServer = new WebSocketServer({ noServer: true });
app.server.on('upgrade', (request, socket, head) => {
const url = new URL(request.url || '/', 'http://localhost');
if (url.pathname !== '/v1/responses') return;
websocketServer.handleUpgrade(request, socket, head, (client) => {
void handleResponsesWebsocketConnection(app, client, request);
});
});
app.addHook('onClose', async () => {
await new Promise<void>((resolve) => {
websocketServer.close(() => resolve());
});
});
}
@@ -0,0 +1,221 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fetch } from 'undici';
import type { BuiltEndpointRequest } from './endpointFlow.js';
vi.mock('undici', () => ({
fetch: vi.fn(),
}));
const fetchMock = vi.mocked(fetch);
describe('dispatchRuntimeRequest', () => {
beforeEach(() => {
fetchMock.mockReset();
});
it('routes antigravity runtime requests through daily then sandbox base urls and rewrites the payload fingerprint', async () => {
const { dispatchRuntimeRequest } = await import('./runtimeExecutor.js');
const request: BuiltEndpointRequest = {
endpoint: 'chat',
path: '/v1internal:generateContent',
headers: {
Authorization: 'Bearer antigravity-token',
'Content-Type': 'application/json',
'User-Agent': 'google-api-nodejs-client/9.15.1',
},
body: {
project: 'project-demo',
model: 'gemini-3-pro-preview',
request: {
contents: [
{
role: 'user',
parts: [{ text: 'hello runtime executor' }],
},
],
},
},
runtime: {
executor: 'antigravity',
modelName: 'gemini-3-pro-preview',
stream: false,
},
};
fetchMock
.mockResolvedValueOnce(new Response(JSON.stringify({
error: { message: 'try fallback base url' },
}), {
status: 429,
headers: { 'content-type': 'application/json' },
}) as unknown as Awaited<ReturnType<typeof fetch>>)
.mockResolvedValueOnce(new Response(JSON.stringify({
response: { responseId: 'ok' },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}) as unknown as Awaited<ReturnType<typeof fetch>>);
const response = await dispatchRuntimeRequest({
siteUrl: 'https://cloudcode-pa.googleapis.com',
request,
buildInit: (_url, nextRequest) => ({
method: 'POST',
headers: nextRequest.headers,
body: JSON.stringify(nextRequest.body),
}),
});
expect(response.ok).toBe(true);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent');
expect(fetchMock.mock.calls[1]?.[0]).toBe('https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent');
const firstInit = fetchMock.mock.calls[0]?.[1] as RequestInit;
expect(firstInit.headers).toMatchObject({
Authorization: 'Bearer antigravity-token',
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent': 'antigravity/1.19.6 darwin/arm64',
});
const upstreamBody = JSON.parse(String(firstInit.body));
expect(upstreamBody).toMatchObject({
project: 'project-demo',
model: 'gemini-3-pro-preview',
userAgent: 'antigravity',
requestType: 'agent',
request: {
sessionId: expect.any(String),
contents: [
{
role: 'user',
parts: [{ text: 'hello runtime executor' }],
},
],
},
});
expect(upstreamBody.requestId).toMatch(/^agent-[0-9a-f-]{36}$/i);
});
it('keeps gemini-cli countTokens payload lean while forcing a model-aware user agent', async () => {
const { dispatchRuntimeRequest } = await import('./runtimeExecutor.js');
const request: BuiltEndpointRequest = {
endpoint: 'chat',
path: '/v1internal:countTokens',
headers: {
Authorization: 'Bearer gemini-cli-token',
'Content-Type': 'application/json',
'User-Agent': 'GeminiCLI/0.31.0/unknown (win32; x64)',
'X-Goog-Api-Client': 'google-genai-sdk/1.41.0 gl-node/v22.19.0',
},
body: {
project: 'project-demo',
model: 'gemini-2.5-pro',
request: {
contents: [
{
role: 'user',
parts: [{ text: 'count these tokens' }],
},
],
},
},
runtime: {
executor: 'gemini-cli',
modelName: 'gemini-2.5-pro',
stream: false,
action: 'countTokens',
},
};
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
totalTokens: 12,
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}) as unknown as Awaited<ReturnType<typeof fetch>>);
const response = await dispatchRuntimeRequest({
siteUrl: 'https://cloudcode-pa.googleapis.com',
request,
buildInit: (_url, nextRequest) => ({
method: 'POST',
headers: nextRequest.headers,
body: JSON.stringify(nextRequest.body),
}),
});
expect(response.ok).toBe(true);
const requestInit = fetchMock.mock.calls[0]?.[1] as RequestInit;
expect(requestInit.headers).toMatchObject({
Authorization: 'Bearer gemini-cli-token',
'Content-Type': 'application/json',
'User-Agent': 'GeminiCLI/0.31.0/gemini-2.5-pro (win32; x64)',
'X-Goog-Api-Client': 'google-genai-sdk/1.41.0 gl-node/v22.19.0',
});
expect(JSON.parse(String(requestInit.body))).toEqual({
request: {
contents: [
{
role: 'user',
parts: [{ text: 'count these tokens' }],
},
],
},
});
});
it('retries antigravity runtime requests on transport errors before falling back to the next base url', async () => {
const { dispatchRuntimeRequest } = await import('./runtimeExecutor.js');
const request: BuiltEndpointRequest = {
endpoint: 'chat',
path: '/v1internal:generateContent',
headers: {
Authorization: 'Bearer antigravity-token',
'Content-Type': 'application/json',
},
body: {
project: 'project-demo',
model: 'gemini-3-pro-preview',
request: {
contents: [
{
role: 'user',
parts: [{ text: 'hello runtime executor' }],
},
],
},
},
runtime: {
executor: 'antigravity',
modelName: 'gemini-3-pro-preview',
stream: false,
},
};
fetchMock
.mockRejectedValueOnce(new Error('socket hang up'))
.mockResolvedValueOnce(new Response(JSON.stringify({
response: { responseId: 'ok' },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}) as unknown as Awaited<ReturnType<typeof fetch>>);
const response = await dispatchRuntimeRequest({
siteUrl: 'https://cloudcode-pa.googleapis.com',
request,
buildInit: (_url, nextRequest) => ({
method: 'POST',
headers: nextRequest.headers,
body: JSON.stringify(nextRequest.body),
}),
});
expect(response.ok).toBe(true);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent');
expect(fetchMock.mock.calls[1]?.[0]).toBe('https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent');
});
});
+346
View File
@@ -0,0 +1,346 @@
import { createHash, randomUUID } from 'node:crypto';
import { fetch, type RequestInit as UndiciRequestInit } from 'undici';
import type { BuiltEndpointRequest } from './endpointFlow.js';
import { buildUpstreamUrl } from './upstreamUrl.js';
const ANTIGRAVITY_RUNTIME_BASE_URLS = [
'https://daily-cloudcode-pa.googleapis.com',
'https://daily-cloudcode-pa.sandbox.googleapis.com',
] as const;
type RuntimeDispatchInput = {
siteUrl: string;
request: BuiltEndpointRequest;
targetUrl?: string;
buildInit: (requestUrl: string, request: BuiltEndpointRequest) => Promise<UndiciRequestInit> | UndiciRequestInit;
};
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function withRequestBody(
request: BuiltEndpointRequest,
body: Record<string, unknown>,
headers?: Record<string, string>,
): BuiltEndpointRequest {
return {
...request,
headers: headers ? { ...headers } : { ...request.headers },
body,
};
}
async function performFetch(
input: RuntimeDispatchInput,
request: BuiltEndpointRequest,
requestUrl = input.targetUrl || buildUpstreamUrl(input.siteUrl, request.path),
): Promise<Awaited<ReturnType<typeof fetch>>> {
const init = await input.buildInit(requestUrl, request);
return fetch(requestUrl, init);
}
async function materializeErrorResponse(
response: Awaited<ReturnType<typeof fetch>>,
): Promise<Awaited<ReturnType<typeof fetch>>> {
if (response.ok) return response;
const text = await response.text().catch(() => '');
return new Response(text, {
status: response.status,
headers: response.headers,
}) as unknown as Awaited<ReturnType<typeof fetch>>;
}
function parseGeminiCliPreviewFallbackOrder(modelName: string): string[] {
switch (modelName.trim()) {
case 'gemini-2.5-pro':
return [];
case 'gemini-2.5-flash':
return [];
case 'gemini-2.5-flash-lite':
return [];
default:
return [];
}
}
function replaceGeminiCliModelInUserAgent(userAgent: string | undefined, modelName: string): string | undefined {
const raw = asTrimmedString(userAgent);
if (!raw) return undefined;
return raw.replace(/^GeminiCLI\/([^/]+)\/[^ ]+ /i, `GeminiCLI/$1/${modelName} `);
}
function buildGeminiCliAttemptRequest(request: BuiltEndpointRequest, modelName: string): BuiltEndpointRequest {
const body = structuredClone(request.body);
const action = request.runtime?.action;
if (action === 'countTokens') {
delete body.model;
delete body.project;
} else {
body.model = modelName;
}
const headers = { ...request.headers };
const nextUserAgent = replaceGeminiCliModelInUserAgent(
headers['User-Agent'] || headers['user-agent'],
modelName,
);
if (nextUserAgent) {
headers['User-Agent'] = nextUserAgent;
delete headers['user-agent'];
}
return withRequestBody(request, body, headers);
}
function antigravityRequestType(modelName: string): 'image_gen' | 'agent' {
return modelName.includes('image') ? 'image_gen' : 'agent';
}
function generateAntigravityProjectId(): string {
const adjectives = ['useful', 'bright', 'swift', 'calm', 'bold'];
const nouns = ['signal', 'river', 'rocket', 'forest', 'bridge'];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)] || 'useful';
const noun = nouns[Math.floor(Math.random() * nouns.length)] || 'signal';
const suffix = Math.floor(100000 + Math.random() * 900000);
return `${adjective}-${noun}-${suffix}`;
}
function extractFirstUserText(value: unknown): string {
if (!Array.isArray(value)) return '';
for (const content of value) {
if (!content || typeof content !== 'object' || Array.isArray(content)) continue;
const record = content as Record<string, unknown>;
if (asTrimmedString(record.role) !== 'user') continue;
const parts = Array.isArray(record.parts) ? record.parts : [];
for (const part of parts) {
if (!part || typeof part !== 'object' || Array.isArray(part)) continue;
const text = asTrimmedString((part as Record<string, unknown>).text);
if (text) return text;
}
}
return '';
}
function generateStableAntigravitySessionId(payload: Record<string, unknown>): string {
const firstUserText = extractFirstUserText(
payload.request && typeof payload.request === 'object' && !Array.isArray(payload.request)
? (payload.request as Record<string, unknown>).contents
: undefined,
);
if (!firstUserText) {
return `-${BigInt(`0x${randomUUID().replace(/-/g, '').slice(0, 16)}`).toString()}`;
}
const digest = createHash('sha256').update(firstUserText).digest('hex').slice(0, 16);
const bigint = BigInt(`0x${digest}`) & BigInt('0x7fffffffffffffff');
return `-${bigint.toString()}`;
}
function renameParametersJsonSchema(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map((item) => renameParametersJsonSchema(item));
}
if (!value || typeof value !== 'object') return value;
const input = value as Record<string, unknown>;
const output: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(input)) {
const nextKey = key === 'parametersJsonSchema' ? 'parameters' : key;
output[nextKey] = renameParametersJsonSchema(entry);
}
return output;
}
function deleteNestedMaxOutputTokens(payload: Record<string, unknown>): void {
const request = payload.request;
if (!request || typeof request !== 'object' || Array.isArray(request)) return;
const generationConfig = (request as Record<string, unknown>).generationConfig;
if (!generationConfig || typeof generationConfig !== 'object' || Array.isArray(generationConfig)) return;
delete (generationConfig as Record<string, unknown>).maxOutputTokens;
}
function buildAntigravityRuntimeBody(
originalBody: Record<string, unknown>,
modelName: string,
action?: NonNullable<BuiltEndpointRequest['runtime']>['action'],
): Record<string, unknown> {
const payload = renameParametersJsonSchema(structuredClone(originalBody)) as Record<string, unknown>;
if (action === 'countTokens') {
return payload;
}
const requestType = antigravityRequestType(modelName);
const projectId = asTrimmedString(payload.project) || generateAntigravityProjectId();
payload.model = modelName;
payload.project = projectId;
payload.userAgent = 'antigravity';
payload.requestType = requestType;
payload.requestId = requestType === 'image_gen'
? `image_gen/${Date.now()}/${randomUUID()}/12`
: `agent-${randomUUID()}`;
const request = payload.request;
if (request && typeof request === 'object' && !Array.isArray(request)) {
delete (request as Record<string, unknown>).safetySettings;
if (requestType !== 'image_gen') {
(request as Record<string, unknown>).sessionId = generateStableAntigravitySessionId(payload);
}
if (modelName.includes('claude')) {
const toolConfig = (
(request as Record<string, unknown>).toolConfig
&& typeof (request as Record<string, unknown>).toolConfig === 'object'
&& !Array.isArray((request as Record<string, unknown>).toolConfig)
)
? (request as Record<string, unknown>).toolConfig as Record<string, unknown>
: (((request as Record<string, unknown>).toolConfig = {}) as Record<string, unknown>);
toolConfig.functionCallingConfig = { mode: 'VALIDATED' };
} else {
deleteNestedMaxOutputTokens(payload);
}
}
return payload;
}
function antigravityShouldRetryNoCapacity(
status: number,
responseText: string,
): boolean {
if (status !== 503) return false;
return responseText.toLowerCase().includes('no capacity available');
}
function antigravityNoCapacityRetryDelay(attempt: number): number {
const delay = Math.min((attempt + 1) * 250, 2000);
return delay;
}
async function waitMs(ms: number): Promise<void> {
if (ms <= 0) return;
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function dispatchGeminiCliRuntimeRequest(
input: RuntimeDispatchInput,
): Promise<Awaited<ReturnType<typeof fetch>>> {
const baseModel = asTrimmedString(input.request.runtime?.modelName) || asTrimmedString(input.request.body.model);
const models = [baseModel, ...parseGeminiCliPreviewFallbackOrder(baseModel)].filter(Boolean);
let lastResponse: Awaited<ReturnType<typeof fetch>> | null = null;
for (const modelName of models.length > 0 ? models : [baseModel || 'unknown']) {
const attemptRequest = buildGeminiCliAttemptRequest(input.request, modelName);
const response = await performFetch(input, attemptRequest);
if (response.ok) return response;
if (response.status === 429) {
lastResponse = await materializeErrorResponse(response);
continue;
}
return materializeErrorResponse(response);
}
return lastResponse || performFetch(input, input.request);
}
async function dispatchCodexRuntimeRequest(
input: RuntimeDispatchInput,
): Promise<Awaited<ReturnType<typeof fetch>>> {
return performFetch(input, input.request);
}
async function dispatchClaudeRuntimeRequest(
input: RuntimeDispatchInput,
): Promise<Awaited<ReturnType<typeof fetch>>> {
return performFetch(input, input.request);
}
async function dispatchAntigravityRuntimeRequest(
input: RuntimeDispatchInput,
): Promise<Awaited<ReturnType<typeof fetch>>> {
const modelName = asTrimmedString(input.request.runtime?.modelName) || asTrimmedString(input.request.body.model);
const runtimeBody = buildAntigravityRuntimeBody(
input.request.body,
modelName,
input.request.runtime?.action,
);
const baseAttempts = 3;
let lastResponse: Awaited<ReturnType<typeof fetch>> | null = null;
attemptLoop:
for (let attempt = 0; attempt < baseAttempts; attempt += 1) {
for (const baseUrl of ANTIGRAVITY_RUNTIME_BASE_URLS) {
const requestUrl = `${baseUrl}${input.request.path}`;
const minimalHeaders: Record<string, string> = {
Authorization: input.request.headers.Authorization || input.request.headers.authorization || '',
'Content-Type': 'application/json',
Accept: input.request.runtime?.stream ? 'text/event-stream' : 'application/json',
'User-Agent': 'antigravity/1.19.6 darwin/arm64',
};
let response: Awaited<ReturnType<typeof fetch>>;
try {
response = await performFetch(
input,
withRequestBody(input.request, runtimeBody, minimalHeaders),
requestUrl,
);
} catch (error) {
if (baseUrl !== ANTIGRAVITY_RUNTIME_BASE_URLS[ANTIGRAVITY_RUNTIME_BASE_URLS.length - 1]) {
continue;
}
if (attempt + 1 < baseAttempts) {
continue attemptLoop;
}
throw error;
}
if (response.ok) return response;
const errorResponse = await materializeErrorResponse(response);
const errorText = await errorResponse.text().catch(() => '');
lastResponse = new Response(errorText, {
status: errorResponse.status,
headers: errorResponse.headers,
}) as unknown as Awaited<ReturnType<typeof fetch>>;
if (errorResponse.status === 429) {
continue;
}
if (antigravityShouldRetryNoCapacity(errorResponse.status, errorText)) {
if (baseUrl !== ANTIGRAVITY_RUNTIME_BASE_URLS[ANTIGRAVITY_RUNTIME_BASE_URLS.length - 1]) {
continue;
}
if (attempt + 1 < baseAttempts) {
await waitMs(antigravityNoCapacityRetryDelay(attempt));
continue attemptLoop;
}
}
return lastResponse;
}
}
return lastResponse || performFetch(input, withRequestBody(input.request, runtimeBody, {
Authorization: input.request.headers.Authorization || input.request.headers.authorization || '',
'Content-Type': 'application/json',
Accept: input.request.runtime?.stream ? 'text/event-stream' : 'application/json',
'User-Agent': 'antigravity/1.19.6 darwin/arm64',
}));
}
export async function dispatchRuntimeRequest(
input: RuntimeDispatchInput,
): Promise<Awaited<ReturnType<typeof fetch>>> {
const executor = input.request.runtime?.executor || 'default';
if (executor === 'codex') {
return dispatchCodexRuntimeRequest(input);
}
if (executor === 'claude') {
return dispatchClaudeRuntimeRequest(input);
}
if (executor === 'gemini-cli') {
return dispatchGeminiCliRuntimeRequest(input);
}
if (executor === 'antigravity') {
return dispatchAntigravityRuntimeRequest(input);
}
return performFetch(input, input.request);
}
@@ -356,7 +356,14 @@ describe('buildUpstreamEndpointRequest', () => {
expect(request.headers.Authorization).toBe('Bearer oauth-access-token');
expect(request.headers.Originator).toBe('codex_cli_rs');
expect(request.headers['Chatgpt-Account-Id']).toBe('chatgpt-account-123');
expect(request.headers.Version).toBe('0.101.0');
expect(request.headers.Session_id).toMatch(/^[0-9a-f-]{36}$/i);
expect(request.headers.Conversation_id).toBe(request.headers.Session_id);
expect(request.headers['User-Agent']).toBe('codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464');
expect(request.headers.Accept).toBe('text/event-stream');
expect(request.headers.Connection).toBe('Keep-Alive');
expect(request.body.instructions).toBe('');
expect(request.body.prompt_cache_key).toBe(request.headers.Session_id);
expect(request.body.stream).toBe(true);
expect(request.body.store).toBe(false);
expect(request.body.parallel_tool_calls).toBe(true);
@@ -368,6 +375,48 @@ describe('buildUpstreamEndpointRequest', () => {
expect(request.body.service_tier).toBeUndefined();
});
it('reuses a stable codex session id when the same downstream continuity key is provided', () => {
const firstRequest = buildUpstreamEndpointRequest({
endpoint: 'responses',
modelName: 'gpt-5.4',
stream: false,
tokenValue: 'oauth-access-token',
sitePlatform: 'codex',
siteUrl: 'https://chatgpt.com/backend-api/codex',
openaiBody: {
model: 'gpt-5.4',
messages: [{ role: 'user', content: 'hello codex' }],
},
downstreamFormat: 'openai',
providerHeaders: {
Originator: 'codex_cli_rs',
},
codexSessionCacheKey: 'gpt-5.4:user-123',
} as any);
const secondRequest = buildUpstreamEndpointRequest({
endpoint: 'responses',
modelName: 'gpt-5.4',
stream: false,
tokenValue: 'oauth-access-token',
sitePlatform: 'codex',
siteUrl: 'https://chatgpt.com/backend-api/codex',
openaiBody: {
model: 'gpt-5.4',
messages: [{ role: 'user', content: 'hello again codex' }],
},
downstreamFormat: 'openai',
providerHeaders: {
Originator: 'codex_cli_rs',
},
codexSessionCacheKey: 'gpt-5.4:user-123',
} as any);
expect(firstRequest.headers.Session_id).toBe(secondRequest.headers.Session_id);
expect(firstRequest.headers.Conversation_id).toBe(secondRequest.headers.Conversation_id);
expect(firstRequest.body.prompt_cache_key).toBe(secondRequest.body.prompt_cache_key);
});
it('builds gemini-cli native requests with project envelope and bearer headers', () => {
const request = buildUpstreamEndpointRequest({
endpoint: 'responses',
@@ -395,7 +444,7 @@ describe('buildUpstreamEndpointRequest', () => {
expect(request.path).toBe('/v1internal:streamGenerateContent?alt=sse');
expect(request.headers.Authorization).toBe('Bearer oauth-access-token');
expect(request.headers['User-Agent']).toContain('GeminiCLI/');
expect(request.headers['User-Agent']).toBe('GeminiCLI/0.31.0/gemini-2.5-pro (win32; x64)');
expect(request.headers['X-Goog-Api-Client']).toContain('google-genai-sdk/');
expect(request.body.project).toBe('project-demo');
expect(request.body.model).toBe('gemini-2.5-pro');
@@ -441,8 +490,9 @@ describe('buildUpstreamEndpointRequest', () => {
expect(request.path).toBe('/v1internal:generateContent');
expect(request.headers.Authorization).toBe('Bearer oauth-access-token');
expect(request.headers['User-Agent']).toContain('google-api-nodejs-client');
expect(request.headers['X-Goog-Api-Client']).toContain('google-cloud-sdk');
expect(request.headers['User-Agent']).toBe('antigravity/1.19.6 darwin/arm64');
expect(request.headers['X-Goog-Api-Client']).toBeUndefined();
expect(request.headers['Client-Metadata']).toBeUndefined();
expect(request.body).toEqual({
project: 'project-demo',
model: 'gemini-3-pro-preview',
@@ -461,7 +511,43 @@ describe('buildUpstreamEndpointRequest', () => {
});
});
it('uses bearer auth for claude oauth upstream requests', () => {
it('uses claude-code runtime headers for claude oauth upstream requests', () => {
const request = buildUpstreamEndpointRequest({
endpoint: 'messages',
modelName: 'claude-opus-4-6',
stream: true,
tokenValue: 'oauth-access-token',
oauthProvider: 'claude',
sitePlatform: 'claude',
siteUrl: 'https://api.anthropic.com',
openaiBody: {
model: 'claude-opus-4-6',
messages: [{ role: 'user', content: 'hello claude oauth' }],
},
downstreamFormat: 'openai',
});
expect(request.path).toBe('/v1/messages');
expect(request.headers.Authorization).toBe('Bearer oauth-access-token');
expect(request.headers['x-api-key']).toBeUndefined();
expect(request.headers['anthropic-version']).toBe('2023-06-01');
expect(request.headers['Anthropic-Dangerous-Direct-Browser-Access']).toBe('true');
expect(request.headers['X-App']).toBe('cli');
expect(request.headers['X-Stainless-Retry-Count']).toBe('0');
expect(request.headers['X-Stainless-Runtime-Version']).toBe('v24.3.0');
expect(request.headers['X-Stainless-Package-Version']).toBe('0.74.0');
expect(request.headers['X-Stainless-Runtime']).toBe('node');
expect(request.headers['X-Stainless-Lang']).toBe('js');
expect(request.headers['X-Stainless-Arch']).toBe('x64');
expect(request.headers['X-Stainless-Os']).toBe('Windows');
expect(request.headers['X-Stainless-Timeout']).toBe('600');
expect(request.headers['User-Agent']).toBe('claude-cli/2.1.63 (external, cli)');
expect(request.headers.Connection).toBe('keep-alive');
expect(request.headers.Accept).toBe('text/event-stream');
expect(request.headers['Accept-Encoding']).toBe('identity');
});
it('uses claude-code beta headers and compressed non-stream responses for claude upstream requests', () => {
const request = buildUpstreamEndpointRequest({
endpoint: 'messages',
modelName: 'claude-opus-4-6',
@@ -477,9 +563,11 @@ describe('buildUpstreamEndpointRequest', () => {
downstreamFormat: 'openai',
});
expect(request.path).toBe('/v1/messages');
expect(request.headers.Authorization).toBe('Bearer oauth-access-token');
expect(request.headers['x-api-key']).toBeUndefined();
expect(request.headers['anthropic-beta']).toContain('claude-code-20250219');
expect(request.headers['anthropic-beta']).toContain('oauth-2025-04-20');
expect(request.headers['anthropic-beta']).toContain('context-management-2025-06-27');
expect(request.headers.Accept).toBe('application/json');
expect(request.headers['Accept-Encoding']).toBe('gzip, deflate, br, zstd');
});
it('converts system roles to developer in native codex responses bodies', () => {
+348 -14
View File
@@ -1,3 +1,4 @@
import { createHash, randomUUID } from 'node:crypto';
import { fetchModelPricingCatalog } from '../../services/modelPricingService.js';
import type { DownstreamFormat } from '../../transformers/shared/normalized.js';
import {
@@ -98,6 +99,12 @@ const BLOCKED_PASSTHROUGH_HEADERS = new Set([
'sec-websocket-extensions',
]);
const CODEX_CLIENT_VERSION = '0.101.0';
const CODEX_DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
const ANTIGRAVITY_RUNTIME_USER_AGENT = 'antigravity/1.19.6 darwin/arm64';
const CLAUDE_DEFAULT_USER_AGENT = 'claude-cli/2.1.63 (external, cli)';
const CLAUDE_DEFAULT_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
function shouldSkipPassthroughHeader(key: string): boolean {
return HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key);
}
@@ -168,6 +175,221 @@ function extractResponsesPassthroughHeaders(
return forwarded;
}
function getInputHeader(
headers: Record<string, unknown> | Record<string, string> | undefined,
key: string,
): string | null {
if (!headers) return null;
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
return headerValueToString(candidateValue);
}
return null;
}
function parseGeminiCliUserAgentRuntime(userAgent: string | null): {
version: string;
platform: string;
arch: string;
} | null {
if (!userAgent) return null;
const match = /^GeminiCLI\/([^/]+)\/[^ ]+ \(([^;]+); ([^)]+)\)$/i.exec(userAgent.trim());
if (!match) return null;
return {
version: match[1] || '0.31.0',
platform: match[2] || 'win32',
arch: match[3] || 'x64',
};
}
function buildGeminiCLIUserAgent(modelName: string, existingUserAgent?: string | null): string {
const parsed = parseGeminiCliUserAgentRuntime(existingUserAgent ?? null);
const version = parsed?.version || '0.31.0';
const platform = parsed?.platform || 'win32';
const arch = parsed?.arch || 'x64';
const effectiveModel = asTrimmedString(modelName) || 'unknown';
return `GeminiCLI/${version}/${effectiveModel} (${platform}; ${arch})`;
}
function uuidFromSeed(seed: string): string {
const hash = createHash('sha1').update(seed).digest();
const bytes = new Uint8Array(hash.subarray(0, 16));
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join('-');
}
function mergeClaudeBetaHeader(
explicitValue: string | null,
extraBetas: string[] = [],
): string {
const source = explicitValue || CLAUDE_DEFAULT_BETA_HEADER;
const seen = new Set<string>();
const merged: string[] = [];
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(',');
}
function extractClaudeBetasFromBody(body: Record<string, unknown>): {
body: Record<string, unknown>;
betas: string[];
} {
const next = { ...body };
const rawBetas = next.betas;
delete next.betas;
if (typeof rawBetas === 'string') {
return {
body: next,
betas: rawBetas.split(',').map((entry) => entry.trim()).filter(Boolean),
};
}
if (Array.isArray(rawBetas)) {
return {
body: next,
betas: rawBetas
.map((entry) => asTrimmedString(entry))
.filter(Boolean),
};
}
return {
body: next,
betas: [],
};
}
function buildCodexRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
continuityKey?: string | null;
}): Record<string, string> {
const originator = getInputHeader(input.providerHeaders, 'originator') || 'codex_cli_rs';
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
const sessionId = (
getInputHeader(input.baseHeaders, 'session_id')
|| getInputHeader(input.baseHeaders, 'session-id')
|| (input.continuityKey ? uuidFromSeed(`metapi:codex:${input.continuityKey}`) : null)
|| randomUUID()
);
return {
...input.baseHeaders,
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
Originator: originator,
Version: CODEX_CLIENT_VERSION,
Session_id: sessionId,
Conversation_id: sessionId,
'User-Agent': CODEX_DEFAULT_USER_AGENT,
Accept: 'text/event-stream',
Connection: 'Keep-Alive',
};
}
function buildGeminiCliRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
modelName: string;
stream: boolean;
}): Record<string, string> {
const apiClient = (
getInputHeader(input.providerHeaders, 'x-goog-api-client')
|| getInputHeader(input.baseHeaders, 'x-goog-api-client')
);
const userAgent = buildGeminiCLIUserAgent(
input.modelName,
getInputHeader(input.providerHeaders, 'user-agent') || getInputHeader(input.baseHeaders, 'user-agent'),
);
const headers: Record<string, string> = {
Authorization: input.baseHeaders.Authorization,
'Content-Type': 'application/json',
'User-Agent': userAgent,
};
if (apiClient) {
headers['X-Goog-Api-Client'] = apiClient;
}
if (input.stream) {
headers.Accept = 'text/event-stream';
}
return headers;
}
function buildAntigravityRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
stream: boolean;
}): Record<string, string> {
const headers: Record<string, string> = {
Authorization: input.baseHeaders.Authorization,
'Content-Type': 'application/json',
Accept: input.stream ? 'text/event-stream' : 'application/json',
'User-Agent': ANTIGRAVITY_RUNTIME_USER_AGENT,
};
return headers;
}
function buildClaudeRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
claudeHeaders: Record<string, string>;
anthropicVersion: string;
stream: boolean;
isClaudeOauthUpstream: boolean;
tokenValue: string;
extraBetas?: string[];
}): Record<string, string> {
const anthropicBeta = mergeClaudeBetaHeader(
getInputHeader(input.claudeHeaders, 'anthropic-beta'),
input.extraBetas,
);
const headers: Record<string, string> = {
...input.baseHeaders,
...input.claudeHeaders,
'anthropic-version': input.anthropicVersion,
...(anthropicBeta ? { 'anthropic-beta': anthropicBeta } : {}),
'Anthropic-Dangerous-Direct-Browser-Access': 'true',
'X-App': 'cli',
'X-Stainless-Retry-Count': getInputHeader(input.claudeHeaders, 'x-stainless-retry-count') || '0',
'X-Stainless-Runtime-Version': getInputHeader(input.claudeHeaders, 'x-stainless-runtime-version') || 'v24.3.0',
'X-Stainless-Package-Version': getInputHeader(input.claudeHeaders, 'x-stainless-package-version') || '0.74.0',
'X-Stainless-Runtime': getInputHeader(input.claudeHeaders, 'x-stainless-runtime') || 'node',
'X-Stainless-Lang': getInputHeader(input.claudeHeaders, 'x-stainless-lang') || 'js',
'X-Stainless-Arch': getInputHeader(input.claudeHeaders, 'x-stainless-arch') || 'x64',
'X-Stainless-Os': getInputHeader(input.claudeHeaders, 'x-stainless-os') || 'Windows',
'X-Stainless-Timeout': getInputHeader(input.claudeHeaders, 'x-stainless-timeout') || '600',
'User-Agent': getInputHeader(input.claudeHeaders, 'user-agent') || CLAUDE_DEFAULT_USER_AGENT,
Connection: 'keep-alive',
Accept: input.stream ? 'text/event-stream' : 'application/json',
'Accept-Encoding': input.stream ? 'identity' : 'gzip, deflate, br, zstd',
};
if (input.isClaudeOauthUpstream) {
headers.Authorization = `Bearer ${input.tokenValue}`;
} else {
headers['x-api-key'] = input.tokenValue;
}
return headers;
}
function ensureStreamAcceptHeader(
headers: Record<string, string>,
stream: boolean,
@@ -530,10 +752,18 @@ export function buildUpstreamEndpointRequest(input: {
responsesOriginalBody?: Record<string, unknown>;
downstreamHeaders?: Record<string, unknown>;
providerHeaders?: Record<string, string>;
codexSessionCacheKey?: string | null;
}): {
path: string;
headers: Record<string, string>;
body: Record<string, unknown>;
runtime?: {
executor: 'default' | 'codex' | 'gemini-cli' | 'antigravity' | 'claude';
modelName?: string;
stream?: boolean;
oauthProjectId?: string | null;
action?: 'generateContent' | 'streamGenerateContent' | 'countTokens';
};
} {
const sitePlatform = normalizePlatformName(input.sitePlatform);
const isClaudeUpstream = sitePlatform === 'claude';
@@ -614,6 +844,22 @@ export function buildUpstreamEndpointRequest(input: {
};
const openaiBody = stripGeminiUnsupportedFields(input.openaiBody);
const runtime = {
executor: (
sitePlatform === 'codex'
? 'codex'
: sitePlatform === 'gemini-cli'
? 'gemini-cli'
: sitePlatform === 'antigravity'
? 'antigravity'
: sitePlatform === 'claude'
? 'claude'
: 'default'
) as 'default' | 'codex' | 'gemini-cli' | 'antigravity' | 'claude',
modelName: input.modelName,
stream: input.stream,
oauthProjectId: asTrimmedString(input.oauthProjectId) || null,
};
if (isInternalGeminiUpstream) {
const projectId = asTrimmedString(input.oauthProjectId);
@@ -631,7 +877,17 @@ export function buildUpstreamEndpointRequest(input: {
modelName: input.modelName,
instructions,
});
const headers = ensureStreamAcceptHeader(commonHeaders, input.stream);
const headers = isGeminiCliUpstream
? buildGeminiCliRuntimeHeaders({
baseHeaders: commonHeaders,
providerHeaders: input.providerHeaders,
modelName: input.modelName,
stream: input.stream,
})
: buildAntigravityRuntimeHeaders({
baseHeaders: commonHeaders,
stream: input.stream,
});
return {
path: resolveEndpointPath(input.endpoint),
headers,
@@ -640,6 +896,7 @@ export function buildUpstreamEndpointRequest(input: {
projectId: projectId || '',
request: geminiRequest,
}) as Record<string, unknown>,
runtime,
};
}
@@ -680,19 +937,20 @@ export function buildUpstreamEndpointRequest(input: {
convertOpenAiBodyToAnthropicMessagesBody(openaiBody, input.modelName, input.stream),
);
const headers = ensureStreamAcceptHeader({
...commonHeaders,
...claudeHeaders,
'anthropic-version': anthropicVersion,
...(isClaudeOauthUpstream
? { Authorization: `Bearer ${input.tokenValue}` }
: { 'x-api-key': input.tokenValue }),
}, input.stream);
const headers = buildClaudeRuntimeHeaders({
baseHeaders: commonHeaders,
claudeHeaders,
anthropicVersion,
stream: input.stream,
isClaudeOauthUpstream,
tokenValue: input.tokenValue,
});
return {
path: resolveEndpointPath('messages'),
headers,
body: sanitizedBody,
runtime,
};
}
@@ -720,15 +978,34 @@ export function buildUpstreamEndpointRequest(input: {
sitePlatform,
);
const headers = ensureStreamAcceptHeader({
...commonHeaders,
...responsesHeaders,
}, input.stream);
const headers = sitePlatform === 'codex'
? buildCodexRuntimeHeaders({
baseHeaders: {
...commonHeaders,
...responsesHeaders,
},
providerHeaders: input.providerHeaders,
continuityKey: asTrimmedString(input.codexSessionCacheKey) || null,
})
: ensureStreamAcceptHeader({
...commonHeaders,
...responsesHeaders,
}, input.stream);
const codexSessionId = sitePlatform === 'codex'
? (getInputHeader(headers, 'session_id') || getInputHeader(headers, 'session-id'))
: null;
const runtimeBody = sitePlatform === 'codex' && codexSessionId
? {
...body,
prompt_cache_key: codexSessionId,
}
: body;
return {
path: resolveEndpointPath('responses'),
headers,
body,
body: runtimeBody,
runtime,
};
}
@@ -741,6 +1018,63 @@ export function buildUpstreamEndpointRequest(input: {
model: input.modelName,
stream: input.stream,
},
runtime,
};
}
export function buildClaudeCountTokensUpstreamRequest(input: {
modelName: string;
tokenValue: string;
oauthProvider?: string;
sitePlatform?: string;
claudeBody: Record<string, unknown>;
downstreamHeaders?: Record<string, unknown>;
}): {
path: string;
headers: Record<string, string>;
body: Record<string, unknown>;
runtime: {
executor: 'claude';
modelName: string;
stream: false;
action: 'countTokens';
};
} {
const sitePlatform = normalizePlatformName(input.sitePlatform);
const isClaudeOauthUpstream = sitePlatform === 'claude' && input.oauthProvider === 'claude';
const claudeHeaders = extractClaudePassthroughHeaders(input.downstreamHeaders);
const anthropicVersion = claudeHeaders['anthropic-version'] || '2023-06-01';
const { body: bodyWithoutBetas, betas } = extractClaudeBetasFromBody({
...input.claudeBody,
model: input.modelName,
});
const sanitizedBody = sanitizeAnthropicMessagesBody(bodyWithoutBetas);
delete sanitizedBody.max_tokens;
delete sanitizedBody.maxTokens;
delete sanitizedBody.stream;
const headers = buildClaudeRuntimeHeaders({
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders,
anthropicVersion,
stream: false,
isClaudeOauthUpstream,
tokenValue: input.tokenValue,
extraBetas: betas,
});
return {
path: '/v1/messages/count_tokens?beta=true',
headers,
body: sanitizedBody,
runtime: {
executor: 'claude',
modelName: input.modelName,
stream: false,
action: 'countTokens',
},
};
}
+106
View File
@@ -0,0 +1,106 @@
import { describe, expect, it } from 'vitest';
import {
buildQuotaSnapshotFromOauthInfo,
parseCodexQuotaResetHint,
} from './quota.js';
function buildJwt(payload: Record<string, unknown>) {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value))
.toString('base64url');
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.signature`;
}
describe('oauth quota snapshot helpers', () => {
it('builds a codex quota snapshot from stored id_token claims and reset hint', () => {
const snapshot = buildQuotaSnapshotFromOauthInfo({
provider: 'codex',
planType: 'plus',
idToken: buildJwt({
email: 'codex-user@example.com',
'https://api.openai.com/auth': {
chatgpt_account_id: 'chatgpt-account-123',
chatgpt_plan_type: 'plus',
chatgpt_subscription_active_start: '2026-03-01T00:00:00.000Z',
chatgpt_subscription_active_until: '2026-04-01T00:00:00.000Z',
},
}),
quota: {
status: 'supported',
source: 'reverse_engineered',
lastSyncAt: '2026-03-18T01:00:00.000Z',
lastLimitResetAt: '2026-03-18T05:00:00.000Z',
providerMessage: 'current codex oauth signals do not expose stable 5h/7d remaining values',
windows: {
fiveHour: {
supported: false,
message: 'official 5h quota window is not exposed by current codex oauth artifacts',
},
sevenDay: {
supported: false,
message: 'official 7d quota window is not exposed by current codex oauth artifacts',
},
},
},
});
expect(snapshot).toEqual({
status: 'supported',
source: 'reverse_engineered',
lastSyncAt: '2026-03-18T01:00:00.000Z',
lastLimitResetAt: '2026-03-18T05:00:00.000Z',
providerMessage: 'current codex oauth signals do not expose stable 5h/7d remaining values',
subscription: {
planType: 'plus',
activeStart: '2026-03-01T00:00:00.000Z',
activeUntil: '2026-04-01T00:00:00.000Z',
},
windows: {
fiveHour: {
supported: false,
message: 'official 5h quota window is not exposed by current codex oauth artifacts',
},
sevenDay: {
supported: false,
message: 'official 7d quota window is not exposed by current codex oauth artifacts',
},
},
});
});
it('returns unsupported snapshots for non-codex providers', () => {
const snapshot = buildQuotaSnapshotFromOauthInfo({
provider: 'antigravity',
planType: 'pro',
});
expect(snapshot).toEqual({
status: 'unsupported',
source: 'official',
providerMessage: 'current local CLIProxyAPI evidence does not expose official quota windows for antigravity oauth',
windows: {
fiveHour: {
supported: false,
message: 'official 5h quota window is unavailable for this provider',
},
sevenDay: {
supported: false,
message: 'official 7d quota window is unavailable for this provider',
},
},
});
});
it('parses codex usage_limit_reached reset hints', () => {
const reset = parseCodexQuotaResetHint(429, JSON.stringify({
error: {
type: 'usage_limit_reached',
resets_at: 1773800400,
},
}));
expect(reset).toEqual({
resetAt: '2026-03-18T02:20:00.000Z',
message: 'codex usage_limit_reached reset hint observed from upstream',
});
});
});
+259
View File
@@ -0,0 +1,259 @@
import { eq } from 'drizzle-orm';
import { db, schema } from '../../db/index.js';
import { mergeAccountExtraConfig } from '../accountExtraConfig.js';
import { buildOauthInfo, getOauthInfoFromExtraConfig, type OauthInfo } from './oauthAccount.js';
import type { OauthQuotaSnapshot, OauthQuotaWindowSnapshot } from './quotaTypes.js';
type CodexJwtClaims = {
'https://api.openai.com/auth'?: {
chatgpt_plan_type?: unknown;
chatgpt_subscription_active_start?: unknown;
chatgpt_subscription_active_until?: unknown;
};
};
function asTrimmedString(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed || undefined;
}
function asIsoDateTime(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const parsed = Date.parse(trimmed);
return Number.isNaN(parsed) ? undefined : new Date(parsed).toISOString();
}
function parseCodexJwtClaims(idToken?: string): CodexJwtClaims | null {
if (!idToken) return null;
const parts = idToken.split('.');
if (parts.length !== 3) return null;
try {
const payload = JSON.parse(Buffer.from(parts[1] || '', 'base64url').toString('utf8')) as unknown;
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
return payload as CodexJwtClaims;
} catch {
return null;
}
}
function buildUnsupportedWindow(message: string): OauthQuotaWindowSnapshot {
return { supported: false, message };
}
function buildCodexUnsupportedWindows(): OauthQuotaSnapshot['windows'] {
return {
fiveHour: buildUnsupportedWindow('official 5h quota window is not exposed by current codex oauth artifacts'),
sevenDay: buildUnsupportedWindow('official 7d quota window is not exposed by current codex oauth artifacts'),
};
}
function buildProviderUnsupportedSnapshot(provider: string): OauthQuotaSnapshot {
return {
status: 'unsupported',
source: 'official',
providerMessage: `current local CLIProxyAPI evidence does not expose official quota windows for ${provider} oauth`,
windows: {
fiveHour: buildUnsupportedWindow('official 5h quota window is unavailable for this provider'),
sevenDay: buildUnsupportedWindow('official 7d quota window is unavailable for this provider'),
},
};
}
function normalizeStoredWindow(value: unknown): OauthQuotaWindowSnapshot | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const raw = value as Record<string, unknown>;
const supported = typeof raw.supported === 'boolean' ? raw.supported : undefined;
if (supported === undefined) return undefined;
const pickNumber = (field: string) => {
const item = raw[field];
return typeof item === 'number' && Number.isFinite(item) ? item : undefined;
};
const normalized: OauthQuotaWindowSnapshot = {
supported,
};
const limit = pickNumber('limit');
const used = pickNumber('used');
const remaining = pickNumber('remaining');
const resetAt = asIsoDateTime(raw.resetAt);
const message = asTrimmedString(raw.message);
if (limit !== undefined) normalized.limit = limit;
if (used !== undefined) normalized.used = used;
if (remaining !== undefined) normalized.remaining = remaining;
if (resetAt) normalized.resetAt = resetAt;
if (message) normalized.message = message;
return normalized;
}
function normalizeStoredQuotaSnapshot(value: unknown): OauthQuotaSnapshot | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const raw = value as Record<string, unknown>;
const status = raw.status === 'supported' || raw.status === 'unsupported' || raw.status === 'error'
? raw.status
: undefined;
const source = raw.source === 'official' || raw.source === 'reverse_engineered'
? raw.source
: undefined;
const windowsRaw = raw.windows;
if (!status || !source || !windowsRaw || typeof windowsRaw !== 'object' || Array.isArray(windowsRaw)) {
return undefined;
}
const windowsObject = windowsRaw as Record<string, unknown>;
const fiveHour = normalizeStoredWindow(windowsObject.fiveHour);
const sevenDay = normalizeStoredWindow(windowsObject.sevenDay);
if (!fiveHour || !sevenDay) return undefined;
const subscriptionRaw = raw.subscription;
const subscription = subscriptionRaw && typeof subscriptionRaw === 'object' && !Array.isArray(subscriptionRaw)
? {
planType: asTrimmedString((subscriptionRaw as Record<string, unknown>).planType),
activeStart: asIsoDateTime((subscriptionRaw as Record<string, unknown>).activeStart),
activeUntil: asIsoDateTime((subscriptionRaw as Record<string, unknown>).activeUntil),
}
: undefined;
return {
status,
source,
...(asIsoDateTime(raw.lastSyncAt) ? { lastSyncAt: asIsoDateTime(raw.lastSyncAt)! } : {}),
...(asTrimmedString(raw.lastError) ? { lastError: asTrimmedString(raw.lastError)! } : {}),
...(asTrimmedString(raw.providerMessage) ? { providerMessage: asTrimmedString(raw.providerMessage)! } : {}),
...(subscription && (subscription.planType || subscription.activeStart || subscription.activeUntil)
? { subscription }
: {}),
windows: { fiveHour, sevenDay },
...(asIsoDateTime(raw.lastLimitResetAt) ? { lastLimitResetAt: asIsoDateTime(raw.lastLimitResetAt)! } : {}),
};
}
function buildStoredCodexSnapshot(oauth: Pick<OauthInfo, 'planType' | 'idToken' | 'quota'>): OauthQuotaSnapshot {
const claims = parseCodexJwtClaims(oauth.idToken);
const authClaims = claims?.['https://api.openai.com/auth'];
const storedQuota = normalizeStoredQuotaSnapshot(oauth.quota);
const subscription = {
planType: asTrimmedString(authClaims?.chatgpt_plan_type) || oauth.planType,
activeStart: asIsoDateTime(authClaims?.chatgpt_subscription_active_start),
activeUntil: asIsoDateTime(authClaims?.chatgpt_subscription_active_until),
};
return {
status: storedQuota?.status || 'supported',
source: storedQuota?.source || 'reverse_engineered',
...(storedQuota?.lastSyncAt ? { lastSyncAt: storedQuota.lastSyncAt } : {}),
...(storedQuota?.lastError ? { lastError: storedQuota.lastError } : {}),
providerMessage: storedQuota?.providerMessage || 'current codex oauth signals do not expose stable 5h/7d remaining values',
...((subscription.planType || subscription.activeStart || subscription.activeUntil) ? { subscription } : {}),
windows: storedQuota?.windows || buildCodexUnsupportedWindows(),
...(storedQuota?.lastLimitResetAt ? { lastLimitResetAt: storedQuota.lastLimitResetAt } : {}),
};
}
export function buildQuotaSnapshotFromOauthInfo(oauth: Pick<OauthInfo, 'provider' | 'planType' | 'idToken' | 'quota'>): OauthQuotaSnapshot {
if (oauth.provider === 'codex') {
return buildStoredCodexSnapshot(oauth);
}
return buildProviderUnsupportedSnapshot(oauth.provider);
}
export function parseCodexQuotaResetHint(
statusCode: number,
errorBody: string | null | undefined,
nowMs = Date.now(),
): { resetAt: string; message: string } | null {
if (statusCode !== 429 || !errorBody) return null;
try {
const parsed = JSON.parse(errorBody) as Record<string, any>;
const error = parsed?.error;
if (!error || typeof error !== 'object' || error.type !== 'usage_limit_reached') {
return null;
}
if (typeof error.resets_at === 'number' && Number.isFinite(error.resets_at) && error.resets_at > 0) {
return {
resetAt: new Date(error.resets_at * 1000).toISOString(),
message: 'codex usage_limit_reached reset hint observed from upstream',
};
}
if (typeof error.resets_in_seconds === 'number' && Number.isFinite(error.resets_in_seconds) && error.resets_in_seconds > 0) {
return {
resetAt: new Date(nowMs + error.resets_in_seconds * 1000).toISOString(),
message: 'codex usage_limit_reached reset hint observed from upstream',
};
}
} catch {
return null;
}
return null;
}
async function persistQuotaSnapshot(accountId: number, snapshot: OauthQuotaSnapshot) {
const account = await db.select().from(schema.accounts).where(eq(schema.accounts.id, accountId)).get();
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
if (!oauth) {
throw new Error('account is not managed by oauth');
}
const nextExtraConfig = mergeAccountExtraConfig(account.extraConfig, {
oauth: buildOauthInfo(account.extraConfig, {
quota: snapshot,
}),
});
await db.update(schema.accounts).set({
extraConfig: nextExtraConfig,
updatedAt: new Date().toISOString(),
}).where(eq(schema.accounts.id, accountId)).run();
return snapshot;
}
export async function refreshOauthQuotaSnapshot(accountId: number): Promise<OauthQuotaSnapshot> {
const account = await db.select().from(schema.accounts).where(eq(schema.accounts.id, accountId)).get();
if (!account) {
throw new Error('oauth account not found');
}
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
if (!oauth) {
throw new Error('account is not managed by oauth');
}
const baseSnapshot = buildQuotaSnapshotFromOauthInfo(oauth);
const snapshot: OauthQuotaSnapshot = {
...baseSnapshot,
lastSyncAt: new Date().toISOString(),
...(baseSnapshot.status === 'error' ? {} : { lastError: undefined }),
};
return persistQuotaSnapshot(accountId, snapshot);
}
export async function recordOauthQuotaResetHint(input: {
accountId: number;
statusCode: number;
errorText?: string | null;
}): Promise<OauthQuotaSnapshot | null> {
const resetHint = parseCodexQuotaResetHint(input.statusCode, input.errorText);
if (!resetHint) return null;
const account = await db.select().from(schema.accounts).where(eq(schema.accounts.id, input.accountId)).get();
if (!account) return null;
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
if (!oauth || oauth.provider !== 'codex') return null;
const baseSnapshot = buildQuotaSnapshotFromOauthInfo({
...oauth,
quota: {
...normalizeStoredQuotaSnapshot(oauth.quota),
status: 'supported',
source: 'reverse_engineered',
lastLimitResetAt: resetHint.resetAt,
providerMessage: 'current codex oauth signals do not expose stable 5h/7d remaining values',
windows: normalizeStoredQuotaSnapshot(oauth.quota)?.windows || buildCodexUnsupportedWindows(),
},
});
return persistQuotaSnapshot(input.accountId, {
...baseSnapshot,
lastSyncAt: new Date().toISOString(),
lastLimitResetAt: resetHint.resetAt,
});
}
+26
View File
@@ -0,0 +1,26 @@
export type OauthQuotaWindowSnapshot = {
supported: boolean;
limit?: number | null;
used?: number | null;
remaining?: number | null;
resetAt?: string | null;
message?: string;
};
export type OauthQuotaSnapshot = {
status: 'supported' | 'unsupported' | 'error';
source: 'official' | 'reverse_engineered';
lastSyncAt?: string;
lastError?: string;
providerMessage?: string;
subscription?: {
planType?: string;
activeStart?: string;
activeUntil?: string;
};
windows: {
fiveHour: OauthQuotaWindowSnapshot;
sevenDay: OauthQuotaWindowSnapshot;
};
lastLimitResetAt?: string;
};
@@ -524,6 +524,44 @@ describe('convertOpenAiBodyToAnthropicMessagesBody', () => {
]);
});
it('preserves tool_reference blocks inside tool_result content arrays', () => {
const result = sanitizeAnthropicMessagesBody({
model: 'claude-opus-4-6',
messages: [
{
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool_1',
content: [
{ type: 'tool_reference', tool_name: 'lookup' },
{ type: 'text', text: 'follow-up' },
],
},
],
},
],
});
expect(result.messages).toEqual([
{
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool_1',
content: [
{ type: 'tool_reference', tool_name: 'lookup' },
{ type: 'text', text: 'follow-up' },
],
cache_control: { type: 'ephemeral' },
},
],
},
]);
});
it('adds a second message cache anchor around the 20-block window for long prompts', () => {
const longContent = Array.from({ length: 25 }, (_, index) => ({
type: 'text',
@@ -187,12 +187,39 @@ function normalizeTextCandidate(value: unknown): string {
return asTrimmedString(value.text ?? value.content ?? value.output_text);
}
function normalizeToolMessageContent(raw: unknown): string {
function sanitizeToolReferenceBlock(item: Record<string, unknown>): Record<string, unknown> | null {
const toolName = asTrimmedString(item.tool_name ?? item.toolName ?? item.name);
if (!toolName) return null;
const next: Record<string, unknown> = {
...item,
type: 'tool_reference',
tool_name: toolName,
};
delete next.toolName;
if ('name' in next) delete next.name;
return next;
}
function normalizeToolMessageContent(raw: unknown): string | Array<Record<string, unknown>> {
if (raw === undefined || raw === null) return '';
if (typeof raw === 'string') return raw;
if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
if (Array.isArray(raw)) {
const structuredBlocks = raw
.map((item) => {
if (typeof item === 'string') return toAnthropicTextBlock(item);
if (!isRecord(item)) return null;
const type = asTrimmedString(item.type).toLowerCase();
if (type === 'tool_reference') return sanitizeToolReferenceBlock(item);
if (type === 'text' || type === 'input_text' || type === 'output_text') {
return toAnthropicTextBlock(normalizeTextCandidate(item));
}
return null;
})
.filter((item): item is Record<string, unknown> => item !== null);
if (structuredBlocks.length > 0) return structuredBlocks;
const textParts = raw
.map((item) => {
if (typeof item === 'string') return item;
@@ -329,7 +356,7 @@ function sanitizeAnthropicContentBlock(item: Record<string, unknown>): Record<st
if (type === 'tool_result') {
const toolUseId = asTrimmedString(item.tool_use_id ?? item.toolUseId);
const content = normalizeToolMessageContent(item.content ?? item.result);
if (!toolUseId || !content) return null;
if (!toolUseId || (typeof content === 'string' ? !content : content.length <= 0)) return null;
const next: Record<string, unknown> = {
type: 'tool_result',
tool_use_id: toolUseId,
@@ -355,6 +382,10 @@ function sanitizeAnthropicContentBlock(item: Record<string, unknown>): Record<st
return next;
}
if (type === 'tool_reference') {
return sanitizeToolReferenceBlock(item);
}
return { ...item };
}
@@ -846,8 +877,11 @@ export function convertOpenAiBodyToAnthropicMessagesBody(
asTrimmedString(toolCandidate.tool_call_id)
|| asTrimmedString(toolCandidate.id)
);
const toolResultContent = normalizeToolMessageContent(toolCandidate.content).trim();
if (toolUseId && toolResultContent) {
const toolResultContent = normalizeToolMessageContent(toolCandidate.content);
const hasToolResultContent = typeof toolResultContent === 'string'
? toolResultContent.trim().length > 0
: toolResultContent.length > 0;
if (toolUseId && hasToolResultContent) {
toolResultBlocks.push({
type: 'tool_result',
tool_use_id: toolUseId,