mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
Upgrade default Claude Opus model to 4.8
Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8 across provider presets (claude, claudeDesktop, hermes, openclaw, opencode, universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the user-manual docs. - Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row for historical usage stats (seeded via INSERT OR IGNORE). - Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias during rollout so previously saved routes keep resolving. - thinking_optimizer: route opus-4-8 through adaptive thinking and normalize dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy). - usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base pricing. Also merge role:"system" messages into the Gemini systemInstruction in the Anthropic->Gemini transform.
This commit is contained in:
@@ -32,6 +32,9 @@ pub const ANTHROPIC_CLAUDE_ROUTE_PREFIX: &str = "anthropic/claude-";
|
||||
/// Claude Desktop schema 不接受此后缀,import 边界翻译为 `supports1m` 字段。
|
||||
pub const ONE_M_CONTEXT_MARKER: &str = "[1m]";
|
||||
|
||||
const CURRENT_OPUS_ROUTE_ID: &str = "claude-opus-4-8";
|
||||
const LEGACY_OPUS_ROUTE_ID: &str = "claude-opus-4-7";
|
||||
|
||||
const NON_ANTHROPIC_ROUTE_MARKERS: &[&str] = &[
|
||||
"ark-code",
|
||||
"astron",
|
||||
@@ -87,7 +90,7 @@ pub const DEFAULT_PROXY_ROUTES: &[ClaudeDesktopDefaultRoute] = &[
|
||||
supports_1m: true,
|
||||
},
|
||||
ClaudeDesktopDefaultRoute {
|
||||
route_id: "claude-opus-4-7",
|
||||
route_id: CURRENT_OPUS_ROUTE_ID,
|
||||
env_key: "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
supports_1m: true,
|
||||
},
|
||||
@@ -710,7 +713,11 @@ pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<V
|
||||
})?;
|
||||
|
||||
let routes = proxy_model_routes(provider)?;
|
||||
let route = routes.iter().find(|r| r.route_id == requested);
|
||||
let route = routes.iter().find(|r| r.route_id == requested).or_else(|| {
|
||||
routes
|
||||
.iter()
|
||||
.find(|r| is_compatible_opus_route_alias(&r.route_id, &requested))
|
||||
});
|
||||
let Some(route) = route else {
|
||||
return Err(AppError::localized(
|
||||
"claude_desktop.provider.route_unknown",
|
||||
@@ -726,6 +733,14 @@ pub fn map_proxy_request_model(mut body: Value, provider: &Provider) -> Result<V
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
fn is_compatible_opus_route_alias(route_id: &str, requested: &str) -> bool {
|
||||
matches!(
|
||||
(route_id, requested),
|
||||
(CURRENT_OPUS_ROUTE_ID, LEGACY_OPUS_ROUTE_ID)
|
||||
| (LEGACY_OPUS_ROUTE_ID, CURRENT_OPUS_ROUTE_ID)
|
||||
)
|
||||
}
|
||||
|
||||
fn should_normalize_mimo_anthropic_thinking_history(
|
||||
provider: &Provider,
|
||||
upstream_model: &str,
|
||||
@@ -1520,9 +1535,55 @@ mod tests {
|
||||
assert_eq!(models["data"][0]["id"], json!("claude-sonnet-4-6"));
|
||||
assert_eq!(models["data"][0]["supports1m"], json!(true));
|
||||
|
||||
let err = map_proxy_request_model(json!({"model": "claude-opus-4-7"}), &provider)
|
||||
let err = map_proxy_request_model(json!({"model": "claude-opus-4-8"}), &provider)
|
||||
.expect_err("unknown route should fail");
|
||||
assert!(err.to_string().contains("claude-opus-4-7"));
|
||||
assert!(err.to_string().contains("claude-opus-4-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_proxy_accepts_opus_4_7_4_8_alias_during_rollout() {
|
||||
let mut provider = proxy_provider("proxy");
|
||||
let current_routes = std::collections::HashMap::from([(
|
||||
"claude-opus-4-8".to_string(),
|
||||
ClaudeDesktopModelRoute {
|
||||
model: "upstream-opus-new".to_string(),
|
||||
label_override: None,
|
||||
supports_1m: Some(true),
|
||||
},
|
||||
)]);
|
||||
provider
|
||||
.meta
|
||||
.as_mut()
|
||||
.expect("meta")
|
||||
.claude_desktop_model_routes = current_routes;
|
||||
|
||||
let mapped = map_proxy_request_model(
|
||||
json!({"model": "claude-opus-4-7", "messages": []}),
|
||||
&provider,
|
||||
)
|
||||
.expect("legacy Opus route should map to current route");
|
||||
assert_eq!(mapped["model"], json!("upstream-opus-new"));
|
||||
|
||||
let legacy_routes = std::collections::HashMap::from([(
|
||||
"claude-opus-4-7".to_string(),
|
||||
ClaudeDesktopModelRoute {
|
||||
model: "upstream-opus-legacy".to_string(),
|
||||
label_override: None,
|
||||
supports_1m: Some(true),
|
||||
},
|
||||
)]);
|
||||
provider
|
||||
.meta
|
||||
.as_mut()
|
||||
.expect("meta")
|
||||
.claude_desktop_model_routes = legacy_routes;
|
||||
|
||||
let mapped = map_proxy_request_model(
|
||||
json!({"model": "claude-opus-4-8", "messages": []}),
|
||||
&provider,
|
||||
)
|
||||
.expect("current Opus route should map to legacy saved route");
|
||||
assert_eq!(mapped["model"], json!("upstream-opus-legacy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1652,12 +1713,12 @@ mod tests {
|
||||
.iter()
|
||||
.find(|route| route.upstream_model == "deepseek-v4-pro")
|
||||
.expect("repaired route");
|
||||
assert_eq!(repaired.route_id, "claude-opus-4-7");
|
||||
assert_eq!(repaired.route_id, "claude-opus-4-8");
|
||||
assert_eq!(repaired.label_override.as_deref(), Some("deepseek-v4-pro"));
|
||||
assert!(repaired.supports_1m);
|
||||
|
||||
let mapped = map_proxy_request_model(
|
||||
json!({"model": "claude-opus-4-7", "messages": []}),
|
||||
json!({"model": "claude-opus-4-8", "messages": []}),
|
||||
&provider,
|
||||
)
|
||||
.expect("map repaired route");
|
||||
|
||||
@@ -907,7 +907,7 @@ mod import_claude_desktop_tests {
|
||||
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
|
||||
assert_eq!(routes.len(), 3);
|
||||
assert_eq!(routes.get("claude-sonnet-4-6").unwrap().model, "GLM-4.6");
|
||||
assert_eq!(routes.get("claude-opus-4-7").unwrap().model, "GLM-4-Air");
|
||||
assert_eq!(routes.get("claude-opus-4-8").unwrap().model, "GLM-4-Air");
|
||||
assert_eq!(routes.get("claude-haiku-4-5").unwrap().model, "GLM-4-Flash");
|
||||
assert_eq!(
|
||||
routes
|
||||
|
||||
@@ -1205,6 +1205,15 @@ impl Database {
|
||||
/// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致
|
||||
fn seed_model_pricing(conn: &Connection) -> Result<(), AppError> {
|
||||
let pricing_data = [
|
||||
// Claude 4.8 系列
|
||||
(
|
||||
"claude-opus-4-8",
|
||||
"Claude Opus 4.8",
|
||||
"5",
|
||||
"25",
|
||||
"0.50",
|
||||
"6.25",
|
||||
),
|
||||
// Claude 4.7 系列
|
||||
(
|
||||
"claude-opus-4-7",
|
||||
|
||||
@@ -805,7 +805,7 @@ mod tests {
|
||||
name: Some("MyHermes".to_string()),
|
||||
endpoint: Some("https://api.example.com/v1".to_string()),
|
||||
api_key: Some("sk-test".to_string()),
|
||||
model: Some("anthropic/claude-opus-4-7".to_string()),
|
||||
model: Some("anthropic/claude-opus-4-8".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -827,7 +827,7 @@ mod tests {
|
||||
// models array with the deeplink model id
|
||||
let models = obj.get("models").unwrap().as_array().unwrap();
|
||||
assert_eq!(models.len(), 1);
|
||||
assert_eq!(models[0]["id"], "anthropic/claude-opus-4-7");
|
||||
assert_eq!(models[0]["id"], "anthropic/claude-opus-4-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//!
|
||||
//! ```yaml
|
||||
//! model:
|
||||
//! default: "anthropic/claude-opus-4-7"
|
||||
//! default: "anthropic/claude-opus-4-8"
|
||||
//! provider: "openrouter"
|
||||
//! base_url: "https://openrouter.ai/api/v1"
|
||||
//!
|
||||
@@ -19,9 +19,9 @@
|
||||
//! - name: openrouter
|
||||
//! base_url: https://openrouter.ai/api/v1
|
||||
//! api_key: sk-or-...
|
||||
//! model: anthropic/claude-opus-4-7
|
||||
//! model: anthropic/claude-opus-4-8
|
||||
//! models:
|
||||
//! anthropic/claude-opus-4-7:
|
||||
//! anthropic/claude-opus-4-8:
|
||||
//! context_length: 200000
|
||||
//!
|
||||
//! mcp_servers:
|
||||
@@ -185,7 +185,7 @@ fn find_yaml_section_range(raw: &str, section_key: &str) -> Option<(usize, usize
|
||||
///
|
||||
/// ```yaml
|
||||
/// model:
|
||||
/// default: "anthropic/claude-opus-4-7"
|
||||
/// default: "anthropic/claude-opus-4-8"
|
||||
/// provider: "openrouter"
|
||||
/// ```
|
||||
fn serialize_yaml_section(key: &str, value: &serde_yaml::Value) -> Result<String, AppError> {
|
||||
@@ -1219,7 +1219,7 @@ agent:
|
||||
let mut m = serde_yaml::Mapping::new();
|
||||
m.insert(
|
||||
serde_yaml::Value::String("default".to_string()),
|
||||
serde_yaml::Value::String("claude-opus-4-7".to_string()),
|
||||
serde_yaml::Value::String("claude-opus-4-8".to_string()),
|
||||
);
|
||||
m.insert(
|
||||
serde_yaml::Value::String("provider".to_string()),
|
||||
@@ -1233,7 +1233,7 @@ agent:
|
||||
assert!(result.contains("agent:"));
|
||||
assert!(result.contains("max_turns"));
|
||||
// And the model section should be updated
|
||||
assert!(result.contains("claude-opus-4-7"));
|
||||
assert!(result.contains("claude-opus-4-8"));
|
||||
assert!(result.contains("anthropic"));
|
||||
assert!(!result.contains("gpt-4"));
|
||||
assert!(!result.contains("openai"));
|
||||
@@ -1517,7 +1517,7 @@ custom_providers:
|
||||
assert!(get_model_config().unwrap().is_none());
|
||||
|
||||
let model = HermesModelConfig {
|
||||
default: Some("anthropic/claude-opus-4-7".to_string()),
|
||||
default: Some("anthropic/claude-opus-4-8".to_string()),
|
||||
provider: Some("openrouter".to_string()),
|
||||
base_url: Some("https://openrouter.ai/api/v1".to_string()),
|
||||
context_length: Some(200000),
|
||||
@@ -1529,7 +1529,7 @@ custom_providers:
|
||||
let read_model = get_model_config().unwrap().unwrap();
|
||||
assert_eq!(
|
||||
read_model.default.as_deref(),
|
||||
Some("anthropic/claude-opus-4-7")
|
||||
Some("anthropic/claude-opus-4-8")
|
||||
);
|
||||
assert_eq!(read_model.provider.as_deref(), Some("openrouter"));
|
||||
assert_eq!(read_model.context_length, Some(200000));
|
||||
|
||||
@@ -324,14 +324,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_falls_back_to_highest_family_version() {
|
||||
// 用户请求 opus 4.7 但 Copilot 账号只有 opus 4.6
|
||||
// 用户请求 opus 4.8 但 Copilot 账号只有 opus 4.6
|
||||
let models = vec![
|
||||
model("claude-opus-4.5"),
|
||||
model("claude-opus-4.6"),
|
||||
model("claude-sonnet-4.6"),
|
||||
];
|
||||
assert_eq!(
|
||||
resolve_against_models("claude-opus-4.7", &models),
|
||||
resolve_against_models("claude-opus-4.8", &models),
|
||||
Some("claude-opus-4.6".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,11 +57,17 @@ pub fn anthropic_to_gemini_with_shadow(
|
||||
.map(|snapshot| snapshot.turns)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(system) = build_system_instruction(body.get("system"))? {
|
||||
let messages = body.get("messages").and_then(|value| value.as_array());
|
||||
|
||||
let system_instruction = build_system_instruction(
|
||||
body.get("system"),
|
||||
messages.map(|messages| messages.as_slice()),
|
||||
)?;
|
||||
if let Some(system) = system_instruction {
|
||||
result["systemInstruction"] = system;
|
||||
}
|
||||
|
||||
if let Some(messages) = body.get("messages").and_then(|value| value.as_array()) {
|
||||
if let Some(messages) = messages {
|
||||
result["contents"] = json!(convert_messages_to_contents(messages, &shadow_turns)?);
|
||||
}
|
||||
|
||||
@@ -269,31 +275,26 @@ pub fn extract_gemini_model(body: &Value) -> Option<&str> {
|
||||
body.get("model").and_then(|value| value.as_str())
|
||||
}
|
||||
|
||||
fn build_system_instruction(system: Option<&Value>) -> Result<Option<Value>, ProxyError> {
|
||||
let Some(system) = system else {
|
||||
return Ok(None);
|
||||
};
|
||||
fn build_system_instruction(
|
||||
system: Option<&Value>,
|
||||
messages: Option<&[Value]>,
|
||||
) -> Result<Option<Value>, ProxyError> {
|
||||
let mut texts = Vec::new();
|
||||
|
||||
if let Some(text) = system.as_str() {
|
||||
if text.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
return Ok(Some(json!({
|
||||
"parts": [{ "text": text }]
|
||||
})));
|
||||
if let Some(system) = system {
|
||||
collect_system_texts(system, &mut texts)?;
|
||||
}
|
||||
|
||||
let Some(blocks) = system.as_array() else {
|
||||
return Err(ProxyError::TransformError(
|
||||
"Anthropic system must be a string or an array".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let texts: Vec<&str> = blocks
|
||||
.iter()
|
||||
.filter_map(|block| block.get("text").and_then(|value| value.as_str()))
|
||||
.filter(|text| !text.is_empty())
|
||||
.collect();
|
||||
if let Some(messages) = messages {
|
||||
for message in messages {
|
||||
if message.get("role").and_then(|value| value.as_str()) != Some("system") {
|
||||
continue;
|
||||
}
|
||||
if let Some(content) = message.get("content") {
|
||||
collect_system_texts(content, &mut texts)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if texts.is_empty() {
|
||||
return Ok(None);
|
||||
@@ -304,6 +305,31 @@ fn build_system_instruction(system: Option<&Value>) -> Result<Option<Value>, Pro
|
||||
})))
|
||||
}
|
||||
|
||||
fn collect_system_texts(value: &Value, texts: &mut Vec<String>) -> Result<(), ProxyError> {
|
||||
if let Some(text) = value.as_str() {
|
||||
if !text.is_empty() {
|
||||
texts.push(text.to_string());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(blocks) = value.as_array() else {
|
||||
return Err(ProxyError::TransformError(
|
||||
"Anthropic system must be a string or an array".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
texts.extend(
|
||||
blocks
|
||||
.iter()
|
||||
.filter_map(|block| block.get("text").and_then(|value| value.as_str()))
|
||||
.filter(|text| !text.is_empty())
|
||||
.map(ToString::to_string),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_generation_config(body: &Value) -> Option<Value> {
|
||||
let mut config = Map::new();
|
||||
|
||||
@@ -383,6 +409,9 @@ fn convert_messages_to_contents(
|
||||
.get("role")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("user");
|
||||
if role == "system" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let gemini_role = if role == "assistant" { "model" } else { "user" };
|
||||
|
||||
@@ -1149,6 +1178,32 @@ mod tests {
|
||||
assert_eq!(result["generationConfig"]["maxOutputTokens"], 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_to_gemini_merges_system_messages_into_system_instruction() {
|
||||
let input = json!({
|
||||
"model": "gemini-3-pro",
|
||||
"system": [{ "type": "text", "text": "Top level system." }],
|
||||
"messages": [
|
||||
{ "role": "system", "content": "Message system." },
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{ "type": "text", "text": "Block system." }]
|
||||
},
|
||||
{ "role": "user", "content": "Hello" }
|
||||
]
|
||||
});
|
||||
|
||||
let result = anthropic_to_gemini(input).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result["systemInstruction"]["parts"][0]["text"],
|
||||
"Top level system.\n\nMessage system.\n\nBlock system."
|
||||
);
|
||||
assert_eq!(result["contents"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(result["contents"][0]["role"], "user");
|
||||
assert_eq!(result["contents"][0]["parts"][0]["text"], "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_to_gemini_maps_tools_and_tool_results() {
|
||||
let input = json!({
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde_json::{json, Value};
|
||||
///
|
||||
/// 三路径分发:
|
||||
/// - skip: haiku 模型直接跳过
|
||||
/// - adaptive: opus-4-7 / opus-4-6 / sonnet-4-6 使用 adaptive thinking
|
||||
/// - adaptive: opus-4-8 / opus-4-7 / opus-4-6 / sonnet-4-6 使用 adaptive thinking
|
||||
/// - legacy: 其他模型注入 enabled thinking + budget_tokens
|
||||
pub fn optimize(body: &mut Value, config: &OptimizerConfig) {
|
||||
if !config.thinking_optimizer {
|
||||
@@ -24,7 +24,7 @@ pub fn optimize(body: &mut Value, config: &OptimizerConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
if model.contains("opus-4-7") || model.contains("opus-4-6") || model.contains("sonnet-4-6") {
|
||||
if uses_adaptive_thinking(&model) {
|
||||
log::info!("[OPT] thinking: adaptive({model})");
|
||||
body["thinking"] = json!({"type": "adaptive"});
|
||||
body["output_config"] = json!({"effort": "max"});
|
||||
@@ -73,6 +73,13 @@ pub fn optimize(body: &mut Value, config: &OptimizerConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
fn uses_adaptive_thinking(model: &str) -> bool {
|
||||
let normalized = model.replace('.', "-");
|
||||
["opus-4-8", "opus-4-7", "opus-4-6", "sonnet-4-6"]
|
||||
.iter()
|
||||
.any(|needle| normalized.contains(needle))
|
||||
}
|
||||
|
||||
/// 追加 beta 标识到 anthropic_beta 数组(去重)
|
||||
fn append_beta(body: &mut Value, beta: &str) {
|
||||
match body.get_mut("anthropic_beta") {
|
||||
@@ -114,6 +121,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_opus_4_8() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic/claude-opus-4.8",
|
||||
"max_tokens": 16384,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 8000},
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
assert!(body["thinking"].get("budget_tokens").is_none());
|
||||
assert_eq!(body["output_config"]["effort"], "max");
|
||||
let betas = body["anthropic_beta"].as_array().unwrap();
|
||||
assert!(betas.iter().any(|v| v == "context-1m-2025-08-07"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_opus_4_6() {
|
||||
let mut body = json!({
|
||||
|
||||
@@ -41,7 +41,7 @@ const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 9] = [
|
||||
|
||||
const CLAUDE_TAKEOVER_HAIKU_MODEL: &str = "claude-haiku-4-5";
|
||||
const CLAUDE_TAKEOVER_SONNET_MODEL: &str = "claude-sonnet-4-6";
|
||||
const CLAUDE_TAKEOVER_OPUS_MODEL: &str = "claude-opus-4-7";
|
||||
const CLAUDE_TAKEOVER_OPUS_MODEL: &str = "claude-opus-4-8";
|
||||
// 写给 Claude Code 时沿用文档示例的大写形式;解析侧大小写不敏感。
|
||||
const CLAUDE_ONE_M_MARKER_FOR_CLIENT: &str = "[1M]";
|
||||
|
||||
@@ -2451,7 +2451,7 @@ mod tests {
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
||||
Some("claude-sonnet-4.6"),
|
||||
);
|
||||
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-7"));
|
||||
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-8"));
|
||||
assert_env_str(
|
||||
env,
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
@@ -2522,7 +2522,7 @@ mod tests {
|
||||
Some("claude-sonnet-4-6"),
|
||||
);
|
||||
assert_env_str(env, "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", Some("gpt-5.4"));
|
||||
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-7"));
|
||||
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-8"));
|
||||
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", Some("gpt-5.4"));
|
||||
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
|
||||
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
|
||||
@@ -3170,7 +3170,7 @@ model = "gpt-5.1-codex"
|
||||
live_env
|
||||
.get("ANTHROPIC_DEFAULT_OPUS_MODEL")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("claude-opus-4-7[1M]"),
|
||||
Some("claude-opus-4-8[1M]"),
|
||||
"Opus role should preserve the current provider 1M capability marker"
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -3214,6 +3214,11 @@ mod tests {
|
||||
result.is_some(),
|
||||
"Claude Desktop 短路由 claude-haiku-4-5 应能匹配到 claude-haiku-4-5-20251001"
|
||||
);
|
||||
let result = find_model_pricing_row(&conn, "anthropic/claude-opus-4.8")?;
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"聚合商点号格式 anthropic/claude-opus-4.8 应能匹配到 claude-opus-4-8"
|
||||
);
|
||||
|
||||
// Claude Desktop 旧版/异常包装的非 Anthropic route:claude-gpt-5.5 → gpt-5.5
|
||||
let result = find_model_pricing_row(&conn, "claude-gpt-5.5")?;
|
||||
@@ -3229,6 +3234,16 @@ mod tests {
|
||||
result.is_some(),
|
||||
"Bedrock/Vertex 风格 Claude 模型 ID 应能归一化到基础 Claude 模型定价"
|
||||
);
|
||||
let result = find_model_pricing_row(&conn, "global.anthropic.claude-opus-4-8-v1:0")?;
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Bedrock 风格 Claude Opus 4.8 模型 ID 应能归一化到基础 Claude 模型定价"
|
||||
);
|
||||
let result = find_model_pricing_row(&conn, "claude-opus-4-8@20260527")?;
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Vertex 风格 Claude Opus 4.8 模型 ID 应能归一化到基础 Claude 模型定价"
|
||||
);
|
||||
|
||||
// Reasoning effort 后缀:没有专门价格时回退到基础模型
|
||||
let result = find_model_pricing_row(&conn, "gpt-5.4@low")?;
|
||||
|
||||
Reference in New Issue
Block a user