diff --git a/ui/web/src/i18n/locales/en/setup.json b/ui/web/src/i18n/locales/en/setup.json index 593f3fa9..2f69b663 100644 --- a/ui/web/src/i18n/locales/en/setup.json +++ b/ui/web/src/i18n/locales/en/setup.json @@ -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" } }, diff --git a/ui/web/src/i18n/locales/vi/setup.json b/ui/web/src/i18n/locales/vi/setup.json index a2063cdd..dd1fd101 100644 --- a/ui/web/src/i18n/locales/vi/setup.json +++ b/ui/web/src/i18n/locales/vi/setup.json @@ -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" } }, diff --git a/ui/web/src/pages/providers/provider-oauth-section.tsx b/ui/web/src/pages/providers/provider-oauth-section.tsx index f42d50fc..eab82048 100644 --- a/ui/web/src/pages/providers/provider-oauth-section.tsx +++ b/ui/web/src/pages/providers/provider-oauth-section.tsx @@ -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(null); @@ -176,9 +181,16 @@ export function OAuthSection({ onSuccess }: { onSuccess: () => void }) { {t("oauth.modelPrefixHint")} openai-codex/{" "} {t("oauth.modelPrefixExample")}

- +
+ {authenticatedActionLabel && ( + + )} + +
); } diff --git a/ui/web/src/pages/setup/step-provider.tsx b/ui/web/src/pages/setup/step-provider.tsx index 707c5f5b..1d8696f9 100644 --- a/ui/web/src/pages/setup/step-provider.tsx +++ b/ui/web/src/pages/setup/step-provider.tsx @@ -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

{t("provider.title")}

- {isCLI + {isOAuth + ? t("provider.descriptionOauth") + : isCLI ? t("provider.descriptionCli") : t("provider.description")}

@@ -124,11 +149,20 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps {t("provider.name")} - setName(slugify(e.target.value))} /> + setName(slugify(e.target.value))} + disabled={isOAuth} + />
- {isCLI ? ( + {isOAuth ? ( + + ) : isCLI ? ( ) : ( <> @@ -161,13 +195,15 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps {error &&

{error}

} -
- -
+ {!isOAuth && ( +
+ +
+ )}