fix(gemini): strip native self prefixes before generateContent (#36141)
Strip `google/` and `gemini/` self-prefixes before native Gemini generateContent calls, and keep provider-normalization expectations aligned.
This commit is contained in:
parent
7d11fa4e9e
commit
c8e5f34f24
@ -41,6 +41,16 @@ DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
|||||||
GEMINI_DEFAULT_MAX_OUTPUT_TOKENS = 65535
|
GEMINI_DEFAULT_MAX_OUTPUT_TOKENS = 65535
|
||||||
|
|
||||||
|
|
||||||
|
def bare_gemini_model_id(model: str) -> str:
|
||||||
|
"""Strip Gemini's own provider prefix from an aggregator-style model id."""
|
||||||
|
name = (model or "").strip()
|
||||||
|
lowered = name.lower()
|
||||||
|
for prefix in ("google/", "gemini/"):
|
||||||
|
if lowered.startswith(prefix):
|
||||||
|
return name[len(prefix):].strip() or name
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def is_native_gemini_base_url(base_url: str) -> bool:
|
def is_native_gemini_base_url(base_url: str) -> bool:
|
||||||
"""Return True when the endpoint speaks Gemini's native REST API."""
|
"""Return True when the endpoint speaks Gemini's native REST API."""
|
||||||
normalized = str(base_url or "").strip().rstrip("/").lower()
|
normalized = str(base_url or "").strip().rstrip("/").lower()
|
||||||
@ -914,6 +924,7 @@ class GeminiNativeClient:
|
|||||||
thinking_config=thinking_config,
|
thinking_config=thinking_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model = bare_gemini_model_id(model)
|
||||||
if stream:
|
if stream:
|
||||||
return self._stream_completion(model=model, request=request, timeout=timeout)
|
return self._stream_completion(model=model, request=request, timeout=timeout)
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,6 @@ _STRIP_VENDOR_ONLY_PROVIDERS: frozenset[str] = frozenset({
|
|||||||
|
|
||||||
# Providers whose native naming is authoritative -- pass through unchanged.
|
# Providers whose native naming is authoritative -- pass through unchanged.
|
||||||
_AUTHORITATIVE_NATIVE_PROVIDERS: frozenset[str] = frozenset({
|
_AUTHORITATIVE_NATIVE_PROVIDERS: frozenset[str] = frozenset({
|
||||||
"gemini",
|
|
||||||
"huggingface",
|
"huggingface",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -103,6 +102,8 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({
|
|||||||
"arcee",
|
"arcee",
|
||||||
"ollama-cloud",
|
"ollama-cloud",
|
||||||
"custom",
|
"custom",
|
||||||
|
"gemini",
|
||||||
|
"xai",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Providers whose APIs require lowercase model IDs. Xiaomi's
|
# Providers whose APIs require lowercase model IDs. Xiaomi's
|
||||||
|
|||||||
@ -198,6 +198,45 @@ def test_native_client_uses_x_goog_api_key_and_native_models_endpoint(monkeypatc
|
|||||||
assert response.choices[0].message.content == "hello"
|
assert response.choices[0].message.content == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("model, expected", [
|
||||||
|
("google/gemini-2.0-flash", "gemini-2.0-flash"),
|
||||||
|
("gemini/gemini-3-pro-preview", "gemini-3-pro-preview"),
|
||||||
|
("Google/Gemini-2.5-Pro", "Gemini-2.5-Pro"),
|
||||||
|
("models/gemini-x", "models/gemini-x"),
|
||||||
|
("tunedModels/my-tune", "tunedModels/my-tune"),
|
||||||
|
])
|
||||||
|
def test_bare_gemini_model_id_strips_only_self_prefix(model, expected):
|
||||||
|
from agent.gemini_native_adapter import bare_gemini_model_id
|
||||||
|
|
||||||
|
assert bare_gemini_model_id(model) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_client_strips_self_prefix_from_model_url(monkeypatch):
|
||||||
|
from agent.gemini_native_adapter import GeminiNativeClient
|
||||||
|
|
||||||
|
recorded = {}
|
||||||
|
|
||||||
|
class DummyHTTP:
|
||||||
|
def post(self, url, json=None, headers=None, timeout=None):
|
||||||
|
recorded["url"] = url
|
||||||
|
return DummyResponse(payload={
|
||||||
|
"candidates": [{"content": {"parts": [{"text": "ok"}]}, "finishReason": "STOP"}],
|
||||||
|
"usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2},
|
||||||
|
})
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("agent.gemini_native_adapter.httpx.Client", lambda *a, **k: DummyHTTP())
|
||||||
|
client = GeminiNativeClient(api_key="AIza-test", base_url="https://generativelanguage.googleapis.com/v1beta")
|
||||||
|
client.chat.completions.create(
|
||||||
|
model="google/gemini-2.0-flash",
|
||||||
|
messages=[{"role": "user", "content": "Hello"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert recorded["url"] == "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
|
||||||
|
|
||||||
|
|
||||||
def test_native_http_error_keeps_status_and_retry_after():
|
def test_native_http_error_keeps_status_and_retry_after():
|
||||||
from agent.gemini_native_adapter import gemini_http_error
|
from agent.gemini_native_adapter import gemini_http_error
|
||||||
|
|
||||||
|
|||||||
@ -143,7 +143,8 @@ class TestGeminiModelNormalization:
|
|||||||
assert normalize_model_for_provider("gemini-2.5-flash", "gemini") == "gemini-2.5-flash"
|
assert normalize_model_for_provider("gemini-2.5-flash", "gemini") == "gemini-2.5-flash"
|
||||||
|
|
||||||
def test_strip_vendor_prefix(self):
|
def test_strip_vendor_prefix(self):
|
||||||
assert normalize_model_for_provider("google/gemini-2.5-flash", "gemini") == "google/gemini-2.5-flash"
|
assert normalize_model_for_provider("google/gemini-2.5-flash", "gemini") == "gemini-2.5-flash"
|
||||||
|
assert normalize_model_for_provider("gemini/gemini-2.5-flash", "gemini") == "gemini-2.5-flash"
|
||||||
|
|
||||||
def test_gemma_vendor_detection(self):
|
def test_gemma_vendor_detection(self):
|
||||||
assert detect_vendor("gemma-4-31b-it") == "google"
|
assert detect_vendor("gemma-4-31b-it") == "google"
|
||||||
|
|||||||
@ -167,10 +167,13 @@ class TestAggregatorProviders:
|
|||||||
class TestIssue6211NativeProviderPrefixNormalization:
|
class TestIssue6211NativeProviderPrefixNormalization:
|
||||||
@pytest.mark.parametrize("model,target_provider,expected", [
|
@pytest.mark.parametrize("model,target_provider,expected", [
|
||||||
("zai/glm-5.1", "zai", "glm-5.1"),
|
("zai/glm-5.1", "zai", "glm-5.1"),
|
||||||
("google/gemini-2.5-pro", "gemini", "google/gemini-2.5-pro"),
|
("google/gemini-2.5-pro", "gemini", "gemini-2.5-pro"),
|
||||||
|
("gemini/gemini-2.5-pro", "gemini", "gemini-2.5-pro"),
|
||||||
("moonshot/kimi-k2.5", "kimi-coding", "kimi-k2.5"),
|
("moonshot/kimi-k2.5", "kimi-coding", "kimi-k2.5"),
|
||||||
("anthropic/claude-sonnet-4.6", "openrouter", "anthropic/claude-sonnet-4.6"),
|
("anthropic/claude-sonnet-4.6", "openrouter", "anthropic/claude-sonnet-4.6"),
|
||||||
|
("x-ai/grok-4-fast-reasoning", "xai", "grok-4-fast-reasoning"),
|
||||||
("Qwen/Qwen3.5-397B-A17B", "huggingface", "Qwen/Qwen3.5-397B-A17B"),
|
("Qwen/Qwen3.5-397B-A17B", "huggingface", "Qwen/Qwen3.5-397B-A17B"),
|
||||||
|
("openai/gpt-5.4", "xai", "openai/gpt-5.4"),
|
||||||
("modal/zai-org/GLM-5-FP8", "custom", "modal/zai-org/GLM-5-FP8"),
|
("modal/zai-org/GLM-5-FP8", "custom", "modal/zai-org/GLM-5-FP8"),
|
||||||
])
|
])
|
||||||
def test_native_provider_prefixes_are_only_stripped_on_matching_provider(
|
def test_native_provider_prefixes_are_only_stripped_on_matching_provider(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user