Merge dev/3.1 into main for v3.0.19

# Conflicts:
#	packages/ui/src/pages/home/components/network-logs.tsx
This commit is contained in:
musistudio
2026-08-05 18:56:16 +08:00
35 changed files with 2563 additions and 180 deletions
+2 -1
View File
@@ -1,4 +1,4 @@
import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyBundledClaudeRuntimePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
import { buildBrowserRenderer, buildMain, buildRenderer, buildRequestLogBodyWorker, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyBundledClaudeRuntimePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production";
@@ -14,6 +14,7 @@ await Promise.all([
buildMain({ mode }),
buildBrowserRenderer({ mode }),
buildRenderer({ mode }),
buildRequestLogBodyWorker({ mode }),
buildTrayRenderer({ mode }),
buildWebClientBridge({ mode }),
buildStyles({ minify: mode === "production" })
+15 -1
View File
@@ -17,6 +17,7 @@ import {
copyModelCatalog,
copyRendererHtml,
copyTrayRendererHtml,
createRequestLogBodyWorkerBuildOptions,
createBrowserRendererBuildOptions,
createCliBuildOptions,
createMainBuildOptions,
@@ -51,6 +52,7 @@ let queuedStyleBuildReason = null;
const ready = {
browser: false,
cli: false,
logWorker: false,
main: false,
renderer: false,
tray: false,
@@ -66,6 +68,7 @@ const coreSharedSourceRoot = path.join(coreSourceRoot, "shared");
const styleWatchRoots = [rendererRoot, coreSharedSourceRoot].filter((watchRoot) => existsSync(watchRoot));
const activeReadyNames = new Set([
...(enabled.ui ? ["browser", "renderer", "tray", "webBridge"] : []),
...(enabled.ui ? ["logWorker"] : []),
...(enabled.cli ? ["cli"] : []),
...(enabled.electron ? ["main"] : [])
]);
@@ -295,7 +298,7 @@ function pollSourceWatchTargets() {
}
function markReady(name, reason = `${name} esbuild completed`) {
if (name === "browser" || name === "cli" || name === "main" || name === "renderer" || name === "tray" || name === "webBridge") {
if (name === "browser" || name === "cli" || name === "logWorker" || name === "main" || name === "renderer" || name === "tray" || name === "webBridge") {
ready[name] = true;
}
logDev(`build ready: ${reason}; ${readyState()}`);
@@ -509,6 +512,17 @@ if (enabled.ui) {
})
]
})
),
await esbuild.context(
createRequestLogBodyWorkerBuildOptions({
mode: "development",
plugins: [
watchPlugin("logWorker", (name) => {
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
)
);
}
+30
View File
@@ -66,7 +66,10 @@ export const trayRendererHtmlOutput = path.join(rendererOutDir, "pages", "tray",
export const cssInput = path.join(rendererRoot, "styles", "globals.css");
export const cssOutput = path.join(rendererAssetsDir, "main.css");
export const webClientBridgeOutput = path.join(rendererAssetsDir, "web-client-bridge.js");
export const requestLogBodyWorkerOutput = path.join(rendererAssetsDir, "log-body.worker.js");
export const requestLogBodyWorkerInput = path.join(rendererRoot, "pages", "home", "shared", "log-body.worker.ts");
export const electronUndiciProxyAgentInput = path.join(coreSourceRoot, "proxy", "undici-proxy-agent.ts");
export const localAgentAuthProviderHookInput = path.join(coreSourceRoot, "gateway", "core-runtime", "local-agent-auth-provider-hook.ts");
export const upstreamHeaderSanitizerInput = path.join(coreSourceRoot, "gateway", "core-runtime", "upstream-header-sanitizer.ts");
const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js", "media-tools-proxy-mcp.js"];
const lightweightMcpBundleMaxBytes = 128 * 1024;
@@ -234,6 +237,7 @@ export function createMainBuildOptions({ mode = "production", plugins = [] } = {
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts"),
path.join(coreSourceRoot, "observability", "request-log-worker.ts"),
path.join(coreSourceRoot, "routing", "route-script-worker.ts"),
localAgentAuthProviderHookInput,
upstreamHeaderSanitizerInput,
electronUndiciProxyAgentInput,
path.join(electronSourceRoot, "main", "preload.ts")
@@ -266,6 +270,7 @@ export function createCliBuildOptions({ mode = "production", plugins = [] } = {}
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts"),
path.join(coreSourceRoot, "observability", "request-log-worker.ts"),
path.join(coreSourceRoot, "routing", "route-script-worker.ts"),
localAgentAuthProviderHookInput,
upstreamHeaderSanitizerInput
],
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
@@ -295,6 +300,7 @@ export function createCoreServerBuildOptions({ mode = "production", plugins = []
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts"),
path.join(coreSourceRoot, "observability", "request-log-worker.ts"),
path.join(coreSourceRoot, "routing", "route-script-worker.ts"),
localAgentAuthProviderHookInput,
upstreamHeaderSanitizerInput
],
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
@@ -375,6 +381,26 @@ export function createWebClientBridgeBuildOptions({ mode = "production", plugins
};
}
export function createRequestLogBodyWorkerBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
define: {
"process.env.NODE_ENV": JSON.stringify(mode)
},
entryPoints: [requestLogBodyWorkerInput],
format: "esm",
legalComments: "none",
logLevel: "info",
minify: mode === "production",
outfile: requestLogBodyWorkerOutput,
platform: "browser",
plugins: [rendererAliasPlugin(), packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production",
target: "chrome120"
};
}
export function createBotGatewaySdkBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
@@ -460,6 +486,10 @@ export async function buildWebClientBridge(options = {}) {
await esbuild.build(createWebClientBridgeBuildOptions(options));
}
export async function buildRequestLogBodyWorker(options = {}) {
await esbuild.build(createRequestLogBodyWorkerBuildOptions(options));
}
export function copyCliRuntimeToElectronDist() {
ensureDist();
const cliRuntime = path.join(cliMainOutDir, "cli.js");
+1
View File
@@ -27,6 +27,7 @@ const testProjects = {
runtimeEntryPoints: {
"runtime/fusion-vision-mcp": path.join(packageRoots.core, "mcp", "fusion-vision-mcp.ts"),
"runtime/gateway-bootstrap": path.join(packageRoots.core, "gateway", "core-runtime", "gateway-bootstrap.ts"),
"runtime/local-agent-auth-provider-hook": path.join(packageRoots.core, "gateway", "core-runtime", "local-agent-auth-provider-hook.ts"),
"runtime/media-tools-proxy-mcp": path.join(packageRoots.core, "mcp", "media-tools-proxy-mcp.ts"),
"runtime/request-log-worker": path.join(packageRoots.core, "observability", "request-log-worker.ts"),
"runtime/route-script-worker": path.join(packageRoots.core, "routing", "route-script-worker.ts"),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

+16 -6
View File
@@ -12,7 +12,7 @@
"packages/*"
],
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.15",
"@the-next-ai/ai-gateway": "^1.0.16",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"electron-updater": "^6.8.9",
@@ -2311,15 +2311,16 @@
}
},
"node_modules/@the-next-ai/ai-gateway": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/@the-next-ai/ai-gateway/-/ai-gateway-1.0.15.tgz",
"integrity": "sha512-U5SnIBGXHVq0uzzljpUb/hvND5cGehzMZegtvnB8+pRsRTM19yrAsPSaUocolB7JYun7TlKhi+BdOWp5Az17gA==",
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/@the-next-ai/ai-gateway/-/ai-gateway-1.0.16.tgz",
"integrity": "sha512-9umpJ3gGROlXXGBHXHwOdhO9TEcQeG6AlnNmbccaUY5v64czQNFEod23mR31UfqLkv+TpHbKrcDtmuZ5//6S6w==",
"license": "MIT",
"dependencies": {
"diff": "^8.0.3",
"fastify": "^5.8.2",
"glob": "^13.0.6",
"openai": "^6.27.0",
"undici": "^6.28.0",
"ws": "^8.19.0"
},
"bin": {
@@ -2329,6 +2330,15 @@
"node": ">=20"
}
},
"node_modules/@the-next-ai/ai-gateway/node_modules/undici": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/@the-next-ai/bot-gateway-sdk": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@the-next-ai/bot-gateway-sdk/-/bot-gateway-sdk-0.1.0.tgz",
@@ -9546,7 +9556,7 @@
"version": "3.0.18",
"license": "MIT",
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.15",
"@the-next-ai/ai-gateway": "^1.0.16",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"node-forge": "^1.4.0",
@@ -9563,7 +9573,7 @@
"name": "@claude-code-router/core",
"version": "3.0.18",
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.15",
"@the-next-ai/ai-gateway": "^1.0.16",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"node-forge": "^1.4.0",
+1 -1
View File
@@ -72,7 +72,7 @@
"rebuild:sqlite3": "electron-rebuild -f -w better-sqlite3"
},
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.15",
"@the-next-ai/ai-gateway": "^1.0.16",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"electron-updater": "^6.8.9",
+1 -1
View File
@@ -41,7 +41,7 @@
"test:integration": "node ../../build/test.mjs cli --scope integration && node ../../build/run-tests.mjs cli"
},
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.15",
"@the-next-ai/ai-gateway": "^1.0.16",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"node-forge": "^1.4.0",
+1 -1
View File
@@ -16,7 +16,7 @@
"test:integration": "node ../../build/test.mjs core --scope integration && node ../../build/run-tests.mjs core"
},
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.15",
"@the-next-ai/ai-gateway": "^1.0.16",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"node-forge": "^1.4.0",
@@ -24,6 +24,13 @@ type BotGatewaySdkModule = {
createBotGatewayClient: (options?: unknown) => unknown;
};
type BotGatewayCommand = {
args?: string[];
command: string;
cwd?: string;
electronRunAsNode?: boolean;
};
type QrSession = {
botConfigId: string;
client: BotGatewayClientWithRequest;
@@ -172,22 +179,25 @@ export function cancelBotGatewayQrLogin(
async function createQrClient(bot: BotGatewayRuntimeConfig, stateDir: string): Promise<BotGatewayClientWithRequest> {
const sdk = await loadBotGatewaySdk();
const command = resolveBotGatewayCommand(sdk, bot);
const { electronRunAsNode, ...commandOptions } = command ?? {};
const client = sdk.createBotGatewayClient({
transport: "stdio",
env: {
...process.env,
...(electronRunAsNode ? { ELECTRON_RUN_AS_NODE: "1" } : {}),
BOT_GATEWAY_STATE_DIR: stateDir,
CODEXL_HOME: CONFIGDIR
},
...command
...commandOptions
}) as BotGatewayClientWithRequest;
if (!client || typeof client.request !== "function" || typeof client.health !== "function") {
throw new Error("Bot Gateway SDK client does not expose request().");
}
attachBotGatewayStdioErrorHandler(client);
return client;
}
function resolveBotGatewayCommand(sdk: BotGatewaySdkModule, bot: BotGatewayRuntimeConfig): { args?: string[]; command: string; cwd?: string } | undefined {
function resolveBotGatewayCommand(sdk: BotGatewaySdkModule, bot: BotGatewayRuntimeConfig): BotGatewayCommand | undefined {
if (bot.command) {
return {
args: bot.args,
@@ -199,21 +209,19 @@ function resolveBotGatewayCommand(sdk: BotGatewaySdkModule, bot: BotGatewayRunti
return undefined;
}
const bundledPath = sdk.bundledStdioPath();
const runnerPath = materializeBotGatewayStdioRunnerPath(bundledPath);
return {
args: [sanitizedBotGatewayStdioRunnerPath(bundledPath)],
args: [runnerPath],
command: process.execPath,
cwd: path.dirname(bundledPath)
cwd: path.dirname(runnerPath),
electronRunAsNode: Boolean(process.versions.electron)
};
}
function sanitizedBotGatewayStdioRunnerPath(sourcePath: string): string {
export function materializeBotGatewayStdioRunnerPath(sourcePath: string, configDir = CONFIGDIR): string {
const source = readFileSync(sourcePath, "utf8");
const normalized = normalizeDuplicateShebangs(source);
if (normalized === source) {
return sourcePath;
}
const targetDir = path.join(CONFIGDIR, "bot-gateway", "runners");
const targetDir = path.join(configDir, "bot-gateway", "runners");
const targetPath = path.join(targetDir, "bot-gateway-stdio.mjs");
mkdirSync(targetDir, { recursive: true });
if (!existsSync(targetPath) || readFileSync(targetPath, "utf8") !== normalized) {
@@ -234,6 +242,44 @@ function normalizeDuplicateShebangs(source: string): string {
return [lines[0], ...lines.slice(index)].join("\n");
}
function attachBotGatewayStdioErrorHandler(client: BotGatewayClientWithRequest): void {
const stdioClient = client as BotGatewayClientWithRequest & {
child?: {
listenerCount?: (eventName: string) => number;
once: (eventName: string, listener: (error: Error) => void) => unknown;
};
pending?: Map<unknown, { reject?: (error: Error) => void }>;
};
const attach = () => {
const child = stdioClient.child;
if (!child || typeof child.once !== "function") {
return;
}
const listenerCount = typeof child.listenerCount === "function" ? child.listenerCount("error") : 0;
if (listenerCount > 0) {
return;
}
child.once("error", (error: Error) => {
const pending = stdioClient.pending instanceof Map ? stdioClient.pending : undefined;
if (pending) {
for (const request of pending.values()) {
request.reject?.(error);
}
pending.clear();
}
});
};
const originalRequest = client.request.bind(client);
client.request = <T = unknown>(method: string, params?: unknown): Promise<T> => {
try {
return originalRequest<T>(method, params);
} finally {
attach();
}
};
attach();
}
async function loadBotGatewaySdk(): Promise<BotGatewaySdkModule> {
if (!sdkPromise) {
sdkPromise = importBotGatewaySdk();
@@ -5498,6 +5498,7 @@ class BotGatewayBridge {
});
const clientOptions = botGatewaySdkClientOptions(this.config, env, sdk);
this.client = sdk.createBotGatewayClient(clientOptions);
attachBotGatewayStdioErrorHandler(this.client);
await withTimeout(this.client.health(), this.config.startupTimeoutMs, "Bot Gateway health check timed out.");
this.updateDiagnostics({ state: "connected", connectedAt: new Date().toISOString(), lastError: "" });
await this.ensureIntegration();
@@ -5686,10 +5687,13 @@ function botGatewaySdkImportSpecifier(value) {
function botGatewaySdkClientOptions(config, env, sdk) {
const command = resolveBotGatewayCommand(config) || resolveBundledBotGatewayCommand(sdk);
const commandOptions = Object.assign({}, command || {});
const electronRunAsNode = Boolean(commandOptions.electronRunAsNode);
delete commandOptions.electronRunAsNode;
return {
transport: "stdio",
...(command || {}),
env
...commandOptions,
env: electronRunAsNode ? { ...env, ELECTRON_RUN_AS_NODE: "1" } : env
};
}
@@ -5709,20 +5713,18 @@ function resolveBundledBotGatewayCommand(sdk) {
return undefined;
}
const bundledPath = sdk.bundledStdioPath();
const runnerPath = materializeBotGatewayStdioRunnerPath(bundledPath);
return {
command: process.execPath,
args: [sanitizedBotGatewayStdioRunnerPath(bundledPath)],
cwd: path.dirname(bundledPath)
args: [runnerPath],
cwd: path.dirname(runnerPath),
electronRunAsNode: Boolean(process.versions && process.versions.electron)
};
}
function sanitizedBotGatewayStdioRunnerPath(sourcePath) {
function materializeBotGatewayStdioRunnerPath(sourcePath) {
const source = fs.readFileSync(sourcePath, "utf8");
const normalized = normalizeDuplicateShebangs(source);
if (normalized === source) {
return sourcePath;
}
const targetDir = path.join(CONFIG_DIR, "bot-gateway", "runners");
const targetPath = path.join(targetDir, "bot-gateway-stdio.mjs");
fs.mkdirSync(targetDir, { recursive: true });
@@ -5744,6 +5746,35 @@ function normalizeDuplicateShebangs(source) {
return [lines[0], ...lines.slice(index)].join("\n");
}
function attachBotGatewayStdioErrorHandler(client) {
const attach = () => {
const child = client && client.child;
if (!child || typeof child.once !== "function") return;
const listenerCount = typeof child.listenerCount === "function" ? child.listenerCount("error") : 0;
if (listenerCount > 0) return;
child.once("error", (error) => {
const pending = client && client.pending instanceof Map ? client.pending : null;
if (!pending) return;
for (const request of pending.values()) {
if (request && typeof request.reject === "function") request.reject(error);
}
pending.clear();
});
};
if (!client || typeof client.request !== "function") {
return;
}
const originalRequest = client.request.bind(client);
client.request = (method, params) => {
try {
return originalRequest(method, params);
} finally {
attach();
}
};
attach();
}
function botGatewayClientRequest(client, method, params, timeoutMs) {
if (!client || typeof client.request !== "function") {
return Promise.reject(new Error("Bot Gateway SDK client does not expose request()."));
@@ -137,7 +137,7 @@ function codexModelCatalogItem(
default_reasoning_level: profile.defaultReasoningLevel,
default_reasoning_effort: profile.defaultReasoningLevel,
default_reasoning_summary: profile.defaultReasoningSummary,
description: `CCR gateway model ${model}`,
description: profile.description ?? `CCR gateway model ${model}`,
displayName: model,
display_name: model,
effective_context_window_percent: effectiveContextWindowPercent,
@@ -179,6 +179,7 @@ type CodexCapabilityProfile = {
applyPatchToolType: string | null;
catalogEntry?: ModelCatalogEntry;
contextWindow?: number;
description?: string;
defaultReasoningLevel: string | null;
defaultReasoningSummary: string;
effectiveContextWindowPercent?: number;
@@ -250,6 +251,9 @@ function codexModelCapabilityProfile(
applyPatchToolType,
catalogEntry,
contextWindow: providerModelMetadata?.contextWindow,
description: provider
? providerModelDescriptionFor(provider, providerModel)
: undefined,
defaultReasoningLevel: resolveDefaultReasoningLevel(
providerModelMetadata?.defaultReasoningLevel !== undefined
? providerModelMetadata.defaultReasoningLevel
@@ -281,6 +285,17 @@ function providerModelMetadataFor(provider: GatewayProviderConfig, model: string
return match?.[1];
}
function providerModelDescriptionFor(provider: GatewayProviderConfig, model: string): string | undefined {
const descriptions = provider.modelDescriptions ?? {};
const direct = descriptions[model]?.trim();
if (direct) {
return direct;
}
const normalized = model.trim().toLowerCase();
const match = Object.entries(descriptions).find(([candidate]) => candidate.trim().toLowerCase() === normalized);
return match?.[1]?.trim() || undefined;
}
function codexProviderModelMetadataFor(provider: GatewayProviderConfig, model: string): ProviderModelMetadata | undefined {
const metadata = providerModelMetadataFor(provider, model) ?? localCodexModelMetadataFor(provider, model);
if (!isLocalCodexProvider(provider)) {
@@ -13,14 +13,16 @@ import { mediaToolsMcpServer } from "@ccr/core/mcp/grok-media-config";
import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery";
import { activeProviderCredentials, inferProtocol, normalizedProviderCapabilities, normalizeProviderProtocol, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCapabilityNameMatches, providerCredentialInternalName, providerProtocolForClientProtocol, sortProviderCredentialsForConfig, toCoreGatewayProviders } from "@ccr/core/providers/runtime-topology";
import { buildRawTraceConfig } from "@ccr/core/observability/raw-trace-sync";
import { endpoint, resolveUndiciProxyAgentModule, resolveUpstreamHeaderSanitizerEntry, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor";
import { endpoint, resolveLocalAgentAuthProviderHookEntry, resolveUndiciProxyAgentModule, resolveUpstreamHeaderSanitizerEntry, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor";
import { billingUsageSyncHeader, billingUsageSyncPath, 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 { isLocalAgentOauthProviderPlugin } from "@ccr/core/gateway/core-runtime/local-agent-auth-provider-hook";
import { resolveConfiguredProviderModelSelector, resolveUniqueConfiguredProviderModelSelector } from "@ccr/core/routing/model-resolution";
const upstreamHeaderSanitizerPluginKey = "ccr-upstream-header-sanitizer";
const localAgentAuthProviderHookPluginKey = "ccr-local-agent-auth-provider-hooks";
export const unlimitedVirtualModelToolCalls = Number.MAX_SAFE_INTEGER;
export const unlimitedVirtualModelToolTurns = Number.MAX_SAFE_INTEGER;
@@ -36,7 +38,8 @@ export async function compileCoreGatewayConfig(
const pluginCoreGatewayConfig = pluginService.getCoreGatewayConfig();
const configuredGatewayPlugins = Array.isArray(pluginCoreGatewayConfig.plugins)
? pluginCoreGatewayConfig.plugins.filter((plugin) =>
!isRecord(plugin) || stringValue(plugin.key) !== upstreamHeaderSanitizerPluginKey
!isRecord(plugin) ||
![localAgentAuthProviderHookPluginKey, upstreamHeaderSanitizerPluginKey].includes(stringValue(plugin.key) ?? "")
)
: [];
const pluginBillingConfig = isRecord(pluginCoreGatewayConfig.billing) ? pluginCoreGatewayConfig.billing : {};
@@ -82,6 +85,7 @@ export async function compileCoreGatewayConfig(
.filter((provider): provider is CoreGatewayProvider => Boolean(provider)),
...builtinToolArtifacts.providers
];
const localAgentAuthProviderHookPlugin = localAgentAuthProviderHookPluginConfig(providerPluginsWithCapabilityAliases);
const pluginAgentConfig = isRecord(pluginCoreGatewayConfig.agent) ? pluginCoreGatewayConfig.agent : {};
const pluginMcpServers = Array.isArray(pluginAgentConfig.mcpServers) ? pluginAgentConfig.mcpServers : [];
const externalMcpServers = [
@@ -138,6 +142,7 @@ export async function compileCoreGatewayConfig(
port: config.gateway.corePort,
plugins: [
...configuredGatewayPlugins,
...(localAgentAuthProviderHookPlugin ? [localAgentAuthProviderHookPlugin] : []),
{
enabled: true,
key: upstreamHeaderSanitizerPluginKey,
@@ -156,6 +161,17 @@ export async function compileCoreGatewayConfig(
};
}
function localAgentAuthProviderHookPluginConfig(providerPlugins: unknown[]): Record<string, unknown> | undefined {
if (!providerPlugins.some(isLocalAgentOauthProviderPlugin)) {
return undefined;
}
return {
enabled: true,
key: localAgentAuthProviderHookPluginKey,
modulePath: resolveLocalAgentAuthProviderHookEntry()
};
}
function withProviderCapabilityPluginAliases(
providerPlugins: unknown[],
providers: GatewayProviderConfig[]
@@ -518,6 +534,7 @@ async function withGrokOauthRuntimeDefaults(providerPlugins: unknown[]): Promise
const currentHeaders = isRecord(currentAuth.headers) ? currentAuth.headers : {};
const currentRequest = isRecord(plugin.request) ? plugin.request : {};
const currentRequestHeaders = isRecord(currentRequest.headers) ? currentRequest.headers : {};
const currentBodyRemove = Array.isArray(currentRequest.bodyRemove) ? currentRequest.bodyRemove : [];
return {
...plugin,
auth: {
@@ -529,6 +546,12 @@ async function withGrokOauthRuntimeDefaults(providerPlugins: unknown[]): Promise
},
request: {
...currentRequest,
bodyRemove: uniqueStrings([
...currentBodyRemove
.map((value) => stringValue(value))
.filter((value): value is string => Boolean(value)),
"external_web_access"
]),
headers: {
...currentRequestHeaders,
"x-grok-client-identifier": "xai-grok-cli",
@@ -0,0 +1,438 @@
import { readClaudeCodeOauth, readGrokAuth, readKimiAuth, resolveGrokAuth, resolveKimiAuth } from "@ccr/core/agents/local-providers/service";
import { grokAccessTokenExpired, grokClientVersion } from "@ccr/core/agents/local-providers/grok";
import { kimiAccessTokenExpired, kimiIdentityHeaders } from "@ccr/core/agents/local-providers/kimi";
import { transformCodexApplyPatchBridgeRequestBody } from "@ccr/core/gateway/features/codex-patch-bridge";
import { claudeCodeOauthBetaHeader, claudeCodeOauthRequiredBeta } from "@ccr/core/gateway/internal/shared";
import { isRecord, stringValue } from "@ccr/core/gateway/internal/value";
import { mergeAnthropicBetaValues } from "@ccr/core/providers/oauth-plugin";
const configProviderPluginKeyPrefix = "config:";
const localAgentProviderPluginKeyPrefix = "ccr-local-agent-";
type HeaderRecord = Record<string, string>;
type UpstreamRequest = {
body?: unknown;
bodyEncoding?: "bytes" | "form" | "json" | "none" | "text";
headers?: HeaderRecord;
method?: string;
url: string;
};
type ProviderPluginInput = {
model?: string;
request?: {
headers?: Record<string, string | string[] | undefined>;
};
upstreamRequest: UpstreamRequest;
};
type ProviderHookResult = {
ok: true;
value: UpstreamRequest;
} | {
error: string;
ok: false;
};
type ProviderHook = {
authenticate?: (input: ProviderPluginInput) => Promise<ProviderHookResult> | ProviderHookResult;
key: string;
provider?: string;
providerName?: string;
transformRequest?: (input: ProviderPluginInput) => Promise<ProviderHookResult> | ProviderHookResult;
};
type LocalAgentOauthKind = "claude-code" | "grok" | "kimi";
export function createGatewayPlugin(input: { config?: Record<string, unknown> } = {}) {
return {
providerHooks: localAgentOauthProviderHooks(input.config)
};
}
export function localAgentOauthProviderHooks(config: Record<string, unknown> | undefined): ProviderHook[] {
const providerPlugins = Array.isArray(config?.providerPlugins) ? config.providerPlugins : [];
return providerPlugins
.map(localAgentOauthProviderHook)
.filter((hook): hook is ProviderHook => Boolean(hook));
}
export function isLocalAgentOauthProviderPlugin(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
return localAgentOauthKind(value) !== undefined;
}
function localAgentOauthProviderHook(plugin: unknown): ProviderHook | undefined {
if (!isRecord(plugin)) {
return undefined;
}
const kind = localAgentOauthKind(plugin);
const key = stringValue(plugin.key);
if (!kind || !key) {
return undefined;
}
const hook: ProviderHook = {
key: `${configProviderPluginKeyPrefix}${key}`,
provider: stringValue(plugin.provider),
providerName: stringValue(plugin.providerName)
};
if (kind === "grok") {
hook.authenticate = (input) => authenticateWithBearer(input, () => resolveLiveGrokAccessToken(plugin), plugin, "Grok CLI access token was not found.");
hook.transformRequest = (input) => transformGrokRequest(input, plugin);
return hook;
}
if (kind === "kimi") {
hook.authenticate = (input) => authenticateWithBearer(input, () => resolveLiveKimiAccessToken(plugin), plugin, "Kimi CLI access token was not found.");
hook.transformRequest = (input) => transformWithHeaders(input, kimiIdentityHeaders());
return hook;
}
hook.authenticate = (input) => authenticateClaudeCode(input, plugin);
return hook;
}
function localAgentOauthKind(plugin: Record<string, unknown>): LocalAgentOauthKind | undefined {
const key = stringValue(plugin.key)?.toLowerCase();
if (!key?.startsWith(localAgentProviderPluginKeyPrefix)) {
return undefined;
}
if (key.includes("grok-cli-oauth")) {
return "grok";
}
if (key.includes("kimi-cli-oauth")) {
return "kimi";
}
if (key.includes("claude-code-oauth")) {
return "claude-code";
}
return undefined;
}
async function authenticateWithBearer(
input: ProviderPluginInput,
tokenResolver: () => Promise<string | undefined> | string | undefined,
plugin: Record<string, unknown>,
missingTokenError: string
): Promise<ProviderHookResult> {
const token = await tokenResolver() || originalBearerToken(plugin);
if (!token) {
return { error: missingTokenError, ok: false };
}
return {
ok: true,
value: {
...input.upstreamRequest,
headers: withBearerAuth(input.upstreamRequest.headers, token, originalRemoveHeaders(plugin))
}
};
}
async function authenticateClaudeCode(
input: ProviderPluginInput,
plugin: Record<string, unknown>
): Promise<ProviderHookResult> {
const token = readClaudeCodeOauth()?.accessToken || originalBearerToken(plugin);
if (!token) {
return { error: "Claude Code access token was not found.", ok: false };
}
const headers = withBearerAuth(input.upstreamRequest.headers, token, originalRemoveHeaders(plugin));
headers[claudeCodeOauthBetaHeader] = mergeAnthropicBetaValues(
requestHeader(input.request?.headers, claudeCodeOauthBetaHeader),
originalAnthropicBetaDefault(plugin),
claudeCodeOauthRequiredBeta
);
return {
ok: true,
value: {
...input.upstreamRequest,
headers
}
};
}
async function resolveLiveGrokAccessToken(plugin: Record<string, unknown>): Promise<string | undefined> {
const auth = await resolveGrokAuth().catch(() => readGrokAuth());
if (auth?.accessToken && !grokAccessTokenExpired(auth)) {
return auth.accessToken;
}
return originalBearerToken(plugin);
}
async function resolveLiveKimiAccessToken(plugin: Record<string, unknown>): Promise<string | undefined> {
const reference = kimiOauthReference(plugin);
const auth = await resolveKimiAuth(reference).catch(() => readKimiAuth(reference));
if (auth?.accessToken && !kimiAccessTokenExpired(auth)) {
return auth.accessToken;
}
return originalBearerToken(plugin);
}
function transformWithHeaders(input: ProviderPluginInput, headers: HeaderRecord): ProviderHookResult {
return {
ok: true,
value: {
...input.upstreamRequest,
headers: {
...(input.upstreamRequest.headers ?? {}),
...headers
}
}
};
}
function transformGrokRequest(input: ProviderPluginInput, plugin: Record<string, unknown>): ProviderHookResult {
const headers = {
...(input.upstreamRequest.headers ?? {}),
"x-grok-client-identifier": "xai-grok-cli",
"x-grok-client-version": grokClientVersion(),
"x-grok-model-override": input.model || originalRequestHeader(plugin, "x-grok-model-override") || ""
};
const body = transformGrokResponsesBody(input.upstreamRequest.body);
return {
ok: true,
value: {
...input.upstreamRequest,
body: body.changed ? body.value : input.upstreamRequest.body,
headers
}
};
}
function transformGrokResponsesBody(value: unknown): { value: unknown; changed: boolean } {
if (!isRecord(value)) {
return { value, changed: false };
}
const patched = transformCodexApplyPatchBridgeRequestBody(value);
const sanitizedOptions = sanitizeGrokUnsupportedResponsesOptions(patched.body);
const sanitized = sanitizeGrokResponsesTools(sanitizedOptions.value);
return {
value: sanitized.value,
changed: patched.changed || sanitizedOptions.changed || sanitized.changed
};
}
function sanitizeGrokUnsupportedResponsesOptions(body: Record<string, unknown>): {
value: Record<string, unknown>;
changed: boolean;
} {
let next: Record<string, unknown> | undefined;
for (const key of grokUnsupportedResponsesOptions) {
if (Object.hasOwn(body, key)) {
next ??= { ...body };
delete next[key];
}
}
return next ? { value: next, changed: true } : { value: body, changed: false };
}
function sanitizeGrokResponsesTools(body: Record<string, unknown>): { value: Record<string, unknown>; changed: boolean } {
if (!Array.isArray(body.tools)) {
return { value: body, changed: false };
}
let changed = false;
const removedToolNames = new Set<string>();
const tools = body.tools.filter((tool) => {
const supported = isGrokSupportedResponsesTool(tool);
if (!supported) {
changed = true;
const name = responseToolName(tool);
if (name) {
removedToolNames.add(name);
}
}
return supported;
});
if (!changed) {
return { value: body, changed: false };
}
const next = { ...body };
if (tools.length > 0) {
next.tools = tools;
} else {
delete next.tools;
delete next.parallel_tool_calls;
}
if (tools.length === 0 || grokToolChoiceNamesRemovedTool(next.tool_choice, removedToolNames)) {
delete next.tool_choice;
}
return { value: next, changed: true };
}
function isGrokSupportedResponsesTool(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
return grokSupportedResponsesToolTypes.has(stringValue(value.type) ?? "");
}
function responseToolName(value: unknown): string | undefined {
if (!isRecord(value)) {
return undefined;
}
return stringValue(value.name) || stringValue(value.server_label);
}
function grokToolChoiceNamesRemovedTool(value: unknown, removedToolNames: Set<string>): boolean {
if (removedToolNames.size === 0) {
return false;
}
if (!isRecord(value)) {
return false;
}
const name = stringValue(value.name) || stringValue(value.server_label);
return name ? removedToolNames.has(name) : false;
}
function withBearerAuth(
headers: HeaderRecord | undefined,
token: string,
removeHeaders: string[]
): HeaderRecord {
const next = { ...(headers ?? {}) };
for (const name of removeHeaders) {
deleteHeader(next, name);
}
setHeader(next, "authorization", `Bearer ${token}`);
return next;
}
function originalRemoveHeaders(plugin: Record<string, unknown>): string[] {
const auth = isRecord(plugin.auth) ? plugin.auth : undefined;
const removeHeaders = Array.isArray(auth?.removeHeaders) ? auth.removeHeaders : [];
return uniqueStrings([
...removeHeaders
.map((value) => stringValue(value))
.filter((value): value is string => Boolean(value)),
"x-api-key"
]);
}
function originalBearerToken(plugin: Record<string, unknown>): string | undefined {
const authorization = originalAuthHeader(plugin, "authorization");
const prefix = "Bearer ";
return authorization?.startsWith(prefix) ? authorization.slice(prefix.length).trim() || undefined : undefined;
}
function originalAuthHeader(plugin: Record<string, unknown>, name: string): string | undefined {
const auth = isRecord(plugin.auth) ? plugin.auth : undefined;
const headers = isRecord(auth?.headers) ? auth.headers : undefined;
return recordHeader(headers, name);
}
function originalRequestHeader(plugin: Record<string, unknown>, name: string): string | undefined {
const request = isRecord(plugin.request) ? plugin.request : undefined;
const headers = isRecord(request?.headers) ? request.headers : undefined;
return recordHeader(headers, name);
}
function originalAnthropicBetaDefault(plugin: Record<string, unknown>): string | undefined {
const value = originalAuthHeaderValue(plugin, claudeCodeOauthBetaHeader);
if (typeof value === "string") {
return value;
}
if (isRecord(value)) {
return stringValue(value.default);
}
return undefined;
}
function originalAuthHeaderValue(plugin: Record<string, unknown>, name: string): unknown {
const auth = isRecord(plugin.auth) ? plugin.auth : undefined;
const headers = isRecord(auth?.headers) ? auth.headers : undefined;
if (!headers) {
return undefined;
}
const normalizedName = name.toLowerCase();
return Object.entries(headers).find(([key]) => key.trim().toLowerCase() === normalizedName)?.[1];
}
function recordHeader(headers: Record<string, unknown> | undefined, name: string): string | undefined {
const value = recordHeaderValue(headers, name);
return typeof value === "string" ? value : undefined;
}
function recordHeaderValue(headers: Record<string, unknown> | undefined, name: string): unknown {
if (!headers) {
return undefined;
}
const normalizedName = name.toLowerCase();
return Object.entries(headers).find(([key]) => key.trim().toLowerCase() === normalizedName)?.[1];
}
function requestHeader(
headers: Record<string, string | string[] | undefined> | undefined,
name: string
): string | undefined {
if (!headers) {
return undefined;
}
const normalizedName = name.toLowerCase();
const value = Object.entries(headers).find(([key]) => key.trim().toLowerCase() === normalizedName)?.[1];
if (Array.isArray(value)) {
return value.join(",");
}
return value;
}
function setHeader(headers: HeaderRecord, name: string, value: string): void {
deleteHeader(headers, name);
headers[name] = value;
}
function deleteHeader(headers: HeaderRecord, name: string): void {
const normalizedName = name.toLowerCase();
for (const key of Object.keys(headers)) {
if (key.trim().toLowerCase() === normalizedName) {
delete headers[key];
}
}
}
function kimiOauthReference(plugin: Record<string, unknown>): { key?: string; oauthHost?: string } | undefined {
const oauth = isRecord(plugin.kimiOauth) ? plugin.kimiOauth : undefined;
if (!oauth) {
return undefined;
}
const key = stringValue(oauth.key);
const oauthHost = stringValue(oauth.oauthHost) || stringValue(oauth.oauth_host);
return key || oauthHost ? { key, oauthHost } : undefined;
}
const grokSupportedResponsesToolTypes = new Set([
"function",
"x_search",
"image_generation",
"collections_search",
"file_search",
"code_execution",
"code_interpreter",
"mcp",
"shell"
]);
const grokUnsupportedResponsesOptions = new Set([
"external_web_access"
]);
function uniqueStrings(values: string[]): string[] {
const seen = new Set<string>();
const unique: string[] = [];
for (const value of values) {
const key = value.trim().toLowerCase();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
unique.push(value);
}
return unique;
}
@@ -5,7 +5,7 @@ import { spawn, spawnSync, 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 { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { delimiter as pathDelimiter, join as pathJoin, resolve as pathResolve } from "node:path";
import { CONFIGDIR } from "@ccr/core/config/constants";
import {
@@ -46,11 +46,11 @@ export function spawnGatewayProcess(
coreAuthToken: string
): SpawnedGatewayProcess {
const gatewayEntry = resolveGatewayEntry();
const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile() : undefined;
const fetchPreloadFile = writeGatewayFetchPreloadFile();
const nodeRuntime = resolveGatewayNodeRuntime();
const env = createGatewayProcessEnv(config, upstreamProxyUrl, runtimeId, coreAuthToken, nodeRuntime.electronRunAsNode);
const gatewayBootstrapEntry = resolveGatewayBootstrapEntry();
const args = proxyPreloadFile ? ["--require", proxyPreloadFile, gatewayBootstrapEntry] : [gatewayBootstrapEntry];
const args = ["--require", fetchPreloadFile, gatewayBootstrapEntry];
const child = spawn(nodeRuntime.command, args, {
cwd: CONFIGDIR,
env,
@@ -184,6 +184,20 @@ export function resolveUpstreamHeaderSanitizerEntry(): string {
].find((candidate) => existsSync(candidate)) ?? pathJoin(__dirname, "upstream-header-sanitizer.js");
}
export function resolveLocalAgentAuthProviderHookEntry(): string {
const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
return [
pathJoin(__dirname, "local-agent-auth-provider-hook.js"),
pathJoin(process.cwd(), ".test-dist", "core", "runtime", "local-agent-auth-provider-hook.js"),
...(resourcesPath
? [
pathJoin(resourcesPath, "app.asar", "dist", "main", "local-agent-auth-provider-hook.js"),
pathJoin(resourcesPath, "app", "dist", "main", "local-agent-auth-provider-hook.js")
]
: [])
].find((candidate) => existsSync(candidate)) ?? pathJoin(__dirname, "local-agent-auth-provider-hook.js");
}
function resolveGatewayEntry(): string {
const override = process.env[gatewayEntryOverrideEnv]?.trim();
if (override) {
@@ -264,6 +278,8 @@ function createGatewayProcessEnv(
AUTH_STATIC_API_KEY_ENV: coreGatewayAuthTokenEnv,
AUTH_STATIC_API_KEY_HEADER: coreGatewayAuthHeader,
CCR_GATEWAY_RUNTIME_ID: runtimeId,
CCR_UNDICI_MODULE: resolveUndiciProxyAgentModule(),
CCR_UPSTREAM_TIMEOUT_MS: String(gatewayUpstreamTimeoutMs(config)),
[coreGatewayAuthTokenEnv]: coreAuthToken,
HOST: config.gateway.coreHost,
PORT: String(config.gateway.corePort)
@@ -302,10 +318,14 @@ function createGatewayProcessEnv(
env.https_proxy = upstreamProxyUrl;
env.all_proxy = upstreamProxyUrl;
env.CCR_UPSTREAM_PROXY_URL = upstreamProxyUrl;
env.CCR_UNDICI_MODULE = resolveUndiciProxyAgentModule();
return env;
}
function gatewayUpstreamTimeoutMs(config: AppConfig): number {
const value = Number(config.API_TIMEOUT_MS);
return Number.isFinite(value) && value >= 0 ? Math.trunc(value) : 0;
}
function resolveGatewayNodeRuntime(): GatewayNodeRuntime {
const candidates = uniqueGatewayNodeRuntimeCandidates([
...configuredGatewayNodeRuntimeCandidates(),
@@ -419,58 +439,77 @@ function appendGatewayChildOutput(child: ChildProcess, message: string): string
return details ? `${message}\n${details}` : message;
}
export function writeGatewayProxyPreloadFile(): string {
export function writeGatewayFetchPreloadFile(): string {
const file = pathJoin(CONFIGDIR, "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"
);
mkdirSync(CONFIGDIR, { recursive: true });
writeFileSync(file, gatewayFetchPreloadScript(), "utf8");
return file;
}
export function writeGatewayProxyPreloadFile(): string {
return writeGatewayFetchPreloadFile();
}
function gatewayFetchPreloadScript(): string {
return [
"\"use strict\";",
"const up = process.env.CCR_UPSTREAM_PROXY_URL;",
"const um = process.env.CCR_UNDICI_MODULE;",
"const rawTimeout = process.env.CCR_UPSTREAM_TIMEOUT_MS;",
"const parsedTimeout = rawTimeout === undefined || rawTimeout === '' ? NaN : Number(rawTimeout);",
"const hasTimeout = Number.isFinite(parsedTimeout) && parsedTimeout >= 0;",
"if ((up || hasTimeout) && um) {",
" const { Agent, ProxyAgent } = require(um);",
" const timeoutOptions = hasTimeout ? { headersTimeout: Math.trunc(parsedTimeout), bodyTimeout: Math.trunc(parsedTimeout) } : {};",
" const directAgent = hasTimeout ? new Agent(timeoutOptions) : undefined;",
" const proxyAgent = up ? new ProxyAgent(Object.assign({ uri: up }, timeoutOptions)) : undefined;",
" 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 requestUrl = (input) => {",
" try {",
" return typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input && input.url ? input.url : String(input));",
" } catch { return undefined; }",
" };",
" const shouldBypass = (input) => {",
" const u = requestUrl(input);",
" if (!u) return true;",
" const h = norm(u.hostname);",
" 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]) return h === ph && u.port === s[1];",
" if (ph.startsWith('*.')) return h.endsWith(ph.slice(1));",
" if (ph.startsWith('.')) return h.endsWith(ph) || h === ph.slice(1);",
" return h === ph;",
" });",
" };",
" const dispatcherFor = (input) => {",
" if (!proxyAgent) return directAgent;",
" return shouldBypass(input) ? directAgent : proxyAgent;",
" };",
" const patched = function(input, init) {",
" if (init && init.dispatcher) return realFetch(input, init);",
" const dispatcher = dispatcherFor(input);",
" if (!dispatcher) return realFetch(input, init);",
" return realFetch(input, Object.assign({}, init, { dispatcher }));",
" };",
" if (Object.getOwnPropertyDescriptor(globalThis, 'fetch')?.writable) {",
" globalThis.fetch = patched;",
" }",
"}"
].join("\n");
}
export function gatewayFetchPreloadScriptForTest(): string {
return gatewayFetchPreloadScript();
}
function mergeNoProxy(current: string | undefined, values: string[]): string {
const merged = new Set<string>();
for (const value of [...(current || "").split(","), ...values]) {
@@ -0,0 +1,389 @@
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 { parseJsonObjectSafe, serializeJsonBody } from "@ccr/core/gateway/http/body";
import { requestProtocolForPath } from "@ccr/core/routing/protocol-endpoints";
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
const multiAgentNamespaceName = "multi_agent_v1";
const multiAgentFunctionPrefix = `${multiAgentNamespaceName}_`;
const multiAgentToolNames = new Set(["close_agent", "resume_agent", "send_input", "spawn_agent", "wait_agent"]);
export function prepareCodexMultiAgentBridgeRequest(input: {
body?: Buffer;
config: AppConfig;
headers: IncomingHttpHeaders;
method: string;
path: string;
routedModel?: string;
}): { body: Buffer; diagnostic: string } | undefined {
if (!codexMultiAgentBridgeEnabled(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 (!codexMultiAgentBridgeModelEligible(model, input.config)) {
return undefined;
}
const transformed = transformCodexMultiAgentBridgeRequestBody(parsedBody);
if (!transformed.changed) {
return undefined;
}
return {
body: serializeJsonBody(transformed.body),
diagnostic: `${model ?? "unknown"}:${transformed.changedParts.join(",")}`
};
}
export function transformCodexMultiAgentBridgeRequestBody(body: Record<string, unknown>): {
body: Record<string, unknown>;
changed: boolean;
changedParts: string[];
} {
const next = { ...body };
const changedParts: string[] = [];
const tools = transformCodexMultiAgentBridgeTools(body.tools);
if (tools.changed) {
next.tools = tools.value;
changedParts.push("tools");
const toolChoice = transformCodexMultiAgentBridgeToolChoice(body.tool_choice);
if (toolChoice.changed) {
if (toolChoice.value === undefined) {
delete next.tool_choice;
} else {
next.tool_choice = toolChoice.value;
}
changedParts.push("tool_choice");
}
const input = transformCodexMultiAgentBridgeInput(body.input);
if (input.changed) {
next.input = input.value;
changedParts.push("input");
}
}
return {
body: next,
changed: changedParts.length > 0,
changedParts
};
}
function transformCodexMultiAgentBridgeTools(value: unknown): { value: unknown; changed: boolean } {
if (!Array.isArray(value)) {
return { value, changed: false };
}
let changed = false;
const tools = value.flatMap((tool) => {
if (!isRecord(tool) || tool.type !== "namespace" || tool.name !== multiAgentNamespaceName) {
return [tool];
}
const namespaceTools = Array.isArray(tool.tools) ? tool.tools : [];
const flattened = namespaceTools
.filter((item) => isRecord(item) && item.type === "function" && multiAgentToolNames.has(stringValue(item.name) ?? ""))
.map((item) => codexMultiAgentFunctionTool(item as Record<string, unknown>));
if (flattened.length === 0) {
return [tool];
}
changed = true;
return flattened;
});
return { value: tools, changed };
}
function codexMultiAgentFunctionTool(tool: Record<string, unknown>): Record<string, unknown> {
const name = stringValue(tool.name) ?? "";
const description = rawStringValue(tool.description) ?? "";
return {
...tool,
name: codexMultiAgentFunctionName(name),
description: description
? `Namespaced ${multiAgentNamespaceName}.${name} tool.\n\n${description}`
: `Namespaced ${multiAgentNamespaceName}.${name} tool.`
};
}
function transformCodexMultiAgentBridgeToolChoice(value: unknown): { value: unknown; changed: boolean } {
const name = toolChoiceName(value);
if (!name) {
return { value, changed: false };
}
if (normalizeMultiAgentToolName(name) === undefined && name !== multiAgentNamespaceName) {
return { value, changed: false };
}
if (name === multiAgentNamespaceName) {
return { value: undefined, changed: true };
}
const mappedName = normalizeMultiAgentToolName(name);
if (!mappedName || mappedName === name) {
return { value, changed: false };
}
if (isRecord(value) && isRecord(value.function)) {
return {
value: {
...value,
function: {
...value.function,
name: mappedName
}
},
changed: true
};
}
if (isRecord(value)) {
return {
value: {
...value,
name: mappedName,
type: value.type === "tool" ? "function" : value.type
},
changed: true
};
}
return { value, changed: false };
}
function toolChoiceName(value: unknown): string | undefined {
if (!isRecord(value)) {
return undefined;
}
return stringValue(value.name) ?? (isRecord(value.function) ? stringValue(value.function.name) : undefined);
}
function transformCodexMultiAgentBridgeInput(value: unknown): { value: unknown; changed: boolean } {
if (!Array.isArray(value)) {
return { value, changed: false };
}
let changed = false;
const items = value.map((item) => {
const transformed = transformCodexMultiAgentBridgeRequestItem(item);
changed ||= transformed.changed;
return transformed.value;
});
return { value: items, changed };
}
function transformCodexMultiAgentBridgeRequestItem(value: unknown): { value: unknown; changed: boolean } {
if (!isRecord(value) || value.type !== "function_call") {
return { value, changed: false };
}
const mappedName = normalizeMultiAgentToolName(stringValue(value.name) ?? "");
if (!mappedName || mappedName === value.name) {
return { value, changed: false };
}
const { namespace: _namespace, ...rest } = value;
return {
value: {
...rest,
name: mappedName
},
changed: true
};
}
function codexMultiAgentBridgeEnabled(headers: IncomingHttpHeaders, method: string, path: string): boolean {
return (method || "GET").toUpperCase() === "POST" &&
requestProtocolForPath(path) === "openai_responses" &&
isCodexUserAgent(headers);
}
function isCodexUserAgent(headers: IncomingHttpHeaders): boolean {
return readHeader(headers["user-agent"])?.toLowerCase().includes("codex") ?? false;
}
function codexMultiAgentBridgeModelEligible(model: string | undefined, config: AppConfig): boolean {
const modelName = modelNameForMultiAgentBridge(model);
if (!modelName || modelName.toLowerCase().includes("gpt")) {
return false;
}
const baseModelName = modelNameForMultiAgentBridge(resolveUsageModelAttribution(config, model).model);
return !baseModelName.toLowerCase().includes("gpt");
}
function modelNameForMultiAgentBridge(model: string | undefined): string {
const normalized = normalizeRouteSelector(model) ?? "";
const slashIndex = normalized.lastIndexOf("/");
return slashIndex >= 0 ? normalized.slice(slashIndex + 1) : normalized;
}
function codexMultiAgentFunctionName(name: string): string {
return `${multiAgentFunctionPrefix}${name}`;
}
function normalizeMultiAgentToolName(name: string): string | undefined {
const normalized = name.trim();
if (!normalized) {
return undefined;
}
if (normalized.startsWith(multiAgentFunctionPrefix)) {
const inner = normalized.slice(multiAgentFunctionPrefix.length);
return multiAgentToolNames.has(inner) ? codexMultiAgentFunctionName(inner) : undefined;
}
const dottedPrefix = `${multiAgentNamespaceName}.`;
if (normalized.startsWith(dottedPrefix)) {
const inner = normalized.slice(dottedPrefix.length);
return multiAgentToolNames.has(inner) ? codexMultiAgentFunctionName(inner) : undefined;
}
return multiAgentToolNames.has(normalized) ? codexMultiAgentFunctionName(normalized) : undefined;
}
function nativeMultiAgentToolName(name: string): string | undefined {
const normalized = name.trim();
if (!normalized.startsWith(multiAgentFunctionPrefix)) {
return undefined;
}
const inner = normalized.slice(multiAgentFunctionPrefix.length);
return multiAgentToolNames.has(inner) ? inner : undefined;
}
export function codexMultiAgentBridgeResponseStream(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 = transformCodexMultiAgentBridgeResponseValue(parsed);
this.push(Buffer.from(`${JSON.stringify(transformed.value)}\n`, "utf8"));
} catch {
this.push(Buffer.from(raw, "utf8"));
}
callback();
}
}));
}
return input;
}
export function transformCodexMultiAgentBridgeResponseValue(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 = transformMultiAgentFunctionCall(value.item);
if (item.changed) {
next.item = item.value;
changed = true;
}
}
if (Array.isArray(value.output)) {
const output = transformCodexMultiAgentBridgeResponseItems(value.output);
if (output.changed) {
next.output = output.value;
changed = true;
}
}
if (isRecord(value.response) && Array.isArray(value.response.output)) {
const output = transformCodexMultiAgentBridgeResponseItems(value.response.output);
if (output.changed) {
next.response = {
...value.response,
output: output.value
};
changed = true;
}
}
const item = transformMultiAgentFunctionCall(next);
if (item.changed) {
return item;
}
return { value: next, changed };
}
function transformCodexMultiAgentBridgeResponseItems(items: unknown[]): { value: unknown[]; changed: boolean } {
let changed = false;
const value = items.map((item) => {
const transformed = isRecord(item)
? transformMultiAgentFunctionCall(item)
: { value: item, changed: false };
changed ||= transformed.changed;
return transformed.value;
});
return { value, changed };
}
function transformMultiAgentFunctionCall(item: Record<string, unknown>): { value: unknown; changed: boolean } {
if (item.type !== "function_call") {
return { value: item, changed: false };
}
const nativeName = nativeMultiAgentToolName(stringValue(item.name) ?? "");
if (!nativeName) {
return { value: item, changed: false };
}
return {
value: {
...item,
name: nativeName,
namespace: multiAgentNamespaceName
},
changed: true
};
}
function transformSseChunk(stream: Transform, chunk: Buffer | string): void {
const state = stream as Transform & { __ccrCodexMultiAgentBridgeSsePending?: string };
state.__ccrCodexMultiAgentBridgeSsePending = (state.__ccrCodexMultiAgentBridgeSsePending ?? "") + chunk.toString();
while (state.__ccrCodexMultiAgentBridgeSsePending) {
const match = /\r?\n\r?\n/.exec(state.__ccrCodexMultiAgentBridgeSsePending);
if (!match || match.index === undefined) {
break;
}
const block = state.__ccrCodexMultiAgentBridgeSsePending.slice(0, match.index);
const delimiter = match[0];
state.__ccrCodexMultiAgentBridgeSsePending = state.__ccrCodexMultiAgentBridgeSsePending.slice(match.index + delimiter.length);
stream.push(transformCodexMultiAgentBridgeSseEvent(block) + delimiter);
}
}
function flushSseTransform(stream: Transform): void {
const state = stream as Transform & { __ccrCodexMultiAgentBridgeSsePending?: string };
if (state.__ccrCodexMultiAgentBridgeSsePending) {
stream.push(transformCodexMultiAgentBridgeSseEvent(state.__ccrCodexMultiAgentBridgeSsePending));
state.__ccrCodexMultiAgentBridgeSsePending = "";
}
}
export function transformCodexMultiAgentBridgeSseEvent(block: string): string {
const lines = block.split(/\r?\n/);
const dataIndex = lines.findIndex((line) => line.startsWith("data: "));
if (dataIndex < 0) {
return block;
}
const raw = lines[dataIndex].slice("data: ".length);
try {
const parsed = JSON.parse(raw);
const transformed = transformCodexMultiAgentBridgeResponseValue(parsed);
if (!transformed.changed) {
return block;
}
lines[dataIndex] = `data: ${JSON.stringify(transformed.value)}`;
return lines.join("\n");
} catch {
return block;
}
}
@@ -2,6 +2,7 @@ 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, serializeJsonBody } from "@ccr/core/gateway/http/body";
import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery";
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";
@@ -24,7 +25,10 @@ export function createHostedWebSearchProtocolContext(input: {
if (!body || !hasHostedWebSearchDeclaration(body, protocol)) {
return undefined;
}
const toolName = fusionWebSearchToolNameForRequest(input.config, stringValue(body.model) || input.routedModel);
const toolName = fusionWebSearchToolNameForRequest(
input.config,
hostedWebSearchModelForRequest(input.config, stringValue(body.model) || input.routedModel)
);
if (!toolName) {
return undefined;
}
@@ -53,7 +57,10 @@ export function createClaudeCodeWebSearchContinuationContext(input: {
if (!body || claudeCodeWebSearchToolResultTexts(body).length === 0) {
return undefined;
}
const toolName = fusionWebSearchToolNameForRequest(input.config, stringValue(body.model) || input.routedModel);
const toolName = fusionWebSearchToolNameForRequest(
input.config,
hostedWebSearchModelForRequest(input.config, stringValue(body.model) || input.routedModel)
);
if (!toolName) {
return undefined;
}
@@ -64,6 +71,11 @@ export function createClaudeCodeWebSearchContinuationContext(input: {
};
}
function hostedWebSearchModelForRequest(config: AppConfig, model: string | undefined): string | undefined {
return resolveGatewayPublicModelId(model, config) ?? model;
}
export function prepareHostedWebSearchProtocolRequestBody(
body: Buffer | undefined,
records: BrowserWebSearchProtocolRecord[],
+32 -36
View File
@@ -30,6 +30,7 @@ import { adaptRouteRequestBody, restoreRouteRequestBody } from "@ccr/core/routin
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 { codexMultiAgentBridgeResponseStream, prepareCodexMultiAgentBridgeRequest } from "@ccr/core/gateway/features/codex-multi-agent-bridge";
import { prepareCursorOpenAICompatChatBody } from "@ccr/core/gateway/features/cursor-compat";
import { filteredResponseHeaders, formatError, formatUpstreamErrorForLog, forwardHeaders, inferGatewayClient, readRequestBody, sendJson, shouldCaptureGatewayUsage, shouldSendBody, stripLocalGatewayAuthHeaders } from "@ccr/core/gateway/http/io";
import { serializeJsonBody, takeJsonObject } from "@ccr/core/gateway/http/body";
@@ -41,7 +42,7 @@ import { coreGatewayUsageAttributionConfig } from "@ccr/core/gateway/core-runtim
import { providerModelPricingForUsage } from "@ccr/core/models/pricing-service";
import { clientClosedRequestStatusCode, clientDisconnectMessage, resolveStreamRequestLogOutcome, 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 { cancelResponseBody, destroyResponseStreams, fetchUpstreamWithFallback, mergeFallbackResponseHeaders, rewriteCapabilityResponseHeaders, uniqueStreams, upstreamResponseHeaders } from "@ccr/core/gateway/upstream/executor";
import { requestProtocolForPath, shouldApplyGatewayRouting } from "@ccr/core/routing/protocol-endpoints";
import { createClaudeCodeWebSearchContinuationContext, createHostedWebSearchProtocolContext, hostedWebSearchProtocolResponseStream, hostedWebSearchUnavailableMessage, prepareClaudeCodeWebSearchContinuationRequestBody, prepareHostedWebSearchProtocolRequestBody, selectClaudeCodeWebSearchContinuationRecords, selectHostedWebSearchProtocolRecords } from "@ccr/core/gateway/features/hosted-web-search/index";
@@ -155,6 +156,7 @@ export class GatewayRequestPipeline {
let routeFallback = this.config.Router.fallback;
let routedModel: string | undefined;
let codexApplyPatchBridgeActive = false;
let codexMultiAgentBridgeActive = false;
const claudeModelRewriteStartedAt = Date.now();
const claudeModelRewrite = prepareClaudeCodeDiscoveredModelRequest(this.config, request.headers, method, path, bodyToForward);
if (claudeModelRewrite) {
@@ -380,42 +382,33 @@ export class GatewayRequestPipeline {
});
}
const capabilityRoutingStartedAt = Date.now();
const capabilityBodyBefore = bodyToForward;
const capabilityFallbackBefore = routeFallback;
const capabilityModelBefore = routedModel;
const capabilityProviderHeadersBefore = {
gateway: headers["x-gateway-target-provider"],
list: headers["x-target-providers"],
target: headers["x-target-provider"]
};
const providerCapabilityRouting = applyProviderCapabilityRouting({
const codexMultiAgentBridgeStartedAt = Date.now();
const codexMultiAgentBridgeRequest = prepareCodexMultiAgentBridgeRequest({
body: bodyToForward,
config: this.config,
fallback: routeFallback,
headers,
headers: request.headers,
method,
path,
routedModel
});
bodyToForward = providerCapabilityRouting.body;
routeFallback = providerCapabilityRouting.fallback;
routedModel = providerCapabilityRouting.routedModel;
routeTrace?.capture({
changes: [
...(capabilityBodyBefore === bodyToForward ? [] : [{ operation: "replace" as const, path: "/body/model", scope: "body" as const }]),
reportedRouteChange("routing", "/routing/model", capabilityModelBefore, routedModel),
...(capabilityFallbackBefore === routeFallback ? [] : [{ after: routeFallback, before: capabilityFallbackBefore, operation: "replace" as const, path: "/routing/fallback", scope: "routing" as const }]),
reportedRouteChange("headers", "/headers/x-target-provider", capabilityProviderHeadersBefore.target, headers["x-target-provider"]),
reportedRouteChange("headers", "/headers/x-target-providers", capabilityProviderHeadersBefore.list, headers["x-target-providers"]),
reportedRouteChange("headers", "/headers/x-gateway-target-provider", capabilityProviderHeadersBefore.gateway, headers["x-gateway-target-provider"])
].filter(isReportedRouteChange),
durationMs: Date.now() - capabilityRoutingStartedAt,
kind: "mutation",
name: "provider.capability-routing",
phase: "capability",
startedAtMs: capabilityRoutingStartedAt,
target: routedModel ? { model: routedModel } : undefined
});
if (codexMultiAgentBridgeRequest) {
bodyToForward = codexMultiAgentBridgeRequest.body;
codexMultiAgentBridgeActive = true;
headers["x-ccr-codex-multi-agent-bridge"] = sanitizeHeaderValue(codexMultiAgentBridgeRequest.diagnostic);
headers["content-type"] = "application/json";
routeTrace?.capture({
changes: [
{ operation: "replace", path: "/body", scope: "body" },
{ after: headers["x-ccr-codex-multi-agent-bridge"], operation: "add", path: "/headers/x-ccr-codex-multi-agent-bridge", scope: "headers" },
{ after: headers["content-type"], operation: "replace", path: "/headers/content-type", scope: "headers" }
],
durationMs: Date.now() - codexMultiAgentBridgeStartedAt,
kind: "mutation",
name: "compatibility.codex-multi-agent",
phase: "compatibility",
startedAtMs: codexMultiAgentBridgeStartedAt
});
}
const hostedWebSearchProtocolContext = createHostedWebSearchProtocolContext({
body: bodyToForward,
@@ -719,7 +712,7 @@ export class GatewayRequestPipeline {
const appendContextArchiveFooter = Boolean(contextArchiveRecord && upstreamResponse.ok);
const transformCodexCompactResponse = Boolean(!contextArchiveRecord && codexCompactCompatResponseMode && upstreamResponse.ok);
const contextArchiveSourceContentType = responseHeaders.get("content-type") ?? undefined;
if (codexApplyPatchBridgeActive || appendContextArchiveFooter || transformCodexCompactResponse) {
if (codexApplyPatchBridgeActive || codexMultiAgentBridgeActive || appendContextArchiveFooter || transformCodexCompactResponse) {
responseHeaders.delete("content-length");
}
if ((appendContextArchiveFooter || transformCodexCompactResponse) && contextArchiveResponseContentType) {
@@ -771,14 +764,17 @@ export class GatewayRequestPipeline {
const patchedResponseBody = codexApplyPatchBridgeActive
? codexApplyPatchBridgeResponseStream(upstreamBody, responseHeaders)
: upstreamBody;
const multiAgentResponseBody = codexMultiAgentBridgeActive
? codexMultiAgentBridgeResponseStream(patchedResponseBody, responseHeaders)
: patchedResponseBody;
const hostedWebSearchResponseBody = hostedWebSearchProtocolContext
? hostedWebSearchProtocolResponseStream(
patchedResponseBody,
multiAgentResponseBody,
responseHeaders,
hostedWebSearchProtocolContext,
this.browserWebSearchMcpIntegration
)
: patchedResponseBody;
: multiAgentResponseBody;
const archiveResponseProtocol = requestProtocolForPath(upstreamPath) ?? requestProtocol ?? "anthropic_messages";
const responseBody = appendContextArchiveFooter && contextArchiveRecord
? contextArchiveHandoffResponseStream(
@@ -796,7 +792,7 @@ export class GatewayRequestPipeline {
codexCompactCompatResponseMode
)
: hostedWebSearchResponseBody;
const responseStreams = uniqueStreams([upstreamBody, patchedResponseBody, hostedWebSearchResponseBody, responseBody]);
const responseStreams = uniqueStreams([upstreamBody, patchedResponseBody, multiAgentResponseBody, hostedWebSearchResponseBody, responseBody]);
const sampler = createBodySampler();
const sseErrorDetector = createSseErrorDetector(responseHeaders.get("content-type") ?? undefined);
let streamDetectedError: string | undefined;
+1
View File
@@ -6,6 +6,7 @@
*/
export { gatewayService } from "@ccr/core/gateway/application/gateway-service";
export { prepareCodexApplyPatchBridgeRequest, transformCodexApplyPatchBridgeRequestBody, transformCodexApplyPatchBridgeResponseValue, transformCodexApplyPatchBridgeSseEvent } from "@ccr/core/gateway/features/codex-patch-bridge";
export { prepareCodexMultiAgentBridgeRequest, transformCodexMultiAgentBridgeRequestBody, transformCodexMultiAgentBridgeResponseValue, transformCodexMultiAgentBridgeSseEvent } from "@ccr/core/gateway/features/codex-multi-agent-bridge";
export { appendContextArchiveToolOutputsForTest, contextArchiveFunctionCallsForTest, parseContextArchiveToolResponseBodyForTest, prepareCodexCompactCompatRequest, prepareContextArchiveToolContinuationRequestForTest } from "@ccr/core/gateway/features/context-archive-continuation";
export { normalizeClaudeCodeOauthProviderPlugins, normalizeCoreGatewayVirtualModelProfiles } from "@ccr/core/gateway/core-runtime/config-compiler";
export { fusionBuiltinToolArtifactsForTest, fusionFallbackToolDefinitions, fusionToolNamesBackedByMcpServers } from "@ccr/core/mcp/fusion-config";
+134 -11
View File
@@ -265,16 +265,50 @@ export async function fetchUpstreamWithFallback(input: {
upstreamUrl: string;
}): Promise<UpstreamFetchResult> {
const fallbackMode = input.fallback.mode;
const planningHeaders = { ...input.headers };
const planningRouting = applyProviderCapabilityRouting({
body: input.body,
config: input.config,
fallback: input.fallback,
headers: planningHeaders,
path: input.path,
routedModel: input.routedModel
});
const attempts = buildUpstreamAttempts(
input.config,
input.fallback,
planningRouting.fallback,
input.method,
input.path,
input.body,
input.routedModel
planningRouting.body,
planningRouting.routedModel
);
const failedAttempts: UpstreamFailedAttempt[] = [];
const attemptRoutingCache = new Map<string | undefined, {
body?: Buffer;
headers: Record<string, string>;
routedModel?: string;
sourceBody?: Buffer;
sourceRoutedModel?: string;
}>();
const primaryAttempt = attempts[0];
const parsedInputBody = parseJsonObjectSafe(input.body);
const planningBodyCanSeedPrimary = requestProtocolForPath(input.path) === "gemini_generate_content" ||
!parsedInputBody ||
!primaryAttempt?.model ||
stringValue(parsedInputBody.model) !== undefined;
if (primaryAttempt && planningBodyCanSeedPrimary) {
attemptRoutingCache.set(primaryAttempt.model, {
body: planningRouting.body,
headers: planningHeaders,
routedModel: primaryAttempt.model,
sourceBody: input.body,
sourceRoutedModel: input.routedModel
});
}
input.trace?.capture({
changes: [
routeTraceChange("routing", "/routing/fallback", input.fallback, planningRouting.fallback)
].filter(isRouteTraceChange),
decision: { reason: `fallback:${fallbackMode}`, source: "execution-plan" },
kind: "decision",
name: "fallback.execution-plan",
@@ -290,17 +324,69 @@ export async function fetchUpstreamWithFallback(input: {
}
const attemptNumber = index + 1;
const plannedAttempt = attempts[index];
const capabilityRoutingStartedAt = Date.now();
let cachedAttemptRouting = attemptRoutingCache.get(plannedAttempt.model);
if (!cachedAttemptRouting) {
const routedHeaders = { ...input.headers };
const sourceBody = buildAttemptBody(input.body, input.path, plannedAttempt.model);
const routing = applyProviderCapabilityRouting({
body: sourceBody,
config: input.config,
fallback: input.fallback,
headers: routedHeaders,
path: input.path,
routedModel: plannedAttempt.model
});
cachedAttemptRouting = {
body: routing.body,
headers: routedHeaders,
routedModel: routing.routedModel,
sourceBody,
sourceRoutedModel: plannedAttempt.model
};
attemptRoutingCache.set(plannedAttempt.model, cachedAttemptRouting);
}
const attemptHeaders = { ...cachedAttemptRouting.headers };
const attemptSourceBody = cachedAttemptRouting.sourceBody;
const capabilityProviderHeadersBefore = {
gateway: input.headers["x-gateway-target-provider"],
list: input.headers["x-target-providers"],
target: input.headers["x-target-provider"]
};
input.trace?.capture({
attempt: attemptNumber,
changes: [
...(attemptSourceBody === cachedAttemptRouting.body
? []
: [{ operation: "replace" as const, path: "/body/model", scope: "body" as const }]),
routeTraceChange("routing", "/routing/model", cachedAttemptRouting.sourceRoutedModel, cachedAttemptRouting.routedModel),
routeTraceChange("headers", "/headers/x-target-provider", capabilityProviderHeadersBefore.target, attemptHeaders["x-target-provider"]),
routeTraceChange("headers", "/headers/x-target-providers", capabilityProviderHeadersBefore.list, attemptHeaders["x-target-providers"]),
routeTraceChange("headers", "/headers/x-gateway-target-provider", capabilityProviderHeadersBefore.gateway, attemptHeaders["x-gateway-target-provider"])
].filter(isRouteTraceChange),
durationMs: Date.now() - capabilityRoutingStartedAt,
kind: "mutation",
name: "provider.capability-routing",
phase: "capability",
startedAtMs: capabilityRoutingStartedAt,
target: cachedAttemptRouting.routedModel ? { model: cachedAttemptRouting.routedModel } : undefined
});
const attemptPreparationStartedAt = Date.now();
const attempt = prepareUpstreamCredentialAttempt({
attempt: attempts[index],
attempt: {
...plannedAttempt,
body: cachedAttemptRouting.body,
model: cachedAttemptRouting.routedModel ?? plannedAttempt.model
},
config: input.config,
headers: input.headers,
headers: attemptHeaders,
method: input.method,
path: input.path
});
const hasNextAttempt = index < attempts.length - 1;
const attemptUrl = rewriteRouteModelInUrl(input.upstreamUrl, attempt.model);
const attemptHeaders = {
const upstreamHeaders = {
...withCoreGatewayAuthHeader(
omitLocalObservabilityHeaders(attempt.headers ?? input.headers),
input.coreAuthToken
@@ -346,13 +432,13 @@ export async function fetchUpstreamWithFallback(input: {
});
releaseJsonObject(attempt.body);
releaseJsonObject(attempts[index].body);
releaseJsonObject(attemptSourceBody);
releaseJsonObject(input.body);
try {
const response = await fetchWithSystemProxy(attemptUrl, {
body: shouldSendBody(input.method) ? attempt.body?.toString("utf8") : undefined,
headers: attemptHeaders,
headers: upstreamHeaders,
method: input.method,
signal: input.signal
});
@@ -904,9 +990,6 @@ function buildUpstreamAttempts(
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
@@ -914,6 +997,46 @@ function buildUpstreamAttempts(
}
function buildAttemptBody(
body: Buffer | undefined,
path: string,
model: string | undefined
): Buffer | undefined {
if (!body || !model || requestProtocolForPath(path) === "gemini_generate_content") {
return body;
}
const parsedBody = parseJsonObjectSafe(body);
if (!parsedBody || stringValue(parsedBody.model) === model) {
return body;
}
return serializeJsonBodyWithModel(parsedBody, model);
}
function routeTraceChange(
scope: RequestRouteTraceChange["scope"],
path: string,
before: unknown,
after: unknown
): RequestRouteTraceChange | undefined {
if (before === after) {
return undefined;
}
return {
...(before === undefined ? {} : { before }),
...(after === undefined ? {} : { after }),
operation: before === undefined ? "add" : after === undefined ? "remove" : "replace",
path,
scope
};
}
function isRouteTraceChange(value: RequestRouteTraceChange | undefined): value is RequestRouteTraceChange {
return Boolean(value);
}
async function drainResponseBody(response: Response): Promise<void> {
try {
await response.arrayBuffer();
+20 -5
View File
@@ -917,8 +917,8 @@ function buildCodexConfigToml(
managedContextArchiveMcpEnd
]);
content = removeCodexProviderTable(content, values.providerId);
content = removeCodexMcpServerTable(content, TOOL_HUB_MCP_SERVER_NAME);
content = removeCodexMcpServerTable(content, CONTEXT_ARCHIVE_MCP_SERVER_NAME);
content = removeCodexMcpServerTable(content, TOOL_HUB_MCP_SERVER_NAME, { includeChildTables: !values.toolHubMcp });
content = removeCodexMcpServerTable(content, CONTEXT_ARCHIVE_MCP_SERVER_NAME, { includeChildTables: !values.contextArchiveMcp });
if (values.configFormat === "separate_profile_files") {
content = removeCodexProfileTable(content, values.providerId);
}
@@ -1034,7 +1034,9 @@ function buildSeparateCodexProfileToml(
): string {
const firstTableIndex = firstTomlTableIndex(source);
const rootSource = firstTableIndex === -1 ? source : source.slice(0, firstTableIndex);
const restSource = firstTableIndex === -1 ? "" : source.slice(firstTableIndex);
let restSource = firstTableIndex === -1 ? "" : source.slice(firstTableIndex);
restSource = removeCodexMcpServerTable(restSource, TOOL_HUB_MCP_SERVER_NAME, { includeChildTables: true });
restSource = removeCodexMcpServerTable(restSource, CONTEXT_ARCHIVE_MCP_SERVER_NAME, { includeChildTables: true });
const modelAssignment = managedModelAssignment(rootSource, values.model);
const showAllSessionsAssignment = rootTomlAssignment(rootSource, "show_all_sessions")
?? (values.showAllSessions ? "show_all_sessions = true" : undefined);
@@ -2323,7 +2325,11 @@ function removeCodexProfileTable(source: string, providerId: string): string {
return removeTomlTable(source, "profiles", providerId);
}
function removeCodexMcpServerTable(source: string, serverName: string): string {
function removeCodexMcpServerTable(
source: string,
serverName: string,
options: { includeChildTables?: boolean } = {}
): string {
const lines = source.split(/(?<=\n)/);
const headers = new Set([
`[mcp_servers.${serverName}]`,
@@ -2338,7 +2344,8 @@ function removeCodexMcpServerTable(source: string, serverName: string): string {
const kept: string[] = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (!headers.has(line.trim())) {
const trimmed = line.trim();
if (!headers.has(trimmed) && !isCodexMcpServerChildTable(trimmed, serverName, Boolean(options.includeChildTables))) {
kept.push(line);
continue;
}
@@ -2352,6 +2359,14 @@ function removeCodexMcpServerTable(source: string, serverName: string): string {
return kept.join("");
}
function isCodexMcpServerChildTable(trimmedLine: string, serverName: string, enabled: boolean): boolean {
if (!enabled) {
return false;
}
return trimmedLine.startsWith(`[mcp_servers.${serverName}.`) ||
trimmedLine.startsWith(`[mcp_servers.${tomlQuotedKey(serverName)}.`);
}
function removeTomlTable(source: string, section: string, name: string): string {
const lines = source.split(/(?<=\n)/);
const headers = new Set([
@@ -1 +1 @@
export { ProxyAgent } from "undici";
export { Agent, ProxyAgent } from "undici";
@@ -26,6 +26,28 @@ test("generated Codex CLI middleware converts Windows SDK paths before URL schem
assert.equal(fn("@the-next-ai/bot-gateway-sdk"), "@the-next-ai/bot-gateway-sdk");
});
test("generated Codex CLI middleware materializes bundled Bot Gateway stdio runner", () => {
const dir = mkdtempSync(path.join(os.tmpdir(), "ccr-runtime-bot-runner-"));
const source = path.join(dir, "resources", "app.asar", "dist", "main", "bot-gateway-sdk", "bin", "bot-gateway-stdio.mjs");
const configDir = path.join(dir, "config");
mkdirSync(path.dirname(source), { recursive: true });
writeFileSync(source, "#!/usr/bin/env node\nconsole.log('ok');\n");
const resolveCommand = evaluateRuntimeFunction(
"resolveBundledBotGatewayCommand",
["normalizeDuplicateShebangs", "materializeBotGatewayStdioRunnerPath"],
configDir
);
const command = resolveCommand({ bundledStdioPath: () => source });
const expectedRunner = path.join(configDir, "bot-gateway", "runners", "bot-gateway-stdio.mjs");
assert.equal(command.command, process.execPath);
assert.deepEqual(command.args, [expectedRunner]);
assert.equal(command.cwd, path.dirname(expectedRunner));
assert.notEqual(command.cwd, path.dirname(source));
assert.equal(readFileSync(expectedRunner, "utf8"), "#!/usr/bin/env node\nconsole.log('ok');\n");
});
test("Codex app-server uses ChatGPT's bundled Node as a signed supervisor", { skip: process.platform !== "darwin" }, () => {
const dir = mkdtempSync(path.join(os.tmpdir(), "ccr-runtime-signed-supervisor-"));
const runtimeFile = writeRuntimeScript(dir);
@@ -1055,9 +1077,19 @@ function writeRuntimeScript(dir) {
return file;
}
function evaluateRuntimeFunction(name) {
const source = extractRuntimeFunctionSource(codexCliMiddlewareRuntimeScript(), name);
return Function("path", "pathToFileURL", `${source}; return ${name};`)(path, pathToFileURL);
function evaluateRuntimeFunction(name, dependencies = [], configDir = "") {
const runtime = codexCliMiddlewareRuntimeScript();
const source = [
...dependencies.map((dependency) => extractRuntimeFunctionSource(runtime, dependency)),
extractRuntimeFunctionSource(runtime, name)
].join("\n");
const fsRuntime = { existsSync, mkdirSync, readFileSync, writeFileSync };
return Function("path", "pathToFileURL", "fs", "CONFIG_DIR", `${source}; return ${name};`)(
path,
pathToFileURL,
fsRuntime,
configDir
);
}
function extractRuntimeFunctionSource(source, name) {
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { PassThrough, Readable } from "node:stream";
import test from "node:test";
import { createHostedWebSearchProtocolContext } from "@ccr/core/gateway/features/hosted-web-search/index.ts";
import {
fusionFallbackToolDefinitions,
fusionWebSearchToolNameForRequest,
@@ -504,6 +505,51 @@ test("gateway resolves normalized Fusion web search tool names for Anthropic pro
assert.equal(fusionWebSearchToolNameForRequest(config, "Fusion/kimisearch"), "fusion_2_web_search");
});
test("gateway resolves Claude discovery aliases before hosted web search matching", () => {
const config = {
Providers: [],
Router: { fallback: { mode: "off", models: [], retryCount: 0 } },
gateway: {},
virtualModelProfiles: [
{
displayName: "Kimisearch",
enabled: true,
execution: {
clientToolsPolicy: "allow",
matchWebSearch: true,
maxToolCalls: 8,
maxTurns: 6,
mode: "tool_loop",
streamMode: "optimistic"
},
id: "fusion-2",
key: "kimisearch",
match: { exactAliases: ["kimisearch", "Fusion/kimisearch"], prefixes: [], suffixes: [] },
materialization: { enabled: true, includeInGatewayModels: true },
metadata: {
fusionWebSearch: { provider: "browser", toolName: "web_search_fusion_2" }
},
tools: [{ name: "web_search_fusion_2", visibility: "internal" }]
}
]
};
const context = createHostedWebSearchProtocolContext({
body: Buffer.from(JSON.stringify({
messages: [{ content: "weather", role: "user" }],
model: "claude-Fusion/kimisearch",
tools: [{ name: "web_search", type: "web_search_20250305" }]
})),
config,
method: "POST",
path: "/v1/messages",
requestId: "request-1",
routedModel: "claude-Fusion/kimisearch",
sinceMs: 0
});
assert.equal(context?.toolName, "fusion_2_web_search");
});
test("gateway does not route hosted web search through an unrelated Fusion search profile", () => {
const config = {
Providers: [
@@ -756,7 +756,14 @@ test("profile service injects ToolHub MCP into Codex config", { skip: !process.e
separateProfileFile,
initialSeparateProfile
.replace('model = "Provider/model"', 'model = "User/selected-in-cli"')
.replace(/\s*$/, '\nmodel_reasoning_effort = "ultra"\n')
.replace(/\s*$/, [
"",
'model_reasoning_effort = "ultra"',
"",
'[mcp_servers.ccr-toolhub.tools."tool_hub.resolve"]',
'approval_mode = "approve"',
""
].join("\n"))
);
await applyProfileConfig(config);
@@ -773,6 +780,7 @@ test("profile service injects ToolHub MCP into Codex config", { skip: !process.e
const preservedSeparateProfile = readFileSync(separateProfileFile, "utf8");
assert.match(preservedSeparateProfile, /model = "User\/selected-in-cli"/);
assert.match(preservedSeparateProfile, /model_reasoning_effort = "ultra"/);
assert.equal(preservedSeparateProfile.includes("[mcp_servers.ccr-toolhub"), false);
config.Providers[0].models.push("model-2");
config.profile.profiles[0].model = "Provider/model-2";
@@ -1,6 +1,10 @@
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env.ts";
import { materializeBotGatewayStdioRunnerPath } from "@ccr/core/agents/bot-gateway/qr-login-service.ts";
import { botGatewaySdkImportSpecifier } from "@ccr/core/agents/bot-gateway/sdk-import.ts";
function botGateway(overrides = {}) {
@@ -141,3 +145,17 @@ test("botGatewaySdkImportSpecifier converts Windows absolute paths before URL sc
assert.equal(botGatewaySdkImportSpecifier("file:///tmp/sdk/index.js"), "file:///tmp/sdk/index.js");
assert.equal(botGatewaySdkImportSpecifier("@the-next-ai/bot-gateway-sdk"), "@the-next-ai/bot-gateway-sdk");
});
test("materializeBotGatewayStdioRunnerPath copies bundled runner out of app.asar paths", () => {
const dir = mkdtempSync(path.join(os.tmpdir(), "ccr-bot-runner-"));
const source = path.join(dir, "resources", "app.asar", "dist", "main", "bot-gateway-sdk", "bin", "bot-gateway-stdio.mjs");
const configDir = path.join(dir, "config");
mkdirSync(path.dirname(source), { recursive: true });
writeFileSync(source, "#!/usr/bin/env node\n#!/usr/bin/env node\nconsole.log('ok');\n");
const materialized = materializeBotGatewayStdioRunnerPath(source, configDir);
assert.equal(materialized, path.join(configDir, "bot-gateway", "runners", "bot-gateway-stdio.mjs"));
assert.notEqual(path.dirname(materialized), path.dirname(source));
assert.equal(readFileSync(materialized, "utf8"), "#!/usr/bin/env node\nconsole.log('ok');\n");
});
@@ -39,6 +39,23 @@ test("codex catalog treats unknown models as text-only while enabling apply_patc
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog publishes configured provider model descriptions", () => {
const model = catalogModelFor({
Providers: [
{
modelDescriptions: {
"MODEL-A": "Fast sidecar model for simple code search."
},
models: ["model-a"],
name: "Custom",
type: "openai_chat_completions"
}
]
}, "Custom/model-a");
assert.equal(model.description, "Fast sidecar model for simple code search.");
});
test("codex catalog uses model catalog capabilities for known text models", () => {
const model = catalogModelFor({
Providers: [
@@ -0,0 +1,153 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
prepareCodexMultiAgentBridgeRequest,
transformCodexMultiAgentBridgeResponseValue,
transformCodexMultiAgentBridgeSseEvent
} from "@ccr/core/gateway/service.ts";
const config = {
Providers: [],
Router: {
builtInRules: {
"claude-code": { enabled: true },
codex: { enabled: true }
},
fallback: { mode: "off", models: [], retryCount: 1 },
rules: []
}
};
function multiAgentNamespaceTool() {
return {
type: "namespace",
name: "multi_agent_v1",
description: "Tools for spawning and managing sub-agents.",
tools: [
{
type: "function",
name: "spawn_agent",
description: "Spawn a sub-agent.",
strict: false,
parameters: {
type: "object",
properties: {
message: { type: "string" },
model: { type: "string" }
},
additionalProperties: false
}
},
{
type: "function",
name: "wait_agent",
description: "Wait for agents.",
strict: false,
parameters: {
type: "object",
properties: {
targets: { type: "array", items: { type: "string" } }
},
required: ["targets"],
additionalProperties: false
}
}
]
};
}
test("Codex multi-agent bridge expands namespace tools for non-GPT models", () => {
const result = prepareCodexMultiAgentBridgeRequest({
body: Buffer.from(JSON.stringify({
input: [
{
arguments: JSON.stringify({ message: "inspect tests" }),
call_id: "call_agent",
name: "spawn_agent",
namespace: "multi_agent_v1",
type: "function_call"
}
],
model: "Provider/claude-sonnet",
parallel_tool_calls: true,
tool_choice: { type: "tool", name: "multi_agent_v1" },
tools: [
{ type: "function", name: "exec_command" },
multiAgentNamespaceTool()
]
})),
config,
headers: { "user-agent": "codex-test" },
method: "POST",
path: "/v1/responses"
});
assert.ok(result);
const body = JSON.parse(result.body.toString("utf8"));
assert.deepEqual(
body.tools.map((tool) => `${tool.type}:${tool.name}`),
["function:exec_command", "function:multi_agent_v1_spawn_agent", "function:multi_agent_v1_wait_agent"]
);
assert.match(body.tools[1].description, /multi_agent_v1\.spawn_agent/);
assert.equal(body.tool_choice, undefined);
assert.equal(body.parallel_tool_calls, true);
assert.equal(body.input[0].name, "multi_agent_v1_spawn_agent");
assert.equal(body.input[0].namespace, undefined);
});
test("Codex multi-agent bridge leaves GPT models untouched", () => {
const result = prepareCodexMultiAgentBridgeRequest({
body: Buffer.from(JSON.stringify({
model: "openai/gpt-5-codex",
tools: [multiAgentNamespaceTool()]
})),
config,
headers: { "user-agent": "codex-test" },
method: "POST",
path: "/v1/responses"
});
assert.equal(result, undefined);
});
test("Codex multi-agent bridge rewrites flattened function response items to namespace calls", () => {
const result = transformCodexMultiAgentBridgeResponseValue({
type: "response.output_item.done",
item: {
arguments: JSON.stringify({ message: "inspect tests" }),
call_id: "call_agent",
name: "multi_agent_v1_spawn_agent",
type: "function_call"
}
});
assert.equal(result.changed, true);
assert.deepEqual(result.value.item, {
arguments: JSON.stringify({ message: "inspect tests" }),
call_id: "call_agent",
name: "spawn_agent",
namespace: "multi_agent_v1",
type: "function_call"
});
});
test("Codex multi-agent bridge rewrites flattened function SSE events", () => {
const event = transformCodexMultiAgentBridgeSseEvent([
"event: response.output_item.done",
`data: ${JSON.stringify({
type: "response.output_item.done",
item: {
arguments: JSON.stringify({ targets: ["agent-1"] }),
call_id: "call_wait",
name: "multi_agent_v1_wait_agent",
type: "function_call"
}
})}`
].join("\n"));
assert.match(event, /^event: response\.output_item\.done\n/);
const data = JSON.parse(event.split("\ndata: ")[1]);
assert.equal(data.item.type, "function_call");
assert.equal(data.item.name, "wait_agent");
assert.equal(data.item.namespace, "multi_agent_v1");
});
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import test from "node:test";
import vm from "node:vm";
import { gatewayFetchPreloadScriptForTest } from "@ccr/core/gateway/core-runtime/supervisor.ts";
test("gateway fetch preload applies API_TIMEOUT_MS to direct upstream fetches", async () => {
const harness = executePreload({
CCR_UNDICI_MODULE: "mock-undici",
CCR_UPSTREAM_TIMEOUT_MS: "600000"
});
assert.equal(harness.agentOptions.length, 1);
assert.equal(harness.agentOptions[0].headersTimeout, 600000);
assert.equal(harness.agentOptions[0].bodyTimeout, 600000);
assert.equal(harness.proxyAgentOptions.length, 0);
await harness.context.fetch("https://api.example.test/v1/chat/completions", { method: "POST" });
assert.equal(harness.fetchCalls.length, 1);
assert.equal(harness.fetchCalls[0].init.dispatcher.kind, "direct");
});
test("gateway fetch preload combines proxy routing with timeout dispatcher options", async () => {
const harness = executePreload({
CCR_UNDICI_MODULE: "mock-undici",
CCR_UPSTREAM_PROXY_URL: "http://127.0.0.1:8888",
CCR_UPSTREAM_TIMEOUT_MS: "600000",
NO_PROXY: "api.internal.test,.bypass.test"
});
assert.equal(harness.agentOptions.length, 1);
assert.equal(harness.proxyAgentOptions.length, 1);
assert.equal(harness.proxyAgentOptions[0].uri, "http://127.0.0.1:8888");
assert.equal(harness.proxyAgentOptions[0].headersTimeout, 600000);
assert.equal(harness.proxyAgentOptions[0].bodyTimeout, 600000);
await harness.context.fetch("https://api.external.test/v1/messages", {});
await harness.context.fetch("https://api.internal.test/v1/messages", {});
await harness.context.fetch("http://127.0.0.1:3456/health", {});
await harness.context.fetch("https://service.bypass.test/v1/messages", {});
assert.equal(harness.fetchCalls[0].init.dispatcher.kind, "proxy");
assert.equal(harness.fetchCalls[1].init.dispatcher.kind, "direct");
assert.equal(harness.fetchCalls[2].init.dispatcher.kind, "direct");
assert.equal(harness.fetchCalls[3].init.dispatcher.kind, "direct");
});
test("gateway fetch preload preserves explicit fetch dispatchers", async () => {
const harness = executePreload({
CCR_UNDICI_MODULE: "mock-undici",
CCR_UPSTREAM_TIMEOUT_MS: "600000"
});
const dispatcher = { kind: "caller" };
await harness.context.fetch("https://api.example.test/v1/messages", { dispatcher });
assert.equal(harness.fetchCalls.length, 1);
assert.equal(harness.fetchCalls[0].init.dispatcher, dispatcher);
});
function executePreload(env) {
const agentOptions = [];
const proxyAgentOptions = [];
const fetchCalls = [];
class Agent {
constructor(options) {
this.kind = "direct";
this.options = options;
agentOptions.push(options);
}
}
class ProxyAgent {
constructor(options) {
this.kind = "proxy";
this.options = options;
proxyAgentOptions.push(options);
}
}
const context = {
URL,
fetch: async (input, init) => {
fetchCalls.push({ init, input });
return { ok: true };
},
process: { env },
require: (moduleName) => {
assert.equal(moduleName, "mock-undici");
return { Agent, ProxyAgent };
}
};
vm.createContext(context);
vm.runInContext(gatewayFetchPreloadScriptForTest(), context);
return {
agentOptions,
context,
fetchCalls,
proxyAgentOptions
};
}
@@ -0,0 +1,265 @@
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler.ts";
import {
createGatewayPlugin,
isLocalAgentOauthProviderPlugin
} from "@ccr/core/gateway/core-runtime/local-agent-auth-provider-hook.ts";
import { virtualApplyPatchToolName } from "@ccr/core/gateway/internal/shared.ts";
test("Grok local agent auth hook refreshes live login state before authenticating upstream requests", async (t) => {
await withGrokHome(t, async (grokHome) => {
writeGrokAuth(grokHome, {
expires_at: "2000-01-01T00:00:00Z",
key: "expired-grok-access-token",
oidc_client_id: "grok-client-id",
oidc_issuer: "https://auth.x.ai",
refresh_token: "grok-refresh-token"
});
const previousFetch = globalThis.fetch;
const previousTokenEndpoint = process.env.GROK_OIDC_TOKEN_ENDPOINT;
process.env.GROK_OIDC_TOKEN_ENDPOINT = "http://127.0.0.1/grok/oauth/token";
globalThis.fetch = async (input, init) => {
assert.equal(String(input), "http://127.0.0.1/grok/oauth/token");
assert.equal(String(init?.body ?? ""), "client_id=grok-client-id&grant_type=refresh_token&refresh_token=grok-refresh-token");
return new Response(JSON.stringify({
access_token: "refreshed-grok-access-token",
expires_in: 3600,
refresh_token: "refreshed-grok-refresh-token"
}), { headers: { "content-type": "application/json" }, status: 200 });
};
t.after(() => {
globalThis.fetch = previousFetch;
restoreEnv("GROK_OIDC_TOKEN_ENDPOINT", previousTokenEndpoint);
});
const [hook] = createGatewayPlugin({
config: {
providerPlugins: [grokOauthProviderPlugin()]
}
}).providerHooks;
assert.equal(hook.key, "config:ccr-local-agent-grok-cli-api-grok-cli-oauth");
const patch = "*** Begin Patch\n*** Add File: grok.txt\n+hi\n*** End Patch\n";
const upstreamRequest = {
body: {
external_web_access: true,
input: [
{ type: "custom_tool_call", call_id: "call_patch", name: "apply_patch", input: patch },
{ type: "custom_tool_call_output", call_id: "call_patch", output: "Success" }
],
model: "wrong-model",
parallel_tool_calls: true,
tool_choice: "auto",
tools: [
{ type: "function", name: "exec_command" },
{ type: "custom", name: "apply_patch", format: { type: "grammar", syntax: "lark", definition: "start: begin_patch" } },
{ type: "namespace", name: "multi_agent_v1" },
{ type: "web_search" }
]
},
headers: {
"content-type": "application/json",
"x-api-key": "client-key"
},
method: "POST",
url: "https://cli-chat-proxy.grok.com/v1/responses"
};
const authResult = await hook.authenticate({
model: "grok-4.5",
upstreamRequest
});
assert.equal(authResult.ok, true);
assert.equal(authResult.value.headers.authorization, "Bearer refreshed-grok-access-token");
assert.equal(authResult.value.headers["x-api-key"], undefined);
assert.equal(upstreamRequest.headers["x-api-key"], "client-key");
const requestResult = await hook.transformRequest({
model: "grok-4.5",
upstreamRequest: authResult.value
});
assert.equal(requestResult.ok, true);
assert.equal(requestResult.value.headers["x-grok-client-identifier"], "xai-grok-cli");
assert.equal(requestResult.value.headers["x-grok-client-version"], "0.2.93");
assert.equal(requestResult.value.headers["x-grok-model-override"], "grok-4.5");
assert.equal(requestResult.value.body.external_web_access, undefined);
assert.deepEqual(requestResult.value.body.tools.map((tool) => tool.type), ["function", "function"]);
assert.equal(requestResult.value.body.tools[1].name, virtualApplyPatchToolName);
assert.equal(requestResult.value.body.tools.some((tool) => tool.type === "custom" || tool.type === "namespace"), false);
assert.equal(requestResult.value.body.tool_choice, "auto");
assert.equal(requestResult.value.body.parallel_tool_calls, true);
assert.match(requestResult.value.body.instructions, /When modifying files, call virtual_apply_patch/);
assert.equal(requestResult.value.body.input[0].type, "function_call");
assert.equal(requestResult.value.body.input[0].name, virtualApplyPatchToolName);
assert.deepEqual(JSON.parse(requestResult.value.body.input[0].arguments), { patch });
assert.equal(requestResult.value.body.input[1].type, "function_call_output");
const persisted = JSON.parse(readFileSync(path.join(grokHome, "auth.json"), "utf8"));
assert.equal(persisted["https://auth.x.ai::test-account"].key, "refreshed-grok-access-token");
});
});
test("Grok local agent request hook removes unsupported Responses tools and stale tool choice", () => {
const [hook] = createGatewayPlugin({
config: {
providerPlugins: [grokOauthProviderPlugin()]
}
}).providerHooks;
const requestResult = hook.transformRequest({
model: "grok-4.5",
upstreamRequest: {
body: {
parallel_tool_calls: true,
tool_choice: { type: "tool", name: "multi_agent_v1" },
tools: [
{ type: "namespace", name: "multi_agent_v1" },
{ type: "function", name: "exec_command" }
]
},
headers: {},
method: "POST",
url: "https://cli-chat-proxy.grok.com/v1/responses"
}
});
assert.equal(requestResult.ok, true);
assert.deepEqual(requestResult.value.body.tools, [{ type: "function", name: "exec_command" }]);
assert.equal(requestResult.value.body.tool_choice, undefined);
assert.equal(requestResult.value.body.parallel_tool_calls, true);
});
test("core gateway config installs the local agent dynamic auth runtime hook when OAuth plugins are present", async () => {
const config = createDefaultAppConfig();
config.providerPlugins = [grokOauthProviderPlugin()];
config.Providers = [
{
api_base_url: "https://cli-chat-proxy.grok.com/v1",
api_key: "ccr-local-agent-login",
id: "grok-cli-api",
models: ["grok-4.5"],
name: "Grok CLI API",
type: "openai_responses"
}
];
const compiled = await compileCoreGatewayConfig(
config,
"raw-trace-token",
"billing-usage-token",
"core-auth-token"
);
const plugins = Array.isArray(compiled.plugins) ? compiled.plugins : [];
const localAgentAuthPlugin = plugins.find((plugin) => plugin.key === "ccr-local-agent-auth-provider-hooks");
assert.ok(localAgentAuthPlugin);
assert.match(localAgentAuthPlugin.modulePath, /local-agent-auth-provider-hook\.js$/);
});
test("core gateway config removes Grok unsupported Responses options through declarative request transforms", async (t) => {
await withGrokHome(t, async (grokHome) => {
writeGrokAuth(grokHome, {
expires_at: "2099-01-01T00:00:00Z",
key: "live-grok-access-token",
oidc_client_id: "grok-client-id",
oidc_issuer: "https://auth.x.ai",
refresh_token: "grok-refresh-token"
});
const plugin = grokOauthProviderPlugin();
plugin.request.bodyRemove = ["metadata"];
const config = createDefaultAppConfig();
config.providerPlugins = [plugin];
config.Providers = [
{
api_base_url: "https://cli-chat-proxy.grok.com/v1",
api_key: "ccr-local-agent-login",
id: "grok-cli-api",
models: ["grok-4.5"],
name: "Grok CLI API",
type: "openai_responses"
}
];
const compiled = await compileCoreGatewayConfig(
config,
"raw-trace-token",
"billing-usage-token",
"core-auth-token"
);
const providerPlugins = Array.isArray(compiled.providerPlugins) ? compiled.providerPlugins : [];
const grokPlugin = providerPlugins.find((value) => value.key === "ccr-local-agent-grok-cli-api-grok-cli-oauth");
assert.ok(grokPlugin);
assert.deepEqual(grokPlugin.request.bodyRemove, ["metadata", "external_web_access"]);
});
});
test("local agent OAuth plugin detector only matches managed OAuth imports", () => {
assert.equal(isLocalAgentOauthProviderPlugin(grokOauthProviderPlugin()), true);
assert.equal(isLocalAgentOauthProviderPlugin({
key: "ccr-local-agent-grok-cli-api-grok-cli-api-key"
}), false);
assert.equal(isLocalAgentOauthProviderPlugin({
key: "external-grok-cli-oauth"
}), false);
});
function grokOauthProviderPlugin() {
return {
auth: {
headers: {
authorization: "Bearer imported-stale-token"
},
removeHeaders: ["x-api-key"],
strict: true
},
key: "ccr-local-agent-grok-cli-api-grok-cli-oauth",
providerName: "Grok CLI API",
request: {
headers: {
"x-grok-client-identifier": "xai-grok-cli",
"x-grok-client-version": "0.2.93",
"x-grok-model-override": "{{ model }}"
},
strict: true
}
};
}
async function withGrokHome(t, run) {
const previousGrokHome = process.env.GROK_HOME;
const previousGrokAuthFile = process.env.GROK_AUTH_FILE;
const previousGrokCliVersion = process.env.GROK_CLI_VERSION;
const grokHome = mkdtempSync(path.join(os.tmpdir(), "ccr-grok-hook-test-"));
process.env.GROK_HOME = grokHome;
process.env.GROK_CLI_VERSION = "0.2.93";
delete process.env.GROK_AUTH_FILE;
try {
await run(grokHome);
} finally {
restoreEnv("GROK_HOME", previousGrokHome);
restoreEnv("GROK_AUTH_FILE", previousGrokAuthFile);
restoreEnv("GROK_CLI_VERSION", previousGrokCliVersion);
rmSync(grokHome, { force: true, recursive: true });
}
}
function restoreEnv(name, value) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
function writeGrokAuth(grokHome, auth) {
writeFileSync(path.join(grokHome, "auth.json"), JSON.stringify({
"https://auth.x.ai::test-account": auth
}, null, 2));
}
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { fetchUpstreamWithFallback } from "@ccr/core/gateway/upstream/executor.ts";
import { RequestRouteTraceRecorder } from "@ccr/core/observability/route-trace.ts";
const retryConfig = {
Providers: [],
@@ -64,3 +65,110 @@ test("retry backoff stops after client aborts a network error", async () => {
throw new Error("upstream unavailable");
});
});
test("model-chain fallback rebuilds every protocol attempt from the canonical request", async () => {
const config = {
Providers: [
{
capabilities: [{ baseUrl: "https://anthropic-primary.example", type: "anthropic_messages" }],
id: "anthropic-primary",
models: ["claude-primary"],
name: "Anthropic Primary"
},
{
capabilities: [{ baseUrl: "https://openai-fallback.example", type: "openai_responses" }],
id: "openai-fallback",
models: ["gpt-fallback"],
name: "OpenAI Fallback"
},
{
capabilities: [{ baseUrl: "https://anthropic-recovery.example", type: "anthropic_messages" }],
id: "anthropic-recovery",
models: ["claude-recovery"],
name: "Anthropic Recovery"
}
],
Router: { fallback: { mode: "off", models: [], retryCount: 0 }, rules: [] },
virtualModelProfiles: []
};
const fallback = {
mode: "model-chain",
models: ["OpenAI Fallback/gpt-fallback", "Anthropic Recovery/claude-recovery"],
retryCount: 0
};
const canonicalBody = {
context_management: { edits: [{ type: "clear_tool_uses_20250919" }] },
messages: [{ content: "hello", role: "user" }],
model: "Anthropic Primary/claude-primary",
output_config: { effort: "high", verbosity: "medium" },
system: [{ cache_control: { type: "ephemeral" }, text: "system", type: "text" }],
thinking: { type: "adaptive" }
};
const captured = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_url, init) => {
captured.push({
body: JSON.parse(init.body),
headers: init.headers
});
const status = captured.length === 1 ? 429 : captured.length === 2 ? 400 : 200;
return new Response('{"ok":true}', {
headers: {
"content-type": "application/json",
"retry-after": "0.001"
},
status
});
};
try {
const trace = new RequestRouteTraceRecorder(Date.now());
const result = await fetchUpstreamWithFallback({
body: Buffer.from(JSON.stringify(canonicalBody)),
config,
coreAuthToken: "core-token",
fallback,
headers: {},
method: "POST",
path: "/v1/messages",
routedModel: canonicalBody.model,
trace,
upstreamUrl: "http://127.0.0.1:3456/v1/messages"
});
assert.equal(result.response.status, 200);
assert.equal(captured.length, 3);
assert.deepEqual(captured.map((attempt) => attempt.body.model), [
"claude-primary",
"gpt-fallback",
"claude-recovery"
]);
assert.deepEqual(captured[0].body.thinking, { type: "adaptive" });
assert.equal(captured[1].body.thinking, undefined);
assert.deepEqual(captured[2].body.thinking, { type: "adaptive" });
assert.deepEqual(captured[2].body.context_management, canonicalBody.context_management);
assert.deepEqual(captured[2].body.output_config, canonicalBody.output_config);
assert.equal(captured[0].headers["x-target-provider"], "anthropic-primary::anthropic_messages");
assert.equal(captured[1].headers["x-target-provider"], "openai-fallback::openai_responses");
assert.equal(captured[2].headers["x-target-provider"], "anthropic-recovery::anthropic_messages");
const finishedTrace = trace.finish();
const capabilityRoutingHops = finishedTrace.hops
.filter((hop) => hop.name === "provider.capability-routing");
assert.deepEqual(
capabilityRoutingHops.map((hop) => hop.attempt),
[1, 2, 3]
);
assert.deepEqual(
capabilityRoutingHops[0].changes.map((change) => change.path),
["/body/model", "/routing/model"]
);
assert.deepEqual(
finishedTrace.hops
.find((hop) => hop.name === "fallback.execution-plan")
?.changes.map((change) => change.path),
["/routing/fallback"]
);
} finally {
globalThis.fetch = originalFetch;
}
});
@@ -4,10 +4,12 @@ import type { RequestRouteTrace, RequestRouteTraceChange, RequestRouteTraceHop }
import {
AnimatedIconSwap, Check, ChevronDown, ChevronLeft,
ChevronRight, clampNumber, clientInitial, cn, Copy, copyTextToClipboard,
Database, Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle, filterLogText, formatBytes, formatCompactNumber, formatDuration,
createLogBodyPreviewText, Database, Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle, filterLogText, formatBytes, formatCompactNumber, formatDuration,
formatLogBodyView, formatLogDateTime, formatLogTokenSummary, formatNetworkRequestRaw, formatNetworkResponseRaw, formatRouteTracePath, formatUsdCost,
isJsonContainer, jsonChildPath, logRequestModel,
logResolvedRouteModel, logSelectOptions, motion, MoveRight, Network, networkCodeLabel,
FormattedLogBody,
isJsonContainer, isLargeLogBody, jsonChildPath, logRequestModel,
LogBodyFormatMode, logBodyLargeTextThreshold, logBodyPreviewTextLimit, LogBodyWorkerResponse,
logResolvedRouteModel, logResponseModel, logSelectOptions, motion, MoveRight, Network, networkCodeLabel,
networkExchangeMatchesQuery, networkHeaderRows, networkLifecycleLabel, networkQueryRows, networkRowId, networkSummaryRows,
Pause, Play, ProxyNetworkBody, ProxyNetworkExchange, ProxyNetworkSnapshot, ProxyStatus,
ReactNode, ReactPointerEvent, RefreshCw, RequestLogBody, RequestLogEntry, RequestLogListFilter,
@@ -19,11 +21,10 @@ import { TooltipPortal } from "@/components/ui/tooltip";
type NetworkRequestTab = "body" | "header" | "query" | "raw" | "summary";
type NetworkResponseTab = "body" | "header" | "raw";
const logBodyViewCacheLimit = 12;
const logJsonAutoExpandEntryLimit = 60;
const logJsonContainerPreviewLimit = 80;
const logJsonAutoExpandTextLimit = 160 * 1024;
const logBodyViewCache = new Map<string, ReturnType<typeof formatLogBodyView>>();
const logBodyWorkerFilterDebounceMs = 180;
type LogTableColumnId = "time" | "status" | "stream" | "model" | "credential" | "tokens" | "duration";
type LogTableColumn = {
id: LogTableColumnId;
@@ -1496,6 +1497,247 @@ function LogStreamCell({ entry }: { entry: RequestLogEntry }) {
type LogPayloadTab = "body" | "header";
type LogBodyPanelView = FormattedLogBody & {
bodyKey: string;
error: string;
formattedTextLength: number;
large: boolean;
loading: boolean;
mode: LogBodyFormatMode;
preview: boolean;
query: string;
sourceSizeBytes: number;
visible: string;
};
function useLogBodyWorkerView(
body: RequestLogBody | undefined,
bodyKey: string,
mode: LogBodyFormatMode,
query: string
): LogBodyPanelView {
const debouncedQuery = useDebouncedValue(query, logBodyWorkerFilterDebounceMs);
const latestQueryRef = useRef(debouncedQuery);
const workerRef = useRef<Worker>();
const formatRequestIdRef = useRef(0);
const filterRequestIdRef = useRef(0);
const [bodyView, setBodyView] = useState<LogBodyPanelView>(() => createInitialLogBodyPanelView(body, bodyKey, mode, query));
useEffect(() => {
latestQueryRef.current = debouncedQuery;
}, [debouncedQuery]);
useEffect(() => {
const initial = createInitialLogBodyPanelView(body, bodyKey, mode, latestQueryRef.current);
setBodyView(initial);
workerRef.current?.terminate();
workerRef.current = undefined;
if (isStaticLogBody(body)) {
setBodyView(createStaticLogBodyPanelView(body, bodyKey, mode, latestQueryRef.current));
return;
}
if (typeof Worker === "undefined") {
setBodyView({
...initial,
error: "Body formatter worker is unavailable.",
loading: false,
visible: initial.visible || "Body formatter worker is unavailable."
});
return;
}
const worker = createLogBodyFormatterWorker();
const formatRequestId = formatRequestIdRef.current + 1;
formatRequestIdRef.current = formatRequestId;
filterRequestIdRef.current += 1;
workerRef.current = worker;
worker.onmessage = (event: MessageEvent<LogBodyWorkerResponse>) => {
const response = event.data;
if (response.kind === "format-result") {
if (response.id !== formatRequestIdRef.current || response.bodyKey !== bodyKey || response.mode !== mode) {
return;
}
setBodyView(logBodyPanelViewFromWorkerResult(response));
if (response.query !== latestQueryRef.current) {
postLogBodyFilter(worker, bodyKey, mode, latestQueryRef.current, filterRequestIdRef);
}
return;
}
if (response.kind === "filter-result") {
if (response.id !== filterRequestIdRef.current || response.bodyKey !== bodyKey || response.mode !== mode) {
return;
}
setBodyView((current) => current.bodyKey === bodyKey && current.mode === mode
? { ...current, query: response.query, visible: response.visible }
: current);
return;
}
if (
(response.operation === "format" && response.id === formatRequestIdRef.current) ||
(response.operation === "filter" && response.id === filterRequestIdRef.current)
) {
setBodyView((current) => current.bodyKey === bodyKey && current.mode === mode
? { ...current, error: response.message, loading: false, visible: current.visible || response.message }
: current);
}
};
worker.onerror = (event) => {
setBodyView((current) => current.bodyKey === bodyKey && current.mode === mode
? { ...current, error: event.message || "Body formatter worker failed.", loading: false }
: current);
};
worker.postMessage({
body,
bodyKey,
id: formatRequestId,
kind: "format",
largeTextThreshold: logBodyLargeTextThreshold,
mode,
previewTextLimit: logBodyPreviewTextLimit,
query: latestQueryRef.current
});
return () => {
if (workerRef.current === worker) {
workerRef.current = undefined;
}
worker.terminate();
};
}, [body, bodyKey, mode]);
useEffect(() => {
const worker = workerRef.current;
if (!worker || bodyView.loading || bodyView.bodyKey !== bodyKey || bodyView.mode !== mode || bodyView.query === debouncedQuery) {
return;
}
postLogBodyFilter(worker, bodyKey, mode, debouncedQuery, filterRequestIdRef);
}, [bodyKey, bodyView.bodyKey, bodyView.loading, bodyView.mode, bodyView.query, debouncedQuery, mode]);
return bodyView;
}
function useDebouncedValue<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = window.setTimeout(() => setDebounced(value), delayMs);
return () => window.clearTimeout(timer);
}, [delayMs, value]);
return debounced;
}
function createLogBodyFormatterWorker(): Worker {
return new Worker(new URL("../../assets/log-body.worker.js", window.location.href), { type: "module" });
}
function postLogBodyFilter(
worker: Worker,
bodyKey: string,
mode: LogBodyFormatMode,
query: string,
idRef: { current: number }
) {
const id = idRef.current + 1;
idRef.current = id;
worker.postMessage({
bodyKey,
id,
kind: "filter",
mode,
query
});
}
function createInitialLogBodyPanelView(
body: RequestLogBody | undefined,
bodyKey: string,
mode: LogBodyFormatMode,
query: string
): LogBodyPanelView {
if (isStaticLogBody(body)) {
return createStaticLogBodyPanelView(body, bodyKey, mode, query);
}
const large = isLargeLogBody(body, logBodyLargeTextThreshold);
const preview = large && mode !== "full";
const text = preview
? createLogBodyPreviewText(body, logBodyPreviewTextLimit)
: "Loading body...";
return {
bodyKey,
error: "",
formattedTextLength: text.length,
large,
loading: true,
mode,
preview,
query,
sourceSizeBytes: body?.sizeBytes ?? 0,
text,
visible: text
};
}
function createStaticLogBodyPanelView(
body: RequestLogBody | undefined,
bodyKey: string,
mode: LogBodyFormatMode,
query: string
): LogBodyPanelView {
const text = body?.text || "No body";
return {
bodyKey,
error: "",
formattedTextLength: text.length,
large: false,
loading: false,
mode,
preview: false,
query,
sourceSizeBytes: body?.sizeBytes ?? 0,
text,
visible: filterStaticLogBodyText(text, query)
};
}
function logBodyPanelViewFromWorkerResult(result: Extract<LogBodyWorkerResponse, { kind: "format-result" }>): LogBodyPanelView {
return {
bodyKey: result.bodyKey,
error: "",
formattedTextLength: result.formattedTextLength,
json: result.json,
large: result.large,
loading: false,
mode: result.mode,
preview: result.preview,
query: result.query,
sourceSizeBytes: result.sourceSizeBytes,
text: result.text,
visible: result.visible
};
}
function isStaticLogBody(body: RequestLogBody | undefined): boolean {
return !body || (!body.text && body.sizeBytes === 0);
}
function filterStaticLogBodyText(text: string, query: string): string {
const normalized = query.trim().toLowerCase();
if (!normalized) {
return text;
}
return text.toLowerCase().includes(normalized) ? text : "No matching lines";
}
function LogJsonPanel({
body,
className,
@@ -1514,20 +1756,25 @@ function LogJsonPanel({
const t = useAppText();
const [selectedTab, setSelectedTab] = useState<LogPayloadTab>("body");
const [preferTextBody, setPreferTextBody] = useState(false);
const [bodyMode, setBodyMode] = useState<LogBodyFormatMode>("preview");
const [fullscreenOpen, setFullscreenOpen] = useState(false);
const [query, setQuery] = useState("");
const bodyKey = logBodyCacheKey(body);
const bodyView = useMemo(() => cachedFormatLogBodyView(bodyKey, body), [bodyKey]);
const bodyView = useLogBodyWorkerView(body, bodyKey, bodyMode, query);
const formatted = bodyView.text;
const visible = useMemo(() => filterLogText(formatted, query), [formatted, query]);
const visible = bodyView.visible;
const headerRows = useMemo(() => networkHeaderRows(headers ?? {}), [headers]);
const [expandedJsonPaths, setExpandedJsonPaths] = useState<Set<string>>(() => createInitialVisibleJsonPaths(bodyView));
const showJsonTree = bodyView.json !== undefined && query.trim() === "" && !preferTextBody;
const showJsonTree = bodyView.json !== undefined && query.trim() === "" && !preferTextBody && !bodyView.preview;
useEffect(() => {
setPreferTextBody(false);
setBodyMode("preview");
}, [bodyKey]);
useEffect(() => {
setExpandedJsonPaths(createInitialVisibleJsonPaths(bodyView));
setPreferTextBody(false);
}, [bodyKey]);
}, [bodyView.bodyKey, bodyView.json, bodyView.text]);
useEffect(() => {
if (!fullscreenOpen) {
@@ -1582,6 +1829,7 @@ function LogJsonPanel({
<LogJsonBodyToolbar
body={body}
bodyView={bodyView}
onLoadFullBody={() => setBodyMode("full")}
onQueryChange={setQuery}
onToggleTextBody={() => setPreferTextBody((current) => !current)}
preferTextBody={preferTextBody}
@@ -1610,6 +1858,7 @@ function LogJsonPanel({
copyText={formatted}
expandedJsonPaths={expandedJsonPaths}
onClose={() => setFullscreenOpen(false)}
onLoadFullBody={() => setBodyMode("full")}
onQueryChange={setQuery}
onToggleJsonPath={toggleJsonPath}
onToggleTextBody={() => setPreferTextBody((current) => !current)}
@@ -1636,6 +1885,7 @@ function LogJsonPanel({
function LogJsonBodyToolbar({
body,
bodyView,
onLoadFullBody,
onQueryChange,
onToggleTextBody,
preferTextBody,
@@ -1643,7 +1893,8 @@ function LogJsonBodyToolbar({
title
}: {
body?: RequestLogBody;
bodyView: ReturnType<typeof formatLogBodyView>;
bodyView: LogBodyPanelView;
onLoadFullBody: () => void;
onQueryChange: (value: string) => void;
onToggleTextBody: () => void;
preferTextBody: boolean;
@@ -1651,6 +1902,12 @@ function LogJsonBodyToolbar({
title: string;
}) {
const t = useAppText();
const canLoadFullBody = bodyView.preview && bodyView.large && query.trim() === "";
const canToggleJsonText = bodyView.json !== undefined && query.trim() === "";
const showToggleButton = canLoadFullBody || canToggleJsonText;
const toggleLabel = canLoadFullBody
? bodyView.loading && bodyView.mode === "full" ? t("Loading full payload...") : t("Show full content")
: preferTextBody ? "JSON" : t("Show full content");
return (
<div className="network-body-meta flex min-h-9 shrink-0 items-center gap-2 border-b px-3 py-1.5">
@@ -1664,15 +1921,20 @@ function LogJsonBodyToolbar({
value={query}
/>
</div>
{bodyView.json !== undefined && query.trim() === "" ? (
{showToggleButton ? (
<button
className="network-tab shrink-0 border-0 bg-transparent p-0 text-[11px] font-semibold outline-none"
onClick={onToggleTextBody}
disabled={bodyView.loading && bodyView.mode === "full"}
onClick={canLoadFullBody ? onLoadFullBody : onToggleTextBody}
type="button"
>
{preferTextBody ? "JSON" : t("Show full content")}
{toggleLabel}
</button>
) : null}
{bodyView.loading ? <span className="network-muted shrink-0 text-[11px] font-semibold">{t("Loading full payload...")}</span> : null}
{bodyView.error ? <span className="network-error-box shrink-0 rounded px-2 py-0.5 text-[11px] font-semibold">{bodyView.error}</span> : null}
{bodyView.preview ? <span className="network-service-paused rounded-full px-2 py-0.5 text-[11px] font-semibold">{t("preview")}</span> : null}
{bodyView.sourceSizeBytes > 0 ? <span className="network-muted hidden shrink-0 text-[11px] font-semibold sm:inline">{formatBytes(bodyView.sourceSizeBytes)}</span> : null}
{body?.contentType ? <span className="network-muted hidden shrink-0 text-[11px] font-semibold sm:inline">{body.contentType}</span> : null}
{body?.truncated ? <span className="network-service-paused rounded-full px-2 py-0.5 text-[11px] font-semibold">{t("truncated")}</span> : null}
</div>
@@ -1706,6 +1968,7 @@ function LogJsonFullscreenViewer({
copyText,
expandedJsonPaths,
onClose,
onLoadFullBody,
onQueryChange,
onToggleJsonPath,
onToggleTextBody,
@@ -1718,11 +1981,12 @@ function LogJsonFullscreenViewer({
visible
}: {
body?: RequestLogBody;
bodyView: ReturnType<typeof formatLogBodyView>;
bodyView: LogBodyPanelView;
copyLabel: string;
copyText: string;
expandedJsonPaths: Set<string>;
onClose: () => void;
onLoadFullBody: () => void;
onQueryChange: (value: string) => void;
onToggleJsonPath: (path: string) => void;
onToggleTextBody: () => void;
@@ -1760,6 +2024,7 @@ function LogJsonFullscreenViewer({
<LogJsonBodyToolbar
body={body}
bodyView={bodyView}
onLoadFullBody={onLoadFullBody}
onQueryChange={onQueryChange}
onToggleTextBody={onToggleTextBody}
preferTextBody={preferTextBody}
@@ -1798,27 +2063,7 @@ function logBodyCacheKey(body: RequestLogBody | undefined): string {
].join("\u001f");
}
function cachedFormatLogBodyView(key: string, body: RequestLogBody | undefined): ReturnType<typeof formatLogBodyView> {
const cached = logBodyViewCache.get(key);
if (cached) {
logBodyViewCache.delete(key);
logBodyViewCache.set(key, cached);
return cached;
}
const value = formatLogBodyView(body);
logBodyViewCache.set(key, value);
while (logBodyViewCache.size > logBodyViewCacheLimit) {
const oldest = logBodyViewCache.keys().next().value;
if (!oldest) {
break;
}
logBodyViewCache.delete(oldest);
}
return value;
}
function createInitialVisibleJsonPaths(bodyView: ReturnType<typeof formatLogBodyView>): Set<string> {
function createInitialVisibleJsonPaths(bodyView: FormattedLogBody): Set<string> {
if (!isJsonContainer(bodyView.json)) {
return new Set();
}
@@ -11,6 +11,7 @@ export * from "./profiles";
export * from "./services";
export * from "./provider-accounts";
export * from "./logs";
export * from "./log-body-worker-protocol";
export * from "./common";
export * from "./config";
export * from "./api-keys";
@@ -0,0 +1,103 @@
import type { RequestLogBody } from "@ccr/core/contracts/app";
import type { FormattedLogBody } from "./logs";
export const logBodyLargeTextThreshold = 256 * 1024;
export const logBodyPreviewTextLimit = 160 * 1024;
export type LogBodyFormatMode = "full" | "preview";
export type LogBodyFormatRequest = {
body?: RequestLogBody;
bodyKey: string;
id: number;
kind: "format";
largeTextThreshold?: number;
mode: LogBodyFormatMode;
previewTextLimit?: number;
query: string;
};
export type LogBodyFilterRequest = {
bodyKey: string;
id: number;
kind: "filter";
mode: LogBodyFormatMode;
query: string;
};
export type LogBodyWorkerRequest = LogBodyFilterRequest | LogBodyFormatRequest;
export type LogBodyFormatResult = FormattedLogBody & {
bodyKey: string;
formattedTextLength: number;
id: number;
kind: "format-result";
large: boolean;
mode: LogBodyFormatMode;
ok: true;
preview: boolean;
query: string;
sourceSizeBytes: number;
visible: string;
};
export type LogBodyFilterResult = {
bodyKey: string;
id: number;
kind: "filter-result";
mode: LogBodyFormatMode;
ok: true;
query: string;
visible: string;
};
export type LogBodyWorkerError = {
bodyKey?: string;
id: number;
kind: "error";
message: string;
mode?: LogBodyFormatMode;
operation: LogBodyWorkerRequest["kind"];
};
export type LogBodyWorkerResponse = LogBodyFilterResult | LogBodyFormatResult | LogBodyWorkerError;
export function isLargeLogBody(
body: RequestLogBody | undefined,
threshold = logBodyLargeTextThreshold
): boolean {
if (!body) {
return false;
}
return Math.max(body.sizeBytes, body.text.length) > threshold;
}
export function createLogBodyPreviewText(
body: RequestLogBody | undefined,
limit = logBodyPreviewTextLimit
): string {
if (!body || (!body.text && body.sizeBytes === 0)) {
return "No body";
}
const text = body.text || "";
if (!text) {
return "Body text is not loaded.";
}
if (text.length <= limit) {
return text;
}
const headLength = Math.max(0, Math.floor(limit * 0.65));
const tailLength = Math.max(0, limit - headLength);
const omitted = Math.max(0, text.length - headLength - tailLength);
const head = text.slice(0, headLength);
const tail = tailLength > 0 ? text.slice(-tailLength) : "";
return [
head,
"",
`... ${omitted} characters omitted from preview ...`,
"",
tail
].join("\n");
}
@@ -0,0 +1,100 @@
import {
createLogBodyPreviewText,
isLargeLogBody,
logBodyLargeTextThreshold,
logBodyPreviewTextLimit,
type LogBodyFilterRequest,
type LogBodyFormatRequest,
type LogBodyFormatResult,
type LogBodyWorkerRequest,
type LogBodyWorkerResponse
} from "./log-body-worker-protocol";
import { filterLogText, formatLogBodyView, type FormattedLogBody } from "./logs";
type CachedFormattedBody = FormattedLogBody & {
bodyKey: string;
large: boolean;
mode: LogBodyFormatRequest["mode"];
preview: boolean;
sourceSizeBytes: number;
};
type LogBodyWorkerGlobal = {
onmessage: ((event: MessageEvent<LogBodyWorkerRequest>) => void) | null;
postMessage: (message: LogBodyWorkerResponse) => void;
};
const worker = self as unknown as LogBodyWorkerGlobal;
let cachedBody: CachedFormattedBody | undefined;
worker.onmessage = (event: MessageEvent<LogBodyWorkerRequest>) => {
const request = event.data;
try {
if (request.kind === "format") {
worker.postMessage(formatBody(request) satisfies LogBodyWorkerResponse);
return;
}
worker.postMessage(filterBody(request) satisfies LogBodyWorkerResponse);
} catch (error) {
worker.postMessage({
bodyKey: "bodyKey" in request ? request.bodyKey : undefined,
id: request.id,
kind: "error",
message: error instanceof Error ? error.message : String(error),
mode: "mode" in request ? request.mode : undefined,
operation: request.kind
} satisfies LogBodyWorkerResponse);
}
};
function formatBody(request: LogBodyFormatRequest): LogBodyFormatResult {
const threshold = request.largeTextThreshold ?? logBodyLargeTextThreshold;
const previewLimit = request.previewTextLimit ?? logBodyPreviewTextLimit;
const large = isLargeLogBody(request.body, threshold);
const preview = large && request.mode !== "full";
const bodyView = preview
? { text: createLogBodyPreviewText(request.body, previewLimit) }
: formatLogBodyView(request.body);
const sourceSizeBytes = request.body?.sizeBytes ?? 0;
const visible = filterLogText(bodyView.text, request.query);
cachedBody = {
...bodyView,
bodyKey: request.bodyKey,
large,
mode: request.mode,
preview,
sourceSizeBytes
};
return {
...bodyView,
bodyKey: request.bodyKey,
formattedTextLength: bodyView.text.length,
id: request.id,
kind: "format-result",
large,
mode: request.mode,
ok: true,
preview,
query: request.query,
sourceSizeBytes,
visible
};
}
function filterBody(request: LogBodyFilterRequest): LogBodyWorkerResponse {
if (!cachedBody || cachedBody.bodyKey !== request.bodyKey || cachedBody.mode !== request.mode) {
throw new Error("Formatted body cache is not available.");
}
return {
bodyKey: request.bodyKey,
id: request.id,
kind: "filter-result",
mode: request.mode,
ok: true,
query: request.query,
visible: filterLogText(cachedBody.text, request.query)
};
}