mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-05 00:13:20 +00:00
144 lines
4.4 KiB
TypeScript
144 lines
4.4 KiB
TypeScript
/**
|
|
* Resolve upstream URLs for OpenAI-compat proxy requests.
|
|
*
|
|
* Two modes are supported:
|
|
*
|
|
* 1. Default (OpenAI mode) - appends `/chat/completions` or `/models` to the
|
|
* base URL so requests are routed to the OpenAI-compatible endpoint of the
|
|
* upstream provider.
|
|
*
|
|
* 2. Anthropic passthrough - some providers (e.g. Kimi coding endpoints)
|
|
* expose an Anthropic-compatible `/v1/messages` endpoint and reject
|
|
* OpenAI-format requests. For these profiles we preserve the incoming
|
|
* Anthropic body and forward it directly to the provider's
|
|
* `/v1/messages` endpoint.
|
|
*
|
|
* Passthrough is enabled when:
|
|
* - `CCS_OPENAI_PROXY_PASSTHROUGH=1` is set in the profile env, OR
|
|
* - The base URL is a known Anthropic-style provider host.
|
|
*/
|
|
|
|
function normalizePathname(pathname: string): string {
|
|
const trimmed = pathname.replace(/\/+$/, '');
|
|
return trimmed || '';
|
|
}
|
|
|
|
function ensureSupportedProtocol(parsed: URL): void {
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
throw new Error(`Unsupported upstream protocol: ${parsed.protocol}`);
|
|
}
|
|
}
|
|
|
|
function isAnthropicPassthroughHost(hostname: string): boolean {
|
|
const normalized = hostname.toLowerCase();
|
|
return (
|
|
normalized === 'api.anthropic.com' ||
|
|
normalized.endsWith('.anthropic.com') ||
|
|
normalized === 'api.kimi.com' ||
|
|
normalized.endsWith('.kimi.com')
|
|
);
|
|
}
|
|
|
|
function isOpenRouterHost(hostname: string): boolean {
|
|
const normalized = hostname.toLowerCase();
|
|
return normalized === 'openrouter.ai' || normalized.endsWith('.openrouter.ai');
|
|
}
|
|
|
|
/**
|
|
* Returns true if the profile should pass Anthropic-format requests through
|
|
* directly to the upstream provider, skipping the OpenAI translation.
|
|
*/
|
|
export function isAnthropicPassthroughProfile(
|
|
baseUrl: string,
|
|
options: { forcePassthrough?: boolean } = {}
|
|
): boolean {
|
|
if (options.forcePassthrough) {
|
|
return true;
|
|
}
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(baseUrl);
|
|
} catch {
|
|
return false;
|
|
}
|
|
ensureSupportedProtocol(parsed);
|
|
return isAnthropicPassthroughHost(parsed.hostname);
|
|
}
|
|
|
|
/**
|
|
* Build the upstream URL for either the messages path or the models path.
|
|
* When passthrough is enabled, the messages path is appended verbatim
|
|
* (i.e. `/v1/messages`); otherwise the OpenAI suffix is appended.
|
|
*/
|
|
function buildResolvedUrl(
|
|
baseUrl: string,
|
|
suffix: string,
|
|
options: { passthrough?: boolean } = {}
|
|
): string {
|
|
const parsed = new URL(baseUrl);
|
|
ensureSupportedProtocol(parsed);
|
|
|
|
const pathname = normalizePathname(parsed.pathname);
|
|
|
|
if (options.passthrough) {
|
|
// For Anthropic passthrough, the suffix is always `/v1/messages` or
|
|
// `/v1/models`. If the base URL already ends in `/v1` we drop the
|
|
// duplicated prefix; otherwise we append the suffix verbatim.
|
|
if (pathname.endsWith('/v1') && suffix.startsWith('/v1/')) {
|
|
parsed.pathname = `${pathname}${suffix.slice(3)}`;
|
|
parsed.search = '';
|
|
return parsed.toString();
|
|
}
|
|
parsed.pathname = pathname ? `${pathname}${suffix}` : suffix;
|
|
parsed.search = '';
|
|
return parsed.toString();
|
|
}
|
|
|
|
if (pathname.endsWith(suffix)) {
|
|
return parsed.toString();
|
|
}
|
|
|
|
if (isOpenRouterHost(parsed.hostname) && pathname.endsWith('/api')) {
|
|
parsed.pathname = `${pathname}/v1${suffix}`;
|
|
return parsed.toString();
|
|
}
|
|
|
|
if (pathname.endsWith('/v1') || pathname.endsWith('/api')) {
|
|
parsed.pathname = `${pathname}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
|
|
return parsed.toString();
|
|
}
|
|
|
|
parsed.pathname = pathname ? `${pathname}/v1${suffix}` : `/v1${suffix}`;
|
|
return parsed.toString();
|
|
}
|
|
|
|
/**
|
|
* Resolve the upstream URL for a chat completions request.
|
|
*
|
|
* For OpenAI-compatible providers this resolves to
|
|
* `<base>/v1/chat/completions`. For Anthropic passthrough providers this
|
|
* resolves to `<base>/v1/messages`.
|
|
*/
|
|
export function resolveOpenAIChatCompletionsUrl(
|
|
baseUrl: string,
|
|
options: { passthrough?: boolean } = {}
|
|
): string {
|
|
if (options.passthrough) {
|
|
return buildResolvedUrl(baseUrl, '/v1/messages', { passthrough: true });
|
|
}
|
|
return buildResolvedUrl(baseUrl, '/chat/completions');
|
|
}
|
|
|
|
/**
|
|
* Resolve the upstream URL for the models endpoint.
|
|
*/
|
|
export function resolveOpenAIModelsUrl(
|
|
baseUrl: string,
|
|
options: { passthrough?: boolean } = {}
|
|
): string {
|
|
if (options.passthrough) {
|
|
return buildResolvedUrl(baseUrl, '/v1/models', { passthrough: true });
|
|
}
|
|
return buildResolvedUrl(baseUrl, '/models');
|
|
}
|