feat(chatgpt): add the ChatGPT subscription adapter and login

Rhema had no way to spend a ChatGPT subscription: the models were reachable only through an OpenAI API key, which bills the API account instead. The new adapter signs in through OpenAI's browser OAuth flow with PKCE and a loopback callback, stores the returned token JSON as the instance credential, and sends turns to the Codex Responses backend with the subscription's live model catalog.

The flow runs on the existing InteractiveLogin seam, so the wizard, the settings dialog, and the headless frontend all drive it. The TUI shows the link's host with an open button and a copy button: enter opens the default browser, and c copies the full URL, which keeps a long authorization query off the screen and out of a manual selection. The provider registry, config, ACP, claw, and the web settings page gain the new provider kind.
This commit is contained in:
vhaudiquet committed 2026-11-10 18:06:00 +01:00
1 parent 05045e4ee5
commit 3e2ff7599d
5 files changed
+16 -5

No files matched your search

+3
View File
@@ -320,6 +320,7 @@ fn env_vars_for(kind: &str) -> &'static [&'static str] {
"zai" => &["ZAI_API_KEY", "Z_AI_API_KEY"], "zai" => &["ZAI_API_KEY", "Z_AI_API_KEY"],
"opencode" => &["OPENCODE_API_KEY"], "opencode" => &["OPENCODE_API_KEY"],
"copilot" => &["GITHUB_COPILOT_TOKEN"], "copilot" => &["GITHUB_COPILOT_TOKEN"],
"chatgpt" => &["CHATGPT_ACCESS_TOKEN"],
_ => &[], _ => &[],
} }
} }
@@ -366,6 +367,7 @@ pub struct ProviderParams {
pub fn addable_kinds() -> &'static [&'static str] { pub fn addable_kinds() -> &'static [&'static str] {
&[ &[
"anthropic", "anthropic",
"chatgpt",
"copilot", "copilot",
"deepseek", "deepseek",
"google", "google",
@@ -542,6 +544,7 @@ pub fn discover(
"zai", "zai",
"opencode", "opencode",
"copilot", "copilot",
"chatgpt",
] { ] {
let stored_of_kind = rows.iter().any(|(_, params)| params.kind == kind); let stored_of_kind = rows.iter().any(|(_, params)| params.kind == kind);
let params = if kind == "llama-cpp" { let params = if kind == "llama-cpp" {
+1
View File
@@ -26,6 +26,7 @@ COPY crates/rhema-memory/Cargo.toml crates/rhema-memory/Cargo.toml
COPY crates/rhema-mcp/Cargo.toml crates/rhema-mcp/Cargo.toml COPY crates/rhema-mcp/Cargo.toml crates/rhema-mcp/Cargo.toml
COPY crates/rhema-provider/Cargo.toml crates/rhema-provider/Cargo.toml COPY crates/rhema-provider/Cargo.toml crates/rhema-provider/Cargo.toml
COPY crates/rhema-provider-anthropic/Cargo.toml crates/rhema-provider-anthropic/Cargo.toml COPY crates/rhema-provider-anthropic/Cargo.toml crates/rhema-provider-anthropic/Cargo.toml
COPY crates/rhema-provider-chatgpt/Cargo.toml crates/rhema-provider-chatgpt/Cargo.toml
COPY crates/rhema-provider-copilot/Cargo.toml crates/rhema-provider-copilot/Cargo.toml COPY crates/rhema-provider-copilot/Cargo.toml crates/rhema-provider-copilot/Cargo.toml
COPY crates/rhema-provider-google/Cargo.toml crates/rhema-provider-google/Cargo.toml COPY crates/rhema-provider-google/Cargo.toml crates/rhema-provider-google/Cargo.toml
COPY crates/rhema-provider-llama-cpp/Cargo.toml crates/rhema-provider-llama-cpp/Cargo.toml COPY crates/rhema-provider-llama-cpp/Cargo.toml crates/rhema-provider-llama-cpp/Cargo.toml
+1
View File
@@ -28,6 +28,7 @@ COPY crates/rhema-memory/Cargo.toml crates/rhema-memory/Cargo.toml
COPY crates/rhema-mcp/Cargo.toml crates/rhema-mcp/Cargo.toml COPY crates/rhema-mcp/Cargo.toml crates/rhema-mcp/Cargo.toml
COPY crates/rhema-provider/Cargo.toml crates/rhema-provider/Cargo.toml COPY crates/rhema-provider/Cargo.toml crates/rhema-provider/Cargo.toml
COPY crates/rhema-provider-anthropic/Cargo.toml crates/rhema-provider-anthropic/Cargo.toml COPY crates/rhema-provider-anthropic/Cargo.toml crates/rhema-provider-anthropic/Cargo.toml
COPY crates/rhema-provider-chatgpt/Cargo.toml crates/rhema-provider-chatgpt/Cargo.toml
COPY crates/rhema-provider-copilot/Cargo.toml crates/rhema-provider-copilot/Cargo.toml COPY crates/rhema-provider-copilot/Cargo.toml crates/rhema-provider-copilot/Cargo.toml
COPY crates/rhema-provider-google/Cargo.toml crates/rhema-provider-google/Cargo.toml COPY crates/rhema-provider-google/Cargo.toml crates/rhema-provider-google/Cargo.toml
COPY crates/rhema-provider-llama-cpp/Cargo.toml crates/rhema-provider-llama-cpp/Cargo.toml COPY crates/rhema-provider-llama-cpp/Cargo.toml crates/rhema-provider-llama-cpp/Cargo.toml
@@ -98,6 +98,7 @@ function save(): void {
// --- the login form --------------------------------------------------- // --- the login form ---------------------------------------------------
const KIND_LABELS: Record<string, string> = { const KIND_LABELS: Record<string, string> = {
anthropic: 'Anthropic', anthropic: 'Anthropic',
chatgpt: 'ChatGPT (subscription)',
copilot: 'GitHub Copilot', copilot: 'GitHub Copilot',
deepseek: 'DeepSeek', deepseek: 'DeepSeek',
google: 'Google Gemini', google: 'Google Gemini',
@@ -111,10 +112,14 @@ function kindLabel(kind: string): string {
return KIND_LABELS[kind] ?? kind return KIND_LABELS[kind] ?? kind
} }
/** The key field's voice: copilot logs in with a GitHub token. */ /** The key field's voice: copilot logs in with a GitHub token, and a
const keyLabel = computed(() => * ChatGPT login takes the browser flow's credential JSON (or a bare
addKind.value === 'copilot' ? 'GitHub token' : 'API key', * access token). */
) const keyLabel = computed(() => {
if (addKind.value === 'copilot') return 'GitHub token'
if (addKind.value === 'chatgpt') return 'ChatGPT credential (JSON or access token)'
return 'API key'
})
/** A field the daemon does not demand reads as optional, not hidden. */ /** A field the daemon does not demand reads as optional, not hidden. */
function label(name: string, base: string): string { function label(name: string, base: string): string {
+2 -1
View File
@@ -204,7 +204,8 @@ export type AgentImageKind = 'avatar' | 'logo'
/** The credentials a provider login carries: the instance name (the /** The credentials a provider login carries: the instance name (the
* kind when omitted), the kind (required on an id's first login), an * kind when omitted), the kind (required on an id's first login), an
* API key (a GitHub token for copilot), and a server URL for * API key (a GitHub token for copilot, the sign-in JSON for chatgpt),
* and a server URL for
* llama.cpp. Empty optionals are omitted, never sent as "" strings. */ * llama.cpp. Empty optionals are omitted, never sent as "" strings. */
export interface ProviderCredentials { export interface ProviderCredentials {
name?: string name?: string