0f8176cd04
- Added `routeCompatibility.ts` to handle response compatibility for different endpoints. - Introduced `chatEndpointStrategy.ts` to manage chat endpoint recovery and downgrading logic. - Created tests for chat formats and input file handling in `chatFormatsCore.test.ts` and `inputFile.test.ts`. - Developed `endpointCompatibility.ts` to define compatibility checks and header building for various endpoints. - Implemented protocol lifecycle management in `protocolLifecycle.ts` and corresponding tests in `protocolModel.test.ts`. - Enhanced tooltip functionality with a new `TooltipLayer` component and associated tests. - Refactored existing components to improve tooltip handling and ensure proper rendering in the application.
247 lines
8.1 KiB
TypeScript
247 lines
8.1 KiB
TypeScript
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 estimateProxyCostMock = vi.fn(async (_arg?: any) => 0);
|
|
const buildProxyBillingDetailsMock = vi.fn(async (_arg?: any) => null);
|
|
const fetchModelPricingCatalogMock = vi.fn(async (_arg?: any): Promise<any> => null);
|
|
const resolveProxyUsageWithSelfLogFallbackMock = vi.fn(async ({ usage }: any) => ({
|
|
...usage,
|
|
estimatedCostFromQuota: 0,
|
|
recoveredFromSelfLog: false,
|
|
}));
|
|
const dbValuesMock = vi.fn((_arg?: any) => ({
|
|
run: () => undefined,
|
|
}));
|
|
const dbInsertMock = vi.fn((_arg?: any) => ({
|
|
values: (arg: any) => dbValuesMock(arg),
|
|
}));
|
|
|
|
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: (arg: any) => estimateProxyCostMock(arg),
|
|
buildProxyBillingDetails: (arg: any) => buildProxyBillingDetailsMock(arg),
|
|
fetchModelPricingCatalog: (arg: any) => fetchModelPricingCatalogMock(arg),
|
|
}));
|
|
|
|
vi.mock('../../services/proxyRetryPolicy.js', () => ({
|
|
shouldRetryProxyRequest: () => false,
|
|
}));
|
|
|
|
vi.mock('../../services/proxyUsageFallbackService.js', () => ({
|
|
resolveProxyUsageWithSelfLogFallback: (arg: any) => resolveProxyUsageWithSelfLogFallbackMock(arg),
|
|
}));
|
|
|
|
vi.mock('../../db/index.js', () => ({
|
|
db: {
|
|
insert: (arg: any) => dbInsertMock(arg),
|
|
},
|
|
schema: {
|
|
proxyLogs: {},
|
|
},
|
|
}));
|
|
|
|
describe('downstream client context route logging', () => {
|
|
let app: FastifyInstance;
|
|
|
|
beforeAll(async () => {
|
|
const { claudeMessagesProxyRoute } = await import('./chat.js');
|
|
const { responsesProxyRoute } = await import('./responses.js');
|
|
app = Fastify();
|
|
await app.register(claudeMessagesProxyRoute);
|
|
await app.register(responsesProxyRoute);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
fetchMock.mockReset();
|
|
selectChannelMock.mockReset();
|
|
selectNextChannelMock.mockReset();
|
|
recordSuccessMock.mockReset();
|
|
recordFailureMock.mockReset();
|
|
refreshModelsAndRebuildRoutesMock.mockReset();
|
|
reportProxyAllFailedMock.mockReset();
|
|
reportTokenExpiredMock.mockReset();
|
|
estimateProxyCostMock.mockClear();
|
|
buildProxyBillingDetailsMock.mockClear();
|
|
fetchModelPricingCatalogMock.mockReset();
|
|
resolveProxyUsageWithSelfLogFallbackMock.mockClear();
|
|
dbInsertMock.mockClear();
|
|
dbValuesMock.mockClear();
|
|
|
|
selectChannelMock.mockReturnValue({
|
|
channel: { id: 11, routeId: 22 },
|
|
site: { id: 44, name: 'demo-site', url: 'https://upstream.example.com', platform: 'openai' },
|
|
account: { id: 33, username: 'demo-user' },
|
|
tokenName: 'default',
|
|
tokenValue: 'sk-demo',
|
|
actualModel: 'upstream-gpt',
|
|
});
|
|
selectNextChannelMock.mockReturnValue(null);
|
|
fetchModelPricingCatalogMock.mockResolvedValue(null);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
it('includes Codex client and session metadata in /v1/responses failure logs', async () => {
|
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
|
error: {
|
|
message: 'bad request',
|
|
type: 'upstream_error',
|
|
},
|
|
}), {
|
|
status: 400,
|
|
headers: { 'content-type': 'application/json' },
|
|
}));
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/responses',
|
|
headers: {
|
|
originator: 'codex_cli_rs',
|
|
Session_id: 'codex-session-123',
|
|
},
|
|
payload: {
|
|
model: 'gpt-5.2',
|
|
input: 'hello',
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(dbValuesMock).toHaveBeenCalled();
|
|
const insertedLog = dbValuesMock.mock.calls.at(-1)?.[0];
|
|
expect(insertedLog.errorMessage).toContain('[client:codex]');
|
|
expect(insertedLog.errorMessage).toContain('[session:codex-session-123]');
|
|
expect(insertedLog.errorMessage).toContain('[downstream:/v1/responses]');
|
|
});
|
|
|
|
it('reuses the same Codex detection on /v1/responses/compact failure logs', async () => {
|
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
|
error: {
|
|
message: 'compact failed',
|
|
type: 'upstream_error',
|
|
},
|
|
}), {
|
|
status: 400,
|
|
headers: { 'content-type': 'application/json' },
|
|
}));
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/responses/compact',
|
|
headers: {
|
|
'x-stainless-lang': 'typescript',
|
|
Session_id: 'codex-session-compact',
|
|
},
|
|
payload: {
|
|
model: 'gpt-5.2',
|
|
input: 'hello',
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(dbValuesMock).toHaveBeenCalled();
|
|
const insertedLog = dbValuesMock.mock.calls.at(-1)?.[0];
|
|
expect(insertedLog.errorMessage).toContain('[client:codex]');
|
|
expect(insertedLog.errorMessage).toContain('[session:codex-session-compact]');
|
|
expect(insertedLog.errorMessage).toContain('[downstream:/v1/responses/compact]');
|
|
});
|
|
|
|
it('includes Claude Code client and session metadata in /v1/messages failure logs', async () => {
|
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
|
error: {
|
|
message: 'messages failed',
|
|
type: 'upstream_error',
|
|
},
|
|
}), {
|
|
status: 400,
|
|
headers: { 'content-type': 'application/json' },
|
|
}));
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/messages',
|
|
payload: {
|
|
model: 'claude-opus-4-6',
|
|
max_tokens: 256,
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
metadata: {
|
|
user_id: 'user_20836b5653ed68aa981604f502c0a491397f6053826a93c953423632578d38ad_account__session_f25958b8-e75c-455d-8b40-f006d87cc2a4',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(dbValuesMock).toHaveBeenCalled();
|
|
const insertedLog = dbValuesMock.mock.calls.at(-1)?.[0];
|
|
expect(insertedLog.errorMessage).toContain('[client:claude_code]');
|
|
expect(insertedLog.errorMessage).toContain('[session:f25958b8-e75c-455d-8b40-f006d87cc2a4]');
|
|
expect(insertedLog.errorMessage).toContain('[downstream:/v1/messages]');
|
|
});
|
|
|
|
it('keeps invalid Claude metadata.user_id requests generic in failure logs', async () => {
|
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
|
error: {
|
|
message: 'messages failed',
|
|
type: 'upstream_error',
|
|
},
|
|
}), {
|
|
status: 400,
|
|
headers: { 'content-type': 'application/json' },
|
|
}));
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/messages',
|
|
payload: {
|
|
model: 'claude-opus-4-6',
|
|
max_tokens: 256,
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
metadata: {
|
|
user_id: 'user_123',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(dbValuesMock).toHaveBeenCalled();
|
|
const insertedLog = dbValuesMock.mock.calls.at(-1)?.[0];
|
|
expect(insertedLog.errorMessage).toContain('[downstream:/v1/messages]');
|
|
expect(insertedLog.errorMessage).not.toContain('[client:');
|
|
expect(insertedLog.errorMessage).not.toContain('[session:');
|
|
});
|
|
});
|