Rework router internals and refresh provider adapters

This commit is contained in:
musistudio
2026-07-13 17:39:31 +08:00
parent 96467abe5f
commit 6f3df2f331
46 changed files with 9816 additions and 8616 deletions
+1
View File
@@ -1,6 +1,7 @@
export type AppInfo = {
appConfigDbFile: string;
apiKeysDbFile: string;
chatgptAppPath?: string;
configDir: string;
configFile: string;
dataDir: string;
@@ -0,0 +1,265 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { ChildProcess } from "node:child_process";
import { randomUUID } from "node:crypto";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import type { ApiKeyConfig, AppConfig, GatewayStatus } from "@ccr/core/contracts/app";
import { NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, hasAvailableGatewayModels } from "@ccr/core/contracts/app";
import { backendService } from "@ccr/core/plugins/backend-service";
import { getSystemProxyUrlForProtocol } from "@ccr/core/proxy/system-proxy-fetch";
import { pluginService } from "@ccr/core/plugins/service";
import { proxyService } from "@ccr/core/proxy/service";
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
import { writeCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-writer";
import { closeServer, formatError } from "@ccr/core/gateway/http/io";
import { RawTraceSynchronizer } from "@ccr/core/observability/raw-trace-sync";
import { assertLoopbackCoreHost, endpoint, gatewayNetworkEndpoints, generateCoreGatewayAuthToken, isCoreGatewayHealthy, loopbackCoreHostError, removeManagedCoreGatewayMarker, shouldRunGatewayRuntime, shouldRunUnifiedServer, spawnGatewayProcess, stopPreviousManagedCoreGateway, writeManagedCoreGatewayMarker } from "@ccr/core/gateway/core-runtime/supervisor";
import type { BrowserAutomationMcpIntegration, BrowserWebSearchMcpIntegration, GatewayStopOptions } from "@ccr/core/gateway/internal/shared";
import { GatewayRequestPipeline } from "@ccr/core/gateway/request/pipeline";
import { GatewayHttpRequestHandler } from "@ccr/core/gateway/http/request-handler";
class GatewayService {
private readonly requestHandler = new GatewayHttpRequestHandler({
getBrowserAutomationMcpIntegration: () => this.browserAutomationMcpIntegration,
getConfig: () => this.config,
getPlugin: () => this.plugin,
getStatus: () => ({
coreEndpoint: this.status.coreEndpoint,
coreManagedExternally: this.status.coreManagedExternally,
endpoint: this.status.endpoint,
state: this.status.state
}),
handleRawTraceSync: (request, response) => this.rawTraceSynchronizer.handle(request, response),
proxyRequest: (request, response, path, apiKey) => this.proxyRequest(request, response, path, apiKey)
});
private readonly requestPipeline = new GatewayRequestPipeline({
getBrowserWebSearchMcpIntegration: () => this.browserWebSearchMcpIntegration,
getConfig: () => this.config,
getCoreAuthToken: () => this.coreAuthToken,
getPlugin: () => this.plugin,
getStatus: () => ({ coreEndpoint: this.status.coreEndpoint, endpoint: this.status.endpoint }),
takePendingRawTraceUpdate: (requestId) => this.rawTraceSynchronizer.take(requestId)
});
private browserAutomationMcpIntegration?: BrowserAutomationMcpIntegration;
private browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration;
private child?: ChildProcess;
private config?: AppConfig;
private coreAuthToken = "";
private plugin?: ClaudeCodeRouterPlugin;
private readonly rawTraceSynchronizer = new RawTraceSynchronizer();
private server?: Server;
private status: GatewayStatus = {
coreEndpoint: "",
endpoint: "",
generatedConfigFile: "",
networkEndpoints: [],
state: "stopped"
};
setBrowserWebSearchMcpIntegration(integration: BrowserWebSearchMcpIntegration): void {
this.browserWebSearchMcpIntegration = integration;
}
setBrowserAutomationMcpIntegration(integration: BrowserAutomationMcpIntegration): void {
this.browserAutomationMcpIntegration = integration;
}
async start(config: AppConfig): Promise<GatewayStatus> {
const coreHostError = loopbackCoreHostError(config.gateway.coreHost);
if (coreHostError) {
this.status = {
...this.getStatus(),
lastError: coreHostError,
state: "error"
};
return this.status;
}
await this.stop();
this.config = config;
this.coreAuthToken = generateCoreGatewayAuthToken();
this.plugin = new ClaudeCodeRouterPlugin(config);
this.status = {
coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort),
endpoint: endpoint(config.gateway.host, config.gateway.port),
generatedConfigFile: config.gateway.generatedConfigFile,
networkEndpoints: gatewayNetworkEndpoints(config.gateway.host, config.gateway.port),
state: "starting"
};
try {
await pluginService.start(config);
const shouldRunServer = shouldRunUnifiedServer(config) || pluginService.hasGatewayRoutes();
const shouldRunGateway = shouldRunGatewayRuntime(config);
if (shouldRunGateway && !hasAvailableGatewayModels(config)) {
throw new Error(NO_AVAILABLE_GATEWAY_MODELS_MESSAGE);
}
if (!shouldRunServer) {
await pluginService.stop();
await backendService.stopAll();
this.coreAuthToken = "";
this.status = {
...this.status,
state: "stopped"
};
return this.status;
}
await this.listen(config);
if (this.server) {
const proxyStatus = await proxyService.attach(config, this.server);
if (proxyStatus.state === "error" && !config.gateway.enabled) {
throw new Error(proxyStatus.lastError || "Proxy service failed to start.");
}
}
if (shouldRunGateway) {
await writeCoreGatewayConfig(config, this.rawTraceSynchronizer.token, this.coreAuthToken, this.browserWebSearchMcpIntegration);
await stopPreviousManagedCoreGateway(config, this.status.coreEndpoint);
if (await isCoreGatewayHealthy(this.status.coreEndpoint)) {
throw new Error(`Core gateway endpoint is already in use: ${this.status.coreEndpoint}`);
}
await proxyService.refreshUpstreamProxyFromCurrentSystem();
const runtimeId = randomUUID();
const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https", config);
this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId, this.coreAuthToken);
const managedChild = this.child;
writeManagedCoreGatewayMarker(config, this.child, runtimeId);
this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.on("exit", (code, signal) => {
void this.handleCoreGatewayExit(managedChild, code, signal);
});
}
this.status = {
...this.status,
coreManagedExternally: this.status.coreManagedExternally,
lastStartedAt: new Date().toISOString(),
pid: this.child?.pid,
state: "running"
};
return this.status;
} catch (error) {
await this.stop();
this.status = {
...this.status,
lastError: formatError(error),
state: "error"
};
return this.status;
}
}
async stop(options: GatewayStopOptions = {}): Promise<GatewayStatus> {
const child = this.child;
const config = this.config;
this.child = undefined;
this.coreAuthToken = "";
if (child && !child.killed) {
child.kill();
}
removeManagedCoreGatewayMarker(config);
const server = this.server;
this.server = undefined;
if (server) {
await closeServer(server);
}
await proxyService.stop(options.proxyRestoreTimeoutMs);
await pluginService.stop();
await backendService.stopAll();
await this.browserWebSearchMcpIntegration?.stopBrowserWebSearchMcpServers().catch((error) => {
console.warn(`[gateway] Failed to stop browser web search MCP: ${formatError(error)}`);
});
await this.browserAutomationMcpIntegration?.stopBrowserAutomationMcpServer().catch((error) => {
console.warn(`[gateway] Failed to stop browser automation MCP: ${formatError(error)}`);
});
this.status = {
...this.status,
coreManagedExternally: undefined,
pid: undefined,
state: "stopped"
};
return this.getStatus();
}
getStatus(): GatewayStatus {
return {
...this.status,
networkEndpoints: this.config
? gatewayNetworkEndpoints(this.config.gateway.host, this.config.gateway.port)
: this.status.networkEndpoints
};
}
updateConfig(config: AppConfig): void {
assertLoopbackCoreHost(config.gateway.coreHost);
this.config = config;
this.plugin = new ClaudeCodeRouterPlugin(config);
proxyService.updateConfig(config);
this.status = {
...this.status,
coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort),
endpoint: endpoint(config.gateway.host, config.gateway.port),
generatedConfigFile: config.gateway.generatedConfigFile,
networkEndpoints: gatewayNetworkEndpoints(config.gateway.host, config.gateway.port)
};
}
private async listen(config: AppConfig): Promise<void> {
this.server = createServer((request, response) => {
if (proxyService.shouldHandleHttpRequest(request)) {
void proxyService.handleHttpRequest(request, response).catch((error) => {
response.writeHead(502, { "content-type": "application/json" });
response.end(JSON.stringify({ error: { message: formatError(error) } }));
});
return;
}
void this.handleRequest(request, response).catch((error) => {
response.writeHead(502, { "content-type": "application/json" });
response.end(JSON.stringify({ error: { message: formatError(error) } }));
});
});
await new Promise<void>((resolve, reject) => {
this.server?.once("error", reject);
this.server?.listen(config.gateway.port, config.gateway.host, () => {
this.server?.off("error", reject);
resolve();
});
});
}
private async handleCoreGatewayExit(child: ChildProcess, code: number | null, signal: NodeJS.Signals | null): Promise<void> {
if (this.child !== child || this.status.state === "stopped") {
return;
}
removeManagedCoreGatewayMarker(this.config);
this.status = {
...this.status,
coreManagedExternally: undefined,
lastError: `Core gateway exited with ${signal ?? code ?? "unknown status"}`,
pid: undefined,
state: "error"
};
}
private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
return this.requestHandler.handleRequest(request, response);
}
private async proxyRequest(request: IncomingMessage, response: ServerResponse, path: string, apiKey?: ApiKeyConfig): Promise<void> {
return this.requestPipeline.proxyRequest(request, response, path, apiKey);
}
}
export const gatewayService = new GatewayService();
@@ -0,0 +1,132 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app";
import { loadPersistedApiKeys } from "@ccr/core/config/api-key-store";
import { formatError, readAuthToken, readRemoteControlQueryAuthToken, sendJson } from "@ccr/core/gateway/http/io";
import { estimateLimitUsage, limitRules, readWindowCounter } from "@ccr/core/gateway/limits/window-limiter";
import type { ApiKeyAuthorizationResult, ApiKeyLimitRule, ApiKeyLimitUsage } from "@ccr/core/gateway/internal/shared";
const persistedApiKeyCacheTtlMs = 1000;
let persistedApiKeyCache: { loadedAt: number; values: ApiKeyConfig[] } | undefined;
export async function authorize(
request: IncomingMessage,
response: ServerResponse,
config: AppConfig
): Promise<ApiKeyAuthorizationResult> {
let apiKeys = await configuredApiKeys(config);
if (apiKeys.length === 0) {
sendJson(response, 403, {
error: {
message: "CCR API key is not initialized. Save a gateway API key or restart CCR to generate one."
}
});
return { ok: false };
}
const token = readAuthToken(request.headers) || readRemoteControlQueryAuthToken(request);
let apiKey = token ? apiKeys.find((item) => item.key === token) : undefined;
if (!apiKey && token) {
apiKeys = await configuredApiKeys(config, { refresh: true });
apiKey = apiKeys.find((item) => item.key === token);
}
if (apiKey) {
if (isApiKeyExpired(apiKey)) {
sendJson(response, 401, { error: { message: "API key is expired." } });
return { ok: false };
}
return { ok: true, apiKey };
}
sendJson(response, 401, { error: { message: token ? "Invalid API key." : "API key is missing." } });
return { ok: false };
}
export function reserveApiKeyLimits(
apiKey: ApiKeyConfig | undefined,
request: IncomingMessage,
response: ServerResponse,
requestBody: Buffer
): boolean {
if (!apiKey?.limits) return true;
const usage = estimateLimitUsage(request.method ?? "GET", requestBody);
const rules = apiKeyLimitRules(apiKey, usage);
const now = Date.now();
const checks = rules.map((rule) => {
const windowStart = Math.floor(now / rule.windowMs) * rule.windowMs;
return {
counterKey: ["api-key", apiKey.id, rule.name, rule.metric, rule.windowMs, windowStart].join("|"),
rule,
windowStart
};
});
for (const check of checks) {
const counter = readWindowCounter(check.counterKey, check.windowStart, check.rule.windowMs, now);
if (counter.value + check.rule.requested > check.rule.limit) {
sendJson(response, 429, {
error: {
code: "rate_limit_exceeded",
message: `API key ${check.rule.name} limit exceeded.`,
details: {
limit: check.rule.limit,
limit_name: check.rule.name,
metric: check.rule.metric,
requested: check.rule.requested,
used: counter.value,
window_ms: check.rule.windowMs
}
}
});
return false;
}
}
for (const check of checks) {
readWindowCounter(check.counterKey, check.windowStart, check.rule.windowMs, now).value += check.rule.requested;
}
return true;
}
async function configuredApiKeys(config: AppConfig, options: { refresh?: boolean } = {}): Promise<ApiKeyConfig[]> {
const persistedApiKeys = await loadPersistedApiKeysCached(options);
const values = [
...persistedApiKeys,
...(Array.isArray(config.APIKEYS) ? config.APIKEYS : []),
...(config.APIKEY ? [{ createdAt: new Date(0).toISOString(), id: "legacy", key: config.APIKEY }] : [])
];
const seen = new Set<string>();
const result: ApiKeyConfig[] = [];
for (const value of values) {
const key = value?.key?.trim();
if (!key || seen.has(key)) continue;
seen.add(key);
result.push({ ...value, key });
}
return result;
}
async function loadPersistedApiKeysCached(options: { refresh?: boolean } = {}): Promise<ApiKeyConfig[]> {
const now = Date.now();
if (!options.refresh && persistedApiKeyCache && now - persistedApiKeyCache.loadedAt < persistedApiKeyCacheTtlMs) {
return persistedApiKeyCache.values;
}
try {
const values = await loadPersistedApiKeys();
persistedApiKeyCache = { loadedAt: now, values };
return values;
} catch (error) {
console.warn(`[gateway] Failed to load persisted API keys: ${formatError(error)}`);
return [];
}
}
function isApiKeyExpired(apiKey: ApiKeyConfig): boolean {
if (!apiKey.expiresAt) return false;
const expiresAt = Date.parse(apiKey.expiresAt);
return Number.isFinite(expiresAt) && expiresAt <= Date.now();
}
function apiKeyLimitRules(apiKey: ApiKeyConfig, usage: ApiKeyLimitUsage): ApiKeyLimitRule[] {
return limitRules(apiKey.limits, usage);
}
@@ -308,7 +308,7 @@ function resolveBuiltInAgentRouteDecision(
if (!builtInAgentRouteMatches(request, config, agent)) {
continue;
}
const target = modelRegistry.resolve(resolveBuiltInAgentRouteTarget(config, agent));
const target = modelRegistry.resolve(resolveBuiltInAgentRouteTarget(request, config, agent));
if (!target) {
continue;
}
@@ -337,22 +337,43 @@ function builtInAgentRouteMatches(
if (config.Router.builtInRules?.[agent]?.enabled === false) {
return false;
}
if (!resolveBuiltInAgentProfile(config, agent)) {
if (!resolveBuiltInAgentProfile(request, config, agent)) {
return false;
}
const userAgent = readRequestHeader(request.headers, "user-agent")?.toLowerCase() ?? "";
return userAgent.includes(builtInAgentUserAgentNeedle(agent));
}
function resolveBuiltInAgentProfile(config: AppConfig, agent: RouterBuiltInAgentRuleId) {
function resolveBuiltInAgentProfile(
request: MutableRequestLike,
config: AppConfig,
agent: RouterBuiltInAgentRuleId
) {
if (config.profile.enabled === false) {
return undefined;
}
return config.profile.profiles.find((profile) => profile.enabled && profile.agent === agent);
const authenticatedApiKeyId = readRequestHeader(request.headers, "x-auth-api-key-id")?.trim();
if (!authenticatedApiKeyId) {
return undefined;
}
return config.profile.profiles.find((profile) =>
profile.enabled &&
profile.agent === agent &&
profileApiKeyId(profile.id || profile.name || profile.agent) === authenticatedApiKeyId
);
}
function resolveBuiltInAgentRouteTarget(config: AppConfig, agent: RouterBuiltInAgentRuleId): string | undefined {
return normalizeRouteSelector(resolveBuiltInAgentProfile(config, agent)?.model);
function resolveBuiltInAgentRouteTarget(
request: MutableRequestLike,
config: AppConfig,
agent: RouterBuiltInAgentRuleId
): string | undefined {
return normalizeRouteSelector(resolveBuiltInAgentProfile(request, config, agent)?.model);
}
function profileApiKeyId(value: string): string {
const profileId = value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
return `profile:${profileId || "profile"}`;
}
function builtInAgentUserAgentNeedle(agent: RouterBuiltInAgentRuleId): string {
@@ -0,0 +1,413 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol } from "@ccr/core/contracts/app";
import { codexDefaultBaseUrl, readCodexAuth, readGrokAuth, resolveGrokAuth } from "@ccr/core/agents/local-providers/service";
import { grokAccessTokenExpired } from "@ccr/core/agents/local-providers/grok";
import { pluginService } from "@ccr/core/plugins/service";
import { normalizeRouteSelector, providerRuntimeId } from "@ccr/core/routing/model-registry";
import { isRecord, stringValue } from "@ccr/core/gateway/internal/value";
import { fusionBuiltinToolArtifacts, fusionToolFallbackMcpServer, normalizeFusionWebSearchProfileToolName, toolHubMcpServer, withCodexCompatibleVirtualModelProfiles, withFusionVirtualModelAliases, withFusionWebSearchToolInstructions } from "@ccr/core/mcp/fusion-config";
import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery";
import { activeProviderCredentials, inferProtocol, normalizedProviderCapabilities, normalizeProviderProtocol, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCredentialInternalName, providerProtocolForClientProtocol, sortProviderCredentialsForConfig, toCoreGatewayProviders } from "@ccr/core/providers/runtime-topology";
import { buildRawTraceConfig } from "@ccr/core/observability/raw-trace-sync";
import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor";
import { claudeCodeOauthBetaHeader, claudeCodeOauthRequiredBeta, coreGatewayAuthHeader, coreGatewayAuthTokenEnv } from "@ccr/core/gateway/internal/shared";
import type { BrowserWebSearchMcpIntegration, CoreGatewayProvider } from "@ccr/core/gateway/internal/shared";
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
import { isLocalClaudeCodeOauthProviderPlugin, mergeAnthropicBetaValues } from "@ccr/core/providers/oauth-plugin";
import { resolveConfiguredProviderModelSelector, resolveUniqueConfiguredProviderModelSelector } from "@ccr/core/routing/model-resolution";
export async function compileCoreGatewayConfig(
config: AppConfig,
rawTraceSyncToken: string,
coreAuthToken: string,
browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration
): Promise<Record<string, unknown>> {
const pluginCoreGatewayConfig = pluginService.getCoreGatewayConfig();
const configuredProviderPlugins = normalizeClaudeCodeOauthProviderPlugins([
...(config.providerPlugins ?? []).filter(providerPluginEnabled),
...pluginService.getCoreProviderPlugins().filter(providerPluginEnabled)
]);
const providerPlugins = await withGrokOauthRuntimeDefaults(withCodexOauthRuntimeDefaults(configuredProviderPlugins));
const codexOauthProviderNames = codexOauthLocalProviderNames(providerPlugins);
const virtualModelProfiles = normalizeCoreGatewayVirtualModelProfiles(withCodexCompatibleVirtualModelProfiles(withFusionVirtualModelAliases([
...(config.virtualModelProfiles ?? []),
...pluginService.getVirtualModelProfiles()
])), config);
const coreEndpoint = endpoint(config.gateway.coreHost, config.gateway.corePort);
const builtinToolArtifacts = await fusionBuiltinToolArtifacts(virtualModelProfiles, coreEndpoint, coreAuthToken, browserWebSearchMcpIntegration);
const providers = [
...config.Providers
.flatMap((provider) => toCoreGatewayProviders(withCodexOauthProviderBaseUrl(provider, codexOauthProviderNames)))
.filter((provider): provider is CoreGatewayProvider => Boolean(provider)),
...builtinToolArtifacts.providers
];
const pluginAgentConfig = isRecord(pluginCoreGatewayConfig.agent) ? pluginCoreGatewayConfig.agent : {};
const pluginMcpServers = Array.isArray(pluginAgentConfig.mcpServers) ? pluginAgentConfig.mcpServers : [];
const externalMcpServers = [
...pluginMcpServers,
...(config.agent?.mcpServers ?? []),
...(config.toolHub?.mcpServers ?? [])
];
const toolHubServer = toolHubMcpServer(config, externalMcpServers);
const mcpServers = [
...builtinToolArtifacts.mcpServers,
...(toolHubServer ? [toolHubServer] : externalMcpServers)
];
const fallbackMcpServer = fusionToolFallbackMcpServer(virtualModelProfiles, [
...builtinToolArtifacts.mcpServers,
...externalMcpServers
]);
if (fallbackMcpServer) {
mcpServers.push(fallbackMcpServer);
}
return {
...pluginCoreGatewayConfig,
auth: {
enabled: true,
mode: "static_api_key",
required: true,
staticApiKeys: {
keyBearerOnly: false,
keyEnv: coreGatewayAuthTokenEnv,
keyHeader: coreGatewayAuthHeader
}
},
billing: {
enabled: true
},
billingQueue: {
enabled: false
},
billingWebhook: {
enabled: false
},
bodyLimitBytes: 50 * 1024 * 1024,
host: config.gateway.coreHost,
mcpGateway: {
enabled: false
},
port: config.gateway.corePort,
upstreamTimeoutMs: Number(config.API_TIMEOUT_MS) || 0,
agent: {
...pluginAgentConfig,
mcpServers
},
rawTrace: buildRawTraceConfig(config, rawTraceSyncToken),
providerPlugins,
providers,
virtualModelProfiles
};
}
function providerPluginEnabled(plugin: unknown): boolean {
return !isRecord(plugin) || plugin.enabled !== false;
}
export function normalizeCoreGatewayVirtualModelProfiles(profiles: unknown[], config: AppConfig): unknown[] {
return profiles.map((profile) => normalizeCoreGatewayVirtualModelProfile(profile, config));
}
function normalizeCoreGatewayVirtualModelProfile(profile: unknown, config: AppConfig): unknown {
if (!isRecord(profile)) {
return profile;
}
let nextProfile: Record<string, unknown> | undefined;
const baseModel = isRecord(profile.baseModel) ? profile.baseModel : undefined;
const fixedModel = stringValue(baseModel?.fixedModel);
const rewrittenFixedModel = fixedModel
? rewriteModelSelectorForCoreGatewayProfile(fixedModel, config, "anthropic_messages")
: undefined;
if (baseModel && rewrittenFixedModel && rewrittenFixedModel !== fixedModel) {
nextProfile = {
...profile,
baseModel: {
...baseModel,
fixedModel: rewrittenFixedModel
}
};
}
const sourceProfile = nextProfile ?? profile;
const metadata = isRecord(sourceProfile.metadata) ? sourceProfile.metadata : undefined;
const fusionVision = isRecord(metadata?.fusionVision) ? metadata.fusionVision : undefined;
const visionBaseUrl = stringValue(fusionVision?.baseUrl);
const visionSelectorField = stringValue(fusionVision?.modelSelector) ? "modelSelector" : stringValue(fusionVision?.model) ? "model" : undefined;
const visionSelector = visionSelectorField ? stringValue(fusionVision?.[visionSelectorField]) : undefined;
const rewrittenVisionSelector = fusionVision && !visionBaseUrl && visionSelector
? rewriteModelSelectorForCoreGatewayProfile(visionSelector, config, "openai_chat_completions")
: undefined;
if (metadata && fusionVision && visionSelectorField && rewrittenVisionSelector && rewrittenVisionSelector !== visionSelector) {
nextProfile = {
...sourceProfile,
metadata: {
...metadata,
fusionVision: {
...fusionVision,
[visionSelectorField]: rewrittenVisionSelector
}
}
};
}
const profileAfterVision = nextProfile ?? profile;
const profileAfterWebSearchToolName = normalizeFusionWebSearchProfileToolName(profileAfterVision) ?? profileAfterVision;
return withFusionWebSearchToolInstructions(profileAfterWebSearchToolName) ?? profileAfterWebSearchToolName;
}
function rewriteModelSelectorForCoreGatewayProfile(
model: string,
config: AppConfig,
clientProtocol: GatewayProviderProtocol
): string | undefined {
const normalized = normalizeRouteSelector(model);
if (!normalized) {
return undefined;
}
const publicModel = resolveGatewayPublicModelId(normalized, config) ?? normalized;
const selector =
resolveConfiguredProviderModelSelector(publicModel, config) ??
resolveUniqueConfiguredProviderModelSelector(publicModel, config);
if (!selector) {
return publicModel;
}
const providerName = coreGatewayProviderSelectorName(selector.provider, clientProtocol);
return providerName ? `${providerName}/${selector.model}` : publicModel;
}
function coreGatewayProviderSelectorName(
provider: GatewayProviderConfig,
clientProtocol: GatewayProviderProtocol
): string | undefined {
const capability = providerCapabilityForClientProtocol(provider, clientProtocol);
const explicitCapabilities = normalizedProviderCapabilities(provider);
const protocol = capability?.type ?? (explicitCapabilities.length === 0 ? providerProtocolForClientProtocol(provider, clientProtocol) : undefined);
if (!protocol) {
return undefined;
}
const credentials = sortProviderCredentialsForConfig(activeProviderCredentials(provider));
if (credentials.length > 0) {
return providerCredentialInternalName(provider, protocol, credentials[0]);
}
return capability ? providerCapabilityInternalName(provider, protocol) : providerRuntimeId(provider);
}
function withCodexOauthRuntimeDefaults(providerPlugins: unknown[]): unknown[] {
const codexAuth = readCodexAuth();
return providerPlugins.map((plugin) => {
if (!isLocalCodexOauthProviderPlugin(plugin)) {
return plugin;
}
const codexOauth = plugin.codexOauth;
const nextCodexOauth = {
...codexOauth,
...(!hasOwn(codexOauth, "accountId") && !hasOwn(codexOauth, "account_id") && codexAuth?.accountId
? { accountId: codexAuth.accountId }
: {})
};
const nextPlugin: Record<string, unknown> = {
...plugin,
codexOauth: nextCodexOauth,
request: withCodexBackendRequestTransform(plugin.request)
};
if (codexAuth?.isFedrampAccount) {
const currentAuth = isRecord(plugin.auth) ? plugin.auth : {};
const currentHeaders = isRecord(currentAuth.headers) ? currentAuth.headers : {};
nextPlugin.auth = {
...currentAuth,
headers: {
...currentHeaders,
"X-OpenAI-Fedramp": "true"
}
};
}
return nextPlugin;
});
}
async function withGrokOauthRuntimeDefaults(providerPlugins: unknown[]): Promise<unknown[]> {
const grokAuth = await resolveGrokAuth().catch(() => readGrokAuth());
if (!grokAuth?.accessToken || grokAccessTokenExpired(grokAuth)) {
return providerPlugins;
}
return providerPlugins.map((plugin) => {
if (!isLocalGrokOauthProviderPlugin(plugin)) {
return plugin;
}
const currentAuth = isRecord(plugin.auth) ? plugin.auth : {};
const currentHeaders = isRecord(currentAuth.headers) ? currentAuth.headers : {};
return {
...plugin,
auth: {
...currentAuth,
headers: {
...currentHeaders,
authorization: `Bearer ${grokAuth.accessToken}`
}
}
};
});
}
function codexOauthLocalProviderNames(providerPlugins: unknown[]): Set<string> {
const names = new Set<string>();
for (const plugin of providerPlugins) {
if (!isLocalCodexOauthProviderPlugin(plugin)) {
continue;
}
addProviderNameVariants(names, stringValue(plugin.providerName));
}
return names;
}
function withCodexOauthProviderBaseUrl(
provider: GatewayProviderConfig,
codexOauthProviderNames: Set<string>
): GatewayProviderConfig {
if (!codexOauthProviderNames.has(provider.name)) {
return provider;
}
const protocol =
normalizeProviderProtocol(provider.type) ??
normalizeProviderProtocol(provider.provider) ??
inferProtocol(provider);
if (protocol !== "openai_responses") {
return provider;
}
const capabilities = Array.isArray(provider.capabilities)
? provider.capabilities.map((capability) => {
const capabilityProtocol = normalizeProviderProtocol(capability.type);
if (capabilityProtocol !== "openai_responses") {
return capability;
}
return {
...capability,
baseUrl: codexDefaultBaseUrl
};
})
: provider.capabilities;
return {
...provider,
api_base_url: codexDefaultBaseUrl,
baseUrl: codexDefaultBaseUrl,
baseurl: codexDefaultBaseUrl,
capabilities
};
}
function isLocalCodexOauthProviderPlugin(value: unknown): value is Record<string, unknown> & { codexOauth: Record<string, unknown> } {
if (!isRecord(value) || !isRecord(value.codexOauth)) {
return false;
}
const key = stringValue(value.key)?.toLowerCase() ?? "";
return key.startsWith("ccr-local-agent-") && key.includes("codex-oauth");
}
export function normalizeClaudeCodeOauthProviderPlugins(providerPlugins: unknown[]): unknown[] {
return providerPlugins.map((plugin) => {
if (!isLocalClaudeCodeOauthProviderPlugin(plugin)) {
return plugin;
}
const auth = isRecord(plugin.auth) ? plugin.auth : {};
const headers = isRecord(auth.headers) ? auth.headers : {};
const configuredBeta = Object.entries(headers)
.find(([name]) => name.trim().toLowerCase() === claudeCodeOauthBetaHeader)?.[1];
const defaultBeta = mergeAnthropicBetaValues(
configuredAnthropicBetaDefault(configuredBeta),
claudeCodeOauthRequiredBeta
);
const normalizedHeaders = Object.fromEntries(
Object.entries(headers).filter(([name]) => name.trim().toLowerCase() !== claudeCodeOauthBetaHeader)
);
return {
...plugin,
auth: {
...auth,
headers: {
...normalizedHeaders,
[claudeCodeOauthBetaHeader]: {
default: defaultBeta,
from: `request.headers.${claudeCodeOauthBetaHeader}`
}
}
}
};
});
}
function configuredAnthropicBetaDefault(value: unknown): string | undefined {
if (typeof value === "string") {
return value;
}
if (!isRecord(value)) {
return undefined;
}
return stringValue(value.default);
}
function isLocalGrokOauthProviderPlugin(value: unknown): value is Record<string, unknown> {
if (!isRecord(value)) {
return false;
}
const key = stringValue(value.key)?.toLowerCase() ?? "";
return key.startsWith("ccr-local-agent-") && key.includes("grok-cli-oauth");
}
function withCodexBackendRequestTransform(request: unknown): Record<string, unknown> {
const currentRequest = isRecord(request) ? request : {};
const bodyRemove = Array.isArray(currentRequest.bodyRemove)
? currentRequest.bodyRemove.map((item) => stringValue(item)).filter((item): item is string => Boolean(item))
: [];
return {
...currentRequest,
bodyRemove: uniqueStrings([...bodyRemove, "max_output_tokens"])
};
}
function addProviderNameVariants(names: Set<string>, providerName: string | undefined): void {
if (!providerName) {
return;
}
names.add(providerName);
const capabilitySeparatorIndex = providerName.indexOf("::");
if (capabilitySeparatorIndex > 0) {
names.add(providerName.slice(0, capabilitySeparatorIndex));
}
}
function hasOwn(value: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(value, key);
}
@@ -0,0 +1,45 @@
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import type { AppConfig } from "@ccr/core/contracts/app";
import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler";
import { assertLoopbackCoreHost } from "@ccr/core/gateway/core-runtime/supervisor";
import {
privateDirMode,
privateFileMode,
type BrowserWebSearchMcpIntegration
} from "@ccr/core/gateway/internal/shared";
export async function writeCoreGatewayConfig(
config: AppConfig,
rawTraceSyncToken: string,
coreAuthToken: string,
browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration
): Promise<void> {
assertLoopbackCoreHost(config.gateway.coreHost);
mkdirSync(dirname(config.gateway.generatedConfigFile), {
mode: privateDirMode,
recursive: true
});
const payload = await compileCoreGatewayConfig(
config,
rawTraceSyncToken,
coreAuthToken,
browserWebSearchMcpIntegration
);
writePrivateTextFile(
config.gateway.generatedConfigFile,
`${JSON.stringify(payload, null, 2)}\n`
);
}
function writePrivateTextFile(file: string, content: string): void {
writeFileSync(file, content, { encoding: "utf8", mode: privateFileMode });
if (process.platform !== "win32") {
try {
chmodSync(file, privateFileMode);
} catch {
// Best effort for filesystems that do not support chmod.
}
}
}
@@ -0,0 +1,525 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import { spawn, type ChildProcess } from "node:child_process";
import { randomBytes } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { networkInterfaces } from "node:os";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join as pathJoin, resolve as pathResolve } from "node:path";
import type { AppConfig, GatewayNetworkEndpoint } from "@ccr/core/contracts/app";
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import { isRecord, numberValue, stringValue } from "@ccr/core/gateway/internal/value";
import { formatError, readHeader } from "@ccr/core/gateway/http/io";
import { coreGatewayAuthHeader, coreGatewayAuthTokenEnv, gatewayEntryOverrideEnv, gatewayPackageCandidates, gatewayRuntimeMarkerFile, requireFromHere } from "@ccr/core/gateway/internal/shared";
import type { CoreGatewayHealth, ManagedGatewayRuntimeMarker } from "@ccr/core/gateway/internal/shared";
import { delay } from "@ccr/core/gateway/internal/clock";
export function spawnGatewayProcess(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string, coreAuthToken: string): ChildProcess {
const gatewayEntry = resolveGatewayEntry();
const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile(config, upstreamProxyUrl) : undefined;
const env = createGatewayProcessEnv(config, upstreamProxyUrl, runtimeId, coreAuthToken);
const args = proxyPreloadFile ? ["--require", proxyPreloadFile, gatewayEntry] : [gatewayEntry];
return spawn(process.execPath, args, {
cwd: dirname(config.gateway.generatedConfigFile),
env,
stdio: ["ignore", "pipe", "pipe"]
});
}
function resolveGatewayEntry(): string {
const override = process.env[gatewayEntryOverrideEnv]?.trim();
if (override) {
const entry = pathResolve(override);
if (!existsSync(entry)) {
throw new Error(`${gatewayEntryOverrideEnv} points to a missing gateway entry: ${entry}`);
}
return entry;
}
const bundledEntry = resolveBundledGatewayEntry();
if (bundledEntry) {
return bundledEntry;
}
for (const packageName of gatewayPackageCandidates) {
try {
return requireFromHere.resolve(packageName);
} catch {
// Try the next known package name.
}
}
return requireFromHere.resolve(gatewayPackageCandidates[0]);
}
function resolveBundledGatewayEntry(): string | undefined {
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
return [
pathJoin(__dirname, "next-ai-gateway.js"),
...(resourcesPath
? [
pathJoin(resourcesPath, "app.asar", "dist", "main", "next-ai-gateway.js"),
pathJoin(resourcesPath, "app", "dist", "main", "next-ai-gateway.js")
]
: [])
].find((candidate) => existsSync(candidate));
}
function resolveUndiciProxyAgentModule(): string {
const bundled = resolveBundledUndiciProxyAgentModule();
if (bundled) {
return bundled;
}
try {
return requireFromHere.resolve("undici");
} catch (error) {
throw new Error(`Unable to resolve undici ProxyAgent module for gateway proxy preload: ${formatError(error)}`);
}
}
function resolveBundledUndiciProxyAgentModule(): string | undefined {
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
return [
pathJoin(__dirname, "undici-proxy-agent.js"),
...(resourcesPath
? [
pathJoin(resourcesPath, "app.asar", "dist", "main", "undici-proxy-agent.js"),
pathJoin(resourcesPath, "app", "dist", "main", "undici-proxy-agent.js")
]
: [])
].find((candidate) => existsSync(candidate));
}
function createGatewayProcessEnv(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string, coreAuthToken: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
AUTH_ENABLED: "true",
AUTH_MODE: "static_api_key",
AUTH_REQUIRED: "true",
AUTH_STATIC_API_KEY_BEARER_ONLY: "false",
AUTH_STATIC_API_KEY_ENV: coreGatewayAuthTokenEnv,
AUTH_STATIC_API_KEY_HEADER: coreGatewayAuthHeader,
CCR_GATEWAY_RUNTIME_ID: runtimeId,
[coreGatewayAuthTokenEnv]: coreAuthToken,
ELECTRON_RUN_AS_NODE: "1",
GATEWAY_CONFIG_PATH: config.gateway.generatedConfigFile,
HOST: config.gateway.coreHost,
PORT: String(config.gateway.corePort)
};
const noProxy = mergeNoProxy(env.NO_PROXY || env.no_proxy, [
"127.0.0.1",
"localhost",
"::1",
config.gateway.host,
config.gateway.coreHost
]);
env.NO_PROXY = noProxy;
env.no_proxy = noProxy;
if (!upstreamProxyUrl) {
return env;
}
env.HTTP_PROXY = upstreamProxyUrl;
env.HTTPS_PROXY = upstreamProxyUrl;
env.ALL_PROXY = upstreamProxyUrl;
env.http_proxy = upstreamProxyUrl;
env.https_proxy = upstreamProxyUrl;
env.all_proxy = upstreamProxyUrl;
env.CCR_UPSTREAM_PROXY_URL = upstreamProxyUrl;
env.CCR_UNDICI_MODULE = resolveUndiciProxyAgentModule();
return env;
}
function writeGatewayProxyPreloadFile(config: AppConfig, upstreamProxyUrl: string): string {
const file = pathJoin(dirname(config.gateway.generatedConfigFile), "gateway-proxy-preload.cjs");
writeFileSync(
file,
[
"\"use strict\";",
"const up = process.env.CCR_UPSTREAM_PROXY_URL;",
"const um = process.env.CCR_UNDICI_MODULE;",
"if (up && um) {",
" const { ProxyAgent } = require(um);",
" const agent = new ProxyAgent(up);",
" const realFetch = globalThis.fetch.bind(globalThis);",
" const raw = (process.env.NO_PROXY || process.env.no_proxy || '').toLowerCase();",
" const byp = raw.split(',').map((s) => s.trim()).filter(Boolean);",
" const norm = (h) => h.replace(/^\\[/, '').replace(/\\]$/, '').replace(/\\.$/, '');",
" const isLP = (h) => h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0:0:0:0:0:0:0:1' || h === '0.0.0.0' || h.startsWith('127.');",
" const shouldBypass = (input) => {",
" let h;",
" try {",
" const u = typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input && input.url ? input.url : String(input));",
" h = norm(u.hostname);",
" } catch { return true; }",
" if (!h) return false;",
" if (isLP(h)) return true;",
" return byp.some((p) => {",
" if (p === '*') return true;",
" const s = p.split(':');",
" const ph = norm(s[0]);",
" if (s.length === 2 && s[1]) {",
" if (h !== ph) return false;",
" try { return new URL(input).port === s[1]; } catch { return false; }",
" }",
" if (ph.startsWith('*.')) return h.endsWith(ph.slice(1));",
" if (ph.startsWith('.')) return h.endsWith(ph) || h === ph.slice(1);",
" return h === ph;",
" });",
" };",
" const patched = function(input, init) {",
" if (init && init.dispatcher) return realFetch(input, init);",
" if (shouldBypass(input)) return realFetch(input, init);",
" return realFetch(input, Object.assign({}, init, { dispatcher: agent }));",
" };",
" if (Object.getOwnPropertyDescriptor(globalThis, 'fetch')?.writable) {",
" globalThis.fetch = patched;",
" }",
"}"
].join("\n"),
"utf8"
);
return file;
}
function mergeNoProxy(current: string | undefined, values: string[]): string {
const merged = new Set<string>();
for (const value of [...(current || "").split(","), ...values]) {
const trimmed = value.trim();
if (trimmed) {
merged.add(trimmed);
}
}
return [...merged].join(",");
}
export function endpoint(host: string, port: number): string {
const endpointHost = host === "0.0.0.0" ? "127.0.0.1" : host;
return `http://${endpointHost}:${port}`;
}
export function gatewayNetworkEndpoints(host: string, port: number): GatewayNetworkEndpoint[] {
const normalizedHost = normalizeBindHost(host);
const lanAddresses = physicalLanAddresses();
const addresses = isWildcardBindHost(normalizedHost)
? lanAddresses
: lanAddresses.filter((entry) => entry.address === normalizedHost);
return addresses.map((entry) => ({
address: entry.address,
endpoint: endpoint(entry.address, port),
interfaceName: entry.interfaceName
}));
}
function physicalLanAddresses(): Array<{ address: string; interfaceName: string }> {
const seen = new Set<string>();
const result: Array<{ address: string; interfaceName: string }> = [];
for (const [interfaceName, entries] of Object.entries(networkInterfaces())) {
if (!entries || isVirtualNetworkInterface(interfaceName)) {
continue;
}
for (const entry of entries) {
if (entry.internal || entry.family !== "IPv4" || !isPrivateIpv4(entry.address)) {
continue;
}
const key = `${interfaceName}:${entry.address}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push({ address: entry.address, interfaceName });
}
}
return result.sort((left, right) =>
left.interfaceName.localeCompare(right.interfaceName) ||
left.address.localeCompare(right.address, undefined, { numeric: true })
);
}
function normalizeBindHost(host: string): string {
return host.trim().replace(/^\[|\]$/g, "").toLowerCase();
}
function isLoopbackBindHost(host: string): boolean {
const normalized = normalizeBindHost(host).replace(/\.$/, "");
return normalized === "localhost" ||
normalized === "127.0.0.1" ||
normalized === "::1" ||
normalized === "0:0:0:0:0:0:0:1" ||
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized);
}
function isWildcardBindHost(host: string): boolean {
return host === "" || host === "0.0.0.0" || host === "::" || host === "::0";
}
function isPrivateIpv4(address: string): boolean {
const parts = address.split(".").map((part) => Number(part));
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
return false;
}
return parts[0] === 10 ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
(parts[0] === 192 && parts[1] === 168);
}
function isVirtualNetworkInterface(interfaceName: string): boolean {
const normalized = interfaceName.toLowerCase();
return [
/^lo\d*$/,
/^awdl\d*$/,
/^llw\d*$/,
/^utun\d*$/,
/^gif\d*$/,
/^stf\d*$/,
/^bridge\d*$/,
/^br-/,
/^docker/,
/^veth/,
/^vmnet/,
/^vbox/,
/^tun\d*$/,
/^tap\d*$/,
/^wg\d*$/,
/\bloopback\b/,
/\bvirtual\b/,
/\bvirtualbox\b/,
/\bvmware\b/,
/\bhyper-v\b/,
/\bvethernet\b/,
/\bwsl\b/,
/\btunnel\b/,
/\btailscale\b/,
/\bzerotier\b/,
/\bwireguard\b/,
/\bhamachi\b/,
/\bparallels\b/,
/\bvpn\b/
].some((pattern) => pattern.test(normalized));
}
export async function stopPreviousManagedCoreGateway(config: AppConfig, coreEndpoint: string): Promise<void> {
const marker = readManagedCoreGatewayMarker(config);
const markerRuntimeId = stringValue(marker?.runtimeId);
const pid = numberValue(marker?.pid);
if (!markerRuntimeId || !pid) {
return;
}
const health = await readCoreGatewayHealth(coreEndpoint);
if (health?.runtimeId !== markerRuntimeId) {
return;
}
if (!isProcessAlive(pid)) {
removeManagedCoreGatewayMarker(config);
return;
}
try {
process.kill(pid, "SIGTERM");
} catch {
removeManagedCoreGatewayMarker(config);
return;
}
if (await waitForCoreGatewayStop(coreEndpoint)) {
removeManagedCoreGatewayMarker(config);
return;
}
try {
process.kill(pid, "SIGKILL");
} catch {
// Process may have exited between the health check and SIGKILL.
}
await waitForCoreGatewayStop(coreEndpoint);
removeManagedCoreGatewayMarker(config);
}
function readManagedCoreGatewayMarker(config: AppConfig): ManagedGatewayRuntimeMarker | undefined {
const file = managedCoreGatewayMarkerPath(config);
if (!existsSync(file)) {
return undefined;
}
try {
const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
return isRecord(parsed) ? parsed : undefined;
} catch {
return undefined;
}
}
export function writeManagedCoreGatewayMarker(config: AppConfig, child: ChildProcess, runtimeId: string): void {
if (!child.pid) {
return;
}
try {
writeFileSync(
managedCoreGatewayMarkerPath(config),
`${JSON.stringify(
{
generatedConfigFile: config.gateway.generatedConfigFile,
gatewayEntry: resolveGatewayEntry(),
pid: child.pid,
runtimeId,
startedAt: new Date().toISOString()
},
null,
2
)}\n`,
"utf8"
);
} catch (error) {
console.warn(`[gateway] Failed to write gateway runtime marker: ${formatError(error)}`);
}
}
export function removeManagedCoreGatewayMarker(config: AppConfig | undefined): void {
if (!config) {
return;
}
try {
rmSync(managedCoreGatewayMarkerPath(config), { force: true });
} catch (error) {
console.warn(`[gateway] Failed to remove gateway runtime marker: ${formatError(error)}`);
}
}
function managedCoreGatewayMarkerPath(config: AppConfig): string {
return pathJoin(dirname(config.gateway.generatedConfigFile), gatewayRuntimeMarkerFile);
}
async function waitForCoreGatewayStop(coreEndpoint: string): Promise<boolean> {
for (let index = 0; index < 20; index += 1) {
if (!(await isCoreGatewayHealthy(coreEndpoint))) {
return true;
}
await delay(100);
}
return false;
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
export function assertLoopbackCoreHost(host: string): void {
const error = loopbackCoreHostError(host);
if (error) {
throw new Error(error);
}
}
export function loopbackCoreHostError(host: string): string | undefined {
const normalized = host.trim().toLowerCase();
return normalized === "127.0.0.1" || normalized === "::1"
? undefined
: "Core gateway host must be 127.0.0.1 or ::1.";
}
export function generateCoreGatewayAuthToken(): string {
return randomBytes(32).toString("base64url");
}
export async function isCoreGatewayHealthy(coreEndpoint: string): Promise<boolean> {
const health = await readCoreGatewayHealth(coreEndpoint);
return health?.status === "ok";
}
async function readCoreGatewayHealth(coreEndpoint: string): Promise<CoreGatewayHealth | undefined> {
if (!coreEndpoint) {
return undefined;
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 500);
try {
const healthUrl = new URL("/health", coreEndpoint);
const response = await fetchWithSystemProxy(healthUrl, { signal: controller.signal });
if (!response.ok) {
return undefined;
}
const body = await response.json().catch(() => undefined);
if (!isRecord(body)) {
return undefined;
}
return {
runtimeId: stringValue(body.runtimeId),
status: stringValue(body.status)
};
} catch {
return undefined;
} finally {
clearTimeout(timer);
}
}
export function shouldRunUnifiedServer(config: AppConfig): boolean {
return config.gateway.enabled || config.proxy.enabled;
}
export function shouldRunGatewayRuntime(config: AppConfig): boolean {
return config.gateway.enabled || (config.proxy.enabled && config.proxy.mode === "gateway");
}
export function shouldServeGatewayRequest(config: AppConfig, request: IncomingMessage): boolean {
if (config.gateway.enabled) {
return true;
}
return config.proxy.enabled && config.proxy.mode === "gateway" && readHeader(request.headers["x-ccr-proxy-mode"]) === "gateway";
}
export function applyCors(response: ServerResponse, config?: AppConfig): void {
const origin = config ? endpoint(config.gateway.host, config.gateway.port) : "*";
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, Last-Event-ID, Anthropic-Version, Anthropic-Beta, Mcp-Session-Id, MCP-Protocol-Version");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
response.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
}
@@ -0,0 +1,460 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { IncomingHttpHeaders } from "node:http";
import { Readable, Transform } from "node:stream";
import type { AppConfig } from "@ccr/core/contracts/app";
import { normalizeRouteSelector } from "@ccr/core/routing/model-registry";
import { isRecord, rawStringValue, stringValue } from "@ccr/core/gateway/internal/value";
import { readHeader } from "@ccr/core/gateway/http/io";
import { codexPatchBridgeInstructionText, codexPatchBridgeShellToolGuidance, virtualApplyPatchLarkGrammar, virtualApplyPatchToolName } from "@ccr/core/gateway/internal/shared";
import { parseJsonObjectSafe } from "@ccr/core/gateway/http/body";
import { requestProtocolForPath } from "@ccr/core/routing/protocol-endpoints";
export function prepareCodexApplyPatchBridgeRequest(input: {
body?: Buffer;
config: AppConfig;
headers: IncomingHttpHeaders;
method: string;
path: string;
routedModel?: string;
}): { body: Buffer; diagnostic: string } | undefined {
if (!codexApplyPatchBridgeEnabled(input.config, input.headers, input.method, input.path)) {
return undefined;
}
const parsedBody = parseJsonObjectSafe(input.body);
if (!parsedBody) {
return undefined;
}
const model = input.routedModel || stringValue(parsedBody.model);
if (!codexPatchBridgeModelEligible(model)) {
return undefined;
}
const transformed = transformCodexApplyPatchBridgeRequestBody(parsedBody);
if (!transformed.changed) {
return undefined;
}
return {
body: Buffer.from(`${JSON.stringify(transformed.body)}\n`, "utf8"),
diagnostic: `${model ?? "unknown"}:${transformed.changedParts.join(",")}`
};
}
export function transformCodexApplyPatchBridgeRequestBody(body: Record<string, unknown>): {
body: Record<string, unknown>;
changed: boolean;
changedParts: string[];
} {
const next = { ...body };
const changedParts: string[] = [];
const tools = transformCodexApplyPatchBridgeTools(body.tools);
if (tools.changed) {
next.tools = tools.value;
changedParts.push("tools");
const instructions = transformCodexApplyPatchBridgeInstructions(body.instructions);
if (instructions.changed) {
next.instructions = instructions.value;
changedParts.push("instructions");
}
const input = transformCodexApplyPatchBridgeInput(body.input);
if (input.changed) {
next.input = input.value;
changedParts.push("input");
}
}
return {
body: next,
changed: changedParts.length > 0,
changedParts
};
}
function transformCodexApplyPatchBridgeTools(value: unknown): { value: unknown; changed: boolean } {
if (!Array.isArray(value)) {
return { value, changed: false };
}
const hasApplyPatchTool = value.some((tool) => isRecord(tool) && tool.type === "custom" && tool.name === "apply_patch");
if (!hasApplyPatchTool) {
return { value, changed: false };
}
let changed = false;
const tools = value.map((tool) => {
if (isRecord(tool) && tool.type === "custom" && tool.name === "apply_patch") {
changed = true;
return virtualApplyPatchToolSpec();
}
const shellTool = transformCodexPatchBridgeShellTool(tool);
if (shellTool.changed) {
changed = true;
return shellTool.value;
}
return tool;
});
return { value: tools, changed };
}
function transformCodexApplyPatchBridgeInstructions(value: unknown): { value: unknown; changed: boolean } {
const text = rawStringValue(value);
if (text === undefined) {
return value === undefined
? { value: codexPatchBridgeInstructionText, changed: true }
: { value, changed: false };
}
if (text.includes(codexPatchBridgeInstructionText)) {
return { value, changed: false };
}
return {
value: `${text.trimEnd()}\n\n${codexPatchBridgeInstructionText}`,
changed: true
};
}
function transformCodexPatchBridgeShellTool(value: unknown): { value: unknown; changed: boolean } {
if (!isRecord(value) || value.type !== "function") {
return { value, changed: false };
}
const name = stringValue(value.name);
if (name !== "exec_command" && name !== "write_stdin") {
return { value, changed: false };
}
let changed = false;
const next: Record<string, unknown> = { ...value };
const description = rawStringValue(value.description) ?? "";
if (!description.includes(codexPatchBridgeShellToolGuidance)) {
next.description = description
? `${description} ${codexPatchBridgeShellToolGuidance}`
: codexPatchBridgeShellToolGuidance;
changed = true;
}
if (name === "exec_command") {
const parameters = transformCodexPatchBridgeExecCommandParameters(value.parameters);
if (parameters.changed) {
next.parameters = parameters.value;
changed = true;
}
}
return { value: changed ? next : value, changed };
}
function transformCodexPatchBridgeExecCommandParameters(value: unknown): { value: unknown; changed: boolean } {
if (!isRecord(value) || !isRecord(value.properties) || !isRecord(value.properties.cmd)) {
return { value, changed: false };
}
const cmd = value.properties.cmd;
const description = rawStringValue(cmd.description) ?? "";
if (description.includes(codexPatchBridgeShellToolGuidance)) {
return { value, changed: false };
}
return {
value: {
...value,
properties: {
...value.properties,
cmd: {
...cmd,
description: description
? `${description} ${codexPatchBridgeShellToolGuidance}`
: codexPatchBridgeShellToolGuidance
}
}
},
changed: true
};
}
function transformCodexApplyPatchBridgeInput(value: unknown): { value: unknown; changed: boolean } {
if (!Array.isArray(value)) {
return { value, changed: false };
}
const applyPatchCallIds = new Set<string>();
for (const item of value) {
if (isRecord(item) && item.type === "custom_tool_call" && item.name === "apply_patch") {
const callId = stringValue(item.call_id);
if (callId) {
applyPatchCallIds.add(callId);
}
}
}
let changed = false;
const items = value.map((item) => {
const transformed = transformCodexApplyPatchBridgeInputItem(item, applyPatchCallIds);
changed ||= transformed.changed;
return transformed.value;
});
return { value: items, changed };
}
function transformCodexApplyPatchBridgeInputItem(value: unknown, applyPatchCallIds: Set<string>): { value: unknown; changed: boolean } {
if (!isRecord(value)) {
return { value, changed: false };
}
if (value.type === "custom_tool_call" && value.name === "apply_patch") {
const { input: patchInput, name: _name, type: _type, ...rest } = value;
return {
value: {
...rest,
type: "function_call",
name: virtualApplyPatchToolName,
arguments: JSON.stringify({ patch: rawStringValue(patchInput) ?? "" })
},
changed: true
};
}
if (
value.type === "custom_tool_call_output" &&
(applyPatchCallIds.has(stringValue(value.call_id) ?? "") || value.name === "apply_patch")
) {
const { name: _name, type: _type, ...rest } = value;
return {
value: {
...rest,
type: "function_call_output"
},
changed: true
};
}
return { value, changed: false };
}
function virtualApplyPatchToolSpec(): Record<string, unknown> {
return {
type: "function",
name: virtualApplyPatchToolName,
description: [
"Edit files by returning exactly one complete apply_patch patch.",
"The patch field must be raw patch grammar text starting with *** Begin Patch and ending with *** End Patch.",
"Do not wrap the patch in JSON, markdown fences, shell commands, cat, sed, perl, or python.",
"The patch field must match this Lark grammar:",
virtualApplyPatchLarkGrammar
].join("\n\n"),
strict: true,
parameters: {
type: "object",
additionalProperties: false,
required: ["patch"],
properties: {
patch: {
type: "string",
description: [
"Raw apply_patch grammar text matching this Lark grammar:",
virtualApplyPatchLarkGrammar
].join("\n\n")
}
}
}
};
}
function codexApplyPatchBridgeEnabled(config: AppConfig, headers: IncomingHttpHeaders, method: string, path: string): boolean {
const codexRule = config.Router.builtInRules?.codex;
return (method || "GET").toUpperCase() === "POST" &&
requestProtocolForPath(path) === "openai_responses" &&
isCodexUserAgent(headers) &&
codexRule?.enabled !== false;
}
function isCodexUserAgent(headers: IncomingHttpHeaders): boolean {
return readHeader(headers["user-agent"])?.toLowerCase().includes("codex") ?? false;
}
function codexPatchBridgeModelEligible(model: string | undefined): boolean {
const modelName = modelNameForPatchBridge(model);
return Boolean(modelName) && !modelName.toLowerCase().includes("gpt");
}
function modelNameForPatchBridge(model: string | undefined): string {
const normalized = normalizeRouteSelector(model) ?? "";
const slashIndex = normalized.lastIndexOf("/");
return slashIndex >= 0 ? normalized.slice(slashIndex + 1) : normalized;
}
export function codexApplyPatchBridgeResponseStream(input: Readable, headers: Headers): Readable {
const contentType = headers.get("content-type")?.toLowerCase() ?? "";
if (contentType.includes("text/event-stream")) {
return input.pipe(new Transform({
transform(chunk, _encoding, callback) {
transformSseChunk(this, chunk);
callback();
},
flush(callback) {
flushSseTransform(this);
callback();
}
}));
}
if (contentType.includes("application/json")) {
const chunks: Buffer[] = [];
return input.pipe(new Transform({
transform(chunk, _encoding, callback) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
callback();
},
flush(callback) {
const raw = Buffer.concat(chunks).toString("utf8");
try {
const parsed = JSON.parse(raw);
const transformed = transformCodexApplyPatchBridgeResponseValue(parsed);
this.push(Buffer.from(`${JSON.stringify(transformed.value)}\n`, "utf8"));
} catch {
this.push(Buffer.from(raw, "utf8"));
}
callback();
}
}));
}
return input;
}
export function transformCodexApplyPatchBridgeResponseValue(value: unknown): { value: unknown; changed: boolean } {
if (!isRecord(value)) {
return { value, changed: false };
}
let changed = false;
const next = { ...value };
if (isRecord(value.item)) {
const item = transformVirtualApplyPatchFunctionCall(value.item, value.type === "response.output_item.added");
if (item.changed) {
next.item = item.value;
changed = true;
}
}
if (Array.isArray(value.output)) {
const output = transformCodexApplyPatchBridgeResponseItems(value.output);
if (output.changed) {
next.output = output.value;
changed = true;
}
}
if (isRecord(value.response) && Array.isArray(value.response.output)) {
const output = transformCodexApplyPatchBridgeResponseItems(value.response.output);
if (output.changed) {
next.response = {
...value.response,
output: output.value
};
changed = true;
}
}
const item = transformVirtualApplyPatchFunctionCall(next, false);
if (item.changed) {
return item;
}
return { value: next, changed };
}
function transformCodexApplyPatchBridgeResponseItems(items: unknown[]): { value: unknown[]; changed: boolean } {
let changed = false;
const value = items.map((item) => {
const transformed = isRecord(item)
? transformVirtualApplyPatchFunctionCall(item, false)
: { value: item, changed: false };
changed ||= transformed.changed;
return transformed.value;
});
return { value, changed };
}
function transformVirtualApplyPatchFunctionCall(item: Record<string, unknown>, allowEmptyInput: boolean): { value: unknown; changed: boolean } {
if (item.type !== "function_call" || item.name !== virtualApplyPatchToolName) {
return { value: item, changed: false };
}
const patch = patchInputFromVirtualApplyPatchArguments(item.arguments);
if (patch === undefined && !allowEmptyInput) {
return { value: item, changed: false };
}
const { arguments: _arguments, name: _name, type: _type, ...rest } = item;
return {
value: {
...rest,
type: "custom_tool_call",
name: "apply_patch",
input: patch ?? ""
},
changed: true
};
}
function patchInputFromVirtualApplyPatchArguments(value: unknown): string | undefined {
if (isRecord(value)) {
return rawStringValue(value.patch);
}
const text = rawStringValue(value);
if (text === undefined) {
return undefined;
}
try {
const parsed = JSON.parse(text);
return isRecord(parsed) ? rawStringValue(parsed.patch) : undefined;
} catch {
return undefined;
}
}
function transformSseChunk(stream: Transform, chunk: Buffer | string): void {
const state = stream as Transform & { __ccrCodexPatchBridgeSsePending?: string };
state.__ccrCodexPatchBridgeSsePending = (state.__ccrCodexPatchBridgeSsePending ?? "") + chunk.toString();
while (state.__ccrCodexPatchBridgeSsePending) {
const match = /\r?\n\r?\n/.exec(state.__ccrCodexPatchBridgeSsePending);
if (!match || match.index === undefined) {
break;
}
const block = state.__ccrCodexPatchBridgeSsePending.slice(0, match.index);
const delimiter = match[0];
state.__ccrCodexPatchBridgeSsePending = state.__ccrCodexPatchBridgeSsePending.slice(match.index + delimiter.length);
stream.push(transformCodexApplyPatchBridgeSseEvent(block) + delimiter);
}
}
function flushSseTransform(stream: Transform): void {
const state = stream as Transform & { __ccrCodexPatchBridgeSsePending?: string };
if (state.__ccrCodexPatchBridgeSsePending) {
stream.push(transformCodexApplyPatchBridgeSseEvent(state.__ccrCodexPatchBridgeSsePending));
state.__ccrCodexPatchBridgeSsePending = "";
}
}
export function transformCodexApplyPatchBridgeSseEvent(block: string): string {
const lines = block.split(/\r?\n/g);
const data = lines
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, ""))
.join("\n");
if (!data || data === "[DONE]") {
return block;
}
try {
const parsed = JSON.parse(data);
const transformed = transformCodexApplyPatchBridgeResponseValue(parsed);
if (!transformed.changed) {
return block;
}
const event = stringValue((transformed.value as Record<string, unknown>).type) || stringValue(parsed.type);
return [
event ? `event: ${event}` : undefined,
`data: ${JSON.stringify(transformed.value)}`
].filter(Boolean).join("\n");
} catch {
return block;
}
}
@@ -0,0 +1,205 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { AppConfig } from "@ccr/core/contracts/app";
import { isRecord, stringValue } from "@ccr/core/gateway/internal/value";
import { parseJsonObject } from "@ccr/core/gateway/http/io";
import type { CursorOpenAICompatContext, CursorOpenAICompatPreparation } from "@ccr/core/gateway/internal/shared";
let warnedMissingCursorOpenAICompatContext = false;
export function prepareCursorOpenAICompatChatBody(
config: AppConfig,
client: string | undefined,
method: string,
path: string,
requestBody: Buffer
): CursorOpenAICompatPreparation | undefined {
if ((method || "GET").toUpperCase() !== "POST" || !isOpenAICompatChatCompletionsPath(path) || client !== "Cursor") {
return undefined;
}
let body: Record<string, unknown>;
try {
body = parseJsonObject(requestBody);
} catch {
return undefined;
}
if (!isSimplifiedCursorOpenAICompatChat(body)) {
return undefined;
}
const context = readCursorOpenAICompatContext(config);
let changed = false;
if (context.systemPrompt) {
body.messages = [
{ content: context.systemPrompt, role: "system" },
...(Array.isArray(body.messages) ? body.messages : [])
];
changed = true;
}
if (context.tools.length > 0) {
body.tools = context.tools;
changed = true;
}
if (context.toolChoice !== undefined && context.tools.length > 0) {
body.tool_choice = context.toolChoice;
changed = true;
}
if (!changed) {
if (!warnedMissingCursorOpenAICompatContext) {
warnedMissingCursorOpenAICompatContext = true;
console.warn(
"[gateway] Cursor sent an OpenAI-compatible chat request with only user messages and no system/tools. " +
"Configure plugins[].id=\"cursor-proxy\" config.systemPrompt/config.tools to inject fallback context, " +
"or route Cursor native Agent traffic through the proxy."
);
}
return { diagnostic: "simplified-missing-context" };
}
return {
body: Buffer.from(`${JSON.stringify(body)}\n`, "utf8"),
diagnostic: "fallback-injected"
};
}
function isOpenAICompatChatCompletionsPath(path: string): boolean {
return path === "/chat/completions" ||
path === "/v1/chat/completions" ||
path.endsWith("/chat/completions");
}
function isSimplifiedCursorOpenAICompatChat(body: Record<string, unknown>): boolean {
if (body.system !== undefined || body.systemPrompt !== undefined || body.instructions !== undefined) {
return false;
}
if (Array.isArray(body.tools) && body.tools.length > 0) {
return false;
}
if (!Array.isArray(body.messages) || body.messages.length === 0) {
return false;
}
return body.messages.every((message) =>
isRecord(message) &&
stringValue(message.role)?.toLowerCase() === "user"
);
}
function readCursorOpenAICompatContext(config: AppConfig): CursorOpenAICompatContext {
const plugin = config.plugins.find((item) => item.enabled !== false && item.id === "cursor-proxy");
const pluginConfig = isRecord(plugin?.config) ? plugin.config : {};
return {
systemPrompt:
stringValue(pluginConfig.systemPrompt) ||
stringValue(pluginConfig.openaiSystemPrompt) ||
stringValue(pluginConfig.defaultSystemPrompt),
toolChoice: normalizeCursorToolChoice(
pluginConfig.toolChoice ?? pluginConfig.openaiToolChoice ?? pluginConfig.defaultToolChoice
),
tools: normalizeCursorTools(pluginConfig.tools ?? pluginConfig.openaiTools ?? pluginConfig.defaultTools)
};
}
function normalizeCursorTools(value: unknown): unknown[] {
if (Array.isArray(value)) {
return value.map(normalizeCursorTool).filter((tool): tool is Record<string, unknown> => Boolean(tool));
}
if (isRecord(value)) {
if (Array.isArray(value.tools) || isRecord(value.tools)) {
return normalizeCursorTools(value.tools);
}
return Object.entries(value)
.map(([name, item]) => normalizeCursorTool(isRecord(item) ? { ...item, name: stringValue(item.name) || name } : { description: stringValue(item), name }))
.filter((tool): tool is Record<string, unknown> => Boolean(tool));
}
return [];
}
function normalizeCursorTool(value: unknown): Record<string, unknown> | undefined {
if (!isRecord(value)) {
return undefined;
}
const type = stringValue(value.type);
if (type && type.toLowerCase().startsWith("web_search")) {
return { ...value, type };
}
const fn = isRecord(value.function) ? value.function : value;
const name =
stringValue(fn.name) ||
stringValue(value.name) ||
stringValue(value.toolName) ||
stringValue(value.functionName);
if (!name) {
return undefined;
}
return {
function: compactRecord({
description: stringValue(fn.description) || stringValue(value.description),
name,
parameters: normalizeCursorToolParameters(
fn.parameters ??
value.parameters ??
fn.input_schema ??
value.input_schema ??
fn.inputSchema ??
value.inputSchema ??
fn.schema ??
value.schema
)
}),
type: "function"
};
}
function normalizeCursorToolParameters(value: unknown): Record<string, unknown> {
if (isRecord(value)) {
return value;
}
if (typeof value === "string") {
try {
const parsed = JSON.parse(value) as unknown;
if (isRecord(parsed)) {
return parsed;
}
} catch {
// Fall through to an empty object schema.
}
}
return { properties: {}, type: "object" };
}
function normalizeCursorToolChoice(value: unknown): unknown {
if (typeof value === "string" && value.trim()) {
const normalized = value.trim().toLowerCase();
if (normalized === "auto" || normalized === "none" || normalized === "required") {
return normalized;
}
return { function: { name: value.trim() }, type: "function" };
}
if (!isRecord(value)) {
return undefined;
}
const type = stringValue(value.type);
if (type && ["auto", "none", "required"].includes(type.toLowerCase())) {
return type.toLowerCase();
}
const fn = isRecord(value.function) ? value.function : value;
const name = stringValue(fn.name) || stringValue(value.name) || stringValue(value.toolName);
return name ? { function: { name }, type: "function" } : undefined;
}
function compactRecord(value: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}
@@ -0,0 +1,526 @@
import type { AppConfig, GatewayProviderProtocol } from "@ccr/core/contracts/app";
import { isRecord, numberValue, stringListValue, stringValue } from "@ccr/core/gateway/internal/value";
import { normalizeCoreGatewayVirtualModelProfiles } from "@ccr/core/gateway/core-runtime/config-compiler";
import { fusionModelNameFromSelector, readFusionWebSearchConfig, withCodexCompatibleVirtualModelProfiles, withFusionVirtualModelAliases } from "@ccr/core/mcp/fusion-config";
import type { AnthropicWebSearchProtocolContext, BrowserWebSearchMcpIntegration, BrowserWebSearchProtocolRecord, ClaudeCodeWebSearchContinuationContext, HostedWebSearchProtocolContext } from "@ccr/core/gateway/internal/shared";
import { clampNumber, uniqueStrings } from "@ccr/core/gateway/internal/collections";
import { queryMatchScore } from "@ccr/core/gateway/features/hosted-web-search/evidence";
function hasAnthropicHostedWebSearchTool(tools: unknown): boolean {
if (!Array.isArray(tools)) {
return false;
}
return tools.some(isAnthropicHostedWebSearchTool);
}
export function hasHostedWebSearchDeclaration(body: Record<string, unknown>, protocol: GatewayProviderProtocol): boolean {
if (protocol === "anthropic_messages") {
return hasAnthropicHostedWebSearchTool(body.tools);
}
if (protocol === "openai_chat_completions" || protocol === "openai_responses") {
return hasOpenAiHostedWebSearchDeclaration(body);
}
if (protocol === "gemini_generate_content") {
return hasGeminiHostedWebSearchTool(body.tools);
}
return false;
}
function hasOpenAiHostedWebSearchDeclaration(body: Record<string, unknown>): boolean {
if (body.web_search_options !== undefined || body.webSearchOptions !== undefined) {
return true;
}
return Array.isArray(body.tools) && body.tools.some(isOpenAiHostedWebSearchTool);
}
function hasGeminiHostedWebSearchTool(tools: unknown): boolean {
if (!Array.isArray(tools)) {
return false;
}
return tools.some((tool) => {
if (!isRecord(tool)) {
return false;
}
if (tool.google_search !== undefined || tool.googleSearch !== undefined || tool.google_search_retrieval !== undefined || tool.googleSearchRetrieval !== undefined) {
return true;
}
return false;
});
}
export function isAnthropicHostedWebSearchTool(tool: unknown): boolean {
if (!isRecord(tool)) {
return false;
}
return anthropicHostedWebSearchType(stringValue(tool.type));
}
export function isOpenAiHostedWebSearchTool(tool: unknown): boolean {
if (!isRecord(tool)) {
return false;
}
return openAiHostedWebSearchType(stringValue(tool.type));
}
export function openAiToolChoiceNamesWebSearch(value: unknown): boolean {
if (typeof value === "string") {
return openAiHostedWebSearchType(value);
}
if (!isRecord(value)) {
return false;
}
return openAiHostedWebSearchType(stringValue(value.type));
}
function anthropicHostedWebSearchType(value: string | undefined): boolean {
const normalized = normalizedToolProtocolName(value);
return normalized === "web_search" || normalized === "web_search_20250305";
}
function openAiHostedWebSearchType(value: string | undefined): boolean {
const normalized = normalizedToolProtocolName(value);
return normalized === "web_search" ||
normalized === "web_search_preview" ||
normalized.startsWith("web_search_preview_");
}
function normalizedToolProtocolName(value: string | undefined): string {
return value?.trim().toLowerCase().replace(/[-.]/g, "_") ?? "";
}
function readAnthropicWebSearchMaxUses(tools: unknown): number | undefined {
if (!Array.isArray(tools)) {
return undefined;
}
const tool = tools.find((item) => isRecord(item) && stringValue(item.type)?.toLowerCase() === "web_search_20250305");
return isRecord(tool) ? numberValue(tool.max_uses ?? tool.maxUses) : undefined;
}
export function readHostedWebSearchMaxUses(body: Record<string, unknown>, protocol: GatewayProviderProtocol): number | undefined {
if (protocol === "anthropic_messages") {
return readAnthropicWebSearchMaxUses(body.tools);
}
if (protocol === "openai_chat_completions" || protocol === "openai_responses") {
const tool = Array.isArray(body.tools) ? body.tools.find(isOpenAiHostedWebSearchTool) : undefined;
return isRecord(tool) ? numberValue(tool.max_uses ?? tool.maxUses) : undefined;
}
return undefined;
}
export function extractHostedWebSearchQueryHint(body: Record<string, unknown>, protocol: GatewayProviderProtocol): string | undefined {
if (protocol === "anthropic_messages") {
return extractAnthropicWebSearchQueryHint(body);
}
if (protocol === "openai_chat_completions") {
return normalizedWebSearchQueryHintFromParts(textPartsFromOpenAiChatMessages(body.messages));
}
if (protocol === "openai_responses") {
return normalizedWebSearchQueryHintFromParts(textPartsFromOpenAiResponsesInput(body.input));
}
if (protocol === "gemini_generate_content") {
return normalizedWebSearchQueryHintFromParts(textPartsFromGeminiContents(body.contents));
}
return undefined;
}
export function extractAnthropicWebSearchQueryHint(body: Record<string, unknown>): string | undefined {
const userTexts = Array.isArray(body.messages)
? body.messages.flatMap((message) => {
if (!isRecord(message) || stringValue(message.role) !== "user") {
return [];
}
return textPartsFromAnthropicContent(message.content);
})
: [];
return normalizedWebSearchQueryHintFromParts(userTexts);
}
export function extractClaudeCodeWebSearchToolResultQuery(body: Record<string, unknown>): string | undefined {
for (const text of claudeCodeWebSearchToolResultTexts(body)) {
const quoted = /Web search results for query:\s*"([^"]+)"/i.exec(text);
if (quoted?.[1]) {
return normalizedWebSearchQueryHint(quoted[1]);
}
const unquoted = /Web search results for query:\s*([^\n]+)/i.exec(text);
if (unquoted?.[1]) {
return normalizedWebSearchQueryHint(unquoted[1].replace(/^["']|["']$/g, ""));
}
}
return undefined;
}
function normalizedWebSearchQueryHintFromParts(parts: string[]): string | undefined {
const candidates = parts
.map((part) => part.trim())
.filter(Boolean);
for (const candidate of [...candidates].reverse()) {
const explicit = extractExplicitWebSearchQuery(candidate);
if (explicit) {
return normalizedWebSearchQueryHint(explicit);
}
}
for (const candidate of [...candidates].reverse()) {
if (isRuntimeContextText(candidate)) {
continue;
}
return normalizedWebSearchQueryHint(stripSearchIntentPrefix(candidate));
}
return normalizedWebSearchQueryHint(candidates.join("\n"));
}
function normalizedWebSearchQueryHint(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const joined = value.trim();
if (!joined) {
return undefined;
}
return joined.trim().slice(0, 500);
}
function extractExplicitWebSearchQuery(value: string): string | undefined {
const explicit = /perform\s+a\s+web\s+search\s+for\s+the\s+query:\s*([\s\S]+)$/i.exec(value.trim());
return normalizedWebSearchQueryHint(explicit?.[1]);
}
function stripSearchIntentPrefix(value: string): string {
const trimmed = value.trim();
const match = /^(?:请)?(?:帮我)?(?:搜索|查询|查一下|帮我查一下|搜一下)\s*[:]?\s*([\s\S]+)$/i.exec(trimmed);
return (match?.[1] || trimmed).trim();
}
function isRuntimeContextText(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
if (/^<(?:environment_context|permissions instructions|collaboration_mode|skills_instructions|plugins_instructions|apps_instructions)>/i.test(trimmed)) {
return true;
}
return (
trimmed.includes("<workspace_roots>") ||
trimmed.includes("<permission_profile") ||
trimmed.includes("<filesystem>") ||
trimmed.includes("<current_date>") ||
trimmed.includes("<writable_roots>")
);
}
function textPartsFromAnthropicContent(content: unknown): string[] {
if (typeof content === "string") {
return [content];
}
if (!Array.isArray(content)) {
return [];
}
return content.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []);
}
export function claudeCodeWebSearchToolResultTexts(body: Record<string, unknown>): string[] {
if (!Array.isArray(body.messages)) {
return [];
}
const lastMessage = body.messages.at(-1);
if (!isRecord(lastMessage) || stringValue(lastMessage.role) !== "user" || !Array.isArray(lastMessage.content)) {
return [];
}
const latestToolResults = lastMessage.content.filter((part) => isRecord(part) && stringValue(part.type) === "tool_result");
if (latestToolResults.length === 0) {
return [];
}
const webSearchToolUseIds = new Set<string>();
for (let index = body.messages.length - 2; index >= 0; index -= 1) {
const message = body.messages[index];
if (!isRecord(message) || stringValue(message.role) !== "assistant" || !Array.isArray(message.content)) {
continue;
}
for (const part of message.content) {
if (!isRecord(part) || stringValue(part.type) !== "tool_use" || stringValue(part.name)?.toLowerCase() !== "websearch") {
continue;
}
const id = stringValue(part.id);
if (id) {
webSearchToolUseIds.add(id);
}
}
break;
}
if (webSearchToolUseIds.size === 0) {
return [];
}
const texts: string[] = [];
for (const part of latestToolResults) {
if (!isRecord(part)) {
continue;
}
const toolUseId = stringValue(part.tool_use_id);
if (!toolUseId || !webSearchToolUseIds.has(toolUseId)) {
continue;
}
const text = anthropicToolResultContentText(part.content);
if (text) {
texts.push(text);
}
}
return texts;
}
function anthropicToolResultContentText(content: unknown): string {
if (typeof content === "string") {
return content;
}
if (!Array.isArray(content)) {
return "";
}
return content.flatMap((part) => {
if (!isRecord(part)) {
return [];
}
const type = stringValue(part.type);
if (type === "text" || type === "input_text" || type === "output_text") {
const text = stringValue(part.text);
return text ? [text] : [];
}
return [];
}).join("\n");
}
function textPartsFromOpenAiChatMessages(messages: unknown): string[] {
if (!Array.isArray(messages)) {
return [];
}
return messages.flatMap((message) => {
if (!isRecord(message) || stringValue(message.role)?.toLowerCase() !== "user") {
return [];
}
return textPartsFromOpenAiContent(message.content);
});
}
function textPartsFromOpenAiContent(content: unknown): string[] {
if (typeof content === "string") {
return [content];
}
if (!Array.isArray(content)) {
return [];
}
return content.flatMap((part) => {
if (!isRecord(part)) {
return [];
}
const type = stringValue(part.type);
if (type === "text" || type === "input_text" || type === "output_text") {
return stringValue(part.text) ? [stringValue(part.text) as string] : [];
}
return [];
});
}
function textPartsFromOpenAiResponsesInput(input: unknown): string[] {
if (typeof input === "string") {
return [input];
}
if (!Array.isArray(input)) {
return [];
}
return input.flatMap((item) => {
if (!isRecord(item)) {
return [];
}
const role = stringValue(item.role)?.toLowerCase();
if (role && role !== "user") {
return [];
}
return textPartsFromOpenAiContent(item.content);
});
}
function textPartsFromGeminiContents(contents: unknown): string[] {
if (!Array.isArray(contents)) {
return [];
}
return contents.flatMap((content) => {
if (!isRecord(content)) {
return [];
}
const role = stringValue(content.role)?.toLowerCase();
if (role && role !== "user") {
return [];
}
const parts = Array.isArray(content.parts) ? content.parts : [];
return parts.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []);
});
}
export function fusionWebSearchToolNameForRequest(config: AppConfig, model: string | undefined): string | undefined {
const normalizedModel = model ? fusionModelNameFromSelector(model) : "";
for (const candidate of fusionWebSearchToolCandidates(config)) {
if (!normalizedModel || candidate.aliases.some((alias) => fusionModelNameFromSelector(alias).toLowerCase() === normalizedModel.toLowerCase())) {
return candidate.toolName;
}
}
return undefined;
}
function fusionWebSearchToolCandidates(config: AppConfig): Array<{ aliases: string[]; toolName: string }> {
const rawProfiles = Array.isArray(config.virtualModelProfiles) ? config.virtualModelProfiles : [];
const profiles = normalizeCoreGatewayVirtualModelProfiles(
withCodexCompatibleVirtualModelProfiles(withFusionVirtualModelAliases(rawProfiles)),
config
);
const candidates: Array<{ aliases: string[]; toolName: string }> = [];
for (const profile of profiles) {
if (!isRecord(profile) || profile.enabled === false) {
continue;
}
const metadata = isRecord(profile.metadata) ? profile.metadata : undefined;
const fusionWebSearch = isRecord(metadata?.fusionWebSearch) ? metadata.fusionWebSearch : undefined;
const webSearchConfig = readFusionWebSearchConfig(fusionWebSearch);
if (!webSearchConfig?.toolName) {
continue;
}
const match = isRecord(profile.match) ? profile.match : undefined;
const aliases = uniqueStrings([
stringValue(profile.id),
stringValue(profile.key),
stringValue(profile.displayName),
...stringListValue(match?.exactAliases)
].filter((item): item is string => Boolean(item)));
candidates.push({ aliases, toolName: webSearchConfig.toolName });
}
return candidates;
}
export async function selectHostedWebSearchProtocolRecords(
context: HostedWebSearchProtocolContext,
integration: BrowserWebSearchMcpIntegration
): Promise<BrowserWebSearchProtocolRecord[]> {
const records = [
...(context.records ?? []),
...(integration.recentBrowserWebSearchResults?.({ sinceMs: context.sinceMs, toolName: context.toolName }) ?? [])
]
.filter((record) => record.results.length > 0)
.filter(uniqueSearchRecordFilter())
.sort((left, right) => {
const queryScoreDelta = queryMatchScore(context.queryHint, right.query) - queryMatchScore(context.queryHint, left.query);
return queryScoreDelta || left.completedAtMs - right.completedAtMs;
});
if (records.length > 0) {
return records.slice(0, 8);
}
if (!context.queryHint || !integration.runBrowserWebSearch) {
return [];
}
const record = await integration.runBrowserWebSearch({
count: Math.trunc(clampNumber(context.maxUses ?? 5, 1, 10)),
prompt: context.queryHint,
timeoutMs: 30_000,
toolName: context.toolName
});
return record?.results.length ? [record] : [];
}
export function selectClaudeCodeWebSearchContinuationRecords(
context: ClaudeCodeWebSearchContinuationContext,
integration: BrowserWebSearchMcpIntegration
): BrowserWebSearchProtocolRecord[] {
const records = integration.recentBrowserWebSearchResults?.({
sinceMs: context.sinceMs,
toolName: context.toolName
}) ?? [];
return records
.filter((record) => record.results.length > 0)
.filter(uniqueSearchRecordFilter())
.sort((left, right) => {
const queryScoreDelta = queryMatchScore(context.queryHint, right.query) - queryMatchScore(context.queryHint, left.query);
return queryScoreDelta || right.completedAtMs - left.completedAtMs;
})
.slice(0, 3);
}
async function selectAnthropicWebSearchProtocolRecords(
context: AnthropicWebSearchProtocolContext,
integration: BrowserWebSearchMcpIntegration
): Promise<BrowserWebSearchProtocolRecord[]> {
return selectHostedWebSearchProtocolRecords(context, integration);
}
function uniqueSearchRecordFilter(): (record: BrowserWebSearchProtocolRecord) => boolean {
const seen = new Set<string>();
return (record) => {
const key = `${record.toolName}\n${record.query}\n${record.searchUrl}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
};
}
@@ -0,0 +1,607 @@
import { randomBytes } from "node:crypto";
import { isRecord, numberValue, stringValue } from "@ccr/core/gateway/internal/value";
import type { BrowserWebSearchProtocolRecord, BrowserWebSearchProtocolResult } from "@ccr/core/gateway/internal/shared";
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
import { sseEventFromValue } from "@ccr/core/gateway/features/hosted-web-search/sse";
import type { ParsedSseEvent } from "@ccr/core/gateway/features/hosted-web-search/sse";
export function queryMatchScore(queryHint: string | undefined, query: string): number {
if (!queryHint) {
return 0;
}
const left = normalizeSearchComparisonText(queryHint);
const right = normalizeSearchComparisonText(query);
if (!left || !right) {
return 0;
}
if (left === right) {
return 4;
}
if (left.includes(right) || right.includes(left)) {
return 3;
}
const leftTerms = new Set(left.split(" ").filter((item) => item.length > 2));
const rightTerms = right.split(" ").filter((item) => item.length > 2);
return rightTerms.reduce((score, term) => score + (leftTerms.has(term) ? 1 : 0), 0);
}
export function normalizeSearchComparisonText(value: string): string {
return value.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").replace(/\s+/g, " ").trim();
}
export function responseValueContainsAnthropicWebSearchBlocks(value: Record<string, unknown>): boolean {
return Array.isArray(value.content) && value.content.some((block) => {
const type = isRecord(block) ? stringValue(block.type) : undefined;
return type === "server_tool_use" || type === "web_search_tool_result";
});
}
export function responseValueContainsVisibleText(value: Record<string, unknown>): boolean {
return Array.isArray(value.content) && value.content.some((block) => {
if (!isRecord(block) || stringValue(block.type) !== "text") {
return false;
}
return Boolean(stringValue(block.text)?.trim());
});
}
export function responseValueContainsAnthropicClientToolUse(value: Record<string, unknown>): boolean {
return Array.isArray(value.content) && value.content.some((block) => {
return isRecord(block) && stringValue(block.type) === "tool_use";
});
}
function leadingThinkingBlockCount(content: unknown[]): number {
let index = 0;
while (index < content.length) {
const block = content[index];
if (!isRecord(block) || stringValue(block.type) !== "thinking") {
break;
}
index += 1;
}
return index;
}
export function webSearchProtocolInsertIndex(content: unknown[], hasWebSearchBlocks: boolean): number {
let index = leadingThinkingBlockCount(content);
if (!hasWebSearchBlocks) {
return index;
}
while (index < content.length) {
const block = content[index];
const type = isRecord(block) ? stringValue(block.type) : undefined;
if (type !== "server_tool_use" && type !== "web_search_tool_result") {
break;
}
index += 1;
}
return index;
}
export function mergeAnthropicWebSearchUsage(usage: unknown, searchCount: number): Record<string, unknown> {
const nextUsage = isRecord(usage) ? { ...usage } : {};
const serverToolUse = isRecord(nextUsage.server_tool_use) ? { ...nextUsage.server_tool_use } : {};
const webSearchRequests = Math.max(1, Math.trunc(searchCount));
serverToolUse.web_search_requests = Math.max(numberValue(serverToolUse.web_search_requests) ?? 0, webSearchRequests);
nextUsage.server_tool_use = serverToolUse;
return nextUsage;
}
export function sseEventsContainAnthropicWebSearchBlocks(events: ParsedSseEvent[]): boolean {
return events.some((event) => {
return sseEventContainsAnthropicWebSearchBlock(event);
});
}
export function sseEventsContainVisibleText(events: ParsedSseEvent[]): boolean {
return events.some(sseEventContainsVisibleText);
}
export function sseEventContainsAnthropicWebSearchBlock(event: ParsedSseEvent): boolean {
const data = isRecord(event.data) ? event.data : undefined;
const block = isRecord(data?.content_block) ? data.content_block : undefined;
const type = stringValue(block?.type) || stringValue(data?.type);
return type === "server_tool_use" || type === "web_search_tool_result";
}
export function sseEventContainsVisibleText(event: ParsedSseEvent): boolean {
const data = isRecord(event.data) ? event.data : undefined;
if (!data) {
return false;
}
const block = isRecord(data.content_block) ? data.content_block : undefined;
if (stringValue(data.type) === "content_block_start" && stringValue(block?.type) === "text") {
return Boolean(stringValue(block?.text)?.trim());
}
const delta = isRecord(data.delta) ? data.delta : undefined;
return stringValue(data.type) === "content_block_delta" &&
stringValue(delta?.type) === "text_delta" &&
Boolean(stringValue(delta?.text)?.trim());
}
export function sseEventsContainAnthropicClientToolUse(events: ParsedSseEvent[]): boolean {
return events.some(sseEventContainsAnthropicClientToolUse);
}
export function sseEventContainsAnthropicClientToolUse(event: ParsedSseEvent): boolean {
const data = isRecord(event.data) ? event.data : undefined;
const block = isRecord(data?.content_block) ? data.content_block : undefined;
return stringValue(data?.type) === "content_block_start" && stringValue(block?.type) === "tool_use";
}
export function anthropicSseTextBlockStartIndex(event: ParsedSseEvent): number | undefined {
const data = isRecord(event.data) ? event.data : undefined;
const block = isRecord(data?.content_block) ? data.content_block : undefined;
if (stringValue(data?.type) !== "content_block_start" || stringValue(block?.type) !== "text") {
return undefined;
}
const index = numberValue(data?.index);
return index === undefined ? undefined : index;
}
export function sseEventIsAnthropicMessageEnd(event: ParsedSseEvent): boolean {
const type = isRecord(event.data) ? stringValue(event.data.type) : undefined;
return type === "message_delta" || type === "message_stop";
}
export function anthropicWebSearchSseEventsForBlock(block: Record<string, unknown>, index: number): ParsedSseEvent[] {
if (stringValue(block.type) === "text") {
const text = stringValue(block.text) ?? "";
return [
sseEventFromValue({
content_block: { text: "", type: "text" },
index,
type: "content_block_start"
}),
sseEventFromValue({
delta: { text, type: "text_delta" },
index,
type: "content_block_delta"
}),
sseEventFromValue({
index,
type: "content_block_stop"
})
];
}
return [
sseEventFromValue({
content_block: block,
index,
type: "content_block_start"
}),
sseEventFromValue({
index,
type: "content_block_stop"
})
];
}
export function updateAnthropicWebSearchSseUsage(
event: ParsedSseEvent,
searchCount: number,
didSynthesizeAnswer: boolean,
hasClientToolUse: boolean
): ParsedSseEvent {
if (!isRecord(event.data) || stringValue(event.data.type) !== "message_delta") {
return event;
}
const delta = isRecord(event.data.delta) ? { ...event.data.delta } : event.data.delta;
const nextData: Record<string, unknown> = {
...event.data,
usage: mergeAnthropicWebSearchUsage(event.data.usage, searchCount)
};
if (isRecord(delta) && shouldEndAnthropicHostedWebSearchTurn(delta.stop_reason, didSynthesizeAnswer, hasClientToolUse)) {
nextData.delta = { ...delta, stop_reason: "end_turn" };
}
return {
...event,
data: nextData
};
}
export function shouldEndAnthropicHostedWebSearchTurn(
stopReason: unknown,
didSynthesizeAnswer: boolean,
hasClientToolUse: boolean
): boolean {
if (hasClientToolUse) {
return false;
}
const normalized = stringValue(stopReason);
return normalized === "tool_use" || (didSynthesizeAnswer && normalized === "max_tokens");
}
export function synthesizeWebSearchAnswer(records: BrowserWebSearchProtocolRecord[], queryHint: string | undefined): string | undefined {
const query = queryHint || records.map((record) => record.query).find(Boolean) || "";
const weatherAnswer = synthesizeWeatherWebSearchAnswer(records, query);
if (weatherAnswer) {
return weatherAnswer;
}
const componentChangelogAnswer = synthesizeComponentChangelogWebSearchAnswer(records, query);
if (componentChangelogAnswer) {
return componentChangelogAnswer;
}
const evidence = topWebSearchEvidenceSentences(records, query, 3);
if (evidence.length === 0) {
const sources = webSearchSourceNames(records, 3);
if (!sources) {
return undefined;
}
return containsCjkText(query)
? `搜索已完成,但页面可提取正文不足。较相关的来源包括:${sources}`
: `The search completed, but the pages did not expose enough extractable text. The most relevant sources are: ${sources}.`;
}
const sources = webSearchSourceNames(records, 3);
return containsCjkText(query)
? `根据搜索结果,${evidence.join("")}${sources ? `来源:${sources}` : ""}`
: `Based on the search results, ${evidence.join("; ")}.${sources ? ` Sources: ${sources}.` : ""}`;
}
function synthesizeComponentChangelogWebSearchAnswer(records: BrowserWebSearchProtocolRecord[], query: string): string | undefined {
const normalizedQuery = normalizeSearchComparisonText(query);
const asksForComponents = /component|components|组件/i.test(query);
const asksForNewOrChangelog = /new|latest|recent|changelog|release|新增|新组件|最新|更新|官方/i.test(query);
if (!asksForComponents || !asksForNewOrChangelog) {
return undefined;
}
const items = webSearchEvidenceItems(records);
const preferred = items.find((item) => {
const normalized = normalizeSearchComparisonText(`${item.source} ${item.url} ${item.text.slice(0, 500)}`);
return normalized.includes("changelog") || normalized.includes("official") || normalized.includes("docs") || normalizedQuery.includes("official");
}) ?? items[0];
if (!preferred) {
return undefined;
}
const release = extractComponentReleaseTitle(preferred.text);
const components = extractLikelyComponentNames(preferred.text);
const sources = webSearchSourceNames(records, 2);
const cjk = containsCjkText(query);
if (!release && components.length === 0) {
return undefined;
}
if (cjk) {
return [
release ? `官方相关条目是 ${release}` : "官方页面包含新增组件相关内容",
components.length > 0 ? `可提取到的相关组件包括 ${components.join("、")}` : "",
sources ? `来源:${sources}` : ""
].filter(Boolean).join("");
}
return [
release ? `The relevant official entry is ${release}` : "The official page contains new component information",
components.length > 0 ? `extractable related components include ${components.join(", ")}` : "",
sources ? `Sources: ${sources}.` : ""
].filter(Boolean).join("; ");
}
function extractComponentReleaseTitle(text: string): string | undefined {
const patterns = [
/((?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}\s*-\s*Components?[^.。!?]{0,90})/i,
/(\d{4}[-/]\d{1,2}[^.。!?]{0,60}Components?[^.。!?]{0,60})/i
];
for (const pattern of patterns) {
const match = pattern.exec(text);
const title = match?.[1]?.replace(/\s+/g, " ").trim();
if (title) {
return title;
}
}
return undefined;
}
function extractLikelyComponentNames(text: string): string[] {
const knownNames = [
"Message Scroller",
"Message",
"Attachment",
"Bubble",
"Marker",
"Empty",
"Item",
"Field",
"Input OTP",
"Button Group"
];
const lower = text.toLowerCase();
return knownNames.filter((name) => lower.includes(name.toLowerCase())).slice(0, 10);
}
function synthesizeWeatherWebSearchAnswer(records: BrowserWebSearchProtocolRecord[], query: string): string | undefined {
if (!/天气|气温|温度|weather|forecast|temperature/i.test(query)) {
return undefined;
}
const items = webSearchEvidenceItems(records);
const text = items.map((item) => item.text).join(" ");
if (!text) {
return undefined;
}
const cjk = containsCjkText(query);
const location = extractWeatherLocation(query);
const temperatureRange = weatherTemperatureRange(text);
const currentTemperature = firstRegexGroup(text, [
/(?:当前|现在|实时|实况|气温|温度)[^。;,,\d-]{0,12}(-?\d{1,2}(?:\.\d+)?)\s*℃/i,
location ? new RegExp(`${escapeRegExp(location)}\\s+(-?\\d{1,2}(?:\\.\\d+)?)\\s*℃`) : undefined
]);
const feelsLike = firstRegexGroup(text, [/体感温度[:\s]*(-?\d{1,2}(?:\.\d+)?)\s*℃/]);
const high = firstRegexGroup(text, [/最高气温[:\s]*(-?\d{1,2}(?:\.\d+)?)\s*℃/]);
const low = firstRegexGroup(text, [/最低气温[:\s]*(-?\d{1,2}(?:\.\d+)?)\s*℃/]);
const humidity = firstRegexGroup(text, [/(?:最大相对湿度|相对湿度)[:\s]*(-?\d{1,3}(?:\.\d+)?%)/]);
const aqi = firstRegexGroup(text, [/AQI最高值[:\s]*(\d{1,3})/i]);
const airQuality = firstRegexGroup(text, [/空气质量[:\s]*([^\s,。;,;]{1,12})/]);
const rain = firstRegexGroup(text, [/(?:过去24小时总降水量|总降水量|降水量)[:\s]*(-?\d+(?:\.\d+)?mm)/i]);
const wind = firstRegexGroup(text, [/最大风力[:\s]*([<>]?\d+级|微风)/, /(东风|东南风|南风|西南风|西风|西北风|北风|东北风)\s*([<>]?\d+级|微风)/]);
const facts = [
currentTemperature ? (cjk ? `当前约 ${currentTemperature}` : `currently about ${currentTemperature}°C`) : undefined,
!currentTemperature && temperatureRange ? (cjk ? `气温约 ${temperatureRange}` : `temperatures are around ${temperatureRange}`) : undefined,
feelsLike ? (cjk ? `体感约 ${feelsLike}` : `feels like about ${feelsLike}°C`) : undefined,
high || low ? (cjk
? `过去24小时${high ? `最高 ${high}` : ""}${high && low ? "、" : ""}${low ? `最低 ${low}` : ""}`
: `over the past 24 hours ${high ? `the high was ${high}°C` : ""}${high && low ? " and " : ""}${low ? `the low was ${low}°C` : ""}`) : undefined,
humidity ? (cjk ? `相对湿度最高 ${humidity}` : `relative humidity reached ${humidity}`) : undefined,
aqi ? (cjk ? `AQI 最高 ${aqi}` : `AQI reached ${aqi}`) : undefined,
airQuality && !aqi ? (cjk ? `空气质量 ${airQuality}` : `air quality is ${airQuality}`) : undefined,
rain ? (cjk ? `过去24小时降水量 ${rain}` : `24-hour rainfall is ${rain}`) : undefined,
wind ? (cjk ? `风力 ${wind}` : `wind ${wind}`) : undefined
].filter((item): item is string => Boolean(item));
if (facts.length === 0) {
return undefined;
}
const sources = webSearchSourceNames(records, 2);
if (cjk) {
return `${location ? `${location}天气` : "天气"}${facts.slice(0, 6).join("")}${sources ? `来源:${sources}` : ""}`;
}
return `${location ? `${location} weather` : "Weather"}: ${facts.slice(0, 6).join(", ")}.${sources ? ` Sources: ${sources}.` : ""}`;
}
function webSearchEvidenceItems(records: BrowserWebSearchProtocolRecord[]): Array<{ source: string; text: string; url: string }> {
return records.flatMap((record) => record.results.map((result) => ({
source: result.title || hostnameFromUrl(result.url) || record.engine,
text: sanitizeWebSearchEvidenceText(result.content || result.snippet || ""),
url: result.url
}))).filter((item) => item.text);
}
function topWebSearchEvidenceSentences(records: BrowserWebSearchProtocolRecord[], query: string, limit: number): string[] {
const terms = relevantSearchTerms(query);
const scored = webSearchEvidenceItems(records).flatMap((item, itemIndex) => {
const sentences = splitEvidenceSentences(item.text).slice(0, 12);
return sentences.map((sentence, sentenceIndex) => {
const normalizedSentence = normalizeSearchComparisonText(sentence);
const termScore = terms.reduce((score, term) => score + (normalizedSentence.includes(term) ? 2 : 0), 0);
const sourceBonus = itemIndex === 0 ? 2 : itemIndex === 1 ? 1 : 0;
const positionBonus = Math.max(0, 4 - sentenceIndex) / 4;
return {
score: termScore + sourceBonus + positionBonus,
sentence
};
});
}).filter((item) => item.sentence.length >= 12 && item.sentence.length <= 260);
scored.sort((left, right) => right.score - left.score || left.sentence.length - right.sentence.length);
const seen = new Set<string>();
return scored.flatMap((item) => {
const key = normalizeSearchComparisonText(item.sentence).slice(0, 120);
if (!key || seen.has(key)) {
return [];
}
seen.add(key);
return [item.sentence];
}).slice(0, limit);
}
function splitEvidenceSentences(text: string): string[] {
return text
.replace(/\s+/g, " ")
.split(/[。!?!?]\s*|\n+/g)
.map((sentence) => sentence.trim().replace(/[,;:]\s*$/, ""))
.filter((sentence) => sentence && !looksLikeNavigationText(sentence));
}
function looksLikeNavigationText(text: string): boolean {
const punctuationCount = (text.match(/[,。;;:]/g) ?? []).length;
const digitCount = (text.match(/\d/g) ?? []).length;
return text.length > 160 && punctuationCount < 2 && digitCount < 2;
}
function relevantSearchTerms(query: string): string[] {
const normalizedTerms = normalizeSearchComparisonText(query)
.split(" ")
.filter((term) => term.length >= 2);
const cjkTerms = query.match(/[\p{Script=Han}]{2,}/gu) ?? [];
return uniqueStrings([...normalizedTerms, ...cjkTerms].map((term) => term.toLowerCase()));
}
function weatherTemperatureRange(text: string): string | undefined {
const values = Array.from(text.matchAll(/(-?\d{1,2}(?:\.\d+)?)\s*℃/g))
.map((match) => Number(match[1]))
.filter((value) => Number.isFinite(value) && value > -80 && value < 60)
.slice(0, 8);
if (values.length === 0) {
return undefined;
}
const min = Math.min(...values);
const max = Math.max(...values);
const format = (value: number) => Number.isInteger(value) ? String(value) : value.toFixed(1);
return min === max ? `${format(min)}` : `${format(min)}-${format(max)}`;
}
function extractWeatherLocation(query: string): string | undefined {
const cleaned = query
.replace(/perform\s+a\s+web\s+search\s+for\s+the\s+query:\s*/i, "")
.replace(/天气预报|天气|气温|温度|怎么样|如何|查询|搜索|今天|今日|现在|当前|请问|weather|forecast|temperature/gi, " ")
.replace(/\s+/g, " ")
.trim();
if (!cleaned || cleaned.length > 24) {
return undefined;
}
return cleaned;
}
function firstRegexGroup(text: string, patterns: Array<RegExp | undefined>): string | undefined {
for (const pattern of patterns) {
if (!pattern) {
continue;
}
const match = pattern.exec(text);
const value = match?.[1];
if (value) {
return value.trim();
}
}
return undefined;
}
function sanitizeWebSearchEvidenceText(text: string): string {
return text.replace(/\s+/g, " ").trim();
}
function containsCjkText(text: string): boolean {
return /\p{Script=Han}/u.test(text);
}
function webSearchSourceNames(records: BrowserWebSearchProtocolRecord[], limit: number): string {
return uniqueStrings(records.flatMap((record) => record.results.map((result) => {
const title = result.title?.trim();
return title || hostnameFromUrl(result.url) || record.engine;
}))).slice(0, limit).join("、");
}
function hostnameFromUrl(value: string): string | undefined {
try {
return new URL(value).hostname.replace(/^www\./, "");
} catch {
return undefined;
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function anthropicWebSearchProtocolBlocks(records: BrowserWebSearchProtocolRecord[], requestId: string): Record<string, unknown>[] {
const blocks: Record<string, unknown>[] = [];
records.forEach((record, index) => {
const toolUseId = `srvtoolu_${sanitizeAnthropicToolUseId(requestId)}_${index + 1}`;
blocks.push({
id: toolUseId,
input: { query: record.query },
name: "web_search",
type: "server_tool_use"
});
blocks.push({
content: record.results.map(anthropicWebSearchResultBlock),
tool_use_id: toolUseId,
type: "web_search_tool_result"
});
});
return blocks;
}
function anthropicWebSearchResultBlock(result: BrowserWebSearchProtocolResult): Record<string, unknown> {
const snippet = anthropicWebSearchResultSnippet(result);
return {
encrypted_content: "",
...(snippet ? { snippet: snippet.slice(0, 1_200) } : {}),
title: result.title,
type: "web_search_result",
url: result.url
};
}
function anthropicWebSearchResultSnippet(result: BrowserWebSearchProtocolResult): string | undefined {
const parts = [
result.snippet ? `Search snippet: ${sanitizeWebSearchEvidenceText(result.snippet)}` : "",
result.content ? `Extracted page content: ${sanitizeWebSearchEvidenceText(result.content)}` : "",
result.diagnostics?.length ? `Diagnostics: ${result.diagnostics.join("; ")}` : ""
].filter(Boolean);
return parts.length > 0 ? parts.join("\n") : undefined;
}
export function sanitizeAnthropicToolUseId(value: string): string {
return value.replace(/[^a-zA-Z0-9]/g, "").slice(0, 24) || randomBytes(8).toString("hex");
}
@@ -0,0 +1,4 @@
/** Public facade for the hosted web-search protocol bridge. */
export { createClaudeCodeWebSearchContinuationContext, createHostedWebSearchProtocolContext, prepareAnthropicWebSearchProtocolRequestBody, prepareClaudeCodeWebSearchContinuationRequestBody, prepareHostedWebSearchProtocolRequestBody } from "@ccr/core/gateway/features/hosted-web-search/request-transform";
export { hostedWebSearchProtocolResponseStream, transformAnthropicWebSearchProtocolResponseValue, transformAnthropicWebSearchProtocolSseText, transformGeminiHostedWebSearchResponseValue, transformGeminiHostedWebSearchSseText, transformOpenAiChatHostedWebSearchResponseValue, transformOpenAiChatHostedWebSearchSseText, transformOpenAiResponsesHostedWebSearchResponseValue, transformOpenAiResponsesHostedWebSearchSseText } from "@ccr/core/gateway/features/hosted-web-search/response-transform";
export { extractHostedWebSearchQueryHint, fusionWebSearchToolNameForRequest, selectClaudeCodeWebSearchContinuationRecords, selectHostedWebSearchProtocolRecords } from "@ccr/core/gateway/features/hosted-web-search/discovery";
@@ -0,0 +1,456 @@
import type { AppConfig } from "@ccr/core/contracts/app";
import { isRecord, rawStringValue, stringValue } from "@ccr/core/gateway/internal/value";
import type { AnthropicWebSearchProtocolContext, BrowserWebSearchProtocolRecord, ClaudeCodeWebSearchContinuationContext, HostedWebSearchProtocolContext } from "@ccr/core/gateway/internal/shared";
import { parseJsonObjectSafe } from "@ccr/core/gateway/http/body";
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
import { requestProtocolForPath } from "@ccr/core/routing/protocol-endpoints";
import { claudeCodeWebSearchToolResultTexts, extractAnthropicWebSearchQueryHint, extractClaudeCodeWebSearchToolResultQuery, extractHostedWebSearchQueryHint, fusionWebSearchToolNameForRequest, hasHostedWebSearchDeclaration, isAnthropicHostedWebSearchTool, isOpenAiHostedWebSearchTool, openAiToolChoiceNamesWebSearch, readHostedWebSearchMaxUses } from "@ccr/core/gateway/features/hosted-web-search/discovery";
import { normalizeSearchComparisonText } from "@ccr/core/gateway/features/hosted-web-search/evidence";
export function createHostedWebSearchProtocolContext(input: {
body: Buffer | undefined;
config: AppConfig;
method: string;
path: string;
requestId: string;
routedModel?: string;
sinceMs: number;
}): HostedWebSearchProtocolContext | undefined {
const protocol = requestProtocolForPath(input.path);
if (input.method !== "POST" || !protocol) {
return undefined;
}
const body = parseJsonObjectSafe(input.body);
if (!body || !hasHostedWebSearchDeclaration(body, protocol)) {
return undefined;
}
const toolName = fusionWebSearchToolNameForRequest(input.config, stringValue(body.model) || input.routedModel);
if (!toolName) {
return undefined;
}
return {
maxUses: readHostedWebSearchMaxUses(body, protocol),
protocol,
queryHint: extractHostedWebSearchQueryHint(body, protocol),
requestId: input.requestId,
sinceMs: input.sinceMs,
toolName
};
}
function createAnthropicWebSearchProtocolContext(input: {
body: Buffer | undefined;
config: AppConfig;
method: string;
path: string;
requestId: string;
sinceMs: number;
}): AnthropicWebSearchProtocolContext | undefined {
const context = createHostedWebSearchProtocolContext(input);
return context?.protocol === "anthropic_messages" ? context : undefined;
}
export function createClaudeCodeWebSearchContinuationContext(input: {
body: Buffer | undefined;
config: AppConfig;
method: string;
path: string;
routedModel?: string;
sinceMs: number;
}): ClaudeCodeWebSearchContinuationContext | undefined {
if (input.method !== "POST" || requestProtocolForPath(input.path) !== "anthropic_messages") {
return undefined;
}
const body = parseJsonObjectSafe(input.body);
if (!body || claudeCodeWebSearchToolResultTexts(body).length === 0) {
return undefined;
}
const toolName = fusionWebSearchToolNameForRequest(input.config, stringValue(body.model) || input.routedModel);
if (!toolName) {
return undefined;
}
return {
queryHint: extractClaudeCodeWebSearchToolResultQuery(body) || extractAnthropicWebSearchQueryHint(body),
sinceMs: input.sinceMs,
toolName
};
}
export function prepareHostedWebSearchProtocolRequestBody(
body: Buffer | undefined,
records: BrowserWebSearchProtocolRecord[],
context: Pick<HostedWebSearchProtocolContext, "protocol" | "queryHint">
): Buffer | undefined {
if (context.protocol === "anthropic_messages") {
return prepareAnthropicWebSearchProtocolRequestBody(body, records, context);
}
const parsed = parseJsonObjectSafe(body);
if (!parsed || records.length === 0) {
return undefined;
}
const evidence = hostedWebSearchEvidenceText(records, context.queryHint);
if (!evidence) {
return undefined;
}
let next: Record<string, unknown> | undefined;
if (context.protocol === "openai_chat_completions") {
next = prepareOpenAiChatHostedWebSearchRequestBody(parsed, evidence);
} else if (context.protocol === "openai_responses") {
next = prepareOpenAiResponsesHostedWebSearchRequestBody(parsed, evidence);
} else if (context.protocol === "gemini_generate_content") {
next = prepareGeminiHostedWebSearchRequestBody(parsed, evidence);
}
return next ? Buffer.from(`${JSON.stringify(next)}\n`, "utf8") : undefined;
}
export function prepareAnthropicWebSearchProtocolRequestBody(
body: Buffer | undefined,
records: BrowserWebSearchProtocolRecord[],
context: Pick<AnthropicWebSearchProtocolContext, "queryHint">
): Buffer | undefined {
const parsed = parseJsonObjectSafe(body);
if (!parsed || records.length === 0) {
return undefined;
}
const evidence = hostedWebSearchEvidenceText(records, context.queryHint);
if (!evidence) {
return undefined;
}
const next = applyAnthropicWebSearchSynthesisControls(stripAnthropicHostedWebSearchTools({
...parsed,
system: appendAnthropicSystemText(parsed.system, evidence)
}));
return Buffer.from(`${JSON.stringify(next)}\n`, "utf8");
}
export function prepareClaudeCodeWebSearchContinuationRequestBody(
body: Buffer | undefined,
records: BrowserWebSearchProtocolRecord[],
context: Pick<ClaudeCodeWebSearchContinuationContext, "queryHint">
): Buffer | undefined {
const parsed = parseJsonObjectSafe(body);
if (!parsed) {
return undefined;
}
const toolResultTexts = claudeCodeWebSearchToolResultTexts(parsed);
if (toolResultTexts.length === 0) {
return undefined;
}
const queryHint = context.queryHint || extractClaudeCodeWebSearchToolResultQuery(parsed) || extractAnthropicWebSearchQueryHint(parsed);
const evidence = claudeCodeWebSearchContinuationEvidenceText(records, queryHint, toolResultTexts);
if (!evidence) {
return undefined;
}
const next = applyAnthropicWebSearchSynthesisControls(stripClaudeCodeWebSearchContinuationTools({
...parsed,
system: appendAnthropicSystemText(parsed.system, evidence)
}));
return Buffer.from(`${JSON.stringify(next)}\n`, "utf8");
}
function applyAnthropicWebSearchSynthesisControls(body: Record<string, unknown>): Record<string, unknown> {
const next = { ...body };
const outputConfig = isRecord(next.output_config) ? { ...next.output_config } : {};
outputConfig.effort = "low";
next.output_config = outputConfig;
delete next.thinking;
delete next.reasoning;
return next;
}
function prepareOpenAiChatHostedWebSearchRequestBody(body: Record<string, unknown>, evidence: string): Record<string, unknown> {
const next = stripOpenAiHostedWebSearchTools({
...body,
messages: appendOpenAiChatSystemText(body.messages, evidence)
});
return applyOpenAiHostedWebSearchSynthesisControls(next);
}
function prepareOpenAiResponsesHostedWebSearchRequestBody(body: Record<string, unknown>, evidence: string): Record<string, unknown> {
const next = stripOpenAiHostedWebSearchTools({
...body,
instructions: appendStringInstruction(body.instructions, evidence)
});
return applyOpenAiHostedWebSearchSynthesisControls(next);
}
function prepareGeminiHostedWebSearchRequestBody(body: Record<string, unknown>, evidence: string): Record<string, unknown> {
return stripGeminiHostedWebSearchTools({
...body,
systemInstruction: appendGeminiSystemInstruction(body.systemInstruction, evidence)
});
}
function applyOpenAiHostedWebSearchSynthesisControls(body: Record<string, unknown>): Record<string, unknown> {
const next = { ...body };
if (typeof next.reasoning_effort === "string") {
next.reasoning_effort = "low";
}
if (isRecord(next.reasoning)) {
next.reasoning = { ...next.reasoning, effort: "low" };
}
return next;
}
function stripAnthropicHostedWebSearchTools(body: Record<string, unknown>): Record<string, unknown> {
if (!Array.isArray(body.tools)) {
return body;
}
const tools = body.tools.filter((tool) => !isAnthropicHostedWebSearchTool(tool));
if (tools.length === body.tools.length) {
return body;
}
const next = { ...body };
if (tools.length > 0) {
next.tools = tools;
} else {
delete next.tools;
}
const toolChoice = isRecord(next.tool_choice) ? next.tool_choice : undefined;
const toolChoiceName = stringValue(toolChoice?.name);
if (tools.length === 0 || toolChoiceName === "web_search") {
delete next.tool_choice;
}
return next;
}
function stripClaudeCodeWebSearchContinuationTools(body: Record<string, unknown>): Record<string, unknown> {
if (!Array.isArray(body.tools)) {
return body;
}
const next = { ...body };
delete next.tools;
delete next.tool_choice;
return next;
}
function stripOpenAiHostedWebSearchTools(body: Record<string, unknown>): Record<string, unknown> {
const next = { ...body };
let removedTools = false;
if (Array.isArray(body.tools)) {
const tools = body.tools.filter((tool) => !isOpenAiHostedWebSearchTool(tool));
removedTools = tools.length !== body.tools.length;
if (tools.length > 0) {
next.tools = tools;
} else {
delete next.tools;
}
}
if (next.web_search_options !== undefined || next.webSearchOptions !== undefined) {
delete next.web_search_options;
delete next.webSearchOptions;
removedTools = true;
}
if (removedTools && (!Array.isArray(next.tools) || next.tools.length === 0 || openAiToolChoiceNamesWebSearch(next.tool_choice))) {
delete next.tool_choice;
delete next.parallel_tool_calls;
}
return next;
}
function stripGeminiHostedWebSearchTools(body: Record<string, unknown>): Record<string, unknown> {
if (!Array.isArray(body.tools)) {
return body;
}
let changed = false;
const tools = body.tools.flatMap((tool) => {
const transformed = stripGeminiHostedWebSearchTool(tool);
changed ||= transformed.changed;
return transformed.value ? [transformed.value] : [];
});
if (!changed) {
return body;
}
const next = { ...body };
if (tools.length > 0) {
next.tools = tools;
} else {
delete next.tools;
}
return next;
}
function stripGeminiHostedWebSearchTool(tool: unknown): { changed: boolean; value?: unknown } {
if (!isRecord(tool)) {
return { changed: false, value: tool };
}
let changed = false;
const next: Record<string, unknown> = { ...tool };
for (const key of ["google_search", "googleSearch", "google_search_retrieval", "googleSearchRetrieval"]) {
if (key in next) {
delete next[key];
changed = true;
}
}
return Object.keys(next).length === 0 ? { changed, value: undefined } : { changed, value: next };
}
function appendAnthropicSystemText(system: unknown, text: string): unknown {
if (typeof system === "string") {
return `${system.trimEnd()}\n\n${text}`;
}
const block = { text, type: "text" };
if (Array.isArray(system)) {
return [...system, block];
}
return [block];
}
function appendOpenAiChatSystemText(messages: unknown, text: string): unknown[] {
const message = { content: text, role: "system" };
return Array.isArray(messages) ? [message, ...messages] : [message];
}
function appendStringInstruction(value: unknown, text: string): string {
const existing = rawStringValue(value);
return existing ? `${existing.trimEnd()}\n\n${text}` : text;
}
function appendGeminiSystemInstruction(value: unknown, text: string): Record<string, unknown> {
const part = { text };
if (typeof value === "string") {
return { parts: [{ text: value }, part] };
}
if (isRecord(value)) {
const parts = Array.isArray(value.parts) ? value.parts : [];
return {
...value,
parts: [...parts, part]
};
}
return { parts: [part] };
}
function hostedWebSearchEvidenceText(records: BrowserWebSearchProtocolRecord[], queryHint: string | undefined): string {
const sections = records.flatMap((record, recordIndex) => {
const resultLines = record.results.slice(0, 8).map((result, resultIndex) => {
const content = focusedWebSearchContent(result.content, queryHint);
const details = [
result.snippet ? `Search snippet: ${result.snippet}` : "",
content ? `Extracted page content: ${content}` : "",
result.diagnostics?.length ? `Diagnostics: ${result.diagnostics.join("; ")}` : ""
].filter(Boolean).join("\n");
return [
`${resultIndex + 1}. ${result.title}`,
`URL: ${result.url}`,
details
].filter(Boolean).join("\n");
});
if (resultLines.length === 0) {
return [];
}
return [
[
`Search ${recordIndex + 1}`,
`Query: ${record.query}`,
`Engine: ${record.engine}`,
`Search URL: ${record.searchUrl}`,
...resultLines
].join("\n\n")
];
});
if (sections.length === 0) {
return "";
}
return [
"A hidden in-app browser web search has already been performed for this request.",
"Use the evidence below to answer the user's question directly in the visible final response, within 5 concise sentences. Do not call another web search tool, do not merely list links, do not expose hidden reasoning, and do not ask the user to open links. If the evidence is insufficient for an exact value, say that clearly and summarize the most relevant findings with source names.",
queryHint ? `Original search intent: ${queryHint}` : "",
"Web search evidence:",
...sections
].filter(Boolean).join("\n\n").slice(0, 10_000);
}
function claudeCodeWebSearchContinuationEvidenceText(
records: BrowserWebSearchProtocolRecord[],
queryHint: string | undefined,
toolResultTexts: string[]
): string {
const browserEvidence = records.length > 0 ? hostedWebSearchEvidenceText(records, queryHint) : "";
const toolResultEvidence = toolResultTexts
.map((text) => text.trim())
.filter(Boolean)
.join("\n\n---\n\n")
.slice(0, 12_000);
if (!browserEvidence && !toolResultEvidence) {
return "";
}
return [
"A Claude Code WebSearch tool result has already been returned for this turn.",
"Answer the user's search question directly in the visible final response. Do not call any tool. Do not merely list links or ask the user to open links. Include the sources you used as markdown links.",
queryHint ? `Original search intent: ${queryHint}` : "",
browserEvidence ? `In-app browser extracted evidence:\n\n${browserEvidence}` : "",
toolResultEvidence ? `Previous WebSearch tool result:\n\n${toolResultEvidence}` : ""
].filter(Boolean).join("\n\n");
}
function focusedWebSearchContent(content: string | undefined, queryHint: string | undefined): string | undefined {
const text = content?.replace(/\s+/g, " ").trim();
if (!text) {
return undefined;
}
const queryTerms = normalizeSearchComparisonText(queryHint ?? "")
.split(" ")
.filter((term) => term.length >= 2);
const weatherTerms = /天气|weather/i.test(queryHint ?? "")
? ["天气", "气温", "温度", "体感", "空气质量", "湿度", "风", "降水", "℃", "晴", "多云", "阴", "雨"]
: [];
const terms = uniqueStrings([...queryTerms, ...weatherTerms]);
const indexes = terms.flatMap((term) => {
const index = text.toLowerCase().indexOf(term.toLowerCase());
return index >= 0 ? [index] : [];
});
if (indexes.length === 0) {
return text.slice(0, 1_000);
}
const center = Math.min(...indexes);
const start = Math.max(0, center - 300);
return `${start > 0 ? "..." : ""}${text.slice(start, start + 1_200)}${start + 1_200 < text.length ? "..." : ""}`;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
import { isRecord, stringValue } from "@ccr/core/gateway/internal/value";
export type ParsedSseEvent = {
data?: unknown;
event?: string;
raw?: string;
};
export function parseSseEvents(body: string): ParsedSseEvent[] {
return body
.split(/\r?\n\r?\n/g)
.filter((block) => block.trim())
.map(parseSseEventBlock);
}
export function parseSseEventBlock(raw: string): ParsedSseEvent {
const lines = raw.split(/\r?\n/g);
const event = lines
.filter((line) => line.startsWith("event:"))
.map((line) => line.slice(6).trim())
.find(Boolean);
const data = lines
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, ""))
.join("\n");
if (!data || data === "[DONE]") {
return { event, raw };
}
try {
return { data: JSON.parse(data) as unknown, event, raw };
} catch {
return { event, raw };
}
}
export function shiftSseContentBlockIndex(event: ParsedSseEvent, startIndex: number, delta: number): ParsedSseEvent {
if (!isRecord(event.data) || !Number.isFinite(event.data.index) || Number(event.data.index) < startIndex) {
return event;
}
return {
...event,
data: {
...event.data,
index: Number(event.data.index) + delta
}
};
}
export function sseEventFromValue(data: Record<string, unknown>): ParsedSseEvent {
return {
data,
event: stringValue(data.type)
};
}
export function serializeSseEvent(event: ParsedSseEvent): string {
if (event.data === undefined) {
return event.raw ?? "";
}
const type = isRecord(event.data) ? stringValue(event.data.type) : undefined;
return [
event.event || type ? `event: ${event.event || type}` : undefined,
`data: ${JSON.stringify(event.data)}`
].filter(Boolean).join("\n");
}
@@ -0,0 +1,511 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { IncomingHttpHeaders } from "node:http";
import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app";
import { CLAUDE_APP_FALLBACK_MODEL, buildClaudeAppGatewayModelRoutes, inferClaudeAppGatewayTargetModel, resolveClaudeAppGatewayRouteModel } from "@ccr/core/agents/claude-app/gateway-routes";
import { normalizeRouteSelector } from "@ccr/core/routing/model-registry";
import { findModelCatalogEntry, modelCatalogMaxInputTokens, modelCatalogMaxOutputTokens, readCatalogCapability, type ModelCatalogEntry } from "@ccr/core/gateway/model-catalog";
import { stringValue } from "@ccr/core/gateway/internal/value";
import { fusionModelSelector } from "@ccr/core/mcp/fusion-config";
import { readHeader } from "@ccr/core/gateway/http/io";
import { claudeAppGatewayModelRouteOptions, claudeCodeOneMillionContextSuffix } from "@ccr/core/gateway/internal/shared";
import type { ClaudeCodeDiscoverableModel } from "@ccr/core/gateway/internal/shared";
import { parseJsonObjectSafe, serializeJsonBodyWithModel } from "@ccr/core/gateway/http/body";
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
export function shouldServeGatewayModelsResponse(method: string, path: string): boolean {
return (method || "GET").toUpperCase() === "GET" &&
normalizeGatewayPathname(path) === "/v1/models";
}
export function prepareClaudeCodeDiscoveredModelRequest(
config: AppConfig,
headers: IncomingHttpHeaders,
method: string,
path: string,
body: Buffer | undefined
): { body: Buffer; diagnostic: string } | undefined {
if (
(method || "GET").toUpperCase() !== "POST" ||
normalizeGatewayPathname(path) !== "/v1/messages" ||
!isClaudeCodeUserAgent(headers)
) {
return undefined;
}
const parsedBody = parseJsonObjectSafe(body);
const model = stringValue(parsedBody?.model);
const rewrittenModel = resolveClaudeCodeDiscoveredModelId(model, config);
if (!parsedBody || !rewrittenModel || rewrittenModel === model) {
return undefined;
}
return {
body: serializeJsonBodyWithModel(parsedBody, rewrittenModel),
diagnostic: `${model}->${rewrittenModel}`
};
}
export function prepareClaudeAppFallbackModelRequest(
config: AppConfig,
method: string,
path: string,
body: Buffer | undefined
): { body: Buffer; diagnostic: string; routedModel: string } | undefined {
if (
(method || "GET").toUpperCase() !== "POST" ||
normalizeGatewayPathname(path) !== "/v1/messages"
) {
return undefined;
}
const parsedBody = parseJsonObjectSafe(body);
const model = stringValue(parsedBody?.model);
const normalizedModel = normalizeRouteSelector(model);
if (!parsedBody || !normalizedModel) {
return undefined;
}
const routeModel = resolveClaudeAppGatewayRouteModel(normalizedModel, config, claudeAppGatewayModelRouteOptions);
const routedModel = routeModel ??
(normalizedModel.toLowerCase() === CLAUDE_APP_FALLBACK_MODEL ? inferClaudeAppGatewayTargetModel(config) : undefined);
if (!routedModel || routedModel.toLowerCase() === normalizedModel.toLowerCase()) {
return undefined;
}
if (isConfiguredGatewayModelSelector(normalizedModel, config) && !routeModel) {
return undefined;
}
return {
body: serializeJsonBodyWithModel(parsedBody, routedModel),
diagnostic: `${model}->${routedModel}`,
routedModel
};
}
export function createGatewayModelsResponse(config: AppConfig, headers: IncomingHttpHeaders, apiKey?: ApiKeyConfig): Record<string, unknown> {
if (isClaudeAppApiKey(apiKey) || isClaudeCodeUserAgent(headers)) {
return createClaudeAppGatewayModelsResponse(config);
}
return createOpenAICompatibleGatewayModelsResponse(config);
}
function createOpenAICompatibleGatewayModelsResponse(config: AppConfig): Record<string, unknown> {
const data = buildGatewayDiscoverableModelIds(config).map((id) => {
const catalogEntry = findModelCatalogEntry(id);
return {
id,
object: "model",
created: 0,
owned_by: gatewayModelOwner(id),
type: "model",
...(catalogEntry?.displayName ? { display_name: catalogEntry.displayName } : {})
};
});
return {
object: "list",
data
};
}
function createClaudeAppGatewayModelsResponse(config: AppConfig): Record<string, unknown> {
const routes = buildClaudeAppGatewayModelRoutes(config, claudeAppGatewayModelRouteOptions);
const data = routes.map((route) => {
const catalogId = stripClaudeCodeOneMillionContextSuffix(route.targetModel);
const catalogEntry = findModelCatalogEntry(catalogId);
const maxInputTokens = claudeGatewayModelContextWindow(catalogEntry, route.oneMillionContext);
const maxOutputTokens = modelCatalogMaxOutputTokens(catalogEntry);
return {
id: route.id,
capabilities: createClaudeCodeModelCapabilities(catalogEntry, {
maxInputTokens,
oneMillionContext: route.oneMillionContext
}),
created_at: "1970-01-01T00:00:00Z",
display_name: route.displayName,
max_input_tokens: maxInputTokens,
max_tokens: maxOutputTokens,
type: "model"
};
});
return {
data,
first_id: data[0]?.id ?? null,
has_more: false,
last_id: data[data.length - 1]?.id ?? null
};
}
function createClaudeCodeModelsResponse(config: AppConfig): Record<string, unknown> {
const models = buildClaudeCodeDiscoverableModels(config);
const data = models.map((model) => {
const claudeId = claudeCodeDiscoveryModelId(model.id);
const catalogId = stripClaudeCodeOneMillionContextSuffix(model.id);
const catalogEntry = findModelCatalogEntry(catalogId);
const maxInputTokens = claudeGatewayModelContextWindow(catalogEntry, model.oneMillionContext);
const maxOutputTokens = modelCatalogMaxOutputTokens(catalogEntry);
return {
id: claudeId,
capabilities: createClaudeCodeModelCapabilities(catalogEntry, {
maxInputTokens,
oneMillionContext: model.oneMillionContext
}),
created_at: "1970-01-01T00:00:00Z",
display_name: formatClaudeCodeModelDisplayName(claudeId, catalogEntry, model.oneMillionContext),
max_input_tokens: maxInputTokens,
max_tokens: maxOutputTokens,
type: "model"
};
});
return {
data,
first_id: data[0]?.id ?? null,
has_more: false,
last_id: data[data.length - 1]?.id ?? null
};
}
function claudeGatewayModelContextWindow(entry: ModelCatalogEntry | undefined, oneMillionContext: boolean): number {
const contextWindow = modelCatalogMaxInputTokens(entry);
if (contextWindow > 0) {
return contextWindow;
}
return oneMillionContext ? 1_000_000 : 0;
}
function buildClaudeCodeDiscoverableModelIds(config: AppConfig): string[] {
return buildGatewayDiscoverableModelIds(config);
}
function buildGatewayDiscoverableModelIds(config: AppConfig): string[] {
const baseEntries: Array<{ modelName: string; providerName: string }> = [];
for (const provider of config.Providers) {
const providerName = provider.name?.trim();
if (!providerName || !Array.isArray(provider.models)) {
continue;
}
for (const rawModel of provider.models) {
const modelName = rawModel.trim();
if (!modelName) {
continue;
}
baseEntries.push({ modelName, providerName });
}
}
const ids = baseEntries.map((entry) => `${entry.providerName}/${entry.modelName}`);
for (const profile of config.virtualModelProfiles ?? []) {
if (!isVisibleVirtualModelProfile(profile)) {
continue;
}
for (const entry of baseEntries) {
for (const prefix of profile.match?.prefixes ?? []) {
const normalizedPrefix = prefix.trim();
if (normalizedPrefix) {
ids.push(`${entry.providerName}/${normalizedPrefix}${entry.modelName}`);
}
}
for (const suffix of profile.match?.suffixes ?? []) {
const normalizedSuffix = suffix.trim();
if (normalizedSuffix) {
ids.push(`${entry.providerName}/${entry.modelName}${normalizedSuffix}`);
}
}
}
for (const alias of profile.match?.exactAliases ?? []) {
const normalizedAlias = alias.trim();
if (!normalizedAlias) {
continue;
}
ids.push(fusionModelSelector(normalizedAlias));
}
}
return uniqueStrings(ids);
}
function gatewayModelOwner(id: string): string {
const separator = id.indexOf("/");
return separator > 0 ? id.slice(0, separator).trim() || "ccr" : "ccr";
}
function buildClaudeCodeDiscoverableModels(config: AppConfig): ClaudeCodeDiscoverableModel[] {
const seen = new Set<string>();
const models: ClaudeCodeDiscoverableModel[] = [];
const pushModel = (id: string, oneMillionContext: boolean) => {
const normalized = id.trim();
if (!normalized) {
return;
}
const key = normalized.toLowerCase();
if (seen.has(key)) {
return;
}
seen.add(key);
models.push({ id: normalized, oneMillionContext });
};
for (const id of buildClaudeCodeDiscoverableModelIds(config)) {
pushModel(id, hasClaudeCodeOneMillionContextSuffix(id));
const baseId = stripClaudeCodeOneMillionContextSuffix(id);
if (!hasClaudeCodeOneMillionContextSuffix(id) && findModelCatalogEntry(baseId)?.limits?.supports1MContext) {
pushModel(claudeCodeOneMillionContextModelId(baseId), true);
}
}
return models;
}
function isVisibleVirtualModelProfile(profile: NonNullable<AppConfig["virtualModelProfiles"]>[number]): boolean {
return profile.enabled !== false &&
profile.materialization?.enabled !== false &&
profile.materialization?.includeInGatewayModels !== false;
}
function resolveClaudeCodeDiscoveredModelId(model: string | undefined, config: AppConfig): string | undefined {
const normalized = normalizeRouteSelector(model);
if (!normalized || !normalized.toLowerCase().startsWith("claude-")) {
return undefined;
}
if (isConfiguredGatewayModelSelector(normalized, config)) {
return undefined;
}
const unprefixed = normalized.slice("claude-".length);
if (isConfiguredGatewayModelSelector(unprefixed, config)) {
return unprefixed;
}
const withoutOneMillionContextSuffix = stripClaudeCodeOneMillionContextSuffix(unprefixed);
return withoutOneMillionContextSuffix !== unprefixed &&
isConfiguredGatewayModelSelector(withoutOneMillionContextSuffix, config)
? withoutOneMillionContextSuffix
: undefined;
}
export function resolveGatewayPublicModelId(model: string | undefined, config: AppConfig): string | undefined {
const normalized = normalizeRouteSelector(model);
if (!normalized || !normalized.toLowerCase().startsWith("claude-")) {
return undefined;
}
if (isConfiguredGatewayModelSelector(normalized, config)) {
return undefined;
}
return resolveClaudeCodeDiscoveredModelId(normalized, config) ??
resolveClaudeAppGatewayRouteModel(normalized, config, claudeAppGatewayModelRouteOptions);
}
function isConfiguredGatewayModelSelector(model: string, config: AppConfig): boolean {
const normalized = normalizeRouteSelector(model)?.toLowerCase();
if (!normalized) {
return false;
}
for (const id of buildClaudeCodeDiscoverableModelIds(config)) {
if (id.toLowerCase() === normalized) {
return true;
}
}
for (const provider of config.Providers) {
if (provider.models.some((candidate) => candidate.trim().toLowerCase() === normalized)) {
return true;
}
}
return false;
}
function claudeCodeDiscoveryModelId(value: string): string {
return value.toLowerCase().startsWith("claude-") ? value : `claude-${value}`;
}
function claudeCodeOneMillionContextModelId(id: string): string {
return hasClaudeCodeOneMillionContextSuffix(id) ? id : `${id}${claudeCodeOneMillionContextSuffix}`;
}
function hasClaudeCodeOneMillionContextSuffix(id: string): boolean {
return id.trim().toLowerCase().endsWith(claudeCodeOneMillionContextSuffix);
}
function stripClaudeCodeOneMillionContextSuffix(id: string): string {
return id.trim().replace(/\[1m\]$/i, "").trim();
}
function formatClaudeCodeModelDisplayName(
id: string,
entry?: ModelCatalogEntry,
oneMillionContext = hasClaudeCodeOneMillionContextSuffix(id)
): string {
if (entry?.displayName) {
return oneMillionContext ? `${entry.displayName} (1M context)` : entry.displayName;
}
const normalized = stripClaudeCodeOneMillionContextSuffix(id.replace(/^claude-/i, ""));
const model = normalized.includes("/") ? normalized.slice(normalized.lastIndexOf("/") + 1) : normalized;
const words = model
.split(/[-_]+/)
.map((part) => part.trim())
.filter(Boolean)
.map((part) => (/^\d+$/.test(part) ? part : part.slice(0, 1).toUpperCase() + part.slice(1)));
const displayName = ["Claude", ...words].filter(Boolean).join(" ");
return oneMillionContext ? `${displayName} (1M context)` : displayName;
}
function createClaudeCodeModelCapabilities(
entry?: ModelCatalogEntry,
options: { maxInputTokens?: number; oneMillionContext?: boolean } = {}
): Record<string, unknown> {
if (!entry) {
return createDefaultClaudeCodeModelCapabilities();
}
const capabilities = entry.capabilities ?? {};
const inputModalities = new Set((entry.modalities?.input ?? []).map((item) => item.toLowerCase()));
const outputModalities = new Set((entry.modalities?.output ?? []).map((item) => item.toLowerCase()));
const supportsReasoning = readCatalogCapability(capabilities, "reasoning");
const supportsImageInput = readCatalogCapability(capabilities, "imageInput") || inputModalities.has("image");
const supportsPdfInput = readCatalogCapability(capabilities, "pdfInput") || inputModalities.has("pdf");
const supportsStructuredOutput =
readCatalogCapability(capabilities, "structuredOutput") ||
readCatalogCapability(capabilities, "nativeStructuredOutput") ||
readCatalogCapability(capabilities, "responseSchema");
const supportsCodeExecution = readCatalogCapability(capabilities, "codeExecution");
const supportsAdaptiveThinking = readCatalogCapability(capabilities, "adaptiveThinking");
const supportsToolUse =
readCatalogCapability(capabilities, "toolCalling") ||
readCatalogCapability(capabilities, "functionCalling");
const supportsBatch = readCatalogCapability(capabilities, "batch");
const supportsCitations = readCatalogCapability(capabilities, "citations");
const supportsAudioInput = readCatalogCapability(capabilities, "audioInput") || inputModalities.has("audio");
const supportsAudioOutput = readCatalogCapability(capabilities, "audioOutput") || outputModalities.has("audio");
const supportsVideoInput = readCatalogCapability(capabilities, "videoInput") || inputModalities.has("video");
const maxInputTokens = options.maxInputTokens ?? modelCatalogMaxInputTokens(entry);
const supportsOneMillionContext = Boolean(entry.limits?.supports1MContext);
return {
audio_input: { supported: supportsAudioInput },
audio_output: { supported: supportsAudioOutput },
batch: { supported: supportsBatch },
citations: { supported: supportsCitations },
code_execution: { supported: supportsCodeExecution },
context_management: {
clear_thinking_20251015: { supported: supportsReasoning },
clear_tool_uses_20250919: { supported: supportsToolUse },
compact_20260112: { supported: maxInputTokens > 0 },
max_input_tokens: maxInputTokens,
supported: maxInputTokens > 0
},
context_window: {
max_input_tokens: maxInputTokens,
supported: maxInputTokens > 0,
supports_1m_context: supportsOneMillionContext,
one_million_context_variant: options.oneMillionContext === true
},
effort: {
high: { supported: supportsReasoning },
low: { supported: supportsReasoning },
max: { supported: supportsReasoning },
medium: { supported: supportsReasoning },
supported: supportsReasoning,
xhigh: { supported: supportsReasoning }
},
image_input: { supported: supportsImageInput },
pdf_input: { supported: supportsPdfInput },
structured_outputs: { supported: supportsStructuredOutput },
thinking: {
supported: supportsReasoning,
types: {
adaptive: { supported: supportsAdaptiveThinking },
enabled: { supported: supportsReasoning }
}
},
tool_use: { supported: supportsToolUse },
video_input: { supported: supportsVideoInput }
};
}
function createDefaultClaudeCodeModelCapabilities(): Record<string, unknown> {
return {
batch: { supported: true },
citations: { supported: true },
code_execution: { supported: true },
context_management: {
clear_thinking_20251015: { supported: true },
clear_tool_uses_20250919: { supported: true },
compact_20260112: { supported: true },
supported: true
},
effort: {
high: { supported: true },
low: { supported: true },
max: { supported: true },
medium: { supported: true },
supported: true,
xhigh: { supported: true }
},
image_input: { supported: true },
pdf_input: { supported: true },
structured_outputs: { supported: true },
thinking: {
supported: true,
types: {
adaptive: { supported: true },
enabled: { supported: true }
}
}
};
}
function normalizeGatewayPathname(path: string): string {
const normalized = path.trim().replace(/\/+$/, "");
return normalized || "/";
}
function isClaudeCodeUserAgent(headers: IncomingHttpHeaders): boolean {
const userAgent = readHeader(headers["user-agent"]);
if (!userAgent) {
return false;
}
const normalized = userAgent.toLowerCase();
return normalized.includes("claude");
}
function isClaudeAppApiKey(apiKey: ApiKeyConfig | undefined): boolean {
const name = apiKey?.name?.trim().toLowerCase();
return name === "claude app";
}
+16
View File
@@ -0,0 +1,16 @@
import { parseJsonObject } from "@ccr/core/gateway/http/io";
export function parseJsonObjectSafe(buffer: Buffer | undefined): Record<string, unknown> | undefined {
if (!buffer || buffer.byteLength === 0) {
return undefined;
}
try {
return parseJsonObject(buffer);
} catch {
return undefined;
}
}
export function serializeJsonBodyWithModel(body: Record<string, unknown>, model: string): Buffer {
return Buffer.from(`${JSON.stringify({ ...body, model })}\n`, "utf8");
}
+223
View File
@@ -0,0 +1,223 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { IncomingHttpHeaders, IncomingMessage, Server, ServerResponse } from "node:http";
import type { ApiKeyConfig } from "@ccr/core/contracts/app";
import { ccrRemoteControlPathPrefix } from "@ccr/core/gateway/remote-control-service";
import { coreGatewayAuthHeader, localObservabilityHeaderNames, proxyHeaderDenyList, responseHeaderDenyList } from "@ccr/core/gateway/internal/shared";
export function inferGatewayClient(apiKey: ApiKeyConfig | undefined, headers: IncomingHttpHeaders): string | undefined {
const explicit =
readHeader(headers["x-ccr-client"]) ??
readHeader(headers["x-client-name"]) ??
readHeader(headers["x-forwarded-client-cert"]);
if (explicit) {
return explicit;
}
const apiKeyClient = apiKey?.name?.trim() || apiKey?.id?.trim();
const userAgentClient = inferClientFromUserAgent(headers);
if (readHeader(headers["x-ccr-proxy-mode"]) === "gateway") {
return userAgentClient ?? apiKeyClient;
}
return apiKeyClient ?? userAgentClient;
}
function inferClientFromUserAgent(headers: IncomingHttpHeaders): string | undefined {
const userAgent = readHeader(headers["user-agent"]);
if (!userAgent) {
return undefined;
}
const normalized = userAgent.toLowerCase();
if (normalized.includes("codex")) {
return "Codex";
}
if (normalized.includes("@anthropic-ai/claude-code") || normalized.includes("claude-code") || normalized.includes("claude code")) {
return "Claude Code";
}
if (normalized.includes("claude")) {
return "Claude";
}
if (normalized.includes("curl")) {
return "curl";
}
if (normalized.includes("python")) {
return "Python";
}
if (normalized.includes("node")) {
return "Node.js";
}
if (normalized.includes("chrome")) {
return "Google Chrome";
}
if (normalized.includes("safari") && !normalized.includes("chrome")) {
return "Safari";
}
return userAgent.split(/[ /]/)[0]?.trim() || undefined;
}
export function readAuthToken(headers: IncomingHttpHeaders): string | undefined {
const raw = readHeader(headers.authorization) || readHeader(headers["x-api-key"]);
if (!raw) {
return undefined;
}
return raw.toLowerCase().startsWith("bearer ") ? raw.slice(7).trim() : raw;
}
export function readRemoteControlQueryAuthToken(request: IncomingMessage): string | undefined {
const url = new URL(request.url || "/", "http://127.0.0.1");
if (url.pathname !== ccrRemoteControlPathPrefix && !url.pathname.startsWith(`${ccrRemoteControlPathPrefix}/`)) {
return undefined;
}
return url.searchParams.get("api_key")?.trim() || url.searchParams.get("key")?.trim() || undefined;
}
export function forwardHeaders(headers: IncomingHttpHeaders): Record<string, string> {
const forwarded: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
const normalized = key.toLowerCase();
if (proxyHeaderDenyList.has(normalized) || value === undefined) {
continue;
}
forwarded[normalized] = Array.isArray(value) ? value.join(",") : String(value);
}
return forwarded;
}
export function stripLocalGatewayAuthHeaders(headers: Record<string, string>): void {
delete headers.authorization;
delete headers["x-api-key"];
delete headers["api-key"];
}
export function omitLocalObservabilityHeaders(headers: Record<string, string>): Record<string, string> {
const forwarded = { ...headers };
for (const name of localObservabilityHeaderNames) {
delete forwarded[name];
}
return forwarded;
}
export function withCoreGatewayAuthHeader(headers: Record<string, string>, token: string): Record<string, string> {
if (!token) {
throw new Error("Core gateway auth token is not initialized.");
}
return {
...headers,
[coreGatewayAuthHeader]: token
};
}
export function filteredResponseHeaders(headers: Headers): Array<[string, string]> {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
if (!responseHeaderDenyList.has(key.toLowerCase())) {
entries.push([key, value]);
}
});
return entries;
}
export function formatError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export function abortSignalMessage(signal: AbortSignal): string {
const reason = signal.reason as unknown;
if (reason instanceof Error && reason.message) {
return reason.message;
}
if (typeof reason === "string" && reason.trim()) {
return reason.trim();
}
return "Upstream request was aborted.";
}
export function parseJsonObject(buffer: Buffer): Record<string, unknown> {
if (buffer.length === 0) {
return {};
}
const parsed = JSON.parse(buffer.toString("utf8")) as unknown;
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
throw new Error("Request body must be a JSON object.");
}
export function readHeader(value: string | string[] | undefined): string | undefined {
if (Array.isArray(value)) {
return value[0]?.trim();
}
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
export function readRequestBody(request: IncomingMessage): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
request.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
request.on("end", () => resolve(Buffer.concat(chunks)));
request.on("error", reject);
});
}
export function sendJson(response: ServerResponse, statusCode: number, payload: unknown): void {
response.writeHead(statusCode, { "content-type": "application/json" });
response.end(`${JSON.stringify(payload)}\n`);
}
export function closeServer(server: Server): Promise<void> {
return new Promise((resolve) => {
let settled = false;
let timeout: NodeJS.Timeout | undefined;
const finish = () => {
if (settled) {
return;
}
settled = true;
if (timeout) {
clearTimeout(timeout);
}
resolve();
};
try {
server.closeIdleConnections?.();
timeout = setTimeout(() => {
server.closeAllConnections?.();
finish();
}, 800);
server.close(() => finish());
} catch {
finish();
}
});
}
export function shouldSendBody(method: string | undefined): boolean {
const normalized = method?.toUpperCase();
return normalized !== "GET" && normalized !== "HEAD";
}
export function shouldCaptureGatewayUsage(method: string, _path: string): boolean {
return shouldSendBody(method);
}
@@ -0,0 +1,168 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app";
import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "@ccr/core/mcp/network-capture-mcp";
import { BROWSER_AUTOMATION_MCP_PATH, browserAutomationMcpEnabled } from "@ccr/core/mcp/toolhub-config";
import { pluginService } from "@ccr/core/plugins/service";
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "@ccr/core/gateway/remote-control-service";
import { authorize, reserveApiKeyLimits } from "@ccr/core/gateway/auth/api-key-authorizer";
import { parseJsonObject, readRequestBody, sendJson } from "@ccr/core/gateway/http/io";
import { shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
import { applyCors, endpoint, shouldServeGatewayRequest } from "@ccr/core/gateway/core-runtime/supervisor";
import { rawTraceSyncPath } from "@ccr/core/gateway/internal/shared";
import type { BrowserAutomationMcpIntegration } from "@ccr/core/gateway/internal/shared";
export type GatewayHttpRequestHandlerDependencies = {
getBrowserAutomationMcpIntegration: () => BrowserAutomationMcpIntegration | undefined;
getConfig: () => AppConfig | undefined;
getPlugin: () => ClaudeCodeRouterPlugin | undefined;
getStatus: () => { coreEndpoint: string; coreManagedExternally?: boolean; endpoint: string; state: string };
handleRawTraceSync: (request: IncomingMessage, response: ServerResponse) => Promise<void>;
proxyRequest: (request: IncomingMessage, response: ServerResponse, path: string, apiKey?: ApiKeyConfig) => Promise<void>;
};
export class GatewayHttpRequestHandler {
constructor(private readonly dependencies: GatewayHttpRequestHandlerDependencies) {}
private get browserAutomationMcpIntegration() { return this.dependencies.getBrowserAutomationMcpIntegration(); }
private get config() { return this.dependencies.getConfig(); }
private get plugin() { return this.dependencies.getPlugin(); }
private get status() { return this.dependencies.getStatus(); }
private handleRawTraceSync(request: IncomingMessage, response: ServerResponse) { return this.dependencies.handleRawTraceSync(request, response); }
private proxyRequest(request: IncomingMessage, response: ServerResponse, path: string, apiKey?: ApiKeyConfig) { return this.dependencies.proxyRequest(request, response, path, apiKey); }
async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
applyCors(response, this.config);
if (request.method === "OPTIONS") {
response.writeHead(204);
response.end();
return;
}
if (!this.config || !this.plugin) {
sendJson(response, 503, { error: { message: "Gateway service is not configured." } });
return;
}
const path = request.url ? new URL(request.url, this.status.endpoint || "http://127.0.0.1").pathname : "/";
if (path === rawTraceSyncPath) {
if (!shouldRecordRequestLogs(this.config)) {
sendJson(response, 202, { applied: false, disabled: true, ok: true });
return;
}
await this.handleRawTraceSync(request, response);
return;
}
if (path === ccrRemoteControlPathPrefix || path.startsWith(`${ccrRemoteControlPathPrefix}/`)) {
const authorization = await authorize(request, response, this.config);
if (!authorization.ok) {
return;
}
await ccrRemoteControlService.handleRequest({
endpoint: this.status.endpoint,
path,
readBody: readRequestBody,
request,
response,
sendJson
});
return;
}
if (path === BROWSER_AUTOMATION_MCP_PATH || path === `${BROWSER_AUTOMATION_MCP_PATH}/`) {
if (!browserAutomationMcpEnabled(this.config)) {
sendJson(response, 404, {
error: {
message: "CCR browser automation MCP is disabled."
}
});
return;
}
const authorization = await authorize(request, response, this.config);
if (!authorization.ok) {
return;
}
if (!this.browserAutomationMcpIntegration) {
sendJson(response, 503, {
error: {
message: "CCR browser automation MCP is only available in the Electron desktop app."
}
});
return;
}
await this.browserAutomationMcpIntegration.handleBrowserAutomationMcpRequest(request, response);
return;
}
if (isNetworkCaptureMcpPath(path)) {
if (!this.config.proxy.captureNetwork) {
sendJson(response, 404, { error: { message: "Network capture MCP is disabled." } });
return;
}
const authorization = await authorize(request, response, this.config);
if (!authorization.ok) {
return;
}
await handleNetworkCaptureMcpRequest(request, response);
return;
}
const pluginRoute = pluginService.matchGatewayRoute(request.method, path);
if (pluginRoute) {
if (pluginRoute.auth !== "none") {
const authorization = await authorize(request, response, this.config);
if (!authorization.ok) {
return;
}
}
await pluginService.handleGatewayRoute(pluginRoute, request, response);
return;
}
if (!shouldServeGatewayRequest(this.config, request)) {
sendJson(response, 503, { error: { message: "Gateway runtime is disabled." } });
return;
}
if (path === "/health") {
sendJson(response, 200, {
core: this.status.coreEndpoint,
coreManagedExternally: this.status.coreManagedExternally || undefined,
status: this.status.state,
timestamp: new Date().toISOString()
});
return;
}
if (path === "/") {
sendJson(response, 200, {
core: "next-ai-gateway",
endpoints: ["POST /mcp", "POST /v1/messages", "POST /v1/messages/count_tokens", "GET /v1/models"],
name: "claude-code-router",
plugin: "claude-code-router",
wrapperPlugins: this.config.plugins.filter((plugin) => plugin.enabled !== false).map((plugin) => plugin.id)
});
return;
}
const authorization = await authorize(request, response, this.config);
if (!authorization.ok) {
return;
}
if (request.method === "POST" && path === "/v1/messages/count_tokens") {
const requestBody = await readRequestBody(request);
const body = parseJsonObject(requestBody);
if (!reserveApiKeyLimits(authorization.apiKey, request, response, requestBody)) {
return;
}
sendJson(response, 200, this.plugin.countTokens(body));
return;
}
await this.proxyRequest(request, response, path, authorization.apiKey);
}
}
@@ -0,0 +1,3 @@
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@@ -0,0 +1,17 @@
export function clampNumber(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, Math.trunc(Number.isFinite(value) ? value : min)));
}
export function uniqueStrings(values: Array<string | undefined>): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const item = value?.trim();
if (!item || seen.has(item)) {
continue;
}
seen.add(item);
result.push(item);
}
return result;
}
@@ -0,0 +1,313 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import { createRequire } from "node:module";
import type { ApiKeyConfig, GatewayMcpServerConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelFusionWebSearchProvider } from "@ccr/core/contracts/app";
import type { ClaudeAppGatewayModelRouteOptions } from "@ccr/core/agents/claude-app/gateway-routes";
import type { RouteModelRef } from "@ccr/core/routing/contracts";
import { findModelCatalogEntry } from "@ccr/core/gateway/model-catalog";
export type CoreGatewayProvider = {
apikey?: string;
baseurl?: string;
billing?: unknown;
extraBody?: unknown;
extraHeaders?: unknown;
models: string[];
name: string;
type: GatewayProviderProtocol;
};
export const defaultFusionWebSearchProvider: VirtualModelFusionWebSearchProvider = "brave";
export const fusionModelProviderName = "Fusion";
export const claudeCodeOneMillionContextSuffix = "[1m]";
export const claudeAppGatewayModelRouteOptions: ClaudeAppGatewayModelRouteOptions = {
displayName: (model) => findModelCatalogEntry(model)?.displayName,
supportsOneMillionContext: (model) => Boolean(findModelCatalogEntry(model)?.limits?.supports1MContext)
};
export type ApiKeyAuthorizationResult =
| { ok: true; apiKey?: ApiKeyConfig }
| { ok: false };
export type ApiKeyLimitUsage = {
imageCount: number;
totalTokens: number;
};
export type ApiKeyLimitRule = {
limit: number;
metric: "images" | "requests" | "tokens";
name: string;
requested: number;
windowMs: number;
};
export type GatewayStopOptions = {
proxyRestoreTimeoutMs?: number;
};
export type HostedWebSearchProtocolContext = {
maxUses?: number;
protocol: GatewayProviderProtocol;
queryHint?: string;
records?: BrowserWebSearchProtocolRecord[];
requestId: string;
sinceMs: number;
toolName: string;
};
export type AnthropicWebSearchProtocolContext = HostedWebSearchProtocolContext;
export type ClaudeCodeWebSearchContinuationContext = {
queryHint?: string;
sinceMs: number;
toolName: string;
};
export type BrowserWebSearchMcpRegistration = {
env?: Record<string, string>;
name: string;
resultCount?: number;
timeoutMs?: number;
toolName: string;
};
export type BrowserWebSearchProtocolResult = {
content?: string;
diagnostics?: string[];
snippet?: string;
title: string;
url: string;
};
export type BrowserWebSearchProtocolRecord = {
completedAtMs: number;
engine: string;
query: string;
results: BrowserWebSearchProtocolResult[];
searchUrl: string;
toolName: string;
};
export type BrowserWebSearchMcpIntegration = {
registerBrowserWebSearchMcpServer: (options: BrowserWebSearchMcpRegistration) => Promise<GatewayMcpServerConfig | undefined>;
recentBrowserWebSearchResults?: (options: { sinceMs: number; toolName?: string }) => BrowserWebSearchProtocolRecord[];
runBrowserWebSearch?: (options: { count?: number; prompt: string; timeoutMs?: number; toolName?: string }) => Promise<BrowserWebSearchProtocolRecord | undefined>;
stopBrowserWebSearchMcpServers: () => Promise<void>;
};
export type BrowserAutomationMcpIntegration = {
handleBrowserAutomationMcpRequest: (request: IncomingMessage, response: ServerResponse) => Promise<void>;
stopBrowserAutomationMcpServer: () => Promise<void>;
};
export type CoreGatewayHealth = {
runtimeId?: string;
status?: string;
};
export type ManagedGatewayRuntimeMarker = {
generatedConfigFile?: unknown;
gatewayEntry?: unknown;
pid?: unknown;
runtimeId?: unknown;
startedAt?: unknown;
};
export type ApiKeyWindowCounter = {
expiresAt: number;
value: number;
windowStart: number;
};
export type RawTracePartText = {
contentType?: string;
text: string;
};
export type CursorOpenAICompatContext = {
systemPrompt?: string;
toolChoice?: unknown;
tools: unknown[];
};
export type CursorOpenAICompatPreparation = {
body?: Buffer;
diagnostic: "fallback-injected" | "simplified-missing-context";
};
export type ClaudeCodeDiscoverableModel = {
id: string;
oneMillionContext: boolean;
};
export type UpstreamAttempt = {
body?: Buffer;
credentialChain?: string[];
credentialIds?: string[];
credentialProtocol?: GatewayProviderProtocol;
headers?: Record<string, string>;
index: number;
logicalProvider?: string;
model?: string;
target?: RouteModelRef;
};
export type UpstreamFailedAttempt = {
credentialChain?: string[];
credentialIds?: string[];
delayMs?: number;
error?: string;
model?: string;
statusCode?: number;
};
export type UpstreamFetchResult = {
attempt: UpstreamAttempt;
failedAttempts: UpstreamFailedAttempt[];
response: Response;
};
export type ProviderCredentialRoutingTarget = {
body?: Buffer;
model?: string;
provider: GatewayProviderConfig;
protocol: GatewayProviderProtocol;
source: "header" | "model" | "plan";
};
export class UpstreamRequestError extends Error {
readonly attempt?: UpstreamAttempt;
readonly failedAttempts: UpstreamFailedAttempt[];
constructor(message: string, options: { attempt?: UpstreamAttempt; cause?: unknown; failedAttempts: UpstreamFailedAttempt[] }) {
super(message);
this.name = "UpstreamRequestError";
this.attempt = options.attempt;
this.cause = options.cause;
this.failedAttempts = options.failedAttempts;
}
}
export const requireFromHere = createRequire(__filename);
export const claudeCodeOauthBetaHeader = "anthropic-beta";
export const claudeCodeOauthRequiredBeta = "oauth-2025-04-20";
export const coreGatewayAuthHeader = "x-ccr-core-auth";
export const coreGatewayAuthTokenEnv = "CCR_CORE_GATEWAY_AUTH_TOKEN";
export const clientClosedRequestStatusCode = 499;
export const clientDisconnectMessage = "Client connection closed before response completed.";
export const localObservabilityHeaderNames = new Set([
"x-ccr-claude-app-model-rewrite",
"x-ccr-codex-patch-bridge",
"x-ccr-claude-model-discovery",
"x-ccr-cursor-openai-compat",
"x-ccr-logical-provider",
"x-ccr-provider-credential-chain",
"x-ccr-provider-credential-saturated"
]);
export const proxyHeaderDenyList = new Set(["connection", coreGatewayAuthHeader, "host", "upgrade"]);
export const responseHeaderDenyList = new Set(["connection", "content-encoding", "transfer-encoding"]);
export const maxUsageCaptureBytes = 8 * 1024 * 1024;
export const apiKeyLimitCounterRetentionWindows = 2;
export const gatewayRuntimeMarkerFile = "gateway-runtime.json";
export const rawTraceSyncHeader = "x-ccr-raw-trace-token";
export const virtualApplyPatchToolName = "virtual_apply_patch";
export const rawTraceSyncPath = "/__ccr/raw-trace-sync";
export const gatewayEntryOverrideEnv = "CCR_GATEWAY_ENTRY";
export const gatewayPackageCandidates = ["@the-next-ai/ai-gateway", "gateway"];
export const codexPatchBridgeInstructionText = [
"When modifying files, call virtual_apply_patch.",
"Do not use exec_command or write_stdin to edit files, including shell redirection, heredocs, cat >, tee, sed -i, perl -i, python, node scripts, or similar shell-based edits.",
"Use exec_command only for reading files, listing/searching, running builds/tests, starting servers, and other commands that are not manual file edits."
].join(" ");
export const codexPatchBridgeShellToolGuidance = [
"When virtual_apply_patch is available, do not use this tool to edit files.",
"Do not write files with shell redirection, heredocs, cat >, tee, sed -i, perl -i, python, node scripts, or similar commands.",
"Use virtual_apply_patch for manual file changes."
].join(" ");
export const virtualApplyPatchLarkGrammar = [
"start: begin_patch hunk+ end_patch",
"begin_patch: \"*** Begin Patch\" LF",
"end_patch: \"*** End Patch\" LF?",
"",
"hunk: add_hunk | delete_hunk | update_hunk",
"add_hunk: \"*** Add File: \" filename LF add_line+",
"delete_hunk: \"*** Delete File: \" filename LF",
"update_hunk: \"*** Update File: \" filename LF change_move? change?",
"",
"filename: /(.+)/",
"add_line: \"+\" /(.*)/ LF -> line",
"",
"change_move: \"*** Move to: \" filename LF",
"change: (change_context | change_line)+ eof_line?",
"change_context: (\"@@\" | \"@@ \" /(.+)/) LF",
"change_line: (\"+\" | \"-\" | \" \") /(.*)/ LF",
"eof_line: \"*** End of File\" LF",
"",
"%import common.LF"
].join("\n");
export const gatewayProviderProtocolFallbackOrder: GatewayProviderProtocol[] = [
"anthropic_messages",
"openai_chat_completions",
"openai_responses",
"gemini_generate_content",
"gemini_interactions"
];
export const privateDirMode = 0o700;
export const privateFileMode = 0o600;
@@ -0,0 +1,23 @@
/** Runtime-safe readers for untyped gateway and plugin payloads. */
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function stringValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
export function rawStringValue(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
export function stringListValue(value: unknown): string[] {
return Array.isArray(value)
? value.map((item) => stringValue(item)).filter((item): item is string => Boolean(item))
: [];
}
export function numberValue(value: unknown): number | undefined {
const number = Number(value);
return Number.isFinite(number) ? Math.trunc(number) : undefined;
}
@@ -0,0 +1,112 @@
import type { ApiKeyLimitConfig } from "@ccr/core/contracts/app";
import { parseJsonObject } from "@ccr/core/gateway/http/io";
import { isRecord } from "@ccr/core/gateway/internal/value";
import {
type ApiKeyLimitRule,
type ApiKeyLimitUsage,
type ApiKeyWindowCounter
} from "@ccr/core/gateway/internal/shared";
const apiKeyLimitCounterRetentionWindows = 2;
const apiKeyLimitCounters = new Map<string, ApiKeyWindowCounter>();
export function limitRules(limits: ApiKeyLimitConfig | undefined, usage: ApiKeyLimitUsage): ApiKeyLimitRule[] {
if (!limits) {
return [];
}
const rules: ApiKeyLimitRule[] = [];
addLimitRule(rules, "requests", "requests", limits.windowMs ?? 60_000, limits.maxRequests, 1);
addLimitRule(rules, "rpm", "requests", 60_000, limits.rpm, 1);
addLimitRule(rules, "rph", "requests", 3_600_000, limits.rph, 1);
addLimitRule(rules, "rpd", "requests", 86_400_000, limits.rpd, 1);
addLimitRule(rules, "tpm", "tokens", 60_000, limits.tpm, usage.totalTokens);
addLimitRule(rules, "tph", "tokens", 3_600_000, limits.tph, usage.totalTokens);
addLimitRule(rules, "tpd", "tokens", 86_400_000, limits.tpd, usage.totalTokens);
addLimitRule(rules, "ipm", "images", 60_000, limits.ipm, usage.imageCount);
addLimitRule(rules, "iph", "images", 3_600_000, limits.iph, usage.imageCount);
addLimitRule(rules, "ipd", "images", 86_400_000, limits.ipd, usage.imageCount);
addLimitRule(rules, "quota", "tokens", limits.quotaWindowMs ?? 86_400_000, limits.maxTokens, usage.totalTokens);
return rules;
}
export function readWindowCounter(
key: string,
windowStart: number,
windowMs: number,
now = Date.now()
): ApiKeyWindowCounter {
pruneExpiredCounters(now);
const existing = apiKeyLimitCounters.get(key);
if (existing && existing.windowStart === windowStart) {
return existing;
}
const fresh = {
expiresAt: windowStart + windowMs * apiKeyLimitCounterRetentionWindows,
value: 0,
windowStart
};
apiKeyLimitCounters.set(key, fresh);
return fresh;
}
export function estimateLimitUsage(method: string, requestBody: Buffer): ApiKeyLimitUsage {
if (method.toUpperCase() !== "POST" || requestBody.byteLength === 0) {
return { imageCount: 0, totalTokens: 0 };
}
const body = parseJsonObject(requestBody);
const inputCharacters = countUnknownCharacters(body.messages) + countUnknownCharacters(body.system) + countUnknownCharacters(body.tools);
const inputTokens = Math.ceil(inputCharacters / 4);
const outputTokens = readPositiveNumber(body.max_tokens) ?? readPositiveNumber(body.max_output_tokens) ?? 1024;
return {
imageCount: countImageInputs(body),
totalTokens: Math.max(1, inputTokens + outputTokens)
};
}
function addLimitRule(
rules: ApiKeyLimitRule[],
name: string,
metric: ApiKeyLimitRule["metric"],
windowMs: number,
limit: number | undefined,
requested: number
): void {
if (!limit || limit <= 0 || windowMs <= 0) {
return;
}
rules.push({ limit, metric, name, requested, windowMs });
}
function pruneExpiredCounters(now: number): void {
for (const [key, counter] of apiKeyLimitCounters) {
if (counter.expiresAt <= now) {
apiKeyLimitCounters.delete(key);
}
}
}
function countUnknownCharacters(value: unknown): number {
if (value === undefined || value === null) return 0;
if (typeof value === "string") return value.length;
try {
return JSON.stringify(value)?.length || 0;
} catch {
return String(value).length;
}
}
function countImageInputs(value: unknown): number {
if (Array.isArray(value)) {
return value.reduce((sum, item) => sum + countImageInputs(item), 0);
}
if (!isRecord(value)) return 0;
const type = typeof value.type === "string" ? value.type.toLowerCase() : "";
const isImage = type === "image" || type === "image_url" || type === "input_image" || value.image_url !== undefined || value.input_image !== undefined;
return (isImage ? 1 : 0) + Object.values(value).reduce<number>((sum, item) => sum + countImageInputs(item), 0);
}
function readPositiveNumber(value: unknown): number | undefined {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? Math.ceil(number) : undefined;
}
@@ -0,0 +1,475 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { Readable } from "node:stream";
import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app";
import { createSseErrorDetector, recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "@ccr/core/observability/request-log-store";
import { recordGatewayUsageCapture } from "@ccr/core/usage/store";
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
import { adaptRouteRequestBody, restoreRouteRequestBody } from "@ccr/core/routing/protocol-adapter";
import { reserveApiKeyLimits } from "@ccr/core/gateway/auth/api-key-authorizer";
import { recordProviderCredentialOutcome } from "@ccr/core/providers/credential-pool";
import { codexApplyPatchBridgeResponseStream, prepareCodexApplyPatchBridgeRequest } from "@ccr/core/gateway/features/codex-patch-bridge";
import { prepareCursorOpenAICompatChatBody } from "@ccr/core/gateway/features/cursor-compat";
import { browserWebSearchUnavailableMessage } from "@ccr/core/mcp/fusion-config";
import { filteredResponseHeaders, formatError, forwardHeaders, inferGatewayClient, parseJsonObject, readRequestBody, sendJson, shouldCaptureGatewayUsage, shouldSendBody, stripLocalGatewayAuthHeaders } from "@ccr/core/gateway/http/io";
import { createGatewayModelsResponse, prepareClaudeAppFallbackModelRequest, prepareClaudeCodeDiscoveredModelRequest, shouldServeGatewayModelsResponse } from "@ccr/core/gateway/features/model-discovery";
import { resolveProviderLogName, resolveResponseProviderProtocol, sanitizeHeaderValue } from "@ccr/core/providers/runtime-topology";
import { createBodySampler, shouldRecordRequestLogs } from "@ccr/core/observability/raw-trace-sync";
import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor";
import { clientClosedRequestStatusCode, clientDisconnectMessage, UpstreamRequestError } from "@ccr/core/gateway/internal/shared";
import type { BrowserWebSearchMcpIntegration, BrowserWebSearchProtocolRecord, UpstreamFetchResult } from "@ccr/core/gateway/internal/shared";
import { applyProviderCapabilityRouting, cancelResponseBody, destroyResponseStreams, fetchUpstreamWithFallback, mergeFallbackResponseHeaders, rewriteCapabilityResponseHeaders, uniqueStreams, upstreamResponseHeaders } from "@ccr/core/gateway/upstream/executor";
import { shouldApplyGatewayRouting } from "@ccr/core/routing/protocol-endpoints";
import { createClaudeCodeWebSearchContinuationContext, createHostedWebSearchProtocolContext, hostedWebSearchProtocolResponseStream, prepareClaudeCodeWebSearchContinuationRequestBody, prepareHostedWebSearchProtocolRequestBody, selectClaudeCodeWebSearchContinuationRecords, selectHostedWebSearchProtocolRecords } from "@ccr/core/gateway/features/hosted-web-search/index";
export type GatewayRequestPipelineDependencies = {
getBrowserWebSearchMcpIntegration: () => BrowserWebSearchMcpIntegration | undefined;
getConfig: () => AppConfig | undefined;
getCoreAuthToken: () => string;
getPlugin: () => ClaudeCodeRouterPlugin | undefined;
getStatus: () => { coreEndpoint: string; endpoint: string };
takePendingRawTraceUpdate: (requestId: string) => RequestLogRawTraceUpdateInput | undefined;
};
export class GatewayRequestPipeline {
constructor(private readonly dependencies: GatewayRequestPipelineDependencies) {}
private get browserWebSearchMcpIntegration() { return this.dependencies.getBrowserWebSearchMcpIntegration(); }
private get config() { return this.dependencies.getConfig(); }
private get coreAuthToken() { return this.dependencies.getCoreAuthToken(); }
private get plugin() { return this.dependencies.getPlugin(); }
private get status() { return this.dependencies.getStatus(); }
private takePendingRawTraceUpdate(requestId: string) { return this.dependencies.takePendingRawTraceUpdate(requestId); }
async proxyRequest(request: IncomingMessage, response: ServerResponse, path: string, apiKey?: ApiKeyConfig): Promise<void> {
if (!this.config || !this.plugin) {
sendJson(response, 503, { error: { message: "Gateway service is not configured." } });
return;
}
const headers = forwardHeaders(request.headers);
if (apiKey) {
stripLocalGatewayAuthHeaders(headers);
headers["x-auth-api-key-id"] = apiKey.id;
headers["x-auth-sub"] = apiKey.id;
}
const method = request.method ?? "GET";
const requestBody = await readRequestBody(request);
const client = inferGatewayClient(apiKey, request.headers);
const cursorCompatPreparation = prepareCursorOpenAICompatChatBody(this.config, client, method, path, requestBody);
if (cursorCompatPreparation) {
headers["x-ccr-cursor-openai-compat"] = sanitizeHeaderValue(cursorCompatPreparation.diagnostic);
}
let bodyToForward: Buffer | undefined = cursorCompatPreparation?.body ?? requestBody;
let routeFallback = this.config.Router.fallback;
let routedModel: string | undefined;
let codexApplyPatchBridgeActive = false;
const claudeModelRewrite = prepareClaudeCodeDiscoveredModelRequest(this.config, request.headers, method, path, bodyToForward);
if (claudeModelRewrite) {
headers["x-ccr-claude-model-discovery"] = sanitizeHeaderValue(claudeModelRewrite.diagnostic);
bodyToForward = claudeModelRewrite.body;
}
const claudeAppModelRewrite = prepareClaudeAppFallbackModelRequest(this.config, method, path, bodyToForward);
if (claudeAppModelRewrite) {
headers["x-ccr-claude-app-model-rewrite"] = sanitizeHeaderValue(claudeAppModelRewrite.diagnostic);
bodyToForward = claudeAppModelRewrite.body;
routedModel = claudeAppModelRewrite.routedModel;
}
if (!reserveApiKeyLimits(apiKey, request, response, bodyToForward)) {
return;
}
const startedAt = Date.now();
const startedAtIso = new Date(startedAt).toISOString();
const requestId = randomUUID();
headers["x-client-request-id"] = requestId;
const requestUrl = new URL(request.url || path, this.status.endpoint || "http://127.0.0.1").toString();
const upstreamAbortController = new AbortController();
let clientDisconnected = false;
let responseCompleted = false;
let onClientDisconnect: (() => void) | undefined;
let onResponseFinish: (() => void) | undefined;
const handleClientDisconnect = () => {
if (responseCompleted || response.writableEnded) {
return;
}
if (!clientDisconnected) {
clientDisconnected = true;
upstreamAbortController.abort(new Error(clientDisconnectMessage));
}
onClientDisconnect?.();
};
response.once("finish", () => {
responseCompleted = true;
onResponseFinish?.();
});
response.once("close", handleClientDisconnect);
response.on("error", () => {
// Client-side write failures (EPIPE / ECONNRESET when the client closes
// mid-stream, common during tool execution) must not crash the main
// process as an Uncaught Exception. Swallow them here; the close handler
// above already records the disconnect via writeStreamLog.
handleClientDisconnect();
});
const writeRequestLog = (
statusCode: number,
responseHeaders: Headers,
responseBodyText = "",
responseBodyTruncated = false,
error?: string
) => {
const config = this.config;
if (!config || !shouldRecordRequestLogs(config)) {
return;
}
void (async () => {
await recordGatewayRequestLog({
client,
completedAt: new Date().toISOString(),
durationMs: Date.now() - startedAt,
error,
fallbackModel: routedModel,
method,
path,
providerName: resolveProviderLogName(responseHeaders, config, routedModel),
providerProtocol: resolveResponseProviderProtocol(responseHeaders, this.config),
requestBody: shouldSendBody(method) ? bodyToForward ?? Buffer.alloc(0) : Buffer.alloc(0),
requestHeaders: headers,
requestId,
responseBodyText,
responseBodyTruncated,
responseHeaders,
startedAt: startedAtIso,
statusCode,
url: requestUrl
});
const pendingRawTraceUpdate = this.takePendingRawTraceUpdate(requestId);
if (pendingRawTraceUpdate) {
await updateGatewayRequestLogFromRawTrace(pendingRawTraceUpdate);
}
})();
};
const shouldCaptureUsage = shouldCaptureGatewayUsage(method, path);
if (shouldServeGatewayModelsResponse(method, path)) {
const responseText = `${JSON.stringify(createGatewayModelsResponse(this.config, request.headers, apiKey))}\n`;
const modelHeaders = new Headers({
"cache-control": "no-store, max-age=0",
"content-length": String(Buffer.byteLength(responseText)),
"content-type": "application/json; charset=utf-8",
"expires": "0",
"pragma": "no-cache"
});
response.writeHead(200, Object.fromEntries(filteredResponseHeaders(modelHeaders)));
response.end(responseText);
return;
}
if (shouldApplyGatewayRouting(method, path)) {
const adaptation = adaptRouteRequestBody(path, parseJsonObject(bodyToForward ?? requestBody));
const routed = await this.plugin.routeRequest({
body: adaptation.body,
headers: headers as Record<string, string | string[] | undefined>,
method,
url: request.url ?? path
});
const serialized = Buffer.from(`${JSON.stringify(restoreRouteRequestBody(routed.body, adaptation))}\n`, "utf8");
headers["content-type"] = "application/json";
headers["x-ccr-route-reason"] = sanitizeHeaderValue(routed.decision.reason);
headers["x-ccr-route-source"] = routed.decision.source;
if (routed.decision.diagnostics.length > 0) {
headers["x-ccr-route-diagnostics"] = String(routed.decision.diagnostics.length);
}
routeFallback = routed.decision.fallback ?? routeFallback;
if (routed.decision.model) {
headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model);
routedModel = routed.decision.model;
}
bodyToForward = serialized;
}
const codexApplyPatchBridgeRequest = prepareCodexApplyPatchBridgeRequest({
body: bodyToForward,
config: this.config,
headers: request.headers,
method,
path,
routedModel
});
if (codexApplyPatchBridgeRequest) {
bodyToForward = codexApplyPatchBridgeRequest.body;
codexApplyPatchBridgeActive = true;
headers["x-ccr-codex-patch-bridge"] = sanitizeHeaderValue(codexApplyPatchBridgeRequest.diagnostic);
headers["content-type"] = "application/json";
}
const providerCapabilityRouting = applyProviderCapabilityRouting({
body: bodyToForward,
config: this.config,
fallback: routeFallback,
headers,
path,
routedModel
});
bodyToForward = providerCapabilityRouting.body;
routeFallback = providerCapabilityRouting.fallback;
routedModel = providerCapabilityRouting.routedModel;
const hostedWebSearchProtocolContext = createHostedWebSearchProtocolContext({
body: bodyToForward,
config: this.config,
method,
path,
requestId,
routedModel,
sinceMs: startedAt - 1_000
});
if (hostedWebSearchProtocolContext && !this.browserWebSearchMcpIntegration) {
const message = browserWebSearchUnavailableMessage(hostedWebSearchProtocolContext.toolName);
const responseHeaders = new Headers({ "content-type": "application/json; charset=utf-8" });
const responseBody = JSON.stringify({ error: { message } });
writeRequestLog(503, responseHeaders, responseBody, false, message);
sendJson(response, 503, { error: { message } });
return;
}
if (hostedWebSearchProtocolContext && this.browserWebSearchMcpIntegration) {
const records = await selectHostedWebSearchProtocolRecords(
hostedWebSearchProtocolContext,
this.browserWebSearchMcpIntegration
).catch((error) => {
console.warn(`[gateway] Failed to prefetch hosted web search results: ${formatError(error)}`);
return [] as BrowserWebSearchProtocolRecord[];
});
if (records.length > 0) {
hostedWebSearchProtocolContext.records = records;
const webSearchContextBody = prepareHostedWebSearchProtocolRequestBody(
bodyToForward,
records,
hostedWebSearchProtocolContext
);
if (webSearchContextBody) {
bodyToForward = webSearchContextBody;
headers["content-type"] = "application/json";
headers["x-ccr-hosted-web-search-context"] = hostedWebSearchProtocolContext.protocol;
}
}
}
const claudeCodeWebSearchContinuationContext = !hostedWebSearchProtocolContext && this.browserWebSearchMcpIntegration
? createClaudeCodeWebSearchContinuationContext({
body: bodyToForward,
config: this.config,
method,
path,
routedModel,
sinceMs: startedAt - 5 * 60_000
})
: undefined;
if (claudeCodeWebSearchContinuationContext && this.browserWebSearchMcpIntegration) {
const records = selectClaudeCodeWebSearchContinuationRecords(
claudeCodeWebSearchContinuationContext,
this.browserWebSearchMcpIntegration
);
const webSearchContinuationBody = prepareClaudeCodeWebSearchContinuationRequestBody(
bodyToForward,
records,
claudeCodeWebSearchContinuationContext
);
if (webSearchContinuationBody) {
bodyToForward = webSearchContinuationBody;
headers["content-type"] = "application/json";
headers["x-ccr-claude-code-web-search-continuation"] = records.length > 0 ? "in-app-browser-evidence" : "tool-result-evidence";
}
}
delete headers["content-length"];
const upstreamUrl = new URL(request.url || "/", this.status.coreEndpoint).toString();
let upstreamResult: UpstreamFetchResult;
try {
upstreamResult = await fetchUpstreamWithFallback({
body: bodyToForward,
config: this.config,
fallback: routeFallback,
headers,
method,
path,
routedModel,
coreAuthToken: this.coreAuthToken,
signal: upstreamAbortController.signal,
upstreamUrl
});
} catch (error) {
const message = formatError(error);
if (error instanceof UpstreamRequestError) {
bodyToForward = error.attempt?.body ?? bodyToForward;
routedModel = error.attempt?.model ?? routedModel;
}
if (clientDisconnected || upstreamAbortController.signal.aborted) {
writeRequestLog(clientClosedRequestStatusCode, new Headers(), "", false, clientDisconnectMessage);
return;
}
if (shouldCaptureUsage) {
void recordGatewayUsageCapture({
bodyText: "",
client,
durationMs: Date.now() - startedAt,
fallbackModel: routedModel,
method,
path,
providerName: resolveProviderLogName(new Headers(), this.config, routedModel),
providerProtocol: resolveResponseProviderProtocol(new Headers(), this.config),
requestId,
responseHeaders: new Headers(),
statusCode: 502
});
}
writeRequestLog(502, new Headers(), "", false, message);
throw error;
}
bodyToForward = upstreamResult.attempt.body ?? bodyToForward;
routedModel = upstreamResult.attempt.model ?? routedModel;
const responseHeaders = rewriteCapabilityResponseHeaders(
// Copy into a mutable Headers instance: upstream fetch Response.headers
// can be immutable (TypeError: immutable on .delete/.set), and
// mergeFallbackResponseHeaders returns the original object as-is when
// no fallback occurred. Codex apply_patch / web-search paths call
// .delete("content-length") below, which would otherwise throw and
// surface as a 502.
new Headers(mergeFallbackResponseHeaders(upstreamResponseHeaders(upstreamResult), upstreamResult)),
this.config
);
const upstreamResponse = upstreamResult.response;
if (clientDisconnected || upstreamAbortController.signal.aborted) {
await cancelResponseBody(upstreamResponse);
writeRequestLog(clientClosedRequestStatusCode, responseHeaders, "", false, clientDisconnectMessage);
return;
}
if (codexApplyPatchBridgeActive) {
responseHeaders.delete("content-length");
}
const hostedWebSearchResponseContentType = responseHeaders.get("content-type")?.toLowerCase() ?? "";
if (
hostedWebSearchProtocolContext &&
(hostedWebSearchResponseContentType.includes("application/json") ||
hostedWebSearchResponseContentType.includes("text/event-stream")) &&
(this.browserWebSearchMcpIntegration?.recentBrowserWebSearchResults || this.browserWebSearchMcpIntegration?.runBrowserWebSearch)
) {
responseHeaders.delete("content-length");
}
recordProviderCredentialOutcome(this.config, method, upstreamResult.attempt, upstreamResponse.status, responseHeaders);
if (clientDisconnected || response.destroyed) {
await cancelResponseBody(upstreamResponse);
writeRequestLog(clientClosedRequestStatusCode, responseHeaders, "", false, clientDisconnectMessage);
return;
}
response.writeHead(upstreamResponse.status, Object.fromEntries(filteredResponseHeaders(responseHeaders)));
if (!upstreamResponse.body) {
if (shouldCaptureUsage) {
void recordGatewayUsageCapture({
bodyText: "",
client,
durationMs: Date.now() - startedAt,
fallbackModel: routedModel,
method,
path,
providerName: resolveProviderLogName(responseHeaders, this.config, routedModel),
providerProtocol: resolveResponseProviderProtocol(responseHeaders, this.config),
requestId,
responseHeaders,
statusCode: upstreamResponse.status
});
}
writeRequestLog(upstreamResponse.status, responseHeaders);
response.end();
return;
}
const upstreamBody = Readable.fromWeb(upstreamResponse.body as unknown as import("node:stream/web").ReadableStream);
const patchedResponseBody = codexApplyPatchBridgeActive
? codexApplyPatchBridgeResponseStream(upstreamBody, responseHeaders)
: upstreamBody;
const responseBody = hostedWebSearchProtocolContext
? hostedWebSearchProtocolResponseStream(
patchedResponseBody,
responseHeaders,
hostedWebSearchProtocolContext,
this.browserWebSearchMcpIntegration
)
: patchedResponseBody;
const responseStreams = uniqueStreams([upstreamBody, patchedResponseBody, responseBody]);
const sampler = createBodySampler();
const sseErrorDetector = createSseErrorDetector(responseHeaders.get("content-type") ?? undefined);
let streamDetectedError: string | undefined;
let upstreamStreamEnded = false;
let logRecorded = false;
const writeStreamLog = (error?: string) => {
if (logRecorded) {
return;
}
logRecorded = true;
writeRequestLog(
clientDisconnected ? clientClosedRequestStatusCode : upstreamResponse.status,
responseHeaders,
sampler.read(),
sampler.isTruncated(),
error ?? streamDetectedError
);
};
onClientDisconnect = () => {
writeStreamLog(clientDisconnectMessage);
responseBody.unpipe(response);
destroyResponseStreams(responseStreams);
};
onResponseFinish = () => {
if (upstreamStreamEnded) {
writeStreamLog();
}
};
const onResponseStreamError = (error: Error) => {
streamDetectedError ??= sseErrorDetector.finish();
writeStreamLog(clientDisconnected ? clientDisconnectMessage : formatError(error));
};
for (const stream of responseStreams) {
stream.on("error", onResponseStreamError);
}
responseBody.on("data", (chunk) => {
sampler.append(chunk);
streamDetectedError ??= sseErrorDetector.append(chunk);
});
responseBody.once("end", () => {
upstreamStreamEnded = true;
streamDetectedError ??= sseErrorDetector.finish();
if (responseCompleted || response.writableEnded) {
writeStreamLog();
}
});
if (shouldCaptureUsage) {
responseBody.once("end", () => {
void recordGatewayUsageCapture({
bodyText: sampler.read(),
client,
durationMs: Date.now() - startedAt,
fallbackModel: routedModel,
method,
path,
providerName: resolveProviderLogName(responseHeaders, this.config, routedModel),
providerProtocol: resolveResponseProviderProtocol(responseHeaders, this.config),
requestId,
responseHeaders,
statusCode: upstreamResponse.status
});
});
}
if (clientDisconnected || response.destroyed) {
onClientDisconnect();
return;
}
responseBody.pipe(response);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,892 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import { Readable } from "node:stream";
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, ProviderCredentialConfig, RouterFallbackConfig } from "@ccr/core/contracts/app";
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import { createRouteExecutionPlan } from "@ccr/core/routing/execution-plan";
import { rewriteRouteModelInUrl } from "@ccr/core/routing/protocol-adapter";
import { modelRegistryForConfig, normalizeRouteSelector, parseProviderModelSelector, providerRuntimeId } from "@ccr/core/routing/model-registry";
import { requestProtocolForPath } from "@ccr/core/routing/protocol-endpoints";
import { resolveConfiguredProviderModelSelector, resolveUniqueConfiguredProviderModelSelector } from "@ccr/core/routing/model-resolution";
import { estimateLimitUsage } from "@ccr/core/gateway/limits/window-limiter";
import { providerCredentialLimitState, readProviderCredentialCooldown, recordProviderCredentialOutcome } from "@ccr/core/providers/credential-pool";
import { isRecord, stringValue } from "@ccr/core/gateway/internal/value";
import { isLocalClaudeCodeOauthProviderPlugin, mergeAnthropicBetaValues } from "@ccr/core/providers/oauth-plugin";
import { abortSignalMessage, formatError, omitLocalObservabilityHeaders, shouldSendBody, withCoreGatewayAuthHeader } from "@ccr/core/gateway/http/io";
import { parseJsonObjectSafe, serializeJsonBodyWithModel } from "@ccr/core/gateway/http/body";
import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery";
import { activeProviderCredentials, findProviderByPublicOrInternalName, findProviderCredentialBySlug, normalizedProviderCapabilities, parseProviderCredentialInternalName, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCapabilityNameMatches, providerCredentialInternalName, providerCredentialPriority, providerCredentialRuntimeId, providerCredentialSlug, providerProtocolForClientProtocol, sanitizeHeaderValue } from "@ccr/core/providers/runtime-topology";
import { delay } from "@ccr/core/gateway/internal/clock";
import { retryDelayAfterNetworkError, retryDelayAfterStatus, shouldFallbackAfterStatus } from "@ccr/core/gateway/upstream/retry-policy";
import { claudeCodeOauthBetaHeader, claudeCodeOauthRequiredBeta, UpstreamRequestError } from "@ccr/core/gateway/internal/shared";
import type { ApiKeyLimitUsage, ProviderCredentialRoutingTarget, UpstreamAttempt, UpstreamFailedAttempt, UpstreamFetchResult } from "@ccr/core/gateway/internal/shared";
const providerCredentialSpilloverThreshold = 0.8;
export function applyProviderCapabilityRouting(input: {
body?: Buffer;
config: AppConfig;
fallback: RouterFallbackConfig;
headers: Record<string, string>;
path: string;
routedModel?: string;
}): { body?: Buffer; fallback: RouterFallbackConfig; routedModel?: string } {
const protocol = requestProtocolForPath(input.path);
if (!protocol) {
return {
body: input.body,
fallback: input.fallback,
routedModel: input.routedModel
};
}
rewriteProviderHeader(input.headers, "x-target-provider", input.config, protocol);
rewriteProviderListHeader(input.headers, "x-target-providers", input.config, protocol);
rewriteProviderHeader(input.headers, "x-gateway-target-provider", input.config, protocol);
const routedModel = rewriteModelSelectorForProtocol(input.routedModel, input.config, protocol);
const fallback = rewriteFallbackForProtocol(input.fallback, input.config, protocol);
const body = rewriteBodyModelForProtocol(input.body, input.config, protocol);
clearTargetProviderHeadersForModelSelector(input.headers, input.config, body, routedModel);
return {
body,
fallback,
routedModel
};
}
export function prepareGatewayUpstreamAttemptForTest(input: {
body: Record<string, unknown>;
config: AppConfig;
fallback?: RouterFallbackConfig;
headers: Record<string, string>;
method: string;
path: string;
routedModel?: string;
}): {
body?: Record<string, unknown>;
credentialChain?: string[];
credentialIds?: string[];
credentialProtocol?: GatewayProviderProtocol;
fallback: RouterFallbackConfig;
headers?: Record<string, string>;
logicalProvider?: string;
model?: string;
routedModel?: string;
} {
const headers = { ...input.headers };
const providerCapabilityRouting = applyProviderCapabilityRouting({
body: Buffer.from(`${JSON.stringify(input.body)}\n`, "utf8"),
config: input.config,
fallback: input.fallback ?? input.config.Router.fallback,
headers,
path: input.path,
routedModel: input.routedModel
});
const attempt = prepareUpstreamCredentialAttempt({
attempt: {
body: providerCapabilityRouting.body,
index: 0,
model: normalizeRouteSelector(providerCapabilityRouting.routedModel)
},
config: input.config,
headers,
method: input.method,
path: input.path
});
return {
body: parseJsonObjectSafe(attempt.body),
credentialChain: attempt.credentialChain,
credentialIds: attempt.credentialIds,
credentialProtocol: attempt.credentialProtocol,
fallback: providerCapabilityRouting.fallback,
headers: attempt.headers,
logicalProvider: attempt.logicalProvider,
model: attempt.model,
routedModel: providerCapabilityRouting.routedModel
};
}
function rewriteProviderHeader(
headers: Record<string, string>,
headerName: string,
config: AppConfig,
protocol: GatewayProviderProtocol
): void {
const value = headers[headerName];
if (!value) {
return;
}
headers[headerName] = rewriteProviderSelectorForProtocol(value, config, protocol);
}
function rewriteProviderListHeader(
headers: Record<string, string>,
headerName: string,
config: AppConfig,
protocol: GatewayProviderProtocol
): void {
const value = headers[headerName];
if (!value) {
return;
}
headers[headerName] = value
.split(",")
.map((item) => rewriteProviderSelectorForProtocol(item.trim(), config, protocol))
.filter(Boolean)
.join(",");
}
function rewriteProviderSelectorForProtocol(value: string, config: AppConfig, protocol: GatewayProviderProtocol): string {
const provider = findProviderByPublicOrInternalName(config, value);
const capability = provider ? providerCapabilityForClientProtocol(provider, protocol) : undefined;
return provider && capability ? providerCapabilityInternalName(provider, capability.type) : value;
}
function rewriteFallbackForProtocol(fallback: RouterFallbackConfig, config: AppConfig, protocol: GatewayProviderProtocol): RouterFallbackConfig {
const models = fallback.models.map((model) => rewriteModelSelectorForProtocol(model, config, protocol) ?? model);
return models.every((model, index) => model === fallback.models[index])
? fallback
: {
...fallback,
models
};
}
function rewriteBodyModelForProtocol(body: Buffer | undefined, config: AppConfig, protocol: GatewayProviderProtocol): Buffer | undefined {
const parsedBody = parseJsonObjectSafe(body);
if (!parsedBody) {
return body;
}
const model = stringValue(parsedBody.model);
const rewrittenModel = rewriteModelSelectorForProtocol(model, config, protocol);
if (!rewrittenModel || rewrittenModel === model) {
return body;
}
return Buffer.from(`${JSON.stringify({ ...parsedBody, model: rewrittenModel })}\n`, "utf8");
}
function clearTargetProviderHeadersForModelSelector(
headers: Record<string, string>,
config: AppConfig,
body: Buffer | undefined,
routedModel: string | undefined
): void {
const parsedBody = parseJsonObjectSafe(body);
const model = stringValue(parsedBody?.model) || routedModel;
if (!resolveConfiguredProviderModelSelector(model, config)) {
return;
}
delete headers["x-target-provider"];
delete headers["x-target-providers"];
delete headers["x-gateway-target-provider"];
}
function rewriteModelSelectorForProtocol(
model: string | undefined,
config: AppConfig,
protocol: GatewayProviderProtocol
): string | undefined {
const normalized = normalizeRouteSelector(model);
if (!normalized) {
return model;
}
const publicModel = resolveGatewayPublicModelId(normalized, config) ?? normalized;
const selector =
resolveConfiguredProviderModelSelector(publicModel, config) ??
resolveUniqueConfiguredProviderModelSelector(publicModel, config);
const capability = selector ? providerCapabilityForClientProtocol(selector.provider, protocol) : undefined;
return selector && capability
? `${providerCapabilityInternalName(selector.provider, capability.type)}/${selector.model}`
: publicModel;
}
export function rewriteCapabilityResponseHeaders(headers: Headers, config: AppConfig): Headers {
const providerName = headers.get("x-gateway-target-provider-name")?.trim();
if (!providerName) {
return headers;
}
const credentialInternalName = parseProviderCredentialInternalName(providerName);
if (credentialInternalName) {
const provider = findProviderByPublicOrInternalName(config, credentialInternalName.providerId);
if (!provider) {
return headers;
}
const credential = findProviderCredentialBySlug(provider, credentialInternalName.credentialSlug);
const rewritten = new Headers(headers);
rewritten.set("x-gateway-target-provider-name", providerRuntimeId(provider));
rewritten.set("x-ccr-provider-protocol", credentialInternalName.protocol);
rewritten.set("x-ccr-provider-credential-provider", providerRuntimeId(provider));
rewritten.set("x-ccr-provider-credential-id", providerCredentialSlug(credential ? providerCredentialRuntimeId(provider, credential) : credentialInternalName.credentialSlug));
return rewritten;
}
const provider = findProviderByPublicOrInternalName(config, providerName);
if (!provider) {
return headers;
}
const capability = normalizedProviderCapabilities(provider).find((item) =>
providerCapabilityNameMatches(provider, item.type, providerName)
);
const rewritten = new Headers(headers);
rewritten.set("x-gateway-target-provider-name", providerRuntimeId(provider));
if (capability) {
rewritten.set("x-ccr-provider-protocol", capability.type);
}
return rewritten;
}
export async function fetchUpstreamWithFallback(input: {
body?: Buffer;
config: AppConfig;
coreAuthToken: string;
fallback: RouterFallbackConfig;
headers: Record<string, string>;
method: string;
path: string;
routedModel?: string;
signal?: AbortSignal;
upstreamUrl: string;
}): Promise<UpstreamFetchResult> {
const fallbackMode = input.fallback.mode;
const attempts = buildUpstreamAttempts(
input.config,
input.fallback,
input.method,
input.path,
input.body,
input.routedModel
);
const failedAttempts: UpstreamFailedAttempt[] = [];
for (let index = 0; index < attempts.length; index += 1) {
if (input.signal?.aborted) {
throw new UpstreamRequestError(abortSignalMessage(input.signal), {
failedAttempts
});
}
const attempt = prepareUpstreamCredentialAttempt({
attempt: attempts[index],
config: input.config,
headers: input.headers,
method: input.method,
path: input.path
});
const hasNextAttempt = index < attempts.length - 1;
try {
const response = await fetchWithSystemProxy(rewriteRouteModelInUrl(input.upstreamUrl, attempt.model), {
body: shouldSendBody(input.method) ? attempt.body?.toString("utf8") : undefined,
headers: withCoreGatewayAuthHeader(omitLocalObservabilityHeaders(attempt.headers ?? input.headers), input.coreAuthToken),
method: input.method,
signal: input.signal
});
if (hasNextAttempt && shouldFallbackAfterStatus(response.status, fallbackMode)) {
const delayMs = retryDelayAfterStatus(response.headers, failedAttempts.length);
failedAttempts.push({
credentialChain: attempt.credentialChain,
credentialIds: attempt.credentialIds,
delayMs,
model: attempt.model,
statusCode: response.status
});
recordProviderCredentialOutcome(input.config, input.method, attempt, response.status, response.headers);
await drainResponseBody(response);
if (delayMs > 0) {
await delay(delayMs);
}
continue;
}
return {
attempt,
failedAttempts,
response
};
} catch (error) {
const message = formatError(error);
const delayMs = hasNextAttempt && !input.signal?.aborted
? retryDelayAfterNetworkError(failedAttempts.length)
: 0;
failedAttempts.push({
credentialChain: attempt.credentialChain,
credentialIds: attempt.credentialIds,
delayMs,
error: message,
model: attempt.model
});
if (input.signal?.aborted) {
throw new UpstreamRequestError(abortSignalMessage(input.signal), {
attempt,
cause: error,
failedAttempts
});
}
if (hasNextAttempt) {
if (delayMs > 0) {
await delay(delayMs);
}
continue;
}
throw new UpstreamRequestError(message, {
attempt,
cause: error,
failedAttempts
});
}
}
throw new UpstreamRequestError("Gateway request failed before reaching an upstream provider.", {
failedAttempts
});
}
function prepareUpstreamCredentialAttempt(input: {
attempt: UpstreamAttempt;
config: AppConfig;
headers: Record<string, string>;
method: string;
path: string;
}): UpstreamAttempt {
const normalizedBody = normalizeConfiguredProviderModelBody(input.attempt.body, input.config);
const target = resolvePlannedProviderCredentialRoutingTarget(input.attempt, input.path) ??
resolveProviderCredentialRoutingTarget(input.config, input.headers, input.path, input.attempt.body);
const attemptBody = (body: Buffer | undefined) => usageAwareOpenAiChatAttemptBody({
body,
config: input.config,
path: input.path,
target
});
if (!target) {
const body = bodyHasConfiguredProviderModelSelector(input.attempt.body, input.config)
? input.attempt.body
: normalizedBody?.body ?? input.attempt.body;
return {
...input.attempt,
body: attemptBody(body),
headers: input.headers
};
}
const attemptHeaders = withClaudeCodeOauthBetaHeader(input.headers, input.config, target);
const credentials = activeProviderCredentials(target.provider);
if (credentials.length === 0) {
const preserveModelSelector = shouldPreserveCapabilityModelSelector(input.attempt.body, target);
return {
...input.attempt,
body: attemptBody(preserveModelSelector ? input.attempt.body : target.body ?? normalizedBody?.body ?? input.attempt.body),
headers: preserveModelSelector
? clearTargetProviderHeaders(attemptHeaders)
: targetProviderFallbackHeaders(attemptHeaders, target.provider, target.protocol)
};
}
const usage = estimateLimitUsage(input.method, input.attempt.body ?? Buffer.alloc(0));
const selection = selectProviderCredentials(target.provider, target.protocol, credentials, usage);
if (selection.credentials.length === 0) {
const preserveModelSelector = shouldPreserveCapabilityModelSelector(input.attempt.body, target);
return {
...input.attempt,
body: attemptBody(preserveModelSelector ? input.attempt.body : target.body ?? normalizedBody?.body ?? input.attempt.body),
headers: preserveModelSelector
? clearTargetProviderHeaders(attemptHeaders)
: targetProviderFallbackHeaders(attemptHeaders, target.provider, target.protocol)
};
}
const headers: Record<string, string> = {
...attemptHeaders,
"x-target-providers": selection.credentials.map((candidate) => candidate.internalName).join(","),
"x-ccr-logical-provider": providerRuntimeId(target.provider),
"x-ccr-provider-credential-chain": selection.credentials.map((candidate) => candidate.credentialId).join(",")
};
delete headers["x-target-provider"];
if (selection.saturated) {
headers["x-ccr-provider-credential-saturated"] = "true";
}
return {
...input.attempt,
body: attemptBody(target.body ?? normalizedBody?.body ?? input.attempt.body),
credentialChain: selection.credentials.map((candidate) => candidate.internalName),
credentialIds: selection.credentials.map((candidate) => candidate.credentialId),
credentialProtocol: target.protocol,
headers,
logicalProvider: target.provider.name
};
}
function withClaudeCodeOauthBetaHeader(
headers: Record<string, string>,
config: AppConfig,
target: ProviderCredentialRoutingTarget
): Record<string, string> {
if (
target.protocol !== "anthropic_messages" ||
!claudeCodeOauthPluginMatchesTarget(config, target.provider, target.protocol)
) {
return headers;
}
const existingEntry = Object.entries(headers)
.find(([name]) => name.trim().toLowerCase() === claudeCodeOauthBetaHeader);
const merged = mergeAnthropicBetaValues(existingEntry?.[1], claudeCodeOauthRequiredBeta);
if (existingEntry?.[0] === claudeCodeOauthBetaHeader && existingEntry[1] === merged) {
return headers;
}
const next = Object.fromEntries(
Object.entries(headers).filter(([name]) => name.trim().toLowerCase() !== claudeCodeOauthBetaHeader)
);
next[claudeCodeOauthBetaHeader] = merged;
return next;
}
function claudeCodeOauthPluginMatchesTarget(
config: AppConfig,
provider: GatewayProviderConfig,
protocol: GatewayProviderProtocol
): boolean {
const targetNames = new Set([
provider.name,
providerRuntimeId(provider),
providerCapabilityInternalName(provider, protocol)
].map((name) => name.trim().toLowerCase()));
return (config.providerPlugins ?? []).some((plugin) => {
if (!isLocalClaudeCodeOauthProviderPlugin(plugin)) {
return false;
}
const providerName = stringValue(plugin.providerName)?.toLowerCase();
return Boolean(providerName && targetNames.has(providerName));
});
}
function targetProviderFallbackHeaders(
headers: Record<string, string>,
provider: GatewayProviderConfig,
protocol: GatewayProviderProtocol
): Record<string, string> {
const next = { ...headers };
next["x-target-provider"] = targetProviderHeaderValue(provider, protocol);
delete next["x-target-providers"];
delete next["x-gateway-target-provider"];
return next;
}
function clearTargetProviderHeaders(headers: Record<string, string>): Record<string, string> {
const next = { ...headers };
delete next["x-target-provider"];
delete next["x-target-providers"];
delete next["x-gateway-target-provider"];
return next;
}
function shouldPreserveCapabilityModelSelector(body: Buffer | undefined, target: ProviderCredentialRoutingTarget): boolean {
if (target.source === "header" || target.protocol !== "gemini_interactions") {
return false;
}
return Boolean(parseProviderModelSelector(stringValue(parseJsonObjectSafe(body)?.model)));
}
function resolvePlannedProviderCredentialRoutingTarget(
attempt: UpstreamAttempt,
path: string
): ProviderCredentialRoutingTarget | undefined {
if (attempt.target?.kind !== "provider") {
return undefined;
}
const clientProtocol = requestProtocolForPath(path);
const protocol = clientProtocol
? providerProtocolForClientProtocol(attempt.target.provider, clientProtocol)
: undefined;
if (!protocol) {
return undefined;
}
const parsedBody = parseJsonObjectSafe(attempt.body);
return {
body: parsedBody && clientProtocol !== "gemini_generate_content"
? serializeJsonBodyWithModel(parsedBody, attempt.target.model)
: attempt.body,
model: attempt.target.model,
provider: attempt.target.provider,
protocol,
source: "plan"
};
}
function targetProviderHeaderValue(provider: GatewayProviderConfig, protocol: GatewayProviderProtocol): string {
const capability = normalizedProviderCapabilities(provider).find((item) => item.type === protocol);
return capability ? providerCapabilityInternalName(provider, capability.type) : provider.name || providerRuntimeId(provider);
}
function usageAwareOpenAiChatAttemptBody(input: {
body: Buffer | undefined;
config: AppConfig;
path: string;
target?: { protocol: GatewayProviderProtocol };
}): Buffer | undefined {
const clientProtocol = requestProtocolForPath(input.path);
const parsedBody = parseJsonObjectSafe(input.body);
const modelSelector = resolveConfiguredProviderModelSelector(stringValue(parsedBody?.model), input.config);
const providerProtocol = input.target?.protocol ?? (
modelSelector && clientProtocol
? providerProtocolForClientProtocol(modelSelector.provider, clientProtocol)
: undefined
);
if (providerProtocol !== "openai_chat_completions" && providerProtocol !== "openai_responses") {
return input.body;
}
const sanitizedBody = stripUnsupportedOpenAiRequestParameters(input.body);
return providerProtocol === "openai_chat_completions"
? usageAwareOpenAiChatBody(sanitizedBody)
: sanitizedBody;
}
function stripUnsupportedOpenAiRequestParameters(body: Buffer | undefined): Buffer | undefined {
const parsedBody = parseJsonObjectSafe(body);
if (!parsedBody || (!("thinking" in parsedBody) && !("reasoning_split" in parsedBody))) {
return body;
}
const next = { ...parsedBody };
delete next.thinking;
delete next.reasoning_split;
return Buffer.from(`${JSON.stringify(next)}\n`, "utf8");
}
function usageAwareOpenAiChatBody(body: Buffer | undefined): Buffer | undefined {
const parsedBody = parseJsonObjectSafe(body);
if (!parsedBody || parsedBody.stream !== true) {
return body;
}
const streamOptions = isRecord(parsedBody.stream_options)
? parsedBody.stream_options
: isRecord(parsedBody.streamOptions)
? parsedBody.streamOptions
: {};
if (streamOptions.include_usage === true || streamOptions.includeUsage === true) {
return body;
}
return Buffer.from(`${JSON.stringify({
...parsedBody,
stream_options: {
...streamOptions,
include_usage: true
}
})}\n`, "utf8");
}
function normalizeConfiguredProviderModelBody(
body: Buffer | undefined,
config: AppConfig
): { body: Buffer; model: string } | undefined {
const parsedBody = parseJsonObjectSafe(body);
const model = stringValue(parsedBody?.model);
const selector = resolveConfiguredProviderModelSelector(model, config);
if (!parsedBody || !selector || selector.model === model) {
return undefined;
}
return {
body: serializeJsonBodyWithModel(parsedBody, selector.model),
model: selector.model
};
}
function bodyHasConfiguredProviderModelSelector(body: Buffer | undefined, config: AppConfig): boolean {
const parsedBody = parseJsonObjectSafe(body);
const model = stringValue(parsedBody?.model);
return Boolean(resolveConfiguredProviderModelSelector(model, config));
}
function resolveProviderCredentialRoutingTarget(
config: AppConfig,
headers: Record<string, string>,
path: string,
body: Buffer | undefined
): ProviderCredentialRoutingTarget | undefined {
const protocol = requestProtocolForPath(path);
if (!protocol) {
return undefined;
}
const parsedBody = parseJsonObjectSafe(body);
const bodyModel = stringValue(parsedBody?.model);
const modelSelector = resolveConfiguredProviderModelSelector(bodyModel, config) ??
resolveUniqueConfiguredProviderModelSelector(bodyModel, config);
if (modelSelector) {
const provider = modelSelector.provider;
const providerProtocol = provider ? providerProtocolForClientProtocol(provider, protocol) : undefined;
if (provider && providerProtocol) {
return {
body: parsedBody ? serializeJsonBodyWithModel(parsedBody, modelSelector.model) : body,
model: modelSelector.model,
provider,
protocol: providerProtocol,
source: "model"
};
}
}
const targetProviderName = firstTargetProviderHeader(headers);
if (!targetProviderName) {
return undefined;
}
const provider = findProviderByPublicOrInternalName(config, targetProviderName);
if (!provider) {
return undefined;
}
const providerProtocol = providerProtocolForClientProtocol(provider, protocol);
if (!providerProtocol) {
return undefined;
}
const providerModel = resolveModelForProvider(bodyModel, provider);
return {
body: parsedBody && providerModel && providerModel !== bodyModel
? serializeJsonBodyWithModel(parsedBody, providerModel)
: body,
model: providerModel ?? bodyModel,
provider,
protocol: providerProtocol,
source: "header"
};
}
function resolveModelForProvider(
value: string | undefined,
provider: GatewayProviderConfig
): string | undefined {
const normalized = normalizeRouteSelector(value);
if (!normalized) {
return undefined;
}
if (providerHasModel(provider, normalized)) {
return normalized;
}
const parsed = parseProviderModelSelector(normalized);
return parsed && providerHasModel(provider, parsed.model) ? parsed.model : undefined;
}
function providerHasModel(provider: GatewayProviderConfig, model: string): boolean {
const normalized = model.trim().toLowerCase();
return Boolean(normalized) && provider.models.some((candidate) => candidate.trim().toLowerCase() === normalized);
}
function firstTargetProviderHeader(headers: Record<string, string>): string | undefined {
const provider = headers["x-target-provider"] || headers["x-gateway-target-provider"];
if (provider?.trim()) {
return provider.trim();
}
const providers = headers["x-target-providers"];
return providers
?.split(",")
.map((item) => item.trim())
.find(Boolean);
}
function selectProviderCredentials(
provider: GatewayProviderConfig,
protocol: GatewayProviderProtocol,
credentials: ProviderCredentialConfig[],
usage: ApiKeyLimitUsage
): { credentials: Array<{ credential: ProviderCredentialConfig; credentialId: string; internalName: string }>; saturated: boolean } {
const candidates = credentials.map((credential, index) => {
const providerIndex = provider.credentials?.indexOf(credential) ?? index;
const limitState = providerCredentialLimitState(provider, credential, usage);
const cooldown = readProviderCredentialCooldown(provider, credential);
return {
cooldown,
credential,
credentialId: providerCredentialSlug(providerCredentialRuntimeId(provider, credential, providerIndex)),
index: providerIndex,
internalName: providerCredentialInternalName(provider, protocol, credential),
limitState,
priority: providerCredentialPriority(credential, providerIndex),
weight: Math.max(1, credential.weight ?? 1)
};
});
const available = candidates.filter((candidate) => !candidate.cooldown && !candidate.limitState.blocked);
const sorted = sortProviderCredentialCandidates(available.length > 0 ? available : candidates);
return {
credentials: sorted.map((candidate) => ({
credential: candidate.credential,
credentialId: candidate.credentialId,
internalName: candidate.internalName
})),
saturated: available.length === 0 && candidates.length > 0
};
}
function sortProviderCredentialCandidates<T extends {
index: number;
limitState: { utilization: number };
priority: number;
weight: number;
}>(candidates: T[]): T[] {
const prioritySorted = [...candidates].sort((left, right) =>
left.priority - right.priority ||
left.limitState.utilization - right.limitState.utilization ||
right.weight - left.weight ||
left.index - right.index
);
const primaryPriority = prioritySorted[0]?.priority;
const primaryCandidates = prioritySorted.filter((candidate) => candidate.priority === primaryPriority);
const shouldSpillOver = primaryCandidates.length > 0 &&
primaryCandidates.every((candidate) => candidate.limitState.utilization >= providerCredentialSpilloverThreshold);
if (shouldSpillOver) {
return prioritySorted.sort((left, right) =>
left.limitState.utilization - right.limitState.utilization ||
left.priority - right.priority ||
right.weight - left.weight ||
left.index - right.index
);
}
return prioritySorted;
}
function buildUpstreamAttempts(
config: AppConfig,
fallback: RouterFallbackConfig,
method: string,
path: string,
body: Buffer | undefined,
routedModel: string | undefined
): UpstreamAttempt[] {
const parsedBody = parseJsonObjectSafe(body);
const modelInPath = requestProtocolForPath(path) === "gemini_generate_content";
const plan = createRouteExecutionPlan({
bodyModel: modelInPath ? undefined : stringValue(parsedBody?.model),
fallback,
hasRequestBody: shouldSendBody(method) && (fallback.mode !== "model-chain" || Boolean(parsedBody)),
modelRegistry: modelRegistryForConfig(config),
primaryModel: routedModel
});
return plan.attempts.map((attempt) => ({
body: parsedBody && !modelInPath && fallback.mode === "model-chain" && attempt.model
? serializeJsonBodyWithModel(parsedBody, attempt.model)
: body,
index: attempt.index,
model: attempt.model,
target: attempt.target
}));
}
async function drainResponseBody(response: Response): Promise<void> {
try {
await response.arrayBuffer();
} catch {
// The failed attempt is already being skipped; body drain errors should not block the next attempt.
}
}
export async function cancelResponseBody(response: Response): Promise<void> {
try {
await response.body?.cancel();
} catch {
// The client already disconnected; best-effort upstream cleanup must not mask that expected path.
}
}
export function uniqueStreams(streams: Readable[]): Readable[] {
return [...new Set(streams)];
}
export function destroyResponseStreams(streams: Readable[]): void {
for (const stream of streams) {
if (!stream.destroyed) {
// A downstream client close is an expected abort path. Destroying with
// an Error would emit another error event on Readable/Transform stages,
// and intermediate stages may not be the final responseBody listener.
stream.destroy();
}
}
}
export function mergeFallbackResponseHeaders(headers: Headers, result: UpstreamFetchResult): Headers {
const credentialIds = result.attempt.credentialIds ?? [];
const credentialSaturated = result.attempt.headers?.["x-ccr-provider-credential-saturated"] === "true";
if (result.failedAttempts.length === 0 && credentialIds.length === 0 && !credentialSaturated) {
return headers;
}
const merged = new Headers(headers);
if (result.failedAttempts.length > 0) {
merged.set("x-ccr-fallback-attempts", String(result.failedAttempts.length + 1));
merged.set("x-ccr-fallback-failures", formatFallbackFailures(result.failedAttempts));
if (result.failedAttempts.some((attempt) => (attempt.delayMs ?? 0) > 0)) {
merged.set("x-ccr-fallback-delays-ms", formatFallbackDelays(result.failedAttempts));
}
if (result.attempt.model) {
merged.set("x-ccr-fallback-model", sanitizeHeaderValue(result.attempt.model));
}
}
if (credentialIds.length) {
merged.set("x-ccr-provider-credential-chain", credentialIds.join(","));
}
if (credentialSaturated) {
merged.set("x-ccr-provider-credential-saturated", "true");
}
return merged;
}
export function upstreamResponseHeaders(result: UpstreamFetchResult): Headers {
return result.response.headers;
}
function formatFallbackFailures(failedAttempts: UpstreamFailedAttempt[]): string {
return failedAttempts
.map((attempt) => attempt.statusCode ? String(attempt.statusCode) : attempt.error ? "network" : "failed")
.join(",");
}
function formatFallbackDelays(failedAttempts: UpstreamFailedAttempt[]): string {
return failedAttempts
.map((attempt) => String(Math.max(0, attempt.delayMs ?? 0)))
.join(",");
}
@@ -0,0 +1,53 @@
import type { RouterFallbackMode } from "@ccr/core/contracts/app";
import { classifyRouteFailure } from "@ccr/core/routing/failure-classifier";
import { clampNumber } from "@ccr/core/gateway/internal/collections";
const upstreamRetryBackoffBaseMs = 1_000;
const upstreamRetryBackoffMaxMs = 30_000;
const upstreamRetryAfterMaxMs = 60_000;
export function shouldFallbackAfterStatus(statusCode: number, mode: RouterFallbackMode): boolean {
return classifyRouteFailure(statusCode, mode).shouldFallback;
}
export function retryDelayAfterStatus(headers: Headers, failedAttemptIndex: number): number {
const retryAfterMs = parseRetryAfterHeaderMs(headers.get("retry-after"));
if (retryAfterMs !== undefined && retryAfterMs > 0) {
return clampNumber(retryAfterMs, 1, upstreamRetryAfterMaxMs);
}
return exponentialRetryBackoffMs(failedAttemptIndex);
}
export function retryDelayAfterNetworkError(failedAttemptIndex: number): number {
return exponentialRetryBackoffMs(failedAttemptIndex);
}
export function fallbackRetryDelayAfterStatusForTest(input: {
failedAttemptIndex?: number;
retryAfter?: string | null;
statusCode: number;
}): number {
const headers = new Headers();
if (input.retryAfter !== undefined && input.retryAfter !== null) {
headers.set("retry-after", input.retryAfter);
}
return retryDelayAfterStatus(headers, input.failedAttemptIndex ?? 0);
}
export function fallbackRetryDelayAfterNetworkErrorForTest(failedAttemptIndex = 0): number {
return retryDelayAfterNetworkError(failedAttemptIndex);
}
function parseRetryAfterHeaderMs(value: string | null): number | undefined {
const trimmed = value?.trim();
if (!trimmed) return undefined;
const seconds = Number(trimmed);
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
const retryAt = Date.parse(trimmed);
return Number.isFinite(retryAt) ? Math.max(0, retryAt - Date.now()) : undefined;
}
function exponentialRetryBackoffMs(failedAttemptIndex: number): number {
const exponent = Math.min(10, Math.max(0, failedAttemptIndex));
return Math.min(upstreamRetryBackoffMaxMs, upstreamRetryBackoffBaseMs * 2 ** exponent);
}
+733
View File
@@ -0,0 +1,733 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import { join as pathJoin } from "node:path";
import type { AppConfig, GatewayMcpServerConfig, VirtualModelFusionVisionConfig, VirtualModelFusionWebSearchConfig, VirtualModelFusionWebSearchProvider } from "@ccr/core/contracts/app";
import { BUILTIN_FUSION_VISION_TOOL_NAME, BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME } from "@ccr/core/contracts/app";
import { TOOL_HUB_MCP_SERVER_NAME, toolHubBuiltInBackendServers, toolHubMcpRuntimeConfig, toolHubRequestTimeoutMs } from "@ccr/core/mcp/toolhub-config";
import { isRecord, numberValue, stringListValue, stringValue } from "@ccr/core/gateway/internal/value";
import { defaultFusionWebSearchProvider, fusionModelProviderName } from "@ccr/core/gateway/internal/shared";
import type { BrowserWebSearchMcpIntegration, CoreGatewayProvider } from "@ccr/core/gateway/internal/shared";
import { uniqueStrings } from "@ccr/core/gateway/internal/collections";
export async function fusionBuiltinToolArtifacts(
profiles: unknown[],
coreEndpoint: string,
coreAuthToken: string,
browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration
): Promise<{ mcpServers: GatewayMcpServerConfig[]; providers: CoreGatewayProvider[] }> {
const providers: CoreGatewayProvider[] = [];
const mcpServers: GatewayMcpServerConfig[] = [];
const toolServerKeys = new Set<string>();
const entry = bundledFusionBuiltinMcpEntryPath();
for (const [index, profile] of profiles.entries()) {
if (!isRecord(profile) || profile.enabled === false) {
continue;
}
const metadata = isRecord(profile.metadata) ? profile.metadata : undefined;
const profileId = stringValue(profile.id) || stringValue(profile.key) || `fusion-${index + 1}`;
const sanitizedProfileId = sanitizeMcpServerName(profileId);
const visionConfig = readFusionVisionConfig(metadata?.fusionVision) ?? legacyFusionVisionConfig(profile);
if (visionConfig?.toolName) {
const resolvedVision = resolveFusionVisionRuntime(visionConfig);
providers.push(...resolvedVision.providers);
const toolServerKey = `vision:${visionConfig.toolName}`;
if (!toolServerKeys.has(toolServerKey)) {
toolServerKeys.add(toolServerKey);
const useGatewayVisionRuntime = !visionConfig.baseUrl;
mcpServers.push(fusionBuiltinMcpServer({
entry,
env: {
FUSION_BUILTIN_TOOL_KIND: "vision",
FUSION_TOOL_NAME: visionConfig.toolName,
...(useGatewayVisionRuntime ? { VISION_GATEWAY_BASE_URL: `${coreEndpoint}/v1` } : { VISION_BASE_URL: visionConfig.baseUrl || "" }),
...(useGatewayVisionRuntime && coreAuthToken ? { VISION_GATEWAY_API_KEY: coreAuthToken } : {}),
...(resolvedVision.model ? { VISION_MODEL: resolvedVision.model } : {}),
...(visionConfig.baseUrl && visionConfig.apiKey ? { VISION_API_KEY: visionConfig.apiKey } : {}),
...(visionConfig.timeoutMs ? { VISION_TIMEOUT_MS: String(visionConfig.timeoutMs) } : {})
},
name: `fusion-vision-${sanitizedProfileId}`
}));
}
}
const webSearchConfig = readFusionWebSearchConfig(metadata?.fusionWebSearch) ?? legacyFusionWebSearchConfig(profile);
if (webSearchConfig?.toolName) {
const toolServerKey = `web_search:${webSearchConfig.toolName}`;
if (!toolServerKeys.has(toolServerKey)) {
toolServerKeys.add(toolServerKey);
const provider = webSearchConfig.provider ?? defaultFusionWebSearchProvider;
if (provider === "browser") {
const browserMcpServer = await browserWebSearchMcpIntegration?.registerBrowserWebSearchMcpServer({
env: webSearchConfig.env ?? {},
name: `fusion-browser-web-search-${sanitizedProfileId}`,
resultCount: webSearchConfig.resultCount,
timeoutMs: webSearchConfig.timeoutMs,
toolName: webSearchConfig.toolName
});
if (browserMcpServer) {
mcpServers.push(browserMcpServer);
}
} else {
mcpServers.push(fusionBuiltinMcpServer({
entry,
env: {
FUSION_BUILTIN_TOOL_KIND: "web_search",
FUSION_TOOL_NAME: webSearchConfig.toolName,
SEARCH_PROVIDER: provider,
...(webSearchConfig.resultCount ? { SEARCH_RESULT_COUNT: String(webSearchConfig.resultCount) } : {}),
...(webSearchConfig.timeoutMs ? { SEARCH_TIMEOUT_MS: String(webSearchConfig.timeoutMs) } : {}),
...(webSearchConfig.env ?? {})
},
name: `fusion-web-search-${sanitizedProfileId}`
}));
}
}
}
}
return { mcpServers, providers };
}
export async function fusionBuiltinToolArtifactsForTest(
profiles: unknown[],
coreEndpoint: string,
coreAuthToken: string,
browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration
): Promise<{ mcpServers: GatewayMcpServerConfig[]; providers: unknown[] }> {
return fusionBuiltinToolArtifacts(profiles, coreEndpoint, coreAuthToken, browserWebSearchMcpIntegration);
}
function fusionBuiltinMcpServer({
entry,
env,
name
}: {
entry: string;
env: Record<string, string>;
name: string;
}): GatewayMcpServerConfig {
return {
args: [entry],
command: process.execPath,
env: {
ELECTRON_RUN_AS_NODE: "1",
...env
},
name,
protocolVersion: "2024-11-05",
requestTimeoutMs: 600000,
startupTimeoutMs: 600000,
stdioMessageMode: "content-length",
transport: "stdio"
};
}
function bundledFusionBuiltinMcpEntryPath(): string {
return pathJoin(__dirname, "fusion-vision-mcp.js");
}
export function fusionToolFallbackMcpServer(
profiles: unknown[],
existingServers: unknown[]
): GatewayMcpServerConfig | undefined {
const tools = fusionFallbackToolDefinitions(profiles, fusionToolNamesBackedByMcpServers(existingServers));
if (tools.length === 0) {
return undefined;
}
return {
args: [bundledFusionToolFallbackMcpEntryPath()],
command: process.execPath,
env: {
ELECTRON_RUN_AS_NODE: "1",
FUSION_FALLBACK_TOOLS_JSON: JSON.stringify(tools)
},
name: uniqueMcpServerName("ccr-fusion-tool-fallback", existingServers),
protocolVersion: "2024-11-05",
requestTimeoutMs: 600000,
startupTimeoutMs: 600000,
stdioMessageMode: "content-length",
transport: "stdio"
};
}
function bundledFusionToolFallbackMcpEntryPath(): string {
return pathJoin(__dirname, "fusion-tool-fallback-mcp.js");
}
export function toolHubMcpServer(config: AppConfig, backendServers: unknown[]): GatewayMcpServerConfig | undefined {
const toolHub = config.toolHub;
const runtimeBackendServers = [
...toolHubBuiltInBackendServers(config),
...backendServers
];
const runtimeConfig = toolHubMcpRuntimeConfig(config, runtimeBackendServers);
if (!toolHub?.enabled || !runtimeConfig) {
return undefined;
}
return {
...runtimeConfig,
name: uniqueMcpServerName(TOOL_HUB_MCP_SERVER_NAME, runtimeBackendServers),
protocolVersion: "2024-11-05",
requestTimeoutMs: toolHubRequestTimeoutMs(config, runtimeBackendServers),
startupTimeoutMs: 600000,
stdioMessageMode: "content-length",
transport: "stdio"
};
}
export function fusionFallbackToolDefinitions(
profiles: unknown[],
backedToolNames: Set<string> = new Set()
): FusionFallbackToolDefinition[] {
const byName = new Map<string, FusionFallbackToolDefinition>();
for (const profile of profiles) {
if (!isRecord(profile) || profile.enabled === false) {
continue;
}
if (Array.isArray(profile.tools)) {
for (const tool of profile.tools) {
if (!isRecord(tool)) {
continue;
}
const name = stringValue(tool.name);
if (!name) {
continue;
}
if (backedToolNames.has(name)) {
continue;
}
const existing = byName.get(name);
const description = stringValue(tool.description);
const inputSchema = isRecord(tool.inputSchema)
? tool.inputSchema
: isRecord(tool.input_schema)
? tool.input_schema
: undefined;
const unavailableMessage = fusionFallbackToolUnavailableMessage(profile, name);
if (existing) {
if (!existing.description && description) {
existing.description = description;
}
if (!existing.inputSchema && inputSchema) {
existing.inputSchema = inputSchema;
}
if (!existing.unavailableMessage && unavailableMessage) {
existing.unavailableMessage = unavailableMessage;
}
continue;
}
byName.set(name, {
...(description ? { description } : {}),
...(inputSchema ? { inputSchema } : {}),
...(unavailableMessage ? { unavailableMessage } : {}),
name
});
}
}
const browserFallback = browserWebSearchFallbackToolDefinition(profile, backedToolNames);
if (browserFallback && !byName.has(browserFallback.name)) {
byName.set(browserFallback.name, browserFallback);
}
}
return [...byName.values()];
}
type FusionFallbackToolDefinition = {
description?: string;
inputSchema?: Record<string, unknown>;
name: string;
unavailableMessage?: string;
};
function fusionFallbackToolUnavailableMessage(profile: unknown, toolName: string): string | undefined {
if (!isRecord(profile)) {
return undefined;
}
const metadata = isRecord(profile.metadata) ? profile.metadata : undefined;
const fusionWebSearch = isRecord(metadata?.fusionWebSearch) ? metadata.fusionWebSearch : undefined;
const webSearchConfig = readFusionWebSearchConfig(fusionWebSearch);
if (webSearchConfig?.provider !== "browser" || webSearchConfig.toolName !== toolName) {
return undefined;
}
return browserWebSearchUnavailableMessage(toolName);
}
export function browserWebSearchUnavailableMessage(toolName: string): string {
return [
`Fusion MCP tool "${toolName}" is unavailable because In-app Browser web search requires CCR Desktop.`,
"This runtime did not register the Electron browser web search integration, so the hidden browser search tool cannot run here.",
"Run the profile in CCR Desktop or switch the Fusion web search provider to Brave, Bing, Google CSE, Serper, SerpAPI, Tavily, or Exa."
].join(" ");
}
function browserWebSearchFallbackToolDefinition(
profile: Record<string, unknown>,
backedToolNames: Set<string>
): FusionFallbackToolDefinition | undefined {
const metadata = isRecord(profile.metadata) ? profile.metadata : undefined;
const fusionWebSearch = isRecord(metadata?.fusionWebSearch) ? metadata.fusionWebSearch : undefined;
const webSearchConfig = readFusionWebSearchConfig(fusionWebSearch);
if (webSearchConfig?.provider !== "browser" || !webSearchConfig.toolName || backedToolNames.has(webSearchConfig.toolName)) {
return undefined;
}
return {
description: "Fallback registration for CCR In-app Browser web search when the Electron browser integration is unavailable.",
inputSchema: {
additionalProperties: true,
properties: {
count: { maximum: 20, minimum: 1, type: "number" },
prompt: { type: "string" },
query: { type: "string" }
},
required: ["prompt"],
type: "object"
},
name: webSearchConfig.toolName,
unavailableMessage: fusionFallbackToolUnavailableMessage(profile, webSearchConfig.toolName)
};
}
export function fusionToolNamesBackedByMcpServers(servers: unknown[]): Set<string> {
const names = new Set<string>();
for (const server of servers) {
if (!isRecord(server)) {
continue;
}
const serverName = stringValue(server.name);
if (serverName) {
names.add(serverName);
}
const env = isRecord(server.env) ? server.env : undefined;
const fusionToolName = stringValue(env?.FUSION_TOOL_NAME);
if (fusionToolName) {
names.add(fusionToolName);
}
}
return names;
}
function uniqueMcpServerName(baseName: string, servers: unknown[]): string {
const used = new Set(
servers
.map((server) => isRecord(server) ? stringValue(server.name)?.toLowerCase() : undefined)
.filter((name): name is string => Boolean(name))
);
if (!used.has(baseName.toLowerCase())) {
return baseName;
}
for (let index = 2; ; index += 1) {
const candidate = `${baseName}-${index}`;
if (!used.has(candidate.toLowerCase())) {
return candidate;
}
}
}
export function withFusionVirtualModelAliases(profiles: unknown[]): unknown[] {
return profiles.map((profile) => {
if (!isRecord(profile)) {
return profile;
}
const match = isRecord(profile.match) ? profile.match : {};
const exactAliases = stringListValue(match.exactAliases);
const catalogNames = exactAliases.length > 0
? exactAliases
: [stringValue(profile.key) || stringValue(profile.displayName)].filter((value): value is string => Boolean(value));
const fusionAliases = catalogNames.flatMap(fusionModelSelectors).filter(Boolean);
if (fusionAliases.length === 0) {
return profile;
}
return {
...profile,
match: {
...match,
exactAliases: uniqueStrings([...exactAliases, ...fusionAliases])
}
};
});
}
export function withCodexCompatibleVirtualModelProfiles(profiles: unknown[]): unknown[] {
return profiles.map((profile) => {
if (!isRecord(profile) || profile.enabled === false) {
return profile;
}
const materialization = isRecord(profile.materialization) ? profile.materialization : {};
if (materialization.enabled === false || materialization.includeInGatewayModels === false) {
return profile;
}
const execution = isRecord(profile.execution) ? profile.execution : {};
if (execution.clientToolsPolicy === "allow") {
return profile;
}
return {
...profile,
execution: {
...execution,
clientToolsPolicy: "allow"
}
};
});
}
export function fusionModelSelector(model: string): string {
const normalized = fusionModelNameFromSelector(model);
return normalized ? `${fusionModelProviderName}/${normalized}` : "";
}
function fusionModelSelectors(model: string): string[] {
const normalized = fusionModelNameFromSelector(model);
if (!normalized) {
return [];
}
const lowerModel = normalized.toLowerCase();
return uniqueStrings([
fusionModelSelector(normalized),
lowerModel,
`${fusionModelProviderName}/${lowerModel}`,
`${fusionModelProviderName.toLowerCase()}/${lowerModel}`
]);
}
export function fusionModelNameFromSelector(model: string): string {
const trimmed = model.trim();
const prefix = `${fusionModelProviderName}/`;
return trimmed.toLowerCase().startsWith(prefix.toLowerCase())
? trimmed.slice(prefix.length).trim()
: trimmed;
}
function legacyFusionVisionConfig(profile: Record<string, unknown>): VirtualModelFusionVisionConfig | undefined {
const toolName = legacyFusionBuiltinToolName(profile, BUILTIN_FUSION_VISION_TOOL_NAME, "matchMultimodal");
return toolName ? { toolName } : undefined;
}
function legacyFusionWebSearchConfig(profile: Record<string, unknown>): VirtualModelFusionWebSearchConfig | undefined {
const toolName = legacyFusionBuiltinToolName(profile, BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME, "matchWebSearch");
return toolName ? { provider: defaultFusionWebSearchProvider, toolName } : undefined;
}
function legacyFusionBuiltinToolName(
profile: Record<string, unknown>,
baseToolName: string,
executionFlag: "matchMultimodal" | "matchWebSearch"
): string | undefined {
const tools = Array.isArray(profile.tools) ? profile.tools : [];
const toolName = tools
.map((tool) => isRecord(tool) ? stringValue(tool.name) ?? "" : "")
.find((name) => fusionBuiltinToolNameMatches(name, baseToolName));
if (toolName) {
return toolName;
}
const execution = isRecord(profile.execution) ? profile.execution : {};
return execution[executionFlag] === true ? baseToolName : undefined;
}
function fusionBuiltinToolNameMatches(name: string, baseToolName: string): boolean {
if (name === baseToolName || name.startsWith(`${baseToolName}_`)) {
return true;
}
if (baseToolName !== BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME) {
return false;
}
return coreGatewayWebSearchToolNameMatches(name);
}
export function normalizeFusionWebSearchProfileToolName(profile: Record<string, unknown>): Record<string, unknown> | undefined {
const metadata = isRecord(profile.metadata) ? profile.metadata : undefined;
const fusionWebSearch = isRecord(metadata?.fusionWebSearch) ? metadata.fusionWebSearch : undefined;
const configuredToolName = stringValue(fusionWebSearch?.toolName);
const legacyToolName = configuredToolName ? undefined : legacyFusionWebSearchConfig(profile)?.toolName;
const toolName = configuredToolName || legacyToolName;
if (!toolName) {
return undefined;
}
const nextToolName = coreGatewayCompatibleWebSearchToolName(toolName, stringValue(profile.key) || stringValue(profile.id));
if (nextToolName === toolName) {
return undefined;
}
const tools = Array.isArray(profile.tools)
? profile.tools.map((tool) => {
if (!isRecord(tool) || stringValue(tool.name) !== toolName) {
return tool;
}
return {
...tool,
name: nextToolName
};
})
: profile.tools;
return {
...profile,
...(metadata && fusionWebSearch
? {
metadata: {
...metadata,
fusionWebSearch: {
...fusionWebSearch,
toolName: nextToolName
}
}
}
: {}),
...(tools ? { tools } : {})
};
}
export function withFusionWebSearchToolInstructions(profile: Record<string, unknown>): Record<string, unknown> | undefined {
const metadata = isRecord(profile.metadata) ? profile.metadata : undefined;
const fusionWebSearch = isRecord(metadata?.fusionWebSearch) ? metadata.fusionWebSearch : undefined;
const toolName = stringValue(fusionWebSearch?.toolName) || legacyFusionWebSearchConfig(profile)?.toolName;
if (!toolName) {
return undefined;
}
const execution = isRecord(profile.execution) ? profile.execution : {};
if (execution.matchWebSearch !== true) {
return undefined;
}
const instruction = [
`When the client request includes a hosted web_search tool declaration, call the ${toolName} function tool before answering.`,
"Pass the user's search query in the prompt field.",
"Do not use provider-native web search or claim that web search is unavailable unless this function tool returns an error."
].join(" ");
const instructions = isRecord(profile.instructions) ? profile.instructions : {};
if ([instructions.prepend, instructions.append, instructions.replace].some((value) => stringValue(value)?.includes(instruction))) {
return undefined;
}
const replace = stringValue(instructions.replace);
const append = stringValue(instructions.append);
return {
...profile,
instructions: {
...instructions,
...(replace
? { replace: `${replace.trim()}\n\n${instruction}` }
: { append: [append, instruction].filter(Boolean).join("\n\n") })
}
};
}
function coreGatewayCompatibleWebSearchToolName(toolName: string, fallbackName?: string): string {
if (coreGatewayWebSearchToolNameMatches(toolName)) {
return toolName;
}
const normalized = sanitizeFusionToolName(toolName);
const prefix = `${BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME}_`;
if (normalized.startsWith(prefix) && normalized.length > prefix.length) {
return truncateFusionToolName(`${normalized.slice(prefix.length)}_${BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME}`);
}
const fallback = sanitizeFusionToolName(fallbackName || normalized || "fusion");
return truncateFusionToolName(`${fallback}_${BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME}`);
}
function coreGatewayWebSearchToolNameMatches(name: string): boolean {
const normalized = name.toLowerCase().replace(/[-.]/g, "_");
return normalized === BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME ||
normalized.endsWith(`_${BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME}`) ||
normalized.includes("search_web");
}
function sanitizeFusionToolName(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9_]+/g, "_")
.replace(/^_+|_+$/g, "") || "fusion";
}
function truncateFusionToolName(value: string): string {
const maxToolNameLength = 64;
if (value.length <= maxToolNameLength) {
return value;
}
const suffix = `_${BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME}`;
const available = Math.max(1, maxToolNameLength - suffix.length);
return `${value.slice(0, available).replace(/_+$/g, "")}${suffix}`;
}
function readFusionVisionConfig(value: unknown): VirtualModelFusionVisionConfig | undefined {
if (!isRecord(value)) {
return undefined;
}
const toolName = stringValue(value.toolName);
if (!toolName) {
return undefined;
}
const config: VirtualModelFusionVisionConfig = {
toolName,
apiKey: stringValue(value.apiKey),
baseUrl: stringValue(value.baseUrl),
model: stringValue(value.model),
modelSelector: stringValue(value.modelSelector)
};
const timeoutMs = numberValue(value.timeoutMs);
if (timeoutMs) {
config.timeoutMs = timeoutMs;
}
return config;
}
export function readFusionWebSearchConfig(value: unknown): VirtualModelFusionWebSearchConfig | undefined {
if (!isRecord(value)) {
return undefined;
}
const toolName = stringValue(value.toolName);
if (!toolName) {
return undefined;
}
const config: VirtualModelFusionWebSearchConfig = {
toolName,
env: isRecord(value.env) ? stringRecordFromUnknown(value.env) : undefined,
provider: parseFusionWebSearchProvider(value.provider)
};
const resultCount = numberValue(value.resultCount);
if (resultCount) {
config.resultCount = resultCount;
}
const timeoutMs = numberValue(value.timeoutMs);
if (timeoutMs) {
config.timeoutMs = timeoutMs;
}
return config;
}
function resolveFusionVisionRuntime(
config: VirtualModelFusionVisionConfig
): { model?: string; providers: CoreGatewayProvider[] } {
const selector = config.modelSelector || config.model;
if (config.baseUrl) {
return {
model: config.model || config.modelSelector,
providers: []
};
}
const parsed = parseFusionModelSelector(selector);
if (!parsed) {
return {
model: selector ? normalizeGatewayModelSelector(selector) : undefined,
providers: []
};
}
return {
model: `${parsed.providerName}/${parsed.model}`,
providers: []
};
}
function parseFusionModelSelector(value: string | undefined): { model: string; providerName: string } | undefined {
const trimmed = value?.trim();
if (!trimmed) {
return undefined;
}
const commaIndex = trimmed.indexOf(",");
if (commaIndex > 0 && commaIndex < trimmed.length - 1) {
const providerName = trimmed.slice(0, commaIndex).trim();
const model = trimmed.slice(commaIndex + 1).trim();
return providerName && model ? { model, providerName } : undefined;
}
const slashIndex = trimmed.indexOf("/");
if (slashIndex > 0 && slashIndex < trimmed.length - 1) {
const providerName = trimmed.slice(0, slashIndex).trim();
const model = trimmed.slice(slashIndex + 1).trim();
return providerName && model ? { model, providerName } : undefined;
}
return undefined;
}
function normalizeGatewayModelSelector(value: string): string {
const parsed = parseFusionModelSelector(value);
return parsed ? `${parsed.providerName}/${parsed.model}` : value.trim();
}
function parseFusionWebSearchProvider(value: unknown): VirtualModelFusionWebSearchProvider | undefined {
const normalized = stringValue(value)?.toLowerCase();
if (
normalized === "brave" ||
normalized === "bing" ||
normalized === "google_cse" ||
normalized === "serper" ||
normalized === "serpapi" ||
normalized === "tavily" ||
normalized === "exa" ||
normalized === "browser"
) {
return normalized;
}
return undefined;
}
function stringRecordFromUnknown(value: Record<string, unknown>): Record<string, string> | undefined {
const result: Record<string, string> = {};
for (const [key, rawValue] of Object.entries(value)) {
const normalizedKey = key.trim();
const normalizedValue = stringValue(rawValue);
if (normalizedKey && normalizedValue) {
result[normalizedKey] = normalizedValue;
}
}
return Object.keys(result).length ? result : undefined;
}
function sanitizeMcpServerName(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9_.-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || "fusion";
}
@@ -0,0 +1,281 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import { readFileSync, rmSync } from "node:fs";
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { dirname, resolve as pathResolve, sep as pathSep } from "node:path";
import type { AppConfig } from "@ccr/core/contracts/app";
import { RAW_TRACE_SPOOL_DIR } from "@ccr/core/config/constants";
import { updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "@ccr/core/observability/request-log-store";
import { isRecord, numberValue, stringValue } from "@ccr/core/gateway/internal/value";
import { formatError, parseJsonObject, readHeader, readRequestBody, sendJson } from "@ccr/core/gateway/http/io";
import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor";
import { maxUsageCaptureBytes, rawTraceSyncHeader, rawTraceSyncPath } from "@ccr/core/gateway/internal/shared";
import type { RawTracePartText } from "@ccr/core/gateway/internal/shared";
type PendingRawTraceUpdate = RequestLogRawTraceUpdateInput & { receivedAt: number };
const maxPendingRawTraceUpdates = 200;
const pendingRawTraceMaxAgeMs = 5 * 60 * 1000;
export class RawTraceSynchronizer {
readonly token = randomUUID();
private readonly pendingUpdates = new Map<string, PendingRawTraceUpdate>();
async handle(request: IncomingMessage, response: ServerResponse): Promise<void> {
if (request.method !== "POST") {
sendJson(response, 405, { error: { message: "Method not allowed." } });
return;
}
if (readHeader(request.headers[rawTraceSyncHeader]) !== this.token) {
sendJson(response, 401, { error: { message: "Unauthorized raw trace sync." } });
return;
}
const manifest = parseJsonObject(await readRequestBody(request));
const update = readRawTraceRequestLogUpdate(manifest);
cleanupRawTraceBundle(manifest);
if (!update) {
sendJson(response, 202, { applied: false, ok: true });
return;
}
const applied = await updateGatewayRequestLogFromRawTrace(update);
if (!applied) this.store(update);
sendJson(response, 200, { applied, ok: true });
}
take(requestId: string): RequestLogRawTraceUpdateInput | undefined {
const update = this.pendingUpdates.get(requestId);
if (!update) return undefined;
this.pendingUpdates.delete(requestId);
const { receivedAt: _receivedAt, ...input } = update;
return input;
}
private store(update: RequestLogRawTraceUpdateInput): void {
this.prune();
this.pendingUpdates.set(update.requestId, { ...update, receivedAt: Date.now() });
while (this.pendingUpdates.size > maxPendingRawTraceUpdates) {
const oldestKey = this.pendingUpdates.keys().next().value;
if (!oldestKey) break;
this.pendingUpdates.delete(oldestKey);
}
}
private prune(): void {
const cutoff = Date.now() - pendingRawTraceMaxAgeMs;
for (const [requestId, update] of this.pendingUpdates) {
if (update.receivedAt < cutoff) this.pendingUpdates.delete(requestId);
}
}
}
export function buildRawTraceConfig(config: AppConfig, rawTraceSyncToken: string): Record<string, unknown> {
const enabled = rawTraceEnabledFromEnv() && shouldRecordRequestLogs(config);
return {
deleteLocalAfterUpload: false,
enabled,
maxPartBytes: maxUsageCaptureBytes,
mode: "wire_raw",
spoolDir: RAW_TRACE_SPOOL_DIR,
sync: {
enabled,
endpoint: `${endpoint(config.gateway.host, config.gateway.port)}${rawTraceSyncPath}`,
headers: {
[rawTraceSyncHeader]: rawTraceSyncToken
},
timeoutMs: 5000
}
};
}
export function shouldRecordRequestLogs(config: AppConfig): boolean {
return Boolean(config.observability?.requestLogs || config.observability?.agentAnalysis);
}
function rawTraceEnabledFromEnv(): boolean {
const value = (process.env.CCR_RAW_TRACE_ENABLED ?? process.env.CCR_RAW_TRACE ?? "").trim().toLowerCase();
return value === "1" || value === "true" || value === "yes" || value === "on";
}
export function readRawTraceRequestLogUpdate(manifest: Record<string, unknown>): RequestLogRawTraceUpdateInput | undefined {
const requestId = stringValue(manifest.turnKey);
const parts = Array.isArray(manifest.parts)
? manifest.parts.filter((part): part is Record<string, unknown> => isRecord(part))
: [];
if (!requestId || parts.length === 0) {
return undefined;
}
const upstreamRequestMetadata = readRawTraceJsonPart(parts, "upstream_request_metadata");
const upstreamResponseMetadata = readRawTraceJsonPart(parts, "upstream_response_metadata");
const upstreamRequestBody = readRawTraceTextPart(parts, "upstream_request");
const upstreamResponseStream = readRawTraceTextPart(parts, "response_stream");
const upstreamResponseBody = upstreamResponseStream ?? readRawTraceTextPart(parts, "upstream_response");
const target = isRecord(manifest.target) ? manifest.target : {};
const rawUrl = stringValue(upstreamRequestMetadata?.url);
const url = sanitizeUrlForLog(rawUrl);
return {
method: stringValue(upstreamRequestMetadata?.method) || "POST",
model: stringValue(target.model),
path: pathFromUrl(url),
provider: stringValue(target.providerName) || stringValue(target.provider),
requestBodyContentType: upstreamRequestBody?.contentType,
requestBodyText: upstreamRequestBody?.text,
requestHeaders: headerRecordFromUnknown(upstreamRequestMetadata?.headers),
requestId,
isStream: upstreamResponseStream !== undefined,
responseBodyContentType: upstreamResponseBody?.contentType,
responseBodyText: upstreamResponseBody?.text,
responseHeaders: headerRecordFromUnknown(upstreamResponseMetadata?.headers),
statusCode: numberValue(upstreamResponseMetadata?.statusCode),
url
};
}
function readRawTraceJsonPart(parts: Record<string, unknown>[], partType: string): Record<string, unknown> | undefined {
const text = readRawTraceTextPart(parts, partType)?.text;
if (!text) {
return undefined;
}
try {
const parsed = JSON.parse(text) as unknown;
return isRecord(parsed) ? parsed : undefined;
} catch {
return undefined;
}
}
function readRawTraceTextPart(parts: Record<string, unknown>[], partType: string): RawTracePartText | undefined {
const part = parts.find((candidate) => stringValue(candidate.partType) === partType);
const filePath = stringValue(part?.filePath);
if (!filePath || !isRawTraceSpoolFile(filePath)) {
return undefined;
}
try {
return {
contentType: stringValue(part?.contentType),
text: readFileSync(filePath, "utf8")
};
} catch (error) {
console.warn(`[gateway] Failed to read raw trace part ${partType}: ${formatError(error)}`);
return undefined;
}
}
export function cleanupRawTraceBundle(manifest: Record<string, unknown>): void {
const parts = Array.isArray(manifest.parts)
? manifest.parts.filter((part): part is Record<string, unknown> => isRecord(part))
: [];
const firstFilePath = parts.map((part) => stringValue(part.filePath)).find((value): value is string => Boolean(value));
if (!firstFilePath || !isRawTraceSpoolFile(firstFilePath)) {
return;
}
try {
rmSync(dirname(firstFilePath), { force: true, recursive: true });
} catch (error) {
console.warn(`[gateway] Failed to clean raw trace bundle: ${formatError(error)}`);
}
}
function isRawTraceSpoolFile(filePath: string): boolean {
const spoolDir = pathResolve(RAW_TRACE_SPOOL_DIR);
const resolvedFile = pathResolve(filePath);
return dirname(resolvedFile) !== spoolDir && resolvedFile.startsWith(`${spoolDir}${pathSep}`);
}
function headerRecordFromUnknown(value: unknown): Record<string, string> | undefined {
if (!isRecord(value)) {
return undefined;
}
const headers: Record<string, string> = {};
for (const [key, headerValue] of Object.entries(value)) {
if (headerValue === undefined || headerValue === null) {
continue;
}
headers[key] = Array.isArray(headerValue)
? headerValue.map((item) => String(item)).join(", ")
: String(headerValue);
}
return headers;
}
function sanitizeUrlForLog(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
try {
const url = new URL(value);
for (const key of [...url.searchParams.keys()]) {
if (isSensitiveQueryParam(key)) {
url.searchParams.set(key, "[redacted]");
}
}
return url.toString();
} catch {
return value;
}
}
function isSensitiveQueryParam(value: string): boolean {
const normalized = value.trim().toLowerCase();
return normalized === "key" || normalized === "api_key" || normalized === "apikey" || normalized === "access_token";
}
function pathFromUrl(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
try {
return new URL(value).pathname || undefined;
} catch {
return undefined;
}
}
export function createBodySampler() {
const chunks: Buffer[] = [];
let totalBytes = 0;
let truncated = false;
return {
append(chunk: Buffer | string) {
if (truncated) {
return;
}
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
if (totalBytes + buffer.byteLength > maxUsageCaptureBytes) {
const remaining = Math.max(0, maxUsageCaptureBytes - totalBytes);
if (remaining > 0) {
chunks.push(buffer.subarray(0, remaining));
totalBytes += remaining;
}
truncated = true;
return;
}
chunks.push(buffer);
totalBytes += buffer.byteLength;
},
isTruncated() {
return truncated;
},
read() {
return Buffer.concat(chunks, totalBytes).toString("utf8");
}
};
}
@@ -0,0 +1,112 @@
import type { AppConfig, GatewayProviderConfig, ProviderCredentialConfig } from "@ccr/core/contracts/app";
import { estimateLimitUsage, limitRules, readWindowCounter } from "@ccr/core/gateway/limits/window-limiter";
import {
type ApiKeyLimitRule,
type ApiKeyLimitUsage,
type UpstreamAttempt
} from "@ccr/core/gateway/internal/shared";
import {
findProviderByPublicOrInternalName,
findProviderCredentialByRuntimeId,
findProviderCredentialBySlug,
parseProviderCredentialInternalName,
providerCredentialRuntimeId
} from "@ccr/core/providers/runtime-topology";
const providerCredentialCooldownMs = 60_000;
const providerCredentialCooldowns = new Map<string, { reason: string; until: number }>();
export function providerCredentialLimitState(
provider: GatewayProviderConfig,
credential: ProviderCredentialConfig,
usage: ApiKeyLimitUsage
): { blocked: boolean; utilization: number } {
const rules = limitRules(credential.limits, usage);
if (rules.length === 0) return { blocked: false, utilization: 0 };
const now = Date.now();
let blocked = false;
let utilization = 0;
for (const rule of rules) {
const windowStart = Math.floor(now / rule.windowMs) * rule.windowMs;
const counter = readWindowCounter(providerCredentialCounterKey(provider, credential, rule, windowStart), windowStart, rule.windowMs, now);
blocked ||= counter.value + rule.requested > rule.limit;
utilization = Math.max(utilization, (counter.value + rule.requested) / rule.limit);
}
return { blocked, utilization };
}
export function recordProviderCredentialOutcome(
config: AppConfig,
method: string,
attempt: UpstreamAttempt,
statusCode: number,
responseHeaders: Headers
): void {
if (!attempt.logicalProvider || !attempt.credentialProtocol || !attempt.credentialChain?.length) return;
const provider = findProviderByPublicOrInternalName(config, attempt.logicalProvider);
if (!provider) return;
const responseCredentialId = responseHeaders.get("x-ccr-provider-credential-id")?.trim();
const responseCredential = responseCredentialId
? findProviderCredentialByRuntimeId(provider, responseCredentialId)
: undefined;
const credential = responseCredential ?? providerCredentialFromInternalName(provider, attempt.credentialChain[0]);
if (!credential) return;
if (statusCode >= 200 && statusCode < 500 && statusCode !== 401 && statusCode !== 403 && statusCode !== 429) {
incrementProviderCredentialCounters(provider, credential, estimateLimitUsage(method, attempt.body ?? Buffer.alloc(0)));
clearProviderCredentialCooldown(provider, credential);
return;
}
if (statusCode === 401 || statusCode === 403 || statusCode === 429 || statusCode >= 500) {
setProviderCredentialCooldown(provider, credential, providerCredentialCooldownMs, `HTTP ${statusCode}`);
}
}
export function readProviderCredentialCooldown(
provider: GatewayProviderConfig,
credential: ProviderCredentialConfig
): { reason: string; until: number } | undefined {
const key = providerCredentialStateKey(provider, credential);
const cooldown = providerCredentialCooldowns.get(key);
if (!cooldown) return undefined;
if (cooldown.until > Date.now()) return cooldown;
providerCredentialCooldowns.delete(key);
return undefined;
}
function providerCredentialFromInternalName(provider: GatewayProviderConfig, internalName: string | undefined): ProviderCredentialConfig | undefined {
const parsed = parseProviderCredentialInternalName(internalName);
return parsed ? findProviderCredentialBySlug(provider, parsed.credentialSlug) : undefined;
}
function incrementProviderCredentialCounters(provider: GatewayProviderConfig, credential: ProviderCredentialConfig, usage: ApiKeyLimitUsage): void {
const rules = limitRules(credential.limits, usage);
const now = Date.now();
for (const rule of rules) {
const windowStart = Math.floor(now / rule.windowMs) * rule.windowMs;
readWindowCounter(providerCredentialCounterKey(provider, credential, rule, windowStart), windowStart, rule.windowMs, now).value += rule.requested;
}
}
function providerCredentialCounterKey(
provider: GatewayProviderConfig,
credential: ProviderCredentialConfig,
rule: ApiKeyLimitRule,
windowStart: number
): string {
return ["provider-credential", provider.name, providerCredentialRuntimeId(provider, credential), rule.name, rule.metric, rule.windowMs, windowStart].join("|");
}
function setProviderCredentialCooldown(provider: GatewayProviderConfig, credential: ProviderCredentialConfig, cooldownMs: number, reason: string): void {
providerCredentialCooldowns.set(providerCredentialStateKey(provider, credential), { reason, until: Date.now() + cooldownMs });
}
function clearProviderCredentialCooldown(provider: GatewayProviderConfig, credential: ProviderCredentialConfig): void {
providerCredentialCooldowns.delete(providerCredentialStateKey(provider, credential));
}
function providerCredentialStateKey(provider: GatewayProviderConfig, credential: ProviderCredentialConfig): string {
return `${provider.name}::${providerCredentialRuntimeId(provider, credential)}`;
}
@@ -0,0 +1,22 @@
import { isRecord, stringValue } from "@ccr/core/gateway/internal/value";
export function isLocalClaudeCodeOauthProviderPlugin(value: unknown): value is Record<string, unknown> {
if (!isRecord(value)) return false;
const key = stringValue(value.key)?.toLowerCase() ?? "";
return key.startsWith("ccr-local-agent-") && key.includes("claude-code-oauth");
}
export function mergeAnthropicBetaValues(...values: Array<string | undefined>): string {
const seen = new Set<string>();
const merged: string[] = [];
for (const value of values) {
for (const token of value?.split(",") ?? []) {
const normalized = token.trim();
const key = normalized.toLowerCase();
if (!normalized || seen.has(key)) continue;
seen.add(key);
merged.push(normalized);
}
}
return merged.join(",");
}
@@ -0,0 +1,487 @@
/**
* Extracted from gateway/service.ts. Keep this module focused on its named gateway boundary.
*/
import type { AppConfig, GatewayProviderCapability, GatewayProviderConfig, GatewayProviderProtocol, ProviderCredentialConfig } from "@ccr/core/contracts/app";
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue } from "@ccr/core/providers/presets/index";
import { normalizeProviderBaseUrl as normalizeProviderBaseUrlInput } from "@ccr/core/providers/url";
import { modelRegistryForConfig, parseProviderModelSelector, providerRuntimeId } from "@ccr/core/routing/model-registry";
import { gatewayProviderProtocolFallbackOrder, type CoreGatewayProvider } from "@ccr/core/gateway/internal/shared";
export function providerCapabilityForClientProtocol(
provider: GatewayProviderConfig,
clientProtocol: GatewayProviderProtocol
): GatewayProviderCapability | undefined {
const capabilities = normalizedProviderCapabilities(provider);
for (const protocol of providerProtocolPreferenceForClient(clientProtocol)) {
const capability = capabilities.find((item) => item.type === protocol);
if (capability) {
return capability;
}
}
return undefined;
}
export function providerProtocolForClientProtocol(
provider: GatewayProviderConfig,
clientProtocol: GatewayProviderProtocol
): GatewayProviderProtocol | undefined {
const capability = providerCapabilityForClientProtocol(provider, clientProtocol);
if (capability) {
return capability.type;
}
const directProtocol =
normalizeProviderProtocol(provider.type) ??
normalizeProviderProtocol(provider.provider) ??
inferProtocol(provider);
return providerProtocolPreferenceForClient(clientProtocol).includes(directProtocol)
? directProtocol
: undefined;
}
function providerProtocolPreferenceForClient(clientProtocol: GatewayProviderProtocol): GatewayProviderProtocol[] {
if (clientProtocol === "openai_responses") {
return ["openai_responses", "openai_chat_completions", "anthropic_messages", "gemini_interactions"];
}
if (clientProtocol === "anthropic_messages") {
return uniqueProviderProtocols([clientProtocol, ...gatewayProviderProtocolFallbackOrder]);
}
return [clientProtocol];
}
function uniqueProviderProtocols(protocols: GatewayProviderProtocol[]): GatewayProviderProtocol[] {
const seen = new Set<GatewayProviderProtocol>();
const output: GatewayProviderProtocol[] = [];
for (const protocol of protocols) {
if (seen.has(protocol)) {
continue;
}
seen.add(protocol);
output.push(protocol);
}
return output;
}
export function findProviderByPublicOrInternalName(config: AppConfig, name: string): GatewayProviderConfig | undefined {
const normalized = name.trim().toLowerCase();
if (!normalized) {
return undefined;
}
const credentialInternalName = parseProviderCredentialInternalName(name);
if (credentialInternalName) {
const internalProviderId = credentialInternalName.providerId.toLowerCase();
return config.Providers.find((provider) =>
provider.name.trim().toLowerCase() === internalProviderId ||
providerRuntimeId(provider).toLowerCase() === internalProviderId
);
}
return modelRegistryForConfig(config).findProvider(normalized);
}
export function activeProviderCredentials(provider: GatewayProviderConfig): ProviderCredentialConfig[] {
return (provider.credentials ?? []).filter((credential) =>
credential.enabled !== false &&
Boolean(providerCredentialApiKey(credential))
);
}
export function providerCredentialPriority(credential: ProviderCredentialConfig, index: number): number {
return Number.isFinite(credential.priority) ? Number(credential.priority) : index + 1;
}
export function toCoreGatewayProviders(provider: GatewayProviderConfig): CoreGatewayProvider[] {
const capabilities = normalizedProviderCapabilities(provider);
if (capabilities.length === 0) {
return toCoreGatewayProvidersForCapability(provider);
}
return capabilities
.flatMap((capability) => toCoreGatewayProvidersForCapability(provider, capability))
.filter((item): item is CoreGatewayProvider => Boolean(item));
}
function toCoreGatewayProvidersForCapability(
provider: GatewayProviderConfig,
capability?: GatewayProviderCapability
): CoreGatewayProvider[] {
const credentials = activeProviderCredentials(provider);
if (credentials.length === 0) {
const coreProvider = toCoreGatewayProvider(provider, capability);
return coreProvider ? [coreProvider] : [];
}
return sortProviderCredentialsForConfig(credentials)
.map((credential) => toCoreGatewayProvider(provider, capability, credential))
.filter((item): item is CoreGatewayProvider => Boolean(item));
}
function toCoreGatewayProvider(
provider: GatewayProviderConfig,
capability?: GatewayProviderCapability,
credential?: ProviderCredentialConfig
): CoreGatewayProvider | undefined {
const type =
capability?.type ??
normalizeProviderProtocol(provider.type) ??
normalizeProviderProtocol(provider.provider) ??
inferProtocol(provider);
const baseurl = normalizeProviderRuntimeBaseUrl(capability?.baseUrl ?? readBaseUrl(provider), type);
const apikey = credential ? providerCredentialApiKey(credential) : provider.apikey || provider.apiKey || provider.api_key;
if (!provider.name || provider.models.length === 0) {
return undefined;
}
const safetyIssue = providerApiKeySafetyIssue({
apiKey: apikey,
baseUrl: baseurl ?? "",
name: provider.name
});
if (safetyIssue) {
throw new Error(safetyIssue.message);
}
return {
apikey,
baseurl,
billing: provider.billing,
extraBody: provider.extraBody,
extraHeaders: provider.extraHeaders,
models: provider.models,
name: credential
? providerCredentialInternalName(provider, type, credential)
: capability
? providerCapabilityInternalName(provider, type)
: providerRuntimeId(provider),
type
};
}
export function sortProviderCredentialsForConfig(credentials: ProviderCredentialConfig[]): ProviderCredentialConfig[] {
return [...credentials].sort((left, right) =>
providerCredentialPriority(left, 0) - providerCredentialPriority(right, 0) ||
providerCredentialSortKey(left).localeCompare(providerCredentialSortKey(right))
);
}
export function normalizedProviderCapabilities(provider: GatewayProviderConfig): GatewayProviderCapability[] {
const capabilities = Array.isArray(provider.capabilities) ? provider.capabilities : [];
const normalized: GatewayProviderCapability[] = [];
const byProtocol = new Map<GatewayProviderProtocol, GatewayProviderCapability>();
for (const capability of capabilities) {
const type = normalizeProviderProtocol(capability.type);
const baseUrl = capability.baseUrl?.trim();
if (!type || !baseUrl) {
continue;
}
const item = {
...capability,
baseUrl,
type
};
const existing = byProtocol.get(type);
if (!existing || providerCapabilityPriority(item) < providerCapabilityPriority(existing)) {
byProtocol.set(type, item);
}
}
for (const capability of capabilities) {
const type = normalizeProviderProtocol(capability.type);
const selected = type ? byProtocol.get(type) : undefined;
if (selected && !normalized.includes(selected)) {
normalized.push(selected);
}
}
return applyPresetProtocolLock(provider, normalized);
}
function applyPresetProtocolLock(
provider: GatewayProviderConfig,
capabilities: GatewayProviderCapability[]
): GatewayProviderCapability[] {
const lockedProtocols = lockedProviderPresetProtocols(provider, capabilities);
if (lockedProtocols.length === 0) {
return capabilities;
}
const lockedProtocolSet = new Set(lockedProtocols);
const lockedCapabilities = capabilities.filter((capability) => lockedProtocolSet.has(capability.type));
if (lockedCapabilities.length > 0) {
return lockedCapabilities;
}
const lockedProtocol = lockedProtocols[0];
const baseUrl = readBaseUrl(provider);
const normalizedBaseUrl = normalizeProviderRuntimeBaseUrl(baseUrl, lockedProtocol);
return normalizedBaseUrl
? [{ baseUrl: normalizedBaseUrl, source: "preset", type: lockedProtocol }]
: [];
}
function lockedProviderPresetProtocols(
provider: GatewayProviderConfig,
capabilities: GatewayProviderCapability[]
): GatewayProviderProtocol[] {
const baseUrls = [
readBaseUrl(provider),
...capabilities.map((capability) => capability.baseUrl)
].filter((value): value is string => Boolean(value?.trim()));
for (const baseUrl of baseUrls) {
if (findProviderPresetByBaseUrl(baseUrl)?.id === "gemini") {
return ["gemini_generate_content", "gemini_interactions"];
}
}
return [];
}
function providerCapabilityPriority(capability: GatewayProviderCapability): number {
if (capability.source === "preset") {
return 0;
}
if (capability.source === "detected") {
return 2;
}
return 1;
}
export function providerCapabilityInternalName(provider: GatewayProviderConfig, protocol: GatewayProviderProtocol): string {
return `${providerRuntimeId(provider)}::${protocol}`;
}
function providerCapabilityLegacyInternalName(providerName: string, protocol: GatewayProviderProtocol): string {
return `${providerName}::${protocol}`;
}
export function providerCapabilityNameMatches(provider: GatewayProviderConfig, protocol: GatewayProviderProtocol, value: string): boolean {
const normalized = value.trim().toLowerCase();
return providerCapabilityInternalName(provider, protocol).toLowerCase() === normalized ||
providerCapabilityLegacyInternalName(provider.name, protocol).toLowerCase() === normalized;
}
export function sanitizeHeaderValue(value: unknown): string {
// HTTP header values must be ByteString (code point <= 255). Values derived
// from user-facing names — model selectors like "小米mimo/...", provider
// names, route reasons — can contain non-ASCII characters that crash Node's
// fetch/undici with "Cannot convert argument to a ByteString" (surfaced as
// 502). Normalize to ASCII while preserving case and printable punctuation.
const text = typeof value === "string" && value.trim() ? value : "unknown";
const sanitized = text
.replace(/[^\x20-\x7E]+/g, "-")
.replace(/-{2,}/g, "-")
.replace(/^-+|-+$/g, "");
return sanitized || "unknown";
}
export function providerCredentialInternalName(
provider: GatewayProviderConfig,
protocol: GatewayProviderProtocol,
credential: ProviderCredentialConfig
): string {
return `${providerCapabilityInternalName(provider, protocol)}::cred:${providerCredentialSlug(providerCredentialRuntimeId(provider, credential))}`;
}
export function parseProviderCredentialInternalName(value: string | undefined): {
credentialSlug: string;
providerId: string;
protocol: GatewayProviderProtocol;
} | undefined {
const marker = "::cred:";
const markerIndex = value?.lastIndexOf(marker) ?? -1;
if (!value || markerIndex <= 0) {
return undefined;
}
const baseName = value.slice(0, markerIndex);
const credentialSlug = value.slice(markerIndex + marker.length).trim();
const protocolSeparator = baseName.lastIndexOf("::");
if (!credentialSlug || protocolSeparator <= 0) {
return undefined;
}
const protocol = normalizeProviderProtocol(baseName.slice(protocolSeparator + 2));
const providerId = baseName.slice(0, protocolSeparator).trim();
return protocol && providerId ? { credentialSlug, providerId, protocol } : undefined;
}
export function providerCredentialSlug(value: string | undefined): string {
return (value ?? "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9_.-]+/g, "-")
.replace(/^-+|-+$/g, "") || "key";
}
export function providerCredentialRuntimeId(
provider: GatewayProviderConfig,
credential: ProviderCredentialConfig,
index = provider.credentials?.indexOf(credential) ?? -1
): string {
const explicitId = credential.id?.trim();
if (explicitId) {
return explicitId;
}
const oneBasedIndex = index >= 0 ? index + 1 : 1;
const label = credential.name?.trim() || credential.label?.trim();
return label ? `${providerCredentialSlug(label)}-${oneBasedIndex}` : `key-${oneBasedIndex}`;
}
function providerCredentialSortKey(credential: ProviderCredentialConfig): string {
return providerCredentialSlug(credential.id || credential.name || credential.label);
}
export function providerCredentialApiKey(credential: ProviderCredentialConfig): string {
return credential.api_key || credential.apiKey || credential.apikey || "";
}
export function findProviderCredentialByRuntimeId(
provider: GatewayProviderConfig,
credentialId: string
): ProviderCredentialConfig | undefined {
const normalizedId = credentialId.trim();
const normalizedSlug = providerCredentialSlug(normalizedId);
return (provider.credentials ?? []).find((credential, index) => {
const runtimeId = providerCredentialRuntimeId(provider, credential, index);
return runtimeId === normalizedId || providerCredentialSlug(runtimeId) === normalizedSlug || credential.id?.trim() === normalizedId;
});
}
export function findProviderCredentialBySlug(
provider: GatewayProviderConfig,
credentialSlug: string
): ProviderCredentialConfig | undefined {
const normalizedSlug = providerCredentialSlug(credentialSlug);
return (provider.credentials ?? []).find((credential, index) => providerCredentialSlug(providerCredentialRuntimeId(provider, credential, index)) === normalizedSlug);
}
export function normalizeProviderProtocol(value: unknown): GatewayProviderProtocol | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (normalized === "openai" || normalized === "openai_responses") {
return "openai_responses";
}
if (normalized === "openai_chat" || normalized === "openai_chat_completions") {
return "openai_chat_completions";
}
if (normalized === "anthropic" || normalized === "anthropic_messages") {
return "anthropic_messages";
}
if (normalized === "gemini" || normalized === "gemini_generate_content") {
return "gemini_generate_content";
}
if (
normalized === "gemini_interactions" ||
normalized === "gemini-interactions" ||
normalized === "google_interactions" ||
normalized === "google-interactions" ||
normalized === "interactions" ||
normalized === "interaction"
) {
return "gemini_interactions";
}
return undefined;
}
export function inferProtocol(provider: GatewayProviderConfig): GatewayProviderProtocol {
const url = readBaseUrl(provider)?.toLowerCase() ?? "";
const transformerNames = JSON.stringify(provider.transformer ?? "").toLowerCase();
if (url.includes("/interactions") || transformerNames.includes("gemini_interactions")) {
return "gemini_interactions";
}
if (url.includes("generativelanguage.googleapis.com") || transformerNames.includes("gemini")) {
return "gemini_generate_content";
}
if (url.includes("anthropic") || transformerNames.includes("anthropic")) {
return "anthropic_messages";
}
return "openai_chat_completions";
}
export function resolveResponseProviderProtocol(headers: Headers, config: AppConfig | undefined): GatewayProviderProtocol | undefined {
const ccrProtocol = normalizeProviderProtocol(headers.get("x-ccr-provider-protocol"));
if (ccrProtocol) {
return ccrProtocol;
}
const providerName =
headers.get("x-gateway-target-provider-name")?.trim() ||
headers.get("x-gateway-target-provider")?.trim();
if (!providerName) {
return undefined;
}
const credentialInternalName = parseProviderCredentialInternalName(providerName);
if (credentialInternalName) {
return credentialInternalName.protocol;
}
const provider = config ? findProviderByPublicOrInternalName(config, providerName) : undefined;
if (!provider) {
return normalizeProviderProtocol(providerName);
}
const capability = normalizedProviderCapabilities(provider).find((item) =>
providerCapabilityNameMatches(provider, item.type, providerName)
);
if (capability) {
return capability.type;
}
return normalizeProviderProtocol(provider.type) ?? normalizeProviderProtocol(provider.provider) ?? inferProtocol(provider);
}
export function resolveProviderLogName(headers: Headers, config: AppConfig | undefined, fallbackModel?: string): string | undefined {
const providerSelector =
headers.get("x-gateway-target-provider-name")?.trim() ||
headers.get("x-gateway-target-provider")?.trim();
const headerProvider = providerSelector && config
? findProviderByPublicOrInternalName(config, providerSelector)
: undefined;
if (headerProvider) {
return headerProvider.name;
}
const routeProvider = parseProviderModelSelector(fallbackModel)?.provider;
const modelProvider = routeProvider && config
? findProviderByPublicOrInternalName(config, routeProvider)
: undefined;
return modelProvider?.name;
}
function providerMatchesName(provider: GatewayProviderConfig, name: string): boolean {
const normalizedName = name.trim().toLowerCase();
return [provider.id, provider.name, provider.provider]
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
.some((value) => value.trim().toLowerCase() === normalizedName);
}
function normalizeProviderRuntimeBaseUrl(value: string | undefined, type: GatewayProviderProtocol): string | undefined {
if (!value) {
return undefined;
}
return normalizeProviderBaseUrlInput(value, type) || undefined;
}
function readBaseUrl(provider: GatewayProviderConfig): string | undefined {
return provider.baseurl || provider.baseUrl || provider.api_base_url;
}
@@ -0,0 +1,16 @@
import type { AppConfig, GatewayProviderConfig } from "@ccr/core/contracts/app";
import { modelRegistryForConfig } from "@ccr/core/routing/model-registry";
export function resolveConfiguredProviderModelSelector(
value: string | undefined,
config: AppConfig
): { model: string; provider: GatewayProviderConfig } | undefined {
return modelRegistryForConfig(config).resolveProviderModel(value);
}
export function resolveUniqueConfiguredProviderModelSelector(
value: string | undefined,
config: AppConfig
): { model: string; provider: GatewayProviderConfig } | undefined {
return modelRegistryForConfig(config).resolveUniqueProviderModel(value);
}
@@ -0,0 +1,32 @@
import type { GatewayProviderProtocol } from "@ccr/core/contracts/app";
export function requestProtocolForPath(path: string): GatewayProviderProtocol | undefined {
const normalized = path.toLowerCase();
if (normalized === "/v1/messages" || normalized === "/messages" || normalized.endsWith("/v1/messages")) {
return "anthropic_messages";
}
if (normalized === "/v1/chat/completions" || normalized === "/chat/completions" || normalized.endsWith("/chat/completions")) {
return "openai_chat_completions";
}
if (normalized === "/v1/responses" || normalized === "/responses" || normalized.endsWith("/responses")) {
return "openai_responses";
}
if (/\/v1(?:beta)?\/models\/[^/]+:(?:generatecontent|streamgeneratecontent)$/i.test(normalized)) {
return "gemini_generate_content";
}
if (/\/v1(?:beta)?\/interactions(?:\/[^/]+(?:\/cancel)?)?$/i.test(normalized)) {
return "gemini_interactions";
}
return undefined;
}
export function shouldApplyGatewayRouting(method: string, path: string): boolean {
if (method.toUpperCase() !== "POST") {
return false;
}
const protocol = requestProtocolForPath(path);
if (protocol === "gemini_interactions") {
return /\/v1(?:beta)?\/interactions$/i.test(path);
}
return Boolean(protocol);
}
@@ -10,6 +10,7 @@ import { loadPersistedAppSetting, replacePersistedAppSetting } from "@ccr/core/c
import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "@ccr/core/agents/bot-gateway/handoff-scan-service";
import { cancelBotGatewayQrLogin, startBotGatewayQrLogin, waitBotGatewayQrLogin } from "@ccr/core/agents/bot-gateway/qr-login-service";
import { syncClaudeAppGatewayConfig, restoreClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
import { findInstalledCodexAppExecutable } from "@ccr/core/agents/codex/app-launch";
import { loadAppConfig, saveApiKeysConfig, saveAppConfig } from "@ccr/core/config/config";
import { API_KEYS_DB_FILE, APP_CONFIG_DB_FILE, APP_NAME, CONFIGDIR, CONFIG_FILE, DATADIR, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, ONBOARDING_FINISHED_FILE, PROXY_CA_CERT_FILE, REQUEST_LOGS_DB_FILE, USAGE_DB_FILE } from "@ccr/core/config/constants";
import { detectProviderIcon } from "@ccr/core/providers/icons";
@@ -481,9 +482,11 @@ function logProfileApplyResult(result: ProfileApplyResult): void {
}
function getCliAppInfo(): AppInfo {
const chatgptAppPath = findInstalledCodexAppExecutable().executable;
return {
appConfigDbFile: APP_CONFIG_DB_FILE,
apiKeysDbFile: API_KEYS_DB_FILE,
...(chatgptAppPath ? { chatgptAppPath } : {}),
configDir: CONFIGDIR,
configFile: CONFIG_FILE,
dataDir: DATADIR,
+3
View File
@@ -9,6 +9,7 @@ import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "@ccr/
import { cancelBotGatewayQrLogin, startBotGatewayQrLogin, waitBotGatewayQrLogin } from "@ccr/core/agents/bot-gateway/qr-login-service";
import { closeBotGatewayQrWindow, openBotGatewayQrWindow } from "./bot-gateway-qr-window-service";
import { syncClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
import { findInstalledCodexAppExecutable } from "@ccr/core/agents/codex/app-launch";
import { loadAppConfig, saveApiKeysConfig, saveAppConfig } from "@ccr/core/config/config";
import { API_KEYS_DB_FILE, APP_CONFIG_DB_FILE, APP_NAME, CONFIGDIR, CONFIG_FILE, DATADIR, GATEWAY_CONFIG_FILE, IPC_CHANNELS, LEGACY_CONFIG_FILE, ONBOARDING_FINISHED_FILE, PROXY_CA_CERT_FILE, REQUEST_LOGS_DB_FILE, USAGE_DB_FILE } from "@ccr/core/config/constants";
import { deepLinkService } from "./deep-link";
@@ -56,9 +57,11 @@ const onboardingFinishedAtSettingKey = "onboardingFinishedAt";
const imageExportTargets = new Map<string, string>();
ipcMain.handle(IPC_CHANNELS.appGetInfo, () => {
const chatgptAppPath = findInstalledCodexAppExecutable().executable;
return {
appConfigDbFile: APP_CONFIG_DB_FILE,
apiKeysDbFile: API_KEYS_DB_FILE,
...(chatgptAppPath ? { chatgptAppPath } : {}),
configDir: CONFIGDIR,
configFile: CONFIG_FILE,
dataDir: DATADIR,
+38 -9
View File
@@ -24,7 +24,7 @@ import {
OverviewWidgetConfig, parsePluginAppsSettingsText, parsePluginConfigSettingsText, parseProviderAccountDraft,
providerCredentialsFromDraft,
persistLanguagePreference, PluginMarketplaceEntry, PluginRoutingConfigTarget, pluginSettingsConfigFromDraft, PluginSettingsDraft, presetCapabilitiesFromDraft,
probeProviderCandidates, probeProviderDeepLinkPayload, profileAgentLabel, profileEnvRowsForAgent, ProfileConfig, ProfileOpenSurface, ProfileRuntimeStatus, profileConfigFromDraft, providerAccountApiKeySafetyIssue,
probeProviderCandidates, probeProviderDeepLinkPayload, profileAgentLabel, profileDraftWithDetectedAppPath, profileEnvRowsForAgent, ProfileConfig, ProfileOpenSurface, ProfileRuntimeStatus, profileConfigFromDraft, providerAccountApiKeySafetyIssue,
profileOpenCommandFallback, profileOpenSurfaces, ProviderAccountSnapshot, providerApiKeySafetyIssue, ProviderConnectivityCheckReport, ProviderDeepLinkPayload, ProviderDeepLinkRequest, providerIdentitySafetyIssue, providerProbeCandidates,
providerCapabilitiesForProtocols, providerGlobalBaseUrlForProbe, providerProbeCandidatesApiKeySafetyIssue, providerProbeHasSupportedProtocol, providerProbeInputKey, providerSelectableProtocolsFromProbe, ProxyNetworkSnapshot,
ProxyStatus, readLanguagePreference, RequestLogListFilter, RequestLogPage, ResolvedLanguage,
@@ -208,6 +208,7 @@ function App() {
const [profileDraft, setProfileDraft] = useState<AddProfileDraft>(() => createProfileDraft());
const [profileEditDraft, setProfileEditDraft] = useState<AddProfileDraft>(() => createProfileDraft());
const [profileEditIndex, setProfileEditIndex] = useState<number>();
const [profileDeleteIndex, setProfileDeleteIndex] = useState<number>();
const [profileOpenDialog, setProfileOpenDialog] = useState<ProfileOpenDialogState>();
const [profileActionBusy, setProfileActionBusy] = useState<ProfileActionBusy>();
const [profileRuntimeStatus, setProfileRuntimeStatus] = useState<ProfileRuntimeStatus>({ profiles: [] });
@@ -362,6 +363,14 @@ function App() {
};
}, []);
useEffect(() => {
if (!appInfo.chatgptAppPath) {
return;
}
setProfileDraft((current) => profileDraftWithDetectedAppPath(current, appInfo.chatgptAppPath));
setProfileEditDraft((current) => profileDraftWithDetectedAppPath(current, appInfo.chatgptAppPath));
}, [appInfo.chatgptAppPath]);
useEffect(() => {
if (!window.ccr) {
return;
@@ -637,6 +646,7 @@ function App() {
const dirty = draftConfig !== savedConfig;
const apiKeys = useMemo(() => createApiKeyList(draftConfig), [draftConfig.APIKEY, draftConfig.APIKEYS]);
const apiKeyEditItem = apiKeyEditIndex === undefined ? undefined : apiKeys.find((apiKey) => apiKey.index === apiKeyEditIndex);
const profileDeleteItem = profileDeleteIndex === undefined ? undefined : draftConfig.profile.profiles[profileDeleteIndex];
const providerDeleteItem = providerDeleteIndex === undefined ? undefined : draftConfig.Providers[providerDeleteIndex];
const routingDeleteRule = routingDeleteIndex === undefined ? undefined : draftConfig.Router.rules[routingDeleteIndex];
const extensionDeleteItem = useMemo(() => {
@@ -763,7 +773,10 @@ function App() {
onboardingProfileDraftSource.current = source;
setProfileAgentTab(profile.agent);
setProfileDraft(createProfileDraftFromProfile(profile, draftConfig.botConfigs));
setProfileDraft(profileDraftWithDetectedAppPath(
createProfileDraftFromProfile(profile, draftConfig.botConfigs),
appInfo.chatgptAppPath
));
setProfileActionError("");
}, [activeView, onboardingStep, onboardingProfileConfirmed, configLoaded, draftConfig.profile.profiles, draftConfig.botConfigs, profileDraft.agent]);
@@ -2390,7 +2403,7 @@ function App() {
function openAddProfileDialog(agent: ProfileConfig["agent"] = profileAgentTab) {
setProfileAgentTab(agent);
setProfileDraft(createProfileDraft(agent));
setProfileDraft(profileDraftWithDetectedAppPath(createProfileDraft(agent), appInfo.chatgptAppPath));
setProfileActionError("");
setProfileAddOpen(true);
}
@@ -2401,7 +2414,10 @@ function App() {
return;
}
setProfileEditIndex(index);
setProfileEditDraft(createProfileDraftFromProfile(profile, draftConfig.botConfigs));
setProfileEditDraft(profileDraftWithDetectedAppPath(
createProfileDraftFromProfile(profile, draftConfig.botConfigs),
appInfo.chatgptAppPath
));
setProfileActionError("");
}
@@ -2623,10 +2639,10 @@ function App() {
const next = { ...current, ...patch };
if (patch.agent && patch.agent !== current.agent) {
const name = current.name === profileAgentLabel(current.agent) ? undefined : next.name;
return {
return profileDraftWithDetectedAppPath({
...createProfileDraft(patch.agent, name),
envRows: profileEnvRowsForAgent(patch.agent, current.envRows)
};
}, appInfo.chatgptAppPath);
}
return next;
});
@@ -2638,10 +2654,10 @@ function App() {
const next = { ...current, ...patch };
if (patch.agent && patch.agent !== current.agent) {
const name = current.name === profileAgentLabel(current.agent) ? undefined : next.name;
return {
return profileDraftWithDetectedAppPath({
...createProfileDraft(patch.agent, name),
envRows: profileEnvRowsForAgent(patch.agent, current.envRows)
};
}, appInfo.chatgptAppPath);
}
return next;
});
@@ -2769,6 +2785,14 @@ function App() {
}));
}
function confirmProfileDelete() {
if (profileDeleteIndex === undefined) {
return;
}
removeProfile(profileDeleteIndex);
setProfileDeleteIndex(undefined);
}
return (
<AppI18nContext.Provider value={copy}>
<LayoutGroup id="home-shell">
@@ -2894,7 +2918,7 @@ function App() {
openProfileApp: (index) => void openProfileAppFromList(index),
profileActionBusy,
profileRuntimeStatus,
removeProfile,
removeProfile: setProfileDeleteIndex,
stopProfileApp: (index) => void stopProfileAppFromList(index),
updateProfileItem
},
@@ -3032,6 +3056,11 @@ function App() {
virtualModelProfiles: draftConfig.virtualModelProfiles ?? [],
onSubmit: submitProfileDraft
} : undefined}
profileDelete={profileDeleteItem ? {
onClose: () => setProfileDeleteIndex(undefined),
onConfirm: confirmProfileDelete,
profile: profileDeleteItem
} : undefined}
profileEdit={profileEditIndex !== undefined ? {
botConfigs: draftConfig.botConfigs,
canSubmit: canSubmitProfileEdit,
@@ -2,7 +2,7 @@ import type { ComponentProps, ReactElement } from "react";
import { AnimatePresence, DialogStackLayer } from "../shared/index";
import { AddApiKeyDialog, ApiKeyCreatedDialog, EditApiKeyDialog } from "./api-keys";
import { ConfigureClaudeDesignDialog, DeleteExtensionDialog, PluginSettingsDialog } from "./extensions";
import { AddProfileDialog, ProfileOpenDialog } from "./profiles";
import { AddProfileDialog, DeleteProfileDialog, ProfileOpenDialog } from "./profiles";
import { AddProviderDialog, DeleteProviderDialog, ProviderDeepLinkDialog } from "./providers";
import { AddRoutingRuleDialog, DeleteRoutingRuleDialog } from "./routing";
import { AppSettingsDialog } from "./settings";
@@ -19,6 +19,7 @@ export function AppDialogStack({
extensionInstall,
extensionSettings,
profileAdd,
profileDelete,
profileEdit,
profileOpen,
providerDeepLink,
@@ -39,6 +40,7 @@ export function AppDialogStack({
extensionInstall?: ComponentProps<typeof InstallExtensionDialog>;
extensionSettings?: ComponentProps<typeof PluginSettingsDialog>;
profileAdd?: ComponentProps<typeof AddProfileDialog>;
profileDelete?: ComponentProps<typeof DeleteProfileDialog>;
profileEdit?: ComponentProps<typeof AddProfileDialog>;
profileOpen?: ComponentProps<typeof ProfileOpenDialog>;
providerDeepLink?: ComponentProps<typeof ProviderDeepLinkDialog>;
@@ -56,6 +58,7 @@ export function AppDialogStack({
profileAdd ? { key: "profile-add", node: <AddProfileDialog {...profileAdd} /> } : null,
profileEdit ? { key: "profile-edit", node: <AddProfileDialog {...profileEdit} /> } : null,
profileOpen ? { key: "profile-open", node: <ProfileOpenDialog {...profileOpen} /> } : null,
profileDelete ? { key: "profile-delete", node: <DeleteProfileDialog {...profileDelete} /> } : null,
apiKeyEdit ? { key: "api-key-edit", node: <EditApiKeyDialog {...apiKeyEdit} /> } : null,
providerDeepLink ? { key: "provider-deep-link", node: <ProviderDeepLinkDialog {...providerDeepLink} /> } : null,
providerUpsert ? { key: "provider-upsert", node: <AddProviderDialog {...providerUpsert} /> } : null,
@@ -6,7 +6,7 @@ export { AppSettingsDialog } from "./settings";
export { UpdateDialog } from "./update";
export { OverviewView, AgentAnalysisView } from "./dashboard";
export { ApiKeysView, AddApiKeyDialog, EditApiKeyDialog } from "./api-keys";
export { ProfileView, AddProfileForm, AddProfileDialog } from "./profiles";
export { ProfileView, AddProfileForm, AddProfileDialog, DeleteProfileDialog } from "./profiles";
export { NetworkingView, LogsView } from "./network-logs";
export { ProvidersView, ModelsView, DeleteProviderDialog, ProviderDeepLinkDialog, AddProviderForm, AddProviderDialog } from "./providers";
export { RoutingView, DeleteRoutingRuleDialog, AddRoutingRuleDialog } from "./routing";
@@ -1,6 +1,6 @@
import {
AddProfileDraft, AgentLogo, AnimatedIconSwap, AnimatedPopover, AnimatePresence, AppConfig, Badge, BotGatewaySavedConfig, botGatewaySavedConfigLabel, BotHandoffScanTarget, Button,
Card, CardContent, CardHeader, CardTitle, Check, ChevronDown, Copy,
Card, CardContent, CardHeader, CardTitle, Check, ChevronDown, CircleAlert, Copy,
cn, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader,
DialogTitle, Field, GatewayProviderConfig, Info, Input, KeyValueRowsControl, LoaderCircle, motion,
normalizeProfileScope, normalizeProfileSurface, Pencil, Plus, PopoverContent,
@@ -176,6 +176,63 @@ export function ProfileView({
);
}
export function DeleteProfileDialog({
onClose,
onConfirm,
profile
}: {
onClose: () => void;
onConfirm: () => void;
profile: ProfileConfig;
}) {
const t = useAppText();
const name = profile.name || t("Unnamed");
const agent = t(profileAgentLabel(profile.agent));
return (
<Dialog onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-[520px]">
<DialogHeader>
<div className="min-w-0">
<DialogTitle>{t("Delete Profile")}</DialogTitle>
</div>
<Button aria-label={t("Close dialog")} onClick={onClose} size="iconSm" title={t("Close")} type="button" variant="ghost">
<X className="h-4 w-4" />
</Button>
</DialogHeader>
<DialogBody>
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2.5">
<div className="flex items-start gap-2 text-[12px] font-medium text-destructive">
<CircleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>{t("Delete this agent profile from the configuration?")}</span>
</div>
<div className="mt-2 space-y-1 text-[11px] text-muted-foreground">
<div className="truncate" title={name}>
<span className="font-medium text-foreground">{t("Name")}:</span> {name}
</div>
<div className="truncate" title={agent}>
<span className="font-medium text-foreground">{t("Agent")}:</span> {agent}
</div>
<div>{t("This action is applied immediately to the draft config and will auto-save with other changes.")}</div>
</div>
</div>
</DialogBody>
<DialogFooter>
<Button autoFocus onClick={onClose} type="button" variant="outline">
{t("Cancel")}
</Button>
<Button onClick={onConfirm} type="button" variant="destructive">
<Trash2 className="h-4 w-4" />
{t("Delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function ProfileOpenDialog({
appRunning = false,
busy,
@@ -767,10 +767,12 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Delete": "删除",
"Delete bot": "删除 Bot",
"Delete Extension": "删除扩展",
"Delete Profile": "删除 Agent 配置",
"Delete Provider": "删除供应商",
"Delete Routing Rule": "删除路由规则",
"Delete this bot?": "删除这个 Bot",
"Delete this extension from the configuration?": "从配置中删除这个扩展?",
"Delete this agent profile from the configuration?": "从配置中删除这个 Agent 配置档案?",
"Delete this provider from the configuration?": "从配置中删除这个供应商?",
"Delete this routing rule from the configuration?": "从配置中删除这条路由规则?",
"Dependencies": "依赖",
@@ -756,6 +756,14 @@ export function createProfileDraft(agent: ProfileConfig["agent"] = "claude-code"
};
}
export function profileDraftWithDetectedAppPath(draft: AddProfileDraft, chatgptAppPath?: string): AddProfileDraft {
const detectedPath = chatgptAppPath?.trim() || "";
if (draft.agent !== "codex" || draft.appPath.trim() || !detectedPath) {
return draft;
}
return { ...draft, appPath: detectedPath };
}
export function createProfileDraftFromProfile(profile: ProfileConfig, botConfigs: BotGatewaySavedConfig[] = []): AddProfileDraft {
const botDraft = createBotGatewayDraft(profile.botGateway);
const botConfigId = profile.botConfigId || matchingBotConfigId(profile.botGateway, botConfigs);
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import test from "node:test";
const gatewayRoot = path.join(process.cwd(), "packages", "core", "src", "gateway");
function typescriptFiles(directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) {
return typescriptFiles(file);
}
return entry.isFile() && entry.name.endsWith(".ts") ? [file] : [];
});
}
test("gateway service remains a compatibility facade", () => {
const serviceFile = path.join(gatewayRoot, "service.ts");
const source = readFileSync(serviceFile, "utf8");
const implementationLines = source
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("*") && !line.startsWith("/*"));
assert.ok(implementationLines.length <= 20);
assert.doesNotMatch(source, /class\s+GatewayService/);
assert.doesNotMatch(source, /node:http/);
assert.match(source, /gateway\/application\/gateway-service/);
assert.match(source, /routing\/protocol-endpoints/);
});
test("gateway implementation modules do not depend on the public facade", () => {
const serviceFile = path.join(gatewayRoot, "service.ts");
const reverseDependencies = typescriptFiles(gatewayRoot)
.filter((file) => file !== serviceFile)
.filter((file) => /(?:@ccr\/core|\.\.)\/gateway\/service/.test(readFileSync(file, "utf8")));
assert.deepEqual(reverseDependencies, []);
});
test("core config compilation is separated from filesystem persistence", () => {
const compiler = readFileSync(
path.join(gatewayRoot, "core-runtime", "config-compiler.ts"),
"utf8"
);
const writer = readFileSync(
path.join(gatewayRoot, "core-runtime", "config-writer.ts"),
"utf8"
);
assert.doesNotMatch(compiler, /node:fs|writeFileSync|mkdirSync/);
assert.match(writer, /compileCoreGatewayConfig/);
assert.match(writer, /writeFileSync/);
});
+99 -11
View File
@@ -9,7 +9,17 @@ import {
function createRouterPlugin(options = {}) {
const agent = options.agent ?? "claude-code";
return new ClaudeCodeRouterPlugin({
const profiles = options.profiles ?? [
{
agent,
enabled: options.profileEnabled ?? true,
id: `${agent}-profile`,
model: options.profileModel ?? "",
name: agent,
scope: "global"
}
];
const plugin = new ClaudeCodeRouterPlugin({
CUSTOM_ROUTER_PATH: "",
Providers: options.providers ?? [
{
@@ -30,20 +40,22 @@ function createRouterPlugin(options = {}) {
},
profile: {
enabled: options.profileRuntimeEnabled ?? true,
profiles: [
{
agent,
enabled: options.profileEnabled ?? true,
id: `${agent}-profile`,
model: options.profileModel ?? "",
name: agent,
scope: "global"
}
]
profiles
},
toolHub: options.toolHub,
virtualModelProfiles: options.virtualModelProfiles ?? []
});
return {
routeRequest(input) {
if (options.authenticatedProfileId !== null && input.headers["x-auth-api-key-id"] === undefined) {
const authenticatedProfileId = options.authenticatedProfileId ?? profiles[0]?.id;
if (authenticatedProfileId) {
input.headers["x-auth-api-key-id"] = `profile:${authenticatedProfileId}`;
}
}
return plugin.routeRequest(input);
}
};
}
test("fallback retry delay backs off retryable HTTP statuses", () => {
@@ -274,6 +286,79 @@ test("built-in Claude Code route matches user-agent case-insensitively", async (
assert.equal(result.decision.reason, "builtin:claude-code");
});
test("built-in Codex route uses the authenticated profile instead of the first Codex profile", async () => {
const plugin = createRouterPlugin({
agent: "codex",
authenticatedProfileId: "bs-2",
profiles: [
{
agent: "codex",
enabled: true,
id: "codex",
model: "Codex API/gpt-5.6-sol",
name: "Codex",
scope: "ccr"
},
{
agent: "codex",
enabled: true,
id: "bs-2",
model: "uuroute/gpt-5.5",
name: "bs",
scope: "ccr"
}
],
providers: [
{
models: ["gpt-5.6-sol"],
name: "Codex API",
type: "openai_responses"
},
{
models: ["gpt-5.5"],
name: "uuroute",
type: "openai_responses"
}
]
});
const result = await plugin.routeRequest({
body: {
model: "gpt-5"
},
headers: {
"user-agent": "Codex Desktop/0.144.0"
},
method: "POST",
url: "/v1/responses"
});
assert.equal(result.body.model, "uuroute/gpt-5.5");
assert.equal(result.decision.model, "uuroute/gpt-5.5");
assert.equal(result.decision.reason, "builtin:codex");
});
test("built-in Codex route preserves the requested model when the authenticated profile does not match", async () => {
const plugin = createRouterPlugin({
agent: "codex",
authenticatedProfileId: "missing-profile",
profileModel: "Provider/gpt-5-codex"
});
const result = await plugin.routeRequest({
body: {
model: "Provider/gpt-5-codex"
},
headers: {
"user-agent": "Codex Desktop/0.144.0"
},
method: "POST",
url: "/v1/responses"
});
assert.equal(result.body.model, "Provider/gpt-5-codex");
assert.equal(result.decision.model, "Provider/gpt-5-codex");
assert.equal(result.decision.reason, "default");
});
test("built-in Claude Code route does not inject Claude Code native tool search", async () => {
const plugin = createRouterPlugin({
profileModel: "Provider/claude-sonnet",
@@ -808,6 +893,7 @@ test("issue 1480 raw user config reproduces the old failure precondition when Ro
}
});
const headers = {
"x-auth-api-key-id": "profile:claude-code-main",
"user-agent": "claude-cli/2.1.187 (external, cli)"
};
const result = await plugin.routeRequest({
@@ -831,6 +917,7 @@ test("issue 1480 raw user config ignores an unreplaced Provider/model subagent p
const config = createIssue1480UserConfig();
const plugin = new ClaudeCodeRouterPlugin(config);
const headers = {
"x-auth-api-key-id": "profile:claude-code-main",
"user-agent": "claude-cli/2.1.187 (external, cli)"
};
const result = await plugin.routeRequest({
@@ -856,6 +943,7 @@ test("issue 1480 config routes Claude Code profile traffic through the user defa
const config = createIssue1480RouterConfig();
const plugin = new ClaudeCodeRouterPlugin(config);
const headers = {
"x-auth-api-key-id": "profile:claude-code-main",
"user-agent": "claude-cli/2.1.187 (external, cli)"
};
const result = await plugin.routeRequest({
+51
View File
@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import test from "node:test";
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { ProfileConfig } from "../../packages/core/src/contracts/app.ts";
import { DeleteProfileDialog } from "../../packages/ui/src/pages/home/components/profiles.tsx";
import { AppI18nContext, appCopy } from "../../packages/ui/src/pages/home/shared/i18n.tsx";
import { createProfileDraft, profileDraftWithDetectedAppPath } from "../../packages/ui/src/pages/home/shared/profiles.ts";
const profile: ProfileConfig = {
agent: "claude-code",
enabled: true,
id: "claude-code-main",
model: "openai/gpt-5.2",
name: "Claude Code Main"
};
test("DeleteProfileDialog identifies the profile and requires an explicit confirmation", () => {
const html = renderToStaticMarkup(
<DeleteProfileDialog onClose={() => undefined} onConfirm={() => undefined} profile={profile} />
);
assert.match(html, /Delete Profile/);
assert.match(html, /Delete this agent profile from the configuration\?/);
assert.match(html, /Claude Code Main/);
assert.match(html, /Claude Code/);
assert.match(html, />Cancel<\/button>/);
assert.match(html, />Delete<\/button>/);
});
test("DeleteProfileDialog renders the Chinese confirmation copy", () => {
const html = renderToStaticMarkup(
<AppI18nContext.Provider value={appCopy.zh}>
<DeleteProfileDialog onClose={() => undefined} onConfirm={() => undefined} profile={profile} />
</AppI18nContext.Provider>
);
assert.match(html, /删除 Agent 配置/);
assert.match(html, /从配置中删除这个 Agent 配置档案?/);
assert.match(html, />取消<\/button>/);
assert.match(html, />删除<\/button>/);
});
test("detected CHATGPT_APP_PATH is used as the Codex profile default", () => {
const detectedPath = "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT";
const draft = profileDraftWithDetectedAppPath(createProfileDraft("codex"), ` ${detectedPath} `);
assert.equal(draft.appPath, detectedPath);
assert.equal(profileDraftWithDetectedAppPath({ ...draft, appPath: "/custom/chatgpt" }, detectedPath).appPath, "/custom/chatgpt");
assert.equal(profileDraftWithDetectedAppPath(createProfileDraft("claude-code"), detectedPath).appPath, "");
});