mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -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:
+57
-12
@@ -169,13 +169,20 @@ def _recover_empty_session_model(sess, session_id: str, owner: str | None = None
|
||||
Covers the window between endpoint setup and the first chat send: the
|
||||
picker showed a model in the dropdown but the session record never got
|
||||
written (Issue #587 — UI uses the cached endpoint list, not s.model).
|
||||
Without this, we'd POST the upstream with model="" and get a generic
|
||||
401/503 instead of using the model the user already picked.
|
||||
|
||||
Returns True iff sess.model was repaired.
|
||||
For ChatGPT Subscription, also repairs stale OpenAI API model names such as
|
||||
``gpt-5`` that are not accepted by the Codex-backed ChatGPT account route.
|
||||
"""
|
||||
if getattr(sess, "model", None):
|
||||
return False
|
||||
current_model = (getattr(sess, "model", "") or "").strip()
|
||||
endpoint_url = (getattr(sess, "endpoint_url", "") or "").strip()
|
||||
is_chatgpt_subscription = False
|
||||
if current_model:
|
||||
try:
|
||||
from src.chatgpt_subscription import is_chatgpt_subscription_base
|
||||
is_chatgpt_subscription = is_chatgpt_subscription_base(endpoint_url)
|
||||
if not is_chatgpt_subscription:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Prefer the endpoint whose base URL matches the session — we know the
|
||||
@@ -194,16 +201,51 @@ def _recover_empty_session_model(sess, session_id: str, owner: str | None = None
|
||||
break
|
||||
if not ep:
|
||||
return False
|
||||
if not is_chatgpt_subscription:
|
||||
try:
|
||||
from src.chatgpt_subscription import is_chatgpt_subscription_base
|
||||
is_chatgpt_subscription = is_chatgpt_subscription_base(getattr(ep, "base_url", "") or endpoint_url)
|
||||
except Exception:
|
||||
is_chatgpt_subscription = False
|
||||
try:
|
||||
cached = json.loads(ep.cached_models) if isinstance(ep.cached_models, str) else (ep.cached_models or [])
|
||||
except Exception:
|
||||
cached = []
|
||||
if not cached:
|
||||
visible = []
|
||||
else:
|
||||
try:
|
||||
visible = _visible_models(cached, getattr(ep, "hidden_models", None))
|
||||
except Exception:
|
||||
visible = cached
|
||||
if current_model and current_model in {str(item).strip() for item in visible}:
|
||||
return False
|
||||
try:
|
||||
visible = _visible_models(cached, getattr(ep, "hidden_models", None))
|
||||
except Exception:
|
||||
visible = cached
|
||||
if is_chatgpt_subscription:
|
||||
live_models = []
|
||||
if getattr(ep, "provider_auth_id", None):
|
||||
try:
|
||||
from src.chatgpt_subscription import fetch_available_models
|
||||
from src.endpoint_resolver import resolve_endpoint_runtime
|
||||
_base, api_key = resolve_endpoint_runtime(ep, owner=owner)
|
||||
if api_key:
|
||||
live_models = fetch_available_models(api_key)
|
||||
if live_models:
|
||||
ep.cached_models = json.dumps(live_models)
|
||||
db.commit()
|
||||
except Exception:
|
||||
live_models = []
|
||||
# ChatGPT Subscription recovery must use the live Codex catalog.
|
||||
# Cached rows are only trusted above to avoid revalidating a model
|
||||
# that is already present in the visible picker list.
|
||||
cached = live_models
|
||||
if not cached:
|
||||
return False
|
||||
try:
|
||||
visible = _visible_models(cached, getattr(ep, "hidden_models", None))
|
||||
except Exception:
|
||||
visible = cached
|
||||
if current_model and current_model in {str(item).strip() for item in visible}:
|
||||
return False
|
||||
if not visible:
|
||||
return False
|
||||
model = visible[0]
|
||||
@@ -213,14 +255,17 @@ def _recover_empty_session_model(sess, session_id: str, owner: str | None = None
|
||||
# Persist so the next request, websocket reconnect, or page reload
|
||||
# picks up the same model (we'd otherwise re-pick on every send
|
||||
# and silently switch on the user if the cached order shifts).
|
||||
db_session = db.query(DBSession).filter(DBSession.id == session_id).first()
|
||||
db_session_q = db.query(DBSession).filter(DBSession.id == session_id)
|
||||
if owner:
|
||||
db_session_q = db_session_q.filter(DBSession.owner == owner)
|
||||
db_session = db_session_q.first()
|
||||
if db_session:
|
||||
db_session.model = model
|
||||
db_session.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
sess.model = model
|
||||
logger.info(
|
||||
"Recovered empty session model for %s — picked %r from endpoint %s",
|
||||
"Recovered session model for %s — picked %r from endpoint %s",
|
||||
session_id, model, ep.id,
|
||||
)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user