mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-27 23:25:22 -04:00
feat: add ChatGPT Subscription provider (#2876)
* 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>
This commit is contained in:
@@ -70,6 +70,25 @@ def _endpoint_enabled_models(ep) -> list:
|
||||
return [m for m in _endpoint_cached_models(ep) if m not in hidden]
|
||||
|
||||
|
||||
def resolve_endpoint_runtime(ep, owner: Optional[str] = None) -> Tuple[str, Optional[str]]:
|
||||
"""Resolve a ModelEndpoint row to its runtime base URL and bearer/API key.
|
||||
|
||||
Static-key providers use ``ModelEndpoint.api_key``. Session-backed providers
|
||||
store refreshable credentials in ProviderAuthSession and must resolve a
|
||||
current access token at call time.
|
||||
"""
|
||||
base = normalize_base(getattr(ep, "base_url", "") or "")
|
||||
api_key = getattr(ep, "api_key", None)
|
||||
auth_id = getattr(ep, "provider_auth_id", None)
|
||||
if auth_id:
|
||||
from src.chatgpt_subscription import resolve_runtime_credentials
|
||||
|
||||
creds = resolve_runtime_credentials(auth_id, owner=owner)
|
||||
base = normalize_base(creds.get("base_url") or base)
|
||||
api_key = creds.get("api_key")
|
||||
return base, api_key
|
||||
|
||||
|
||||
# Cache for Tailscale hostname → IP resolution
|
||||
_tailscale_cache: Dict[str, Optional[str]] = {}
|
||||
|
||||
@@ -133,7 +152,7 @@ def resolve_url(url: str) -> str:
|
||||
def normalize_base(url: str) -> str:
|
||||
"""Strip known API path suffixes from a base URL."""
|
||||
url = (url or "").strip().rstrip("/")
|
||||
for suffix in ["/models", "/chat/completions", "/completions", "/v1/messages"]:
|
||||
for suffix in ["/models", "/chat/completions", "/completions", "/v1/messages", "/responses"]:
|
||||
if url.endswith(suffix):
|
||||
url = url[: -len(suffix)].rstrip("/")
|
||||
for suffix in ["/chat", "/tags", "/generate"]:
|
||||
@@ -158,10 +177,12 @@ def build_chat_url(base: str) -> str:
|
||||
return _anthropic_api_root(base) + "/v1/messages"
|
||||
if provider == "ollama":
|
||||
return _ollama_api_root(base) + "/chat"
|
||||
if provider == "chatgpt-subscription":
|
||||
return base.rstrip("/") + "/responses"
|
||||
return base + "/chat/completions"
|
||||
|
||||
|
||||
def build_models_url(base: str) -> str:
|
||||
def build_models_url(base: str) -> Optional[str]:
|
||||
"""Return the provider-specific model-list endpoint URL for a base."""
|
||||
base = resolve_url(base)
|
||||
provider = _detect_provider(base)
|
||||
@@ -169,6 +190,8 @@ def build_models_url(base: str) -> str:
|
||||
return _anthropic_api_root(base) + "/v1/models"
|
||||
if provider == "ollama":
|
||||
return _ollama_api_root(base) + "/tags"
|
||||
if provider == "chatgpt-subscription":
|
||||
return None
|
||||
return base + "/models"
|
||||
|
||||
|
||||
@@ -184,6 +207,9 @@ def build_headers(api_key: Optional[str], base: str) -> Dict[str, str]:
|
||||
if provider == "copilot":
|
||||
from src.copilot import copilot_headers
|
||||
return copilot_headers(api_key)
|
||||
if provider == "chatgpt-subscription":
|
||||
from src.chatgpt_subscription import chatgpt_headers
|
||||
return chatgpt_headers(api_key)
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
if provider == "openrouter":
|
||||
@@ -262,9 +288,13 @@ def resolve_endpoint(
|
||||
if not ep:
|
||||
return fallback_url, fallback_model, fallback_headers
|
||||
|
||||
base = normalize_base(ep.base_url)
|
||||
try:
|
||||
base, api_key = resolve_endpoint_runtime(ep, owner=owner)
|
||||
except Exception as e:
|
||||
logger.warning("Could not resolve endpoint runtime credentials: %s", e)
|
||||
return fallback_url, fallback_model, fallback_headers
|
||||
chat_url = build_chat_url(base)
|
||||
headers = build_headers(ep.api_key, base)
|
||||
headers = build_headers(api_key, base)
|
||||
|
||||
# Discard a configured model the user has since disabled on the
|
||||
# endpoint (e.g. a stale `default_model` left pointing at a now-hidden
|
||||
@@ -308,9 +338,13 @@ def resolve_endpoint_by_id(
|
||||
ep = q.first()
|
||||
if not ep:
|
||||
return None
|
||||
base = normalize_base(ep.base_url)
|
||||
try:
|
||||
base, api_key = resolve_endpoint_runtime(ep, owner=owner)
|
||||
except Exception as e:
|
||||
logger.warning("Could not resolve endpoint runtime credentials: %s", e)
|
||||
return None
|
||||
chat_url = build_chat_url(base)
|
||||
headers = build_headers(ep.api_key, base)
|
||||
headers = build_headers(api_key, base)
|
||||
m = (model or "").strip()
|
||||
# Drop a model the user disabled on the endpoint, then pick the first
|
||||
# enabled chat model rather than a hidden one.
|
||||
|
||||
Reference in New Issue
Block a user