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:
kshitijk4poor 2026-04-08 20:48:21 +05:30 committed by Teknium
parent a1213d06bd
commit 3377017eb4
16 changed files with 955 additions and 4 deletions

View file

@ -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: {})