mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 01:21:43 +00:00
feat(qwen): add Qwen OAuth provider with portal request support
Based on #6079 by @tunamitom with critical fixes and comprehensive tests. Changes from #6079: - Fix: sanitization overwrite bug — Qwen message prep now runs AFTER codex field sanitization, not before (was silently discarding Qwen transforms) - Fix: missing try/except AuthError in runtime_provider.py — stale Qwen credentials now fall through to next provider on auto-detect - Fix: 'qwen' alias conflict — bare 'qwen' stays mapped to 'alibaba' (DashScope); use 'qwen-portal' or 'qwen-cli' for the OAuth provider - Fix: hardcoded ['coder-model'] replaced with live API fetch + curated fallback list (qwen3-coder-plus, qwen3-coder) - Fix: extract _is_qwen_portal() helper + _qwen_portal_headers() to replace 5 inline 'portal.qwen.ai' string checks and share headers between init and credential swap - Fix: add Qwen branch to _apply_client_headers_for_base_url for mid-session credential swaps - Fix: remove suspicious TypeError catch blocks around _prompt_provider_choice - Fix: handle bare string items in content lists (were silently dropped) - Fix: remove redundant dict() copies after deepcopy in message prep - Revert: unrelated ai-gateway test mock removal and model_switch.py comment deletion New tests (30 test functions): - _qwen_cli_auth_path, _read_qwen_cli_tokens (success + 3 error paths) - _save_qwen_cli_tokens (roundtrip, parent creation, permissions) - _qwen_access_token_is_expiring (5 edge cases: fresh, expired, within skew, None, non-numeric) - _refresh_qwen_cli_tokens (success, preserve old refresh, 4 error paths, default expires_in, disk persistence) - resolve_qwen_runtime_credentials (fresh, auto-refresh, force-refresh, missing token, env override) - get_qwen_auth_status (logged in, not logged in) - Runtime provider resolution (direct, pool entry, alias) - _build_api_kwargs (metadata, vl_high_resolution_images, message formatting, max_tokens suppression)
This commit is contained in:
parent
a1213d06bd
commit
3377017eb4
16 changed files with 955 additions and 4 deletions
|
|
@ -143,6 +143,82 @@ def test_resolve_runtime_provider_codex(monkeypatch):
|
|||
assert resolved["requested_provider"] == "openai-codex"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_qwen_oauth(monkeypatch):
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "qwen-oauth")
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"resolve_qwen_runtime_credentials",
|
||||
lambda: {
|
||||
"provider": "qwen-oauth",
|
||||
"base_url": "https://portal.qwen.ai/v1",
|
||||
"api_key": "qwen-token",
|
||||
"source": "qwen-cli",
|
||||
"expires_at_ms": 1775640710946,
|
||||
},
|
||||
)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="qwen-oauth")
|
||||
|
||||
assert resolved["provider"] == "qwen-oauth"
|
||||
assert resolved["api_mode"] == "chat_completions"
|
||||
assert resolved["base_url"] == "https://portal.qwen.ai/v1"
|
||||
assert resolved["api_key"] == "qwen-token"
|
||||
assert resolved["requested_provider"] == "qwen-oauth"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_uses_qwen_pool_entry(monkeypatch):
|
||||
class _Entry:
|
||||
access_token = "pool-qwen-token"
|
||||
source = "manual:qwen_cli"
|
||||
base_url = "https://portal.qwen.ai/v1"
|
||||
|
||||
class _Pool:
|
||||
def has_credentials(self):
|
||||
return True
|
||||
|
||||
def select(self):
|
||||
return _Entry()
|
||||
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "qwen-oauth")
|
||||
monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": "qwen-oauth", "default": "coder-model"})
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="qwen-oauth")
|
||||
|
||||
assert resolved["provider"] == "qwen-oauth"
|
||||
assert resolved["api_mode"] == "chat_completions"
|
||||
assert resolved["base_url"] == "https://portal.qwen.ai/v1"
|
||||
assert resolved["api_key"] == "pool-qwen-token"
|
||||
assert resolved["source"] == "manual:qwen_cli"
|
||||
|
||||
|
||||
def test_resolve_provider_alias_qwen(monkeypatch):
|
||||
monkeypatch.setattr(rp.auth_mod, "_load_auth_store", lambda: {})
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
assert rp.resolve_provider("qwen-portal") == "qwen-oauth"
|
||||
assert rp.resolve_provider("qwen-cli") == "qwen-oauth"
|
||||
|
||||
|
||||
def test_qwen_oauth_auto_fallthrough_on_auth_failure(monkeypatch):
|
||||
"""When requested_provider is 'auto' and Qwen creds fail, fall through."""
|
||||
from hermes_cli.auth import AuthError
|
||||
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "qwen-oauth")
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"resolve_qwen_runtime_credentials",
|
||||
lambda **kw: (_ for _ in ()).throw(AuthError("stale", provider="qwen-oauth", code="qwen_auth_missing")),
|
||||
)
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-or-key")
|
||||
|
||||
# Should NOT raise — falls through to OpenRouter
|
||||
resolved = rp.resolve_runtime_provider(requested="auto")
|
||||
# The fallthrough means it won't be qwen-oauth
|
||||
assert resolved["provider"] != "qwen-oauth"
|
||||
|
||||
|
||||
def test_resolve_runtime_provider_ai_gateway(monkeypatch):
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "ai-gateway")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue