mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
1e0d9b92af
* feat: Add ChatGPT Subscription support and related features - Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI. - Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status. - Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance. - Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints. - Added new slash commands for managing skills, including listing, searching, and invoking skills. - Implemented caching for skill catalog to optimize performance. - Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing. - Refactored existing code to accommodate new features and improve maintainability. * refactor: share provider device-flow setup - reuse one device-flow backend for Copilot and ChatGPT Subscription - add one frontend device-flow helper for Settings and /setup - put GitHub Copilot back into Add Models, now as a dropdown option - make provider selection just select; clicking Add starts sign-in - stop ChatGPT Subscription setup from opening auth tabs automatically - make /setup copilot and /setup chatgpt-subscription work from chat - show ChatGPT Subscription in the /setup suggestions - show the real error message when setup fails - add focused tests for the shared flow and setup UI * feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX Backend: - Resolve runtime bearer for provider-auth endpoints at probe time via a shared _resolve_probe_key() that delegates to resolve_endpoint_runtime, applied across all probe/refresh call sites. - Skip live completion probes and health pings for discovery-only providers (centralized behind _is_discovery_only_provider) — the Codex/Responses API has no such endpoints, so status is derived from cached models. - Never persist the short lived ChatGPT bearer to the plaintext sessions table; proactively clear any stale bearer left by an earlier code path. - Revoke orphaned ProviderAuthSession credentials when the last endpoint backing them is deleted (_delete_orphaned_provider_auth), surfaced via cleared_provider_auth in the delete response. Frontend (admin.js): - Auto-start the device-auth flow on provider selection so the authorization panel (code + Authorize) shows immediately instead of behind a "Sign in" click. - Remove the redundant top button for device auth providers, move retry into the panel via an inline "Try again". - Drop the self-evident hint text and add an execCommand clipboard fallback so Copy works in non-secure (HTTP/LAN) contexts. * fix: harden chatgpt subscription provider * chore: remove PR media from branch * Fix chatgpt subscription recovery and token handling --------- Co-authored-by: 5p00kyy <admin@5p00ky.dev>
158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""Node-driven tests for the shared provider device-flow runner."""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_REPO = Path(__file__).resolve().parent.parent
|
|
_HELPER = _REPO / "static" / "js" / "providerDeviceFlow.js"
|
|
pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not on PATH")
|
|
|
|
|
|
def _run_node(script: str):
|
|
proc = subprocess.run(
|
|
["node", "--input-type=module"],
|
|
input=script,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(_REPO),
|
|
timeout=30,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout.strip())
|
|
|
|
|
|
def test_copilot_success_uses_complete_verification_uri():
|
|
js = f"""
|
|
import {{ runProviderDeviceFlow }} from '{_HELPER.as_posix()}';
|
|
const calls = [];
|
|
const opened = [];
|
|
let polls = 0;
|
|
const response = (ok, status, payload) => ({{ ok, status, async json() {{ return payload; }} }});
|
|
const fetchImpl = async (url) => {{
|
|
calls.push(url);
|
|
if (url.endsWith('/device/start')) {{
|
|
return response(true, 200, {{
|
|
poll_id: 'poll-1',
|
|
user_code: 'GH-CODE',
|
|
verification_uri: 'https://github.com/login/device',
|
|
verification_uri_complete: 'https://github.com/login/device?user_code=GH-CODE',
|
|
interval: 2,
|
|
expires_in: 30,
|
|
}});
|
|
}}
|
|
polls += 1;
|
|
return response(true, 200, polls === 1
|
|
? {{ status: 'pending' }}
|
|
: {{ status: 'authorized', endpoint: {{ id: 'ep1', models: ['gpt-4o'] }} }}
|
|
);
|
|
}};
|
|
const result = await runProviderDeviceFlow('copilot', {{
|
|
fetchImpl,
|
|
openWindow: (url) => opened.push(url),
|
|
sleep: async () => {{}},
|
|
now: () => 0,
|
|
}});
|
|
console.log(JSON.stringify({{ result, calls, opened }}));
|
|
"""
|
|
out = _run_node(js)
|
|
assert out["result"]["status"] == "authorized"
|
|
assert out["result"]["endpoint"]["id"] == "ep1"
|
|
assert out["opened"] == ["https://github.com/login/device?user_code=GH-CODE"]
|
|
assert out["calls"] == ["/api/copilot/device/start", "/api/copilot/device/poll", "/api/copilot/device/poll"]
|
|
|
|
|
|
def test_chatgpt_success_uses_plain_verification_uri():
|
|
js = f"""
|
|
import {{ runProviderDeviceFlow }} from '{_HELPER.as_posix()}';
|
|
const opened = [];
|
|
const response = (ok, status, payload) => ({{ ok, status, async json() {{ return payload; }} }});
|
|
const fetchImpl = async (url) => {{
|
|
if (url.endsWith('/device/start')) {{
|
|
return response(true, 200, {{
|
|
poll_id: 'poll-1',
|
|
user_code: 'OA-CODE',
|
|
verification_uri: 'https://auth.openai.com/codex/device',
|
|
interval: 2,
|
|
expires_in: 30,
|
|
}});
|
|
}}
|
|
return response(true, 200, {{ status: 'authorized', endpoint: {{ id: 'chatgpt', models: ['gpt-5.5'] }} }});
|
|
}};
|
|
const result = await runProviderDeviceFlow('chatgpt-subscription', {{
|
|
fetchImpl,
|
|
openWindow: (url) => opened.push(url),
|
|
sleep: async () => {{}},
|
|
now: () => 0,
|
|
}});
|
|
console.log(JSON.stringify({{ result, opened }}));
|
|
"""
|
|
out = _run_node(js)
|
|
assert out["result"]["status"] == "authorized"
|
|
assert out["opened"] == ["https://auth.openai.com/codex/device"]
|
|
|
|
|
|
def test_start_errors_surface_backend_detail():
|
|
js = f"""
|
|
import {{ runProviderDeviceFlow }} from '{_HELPER.as_posix()}';
|
|
const response = (ok, status, payload) => ({{ ok, status, async json() {{ return payload; }} }});
|
|
try {{
|
|
await runProviderDeviceFlow('copilot', {{
|
|
fetchImpl: async () => response(false, 502, {{ detail: 'GitHub device-code request failed: upstream down' }}),
|
|
openWindow: () => {{}},
|
|
sleep: async () => {{}},
|
|
now: () => 0,
|
|
}});
|
|
}} catch (err) {{
|
|
console.log(JSON.stringify({{ message: err.message }}));
|
|
}}
|
|
"""
|
|
out = _run_node(js)
|
|
assert out["message"] == "GitHub device-code request failed: upstream down"
|
|
|
|
|
|
def test_thrown_fetch_errors_are_preserved():
|
|
js = f"""
|
|
import {{ runProviderDeviceFlow }} from '{_HELPER.as_posix()}';
|
|
try {{
|
|
await runProviderDeviceFlow('chatgpt-subscription', {{
|
|
fetchImpl: async () => {{ throw new Error('network offline'); }},
|
|
openWindow: () => {{}},
|
|
sleep: async () => {{}},
|
|
now: () => 0,
|
|
}});
|
|
}} catch (err) {{
|
|
console.log(JSON.stringify({{ message: err.message }}));
|
|
}}
|
|
"""
|
|
out = _run_node(js)
|
|
assert out["message"] == "network offline"
|
|
|
|
|
|
def test_expired_flow_returns_expired_status():
|
|
js = f"""
|
|
import {{ runProviderDeviceFlow }} from '{_HELPER.as_posix()}';
|
|
let currentTime = 0;
|
|
const response = (ok, status, payload) => ({{ ok, status, async json() {{ return payload; }} }});
|
|
const result = await runProviderDeviceFlow('copilot', {{
|
|
fetchImpl: async (url) => url.endsWith('/device/start')
|
|
? response(true, 200, {{
|
|
poll_id: 'poll-1',
|
|
user_code: 'GH-CODE',
|
|
verification_uri: 'https://github.com/login/device',
|
|
interval: 2,
|
|
expires_in: 1,
|
|
}})
|
|
: response(true, 200, {{ status: 'pending' }}),
|
|
openWindow: () => {{}},
|
|
sleep: async () => {{ currentTime += 2000; }},
|
|
now: () => currentTime,
|
|
}});
|
|
console.log(JSON.stringify(result));
|
|
"""
|
|
out = _run_node(js)
|
|
assert out == {"status": "expired"}
|