Files
ccs/tests/unit/proxy/upstream-url.test.ts
T
Sanskar Singh b98b0df084 fix(proxy): add Anthropic passthrough mode for coding-agent-only endpoints
Some providers (e.g. Kimi, Anthropic-API mirrors) reject OpenAI-format
chat-completions requests and/or only accept requests from a recognized
coding-agent User-Agent (e.g. Claude Code, Roo Code, Kilo Code). The
OpenAI-compat proxy previously translated every profile's request to
OpenAI format and overwrote the User-Agent with a fixed sentinel,
which made these providers unreachable.

This change adds an opt-in Anthropic passthrough mode:

- New CCS_OPENAI_PROXY_PASSTHROUGH=1 env var on a profile opts it in.
- The base URL is auto-detected as Anthropic-style for known hosts
  (api.kimi.com, api.minimax.com, api.anthropic.com) or any base URL
  ending in /v1.
- In passthrough mode the proxy forwards the incoming Anthropic body
  verbatim to the upstream /v1/messages endpoint, preserving the
  original User-Agent (or x-stainless-user-agent) so coding-agent
  provider checks pass.
- The Anthropic-format response is streamed back unchanged.

Adds:
- isAnthropicPassthroughProfile() + passthrough option on
  resolveOpenAIChatCompletionsUrl/resolveOpenAIModelsUrl
- CCS_OPENAI_PROXY_PASSTHROUGH env var on OpenAICompatProfileConfig
- readRawBody() helper for the passthrough path
- Preserved User-Agent (or x-stainless-user-agent) on the upstream
  request, falling back to CCS-OpenAI-Compat-Proxy/1.0
- Skip SSE response transformation in passthrough mode (upstream
  already returns Anthropic-format bytes)

Tests:
- 9 new tests in upstream-url.test.ts covering auto-detection and the
  passthrough URL contract
- 2 new tests in profile-router.test.ts covering the env var

Verified end-to-end against api.kimi.com: a request through the
modified proxy returned a real Kimi response (model kimi-k2p7-coding)
with the original claude-cli/2.1.170 User-Agent preserved.
2026-06-13 01:39:49 +05:30

82 lines
3.2 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import {
isAnthropicPassthroughProfile,
resolveOpenAIChatCompletionsUrl,
resolveOpenAIModelsUrl,
} from '../../../src/proxy/upstream-url';
describe('OpenAI-compatible upstream URL resolution', () => {
it('routes current OpenRouter API roots through /api/v1', () => {
expect(resolveOpenAIChatCompletionsUrl('https://openrouter.ai/api/v1')).toBe(
'https://openrouter.ai/api/v1/chat/completions'
);
expect(resolveOpenAIModelsUrl('https://openrouter.ai/api/v1')).toBe(
'https://openrouter.ai/api/v1/models'
);
});
it('repairs legacy OpenRouter /api roots before appending OpenAI endpoints', () => {
expect(resolveOpenAIChatCompletionsUrl('https://openrouter.ai/api')).toBe(
'https://openrouter.ai/api/v1/chat/completions'
);
expect(resolveOpenAIModelsUrl('https://openrouter.ai/api')).toBe(
'https://openrouter.ai/api/v1/models'
);
});
it('does not rewrite non-OpenRouter /api roots', () => {
expect(resolveOpenAIChatCompletionsUrl('https://example.test/api')).toBe(
'https://example.test/api/chat/completions'
);
});
});
describe('Anthropic passthrough URL resolution', () => {
it('resolves to /v1/messages when forcePassthrough is set', () => {
expect(
resolveOpenAIChatCompletionsUrl('https://api.kimi.com/coding/', {
passthrough: true,
})
).toBe('https://api.kimi.com/coding/v1/messages');
});
it('drops a duplicated /v1 prefix when the base URL already ends in /v1', () => {
expect(
resolveOpenAIChatCompletionsUrl('https://api.kimi.com/coding/v1', {
passthrough: true,
})
).toBe('https://api.kimi.com/coding/v1/messages');
});
it('routes /v1/models in passthrough mode', () => {
expect(
resolveOpenAIModelsUrl('https://api.kimi.com/coding/v1', {
passthrough: true,
})
).toBe('https://api.kimi.com/coding/v1/models');
});
it('auto-detects Kimi and MiniMax hosts as Anthropic-style', () => {
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 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.
});
});