2e001bbf8d
- Added formatRoleThinkingModeLabel helper to display 'inherit' for default thinking mode, preventing badge ambiguity when multiple roles share the same model. Enhanced role menu labels to include role tags for clarity. Fixed model resolver to avoid substring matching that could incorrectly resolve exact model IDs to similar variants.
546 lines
20 KiB
TypeScript
546 lines
20 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
import {
|
|
parseModelPattern,
|
|
parseModelString,
|
|
resolveCliModel,
|
|
resolveModelFromString,
|
|
resolveModelOverride,
|
|
resolveModelRoleValue,
|
|
} from "@oh-my-pi/pi-coding-agent/config/model-resolver";
|
|
|
|
// Mock models for testing
|
|
const mockModels: Model<"anthropic-messages">[] = [
|
|
{
|
|
id: "claude-sonnet-4-5",
|
|
name: "Claude Sonnet 4.5",
|
|
api: "anthropic-messages",
|
|
provider: "anthropic",
|
|
baseUrl: "https://api.anthropic.com",
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
contextWindow: 200000,
|
|
maxTokens: 8192,
|
|
},
|
|
{
|
|
id: "gpt-4o",
|
|
name: "GPT-4o",
|
|
api: "anthropic-messages", // Using same type for simplicity
|
|
provider: "openai",
|
|
baseUrl: "https://api.openai.com",
|
|
reasoning: false,
|
|
input: ["text", "image"],
|
|
cost: { input: 5, output: 15, cacheRead: 0.5, cacheWrite: 5 },
|
|
contextWindow: 128000,
|
|
maxTokens: 4096,
|
|
},
|
|
];
|
|
|
|
// Mock OpenRouter models with colons in IDs
|
|
const mockOpenRouterModels: Model<"anthropic-messages">[] = [
|
|
{
|
|
id: "qwen/qwen3-coder:exacto",
|
|
name: "Qwen3 Coder Exacto",
|
|
api: "anthropic-messages",
|
|
provider: "openrouter",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 },
|
|
contextWindow: 128000,
|
|
maxTokens: 8192,
|
|
},
|
|
{
|
|
id: "openai/gpt-4o:extended",
|
|
name: "GPT-4o Extended",
|
|
api: "anthropic-messages",
|
|
provider: "openrouter",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
reasoning: false,
|
|
input: ["text", "image"],
|
|
cost: { input: 5, output: 15, cacheRead: 0.5, cacheWrite: 5 },
|
|
contextWindow: 128000,
|
|
maxTokens: 4096,
|
|
},
|
|
];
|
|
|
|
const mockProviderOverlapModels: Model<"anthropic-messages">[] = [
|
|
{
|
|
id: "kimi-k2.5",
|
|
name: "Kimi K2.5",
|
|
api: "anthropic-messages",
|
|
provider: "kimi-code",
|
|
baseUrl: "https://api.kimi.ai",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 2 },
|
|
contextWindow: 128000,
|
|
maxTokens: 8192,
|
|
},
|
|
{
|
|
id: "moonshotai/kimi-k2.5",
|
|
name: "Kimi K2.5 (OpenRouter)",
|
|
api: "anthropic-messages",
|
|
provider: "openrouter",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 2.2, output: 6.2, cacheRead: 0.22, cacheWrite: 2.2 },
|
|
contextWindow: 128000,
|
|
maxTokens: 8192,
|
|
},
|
|
];
|
|
|
|
const mockCodexOverlapModels: Model<"anthropic-messages">[] = [
|
|
{
|
|
id: "gpt-5.3-codex",
|
|
name: "GPT-5.3 Codex",
|
|
api: "anthropic-messages",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://api.openai.com",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 1.5, output: 6, cacheRead: 0.15, cacheWrite: 1.5 },
|
|
contextWindow: 200000,
|
|
maxTokens: 8192,
|
|
},
|
|
{
|
|
id: "gpt-5.3-codex-spark",
|
|
name: "GPT-5.3 Codex Spark",
|
|
api: "anthropic-messages",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://api.openai.com",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 },
|
|
contextWindow: 200000,
|
|
maxTokens: 8192,
|
|
},
|
|
];
|
|
|
|
const allModels = [...mockModels, ...mockOpenRouterModels, ...mockProviderOverlapModels, ...mockCodexOverlapModels];
|
|
|
|
describe("parseModelPattern", () => {
|
|
describe("simple patterns without colons", () => {
|
|
test("exact match returns model with undefined thinking level", () => {
|
|
const result = parseModelPattern("claude-sonnet-4-5", allModels);
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("partial match returns best model with undefined thinking level", () => {
|
|
const result = parseModelPattern("sonnet", allModels);
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("no match returns undefined model and thinking level", () => {
|
|
const result = parseModelPattern("nonexistent", allModels);
|
|
expect(result.model).toBeUndefined();
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("patterns with valid thinking levels", () => {
|
|
test("sonnet:high returns sonnet with high thinking level", () => {
|
|
const result = parseModelPattern("sonnet:high", allModels);
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBe("high");
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("gpt-4o:medium returns gpt-4o with medium thinking level", () => {
|
|
const result = parseModelPattern("gpt-4o:medium", allModels);
|
|
expect(result.model?.id).toBe("gpt-4o");
|
|
expect(result.thinkingLevel).toBe("medium");
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("all valid thinking levels work", () => {
|
|
const levels = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
for (const level of levels) {
|
|
const result = parseModelPattern(`sonnet:${level}`, allModels);
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBe(level);
|
|
expect(result.warning).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("patterns with invalid thinking levels", () => {
|
|
test("sonnet:random returns sonnet with undefined thinking level and warning", () => {
|
|
const result = parseModelPattern("sonnet:random", allModels);
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toContain("Invalid thinking level");
|
|
expect(result.warning).toContain("random");
|
|
});
|
|
|
|
test("gpt-4o:invalid returns gpt-4o with undefined thinking level and warning", () => {
|
|
const result = parseModelPattern("gpt-4o:invalid", allModels);
|
|
expect(result.model?.id).toBe("gpt-4o");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toContain("Invalid thinking level");
|
|
});
|
|
});
|
|
|
|
describe("OpenRouter models with colons in IDs", () => {
|
|
test("qwen3-coder:exacto matches the model with undefined thinking level", () => {
|
|
const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels);
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("openrouter/qwen/qwen3-coder:exacto matches with provider prefix", () => {
|
|
const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto", allModels);
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("qwen3-coder:exacto:high matches model with high thinking level", () => {
|
|
const result = parseModelPattern("qwen/qwen3-coder:exacto:high", allModels);
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBe("high");
|
|
expect(result.explicitThinkingLevel).toBe(true);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("openrouter/qwen/qwen3-coder:exacto:high matches with provider and thinking level", () => {
|
|
const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto:high", allModels);
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.thinkingLevel).toBe("high");
|
|
expect(result.explicitThinkingLevel).toBe(true);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("gpt-4o:extended matches the extended model with undefined thinking level", () => {
|
|
const result = parseModelPattern("openai/gpt-4o:extended", allModels);
|
|
expect(result.model?.id).toBe("openai/gpt-4o:extended");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("invalid thinking levels with OpenRouter models", () => {
|
|
test("qwen3-coder:exacto:random returns model with undefined thinking level and warning", () => {
|
|
const result = parseModelPattern("qwen/qwen3-coder:exacto:random", allModels);
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toContain("Invalid thinking level");
|
|
expect(result.warning).toContain("random");
|
|
});
|
|
|
|
test("qwen3-coder:exacto:high:random returns model with undefined thinking level and warning", () => {
|
|
const result = parseModelPattern("qwen/qwen3-coder:exacto:high:random", allModels);
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toContain("Invalid thinking level");
|
|
expect(result.warning).toContain("random");
|
|
});
|
|
});
|
|
|
|
describe("edge cases", () => {
|
|
test("empty pattern matches via partial matching", () => {
|
|
// Empty string is included in all model IDs, so partial matching finds a match
|
|
const result = parseModelPattern("", allModels);
|
|
expect(result.model).not.toBeNull();
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
});
|
|
|
|
test("pattern ending with colon treats empty suffix as invalid", () => {
|
|
const result = parseModelPattern("sonnet:", allModels);
|
|
// Empty string after colon is not a valid thinking level
|
|
// So it tries to match "sonnet:" which won't match, then tries "sonnet"
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.warning).toContain("Invalid thinking level");
|
|
});
|
|
});
|
|
|
|
describe("preference logic", () => {
|
|
test("prefers most recently used model when multiple providers match", () => {
|
|
const result = parseModelPattern("k2.5", allModels, {
|
|
usageOrder: ["kimi-code/kimi-k2.5"],
|
|
});
|
|
expect(result.model?.provider).toBe("kimi-code");
|
|
});
|
|
|
|
test("falls back to deprioritizing openrouter when no usage data", () => {
|
|
const result = parseModelPattern("k2.5", allModels, { usageOrder: [] });
|
|
expect(result.model?.provider).toBe("kimi-code");
|
|
});
|
|
|
|
test("respects most recently used provider even if openrouter", () => {
|
|
const result = parseModelPattern("k2.5", allModels, {
|
|
usageOrder: ["openrouter/moonshotai/kimi-k2.5"],
|
|
});
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.model?.id).toBe("moonshotai/kimi-k2.5");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("resolveModelRoleValue", () => {
|
|
test("resolves pi/<role>:<thinking> by expanding role alias before parsing thinking", () => {
|
|
const settings = {
|
|
getModelRole: (role: string) => (role === "smol" ? "openrouter/qwen/qwen3-coder:exacto" : undefined),
|
|
} as NonNullable<Parameters<typeof resolveModelRoleValue>[2]>["settings"];
|
|
|
|
const result = resolveModelRoleValue("pi/smol:high", allModels, { settings });
|
|
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBe("high");
|
|
expect(result.explicitThinkingLevel).toBe(true);
|
|
});
|
|
|
|
test("resolves pi/default through configured default role alias", () => {
|
|
const settings = {
|
|
getModelRole: (role: string) => (role === "default" ? "openrouter/qwen/qwen3-coder:exacto" : undefined),
|
|
} as NonNullable<Parameters<typeof resolveModelRoleValue>[2]>["settings"];
|
|
|
|
const result = resolveModelRoleValue("pi/default", allModels, { settings });
|
|
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBeUndefined();
|
|
expect(result.explicitThinkingLevel).toBe(false);
|
|
expect(result.warning).toBeUndefined();
|
|
});
|
|
|
|
test("does not resolve exact codex role values to codex-spark via substring matching", () => {
|
|
const providerQualified = resolveModelRoleValue("openai-codex/gpt-5.3-codex:xhigh", allModels);
|
|
expect(providerQualified.model?.provider).toBe("openai-codex");
|
|
expect(providerQualified.model?.id).toBe("gpt-5.3-codex");
|
|
expect(providerQualified.thinkingLevel).toBe("xhigh");
|
|
expect(providerQualified.explicitThinkingLevel).toBe(true);
|
|
|
|
const idOnly = resolveModelRoleValue("gpt-5.3-codex:xhigh", allModels);
|
|
expect(idOnly.model?.provider).toBe("openai-codex");
|
|
expect(idOnly.model?.id).toBe("gpt-5.3-codex");
|
|
expect(idOnly.thinkingLevel).toBe("xhigh");
|
|
expect(idOnly.explicitThinkingLevel).toBe(true);
|
|
});
|
|
});
|
|
describe("resolveModelFromString", () => {
|
|
test("falls back to pattern parsing for provider/model:thinking when strict provider+id miss", () => {
|
|
const resolved = resolveModelFromString("openrouter/qwen/qwen3-coder:exacto:high", allModels);
|
|
expect(resolved?.provider).toBe("openrouter");
|
|
expect(resolved?.id).toBe("qwen/qwen3-coder:exacto");
|
|
});
|
|
|
|
test("treats colon-containing model IDs without thinking suffix as exact IDs", () => {
|
|
const resolved = resolveModelFromString("openrouter/qwen/qwen3-coder:exacto", allModels);
|
|
expect(resolved?.provider).toBe("openrouter");
|
|
expect(resolved?.id).toBe("qwen/qwen3-coder:exacto");
|
|
});
|
|
});
|
|
|
|
describe("resolveModelOverride", () => {
|
|
test("preserves explicit off and explicit-thinking metadata", () => {
|
|
const registry = {
|
|
getAvailable: () => allModels,
|
|
} as Parameters<typeof resolveModelOverride>[1];
|
|
|
|
const result = resolveModelOverride(["sonnet:off"], registry);
|
|
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBe("off");
|
|
expect(result.explicitThinkingLevel).toBe(true);
|
|
});
|
|
|
|
test("resolves colon-containing model IDs with appended thinking suffix", () => {
|
|
const registry = {
|
|
getAvailable: () => allModels,
|
|
} as Parameters<typeof resolveModelOverride>[1];
|
|
|
|
const result = resolveModelOverride(["openrouter/qwen/qwen3-coder:exacto:high"], registry);
|
|
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
expect(result.thinkingLevel).toBe("high");
|
|
expect(result.explicitThinkingLevel).toBe(true);
|
|
});
|
|
});
|
|
describe("resolveCliModel", () => {
|
|
test("resolves --model provider/id without --provider", () => {
|
|
const registry = {
|
|
getAll: () => allModels,
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliModel: "openai/gpt-4o",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.model?.provider).toBe("openai");
|
|
expect(result.model?.id).toBe("gpt-4o");
|
|
});
|
|
|
|
test("resolves fuzzy patterns within an explicit provider", () => {
|
|
const registry = {
|
|
getAll: () => allModels,
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliProvider: "openai",
|
|
cliModel: "4o",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.model?.provider).toBe("openai");
|
|
expect(result.model?.id).toBe("gpt-4o");
|
|
});
|
|
|
|
test("supports --model <pattern>:<thinking> (without explicit --thinking)", () => {
|
|
const registry = {
|
|
getAll: () => allModels,
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliModel: "sonnet:high",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
|
expect(result.thinkingLevel).toBe("high");
|
|
});
|
|
|
|
test("prefers exact model id match over provider inference (OpenRouter-style ids)", () => {
|
|
const registry = {
|
|
getAll: () => allModels,
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliModel: "openai/gpt-4o:extended",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.model?.id).toBe("openai/gpt-4o:extended");
|
|
});
|
|
|
|
test("does not strip invalid :suffix as thinking level in --model (fail fast)", () => {
|
|
const registry = {
|
|
getAll: () => allModels,
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliProvider: "openai",
|
|
cliModel: "gpt-4o:extended",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.model).toBeUndefined();
|
|
expect(result.error).toContain("not found");
|
|
});
|
|
|
|
test("returns a clear error when there are no models", () => {
|
|
const registry = {
|
|
getAll: () => [],
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliProvider: "openai",
|
|
cliModel: "gpt-4o",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.model).toBeUndefined();
|
|
expect(result.error).toContain("No models available");
|
|
});
|
|
|
|
test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => {
|
|
const registry = {
|
|
getAll: () => allModels,
|
|
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
|
|
|
|
const result = resolveCliModel({
|
|
cliModel: "openrouter/qwen",
|
|
modelRegistry: registry,
|
|
});
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.model?.provider).toBe("openrouter");
|
|
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
|
});
|
|
});
|
|
|
|
describe("parseModelString", () => {
|
|
test("parses standard provider/id format", () => {
|
|
const result = parseModelString("anthropic/claude-sonnet-4-5");
|
|
expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5" });
|
|
});
|
|
|
|
test("returns undefined for strings without a slash", () => {
|
|
expect(parseModelString("claude-sonnet-4-5")).toBeUndefined();
|
|
expect(parseModelString("")).toBeUndefined();
|
|
expect(parseModelString("sonnet:high")).toBeUndefined();
|
|
});
|
|
|
|
test("returns undefined for strings starting with slash", () => {
|
|
expect(parseModelString("/claude-sonnet-4-5")).toBeUndefined();
|
|
});
|
|
|
|
describe("thinking level suffix extraction", () => {
|
|
test("extracts valid thinking level from provider/id:level", () => {
|
|
const result = parseModelString("anthropic/claude-sonnet-4-5:high");
|
|
expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5", thinkingLevel: "high" });
|
|
});
|
|
|
|
test("extracts all valid thinking levels", () => {
|
|
const levels = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
for (const level of levels) {
|
|
const result = parseModelString(`anthropic/claude-sonnet-4-5:${level}`);
|
|
expect(result?.id).toBe("claude-sonnet-4-5");
|
|
expect(result?.thinkingLevel).toBe(level);
|
|
}
|
|
});
|
|
|
|
test("does NOT strip invalid suffix — treats it as part of model ID", () => {
|
|
const result = parseModelString("openrouter/qwen/qwen3-coder:exacto");
|
|
expect(result).toEqual({ provider: "openrouter", id: "qwen/qwen3-coder:exacto" });
|
|
});
|
|
|
|
test("handles model ID with colon followed by valid thinking level", () => {
|
|
// e.g. "openrouter/qwen/qwen3-coder:exacto:high" — last colon is thinking level
|
|
const result = parseModelString("openrouter/qwen/qwen3-coder:exacto:high");
|
|
expect(result).toEqual({ provider: "openrouter", id: "qwen/qwen3-coder:exacto", thinkingLevel: "high" });
|
|
});
|
|
|
|
test("does not extract thinking level from model ID with invalid suffix", () => {
|
|
const result = parseModelString("openrouter/openai/gpt-4o:extended");
|
|
// :extended is not a valid thinking level, so it stays as part of the ID
|
|
expect(result).toEqual({ provider: "openrouter", id: "openai/gpt-4o:extended" });
|
|
});
|
|
|
|
test("handles empty suffix after colon", () => {
|
|
const result = parseModelString("anthropic/claude-sonnet-4-5:");
|
|
// Empty string is not a valid thinking level, so colon stays as part of ID
|
|
expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5:" });
|
|
});
|
|
});
|
|
});
|