Skip to content

Commit cee62bc

Browse files
fix: 修复 model alias 导致无限递归栈溢出
当用户 settings 中配置 model = "opus[1m]" 等 alias 值时, getDefaultOpusModel() → parseUserSpecifiedModel() → getDefaultOpusModel() 形成无限递归,导致启动时 RangeError: Maximum call stack size exceeded。 在 getDefaultOpusModel/Sonnet/Haiku 的 fallback 路径中增加 isAliasOrAliasWithSuffix 守卫,跳过 alias 值直接使用硬编码默认值。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5fc7c8e commit cee62bc

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { isModelAlias } from "../aliases";
3+
4+
/**
5+
* Replicate the guard used in getDefault*Model to verify it catches
6+
* all alias forms that would cause recursion.
7+
*/
8+
function isAliasOrAliasWithSuffix(value: string): boolean {
9+
const base = value.replace(/\[1m\]$/i, "").trim();
10+
return isModelAlias(base);
11+
}
12+
13+
describe("isAliasOrAliasWithSuffix", () => {
14+
test("detects bare 'opus' alias", () => {
15+
expect(isAliasOrAliasWithSuffix("opus")).toBe(true);
16+
});
17+
18+
test("detects 'opus[1m]' alias", () => {
19+
expect(isAliasOrAliasWithSuffix("opus[1m]")).toBe(true);
20+
});
21+
22+
test("detects 'sonnet' alias", () => {
23+
expect(isAliasOrAliasWithSuffix("sonnet")).toBe(true);
24+
});
25+
26+
test("detects 'sonnet[1m]' alias", () => {
27+
expect(isAliasOrAliasWithSuffix("sonnet[1m]")).toBe(true);
28+
});
29+
30+
test("detects 'haiku' alias", () => {
31+
expect(isAliasOrAliasWithSuffix("haiku")).toBe(true);
32+
});
33+
34+
test("detects 'haiku[1m]' alias", () => {
35+
expect(isAliasOrAliasWithSuffix("haiku[1m]")).toBe(true);
36+
});
37+
38+
test("detects 'opusplan' alias", () => {
39+
expect(isAliasOrAliasWithSuffix("opusplan")).toBe(true);
40+
});
41+
42+
test("detects 'best' alias", () => {
43+
expect(isAliasOrAliasWithSuffix("best")).toBe(true);
44+
});
45+
46+
test("passes through concrete model IDs", () => {
47+
expect(isAliasOrAliasWithSuffix("claude-opus-4-6")).toBe(false);
48+
expect(isAliasOrAliasWithSuffix("claude-sonnet-4-6")).toBe(false);
49+
expect(isAliasOrAliasWithSuffix("claude-haiku-4-5-20251001")).toBe(false);
50+
});
51+
52+
test("passes through concrete model IDs with [1m] suffix", () => {
53+
expect(isAliasOrAliasWithSuffix("claude-opus-4-6[1m]")).toBe(false);
54+
expect(isAliasOrAliasWithSuffix("claude-sonnet-4-6[1m]")).toBe(false);
55+
});
56+
57+
test("passes through 3P provider model IDs", () => {
58+
expect(
59+
isAliasOrAliasWithSuffix("us.anthropic.claude-opus-4-6-v1:0"),
60+
).toBe(false);
61+
expect(isAliasOrAliasWithSuffix("claude-opus-4-6@20251001")).toBe(false);
62+
});
63+
64+
test("passes through arbitrary custom model names", () => {
65+
expect(isAliasOrAliasWithSuffix("my-custom-model")).toBe(false);
66+
expect(isAliasOrAliasWithSuffix("gpt-4o")).toBe(false);
67+
});
68+
69+
test("handles whitespace around alias", () => {
70+
expect(isAliasOrAliasWithSuffix(" opus ")).toBe(true);
71+
expect(isAliasOrAliasWithSuffix(" opus[1m] ")).toBe(true);
72+
});
73+
74+
test("handles case insensitivity of [1m] suffix", () => {
75+
expect(isAliasOrAliasWithSuffix("opus[1M]")).toBe(true);
76+
expect(isAliasOrAliasWithSuffix("sonnet[1M]")).toBe(true);
77+
});
78+
});

src/utils/model/model.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ import { getAPIProvider } from './providers.js'
2828
import { LIGHTNING_BOLT } from '../../constants/figures.js'
2929
import { isModelAllowed } from './modelAllowlist.js'
3030
import { type ModelAlias, isModelAlias } from './aliases.js'
31+
32+
/**
33+
* Returns true if the value is a model alias or a model alias with a suffix
34+
* like [1m] (e.g. "opus", "opus[1m]", "sonnet", "haiku[1m]").
35+
* Used to guard against infinite recursion when getDefault*Model() falls back
36+
* to the user-specified setting — an alias like "opus[1m]" would cause
37+
* parseUserSpecifiedModel → getDefaultOpusModel → parseUserSpecifiedModel loop.
38+
*/
39+
function isAliasOrAliasWithSuffix(value: string): boolean {
40+
const base = value.replace(/\[1m\]$/i, '').trim()
41+
return isModelAlias(base)
42+
}
3143
import { capitalize } from '../stringUtils.js'
3244

3345
export type ModelShortName = string
@@ -128,8 +140,10 @@ export function getDefaultOpusModel(): ModelName {
128140
}
129141
// Fall back to user's configured model — custom providers may not
130142
// recognize hardcoded Anthropic model IDs.
143+
// Skip if the user setting is a model alias (e.g. "opus", "opus[1m]") to
144+
// avoid infinite recursion: parseUserSpecifiedModel(alias) → getDefaultOpusModel().
131145
const userSpecifiedOpus = getUserSpecifiedModelSetting()
132-
if (userSpecifiedOpus) {
146+
if (userSpecifiedOpus && !isAliasOrAliasWithSuffix(userSpecifiedOpus)) {
133147
return parseUserSpecifiedModel(userSpecifiedOpus)
134148
}
135149
// 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch
@@ -162,8 +176,9 @@ export function getDefaultSonnetModel(): ModelName {
162176
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) —
163177
// custom providers (proxies, national clouds) may not recognize the
164178
// hardcoded Anthropic model IDs.
179+
// Skip if the user setting is a model alias to avoid infinite recursion.
165180
const userSpecified = getUserSpecifiedModelSetting()
166-
if (userSpecified) {
181+
if (userSpecified && !isAliasOrAliasWithSuffix(userSpecified)) {
167182
return parseUserSpecifiedModel(userSpecified)
168183
}
169184
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
@@ -190,8 +205,9 @@ export function getDefaultHaikuModel(): ModelName {
190205
}
191206
// Fall back to user's configured model — custom providers may not
192207
// recognize hardcoded Anthropic model IDs.
208+
// Skip if the user setting is a model alias to avoid infinite recursion.
193209
const userSpecifiedHaiku = getUserSpecifiedModelSetting()
194-
if (userSpecifiedHaiku) {
210+
if (userSpecifiedHaiku && !isAliasOrAliasWithSuffix(userSpecifiedHaiku)) {
195211
return parseUserSpecifiedModel(userSpecifiedHaiku)
196212
}
197213

0 commit comments

Comments
 (0)