mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 04:02:02 +08:00
fix(copilot): resolve Claude model IDs against live /models list
Copilot upstream returns model_not_supported when the client sends dash-form Claude IDs (claude-sonnet-4-6, claude-sonnet-4-6[1m]) while /models only accepts dot form (claude-sonnet-4.6, -1m suffix). - Add copilot_model_map: syntax normalize (dash->dot, [1m]->-1m) plus live /models exact match and family-version fallback, reusing the existing 5 min auth cache. Returns None when the whole family is absent so upstream surfaces an explicit error instead of silently switching families. - Wire into forwarder Copilot hook; runs before anthropic_to_openai conversion. - Default Opus slot in the Copilot preset maps to Sonnet 4.6: Pro dropped all Opus on 2026-04-20 and Pro+ bills Opus 4.7 at 7.5x. Users who want real Opus can switch manually in the UI. Refs: https://github.com/farion1231/cc-switch/issues/2016
This commit is contained in:
@@ -790,6 +790,13 @@ impl RequestForwarder {
|
||||
== Some("github_copilot")
|
||||
|| base_url.contains("githubcopilot.com");
|
||||
|
||||
if is_copilot {
|
||||
mapped_body =
|
||||
super::providers::copilot_model_map::apply_copilot_model_normalization(mapped_body);
|
||||
self.apply_copilot_live_model_resolution(provider, &mut mapped_body)
|
||||
.await;
|
||||
}
|
||||
|
||||
// --- Copilot 优化器:分类 + 请求体优化(在格式转换之前执行) ---
|
||||
// 注意:确定性 ID 也在此处计算,因为 mapped_body 在格式转换时会被 move
|
||||
//
|
||||
@@ -1485,6 +1492,49 @@ impl RequestForwarder {
|
||||
"openai_chat".to_string()
|
||||
}
|
||||
|
||||
/// 用 Copilot live `/models` 列表确认 model ID 真实可用,找不到时按 family 降级。
|
||||
/// 命中缓存后是同步的;首次请求或 5 min 缓存过期后会触发一次 HTTP。
|
||||
async fn apply_copilot_live_model_resolution(
|
||||
&self,
|
||||
provider: &Provider,
|
||||
body: &mut serde_json::Value,
|
||||
) {
|
||||
let Some(model_id) = body.get("model").and_then(|v| v.as_str()) else {
|
||||
return;
|
||||
};
|
||||
let model_id = model_id.to_string();
|
||||
|
||||
let Some(app_handle) = &self.app_handle else {
|
||||
return;
|
||||
};
|
||||
let copilot_state = app_handle.state::<CopilotAuthState>();
|
||||
let copilot_auth = copilot_state.0.read().await;
|
||||
let account_id = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.managed_account_id_for("github_copilot"));
|
||||
|
||||
let models_result = match account_id.as_deref() {
|
||||
Some(id) => copilot_auth.fetch_models_for_account(id).await,
|
||||
None => copilot_auth.fetch_models().await,
|
||||
};
|
||||
|
||||
let models = match models_result {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
log::debug!("[Copilot] live model list unavailable, skip resolution: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(resolved) =
|
||||
super::providers::copilot_model_map::resolve_against_models(&model_id, &models)
|
||||
{
|
||||
log::info!("[Copilot] live-model resolve: {model_id} → {resolved}");
|
||||
body["model"] = serde_json::Value::String(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_copilot_openai_vendor_model(&self, provider: &Provider, model_id: &str) -> bool {
|
||||
let Some(app_handle) = &self.app_handle else {
|
||||
log::debug!("[Copilot] AppHandle unavailable, fallback to chat/completions");
|
||||
|
||||
Reference in New Issue
Block a user