mirror of
https://github.com/usestrix/strix.git
synced 2026-08-21 18:52:47 +02:00
feat(llm): opt-in Grok/SuperGrok subscription OAuth
Add Sign in with Grok mirroring the merged ChatGPT/Codex integration: a strix/config/grok.py OAuth module (PKCE loopback flow against auth.x.ai, refresh with cross-process locking, secure ~/.strix/subscription-auth.json store) plus grok/<model> routing through the OpenAI-compatible api.x.ai/v1 endpoint via a bearer-stamping chat-completions client. strix auth login grok / status / logout become provider-aware; subscription.py shares provider-agnostic auth-mode detection; Grok runs are marked zero-cost like other subscriptions. Uses a consumer subscription outside xAI own products, which xAI does not officially support; opt-in and experimental.
This commit is contained in:
+27
-16
@@ -6,23 +6,34 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.config import codex
|
||||
from strix.config import codex, grok
|
||||
from strix.interface import auth_cli
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
_CHATGPT = auth_cli._PROVIDERS["chatgpt"]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _tmp_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(codex, "AUTH_PATH", tmp_path / "home" / ".strix" / "subscription-auth.json")
|
||||
store = tmp_path / "home" / ".strix" / "subscription-auth.json"
|
||||
monkeypatch.setattr(codex, "AUTH_PATH", store)
|
||||
monkeypatch.setattr(grok, "AUTH_PATH", store)
|
||||
|
||||
|
||||
def test_login_provider_is_chatgpt() -> None:
|
||||
assert auth_cli.LOGIN_PROVIDER == "chatgpt"
|
||||
assert codex.PROVIDER in auth_cli._ACCEPTED_PROVIDERS
|
||||
assert "chatgpt" in auth_cli._ACCEPTED_PROVIDERS
|
||||
def test_default_provider_is_chatgpt() -> None:
|
||||
assert auth_cli._DEFAULT_PROVIDER == "chatgpt"
|
||||
assert set(auth_cli._PROVIDERS) == {"chatgpt", "grok"}
|
||||
|
||||
|
||||
def test_provider_aliases_resolve() -> None:
|
||||
assert auth_cli._resolve_provider(codex.PROVIDER) is _CHATGPT
|
||||
assert auth_cli._resolve_provider("ChatGPT") is _CHATGPT
|
||||
assert auth_cli._resolve_provider("grok") is auth_cli._PROVIDERS["grok"]
|
||||
assert auth_cli._resolve_provider("xai") is auth_cli._PROVIDERS["grok"]
|
||||
assert auth_cli._resolve_provider("gemini") is None
|
||||
|
||||
|
||||
def test_unknown_subcommand_returns_usage_error() -> None:
|
||||
@@ -51,32 +62,32 @@ def test_finish_requires_state_on_loopback(monkeypatch: pytest.MonkeyPatch) -> N
|
||||
|
||||
# Loopback (require_state=True): missing or mismatched state is rejected.
|
||||
with pytest.raises(codex.CodexAuthError) as missing:
|
||||
auth_cli._finish("code", None, "verifier", "expected", require_state=True)
|
||||
auth_cli._finish(_CHATGPT, "code", None, "verifier", "expected", require_state=True)
|
||||
assert missing.value.code == "state_mismatch"
|
||||
with pytest.raises(codex.CodexAuthError) as mismatch:
|
||||
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=True)
|
||||
auth_cli._finish(_CHATGPT, "code", "wrong", "verifier", "expected", require_state=True)
|
||||
assert mismatch.value.code == "state_mismatch"
|
||||
|
||||
# Matching state proceeds to the exchange.
|
||||
assert auth_cli._finish("code", "expected", "verifier", "expected", require_state=True) == {
|
||||
"ok": True
|
||||
}
|
||||
assert auth_cli._finish(
|
||||
_CHATGPT, "code", "expected", "verifier", "expected", require_state=True
|
||||
) == {"ok": True}
|
||||
|
||||
|
||||
def test_finish_manual_paste_allows_absent_state(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
|
||||
# Manual paste (require_state=False): a bare code with no state is accepted,
|
||||
# but a present-and-wrong state is still rejected.
|
||||
assert auth_cli._finish("code", None, "verifier", "expected", require_state=False) == {
|
||||
"ok": True
|
||||
}
|
||||
assert auth_cli._finish(
|
||||
_CHATGPT, "code", None, "verifier", "expected", require_state=False
|
||||
) == {"ok": True}
|
||||
with pytest.raises(codex.CodexAuthError):
|
||||
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=False)
|
||||
auth_cli._finish(_CHATGPT, "code", "wrong", "verifier", "expected", require_state=False)
|
||||
|
||||
|
||||
def test_finish_rejects_missing_code() -> None:
|
||||
with pytest.raises(codex.CodexAuthError) as exc:
|
||||
auth_cli._finish(None, "expected", "verifier", "expected", require_state=True)
|
||||
auth_cli._finish(_CHATGPT, None, "expected", "verifier", "expected", require_state=True)
|
||||
assert exc.value.code == "no_code"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user