Files
ccs/src/proxy/profile-router.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

99 lines
3.1 KiB
TypeScript

import {
inferDroidProviderFromBaseUrl,
resolveDroidProvider,
type DroidProvider,
} from '../targets/droid-provider';
export interface OpenAICompatProfileConfig {
profileName: string;
settingsPath: string;
baseUrl: string;
apiKey: string;
provider: DroidProvider;
insecure?: boolean;
forceOpenAIReasoningModel?: boolean;
/**
* When true, forward Anthropic-format requests to the upstream
* `/v1/messages` endpoint instead of translating to OpenAI chat
* completions. Required for providers that reject OpenAI-format
* requests (e.g. Kimi, Anthropic-API mirrors).
*/
passthrough?: boolean;
model?: string;
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
}
export interface OpenAICompatProfileEnv {
ANTHROPIC_BASE_URL?: string;
ANTHROPIC_AUTH_TOKEN?: string;
ANTHROPIC_API_KEY?: string;
ANTHROPIC_MODEL?: string;
ANTHROPIC_DEFAULT_OPUS_MODEL?: string;
ANTHROPIC_DEFAULT_SONNET_MODEL?: string;
ANTHROPIC_DEFAULT_HAIKU_MODEL?: string;
ANTHROPIC_SMALL_FAST_MODEL?: string;
CCS_DROID_PROVIDER?: string;
CCS_OPENAI_PROXY_INSECURE?: string;
CCS_OPENAI_REASONING_MODEL?: string;
/**
* Force Anthropic passthrough for this profile. When set, the
* proxy forwards incoming `/v1/messages` requests to the
* upstream `/v1/messages` endpoint without translating to
* OpenAI format.
*/
CCS_OPENAI_PROXY_PASSTHROUGH?: string;
}
export function isOpenAICompatProvider(provider: DroidProvider | null): provider is DroidProvider {
return provider === 'openai' || provider === 'generic-chat-completion-api';
}
function isTruthyEnv(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase();
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
}
export function resolveOpenAICompatProfileConfig(
profileName: string,
settingsPath: string,
env: OpenAICompatProfileEnv
): OpenAICompatProfileConfig | null {
const baseUrl = env.ANTHROPIC_BASE_URL?.trim() || '';
const apiKey = env.ANTHROPIC_AUTH_TOKEN?.trim() || env.ANTHROPIC_API_KEY?.trim() || '';
if (!baseUrl || !apiKey) {
return null;
}
const providerFromUrl = inferDroidProviderFromBaseUrl(baseUrl);
const provider = isOpenAICompatProvider(providerFromUrl)
? providerFromUrl
: resolveDroidProvider({
provider: env.CCS_DROID_PROVIDER,
baseUrl,
model: env.ANTHROPIC_MODEL,
});
if (!isOpenAICompatProvider(provider)) {
return null;
}
return {
profileName,
settingsPath,
baseUrl,
apiKey,
provider,
insecure: isTruthyEnv(env.CCS_OPENAI_PROXY_INSECURE),
forceOpenAIReasoningModel: isTruthyEnv(env.CCS_OPENAI_REASONING_MODEL),
passthrough: isTruthyEnv(env.CCS_OPENAI_PROXY_PASSTHROUGH),
model: env.ANTHROPIC_MODEL?.trim() || undefined,
opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL?.trim() || undefined,
sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL?.trim() || undefined,
haikuModel:
env.ANTHROPIC_DEFAULT_HAIKU_MODEL?.trim() ||
env.ANTHROPIC_SMALL_FAST_MODEL?.trim() ||
undefined,
};
}