mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
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>
This commit is contained in:
78
src/utils/model/__tests__/model-alias-recursion.test.ts
Normal file
78
src/utils/model/__tests__/model-alias-recursion.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { isModelAlias } from "../aliases";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicate the guard used in getDefault*Model to verify it catches
|
||||||
|
* all alias forms that would cause recursion.
|
||||||
|
*/
|
||||||
|
function isAliasOrAliasWithSuffix(value: string): boolean {
|
||||||
|
const base = value.replace(/\[1m\]$/i, "").trim();
|
||||||
|
return isModelAlias(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("isAliasOrAliasWithSuffix", () => {
|
||||||
|
test("detects bare 'opus' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("opus")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'opus[1m]' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("opus[1m]")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'sonnet' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("sonnet")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'sonnet[1m]' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("sonnet[1m]")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'haiku' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("haiku")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'haiku[1m]' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("haiku[1m]")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'opusplan' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("opusplan")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects 'best' alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("best")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes through concrete model IDs", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("claude-opus-4-6")).toBe(false);
|
||||||
|
expect(isAliasOrAliasWithSuffix("claude-sonnet-4-6")).toBe(false);
|
||||||
|
expect(isAliasOrAliasWithSuffix("claude-haiku-4-5-20251001")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes through concrete model IDs with [1m] suffix", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("claude-opus-4-6[1m]")).toBe(false);
|
||||||
|
expect(isAliasOrAliasWithSuffix("claude-sonnet-4-6[1m]")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes through 3P provider model IDs", () => {
|
||||||
|
expect(
|
||||||
|
isAliasOrAliasWithSuffix("us.anthropic.claude-opus-4-6-v1:0"),
|
||||||
|
).toBe(false);
|
||||||
|
expect(isAliasOrAliasWithSuffix("claude-opus-4-6@20251001")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes through arbitrary custom model names", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("my-custom-model")).toBe(false);
|
||||||
|
expect(isAliasOrAliasWithSuffix("gpt-4o")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles whitespace around alias", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix(" opus ")).toBe(true);
|
||||||
|
expect(isAliasOrAliasWithSuffix(" opus[1m] ")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles case insensitivity of [1m] suffix", () => {
|
||||||
|
expect(isAliasOrAliasWithSuffix("opus[1M]")).toBe(true);
|
||||||
|
expect(isAliasOrAliasWithSuffix("sonnet[1M]")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,6 +28,18 @@ import { getAPIProvider } from './providers.js'
|
|||||||
import { LIGHTNING_BOLT } from '../../constants/figures.js'
|
import { LIGHTNING_BOLT } from '../../constants/figures.js'
|
||||||
import { isModelAllowed } from './modelAllowlist.js'
|
import { isModelAllowed } from './modelAllowlist.js'
|
||||||
import { type ModelAlias, isModelAlias } from './aliases.js'
|
import { type ModelAlias, isModelAlias } from './aliases.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the value is a model alias or a model alias with a suffix
|
||||||
|
* like [1m] (e.g. "opus", "opus[1m]", "sonnet", "haiku[1m]").
|
||||||
|
* Used to guard against infinite recursion when getDefault*Model() falls back
|
||||||
|
* to the user-specified setting — an alias like "opus[1m]" would cause
|
||||||
|
* parseUserSpecifiedModel → getDefaultOpusModel → parseUserSpecifiedModel loop.
|
||||||
|
*/
|
||||||
|
function isAliasOrAliasWithSuffix(value: string): boolean {
|
||||||
|
const base = value.replace(/\[1m\]$/i, '').trim()
|
||||||
|
return isModelAlias(base)
|
||||||
|
}
|
||||||
import { capitalize } from '../stringUtils.js'
|
import { capitalize } from '../stringUtils.js'
|
||||||
|
|
||||||
export type ModelShortName = string
|
export type ModelShortName = string
|
||||||
@@ -128,8 +140,10 @@ export function getDefaultOpusModel(): ModelName {
|
|||||||
}
|
}
|
||||||
// Fall back to user's configured model — custom providers may not
|
// Fall back to user's configured model — custom providers may not
|
||||||
// recognize hardcoded Anthropic model IDs.
|
// recognize hardcoded Anthropic model IDs.
|
||||||
|
// Skip if the user setting is a model alias (e.g. "opus", "opus[1m]") to
|
||||||
|
// avoid infinite recursion: parseUserSpecifiedModel(alias) → getDefaultOpusModel().
|
||||||
const userSpecifiedOpus = getUserSpecifiedModelSetting()
|
const userSpecifiedOpus = getUserSpecifiedModelSetting()
|
||||||
if (userSpecifiedOpus) {
|
if (userSpecifiedOpus && !isAliasOrAliasWithSuffix(userSpecifiedOpus)) {
|
||||||
return parseUserSpecifiedModel(userSpecifiedOpus)
|
return parseUserSpecifiedModel(userSpecifiedOpus)
|
||||||
}
|
}
|
||||||
// 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch
|
// 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch
|
||||||
@@ -162,8 +176,9 @@ export function getDefaultSonnetModel(): ModelName {
|
|||||||
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) —
|
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) —
|
||||||
// custom providers (proxies, national clouds) may not recognize the
|
// custom providers (proxies, national clouds) may not recognize the
|
||||||
// hardcoded Anthropic model IDs.
|
// hardcoded Anthropic model IDs.
|
||||||
|
// Skip if the user setting is a model alias to avoid infinite recursion.
|
||||||
const userSpecified = getUserSpecifiedModelSetting()
|
const userSpecified = getUserSpecifiedModelSetting()
|
||||||
if (userSpecified) {
|
if (userSpecified && !isAliasOrAliasWithSuffix(userSpecified)) {
|
||||||
return parseUserSpecifiedModel(userSpecified)
|
return parseUserSpecifiedModel(userSpecified)
|
||||||
}
|
}
|
||||||
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
|
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
|
||||||
@@ -190,8 +205,9 @@ export function getDefaultHaikuModel(): ModelName {
|
|||||||
}
|
}
|
||||||
// Fall back to user's configured model — custom providers may not
|
// Fall back to user's configured model — custom providers may not
|
||||||
// recognize hardcoded Anthropic model IDs.
|
// recognize hardcoded Anthropic model IDs.
|
||||||
|
// Skip if the user setting is a model alias to avoid infinite recursion.
|
||||||
const userSpecifiedHaiku = getUserSpecifiedModelSetting()
|
const userSpecifiedHaiku = getUserSpecifiedModelSetting()
|
||||||
if (userSpecifiedHaiku) {
|
if (userSpecifiedHaiku && !isAliasOrAliasWithSuffix(userSpecifiedHaiku)) {
|
||||||
return parseUserSpecifiedModel(userSpecifiedHaiku)
|
return parseUserSpecifiedModel(userSpecifiedHaiku)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user