Add Codex multi-agent bridge and provider model descriptions

This commit is contained in:
musistudio
2026-08-05 16:24:36 +08:00
parent b4afa55c49
commit 0e1d9f4c05
8 changed files with 642 additions and 11 deletions
@@ -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)) {
@@ -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;
}
}
+37 -4
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";
@@ -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,6 +382,34 @@ export class GatewayRequestPipeline {
});
}
const codexMultiAgentBridgeStartedAt = Date.now();
const codexMultiAgentBridgeRequest = prepareCodexMultiAgentBridgeRequest({
body: bodyToForward,
config: this.config,
headers: request.headers,
method,
path,
routedModel
});
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,
config: this.config,
@@ -682,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) {
@@ -734,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(
@@ -759,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";
+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([
@@ -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";
@@ -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");
});