mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
feat(agent): add lmstudio integration
This commit is contained in:
parent
7d4648461a
commit
214ca943ac
26 changed files with 1137 additions and 40 deletions
|
|
@ -240,6 +240,110 @@ def test_resolve_runtime_provider_ai_gateway(monkeypatch):
|
|||
assert resolved["requested_provider"] == "ai-gateway"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_lmstudio_uses_token_when_present(monkeypatch):
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "lmstudio")
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"_get_model_config",
|
||||
lambda: {
|
||||
"provider": "lmstudio",
|
||||
"base_url": "http://127.0.0.1:1234/v1",
|
||||
"default": "publisher/model-a",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"load_pool",
|
||||
lambda provider: type("Pool", (), {"has_credentials": lambda self: False})(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"resolve_api_key_provider_credentials",
|
||||
lambda provider: {
|
||||
"provider": "lmstudio",
|
||||
"api_key": "lm-token",
|
||||
"base_url": "http://127.0.0.1:1234/v1",
|
||||
"source": "LM_API_KEY",
|
||||
},
|
||||
)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="lmstudio")
|
||||
|
||||
assert resolved["provider"] == "lmstudio"
|
||||
assert resolved["api_key"] == "lm-token"
|
||||
assert resolved["api_mode"] == "chat_completions"
|
||||
assert resolved["base_url"] == "http://127.0.0.1:1234/v1"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_lmstudio_honors_saved_base_url(monkeypatch):
|
||||
"""Pre-existing configs with `provider: lmstudio` + custom base_url must keep working.
|
||||
|
||||
Before this PR, `lmstudio` aliased to `custom`, so a user with a remote
|
||||
LM Studio (e.g. lab box) could write `provider: "lmstudio"` plus
|
||||
`base_url: "http://192.168.1.10:1234/v1"` and the custom path honored it.
|
||||
Now that `lmstudio` is first-class with `inference_base_url=127.0.0.1`,
|
||||
the saved `base_url` from `model_cfg` must still win — otherwise this
|
||||
PR is a silent breaking change for those users.
|
||||
"""
|
||||
monkeypatch.delenv("LM_API_KEY", raising=False)
|
||||
monkeypatch.delenv("LM_BASE_URL", raising=False)
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "lmstudio")
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"_get_model_config",
|
||||
lambda: {
|
||||
"provider": "lmstudio",
|
||||
"base_url": "http://192.168.1.10:1234/v1",
|
||||
"default": "qwen/qwen3-coder-30b",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"load_pool",
|
||||
lambda provider: type("Pool", (), {"has_credentials": lambda self: False})(),
|
||||
)
|
||||
# Don't mock resolve_api_key_provider_credentials — exercise the real
|
||||
# function so we test the end-to-end precedence between model_cfg and
|
||||
# the pconfig default.
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="lmstudio")
|
||||
|
||||
assert resolved["provider"] == "lmstudio"
|
||||
assert resolved["api_mode"] == "chat_completions"
|
||||
# The saved base_url must NOT be shadowed by the 127.0.0.1 default.
|
||||
assert resolved["base_url"] == "http://192.168.1.10:1234/v1"
|
||||
# No-auth LM Studio: missing LM_API_KEY substitutes the placeholder.
|
||||
assert resolved["api_key"] == "dummy-lm-api-key"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_lmstudio_base_url_env_wins_over_saved_base_url(monkeypatch):
|
||||
"""LM_BASE_URL should override the saved lmstudio base_url for temporary redirects."""
|
||||
monkeypatch.delenv("LM_API_KEY", raising=False)
|
||||
monkeypatch.setenv("LM_BASE_URL", "http://override.local:9999/v1")
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "lmstudio")
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"_get_model_config",
|
||||
lambda: {
|
||||
"provider": "lmstudio",
|
||||
"base_url": "http://192.168.1.10:1234/v1",
|
||||
"default": "qwen/qwen3-coder-30b",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"load_pool",
|
||||
lambda provider: type("Pool", (), {"has_credentials": lambda self: False})(),
|
||||
)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="lmstudio")
|
||||
|
||||
assert resolved["provider"] == "lmstudio"
|
||||
assert resolved["api_mode"] == "chat_completions"
|
||||
assert resolved["base_url"] == "http://override.local:9999/v1"
|
||||
assert resolved["api_key"] == "dummy-lm-api-key"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_ai_gateway_explicit_override_skips_pool(monkeypatch):
|
||||
def _unexpected_pool(provider):
|
||||
raise AssertionError(f"load_pool should not be called for {provider}")
|
||||
|
|
@ -1237,6 +1341,21 @@ def test_resolve_provider_openrouter_unchanged():
|
|||
assert resolve_provider("openrouter") == "openrouter"
|
||||
|
||||
|
||||
def test_resolve_provider_lmstudio_returns_lmstudio(monkeypatch):
|
||||
"""resolve_provider('lmstudio') must return 'lmstudio', not 'custom'.
|
||||
|
||||
Regression for the alias-map bug where 'lmstudio' was rewritten to
|
||||
'custom' before the PROVIDER_REGISTRY lookup, bypassing the first-class
|
||||
LM Studio provider entirely at runtime.
|
||||
"""
|
||||
from hermes_cli.auth import resolve_provider
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
assert resolve_provider("lmstudio") == "lmstudio"
|
||||
assert resolve_provider("lm-studio") == "lmstudio"
|
||||
assert resolve_provider("lm_studio") == "lmstudio"
|
||||
|
||||
|
||||
def test_custom_provider_runtime_preserves_provider_name(monkeypatch):
|
||||
"""resolve_runtime_provider with provider='custom' must return provider='custom'."""
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue