mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 16:15:19 +00:00
fix(proxy): route Anthropic passthrough by upstream profile
This commit is contained in:
1 parent
b98b0df084
commit
bb9d23ad6a
5 files changed
+334
-112
No files matched your search
@@ -103,6 +103,12 @@ beforeEach(() => {
|
||||
ANTHROPIC_MODEL: 'MiniMax-M2.7',
|
||||
CCS_DROID_PROVIDER: 'openai',
|
||||
}),
|
||||
kimic: writeSettings('kimic', {
|
||||
ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/',
|
||||
ANTHROPIC_AUTH_TOKEN: 'kimi_token',
|
||||
ANTHROPIC_MODEL: 'kimi-k2p7-coding',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
@@ -166,6 +172,152 @@ describe('attachDisconnectAbortHandlers', () => {
|
||||
});
|
||||
|
||||
describe('handleProxyMessagesRequest', () => {
|
||||
it('auto-passes through Kimi requests and preserves the coding-agent User-Agent', async () => {
|
||||
const activeProfile = buildProfile('kimic');
|
||||
let capturedInput: RequestInfo | URL | undefined;
|
||||
let capturedInit: RequestInit | undefined;
|
||||
const rawBody = JSON.stringify({
|
||||
model: 'kimi-k2p7-coding',
|
||||
max_tokens: 50,
|
||||
messages: [{ role: 'user', content: 'Say hi in 2 words' }],
|
||||
});
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedInput = input;
|
||||
capturedInit = init;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'msg_1',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Hello there!' }],
|
||||
model: 'kimi-k2p7-coding',
|
||||
stop_reason: 'end_turn',
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
const req = new FakeRequest({
|
||||
'x-api-key': 'local-token',
|
||||
'user-agent': 'claude-cli/2.1.170',
|
||||
});
|
||||
const res = new FakeResponse();
|
||||
const pending = handleProxyMessagesRequest(
|
||||
req as never,
|
||||
res as never,
|
||||
activeProfile,
|
||||
'local-token'
|
||||
);
|
||||
req.end(rawBody);
|
||||
await pending;
|
||||
|
||||
expect(String(capturedInput)).toBe('https://api.kimi.com/coding/v1/messages');
|
||||
expect(capturedInit?.body).toBe(rawBody);
|
||||
expect((capturedInit?.headers as Record<string, string>)['User-Agent']).toBe(
|
||||
'claude-cli/2.1.170'
|
||||
);
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('uses routed-profile passthrough for explicit profile:model selectors', async () => {
|
||||
const activeProfile = buildProfile('hf');
|
||||
let capturedInput: RequestInfo | URL | undefined;
|
||||
let capturedInit: RequestInit | undefined;
|
||||
const rawBody = JSON.stringify({
|
||||
model: 'kimic:kimi-k2p7-coding',
|
||||
max_tokens: 50,
|
||||
messages: [{ role: 'user', content: 'Say hi' }],
|
||||
});
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedInput = input;
|
||||
capturedInit = init;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'msg_2',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Hi' }],
|
||||
model: 'kimi-k2p7-coding',
|
||||
stop_reason: 'end_turn',
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
const req = new FakeRequest({
|
||||
'x-api-key': 'local-token',
|
||||
'x-stainless-user-agent': 'claude-cli/2.1.170',
|
||||
});
|
||||
const res = new FakeResponse();
|
||||
const pending = handleProxyMessagesRequest(
|
||||
req as never,
|
||||
res as never,
|
||||
activeProfile,
|
||||
'local-token'
|
||||
);
|
||||
req.end(rawBody);
|
||||
await pending;
|
||||
|
||||
expect(String(capturedInput)).toBe('https://api.kimi.com/coding/v1/messages');
|
||||
expect(capturedInit?.body).toBe(rawBody);
|
||||
expect((capturedInit?.headers as Record<string, string>)['User-Agent']).toBe(
|
||||
'claude-cli/2.1.170'
|
||||
);
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('keeps default OpenAI-compatible requests on chat completions with CCS User-Agent', async () => {
|
||||
const activeProfile = buildProfile('hf');
|
||||
let capturedInput: RequestInfo | URL | undefined;
|
||||
let capturedInit: RequestInit | undefined;
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedInput = input;
|
||||
capturedInit = init;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl_1',
|
||||
object: 'chat.completion',
|
||||
created: 1,
|
||||
model: 'hf-default',
|
||||
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' } }],
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
const req = new FakeRequest({
|
||||
'x-api-key': 'local-token',
|
||||
'user-agent': 'claude-cli/2.1.170',
|
||||
});
|
||||
const res = new FakeResponse();
|
||||
const pending = handleProxyMessagesRequest(
|
||||
req as never,
|
||||
res as never,
|
||||
activeProfile,
|
||||
'local-token'
|
||||
);
|
||||
req.end(
|
||||
JSON.stringify({
|
||||
model: 'hf-default',
|
||||
messages: [{ role: 'user', content: 'stay translated' }],
|
||||
})
|
||||
);
|
||||
await pending;
|
||||
|
||||
expect(String(capturedInput)).toBe('https://router.huggingface.co/v1/chat/completions');
|
||||
expect(JSON.parse(String(capturedInit?.body))).toMatchObject({
|
||||
model: 'hf-default',
|
||||
messages: [{ role: 'user', content: 'stay translated' }],
|
||||
});
|
||||
expect((capturedInit?.headers as Record<string, string>)['User-Agent']).toBe(
|
||||
'CCS-OpenAI-Compat-Proxy/1.0'
|
||||
);
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('uses a per-request insecure dispatcher for routed profiles and closes it on failure', async () => {
|
||||
const activeProfile = buildProfile('hf');
|
||||
const sharedDispatcher = { name: 'shared-insecure-dispatcher' } as never;
|
||||
|
||||
@@ -95,32 +95,24 @@ describe('resolveOpenAICompatProfileConfig', () => {
|
||||
});
|
||||
|
||||
it('honors CCS_OPENAI_PROXY_PASSTHROUGH=1 for Anthropic-style endpoints', () => {
|
||||
const result = resolveOpenAICompatProfileConfig(
|
||||
'kimic',
|
||||
'/tmp/kimic.settings.json',
|
||||
{
|
||||
ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/',
|
||||
ANTHROPIC_AUTH_TOKEN: 'sk-kimi',
|
||||
ANTHROPIC_MODEL: 'kimi-k2p7-coding',
|
||||
CCS_OPENAI_PROXY_PASSTHROUGH: 'true',
|
||||
}
|
||||
);
|
||||
const result = resolveOpenAICompatProfileConfig('kimic', '/tmp/kimic.settings.json', {
|
||||
ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/',
|
||||
ANTHROPIC_AUTH_TOKEN: 'sk-kimi',
|
||||
ANTHROPIC_MODEL: 'kimi-k2p7-coding',
|
||||
CCS_OPENAI_PROXY_PASSTHROUGH: 'true',
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.passthrough).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults passthrough to false when CCS_OPENAI_PROXY_PASSTHROUGH is unset', () => {
|
||||
const result = resolveOpenAICompatProfileConfig(
|
||||
'gateway',
|
||||
'/tmp/gateway.settings.json',
|
||||
{
|
||||
ANTHROPIC_BASE_URL: 'https://gateway.example.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'gateway-token',
|
||||
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}
|
||||
);
|
||||
const result = resolveOpenAICompatProfileConfig('gateway', '/tmp/gateway.settings.json', {
|
||||
ANTHROPIC_BASE_URL: 'https://gateway.example.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'gateway-token',
|
||||
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.passthrough).toBeFalsy();
|
||||
|
||||
@@ -56,26 +56,30 @@ describe('Anthropic passthrough URL resolution', () => {
|
||||
).toBe('https://api.kimi.com/coding/v1/models');
|
||||
});
|
||||
|
||||
it('auto-detects Kimi and MiniMax hosts as Anthropic-style', () => {
|
||||
it('auto-detects known Anthropic-style hosts', () => {
|
||||
expect(isAnthropicPassthroughProfile('https://api.kimi.com/coding/')).toBe(true);
|
||||
expect(isAnthropicPassthroughProfile('https://api.kimi.com/coding/v1')).toBe(true);
|
||||
expect(isAnthropicPassthroughProfile('https://api.minimax.com/anthropic')).toBe(true);
|
||||
expect(isAnthropicPassthroughProfile('https://api.anthropic.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('auto-detects base URLs that end in /v1 as Anthropic-style', () => {
|
||||
expect(isAnthropicPassthroughProfile('https://example.test/v1')).toBe(true);
|
||||
expect(isAnthropicPassthroughProfile('https://example.test/v1/')).toBe(true);
|
||||
expect(isAnthropicPassthroughProfile('https://example.test/api/v1')).toBe(true);
|
||||
it('does not treat generic /v1 roots as Anthropic-style passthrough', () => {
|
||||
expect(isAnthropicPassthroughProfile('https://example.test/v1')).toBe(false);
|
||||
expect(isAnthropicPassthroughProfile('https://example.test/v1/')).toBe(false);
|
||||
expect(isAnthropicPassthroughProfile('https://example.test/api/v1')).toBe(false);
|
||||
expect(isAnthropicPassthroughProfile('https://api.openai.com/v1')).toBe(false);
|
||||
expect(isAnthropicPassthroughProfile('https://api.minimax.io/v1')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not auto-detect OpenAI-style base URLs as Anthropic-style', () => {
|
||||
expect(isAnthropicPassthroughProfile('https://api.fireworks.ai/inference')).toBe(false);
|
||||
expect(isAnthropicPassthroughProfile('https://api.openai.com/v1')).toBe(true);
|
||||
// The OpenAI base URL happens to end in /v1, so it is treated as
|
||||
// Anthropic-style. This is acceptable because the OpenAI Chat
|
||||
// Completions endpoint is also exposed under /v1/chat/completions
|
||||
// which works either way; the auto-detect errs on the side of
|
||||
// preserving the user's explicit URL shape.
|
||||
expect(isAnthropicPassthroughProfile('https://api.openai.com/v1')).toBe(false);
|
||||
});
|
||||
|
||||
it('honors explicit force passthrough for unknown mirrors', () => {
|
||||
expect(
|
||||
isAnthropicPassthroughProfile('https://anthropic-mirror.example.test/v1', {
|
||||
forcePassthrough: true,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user