mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 04:08:28 +08:00
Compare commits
2 Commits
bb/version
...
openclaw-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e289dbf163 | ||
|
|
0f37e051da |
@@ -33,6 +33,39 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
# Self prefixes that may ride along on a model id when a user copies an
|
||||
# aggregator-style slug (``google/gemini-2.0-flash``) into config.yaml, or
|
||||
# when an upstream resolver leaves the provider prefix attached. Google's
|
||||
# native ``models/{model}:generateContent`` endpoint expects a bare model id
|
||||
# (``gemini-2.0-flash``); a self-prefixed value produces a malformed URL like
|
||||
# ``models/google/gemini-2.0-flash:generateContent`` and a 404. We strip only
|
||||
# prefixes that name THIS provider so genuine slash-bearing ids are untouched.
|
||||
_GEMINI_SELF_PREFIXES = ("google/", "gemini/")
|
||||
|
||||
|
||||
def bare_gemini_model_id(model: str) -> str:
|
||||
"""Strip a leading ``google/`` or ``gemini/`` self prefix from a model id.
|
||||
|
||||
Only the provider's own prefix is removed, case-insensitively. Any other
|
||||
slash-bearing value (or a bare id) passes through unchanged so we never
|
||||
mangle a legitimately namespaced model name.
|
||||
|
||||
Examples::
|
||||
|
||||
>>> bare_gemini_model_id("google/gemini-2.0-flash")
|
||||
'gemini-2.0-flash'
|
||||
>>> bare_gemini_model_id("gemini/gemini-3-pro-preview")
|
||||
'gemini-3-pro-preview'
|
||||
>>> bare_gemini_model_id("gemini-2.5-flash")
|
||||
'gemini-2.5-flash'
|
||||
"""
|
||||
name = (model or "").strip()
|
||||
lowered = name.lower()
|
||||
for prefix in _GEMINI_SELF_PREFIXES:
|
||||
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."""
|
||||
@@ -895,6 +928,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)
|
||||
|
||||
|
||||
@@ -83,8 +83,9 @@ _STRIP_VENDOR_ONLY_PROVIDERS: frozenset[str] = frozenset({
|
||||
})
|
||||
|
||||
# Providers whose native naming is authoritative -- pass through unchanged.
|
||||
# HuggingFace ids are legitimately ``org/model`` (e.g. ``Qwen/Qwen3.5-397B``),
|
||||
# so we must NOT strip the leading segment for it.
|
||||
_AUTHORITATIVE_NATIVE_PROVIDERS: frozenset[str] = frozenset({
|
||||
"gemini",
|
||||
"huggingface",
|
||||
})
|
||||
|
||||
@@ -103,6 +104,13 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({
|
||||
"arcee",
|
||||
"ollama-cloud",
|
||||
"custom",
|
||||
# Native Google (Gemini) and xAI APIs take bare model ids. A self-prefixed
|
||||
# value like ``google/gemini-2.0-flash`` or ``xai/grok-4`` — easily pasted
|
||||
# from an aggregator slug into config.yaml — must have the matching prefix
|
||||
# stripped before the native call, or the request 400/404s. HuggingFace is
|
||||
# deliberately NOT here: its ids are legitimately ``org/model``.
|
||||
"gemini",
|
||||
"xai",
|
||||
})
|
||||
|
||||
# Providers whose APIs require lowercase model IDs. Xiaomi's
|
||||
|
||||
@@ -198,6 +198,66 @@ 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_in,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"), # prefix match is case-insensitive; model casing preserved
|
||||
("gemini-2.5-flash", "gemini-2.5-flash"), # bare id unchanged
|
||||
("models/gemini-x", "models/gemini-x"), # non-self prefix unchanged
|
||||
("tunedModels/my-tune", "tunedModels/my-tune"), # legit Gemini namespaced id unchanged
|
||||
("", ""),
|
||||
])
|
||||
def test_bare_gemini_model_id_strips_only_self_prefix(model_in, expected):
|
||||
from agent.gemini_native_adapter import bare_gemini_model_id
|
||||
|
||||
assert bare_gemini_model_id(model_in) == expected
|
||||
|
||||
|
||||
def test_native_client_strips_self_prefix_from_model_url(monkeypatch):
|
||||
"""A self-prefixed model id must resolve to a bare native resource path.
|
||||
|
||||
Port of openclaw/openclaw#88781. Before the fix, ``google/gemini-2.0-flash``
|
||||
built ``models/google/gemini-2.0-flash:generateContent`` — an invalid
|
||||
Google resource name that 404s. The adapter now strips the matching
|
||||
self-prefix at URL construction so the request reaches the real model.
|
||||
"""
|
||||
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"
|
||||
)
|
||||
assert "models/google/" not in recorded["url"]
|
||||
|
||||
|
||||
def test_native_http_error_keeps_status_and_retry_after():
|
||||
from agent.gemini_native_adapter import gemini_http_error
|
||||
|
||||
|
||||
@@ -143,7 +143,11 @@ 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"
|
||||
# Native Gemini takes a bare model id; a self-prefix (``google/`` or
|
||||
# ``gemini/``) must be stripped before the generateContent call, or the
|
||||
# request 404s on a malformed ``models/google/...`` resource path.
|
||||
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"
|
||||
|
||||
@@ -167,10 +167,19 @@ 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"),
|
||||
# Native Gemini/xAI now strip the matching self-prefix before the
|
||||
# native API call (port of openclaw/openclaw#88781). Sending
|
||||
# ``google/gemini-2.5-pro`` to generativelanguage.googleapis.com builds
|
||||
# a malformed ``models/google/...`` resource path and 404s.
|
||||
("google/gemini-2.5-pro", "gemini", "gemini-2.5-pro"),
|
||||
("gemini/gemini-2.5-pro", "gemini", "gemini-2.5-pro"),
|
||||
("xai/grok-4-fast-reasoning", "xai", "grok-4-fast-reasoning"),
|
||||
("moonshot/kimi-k2.5", "kimi-coding", "kimi-k2.5"),
|
||||
("anthropic/claude-sonnet-4.6", "openrouter", "anthropic/claude-sonnet-4.6"),
|
||||
# HuggingFace ids are legitimately ``org/model`` — never stripped.
|
||||
("Qwen/Qwen3.5-397B-A17B", "huggingface", "Qwen/Qwen3.5-397B-A17B"),
|
||||
# Cross-vendor prefixes on a native provider stay untouched.
|
||||
("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(
|
||||
|
||||
Reference in New Issue
Block a user