fix(think-mode): support GitHub Copilot proxy provider (#336)
* fix(think-mode): support GitHub Copilot proxy provider ### Summary - Adds `github-copilot` support to think-mode by resolving the underlying provider from the model name (Claude → Anthropic, Gemini → Google, GPT/o* → OpenAI). - Normalizes model IDs to handle dotted versions defensively (e.g. `claude-opus-4.5` → `claude-opus-4-5`, `gpt-5.2` → `gpt-5-2`) so high-variant upgrades and capability checks work reliably. - Expands high-variant mappings to cover Gemini preview/flash variants and aligns GPT-5.1/5.2 mappings with normalized IDs. - Adds OpenAI “thinking mode” config (`reasoning_effort: "high"`) alongside existing provider configs. ### Tests - Adds unit coverage for the switcher (`switcher.test.ts`) and integration coverage for the hook (`index.test.ts`), including: - GitHub Copilot model routing + thinking config injection - Dots vs hyphens normalization - Already-`-high` variants not being re-upgraded - Unknown models/providers handled gracefully * fix: support multiple digits in model minor
This commit is contained in:
@@ -1,3 +1,67 @@
|
||||
/**
|
||||
* Think Mode Switcher
|
||||
*
|
||||
* This module handles "thinking mode" activation for reasoning-capable models.
|
||||
* When a user includes "think" keywords in their prompt, models are upgraded to
|
||||
* their high-reasoning variants with extended thinking budgets.
|
||||
*
|
||||
* PROVIDER ALIASING:
|
||||
* GitHub Copilot acts as a proxy provider that routes to underlying providers
|
||||
* (Anthropic, Google, OpenAI). We resolve the proxy to the actual provider
|
||||
* based on model name patterns, allowing GitHub Copilot to inherit thinking
|
||||
* configurations without duplication.
|
||||
*
|
||||
* NORMALIZATION:
|
||||
* Model IDs are normalized (dots → hyphens in version numbers) to handle API
|
||||
* inconsistencies defensively while maintaining backwards compatibility.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalizes model IDs to use consistent hyphen formatting.
|
||||
* GitHub Copilot may use dots (claude-opus-4.5) but our maps use hyphens (claude-opus-4-5).
|
||||
* This ensures lookups work regardless of format.
|
||||
*
|
||||
* @example
|
||||
* normalizeModelID("claude-opus-4.5") // "claude-opus-4-5"
|
||||
* normalizeModelID("gemini-3.5-pro") // "gemini-3-5-pro"
|
||||
* normalizeModelID("gpt-5.2") // "gpt-5-2"
|
||||
*/
|
||||
function normalizeModelID(modelID: string): string {
|
||||
// Replace dots with hyphens when followed by a digit
|
||||
// This handles version numbers like 4.5 → 4-5, 5.2 → 5-2
|
||||
return modelID.replace(/\.(\d+)/g, "-$1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves proxy providers (like github-copilot) to their underlying provider.
|
||||
* This allows GitHub Copilot to inherit thinking configurations from the actual
|
||||
* model provider (Anthropic, Google, OpenAI).
|
||||
*
|
||||
* @example
|
||||
* resolveProvider("github-copilot", "claude-opus-4-5") // "anthropic"
|
||||
* resolveProvider("github-copilot", "gemini-3-pro") // "google"
|
||||
* resolveProvider("github-copilot", "gpt-5.2") // "openai"
|
||||
* resolveProvider("anthropic", "claude-opus-4-5") // "anthropic" (unchanged)
|
||||
*/
|
||||
function resolveProvider(providerID: string, modelID: string): string {
|
||||
// GitHub Copilot is a proxy - infer actual provider from model name
|
||||
if (providerID === "github-copilot") {
|
||||
const modelLower = modelID.toLowerCase()
|
||||
if (modelLower.includes("claude")) return "anthropic"
|
||||
if (modelLower.includes("gemini")) return "google"
|
||||
if (
|
||||
modelLower.includes("gpt") ||
|
||||
modelLower.includes("o1") ||
|
||||
modelLower.includes("o3")
|
||||
) {
|
||||
return "openai"
|
||||
}
|
||||
}
|
||||
|
||||
// Direct providers or unknown - return as-is
|
||||
return providerID
|
||||
}
|
||||
|
||||
// Maps model IDs to their "high reasoning" variant (internal convention)
|
||||
// For OpenAI models, this signals that reasoning_effort should be set to "high"
|
||||
const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||
@@ -7,6 +71,9 @@ const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||
// Gemini
|
||||
"gemini-3-pro": "gemini-3-pro-high",
|
||||
"gemini-3-pro-low": "gemini-3-pro-high",
|
||||
"gemini-3-pro-preview": "gemini-3-pro-preview-high",
|
||||
"gemini-3-flash": "gemini-3-flash-high",
|
||||
"gemini-3-flash-preview": "gemini-3-flash-preview-high",
|
||||
// GPT-5
|
||||
"gpt-5": "gpt-5-high",
|
||||
"gpt-5-mini": "gpt-5-mini-high",
|
||||
@@ -14,42 +81,20 @@ const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||
"gpt-5-pro": "gpt-5-pro-high",
|
||||
"gpt-5-chat-latest": "gpt-5-chat-latest-high",
|
||||
// GPT-5.1
|
||||
"gpt-5.1": "gpt-5.1-high",
|
||||
"gpt-5.1-chat-latest": "gpt-5.1-chat-latest-high",
|
||||
"gpt-5.1-codex": "gpt-5.1-codex-high",
|
||||
"gpt-5.1-codex-mini": "gpt-5.1-codex-mini-high",
|
||||
"gpt-5.1-codex-max": "gpt-5.1-codex-max-high",
|
||||
"gpt-5-1": "gpt-5-1-high",
|
||||
"gpt-5-1-chat-latest": "gpt-5-1-chat-latest-high",
|
||||
"gpt-5-1-codex": "gpt-5-1-codex-high",
|
||||
"gpt-5-1-codex-mini": "gpt-5-1-codex-mini-high",
|
||||
"gpt-5-1-codex-max": "gpt-5-1-codex-max-high",
|
||||
// GPT-5.2
|
||||
"gpt-5.2": "gpt-5.2-high",
|
||||
"gpt-5.2-chat-latest": "gpt-5.2-chat-latest-high",
|
||||
"gpt-5.2-pro": "gpt-5.2-pro-high",
|
||||
"gpt-5-2": "gpt-5-2-high",
|
||||
"gpt-5-2-chat-latest": "gpt-5-2-chat-latest-high",
|
||||
"gpt-5-2-pro": "gpt-5-2-pro-high",
|
||||
}
|
||||
|
||||
const ALREADY_HIGH: Set<string> = new Set([
|
||||
// Claude
|
||||
"claude-sonnet-4-5-high",
|
||||
"claude-opus-4-5-high",
|
||||
// Gemini
|
||||
"gemini-3-pro-high",
|
||||
// GPT-5
|
||||
"gpt-5-high",
|
||||
"gpt-5-mini-high",
|
||||
"gpt-5-nano-high",
|
||||
"gpt-5-pro-high",
|
||||
"gpt-5-chat-latest-high",
|
||||
// GPT-5.1
|
||||
"gpt-5.1-high",
|
||||
"gpt-5.1-chat-latest-high",
|
||||
"gpt-5.1-codex-high",
|
||||
"gpt-5.1-codex-mini-high",
|
||||
"gpt-5.1-codex-max-high",
|
||||
// GPT-5.2
|
||||
"gpt-5.2-high",
|
||||
"gpt-5.2-chat-latest-high",
|
||||
"gpt-5.2-pro-high",
|
||||
])
|
||||
const ALREADY_HIGH: Set<string> = new Set(Object.values(HIGH_VARIANT_MAP))
|
||||
|
||||
export const THINKING_CONFIGS: Record<string, Record<string, unknown>> = {
|
||||
export const THINKING_CONFIGS = {
|
||||
anthropic: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
@@ -82,42 +127,59 @@ export const THINKING_CONFIGS: Record<string, Record<string, unknown>> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
openai: {
|
||||
reasoning_effort: "high",
|
||||
},
|
||||
} as const satisfies Record<string, Record<string, unknown>>
|
||||
|
||||
const THINKING_CAPABLE_MODELS: Record<string, string[]> = {
|
||||
const THINKING_CAPABLE_MODELS = {
|
||||
anthropic: ["claude-sonnet-4", "claude-opus-4", "claude-3"],
|
||||
"amazon-bedrock": ["claude", "anthropic"],
|
||||
google: ["gemini-2", "gemini-3"],
|
||||
"google-vertex": ["gemini-2", "gemini-3"],
|
||||
}
|
||||
openai: ["gpt-5", "o1", "o3"],
|
||||
} as const satisfies Record<string, readonly string[]>
|
||||
|
||||
export function getHighVariant(modelID: string): string | null {
|
||||
if (ALREADY_HIGH.has(modelID)) {
|
||||
const normalized = normalizeModelID(modelID)
|
||||
|
||||
if (ALREADY_HIGH.has(normalized)) {
|
||||
return null
|
||||
}
|
||||
return HIGH_VARIANT_MAP[modelID] ?? null
|
||||
return HIGH_VARIANT_MAP[normalized] ?? null
|
||||
}
|
||||
|
||||
export function isAlreadyHighVariant(modelID: string): boolean {
|
||||
return ALREADY_HIGH.has(modelID) || modelID.endsWith("-high")
|
||||
const normalized = normalizeModelID(modelID)
|
||||
return ALREADY_HIGH.has(normalized) || normalized.endsWith("-high")
|
||||
}
|
||||
|
||||
type ThinkingProvider = keyof typeof THINKING_CONFIGS
|
||||
|
||||
function isThinkingProvider(provider: string): provider is ThinkingProvider {
|
||||
return provider in THINKING_CONFIGS
|
||||
}
|
||||
|
||||
export function getThinkingConfig(
|
||||
providerID: string,
|
||||
modelID: string
|
||||
): Record<string, unknown> | null {
|
||||
if (isAlreadyHighVariant(modelID)) {
|
||||
const normalized = normalizeModelID(modelID)
|
||||
|
||||
if (isAlreadyHighVariant(normalized)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const config = THINKING_CONFIGS[providerID]
|
||||
const capablePatterns = THINKING_CAPABLE_MODELS[providerID]
|
||||
const resolvedProvider = resolveProvider(providerID, modelID)
|
||||
|
||||
if (!config || !capablePatterns) {
|
||||
if (!isThinkingProvider(resolvedProvider)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const modelLower = modelID.toLowerCase()
|
||||
const config = THINKING_CONFIGS[resolvedProvider]
|
||||
const capablePatterns = THINKING_CAPABLE_MODELS[resolvedProvider]
|
||||
|
||||
const modelLower = normalized.toLowerCase()
|
||||
const isCapable = capablePatterns.some((pattern) =>
|
||||
modelLower.includes(pattern.toLowerCase())
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user