claude-code/docs/test-plans/05-model-routing.md
2026-04-01 21:19:41 +08:00

114 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 模型路由测试计划
## 概述
模型路由系统负责 API provider 选择、模型别名解析、模型名规范化和运行时模型决策。测试重点是纯函数和环境变量驱动的逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/model/aliases.ts` | `MODEL_ALIASES`, `MODEL_FAMILY_ALIASES`, `isModelAlias`, `isModelFamilyAlias` |
| `src/utils/model/providers.ts` | `APIProvider`, `getAPIProvider`, `isFirstPartyAnthropicBaseUrl` |
| `src/utils/model/model.ts` | `firstPartyNameToCanonical`, `getCanonicalName`, `parseUserSpecifiedModel`, `normalizeModelStringForAPI`, `getRuntimeMainLoopModel`, `getDefaultMainLoopModelSetting` |
---
## 测试用例
### src/utils/model/aliases.ts
#### describe('isModelAlias')
- test('returns true for "sonnet"') — 有效别名
- test('returns true for "opus"')
- test('returns true for "haiku"')
- test('returns true for "best"')
- test('returns true for "sonnet[1m]"')
- test('returns true for "opus[1m]"')
- test('returns true for "opusplan"')
- test('returns false for full model ID') — `'claude-sonnet-4-6-20250514'` → false
- test('returns false for unknown string') — `'gpt-4'` → false
- test('is case-sensitive') — `'Sonnet'` → false别名是小写
#### describe('isModelFamilyAlias')
- test('returns true for "sonnet"')
- test('returns true for "opus"')
- test('returns true for "haiku"')
- test('returns false for "best"') — best 不是 family alias
- test('returns false for "opusplan"')
- test('returns false for "sonnet[1m]"')
---
### src/utils/model/providers.ts
#### describe('getAPIProvider')
- test('returns "firstParty" by default') — 无相关 env 时返回 firstParty
- test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set') — env 为 truthy 值
- test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set')
- test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set')
- test('bedrock takes precedence over vertex') — 多个 env 同时设置时 bedrock 优先
#### describe('isFirstPartyAnthropicBaseUrl')
- test('returns true when ANTHROPIC_BASE_URL is not set') — 默认 API
- test('returns true for api.anthropic.com') — `'https://api.anthropic.com'` → true
- test('returns false for custom URL') — `'https://my-proxy.com'` → false
- test('returns false for invalid URL') — 非法 URL → false
- test('returns true for staging URL when USER_TYPE is ant') — `'https://api-staging.anthropic.com'` + ant → true
---
### src/utils/model/model.ts
#### describe('firstPartyNameToCanonical')
- test('maps opus-4-6 full name to canonical') — `'claude-opus-4-6-20250514'``'claude-opus-4-6'`
- test('maps sonnet-4-6 full name') — `'claude-sonnet-4-6-20250514'``'claude-sonnet-4-6'`
- test('maps haiku-4-5') — `'claude-haiku-4-5-20251001'``'claude-haiku-4-5'`
- test('maps 3P provider format') — `'us.anthropic.claude-opus-4-6-v1:0'``'claude-opus-4-6'`
- test('maps claude-3-7-sonnet') — `'claude-3-7-sonnet-20250219'``'claude-3-7-sonnet'`
- test('maps claude-3-5-sonnet') → `'claude-3-5-sonnet'`
- test('maps claude-3-5-haiku') → `'claude-3-5-haiku'`
- test('maps claude-3-opus') → `'claude-3-opus'`
- test('is case insensitive') — `'Claude-Opus-4-6'``'claude-opus-4-6'`
- test('falls back to input for unknown model') — `'unknown-model'``'unknown-model'`
- test('differentiates opus-4 vs opus-4-5 vs opus-4-6') — 更具体的版本优先匹配
#### describe('parseUserSpecifiedModel')
- test('resolves "sonnet" to default sonnet model')
- test('resolves "opus" to default opus model')
- test('resolves "haiku" to default haiku model')
- test('resolves "best" to best model')
- test('resolves "opusplan" to default sonnet model') — opusplan 默认用 sonnet
- test('appends [1m] suffix when alias has [1m]') — `'sonnet[1m]'` → 模型名 + `'[1m]'`
- test('preserves original case for custom model names') — `'my-Custom-Model'` 保留大小写
- test('handles [1m] suffix on non-alias models') — `'custom-model[1m]'``'custom-model[1m]'`
- test('trims whitespace') — `' sonnet '` → 正确解析
#### describe('getRuntimeMainLoopModel')
- test('returns mainLoopModel by default') — 无特殊条件时原样返回
- test('returns opus in plan mode when opusplan is set') — opusplan + plan mode → opus
- test('returns sonnet in plan mode when haiku is set') — haiku + plan mode → sonnet 升级
- test('returns mainLoopModel in non-plan mode') — 非 plan 模式不做替换
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `process.env.CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY` | 直接设置/恢复 | provider 选择 |
| `process.env.ANTHROPIC_BASE_URL` | 直接设置/恢复 | URL 检测 |
| `process.env.USER_TYPE` | 直接设置/恢复 | staging URL 和 ant 功能 |
| `getModelStrings()` | mock.module | 返回固定模型 ID |
| `getMainLoopModelOverride` | mock.module | 会话中模型覆盖 |
| `getSettings_DEPRECATED` | mock.module | 用户设置中的模型 |
| `getUserSpecifiedModelSetting` | mock.module | `getRuntimeMainLoopModel` 依赖 |
| `isModelAllowed` | mock.module | allowlist 检查 |