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:
stocky789
2026-06-08 18:19:18 +10:00
committed by GitHub
parent ac94885c84
commit 1e0d9b92af
37 changed files with 3425 additions and 485 deletions
+39
View File
@@ -361,6 +361,24 @@ class ModelEndpoint(TimestampMixin, Base):
# is the historical default. When non-null, the model picker only shows
# the endpoint to that user (admins always see everything).
owner = Column(String, nullable=True, index=True)
# Optional OAuth/session-backed credential row. Used by subscription-backed
# providers that need refresh tokens instead of a static API key.
provider_auth_id = Column(String, nullable=True, index=True)
class ProviderAuthSession(TimestampMixin, Base):
"""Encrypted OAuth/session credentials for refresh-aware model providers."""
__tablename__ = "provider_auth_sessions"
id = Column(String, primary_key=True, index=True)
provider = Column(String, nullable=False, index=True)
owner = Column(String, nullable=True, index=True)
label = Column(String, nullable=True)
base_url = Column(String, nullable=False)
access_token = Column(EncryptedText, nullable=True)
refresh_token = Column(EncryptedText, nullable=True)
last_refresh = Column(DateTime, nullable=True)
auth_mode = Column(String, nullable=True)
class McpServer(TimestampMixin, Base):
"""Admin-configured MCP (Model Context Protocol) tool servers."""
@@ -801,6 +819,26 @@ def _migrate_add_model_endpoint_owner_column():
logging.getLogger(__name__).warning(f"model_endpoints.owner migration failed: {e}")
def _migrate_add_provider_auth_id_column():
"""Add provider_auth_id column to model_endpoints if it doesn't exist."""
import sqlite3
db_path = DATABASE_URL.replace("sqlite:///", "")
if not os.path.exists(db_path):
return
try:
conn = sqlite3.connect(db_path)
cursor = conn.execute("PRAGMA table_info(model_endpoints)")
columns = [row[1] for row in cursor.fetchall()]
if columns and "provider_auth_id" not in columns:
conn.execute("ALTER TABLE model_endpoints ADD COLUMN provider_auth_id VARCHAR")
conn.execute("CREATE INDEX IF NOT EXISTS ix_model_endpoints_provider_auth_id ON model_endpoints(provider_auth_id)")
conn.commit()
logging.getLogger(__name__).info("Migrated: added 'provider_auth_id' column + index to model_endpoints")
conn.close()
except Exception as e:
logging.getLogger(__name__).warning(f"model_endpoints.provider_auth_id migration failed: {e}")
def _migrate_add_model_type_column():
"""Add model_type column to model_endpoints if it doesn't exist."""
import sqlite3
@@ -1599,6 +1637,7 @@ def init_db():
_migrate_add_model_type_column()
_migrate_add_model_endpoint_refresh_columns()
_migrate_add_model_endpoint_owner_column()
_migrate_add_provider_auth_id_column()
_migrate_add_supports_tools_column()
_migrate_add_task_run_model_column()
_migrate_add_owner_column()