When using custom providers with model ID prefixes (e.g., vertex_ai/claude-sonnet-4-5), the think mode switcher was stripping the prefix when mapping to high variants, causing routing failures in custom LLM proxies. Changes: - Add extractModelPrefix() to parse and preserve prefixes like vertex_ai/, openai/, etc. - Update getHighVariant() to preserve prefix when mapping to -high variants - Update isAlreadyHighVariant() to check base model name (without prefix) - Update getThinkingConfig() to check capability using base model name - Add comprehensive tests for custom provider prefix scenarios This fix ensures backward compatibility while supporting custom providers that use prefixed model IDs for routing. Fixes issue where think mode would break custom providers with prefixed models by stripping the routing prefix during model variant switching.
462 lines
18 KiB
TypeScript
462 lines
18 KiB
TypeScript
import { describe, expect, it } from "bun:test"
|
|
import {
|
|
getHighVariant,
|
|
getThinkingConfig,
|
|
isAlreadyHighVariant,
|
|
THINKING_CONFIGS,
|
|
} from "./switcher"
|
|
|
|
describe("think-mode switcher", () => {
|
|
describe("GitHub Copilot provider support", () => {
|
|
describe("Claude models via github-copilot", () => {
|
|
it("should resolve github-copilot Claude Opus to anthropic config", () => {
|
|
// #given a github-copilot provider with Claude Opus model
|
|
const providerID = "github-copilot"
|
|
const modelID = "claude-opus-4-5"
|
|
|
|
// #when getting thinking config
|
|
const config = getThinkingConfig(providerID, modelID)
|
|
|
|
// #then should return anthropic thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.thinking).toBeDefined()
|
|
expect((config?.thinking as Record<string, unknown>)?.type).toBe(
|
|
"enabled"
|
|
)
|
|
expect((config?.thinking as Record<string, unknown>)?.budgetTokens).toBe(
|
|
64000
|
|
)
|
|
})
|
|
|
|
it("should resolve github-copilot Claude Sonnet to anthropic config", () => {
|
|
// #given a github-copilot provider with Claude Sonnet model
|
|
const config = getThinkingConfig("github-copilot", "claude-sonnet-4-5")
|
|
|
|
// #then should return anthropic thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.thinking).toBeDefined()
|
|
})
|
|
|
|
it("should handle Claude with dots in version number", () => {
|
|
// #given a model ID with dots (claude-opus-4.5)
|
|
const config = getThinkingConfig("github-copilot", "claude-opus-4.5")
|
|
|
|
// #then should still return anthropic thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.thinking).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe("Gemini models via github-copilot", () => {
|
|
it("should resolve github-copilot Gemini Pro to google config", () => {
|
|
// #given a github-copilot provider with Gemini Pro model
|
|
const config = getThinkingConfig("github-copilot", "gemini-3-pro-preview")
|
|
|
|
// #then should return google thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.providerOptions).toBeDefined()
|
|
const googleOptions = (
|
|
config?.providerOptions as Record<string, unknown>
|
|
)?.google as Record<string, unknown>
|
|
expect(googleOptions?.thinkingConfig).toBeDefined()
|
|
})
|
|
|
|
it("should resolve github-copilot Gemini Flash to google config", () => {
|
|
// #given a github-copilot provider with Gemini Flash model
|
|
const config = getThinkingConfig(
|
|
"github-copilot",
|
|
"gemini-3-flash-preview"
|
|
)
|
|
|
|
// #then should return google thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.providerOptions).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe("GPT models via github-copilot", () => {
|
|
it("should resolve github-copilot GPT-5.2 to openai config", () => {
|
|
// #given a github-copilot provider with GPT-5.2 model
|
|
const config = getThinkingConfig("github-copilot", "gpt-5.2")
|
|
|
|
// #then should return openai thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.reasoning_effort).toBe("high")
|
|
})
|
|
|
|
it("should resolve github-copilot GPT-5 to openai config", () => {
|
|
// #given a github-copilot provider with GPT-5 model
|
|
const config = getThinkingConfig("github-copilot", "gpt-5")
|
|
|
|
// #then should return openai thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.reasoning_effort).toBe("high")
|
|
})
|
|
|
|
it("should resolve github-copilot o1 to openai config", () => {
|
|
// #given a github-copilot provider with o1 model
|
|
const config = getThinkingConfig("github-copilot", "o1-preview")
|
|
|
|
// #then should return openai thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.reasoning_effort).toBe("high")
|
|
})
|
|
|
|
it("should resolve github-copilot o3 to openai config", () => {
|
|
// #given a github-copilot provider with o3 model
|
|
const config = getThinkingConfig("github-copilot", "o3-mini")
|
|
|
|
// #then should return openai thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.reasoning_effort).toBe("high")
|
|
})
|
|
})
|
|
|
|
describe("Unknown models via github-copilot", () => {
|
|
it("should return null for unknown model types", () => {
|
|
// #given a github-copilot provider with unknown model
|
|
const config = getThinkingConfig("github-copilot", "llama-3-70b")
|
|
|
|
// #then should return null (no matching provider)
|
|
expect(config).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("Model ID normalization", () => {
|
|
describe("getHighVariant with dots vs hyphens", () => {
|
|
it("should handle dots in Claude version numbers", () => {
|
|
// #given a Claude model ID with dot format
|
|
const variant = getHighVariant("claude-opus-4.5")
|
|
|
|
// #then should return high variant with hyphen format
|
|
expect(variant).toBe("claude-opus-4-5-high")
|
|
})
|
|
|
|
it("should handle hyphens in Claude version numbers", () => {
|
|
// #given a Claude model ID with hyphen format
|
|
const variant = getHighVariant("claude-opus-4-5")
|
|
|
|
// #then should return high variant
|
|
expect(variant).toBe("claude-opus-4-5-high")
|
|
})
|
|
|
|
it("should handle dots in GPT version numbers", () => {
|
|
// #given a GPT model ID with dot format (gpt-5.2)
|
|
const variant = getHighVariant("gpt-5.2")
|
|
|
|
// #then should return high variant
|
|
expect(variant).toBe("gpt-5-2-high")
|
|
})
|
|
|
|
it("should handle dots in GPT-5.1 codex variants", () => {
|
|
// #given a GPT-5.1-codex model ID
|
|
const variant = getHighVariant("gpt-5.1-codex")
|
|
|
|
// #then should return high variant
|
|
expect(variant).toBe("gpt-5-1-codex-high")
|
|
})
|
|
|
|
it("should handle Gemini preview variants", () => {
|
|
// #given Gemini preview model IDs
|
|
expect(getHighVariant("gemini-3-pro-preview")).toBe(
|
|
"gemini-3-pro-preview-high"
|
|
)
|
|
expect(getHighVariant("gemini-3-flash-preview")).toBe(
|
|
"gemini-3-flash-preview-high"
|
|
)
|
|
})
|
|
|
|
it("should return null for already-high variants", () => {
|
|
// #given model IDs that are already high variants
|
|
expect(getHighVariant("claude-opus-4-5-high")).toBeNull()
|
|
expect(getHighVariant("gpt-5-2-high")).toBeNull()
|
|
expect(getHighVariant("gemini-3-pro-high")).toBeNull()
|
|
})
|
|
|
|
it("should return null for unknown models", () => {
|
|
// #given unknown model IDs
|
|
expect(getHighVariant("llama-3-70b")).toBeNull()
|
|
expect(getHighVariant("mistral-large")).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("isAlreadyHighVariant", () => {
|
|
it("should detect -high suffix", () => {
|
|
// #given model IDs with -high suffix
|
|
expect(isAlreadyHighVariant("claude-opus-4-5-high")).toBe(true)
|
|
expect(isAlreadyHighVariant("gpt-5-2-high")).toBe(true)
|
|
expect(isAlreadyHighVariant("gemini-3-pro-high")).toBe(true)
|
|
})
|
|
|
|
it("should detect -high suffix after normalization", () => {
|
|
// #given model IDs with dots that end in -high
|
|
expect(isAlreadyHighVariant("gpt-5.2-high")).toBe(true)
|
|
})
|
|
|
|
it("should return false for base models", () => {
|
|
// #given base model IDs without -high suffix
|
|
expect(isAlreadyHighVariant("claude-opus-4-5")).toBe(false)
|
|
expect(isAlreadyHighVariant("claude-opus-4.5")).toBe(false)
|
|
expect(isAlreadyHighVariant("gpt-5.2")).toBe(false)
|
|
expect(isAlreadyHighVariant("gemini-3-pro")).toBe(false)
|
|
})
|
|
|
|
it("should return false for models with 'high' in name but not suffix", () => {
|
|
// #given model IDs that contain 'high' but not as suffix
|
|
expect(isAlreadyHighVariant("high-performance-model")).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("getThinkingConfig", () => {
|
|
describe("Already high variants", () => {
|
|
it("should return null for already-high variants", () => {
|
|
// #given already-high model variants
|
|
expect(
|
|
getThinkingConfig("anthropic", "claude-opus-4-5-high")
|
|
).toBeNull()
|
|
expect(getThinkingConfig("openai", "gpt-5-2-high")).toBeNull()
|
|
expect(getThinkingConfig("google", "gemini-3-pro-high")).toBeNull()
|
|
})
|
|
|
|
it("should return null for already-high variants via github-copilot", () => {
|
|
// #given already-high model variants via github-copilot
|
|
expect(
|
|
getThinkingConfig("github-copilot", "claude-opus-4-5-high")
|
|
).toBeNull()
|
|
expect(getThinkingConfig("github-copilot", "gpt-5.2-high")).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("Non-thinking-capable models", () => {
|
|
it("should return null for non-thinking-capable models", () => {
|
|
// #given models that don't support thinking mode
|
|
expect(getThinkingConfig("anthropic", "claude-2")).toBeNull()
|
|
expect(getThinkingConfig("openai", "gpt-4")).toBeNull()
|
|
expect(getThinkingConfig("google", "gemini-1")).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("Unknown providers", () => {
|
|
it("should return null for unknown providers", () => {
|
|
// #given unknown provider IDs
|
|
expect(getThinkingConfig("unknown-provider", "some-model")).toBeNull()
|
|
expect(getThinkingConfig("azure", "gpt-5")).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("Direct provider configs (backwards compatibility)", () => {
|
|
it("should still work for direct anthropic provider", () => {
|
|
// #given direct anthropic provider
|
|
const config = getThinkingConfig("anthropic", "claude-opus-4-5")
|
|
|
|
// #then should return anthropic thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.thinking).toBeDefined()
|
|
expect((config?.thinking as Record<string, unknown>)?.type).toBe("enabled")
|
|
})
|
|
|
|
it("should still work for direct google provider", () => {
|
|
// #given direct google provider
|
|
const config = getThinkingConfig("google", "gemini-3-pro")
|
|
|
|
// #then should return google thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.providerOptions).toBeDefined()
|
|
})
|
|
|
|
it("should still work for amazon-bedrock provider", () => {
|
|
// #given amazon-bedrock provider with claude model
|
|
const config = getThinkingConfig("amazon-bedrock", "claude-sonnet-4-5")
|
|
|
|
// #then should return bedrock thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.reasoningConfig).toBeDefined()
|
|
})
|
|
|
|
it("should still work for google-vertex provider", () => {
|
|
// #given google-vertex provider
|
|
const config = getThinkingConfig("google-vertex", "gemini-3-pro")
|
|
|
|
// #then should return google-vertex thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.providerOptions).toBeDefined()
|
|
const vertexOptions = (config?.providerOptions as Record<string, unknown>)?.[
|
|
"google-vertex"
|
|
] as Record<string, unknown>
|
|
expect(vertexOptions?.thinkingConfig).toBeDefined()
|
|
})
|
|
|
|
it("should work for direct openai provider", () => {
|
|
// #given direct openai provider
|
|
const config = getThinkingConfig("openai", "gpt-5")
|
|
|
|
// #then should return openai thinking config
|
|
expect(config).not.toBeNull()
|
|
expect(config?.reasoning_effort).toBe("high")
|
|
})
|
|
})
|
|
|
|
describe("THINKING_CONFIGS structure", () => {
|
|
it("should have correct structure for anthropic", () => {
|
|
const config = THINKING_CONFIGS.anthropic
|
|
expect(config.thinking).toBeDefined()
|
|
expect(config.maxTokens).toBe(128000)
|
|
})
|
|
|
|
it("should have correct structure for google", () => {
|
|
const config = THINKING_CONFIGS.google
|
|
expect(config.providerOptions).toBeDefined()
|
|
})
|
|
|
|
it("should have correct structure for openai", () => {
|
|
const config = THINKING_CONFIGS.openai
|
|
expect(config.reasoning_effort).toBe("high")
|
|
})
|
|
|
|
it("should have correct structure for amazon-bedrock", () => {
|
|
const config = THINKING_CONFIGS["amazon-bedrock"]
|
|
expect(config.reasoningConfig).toBeDefined()
|
|
expect(config.maxTokens).toBe(64000)
|
|
})
|
|
})
|
|
|
|
describe("Custom provider prefixes support", () => {
|
|
describe("getHighVariant with prefixes", () => {
|
|
it("should preserve vertex_ai/ prefix when getting high variant", () => {
|
|
// #given a model ID with vertex_ai/ prefix
|
|
const variant = getHighVariant("vertex_ai/claude-sonnet-4-5")
|
|
|
|
// #then should return high variant with prefix preserved
|
|
expect(variant).toBe("vertex_ai/claude-sonnet-4-5-high")
|
|
})
|
|
|
|
it("should preserve openai/ prefix when getting high variant", () => {
|
|
// #given a model ID with openai/ prefix
|
|
const variant = getHighVariant("openai/gpt-5-2")
|
|
|
|
// #then should return high variant with prefix preserved
|
|
expect(variant).toBe("openai/gpt-5-2-high")
|
|
})
|
|
|
|
it("should handle prefixes with dots in version numbers", () => {
|
|
// #given a model ID with prefix and dots
|
|
const variant = getHighVariant("vertex_ai/claude-opus-4.5")
|
|
|
|
// #then should normalize dots and preserve prefix
|
|
expect(variant).toBe("vertex_ai/claude-opus-4-5-high")
|
|
})
|
|
|
|
it("should handle multiple different prefixes", () => {
|
|
// #given various custom prefixes
|
|
expect(getHighVariant("azure/gpt-5")).toBe("azure/gpt-5-high")
|
|
expect(getHighVariant("bedrock/claude-sonnet-4-5")).toBe("bedrock/claude-sonnet-4-5-high")
|
|
expect(getHighVariant("custom-llm/gemini-3-pro")).toBe("custom-llm/gemini-3-pro-high")
|
|
})
|
|
|
|
it("should return null for prefixed models without high variant mapping", () => {
|
|
// #given prefixed model IDs without high variant mapping
|
|
expect(getHighVariant("vertex_ai/unknown-model")).toBeNull()
|
|
expect(getHighVariant("custom/llama-3-70b")).toBeNull()
|
|
})
|
|
|
|
it("should return null for already-high prefixed models", () => {
|
|
// #given prefixed model IDs that are already high
|
|
expect(getHighVariant("vertex_ai/claude-opus-4-5-high")).toBeNull()
|
|
expect(getHighVariant("openai/gpt-5-2-high")).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("isAlreadyHighVariant with prefixes", () => {
|
|
it("should detect -high suffix in prefixed models", () => {
|
|
// #given prefixed model IDs with -high suffix
|
|
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-5-high")).toBe(true)
|
|
expect(isAlreadyHighVariant("openai/gpt-5-2-high")).toBe(true)
|
|
expect(isAlreadyHighVariant("custom/gemini-3-pro-high")).toBe(true)
|
|
})
|
|
|
|
it("should return false for prefixed base models", () => {
|
|
// #given prefixed base model IDs without -high suffix
|
|
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-5")).toBe(false)
|
|
expect(isAlreadyHighVariant("openai/gpt-5-2")).toBe(false)
|
|
})
|
|
|
|
it("should handle prefixed models with dots", () => {
|
|
// #given prefixed model IDs with dots
|
|
expect(isAlreadyHighVariant("vertex_ai/gpt-5.2")).toBe(false)
|
|
expect(isAlreadyHighVariant("vertex_ai/gpt-5.2-high")).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("getThinkingConfig with prefixes", () => {
|
|
it("should return null for custom providers (not in THINKING_CONFIGS)", () => {
|
|
// #given custom provider with prefixed Claude model
|
|
const config = getThinkingConfig("dia-llm", "vertex_ai/claude-sonnet-4-5")
|
|
|
|
// #then should return null (custom provider not in THINKING_CONFIGS)
|
|
expect(config).toBeNull()
|
|
})
|
|
|
|
it("should work with prefixed models on known providers", () => {
|
|
// #given known provider (anthropic) with prefixed model
|
|
// This tests that the base model name is correctly extracted for capability check
|
|
const config = getThinkingConfig("anthropic", "custom-prefix/claude-opus-4-5")
|
|
|
|
// #then should return thinking config (base model is capable)
|
|
expect(config).not.toBeNull()
|
|
expect(config?.thinking).toBeDefined()
|
|
})
|
|
|
|
it("should return null for prefixed models that are already high", () => {
|
|
// #given prefixed already-high model
|
|
const config = getThinkingConfig("anthropic", "vertex_ai/claude-opus-4-5-high")
|
|
|
|
// #then should return null
|
|
expect(config).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("Real-world custom provider scenario", () => {
|
|
it("should handle LLM proxy with vertex_ai prefix correctly", () => {
|
|
// #given a custom LLM proxy provider using vertex_ai/ prefix
|
|
const providerID = "dia-llm"
|
|
const modelID = "vertex_ai/claude-sonnet-4-5"
|
|
|
|
// #when getting high variant
|
|
const highVariant = getHighVariant(modelID)
|
|
|
|
// #then should preserve the prefix
|
|
expect(highVariant).toBe("vertex_ai/claude-sonnet-4-5-high")
|
|
|
|
// #and when checking if already high
|
|
expect(isAlreadyHighVariant(modelID)).toBe(false)
|
|
expect(isAlreadyHighVariant(highVariant!)).toBe(true)
|
|
|
|
// #and when getting thinking config for custom provider
|
|
const config = getThinkingConfig(providerID, modelID)
|
|
|
|
// #then should return null (custom provider, not anthropic)
|
|
// This prevents applying incompatible thinking configs to custom providers
|
|
expect(config).toBeNull()
|
|
})
|
|
|
|
it("should not break when switching to high variant in think mode", () => {
|
|
// #given think mode switching vertex_ai/claude model to high variant
|
|
const original = "vertex_ai/claude-opus-4-5"
|
|
const high = getHighVariant(original)
|
|
|
|
// #then the high variant should be valid
|
|
expect(high).toBe("vertex_ai/claude-opus-4-5-high")
|
|
|
|
// #and should be recognized as already high
|
|
expect(isAlreadyHighVariant(high!)).toBe(true)
|
|
|
|
// #and switching again should return null (already high)
|
|
expect(getHighVariant(high!)).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
})
|