Fix ChatGPT OAuth setup wizard flow (#289)

This commit is contained in:
Plateau Nguyen authored and GitHub committed 2026-03-20 06:54:11 +07:00
1 parent c1893c73d1
commit 43990c7e56
4 files changed
+69 -17

No files matched your search

+2
View File
@@ -18,6 +18,7 @@
"provider": {
"title": "Configure LLM Provider",
"descriptionCli": "Connect using your local Claude CLI installation. No API key needed.",
"descriptionOauth": "Sign in with your ChatGPT account to use your subscription through OAuth. No API key needed.",
"description": "Connect to an AI provider to power your agents. You'll need an API key.",
"providerType": "Provider Type",
"providerTypeHint": "The LLM service you want to connect. OpenRouter is recommended for access to multiple models.",
@@ -31,6 +32,7 @@
"create": "Create Provider",
"errors": {
"apiKeyRequired": "API key is required",
"oauthProviderNotFound": "OAuth authentication succeeded, but the provider could not be found. Please try again.",
"failedCreate": "Failed to create provider"
}
},
+2
View File
@@ -18,6 +18,7 @@
"provider": {
"title": "Cấu hình provider LLM",
"descriptionCli": "Kết nối bằng Claude CLI cục bộ. Không cần API key.",
"descriptionOauth": "Đăng nhập bằng tài khoản ChatGPT để dùng gói thuê bao qua OAuth. Không cần API key.",
"description": "Kết nối với provider AI để cấp nguồn cho agent. Bạn cần có API key.",
"providerType": "Loại provider",
"providerTypeHint": "Dịch vụ LLM bạn muốn kết nối. Khuyến nghị dùng OpenRouter để truy cập nhiều model.",
@@ -31,6 +32,7 @@
"create": "Tạo provider",
"errors": {
"apiKeyRequired": "API key là bắt buộc",
"oauthProviderNotFound": "Xác thực OAuth thành công nhưng không tìm thấy provider. Vui lòng thử lại.",
"failedCreate": "Không thể tạo provider"
}
},
@@ -18,7 +18,12 @@ interface StartResponse {
status?: string;
}
export function OAuthSection({ onSuccess }: { onSuccess: () => void }) {
interface OAuthSectionProps {
onSuccess: () => void;
authenticatedActionLabel?: string;
}
export function OAuthSection({ onSuccess, authenticatedActionLabel }: OAuthSectionProps) {
const { t } = useTranslation("providers");
const http = useHttp();
const [status, setStatus] = useState<OAuthStatus | null>(null);
@@ -176,9 +181,16 @@ export function OAuthSection({ onSuccess }: { onSuccess: () => void }) {
{t("oauth.modelPrefixHint")} <code className="rounded bg-muted px-1 font-mono">openai-codex/</code>{" "}
{t("oauth.modelPrefixExample")}
</p>
<Button variant="outline" size="sm" onClick={handleLogout} className="gap-1.5">
{t("oauth.removeToken")}
</Button>
<div className="flex flex-wrap gap-2">
{authenticatedActionLabel && (
<Button size="sm" onClick={onSuccess}>
{authenticatedActionLabel}
</Button>
)}
<Button variant="outline" size="sm" onClick={handleLogout} className="gap-1.5">
{t("oauth.removeToken")}
</Button>
</div>
</div>
);
}
+49 -13
View File
@@ -1,4 +1,4 @@
import { useState, useMemo } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -6,6 +6,7 @@ import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip";
import { InfoTip } from "@/pages/setup/info-tip";
import { useHttp } from "@/hooks/use-ws";
import {
Select,
SelectContent,
@@ -16,6 +17,7 @@ import {
import { PROVIDER_TYPES } from "@/constants/providers";
import { useProviders } from "@/pages/providers/hooks/use-providers";
import { CLISection } from "@/pages/providers/provider-cli-section";
import { OAuthSection } from "@/pages/providers/provider-oauth-section";
import { slugify } from "@/lib/slug";
import type { ProviderData, ProviderInput } from "@/types/provider";
@@ -26,6 +28,7 @@ interface StepProviderProps {
export function StepProvider({ onComplete, existingProvider }: StepProviderProps) {
const { t } = useTranslation("setup");
const http = useHttp();
const { createProvider, updateProvider } = useProviders();
const isEditing = !!existingProvider;
@@ -39,6 +42,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const isOAuth = providerType === "chatgpt_oauth";
const isCLI = providerType === "claude_cli";
// Local Ollama uses no API key — the server accepts any non-empty Bearer value internally
const isOllama = providerType === "ollama";
@@ -46,7 +50,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
const handleTypeChange = (value: string) => {
setProviderType(value);
const preset = PROVIDER_TYPES.find((t) => t.value === value);
setName(slugify(value));
setName(value === "chatgpt_oauth" ? "openai-codex" : slugify(value));
setApiBase(preset?.apiBase || "");
setApiKey("");
setError("");
@@ -59,7 +63,26 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
[providerType],
);
const handleOAuthSuccess = async () => {
setLoading(true);
setError("");
try {
const res = await http.get<{ providers: ProviderData[] }>("/v1/providers");
const provider = res.providers?.find((p) => p.provider_type === "chatgpt_oauth" && p.name === "openai-codex");
if (!provider) {
setError(t("provider.errors.oauthProviderNotFound"));
return;
}
onComplete(provider);
} catch (err) {
setError(err instanceof Error ? err.message : t("provider.errors.oauthProviderNotFound"));
} finally {
setLoading(false);
}
};
const handleSubmit = async () => {
if (isOAuth) return;
if (!isEditing && !isCLI && !isOllama && !apiKey.trim()) { setError(t("provider.errors.apiKeyRequired")); return; }
setLoading(true);
setError("");
@@ -79,7 +102,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
name: name.trim(),
provider_type: providerType,
api_base: apiBase.trim() || undefined,
api_key: isCLI || isOllama ? undefined : apiKey.trim(),
api_key: isCLI || isOllama || isOAuth ? undefined : apiKey.trim(),
enabled: true,
}) as ProviderData;
onComplete(provider);
@@ -98,7 +121,9 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
<div className="space-y-1">
<h2 className="text-lg font-semibold">{t("provider.title")}</h2>
<p className="text-sm text-muted-foreground">
{isCLI
{isOAuth
? t("provider.descriptionOauth")
: isCLI
? t("provider.descriptionCli")
: t("provider.description")}
</p>
@@ -124,11 +149,20 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
{t("provider.name")}
<InfoTip text={t("provider.nameHint")} />
</Label>
<Input value={name} onChange={(e) => setName(slugify(e.target.value))} />
<Input
value={name}
onChange={(e) => setName(slugify(e.target.value))}
disabled={isOAuth}
/>
</div>
</div>
{isCLI ? (
{isOAuth ? (
<OAuthSection
onSuccess={handleOAuthSuccess}
authenticatedActionLabel={t("model.continue")}
/>
) : isCLI ? (
<CLISection open={true} />
) : (
<>
@@ -161,13 +195,15 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-end">
<Button onClick={handleSubmit} disabled={loading || (!isEditing && !isCLI && !isOllama && !apiKey.trim())}>
{loading
? isEditing ? t("provider.updating", "Updating...") : t("provider.creating")
: isEditing ? t("provider.update", "Update") : t("provider.create")}
</Button>
</div>
{!isOAuth && (
<div className="flex justify-end">
<Button onClick={handleSubmit} disabled={loading || (!isEditing && !isCLI && !isOllama && !apiKey.trim())}>
{loading
? isEditing ? t("provider.updating", "Updating...") : t("provider.creating")
: isEditing ? t("provider.update", "Update") : t("provider.create")}
</Button>
</div>
)}
</TooltipProvider>
</CardContent>
</Card>