mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
fix(proxy/gemini): gate /v1beta behind Google host + normalize models/ model id prefix
Two related P2 corrections to the Gemini Native URL surface, both folding into the existing Google-host-whitelist architecture. ## P2a — `/v1beta` suffix should not unconditionally trigger rewrite `should_normalize_gemini_full_url` placed `/v1beta` and `/v1beta/models` in the unconditional layer on the reasoning that `/v1beta` is Google-specific. In practice an opaque relay fronting a non-Gemini service at `https://relay.example/custom/v1beta` would still be silently rewritten to `/v1beta/models/{model}:generateContent`, breaking the deployment. Move `/v1beta`, `/v1beta/models`, and `/v1beta/openai` into the Google-host gated layer alongside `/v1`, `/models`, and friends. The unconditional layer now only accepts paths whose grammar is intrinsically Gemini — `/models/...:generateContent` method calls and the deep OpenAI-compat endpoints like `/openai/chat/completions` and `/openai/responses`. Pasted AI-Studio URLs such as `https://generativelanguage.googleapis.com/v1beta` still normalize because the host matches the whitelist. ## P2b — `model: "models/gemini-2.5-pro"` produced doubled path prefix Gemini SDKs (and the official `list_models` response) commonly surface model ids in resource-name form `models/gemini-2.5-pro`. Raw interpolation into `format!("/v1beta/models/{model}:...")` produced `/v1beta/models/models/gemini-2.5-pro:streamGenerateContent` which upstream rejects — yielding false-negative health checks for otherwise valid provider configs. Introduce `normalize_gemini_model_id(&str) -> &str` in `gemini_url` as the single source of truth: strips an optional leading `/` then an optional `models/` prefix, leaving bare ids untouched. Apply in the three call sites that build a Gemini method URL: - `services/stream_check.rs::resolve_claude_stream_url` (unified path) - `services/stream_check.rs::check_gemini_stream` (Gemini-only path) - `proxy/forwarder.rs::rewrite_claude_transform_endpoint` (production) Tests (9 new): - `gemini_url`: 3 regressions for opaque vs Google-host `/v1beta*` handling + 5 unit tests pinning `normalize_gemini_model_id` behavior (strip prefix, leave bare id, preserve nested slashes past the one stripped prefix, tolerate leading slash, pass through empty input). - `stream_check`: one end-to-end regression confirming `models/gemini-2.5-pro` collapses to the expected single-prefix URL. - `forwarder`: one end-to-end regression on the production rewrite path. All 864 lib tests pass; cargo fmt + clippy -D warnings clean. Addresses Codex P2 feedback on #1918.
This commit is contained in:
@@ -12,7 +12,7 @@ use std::time::Instant;
|
||||
use crate::app_config::AppType;
|
||||
use crate::error::AppError;
|
||||
use crate::provider::Provider;
|
||||
use crate::proxy::gemini_url::resolve_gemini_native_url;
|
||||
use crate::proxy::gemini_url::{normalize_gemini_model_id, resolve_gemini_native_url};
|
||||
use crate::proxy::providers::copilot_auth;
|
||||
use crate::proxy::providers::transform::anthropic_to_openai;
|
||||
use crate::proxy::providers::transform_gemini::anthropic_to_gemini;
|
||||
@@ -598,13 +598,16 @@ impl StreamCheckService {
|
||||
extra_headers: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
) -> Result<(u16, String), AppError> {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
// Strip `models/` resource-name prefix from the model id — see
|
||||
// `normalize_gemini_model_id` for rationale.
|
||||
let normalized_model = normalize_gemini_model_id(model);
|
||||
// Gemini 原生 API: /v1beta/models/{model}:streamGenerateContent?alt=sse
|
||||
// 智能处理 /v1beta 路径:如果 base_url 不包含版本路径,则添加 /v1beta
|
||||
// alt=sse 参数使 API 返回 SSE 格式(text/event-stream)而非 JSON 数组
|
||||
let url = if base.contains("/v1beta") || base.contains("/v1/") {
|
||||
format!("{base}/models/{model}:streamGenerateContent?alt=sse")
|
||||
format!("{base}/models/{normalized_model}:streamGenerateContent?alt=sse")
|
||||
} else {
|
||||
format!("{base}/v1beta/models/{model}:streamGenerateContent?alt=sse")
|
||||
format!("{base}/v1beta/models/{normalized_model}:streamGenerateContent?alt=sse")
|
||||
};
|
||||
|
||||
// Gemini 原生请求体格式
|
||||
@@ -1325,7 +1328,13 @@ impl StreamCheckService {
|
||||
model: &str,
|
||||
) -> String {
|
||||
if api_format == "gemini_native" {
|
||||
let endpoint = format!("/v1beta/models/{model}:streamGenerateContent?alt=sse");
|
||||
// Strip an optional `models/` resource-name prefix so that model
|
||||
// identifiers copied from Gemini SDK outputs (e.g.
|
||||
// `models/gemini-2.5-pro`) don't produce a doubled
|
||||
// `/v1beta/models/models/...` URL.
|
||||
let normalized_model = normalize_gemini_model_id(model);
|
||||
let endpoint =
|
||||
format!("/v1beta/models/{normalized_model}:streamGenerateContent?alt=sse");
|
||||
return resolve_gemini_native_url(base_url, &endpoint, is_full_url);
|
||||
}
|
||||
|
||||
@@ -1773,6 +1782,27 @@ mod tests {
|
||||
assert_eq!(url, "https://relay.example/custom/generate-content?alt=sse");
|
||||
}
|
||||
|
||||
/// Regression: Gemini SDK outputs commonly surface model ids as the
|
||||
/// resource-name form `models/gemini-2.5-pro`. Interpolating that raw
|
||||
/// value used to produce `/v1beta/models/models/gemini-2.5-pro:...`
|
||||
/// which the upstream rejects and the health check records as a
|
||||
/// false-negative for an otherwise valid provider.
|
||||
#[test]
|
||||
fn test_resolve_claude_stream_url_for_gemini_native_strips_models_prefix() {
|
||||
let url = StreamCheckService::resolve_claude_stream_url(
|
||||
"https://generativelanguage.googleapis.com",
|
||||
AuthStrategy::Google,
|
||||
"gemini_native",
|
||||
false,
|
||||
"models/gemini-2.5-pro",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_stream_urls_for_full_url_mode() {
|
||||
let urls = StreamCheckService::resolve_codex_stream_urls(
|
||||
|
||||
Reference in New Issue
Block a user