feat: implement protocol parity and harden upstream handling (#88)

* feat: implement protocol parity and harden upstream handling

* docs(agents): add lodash-es utility method and named import guideline
This commit is contained in:
vin authored and GitHub committed 2026-02-11 00:42:39 +08:00
1 parent 0f7a629a47
commit 13f10fe32f
23 files changed
+4408 -654

No files matched your search

@@ -0,0 +1,482 @@
import { describe, expect, it, vi } from 'vitest';
import { of } from 'rxjs';
import { ProxyController } from '../../server/modules/proxy/proxy.controller';
function createReplyMock() {
const reply: Record<string, any> = {};
reply.status = vi.fn(() => reply);
reply.header = vi.fn(() => reply);
reply.send = vi.fn(() => reply);
return reply;
}
describe('ProxyController Integration', () => {
it('supports models listing endpoints', () => {
const proxyService = {};
const controller = new ProxyController(proxyService as any);
const allModels = controller.getModels();
const claudeModels = controller.getClaudeModels();
expect(allModels.object).toBe('list');
expect(Array.isArray(allModels.data)).toBe(true);
expect(allModels.data.length).toBeGreaterThan(0);
expect(allModels.data.every((model) => model.owned_by === 'antigravity')).toBe(true);
expect(allModels.data.every((model) => model.created === 1770652800)).toBe(true);
expect(claudeModels).toEqual(allModels);
});
it('routes Claude OpenAI requests to protocol parity path', async () => {
const proxyService = {
handleChatCompletions: vi.fn().mockResolvedValue({ ok: true }),
handleAnthropicMessages: vi.fn(),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.chatCompletions(
{
model: 'claude-sonnet-4-5',
stream: false,
messages: [{ role: 'user', content: 'hello' }],
} as any,
reply as any,
);
expect(proxyService.handleChatCompletions).toHaveBeenCalledOnce();
expect(proxyService.handleAnthropicMessages).not.toHaveBeenCalled();
expect(reply.status).toHaveBeenCalledWith(200);
});
it('returns stream response with SSE headers for parity stream path', async () => {
const stream = of('data: {"ok":true}\n\n');
const proxyService = {
handleChatCompletions: vi.fn().mockResolvedValue(stream),
handleAnthropicMessages: vi.fn(),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.chatCompletions(
{
model: 'claude-sonnet-4-5',
stream: true,
messages: [{ role: 'user', content: 'hello' }],
} as any,
reply as any,
);
expect(reply.header).toHaveBeenCalledWith('Content-Type', 'text/event-stream');
expect(reply.header).toHaveBeenCalledWith('Cache-Control', 'no-cache');
expect(reply.header).toHaveBeenCalledWith('Connection', 'keep-alive');
expect(reply.send).toHaveBeenCalledWith(stream);
});
it('supports OpenAI completions compatibility endpoint', async () => {
const proxyService = {
handleChatCompletions: vi.fn().mockResolvedValue({
id: 'chatcmpl_test',
object: 'chat.completion',
created: 1700000000,
model: 'gpt-4o',
choices: [
{
index: 0,
finish_reason: 'stop',
message: {
role: 'assistant',
content: 'hello from assistant',
},
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}),
handleAnthropicMessages: vi.fn(),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.completions(
{
model: 'gpt-4o',
prompt: 'hello world',
stream: false,
},
reply as any,
);
expect(proxyService.handleChatCompletions).toHaveBeenCalledWith(
expect.objectContaining({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'hello world' }],
}),
);
expect(reply.status).toHaveBeenCalledWith(200);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
object: 'text_completion',
model: 'gpt-4o',
choices: [
expect.objectContaining({
text: 'hello from assistant',
logprobs: null,
}),
],
}),
);
});
it('supports OpenAI responses compatibility endpoint with normalized input', async () => {
const proxyService = {
handleChatCompletions: vi.fn().mockResolvedValue({
id: 'chatcmpl_resp',
object: 'chat.completion',
created: 1700000001,
model: 'gpt-4o',
choices: [
{
index: 0,
finish_reason: 'stop',
message: {
content: 'normalized response',
},
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 6,
total_tokens: 16,
},
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.responses(
{
model: 'gpt-4o',
instructions: 'Follow the tool protocol',
input: [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'hello' }],
},
{
type: 'function_call',
id: 'call_1',
name: 'search_docs',
arguments: '{"query":"token"}',
},
{
type: 'function_call_output',
call_id: 'call_1',
output: { content: 'result: ok' },
},
],
},
reply as any,
);
const callArg = proxyService.handleChatCompletions.mock.calls[0][0];
expect(callArg.messages[0]).toEqual({
role: 'system',
content: 'Follow the tool protocol',
});
expect(callArg.messages.some((message: { role: string }) => message.role === 'assistant')).toBe(
true,
);
expect(callArg.messages.some((message: { role: string }) => message.role === 'tool')).toBe(
true,
);
expect(reply.status).toHaveBeenCalledWith(200);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
object: 'text_completion',
model: 'gpt-4o',
choices: [
expect.objectContaining({
text: 'normalized response',
logprobs: null,
}),
],
usage: expect.objectContaining({
prompt_tokens: 10,
completion_tokens: 6,
total_tokens: 16,
}),
}),
);
});
it('supports image generations endpoint', async () => {
const proxyService = {
handleChatCompletions: vi.fn().mockResolvedValue({
choices: [
{
message: {
content: '![img](data:image/png;base64,AAAABBBB)',
},
},
],
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.imageGenerations(
{
model: 'gemini-3-pro-image',
prompt: 'draw a cat',
},
reply as any,
);
expect(reply.status).toHaveBeenCalledWith(200);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
data: [
expect.objectContaining({
b64_json: 'AAAABBBB',
}),
],
}),
);
});
it('maps image generation upstream quota errors to 429', async () => {
const proxyService = {
handleChatCompletions: vi.fn().mockRejectedValue(new Error('429 quota exceeded')),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.imageGenerations(
{
model: 'gemini-3-pro-image',
prompt: 'draw a dog',
},
reply as any,
);
expect(reply.status).toHaveBeenCalledWith(429);
});
it('falls back to Gemini image generation when chat path hits project context error', async () => {
const proxyService = {
handleChatCompletions: vi
.fn()
.mockRejectedValue(
new Error(
'You are currently configured to use a Google Cloud Project but lack a Gemini Code Assist license. (#3501)',
),
),
handleGeminiGenerateContent: vi.fn().mockResolvedValue({
candidates: [
{
content: {
parts: [
{
inlineData: {
mimeType: 'image/png',
data: 'FALLBACKIMG',
},
},
],
},
},
],
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.imageGenerations(
{
model: 'gemini-3-pro-image',
prompt: 'draw a fox',
},
reply as any,
);
expect(proxyService.handleChatCompletions).toHaveBeenCalledOnce();
expect(proxyService.handleGeminiGenerateContent).toHaveBeenCalledOnce();
expect(reply.status).toHaveBeenCalledWith(200);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
data: [
expect.objectContaining({
b64_json: 'FALLBACKIMG',
}),
],
}),
);
});
it('supports image edits endpoint with supplementary image payload', async () => {
const proxyService = {
handleChatCompletions: vi.fn().mockResolvedValue({
choices: [
{
message: {
content: '![img](data:image/png;base64,CCCCDDDD)',
},
},
],
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.imageEdits(
{
model: 'gemini-3-pro-image',
prompt: 'make it brighter',
image: 'data:image/png;base64,IMGBASE64',
reference_images: ['data:image/jpeg;base64,REFBASE64'],
},
{
headers: {
'content-type': 'multipart/form-data; boundary=----parity',
},
} as any,
reply as any,
);
const request = proxyService.handleChatCompletions.mock.calls[0][0];
expect(Array.isArray(request.messages[0].content)).toBe(true);
expect(reply.status).toHaveBeenCalledWith(200);
});
it('rejects image edits request without multipart boundary', async () => {
const proxyService = {
handleChatCompletions: vi.fn(),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.imageEdits(
{
model: 'gemini-3-pro-image',
prompt: 'make it brighter',
image: 'data:image/png;base64,IMGBASE64',
},
{
headers: {
'content-type': 'application/json',
},
} as any,
reply as any,
);
expect(proxyService.handleChatCompletions).not.toHaveBeenCalled();
expect(reply.status).toHaveBeenCalledWith(400);
expect(reply.send).toHaveBeenCalledWith('Invalid `boundary` for `multipart/form-data` request');
});
it('supports audio transcriptions endpoint', async () => {
const proxyService = {
handleGeminiGenerateContent: vi.fn().mockResolvedValue({
candidates: [
{
content: {
parts: [{ text: 'transcribed text' }],
},
},
],
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.audioTranscriptions(
{
model: 'gemini-2.5-flash',
file: 'data:audio/mpeg;base64,QUJDRA==',
},
{
headers: {
'content-type': 'multipart/form-data; boundary=----parity',
},
} as any,
reply as any,
);
expect(proxyService.handleGeminiGenerateContent).toHaveBeenCalledOnce();
expect(reply.status).toHaveBeenCalledWith(200);
expect(reply.send).toHaveBeenCalledWith({ text: 'transcribed text' });
});
it('rejects audio transcription request without multipart boundary', async () => {
const proxyService = {
handleGeminiGenerateContent: vi.fn(),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.audioTranscriptions(
{
model: 'gemini-2.5-flash',
file: 'data:audio/mpeg;base64,QUJDRA==',
},
{
headers: {
'content-type': 'application/json',
},
} as any,
reply as any,
);
expect(proxyService.handleGeminiGenerateContent).not.toHaveBeenCalled();
expect(reply.status).toHaveBeenCalledWith(400);
expect(reply.send).toHaveBeenCalledWith('Invalid `boundary` for `multipart/form-data` request');
});
it('supports model detect endpoint', () => {
const proxyService = {
detectModelCapabilities: vi.fn().mockReturnValue({
model: 'claude-sonnet-4-5',
mapped_model: 'gemini-3-pro-preview',
type: 'generate-content',
features: { has_web_search: false, is_image_gen: false },
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
controller.detectModel({ model: 'claude-sonnet-4-5' }, reply as any);
expect(proxyService.detectModelCapabilities).toHaveBeenCalledWith('claude-sonnet-4-5');
expect(reply.status).toHaveBeenCalledWith(200);
});
it('supports Anthropic messages endpoint', async () => {
const proxyService = {
handleAnthropicMessages: vi.fn().mockResolvedValue({
id: 'msg_1',
type: 'message',
}),
};
const controller = new ProxyController(proxyService as any);
const reply = createReplyMock();
await controller.anthropicMessages(
{
model: 'claude-sonnet-4-5',
stream: false,
messages: [{ role: 'user', content: 'hello' }],
} as any,
reply as any,
);
expect(proxyService.handleAnthropicMessages).toHaveBeenCalledOnce();
expect(reply.status).toHaveBeenCalledWith(200);
});
});