Skip to content

Commit 6e6f409

Browse files
committed
refactor: move reasoning policies to config file
Replace regex patterns and hardcoded logic with explicit model config. Simpler, more maintainable, and easier to extend. - New src/common/constants/reasoning-config.ts with explicit policies - Removed complex regex matching from policy.ts - Simplified enforceThinkingPolicy fallback logic - All tests passing
1 parent 21be5c0 commit 6e6f409

File tree

3 files changed

+80
-54
lines changed

3 files changed

+80
-54
lines changed

src/browser/utils/thinking/policy.test.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ describe("getThinkingPolicyForModel", () => {
1010
expect(getThinkingPolicyForModel("openai:gpt-5-pro-2025-10-06")).toEqual(["high"]);
1111
});
1212

13-
test("returns single HIGH for gpt-5-pro with whitespace after colon", () => {
14-
expect(getThinkingPolicyForModel("openai: gpt-5-pro")).toEqual(["high"]);
15-
});
16-
1713
test("returns all levels for gpt-5-pro-mini (not a fixed policy)", () => {
14+
// gpt-5-pro-mini shouldn't match the gpt-5-pro config
1815
expect(getThinkingPolicyForModel("openai:gpt-5-pro-mini")).toEqual([
1916
"off",
2017
"low",
@@ -49,16 +46,17 @@ describe("getThinkingPolicyForModel", () => {
4946
expect(getThinkingPolicyForModel("google:gemini-3-pro-preview")).toEqual(["low", "high"]);
5047
});
5148

52-
test("returns binary on/off for xAI Grok-[1-9]* models", () => {
49+
test("returns binary on/off for xAI Grok models", () => {
5350
expect(getThinkingPolicyForModel("xai:grok-4-1-fast")).toEqual(["off", "high"]);
54-
expect(getThinkingPolicyForModel("xai:grok-4")).toEqual(["off", "high"]);
55-
expect(getThinkingPolicyForModel("xai:grok-2")).toEqual(["off", "high"]);
56-
expect(getThinkingPolicyForModel("xai:grok-3")).toEqual(["off", "high"]);
51+
expect(getThinkingPolicyForModel("xai:grok-2-latest")).toEqual(["off", "high"]);
52+
expect(getThinkingPolicyForModel("xai:grok-beta")).toEqual(["off", "high"]);
53+
});
54+
55+
test("grok models with version suffixes match config prefix", () => {
56+
expect(getThinkingPolicyForModel("xai:grok-4-1-fast-v2")).toEqual(["off", "high"]);
5757
});
5858

59-
test("grok-code models get default policy (models.json says supports_reasoning: true)", () => {
60-
// Note: models.json currently has supports_reasoning: true for grok-code
61-
// If this is incorrect, update models.json and this test will automatically reflect it
59+
test("unknown xAI models get default policy", () => {
6260
expect(getThinkingPolicyForModel("xai:grok-code-fast-1")).toEqual([
6361
"off",
6462
"low",

src/browser/utils/thinking/policy.ts

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414

1515
import type { ThinkingLevel } from "@/common/types/thinking";
16+
import { getReasoningConfig } from "@/common/constants/reasoning-config";
1617
import modelsData from "@/common/utils/tokens/models.json";
1718

1819
/**
@@ -48,53 +49,33 @@ function getModelMetadata(modelString: string): Record<string, unknown> | null {
4849
* Returns the thinking policy for a given model.
4950
*
5051
* Rules:
51-
* - openai:gpt-5-pro → ["high"] (only supported level)
52-
* - Models without supports_reasoning in models.json → ["off"]
53-
* - xAI Grok-[1-9]* → ["off", "high"] (binary reasoning)
54-
* - Gemini 3 → ["low", "high"]
55-
* - default → ["off", "low", "medium", "high"] (all levels selectable)
56-
*
57-
* Tolerates version suffixes (e.g., gpt-5-pro-2025-10-06).
58-
* Does NOT match gpt-5-pro-mini (uses negative lookahead).
52+
* 1. Check reasoning-config.ts for explicit model policies
53+
* 2. Check models.json for supports_reasoning: false → ["off"]
54+
* 3. Default → ["off", "low", "medium", "high"]
5955
*/
6056
export function getThinkingPolicyForModel(modelString: string): ThinkingPolicy {
61-
// Match "openai:" followed by optional whitespace and "gpt-5-pro"
62-
// Allow version suffixes like "-2025-10-06" but NOT "-mini" or other text suffixes
63-
if (/^openai:\s*gpt-5-pro(?!-[a-z])/.test(modelString)) {
64-
return ["high"];
65-
}
66-
67-
// Gemini 3 Pro only supports "low" and "high" reasoning levels
68-
if (modelString.includes("gemini-3")) {
69-
return ["low", "high"];
57+
// Check explicit config first
58+
const config = getReasoningConfig(modelString);
59+
if (config) {
60+
return config;
7061
}
7162

72-
// xAI Grok-[1-9]* models only support binary on/off reasoning (no gradual levels)
73-
// Use "high" to represent "reasoning enabled" for consistency with backend logic
74-
// Note: grok-code does not match this pattern
75-
if (modelString.startsWith("xai:") && /grok-[1-9]/.test(modelString)) {
76-
return ["off", "high"];
77-
}
78-
79-
// Check models.json for supports_reasoning
63+
// Check models.json for models that don't support reasoning
8064
const metadata = getModelMetadata(modelString);
8165
if (metadata?.supports_reasoning === false) {
8266
return ["off"];
8367
}
8468

85-
// Default policy: all levels selectable (for models with supports_reasoning: true or unknown models)
69+
// Default: all levels selectable
8670
return ["off", "low", "medium", "high"];
8771
}
8872

8973
/**
9074
* Enforce thinking policy by clamping requested level to allowed set.
9175
*
92-
* Fallback strategy:
93-
* 1. If requested level is allowed, use it
94-
* 2. If requested level is non-"off" but not allowed, map to highest allowed non-"off" level
95-
* (preserves user intent to have reasoning enabled for binary models like Grok)
96-
* 3. If "medium" is allowed, use it (reasonable default)
97-
* 4. Otherwise use first allowed level
76+
* If the requested level isn't allowed:
77+
* - If user wanted reasoning (non-"off"), pick the highest available non-"off" level
78+
* - Otherwise return the first allowed level
9879
*/
9980
export function enforceThinkingPolicy(
10081
modelString: string,
@@ -106,18 +87,12 @@ export function enforceThinkingPolicy(
10687
return requested;
10788
}
10889

109-
// If user requested a non-"off" level but it's not allowed, preserve reasoning intent
110-
// by mapping to the highest non-"off" level available (e.g., "medium" → "high" for Grok)
90+
// If user wanted reasoning, keep it on with the best available level
11191
if (requested !== "off") {
112-
const nonOffLevels = allowed.filter((level) => level !== "off");
113-
if (nonOffLevels.length > 0) {
114-
// Return highest available: prefer high > medium > low
115-
if (nonOffLevels.includes("high")) return "high";
116-
if (nonOffLevels.includes("medium")) return "medium";
117-
if (nonOffLevels.includes("low")) return "low";
118-
}
92+
if (allowed.includes("high")) return "high";
93+
if (allowed.includes("medium")) return "medium";
94+
if (allowed.includes("low")) return "low";
11995
}
12096

121-
// Fallback: prefer "medium" if allowed, else use first allowed level
122-
return allowed.includes("medium") ? "medium" : allowed[0];
97+
return allowed[0];
12398
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Reasoning configuration for models with special policies.
3+
*
4+
* Models not listed here default to:
5+
* - ["off"] if models.json has supports_reasoning: false
6+
* - ["off", "low", "medium", "high"] otherwise
7+
*/
8+
9+
import type { ThinkingLevel } from "@/common/types/thinking";
10+
11+
export const REASONING_CONFIG: Record<string, readonly ThinkingLevel[]> = {
12+
// GPT-5 Pro: fixed high reasoning
13+
"openai:gpt-5-pro": ["high"],
14+
15+
// Gemini 3: limited reasoning levels
16+
"google:gemini-3": ["low", "high"],
17+
18+
// Grok: binary on/off reasoning
19+
"xai:grok-beta": ["off", "high"],
20+
"xai:grok-vision-beta": ["off", "high"],
21+
"xai:grok-2-latest": ["off", "high"],
22+
"xai:grok-2-vision-1212": ["off", "high"],
23+
"xai:grok-2-1212": ["off", "high"],
24+
"xai:grok-4-1-fast": ["off", "high"],
25+
};
26+
27+
/**
28+
* Get reasoning policy for a model, matching prefix patterns.
29+
* e.g., "openai:gpt-5-pro-2025-10-06" matches "openai:gpt-5-pro"
30+
*
31+
* Prefix matching only works for date-style suffixes (starts with digit or dash).
32+
* "openai:gpt-5-pro-mini" does NOT match "openai:gpt-5-pro".
33+
*/
34+
export function getReasoningConfig(modelString: string): readonly ThinkingLevel[] | null {
35+
// Exact match first
36+
if (REASONING_CONFIG[modelString]) {
37+
return REASONING_CONFIG[modelString];
38+
}
39+
40+
// Prefix match for versioned models (but not variants like -mini)
41+
for (const [key, policy] of Object.entries(REASONING_CONFIG)) {
42+
if (modelString.startsWith(key)) {
43+
const suffix = modelString.slice(key.length);
44+
// Allow if no suffix, or suffix starts with dash + (digit, "pro", "vision", "v", etc.)
45+
// Block known variants: -mini, -nano, -micro, -turbo (but allow -turbo-preview etc.)
46+
if (suffix === "" || (suffix.startsWith("-") && !/^-(mini|nano|micro)($|-)/.test(suffix))) {
47+
return policy;
48+
}
49+
}
50+
}
51+
52+
return null;
53+
}

0 commit comments

Comments
 (0)