fix(coding-agent): preserved max model globs

Kept max as a selector alias only after literal model lookup misses and left scoped globs matching literal :max model ids.\n\nFixes #2727
This commit is contained in:
roboomp
2026-06-16 01:32:49 +00:00
parent 83a5260957
commit 46a9867773
4 changed files with 87 additions and 13 deletions
@@ -53,6 +53,16 @@ export interface ScopedModel {
explicitThinkingLevel: boolean;
}
interface ThinkingSuffixOptions {
allowMaxAlias?: boolean;
}
function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): ThinkingLevel | undefined {
const level = parseThinkingLevel(value);
if (level !== undefined) return level;
return options?.allowMaxAlias === true && value === "max" ? ThinkingLevel.XHigh : undefined;
}
/**
* Split a trailing `:<level>` thinking selector off a model pattern.
*
@@ -62,10 +72,14 @@ export interface ScopedModel {
* role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least
* as long as the `pi/` prefix.
*/
function splitThinkingSuffix(pattern: string, minColonIndex = -1): { base: string; level?: ThinkingLevel } {
function splitThinkingSuffix(
pattern: string,
minColonIndex = -1,
options?: ThinkingSuffixOptions,
): { base: string; level?: ThinkingLevel } {
const colonIdx = pattern.lastIndexOf(":");
if (colonIdx <= minColonIndex) return { base: pattern };
const level = parseThinkingLevel(pattern.slice(colonIdx + 1));
const level = parseThinkingSuffix(pattern.slice(colonIdx + 1), options);
return level ? { base: pattern.slice(0, colonIdx), level } : { base: pattern };
}
@@ -574,8 +588,10 @@ function parseModelPatternWithContext(
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
}
// No match - try stripping a valid thinking suffix and recursing
const { base, level } = splitThinkingSuffix(pattern);
// No match - try stripping a valid thinking suffix and recursing.
// `max` is accepted only after the full pattern failed, so literal model IDs
// ending in `:max` keep winning over the alias.
const { base, level } = splitThinkingSuffix(pattern, -1, { allowMaxAlias: true });
if (level) {
const result = parseModelPatternWithContext(base, availableModels, context, options);
if (result.model) {
+1 -1
View File
@@ -53,7 +53,6 @@ const THINKING_LEVEL_BY_SELECTOR: Readonly<Record<string, ThinkingLevel>> = {
[ThinkingLevel.Medium]: ThinkingLevel.Medium,
[ThinkingLevel.High]: ThinkingLevel.High,
[ThinkingLevel.XHigh]: ThinkingLevel.XHigh,
max: ThinkingLevel.XHigh,
};
/**
@@ -141,6 +140,7 @@ const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
*/
export function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined {
if (value === AUTO_THINKING) return AUTO_THINKING;
if (value === "max") return ThinkingLevel.XHigh;
return parseThinkingLevel(value);
}
@@ -3,7 +3,6 @@ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { Effort } from "@oh-my-pi/pi-ai";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import { parseDifficultyBucket, parseDifficultyLevel } from "@oh-my-pi/pi-coding-agent/auto-thinking/classifier";
import { parseModelString } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
import {
AUTO_THINKING,
clampAutoThinkingEffort,
@@ -44,14 +43,9 @@ describe("auto thinking classifier helpers", () => {
expect(clampAutoThinkingEffort(model, Effort.Minimal)).toBe(Effort.Low);
});
it("accepts max as the top thinking selector alias", () => {
it("accepts max as the top configured thinking alias", () => {
expect(parseEffort("max")).toBe(Effort.XHigh);
expect(parseThinkingLevel("max")).toBe(ThinkingLevel.XHigh);
expect(parseThinkingLevel("max")).toBeUndefined();
expect(parseConfiguredThinkingLevel("max")).toBe(ThinkingLevel.XHigh);
expect(parseModelString("deepseek/deepseek-v4-pro:max")).toEqual({
provider: "deepseek",
id: "deepseek-v4-pro",
thinkingLevel: ThinkingLevel.XHigh,
});
});
});
@@ -96,6 +96,37 @@ const mockOpenRouterModels: Model<Api>[] = [
}),
];
const mockMaxSuffixModels: Model<Api>[] = [
buildModel({
id: "coding-router",
name: "NanoGPT Coding Router",
api: "openai-completions",
provider: "nanogpt",
baseUrl: "https://nano-gpt.com/api/v1",
reasoning: true,
thinking: {
mode: "effort",
efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],
},
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
}),
buildModel({
id: "coding-router:max",
name: "NanoGPT Coding Router Max",
api: "openai-completions",
provider: "nanogpt",
baseUrl: "https://nano-gpt.com/api/v1",
reasoning: false,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
}),
];
const mockProviderOverlapModels: Model<"anthropic-messages">[] = [
buildModel({
id: "kimi-k2.5",
@@ -288,6 +319,21 @@ describe("parseModelPattern", () => {
expect(result.warning).toBeUndefined();
}
});
test("max aliases the highest thinking level after the literal pattern misses", () => {
const result = parseModelPattern("gpt-5.3-codex:max", allModels);
expect(result.model?.id).toBe("gpt-5.3-codex");
expect(result.thinkingLevel).toBe(Effort.XHigh);
expect(result.explicitThinkingLevel).toBe(true);
expect(result.warning).toBeUndefined();
});
test("literal model ids ending in max win over the thinking alias", () => {
const result = parseModelPattern("nanogpt/coding-router:max", mockMaxSuffixModels);
expect(result.model?.id).toBe("coding-router:max");
expect(result.thinkingLevel).toBeUndefined();
expect(result.explicitThinkingLevel).toBe(false);
expect(result.warning).toBeUndefined();
});
});
describe("patterns with invalid thinking levels", () => {
@@ -856,6 +902,19 @@ describe("resolveModelScope", () => {
"github-copilot/anthropic/claude-sonnet-4.5",
]);
});
test("preserves literal :max in scoped-model globs", async () => {
const registry = {
getAvailable: () => mockMaxSuffixModels,
getCanonicalVariants: (_id: string, _opts?: unknown): CanonicalModelVariant[] => [],
};
const scoped = await resolveModelScope(["nanogpt/*:max"], registry);
expect(scoped).toHaveLength(1);
expect(scoped[0].model.id).toBe("coding-router:max");
expect(scoped[0].thinkingLevel).toBeUndefined();
expect(scoped[0].explicitThinkingLevel).toBe(false);
});
});
describe("parseModelString", () => {
@@ -1085,6 +1144,11 @@ describe("filterAvailableModelsByEnabledPatterns", () => {
expect(result).toHaveLength(1);
expect(result[0].provider).toBe("anthropic");
});
test("preserves literal :max in enabledModels globs", () => {
const result = filterAvailableModelsByEnabledPatterns(mockMaxSuffixModels, ["nanogpt/*:max"], registry);
expect(result).toHaveLength(1);
expect(result[0].id).toBe("coding-router:max");
});
test("evaluates glob patterns against bare model id", () => {
const result = filterAvailableModelsByEnabledPatterns(models, ["claude-*"], registry);