mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Add ultra effort and refine model reasoning profiles
This commit is contained in:
@@ -1488,8 +1488,8 @@ class OpenCodeBotWorker {
|
||||
const entry = this.conversationEntry(key) || { sessionId: "", projectDirectory: this.projectDirectory(key), title: "" };
|
||||
const value = String(args || "").trim();
|
||||
if (!value) return "Current " + setting + ": " + (entry[setting] || "default") + ".";
|
||||
const allowed = setting === "effort" ? new Set(["low", "medium", "high", "xhigh", "max", "reset"]) : null;
|
||||
if (allowed && !allowed.has(value)) return "Supported effort values: low, medium, high, xhigh, max, reset.";
|
||||
const allowed = setting === "effort" ? new Set(["low", "medium", "high", "xhigh", "max", "ultra", "reset"]) : null;
|
||||
if (allowed && !allowed.has(value)) return "Supported effort values: low, medium, high, xhigh, max, ultra, reset.";
|
||||
entry[setting] = value === "reset" ? "" : value;
|
||||
entry.updatedAt = Date.now();
|
||||
this.setConversationEntry(key, entry);
|
||||
@@ -3194,7 +3194,7 @@ class ClaudeCodeAppServer {
|
||||
? entry.effort || thread && thread.reasoningEffort
|
||||
: entry[setting] || thread && thread[setting];
|
||||
if (!value) return "Current " + setting + ": " + (current || "default") + ".";
|
||||
if (setting === "effort" && !["low", "medium", "high", "xhigh", "max", "reset"].includes(value)) return "Supported effort values: low, medium, high, xhigh, max, reset.";
|
||||
if (setting === "effort" && !["low", "medium", "high", "xhigh", "max", "ultra", "reset"].includes(value)) return "Supported effort values: low, medium, high, xhigh, max, ultra, reset.";
|
||||
if (setting === "mode" && !["manual", "acceptEdits", "plan", "auto", "dontAsk", "reset"].includes(value)) return "Supported modes: manual, acceptEdits, plan, auto, dontAsk, reset.";
|
||||
const next = value === "reset" ? "" : value;
|
||||
if (setting === "mode") entry.permissionMode = next;
|
||||
@@ -6068,7 +6068,7 @@ function sessionCommandHelpText(agentName) {
|
||||
"/session archive <n> | restore <n> | delete <n> confirm",
|
||||
"/session history [count] - show recent turns",
|
||||
"/session model [selector|reset] - show or change model/provider",
|
||||
"/session effort [low|medium|high|xhigh|max|reset]",
|
||||
"/session effort [low|medium|high|xhigh|max|ultra|reset]",
|
||||
"/session mode [manual|acceptEdits|plan|auto|dontAsk|reset]",
|
||||
"/session usage - show the latest token and cost data",
|
||||
"/session memory [list|add <text>|clear] - manage persistent session context",
|
||||
|
||||
@@ -186,7 +186,13 @@ function codexModelCapabilityProfile(
|
||||
const supportsFusionVision = codexVirtualModelSupportsFusionVision(model, config);
|
||||
const supportsFusionWebSearch = codexVirtualModelSupportsFusionWebSearch(model, config);
|
||||
const metadataReasoningLevels = normalizeProviderReasoningLevels(providerModelMetadata?.supportedReasoningLevels);
|
||||
const supportsReasoning = providerModelMetadata?.supportsReasoningSummaries ?? (metadataReasoningLevels ? true : readCatalogCapability(capabilities, "reasoning"));
|
||||
const documentedReasoning = documentedReasoningProfile(providerModel);
|
||||
const resolvedReasoningLevels = metadataReasoningLevels
|
||||
?? documentedReasoning?.levels
|
||||
?? [];
|
||||
const supportsReasoning = providerModelMetadata?.supportsReasoningSummaries
|
||||
?? documentedReasoning?.supportsReasoning
|
||||
?? (metadataReasoningLevels !== undefined || readCatalogCapability(capabilities, "reasoning"));
|
||||
const supportsImageInput = supportsFusionVision || catalogEntrySupportsImageInput(catalogEntry);
|
||||
const supportsParallelToolCalls = readCatalogCapability(capabilities, "parallelFunctionCalling");
|
||||
const applyPatchToolType = providerSupportsResponses || catalogModelLooksLikeGpt(model, catalogEntry) || codexPatchBridgeApplies(model, catalogEntry, config)
|
||||
@@ -208,17 +214,19 @@ function codexModelCapabilityProfile(
|
||||
applyPatchToolType,
|
||||
catalogEntry,
|
||||
contextWindow: providerModelMetadata?.contextWindow,
|
||||
defaultReasoningLevel: providerModelMetadata && providerModelMetadata.defaultReasoningLevel !== undefined
|
||||
? providerModelMetadata.defaultReasoningLevel
|
||||
: supportsReasoning
|
||||
? "medium"
|
||||
: null,
|
||||
defaultReasoningLevel: resolveDefaultReasoningLevel(
|
||||
providerModelMetadata?.defaultReasoningLevel !== undefined
|
||||
? providerModelMetadata.defaultReasoningLevel
|
||||
: documentedReasoning?.defaultLevel,
|
||||
resolvedReasoningLevels,
|
||||
providerModelMetadata?.defaultReasoningLevel !== undefined || documentedReasoning !== undefined
|
||||
),
|
||||
defaultReasoningSummary: providerModelMetadata?.defaultReasoningSummary ?? "none",
|
||||
effectiveContextWindowPercent: providerModelMetadata?.effectiveContextWindowPercent,
|
||||
inputModalities: supportsImageInput ? ["text", "image"] : ["text"],
|
||||
serviceTiers: providerModelMetadata?.serviceTiers ?? [],
|
||||
maxContextWindow: providerModelMetadata?.maxContextWindow,
|
||||
supportedReasoningLevels: metadataReasoningLevels ?? (supportsReasoning ? supportedReasoningLevels(capabilities) : []),
|
||||
supportedReasoningLevels: resolvedReasoningLevels,
|
||||
supportsImageInput,
|
||||
supportsParallelToolCalls,
|
||||
supportsReasoning,
|
||||
@@ -267,13 +275,51 @@ function providerApiKey(provider: GatewayProviderConfig): string {
|
||||
}
|
||||
|
||||
function normalizeProviderReasoningLevels(levels: ProviderReasoningLevel[] | undefined): Array<{ description: string; effort: string }> | undefined {
|
||||
const normalized = (levels ?? [])
|
||||
.map((level) => ({
|
||||
description: level.description.trim() || effortDescription(level.effort),
|
||||
effort: level.effort.trim()
|
||||
}))
|
||||
.filter((level) => level.effort);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
if (levels === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const normalized: Array<{ description: string; effort: string }> = [];
|
||||
for (const level of levels) {
|
||||
const effort = level.effort.trim().toLowerCase();
|
||||
// Codex treats `none` as the absence of a reasoning selection and does not
|
||||
// render it in the effort menu, so do not publish it as a selectable level.
|
||||
if (!effort || effort === "none" || seen.has(effort)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(effort);
|
||||
normalized.push({
|
||||
description: level.description.trim() || effortDescription(effort),
|
||||
effort
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveDefaultReasoningLevel(
|
||||
configuredDefault: string | null | undefined,
|
||||
levels: Array<{ effort: string }>,
|
||||
hasConfiguredDefault: boolean
|
||||
): string | null {
|
||||
if (hasConfiguredDefault && configuredDefault === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedDefault = configuredDefault?.trim().toLowerCase();
|
||||
const configuredMatch = normalizedDefault
|
||||
? levels.find((level) => level.effort.toLowerCase() === normalizedDefault)
|
||||
: undefined;
|
||||
if (configuredMatch) {
|
||||
return configuredMatch.effort;
|
||||
}
|
||||
if (normalizedDefault === "none") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return levels.find((level) => level.effort === "medium")?.effort
|
||||
?? levels.find((level) => level.effort === "high")?.effort
|
||||
?? levels[0]?.effort
|
||||
?? null;
|
||||
}
|
||||
|
||||
function effortDescription(effort: string): string {
|
||||
@@ -281,6 +327,9 @@ function effortDescription(effort: string): string {
|
||||
if (normalized === "xhigh") {
|
||||
return "Extra high reasoning";
|
||||
}
|
||||
if (normalized === "ultra") {
|
||||
return "Maximum reasoning with automatic task delegation";
|
||||
}
|
||||
return `${effort.slice(0, 1).toUpperCase()}${effort.slice(1)} reasoning`;
|
||||
}
|
||||
|
||||
@@ -309,26 +358,100 @@ function catalogEntrySupportsImageInput(entry: ModelCatalogEntry | undefined): b
|
||||
readCatalogCapability(capabilities, "multimodal");
|
||||
}
|
||||
|
||||
function supportedReasoningLevels(capabilities: Record<string, unknown>): Array<{ description: string; effort: string }> {
|
||||
const levels: Array<{ description: string; effort: string }> = [];
|
||||
if (readCatalogCapability(capabilities, "noneReasoningEffort")) {
|
||||
levels.push({ effort: "none", description: "No reasoning" });
|
||||
type DocumentedReasoningProfile = {
|
||||
defaultLevel: string | null;
|
||||
levels: Array<{ description: string; effort: string }>;
|
||||
supportsReasoning: boolean;
|
||||
};
|
||||
|
||||
function documentedReasoningProfile(model: string): DocumentedReasoningProfile | undefined {
|
||||
const name = model.trim().toLowerCase().split("/").at(-1) ?? "";
|
||||
const profile = (efforts: string[], defaultLevel: string | null, supportsReasoning = true): DocumentedReasoningProfile => ({
|
||||
defaultLevel,
|
||||
levels: efforts
|
||||
.filter((effort) => effort !== "none")
|
||||
.map((effort) => ({ description: effortDescription(effort), effort })),
|
||||
supportsReasoning
|
||||
});
|
||||
|
||||
// OpenAI API model pages define the API effort values. Codex additionally
|
||||
// exposes its client-only Ultra mode for Sol/Terra, matching gateway metadata.
|
||||
if (/^gpt-5\.6(?:-(?:sol|terra|luna))?(?:-\d{4}-\d{2}-\d{2})?$/.test(name)) {
|
||||
const supportsUltra = !/^gpt-5\.6-luna(?:-|$)/.test(name);
|
||||
return profile([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
...(supportsUltra ? ["ultra"] : [])
|
||||
], "medium");
|
||||
}
|
||||
if (readCatalogCapability(capabilities, "minimalReasoningEffort")) {
|
||||
levels.push({ effort: "minimal", description: "Minimal reasoning" });
|
||||
if (/^gpt-5\.5(?:-\d{4}-\d{2}-\d{2})?$/.test(name)) {
|
||||
return profile(["none", "low", "medium", "high", "xhigh"], "medium");
|
||||
}
|
||||
levels.push(
|
||||
{ effort: "low", description: "Low reasoning" },
|
||||
{ effort: "medium", description: "Medium reasoning" },
|
||||
{ effort: "high", description: "High reasoning" }
|
||||
);
|
||||
if (readCatalogCapability(capabilities, "xhighReasoningEffort") || readCatalogCapability(capabilities, "maxReasoningEffort")) {
|
||||
levels.push({ effort: "xhigh", description: "Extra high reasoning" });
|
||||
if (/^gpt-5\.5-pro(?:-\d{4}-\d{2}-\d{2})?$/.test(name)) {
|
||||
return profile(["medium", "high", "xhigh"], "high");
|
||||
}
|
||||
if (readCatalogCapability(capabilities, "maxReasoningEffort")) {
|
||||
levels.push({ effort: "max", description: "Maximum reasoning" });
|
||||
if (/^gpt-5\.4(?:-(?:mini|nano))?(?:-\d{4}-\d{2}-\d{2})?$/.test(name)) {
|
||||
return profile(["none", "low", "medium", "high", "xhigh"], "none");
|
||||
}
|
||||
return levels;
|
||||
if (/^gpt-5\.4-pro(?:-\d{4}-\d{2}-\d{2})?$/.test(name)) {
|
||||
return profile(["medium", "high", "xhigh"], "medium");
|
||||
}
|
||||
if (/^gpt-5\.3-codex(?:-\d{4}-\d{2}-\d{2})?$/.test(name)) {
|
||||
return profile(["low", "medium", "high", "xhigh"], "medium");
|
||||
}
|
||||
|
||||
// Anthropic effort is distinct from extended-thinking on/off or token budgets.
|
||||
if (/^claude-(?:fable|mythos)-5(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high", "xhigh", "max"], "high");
|
||||
}
|
||||
if (/^claude-opus-4[.-](?:7|8)(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high", "xhigh", "max"], "high");
|
||||
}
|
||||
if (/^claude-(?:opus-4[.-]6|sonnet-4[.-]6)(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high", "max"], "high");
|
||||
}
|
||||
if (/^claude-opus-4[.-]5(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high"], "high");
|
||||
}
|
||||
if (/^claude-(?:sonnet|haiku)-4[.-]5(?:-|$)/.test(name)) {
|
||||
return profile([], null);
|
||||
}
|
||||
|
||||
// Gemini Interactions thinking levels. Gemini 2.5 defaults dynamically, so
|
||||
// a null default preserves the provider default until the user selects a level.
|
||||
if (/^gemini-3\.5-flash(?:-|$)/.test(name)) {
|
||||
return profile(["minimal", "low", "medium", "high"], "medium");
|
||||
}
|
||||
if (/^gemini-3(?:\.0)?-flash(?:-|$)/.test(name)) {
|
||||
return profile(["minimal", "low", "medium", "high"], "high");
|
||||
}
|
||||
if (/^gemini-3\.1-pro(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high"], "high");
|
||||
}
|
||||
if (/^gemini-2\.5-(?:pro|flash)(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high"], null);
|
||||
}
|
||||
|
||||
if (/^deepseek-v4-(?:flash|pro)(?:-free)?$/.test(name)) {
|
||||
return profile(["high", "max"], "high");
|
||||
}
|
||||
if (/^glm-5\.2(?:-|$)/.test(name)) {
|
||||
return profile(["none", "minimal", "low", "medium", "high", "xhigh", "max"], "max");
|
||||
}
|
||||
if (/^glm-(?:5(?:\.1|-turbo)?|4\.7|4\.5-air|5v-turbo)(?:-|$)/.test(name)) {
|
||||
return profile([], null);
|
||||
}
|
||||
if (/^kimi-(?:k2[.-](?:6|7)(?:-code)?|for-coding)(?:-|$)/.test(name)) {
|
||||
return profile([], null);
|
||||
}
|
||||
if (/^grok-4[.-]5(?:-|$)/.test(name)) {
|
||||
return profile(["low", "medium", "high"], "high");
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findConfiguredProvider(
|
||||
@@ -651,7 +774,8 @@ function normalizeModelSelector(value: string | undefined): string {
|
||||
|
||||
function pushUniqueModel(models: string[], model: string | undefined): void {
|
||||
const normalized = model?.trim();
|
||||
if (normalized && !models.includes(normalized)) {
|
||||
const dedupeKey = normalized?.toLowerCase();
|
||||
if (normalized && dedupeKey && !models.some((candidate) => candidate.toLowerCase() === dedupeKey)) {
|
||||
models.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ function fallbackModelCatalogEntry(model: string): ModelCatalogEntry | undefined
|
||||
capabilities: {
|
||||
functionCalling: true,
|
||||
imageInput: true,
|
||||
lowReasoningEffort: true,
|
||||
maxReasoningEffort: true,
|
||||
noneReasoningEffort: true,
|
||||
parallelFunctionCalling: true,
|
||||
@@ -79,6 +80,7 @@ function fallbackModelCatalogEntry(model: string): ModelCatalogEntry | undefined
|
||||
structuredOutput: true,
|
||||
supports1MContext: true,
|
||||
toolCalling: true,
|
||||
ultraReasoningEffort: !/^gpt-5\.6-luna(?:-|$)/.test(modelName),
|
||||
vision: true,
|
||||
webSearch: true,
|
||||
xhighReasoningEffort: true
|
||||
|
||||
@@ -38,6 +38,7 @@ const managedProviderStart = "# BEGIN CCR managed Codex provider";
|
||||
const managedProviderEnd = "# END CCR managed Codex provider";
|
||||
const managedToolHubMcpStart = "# BEGIN CCR managed ToolHub MCP";
|
||||
const managedToolHubMcpEnd = "# END CCR managed ToolHub MCP";
|
||||
const managedConfiguredModelPrefix = "# CCR configured model = ";
|
||||
const originalBackupSuffix = ".ccr-original";
|
||||
const originalMissingSuffix = ".ccr-original-missing";
|
||||
const globalProfileTakeoverFile = path.join(CONFIGDIR, "global-profile-takeover.json");
|
||||
@@ -737,9 +738,14 @@ function buildCodexConfigToml(
|
||||
toolHubMcp?: ToolHubMcpRuntimeConfig;
|
||||
}
|
||||
): string {
|
||||
let content = removeManagedBlock(source, managedRootStart, managedRootEnd);
|
||||
content = removeManagedBlock(content, managedProviderStart, managedProviderEnd);
|
||||
content = removeManagedBlock(content, managedToolHubMcpStart, managedToolHubMcpEnd);
|
||||
let content = removeManagedMarkerLines(source, [
|
||||
managedRootStart,
|
||||
managedRootEnd,
|
||||
managedProviderStart,
|
||||
managedProviderEnd,
|
||||
managedToolHubMcpStart,
|
||||
managedToolHubMcpEnd
|
||||
]);
|
||||
content = removeCodexProviderTable(content, values.providerId);
|
||||
content = removeCodexMcpServerTable(content, TOOL_HUB_MCP_SERVER_NAME);
|
||||
if (values.configFormat === "separate_profile_files") {
|
||||
@@ -749,13 +755,20 @@ function buildCodexConfigToml(
|
||||
const firstTableIndex = firstTomlTableIndex(content);
|
||||
const rootSource = firstTableIndex === -1 ? content : content.slice(0, firstTableIndex);
|
||||
const restSource = firstTableIndex === -1 ? "" : content.slice(firstTableIndex);
|
||||
const cleanedRoot = removeRootTomlKeys(rootSource, ["model", "model_catalog_json", "model_provider", "profile", "show_all_sessions"]);
|
||||
const modelAssignment = managedModelAssignment(rootSource, values.model);
|
||||
const showAllSessionsAssignment = rootTomlAssignment(rootSource, "show_all_sessions")
|
||||
?? (values.showAllSessions ? "show_all_sessions = true" : undefined);
|
||||
const cleanedRoot = removeManagedConfiguredModelLine(removeRootTomlKeys(
|
||||
rootSource,
|
||||
["model", "model_catalog_json", "model_provider", "show_all_sessions"]
|
||||
));
|
||||
const rootBlock = [
|
||||
managedRootStart,
|
||||
`model_provider = ${tomlString(values.providerId)}`,
|
||||
`model = ${tomlString(values.model)}`,
|
||||
modelAssignment,
|
||||
`model_catalog_json = ${tomlString(values.modelCatalogFile)}`,
|
||||
...(values.showAllSessions ? ["show_all_sessions = true"] : []),
|
||||
`${managedConfiguredModelPrefix}${tomlString(values.model)}`,
|
||||
...(showAllSessionsAssignment ? [showAllSessionsAssignment] : []),
|
||||
managedRootEnd,
|
||||
""
|
||||
].join("\n");
|
||||
@@ -831,12 +844,18 @@ function buildSeparateCodexProfileToml(
|
||||
const firstTableIndex = firstTomlTableIndex(source);
|
||||
const rootSource = firstTableIndex === -1 ? source : source.slice(0, firstTableIndex);
|
||||
const restSource = firstTableIndex === -1 ? "" : source.slice(firstTableIndex);
|
||||
const cleanedRoot = removeRootTomlKeys(rootSource, ["model", "model_provider", "model_reasoning_effort", "show_all_sessions"]);
|
||||
const modelAssignment = managedModelAssignment(rootSource, values.model);
|
||||
const showAllSessionsAssignment = rootTomlAssignment(rootSource, "show_all_sessions")
|
||||
?? (values.showAllSessions ? "show_all_sessions = true" : undefined);
|
||||
const cleanedRoot = removeManagedConfiguredModelLine(removeRootTomlKeys(
|
||||
rootSource,
|
||||
["model", "model_provider", "show_all_sessions"]
|
||||
));
|
||||
const rootBlock = [
|
||||
`model_provider = ${tomlString(values.providerId)}`,
|
||||
`model = ${tomlString(values.model)}`,
|
||||
`model_reasoning_effort = "xhigh"`,
|
||||
...(values.showAllSessions ? ["show_all_sessions = true"] : []),
|
||||
modelAssignment,
|
||||
`${managedConfiguredModelPrefix}${tomlString(values.model)}`,
|
||||
...(showAllSessionsAssignment ? [showAllSessionsAssignment] : []),
|
||||
""
|
||||
].join("\n");
|
||||
return ensureTrailingNewline(`${rootBlock}${trimLeadingBlankLines(cleanedRoot)}${restSource}`.replace(/\n{4,}/g, "\n\n\n"));
|
||||
@@ -1559,6 +1578,32 @@ function removeRootTomlKeys(source: string, keys: string[]): string {
|
||||
return source.replace(pattern, "");
|
||||
}
|
||||
|
||||
function rootTomlAssignment(source: string, key: string): string | undefined {
|
||||
const rootEnd = firstTomlTableIndex(source);
|
||||
const rootSource = rootEnd === -1 ? source : source.slice(0, rootEnd);
|
||||
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=.*$`, "m");
|
||||
return rootSource.match(pattern)?.[0].trim();
|
||||
}
|
||||
|
||||
function managedModelAssignment(source: string, configuredModel: string): string {
|
||||
const currentAssignment = rootTomlAssignment(source, "model");
|
||||
const previousConfiguredModel = managedConfiguredModel(source);
|
||||
const configuredModelChanged = previousConfiguredModel !== undefined && previousConfiguredModel !== tomlString(configuredModel);
|
||||
return currentAssignment && !configuredModelChanged
|
||||
? currentAssignment
|
||||
: `model = ${tomlString(configuredModel)}`;
|
||||
}
|
||||
|
||||
function managedConfiguredModel(source: string): string | undefined {
|
||||
const pattern = new RegExp(`^\\s*${escapeRegExp(managedConfiguredModelPrefix)}(.+?)\\s*$`, "m");
|
||||
return source.match(pattern)?.[1];
|
||||
}
|
||||
|
||||
function removeManagedConfiguredModelLine(source: string): string {
|
||||
const pattern = new RegExp(`^\\s*${escapeRegExp(managedConfiguredModelPrefix)}.*(?:\\n|$)`, "gm");
|
||||
return source.replace(pattern, "");
|
||||
}
|
||||
|
||||
function removeCodexProviderTable(source: string, providerId: string): string {
|
||||
return removeTomlTable(source, "model_providers", providerId);
|
||||
}
|
||||
@@ -1638,9 +1683,10 @@ function legacyCodexProfileTableBody(source: string, providerId: string): string
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
|
||||
function removeManagedBlock(source: string, start: string, end: string): string {
|
||||
const pattern = new RegExp(`\\n?${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "g");
|
||||
return source.replace(pattern, "\n");
|
||||
function removeManagedMarkerLines(source: string, markers: string[]): string {
|
||||
const markerPattern = markers.map(escapeRegExp).join("|");
|
||||
const pattern = new RegExp(`^\\s*(?:${markerPattern})\\s*(?:\\n|$)`, "gm");
|
||||
return source.replace(pattern, "");
|
||||
}
|
||||
|
||||
function firstTomlTableIndex(source: string): number {
|
||||
|
||||
@@ -270,6 +270,7 @@ test("profile service injects ToolHub MCP into Codex config", { skip: !process.e
|
||||
const configFile = path.join(CONFIGDIR, "profiles", profileId, "codex", "config.toml");
|
||||
const content = readFileSync(configFile, "utf8");
|
||||
assert.match(content, /# BEGIN CCR managed ToolHub MCP/);
|
||||
assert.match(content, /# CCR configured model = "Provider\/model"/);
|
||||
assert.match(content, /\[mcp_servers\.ccr-toolhub\]/);
|
||||
assert.equal(content.includes(`command = ${JSON.stringify(process.execPath)}`), true);
|
||||
assert.equal(content.includes(`args = [${JSON.stringify(path.join(CONFIGDIR, "bin", "toolhub-mcp.js"))}]`), true);
|
||||
@@ -277,6 +278,64 @@ test("profile service injects ToolHub MCP into Codex config", { skip: !process.e
|
||||
assert.match(content, /TOOLHUB_OPENAI_API_KEY = "ccr-codex-profile-test"/);
|
||||
assert.match(content, new RegExp(`TOOLHUB_OPENAI_BASE_URL = "http://127\\.0\\.0\\.1:${config.gateway.port}/v1"`));
|
||||
assert.match(content, /TOOLHUB_OPENAI_MODEL = "Provider\/model"/);
|
||||
|
||||
const separateProfileFile = path.join(path.dirname(configFile), "claude-code-router.config.toml");
|
||||
const initialSeparateProfile = readFileSync(separateProfileFile, "utf8");
|
||||
assert.equal(initialSeparateProfile.includes("model_reasoning_effort"), false);
|
||||
|
||||
const codexEditedConfig = content
|
||||
.replace(
|
||||
'model = "Provider/model"',
|
||||
'model = "User/selected-in-codex"\nmodel_reasoning_effort = "max"'
|
||||
)
|
||||
.replace(
|
||||
"# END CCR managed ToolHub MCP",
|
||||
[
|
||||
"[desktop]",
|
||||
'followUpQueueMode = "steer"',
|
||||
"",
|
||||
'[plugins."browser@openai-bundled"]',
|
||||
"enabled = false",
|
||||
"",
|
||||
"[features]",
|
||||
"js_repl = true",
|
||||
"# END CCR managed ToolHub MCP"
|
||||
].join("\n")
|
||||
);
|
||||
writeFileSync(configFile, codexEditedConfig);
|
||||
writeFileSync(
|
||||
separateProfileFile,
|
||||
initialSeparateProfile
|
||||
.replace('model = "Provider/model"', 'model = "User/selected-in-cli"')
|
||||
.replace(/\s*$/, '\nmodel_reasoning_effort = "ultra"\n')
|
||||
);
|
||||
|
||||
await applyProfileConfig(config);
|
||||
|
||||
const preservedConfig = readFileSync(configFile, "utf8");
|
||||
assert.match(preservedConfig, /model = "User\/selected-in-codex"/);
|
||||
assert.match(preservedConfig, /model_reasoning_effort = "max"/);
|
||||
assert.match(preservedConfig, /\[desktop\]\nfollowUpQueueMode = "steer"/);
|
||||
assert.match(preservedConfig, /\[plugins\."browser@openai-bundled"\]\nenabled = false/);
|
||||
assert.match(preservedConfig, /\[features\]\njs_repl = true/);
|
||||
assert.equal((preservedConfig.match(/\[mcp_servers\.ccr-toolhub\]/g) ?? []).length, 1);
|
||||
assert.equal((preservedConfig.match(/# BEGIN CCR managed ToolHub MCP/g) ?? []).length, 1);
|
||||
|
||||
const preservedSeparateProfile = readFileSync(separateProfileFile, "utf8");
|
||||
assert.match(preservedSeparateProfile, /model = "User\/selected-in-cli"/);
|
||||
assert.match(preservedSeparateProfile, /model_reasoning_effort = "ultra"/);
|
||||
|
||||
config.Providers[0].models.push("model-2");
|
||||
config.profile.profiles[0].model = "Provider/model-2";
|
||||
await applyProfileConfig(config);
|
||||
|
||||
const explicitlyUpdatedConfig = readFileSync(configFile, "utf8");
|
||||
assert.match(explicitlyUpdatedConfig, /model = "Provider\/model-2"/);
|
||||
assert.match(explicitlyUpdatedConfig, /model_reasoning_effort = "max"/);
|
||||
assert.match(explicitlyUpdatedConfig, /\[desktop\]\nfollowUpQueueMode = "steer"/);
|
||||
const explicitlyUpdatedSeparateProfile = readFileSync(separateProfileFile, "utf8");
|
||||
assert.match(explicitlyUpdatedSeparateProfile, /model = "Provider\/model-2"/);
|
||||
assert.match(explicitlyUpdatedSeparateProfile, /model_reasoning_effort = "ultra"/);
|
||||
});
|
||||
|
||||
test("profile service writes a Grok CLI wrapper that points model discovery and inference to CCR", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { buildCodexModelCatalog } from "@ccr/core/agents/codex/model-catalog.ts";
|
||||
import { buildCodexModelCatalog, buildCodexModelCatalogIds } from "@ccr/core/agents/codex/model-catalog.ts";
|
||||
|
||||
function catalogModelFor(config, slug) {
|
||||
const catalog = buildCodexModelCatalog(config, slug);
|
||||
@@ -12,6 +12,16 @@ function catalogModelFor(config, slug) {
|
||||
return model;
|
||||
}
|
||||
|
||||
test("codex catalog removes duplicate model IDs case-insensitively without reordering", () => {
|
||||
const ids = buildCodexModelCatalogIds({
|
||||
Providers: [
|
||||
{ name: "Provider", type: "openai_responses", models: ["Model-A", " model-a ", "MODEL-B"] }
|
||||
]
|
||||
}, "provider/model-a");
|
||||
|
||||
assert.deepEqual(ids, ["provider/model-a", "Provider/MODEL-B"]);
|
||||
});
|
||||
|
||||
test("codex catalog treats unknown models as text-only without advanced tools", () => {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
@@ -59,10 +69,12 @@ test("codex catalog enables multimodal reasoning and search when provider protoc
|
||||
assert.equal(model.supports_reasoning_summaries, true);
|
||||
assert.equal(model.supports_search_tool, true);
|
||||
assert.equal(model.web_search_tool_type, "text_and_image");
|
||||
for (const effort of ["low", "medium", "high"]) {
|
||||
assert.ok(model.supported_reasoning_levels.some((level) => level.effort === effort));
|
||||
}
|
||||
assert.equal(model.default_reasoning_level, "medium");
|
||||
assert.deepEqual(model.supported_reasoning_levels.map((level) => level.effort), [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]);
|
||||
assert.equal(model.default_reasoning_level, null);
|
||||
assert.equal(model.apply_patch_tool_type, "freeform");
|
||||
});
|
||||
|
||||
@@ -80,18 +92,143 @@ test("codex catalog exposes current capabilities for the GPT-5.6 family", () =>
|
||||
assert.equal(model.supports_image_detail_original, true);
|
||||
assert.equal(model.supports_parallel_tool_calls, true);
|
||||
assert.equal(model.supports_reasoning_summaries, true);
|
||||
assert.deepEqual(model.supported_reasoning_levels.map((level) => level.effort), [
|
||||
"none",
|
||||
const efforts = model.supported_reasoning_levels.map((level) => level.effort);
|
||||
assert.deepEqual(efforts, [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max"
|
||||
"max",
|
||||
...(/-luna$/.test(modelName) ? [] : ["ultra"])
|
||||
]);
|
||||
assert.equal(new Set(efforts).size, efforts.length);
|
||||
assert.equal(model.default_reasoning_level, "medium");
|
||||
}
|
||||
});
|
||||
|
||||
test("codex catalog uses documented reasoning levels instead of aggregate capability guesses", () => {
|
||||
const cases = [
|
||||
["abacus", "mimo-v2-pro", [], null],
|
||||
["x-ai", "grok-4.5", ["low", "medium", "high"], "high"],
|
||||
["z-ai", "glm-4.5-air", [], null],
|
||||
["z-ai", "glm-5.2", ["minimal", "low", "medium", "high", "xhigh", "max"], "max"],
|
||||
["google", "gemini-3.5-flash", ["minimal", "low", "medium", "high"], "medium"],
|
||||
["deepseek", "deepseek-v4-flash", ["high", "max"], "high"],
|
||||
["deepseek", "deepseek-v4-pro", ["high", "max"], "high"],
|
||||
["opencode", "deepseek-v4-flash-free", ["high", "max"], "high"],
|
||||
["kimi", "kimi-k2.7-code", [], null]
|
||||
];
|
||||
|
||||
for (const [provider, modelName, expectedEfforts, expectedDefault] of cases) {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
{ name: provider, type: "openai_responses", models: [modelName] }
|
||||
]
|
||||
}, `${provider}/${modelName}`);
|
||||
|
||||
assert.deepEqual(model.supported_reasoning_levels.map((level) => level.effort), expectedEfforts);
|
||||
assert.equal(new Set(expectedEfforts).size, expectedEfforts.length);
|
||||
assert.equal(model.default_reasoning_level, expectedDefault);
|
||||
}
|
||||
});
|
||||
|
||||
test("codex catalog follows Anthropic's model-specific effort matrix", () => {
|
||||
const cases = [
|
||||
["claude-fable-5", ["low", "medium", "high", "xhigh", "max"]],
|
||||
["claude-opus-4.8", ["low", "medium", "high", "xhigh", "max"]],
|
||||
["claude-opus-4-7", ["low", "medium", "high", "xhigh", "max"]],
|
||||
["claude-opus-4-6", ["low", "medium", "high", "max"]],
|
||||
["claude-sonnet-4.6", ["low", "medium", "high", "max"]],
|
||||
["claude-opus-4-5", ["low", "medium", "high"]],
|
||||
["claude-sonnet-4-5-20250929", []],
|
||||
["claude-haiku-4-5", []]
|
||||
];
|
||||
|
||||
for (const [modelName, expectedEfforts] of cases) {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
{ name: "anthropic", type: "anthropic_messages", models: [modelName] }
|
||||
]
|
||||
}, `anthropic/${modelName}`);
|
||||
|
||||
assert.deepEqual(model.supported_reasoning_levels.map((level) => level.effort), expectedEfforts);
|
||||
assert.equal(model.default_reasoning_level, expectedEfforts.length > 0 ? "high" : null);
|
||||
}
|
||||
});
|
||||
|
||||
test("codex catalog follows official OpenAI API reasoning levels for custom providers", () => {
|
||||
const cases = [
|
||||
["gpt-5.5", ["low", "medium", "high", "xhigh"], "medium"],
|
||||
["gpt-5.4", ["low", "medium", "high", "xhigh"], null],
|
||||
["gpt-5.4-mini", ["low", "medium", "high", "xhigh"], null],
|
||||
["gpt-5.3-codex", ["low", "medium", "high", "xhigh"], "medium"]
|
||||
];
|
||||
|
||||
for (const [modelName, expectedEfforts, expectedDefault] of cases) {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
{ name: "custom", type: "openai_responses", models: [modelName] }
|
||||
]
|
||||
}, `custom/${modelName}`);
|
||||
|
||||
assert.deepEqual(model.supported_reasoning_levels.map((level) => level.effort), expectedEfforts);
|
||||
assert.equal(model.default_reasoning_level, expectedDefault);
|
||||
}
|
||||
});
|
||||
|
||||
test("codex catalog preserves explicit reasoning metadata while removing duplicate efforts", () => {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
{
|
||||
modelMetadata: {
|
||||
"custom-reasoner": {
|
||||
defaultReasoningLevel: "unsupported",
|
||||
supportedReasoningLevels: [
|
||||
{ description: "No reasoning", effort: "none" },
|
||||
{ description: "First low", effort: " LOW " },
|
||||
{ description: "Duplicate low", effort: "low" },
|
||||
{ description: "High", effort: "HIGH" }
|
||||
],
|
||||
supportsReasoningSummaries: true
|
||||
}
|
||||
},
|
||||
models: ["custom-reasoner"],
|
||||
name: "custom",
|
||||
type: "openai_responses"
|
||||
}
|
||||
]
|
||||
}, "custom/custom-reasoner");
|
||||
|
||||
assert.deepEqual(model.supported_reasoning_levels, [
|
||||
{ description: "First low", effort: "low" },
|
||||
{ description: "High", effort: "high" }
|
||||
]);
|
||||
assert.equal(model.default_reasoning_level, "high");
|
||||
});
|
||||
|
||||
test("codex catalog preserves an explicit empty provider reasoning-level list", () => {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
{
|
||||
modelMetadata: {
|
||||
"gpt-5.6-sol": {
|
||||
defaultReasoningLevel: null,
|
||||
supportedReasoningLevels: [],
|
||||
supportsReasoningSummaries: false
|
||||
}
|
||||
},
|
||||
models: ["gpt-5.6-sol"],
|
||||
name: "custom",
|
||||
type: "openai_responses"
|
||||
}
|
||||
]
|
||||
}, "custom/gpt-5.6-sol");
|
||||
|
||||
assert.deepEqual(model.supported_reasoning_levels, []);
|
||||
assert.equal(model.default_reasoning_level, null);
|
||||
assert.equal(model.supports_reasoning_summaries, false);
|
||||
});
|
||||
|
||||
test("codex catalog uses provider model metadata for reasoning effort and speed tiers", () => {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
|
||||
Reference in New Issue
Block a user