mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 15:31:38 +08:00
Compare commits
2 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85552aa259 | ||
|
|
4c4606c88b |
9
cli.py
9
cli.py
@@ -2710,6 +2710,15 @@ class HermesCLI:
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
|
||||
# When a custom_provider entry carries an explicit `model` field,
|
||||
# use it as the effective model name. Without this, running
|
||||
# `hermes chat --model <provider-name>` sends the provider name
|
||||
# (e.g. "my-provider") as the model string to the API instead of
|
||||
# the configured model (e.g. "qwen3.6-plus"), causing 400 errors.
|
||||
runtime_model = runtime.get("model")
|
||||
if runtime_model and isinstance(runtime_model, str):
|
||||
self.model = runtime_model
|
||||
|
||||
# Normalize model for the resolved provider (e.g. swap non-Codex
|
||||
# models when provider is openai-codex). Fixes #651.
|
||||
model_changed = self._normalize_model_for_provider(resolved_provider)
|
||||
|
||||
@@ -1497,7 +1497,7 @@ _KNOWN_ROOT_KEYS = {
|
||||
|
||||
# Valid fields inside a custom_providers list entry
|
||||
_VALID_CUSTOM_PROVIDER_FIELDS = {
|
||||
"name", "base_url", "api_key", "api_mode", "models",
|
||||
"name", "base_url", "api_key", "api_mode", "model", "models",
|
||||
"context_length", "rate_limit_delay",
|
||||
}
|
||||
|
||||
|
||||
@@ -304,6 +304,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
|
||||
api_mode = _parse_api_mode(entry.get("api_mode"))
|
||||
if api_mode:
|
||||
result["api_mode"] = api_mode
|
||||
model_name = str(entry.get("model", "") or "").strip()
|
||||
if model_name:
|
||||
result["model"] = model_name
|
||||
return result
|
||||
|
||||
return None
|
||||
@@ -329,6 +332,11 @@ def _resolve_named_custom_runtime(
|
||||
# Check if a credential pool exists for this custom endpoint
|
||||
pool_result = _try_resolve_from_custom_pool(base_url, "custom", custom_provider.get("api_mode"))
|
||||
if pool_result:
|
||||
# Propagate the model name even when using pooled credentials —
|
||||
# the pool doesn't know about the custom_providers model field.
|
||||
model_name = custom_provider.get("model")
|
||||
if model_name:
|
||||
pool_result["model"] = model_name
|
||||
return pool_result
|
||||
|
||||
api_key_candidates = [
|
||||
@@ -339,7 +347,7 @@ def _resolve_named_custom_runtime(
|
||||
]
|
||||
api_key = next((candidate for candidate in api_key_candidates if has_usable_secret(candidate)), "")
|
||||
|
||||
return {
|
||||
result = {
|
||||
"provider": "custom",
|
||||
"api_mode": custom_provider.get("api_mode")
|
||||
or _detect_api_mode_for_url(base_url)
|
||||
@@ -348,6 +356,11 @@ def _resolve_named_custom_runtime(
|
||||
"api_key": api_key or "no-key-required",
|
||||
"source": f"custom_provider:{custom_provider.get('name', requested_provider)}",
|
||||
}
|
||||
# Propagate the model name so callers can override self.model when the
|
||||
# provider name differs from the actual model string the API expects.
|
||||
if custom_provider.get("model"):
|
||||
result["model"] = custom_provider["model"]
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_openrouter_runtime(
|
||||
|
||||
@@ -1214,3 +1214,115 @@ def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch):
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="openrouter")
|
||||
assert resolved["provider"] == "openrouter"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# fix #7828 — custom_providers model field must propagate to runtime
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_named_custom_provider_includes_model(monkeypatch):
|
||||
"""_get_named_custom_provider should include the model field from config."""
|
||||
monkeypatch.setattr(rp, "load_config", lambda: {
|
||||
"custom_providers": [{
|
||||
"name": "my-dashscope",
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "test-key",
|
||||
"api_mode": "chat_completions",
|
||||
"model": "qwen3.6-plus",
|
||||
}],
|
||||
})
|
||||
|
||||
result = rp._get_named_custom_provider("my-dashscope")
|
||||
assert result is not None
|
||||
assert result["model"] == "qwen3.6-plus"
|
||||
|
||||
|
||||
def test_get_named_custom_provider_excludes_empty_model(monkeypatch):
|
||||
"""Empty or whitespace-only model field should not appear in result."""
|
||||
for model_val in ["", " ", None]:
|
||||
entry = {
|
||||
"name": "test-ep",
|
||||
"base_url": "https://example.com/v1",
|
||||
"api_key": "key",
|
||||
}
|
||||
if model_val is not None:
|
||||
entry["model"] = model_val
|
||||
|
||||
monkeypatch.setattr(rp, "load_config", lambda e=entry: {
|
||||
"custom_providers": [e],
|
||||
})
|
||||
|
||||
result = rp._get_named_custom_provider("test-ep")
|
||||
assert result is not None
|
||||
assert "model" not in result, (
|
||||
f"model field {model_val!r} should not be included in result"
|
||||
)
|
||||
|
||||
|
||||
def test_named_custom_runtime_propagates_model_direct_path(monkeypatch):
|
||||
"""Model should propagate through the direct (non-pool) resolution path."""
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server")
|
||||
monkeypatch.setattr(
|
||||
rp, "_get_named_custom_provider",
|
||||
lambda p: {
|
||||
"name": "my-server",
|
||||
"base_url": "http://localhost:8000/v1",
|
||||
"api_key": "test-key",
|
||||
"model": "qwen3.6-plus",
|
||||
},
|
||||
)
|
||||
# Ensure pool doesn't intercept
|
||||
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="my-server")
|
||||
assert resolved["model"] == "qwen3.6-plus"
|
||||
assert resolved["provider"] == "custom"
|
||||
|
||||
|
||||
def test_named_custom_runtime_propagates_model_pool_path(monkeypatch):
|
||||
"""Model should propagate even when credential pool handles credentials."""
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server")
|
||||
monkeypatch.setattr(
|
||||
rp, "_get_named_custom_provider",
|
||||
lambda p: {
|
||||
"name": "my-server",
|
||||
"base_url": "http://localhost:8000/v1",
|
||||
"api_key": "test-key",
|
||||
"model": "qwen3.6-plus",
|
||||
},
|
||||
)
|
||||
# Pool returns a result (intercepting the normal path)
|
||||
monkeypatch.setattr(
|
||||
rp, "_try_resolve_from_custom_pool",
|
||||
lambda *a, **k: {
|
||||
"provider": "custom",
|
||||
"api_mode": "chat_completions",
|
||||
"base_url": "http://localhost:8000/v1",
|
||||
"api_key": "pool-key",
|
||||
"source": "pool:custom:my-server",
|
||||
},
|
||||
)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="my-server")
|
||||
assert resolved["model"] == "qwen3.6-plus", (
|
||||
"model must be injected into pool result"
|
||||
)
|
||||
assert resolved["api_key"] == "pool-key", "pool credentials should be used"
|
||||
|
||||
|
||||
def test_named_custom_runtime_no_model_when_absent(monkeypatch):
|
||||
"""When custom_providers entry has no model field, runtime should not either."""
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server")
|
||||
monkeypatch.setattr(
|
||||
rp, "_get_named_custom_provider",
|
||||
lambda p: {
|
||||
"name": "my-server",
|
||||
"base_url": "http://localhost:8000/v1",
|
||||
"api_key": "test-key",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="my-server")
|
||||
assert "model" not in resolved
|
||||
|
||||
Reference in New Issue
Block a user