diff --git a/agent/gemini_native_adapter.py b/agent/gemini_native_adapter.py index d8e6c63c48e..a79effebba4 100644 --- a/agent/gemini_native_adapter.py +++ b/agent/gemini_native_adapter.py @@ -41,6 +41,16 @@ DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" 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: """Return True when the endpoint speaks Gemini's native REST API.""" normalized = str(base_url or "").strip().rstrip("/").lower() @@ -914,6 +924,7 @@ class GeminiNativeClient: thinking_config=thinking_config, ) + model = bare_gemini_model_id(model) if stream: return self._stream_completion(model=model, request=request, timeout=timeout) diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py index d7f8f3ea22e..2c4988cc76e 100644 --- a/hermes_cli/model_normalize.py +++ b/hermes_cli/model_normalize.py @@ -84,7 +84,6 @@ _STRIP_VENDOR_ONLY_PROVIDERS: frozenset[str] = frozenset({ # Providers whose native naming is authoritative -- pass through unchanged. _AUTHORITATIVE_NATIVE_PROVIDERS: frozenset[str] = frozenset({ - "gemini", "huggingface", }) @@ -103,6 +102,8 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({ "arcee", "ollama-cloud", "custom", + "gemini", + "xai", }) # Providers whose APIs require lowercase model IDs. Xiaomi's diff --git a/tests/agent/test_gemini_native_adapter.py b/tests/agent/test_gemini_native_adapter.py index 703428d4eb7..aa9b2a38a52 100644 --- a/tests/agent/test_gemini_native_adapter.py +++ b/tests/agent/test_gemini_native_adapter.py @@ -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" +@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(): from agent.gemini_native_adapter import gemini_http_error diff --git a/tests/hermes_cli/test_gemini_provider.py b/tests/hermes_cli/test_gemini_provider.py index 61d7bc48ebb..0b56abfa9fe 100644 --- a/tests/hermes_cli/test_gemini_provider.py +++ b/tests/hermes_cli/test_gemini_provider.py @@ -143,7 +143,8 @@ class TestGeminiModelNormalization: assert normalize_model_for_provider("gemini-2.5-flash", "gemini") == "gemini-2.5-flash" 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): assert detect_vendor("gemma-4-31b-it") == "google" diff --git a/tests/hermes_cli/test_model_normalize.py b/tests/hermes_cli/test_model_normalize.py index 7e4a6d22e87..77ece210115 100644 --- a/tests/hermes_cli/test_model_normalize.py +++ b/tests/hermes_cli/test_model_normalize.py @@ -167,10 +167,13 @@ class TestAggregatorProviders: class TestIssue6211NativeProviderPrefixNormalization: @pytest.mark.parametrize("model,target_provider,expected", [ ("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"), ("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"), + ("openai/gpt-5.4", "xai", "openai/gpt-5.4"), ("modal/zai-org/GLM-5-FP8", "custom", "modal/zai-org/GLM-5-FP8"), ]) def test_native_provider_prefixes_are_only_stripped_on_matching_provider(