# For AI agents (/docs/ai) These docs are built to be read by agents as much as by people. Everything on the site is available as plain markdown. ## Machine-readable endpoints [#machine-readable-endpoints] | URL | Contents | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | The full page tree with descriptions. Start here. | | [`/llms-full.txt`](/llms-full.txt) | Every page's content in one file. | | `/llms.mdx/docs//content.md` | One page as markdown, e.g. [`/llms.mdx/docs/quickstart/content.md`](/llms.mdx/docs/quickstart/content.md). | A sitemap lives at `/sitemap.xml` and `robots.txt` allows all crawlers. ## Pointing an agent at the docs [#pointing-an-agent-at-the-docs] For a one-off question, fetch the page directly: ```bash curl -s https:///llms.mdx/docs/reference/server/content.md ``` For project-wide context, add a line to your repo's `CLAUDE.md` or `AGENTS.md`: ```md title="AGENTS.md" When working with @opencoredev/loginwithchatgpt-* packages, fetch https:///llms.txt first and follow links from there. Verify option names against /docs/reference/server before writing code. ``` ## What agents get wrong [#what-agents-get-wrong] Two things trip up agents that rely on memorized patterns instead of these docs: * There is no API key. Auth comes from a user's ChatGPT session, and requests go through the app's own `/api/chatgpt/responses` proxy. * Model slugs are per-account. Generated code should call `listModels()` instead of hardcoding a slug. The [model discovery](concepts/models.mdx) page has the pattern. # Introduction (/docs) **Login with ChatGPT** lets a user authorize your app with their ChatGPT account. Your backend then proxies model requests for that session through the Vercel AI SDK. The user brings their own plan, and you never ask them for an API key. Ownership is the point: tokens stay on your server, the browser only gets an HttpOnly session cookie, and every AI request flows through a proxy route you control. ```text Browser button ──▶ POST /api/chatgpt/login ──▶ user authorizes on auth.openai.com │ Browser polls GET /status ◀── server exchanges the code and stores encrypted tokens │ streamText() ──▶ POST /api/chatgpt/responses ──▶ server injects tokens, streams back ``` A signed-in app can spend the user's own ChatGPT or Codex usage. That makes it more than an identity login. The SDK ships mandatory consent, per-session rate limits, and refresh-token redaction by default. Read the [security model](concepts/security.mdx) before shipping. ## Use this when [#use-this-when] * You want users to bring their own ChatGPT account instead of paying for their usage with your API key. * You need a login flow that works in serverless and containers. The device-code flow has no localhost redirect listener. * You want a browser-safe AI SDK provider backed by your own session proxy. ## Do not use this when [#do-not-use-this-when] * You cannot keep refresh tokens server-side. * You want your own OpenAI API key to pay for usage. Use the official `@ai-sdk/openai` provider for that. * Your use case has not been reviewed against OpenAI terms and policy. ## Packages [#packages] | Package | Job | | -------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `@opencoredev/loginwithchatgpt-server` | One Web-standard handler for login, polling, sessions, model listing, and the streaming proxy. | | `@opencoredev/loginwithchatgpt-react` | `useLoginWithChatGPT()` plus a drop-in `` widget with a built-in consent step. | | `@opencoredev/loginwithchatgpt-ai` | Vercel AI SDK providers: a browser-safe proxy provider and a server-side direct provider. | | `@opencoredev/loginwithchatgpt-core` | The primitives underneath: device-code OAuth, PKCE, token refresh, JWT parsing, and the Codex transport. | Most apps install the first three and never touch `core`. ## Where to go [#where-to-go] Install, mount the handler, render the button, stream a response. A working chat UI with a model picker, in one component. The device-code flow and the session state machine. What a signed-in app can and cannot do. # Quickstart (/docs/quickstart) This page wires the default path: a server route at `/api/chatgpt/*`, a React sign-in button, and a browser-safe AI SDK provider that streams through your backend. ### Install the packages [#install-the-packages] bun npm pnpm ```bash bun add @opencoredev/loginwithchatgpt-server @opencoredev/loginwithchatgpt-react @opencoredev/loginwithchatgpt-ai ai @ai-sdk/openai ``` ```bash npm install @opencoredev/loginwithchatgpt-server @opencoredev/loginwithchatgpt-react @opencoredev/loginwithchatgpt-ai ai @ai-sdk/openai ``` ```bash pnpm add @opencoredev/loginwithchatgpt-server @opencoredev/loginwithchatgpt-react @opencoredev/loginwithchatgpt-ai ai @ai-sdk/openai ``` `ai` and `@ai-sdk/openai` are peer dependencies of `@opencoredev/loginwithchatgpt-ai`. ### Set a secret [#set-a-secret] The secret signs the session cookie and encrypts tokens before they enter your session store. Without one, the SDK generates an ephemeral secret and logs a warning, and every restart logs everyone out. ```bash title=".env" LWC_SECRET="use-the-output-of: openssl rand -hex 32" ``` ### Mount the server handler [#mount-the-server-handler] `createChatGPTHandler()` returns one Web-standard handler that owns login, polling, logout, model listing, and the streaming proxy. Bun Next.js ```ts title="index.ts" import { createChatGPTHandler } from "@opencoredev/loginwithchatgpt-server"; import index from "./index.html"; const auth = createChatGPTHandler({ secret: process.env.LWC_SECRET, responsesProxy: { allowedModels: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"], }, }); Bun.serve({ routes: { "/": index, "/api/chatgpt/*": (req) => auth.handler(req), }, }); ``` ```ts title="app/api/chatgpt/[...lwc]/route.ts" import { createChatGPTHandler } from "@opencoredev/loginwithchatgpt-server"; const auth = createChatGPTHandler({ secret: process.env.LWC_SECRET, responsesProxy: { allowedModels: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"], }, }); export const GET = (request: Request) => auth.handler(request); export const POST = (request: Request) => auth.handler(request); ``` The handler only needs Web-standard `Request`, `Response`, `fetch`, and `crypto`, so any runtime with those works. ### Render the sign-in button [#render-the-sign-in-button] ```tsx title="components/sign-in.tsx" "use client"; import { LoginWithChatGPT } from "@opencoredev/loginwithchatgpt-react"; export function SignIn() { return ( console.log("ChatGPT session connected")} /> ); } ``` Clicking the button opens a consent popup first. It cannot be disabled, only customized. If the user continues, the same popup goes to OpenAI's verification page, the widget copies the user code, polls for completion, and renders the signed-in state: The sign-in flow: consent, then the device code and verification window ### Discover models and stream [#discover-models-and-stream] Model availability depends on the account and plan, so ask instead of hardcoding a list. The proxy provider sends everything to your handler, which injects credentials from the session cookie. ```ts title="lib/chat.ts" import { createChatGPTProxyProvider } from "@opencoredev/loginwithchatgpt-ai"; import { streamText } from "ai"; const chatgpt = createChatGPTProxyProvider(); export async function ask(prompt: string) { const models = await chatgpt.listModels(); // GET /api/chatgpt/models const model = models.includes("gpt-5.5") ? "gpt-5.5" : models[0]; if (!model) throw new Error("No ChatGPT models returned for this account."); return streamText({ model: chatgpt(model), prompt }); } ``` ```ts const result = await ask("Explain HTTP cookies in one paragraph."); for await (const delta of result.textStream) { process.stdout.write(delta); } ``` ### Verify [#verify] 1. Click the button, continue past consent, and authorize in the OpenAI window. 2. `GET /api/chatgpt/session` returns `{ "status": "authenticated" }`. 3. `GET /api/chatgpt/models` returns at least one model slug. 4. A prompt streams text back through `POST /api/chatgpt/responses`. ## Next [#next] Turn this into a real chat UI with a model picker and streaming output. Shared session store, guardrails, and a smoke test before you deploy. # How it works (/docs/concepts/how-it-works) Login with ChatGPT is built around a server-owned session. The browser starts login and sends prompts, but it never receives access or refresh tokens. All it holds is an HttpOnly cookie that identifies the session on your server. ## The device-code flow [#the-device-code-flow] The SDK uses OAuth device authorization instead of a redirect flow, so your app never needs a localhost listener or a registered callback URL: 1. The browser calls `POST /api/chatgpt/login`. The handler requests a device code from `auth.openai.com` and stores it in the session. 2. The browser shows the user code (something like `7B0J-DPK78`) and opens OpenAI's verification page in a separate tab/window. 3. The user enters the code and authorizes on OpenAI's page. 4. The browser polls `GET /api/chatgpt/status`. Each request advances at most one upstream poll, at the interval OpenAI asked for. 5. Once authorized, the handler exchanges the code for tokens, encrypts them, and writes them to the session store. 6. From then on, `POST /api/chatgpt/responses` proxies AI requests with the stored tokens injected server-side. Device codes expire after 15 minutes. If the user never finishes, the session becomes `expired` and the UI shows a retry state. ## The state machine [#the-state-machine] The server session and the React hook move through the same states: | State | Stored server-side | Browser sees | | ----------------- | ------------------------------------------------------------------- | ---------------------------------- | | `unauthenticated` | Nothing. | A login button. | | `pending` | Device auth id, user code, verification URL, poll interval, expiry. | The user code and a waiting state. | | `authenticated` | Encrypted tokens plus public user metadata. | Email, name, and plan only. | | `expired` | Nothing usable. The session was deleted. | A re-authentication state. | The React hook adds two client-only states: `loading` while it checks the session on mount, and `connecting` between clicking login and getting a device code. ## Why polling is serverless-safe [#why-polling-is-serverless-safe] The server never runs a background loop. Each `GET /status` request performs at most one upstream poll and returns. If the client stops polling, nothing keeps running. The handler tracks `lastPolledAt` per session, so a client that polls aggressively still cannot hit OpenAI faster than the requested interval. That is also why the flow works on serverless runtimes: all state lives in the session store, and every step is an ordinary request/response cycle. ## Token boundaries [#token-boundaries] `accessToken`, `refreshToken`, and `idToken` are server secrets. They are encrypted at rest and never appear in a route response. The only user data the browser gets is `ChatGPTUser` (account id, email, name, plan), which comes from id-token claims. Normal app code uses `/responses`, `/models`, or `auth.proxyFetch(request)`, so bearer tokens stay inside the handler. Raw token export exists only behind the explicit dangerous escape hatch for advanced custody/migration cases. [Sessions & tokens](sessions.mdx) covers the mechanics. # Model discovery (/docs/concepts/models) Available model slugs depend on the signed-in ChatGPT account, its plan, and the configured `clientVersion`. Treat the model list as account data you fetch, not application config you hardcode. ## Listing models [#listing-models] In the browser, the proxy provider calls `GET /api/chatgpt/models` through your handler: ```ts title="browser" import { createChatGPTProxyProvider } from "@opencoredev/loginwithchatgpt-ai"; const chatgpt = createChatGPTProxyProvider(); const models = await chatgpt.listModels(); // ["gpt-5.5", "gpt-5.4", ...] ``` On the server, use the handler helper or the direct provider: ```ts title="server" const models = await auth.getModels(request); if (!models) return new Response("Unauthorized", { status: 401 }); ``` Both return a plain `string[]`. Upstream response wrappers vary, so the SDK extracts slugs tolerantly and ignores shapes it does not recognize (`extractCodexModelSlugs()` in `core` is the place to update if the wrapper changes). ## Choosing a default [#choosing-a-default] Prefer the current recommended model when the account has it, instead of taking whatever slug happens to be first: ```ts const models = await chatgpt.listModels(); const model = models.includes("gpt-5.5") ? "gpt-5.5" : models[0]; if (!model) throw new Error("No ChatGPT models returned for this account."); ``` Fast tier is not a separate model slug. It is a request setting that some models support, so keep the model picker and the tier toggle separate. See [Fast tier & thinking effort](/docs/guides/fast-and-reasoning). ## Client version drift [#client-version-drift] The Codex backend gates model availability behind a `client_version` query parameter. The SDK sends `DEFAULT_CLIENT_VERSION` (currently `0.142.5`), but the upstream requirement moves over time. If every model starts failing with "model not supported" errors: 1. Call `GET /api/chatgpt/models` and check whether the list is empty. 2. If it is empty, or upstream rejects the request, bump `clientVersion`: ```ts title="server.ts" const auth = createChatGPTHandler({ secret: process.env.LWC_SECRET, clientVersion: "0.143.0", }); ``` 3. Re-test login, model listing, and one streaming response. ## Failure responses [#failure-responses] | Status | Meaning | | ---------------------------------- | --------------------------------------------------------------------------------------------- | | `401` | No authenticated session. Send the user through login again. | | `502` or forwarded upstream status | Token refresh or upstream listing failed. The body includes `{ models: [], error, message }`. | # Response proxy (/docs/concepts/response-proxy) The browser provider sends AI SDK traffic to `POST /api/chatgpt/responses`. The proxy exists so credentials never enter the browser, and so you have one chokepoint to police what a signed-in page can spend. ## Request lifecycle [#request-lifecycle] For every proxied request, the handler: 1. Verifies the signed session cookie. No cookie means `401`. 2. Rejects cross-origin browser requests unless the origin is in `allowedOrigins` (`403`). 3. Applies the per-session rate limit (`429` with `retry-after`). 4. Validates the JSON body: object shape, size under `maxRequestBytes`, model in `allowedModels` if configured. 5. Loads tokens and refreshes them if needed. 6. Normalizes the body for the stateless Codex backend, then adds the bearer token, account id header, and `client_version` query parameter. 7. Streams the upstream body back with `cache-control: no-store`. ## Guardrails and their defaults [#guardrails-and-their-defaults] Every proxied request spends the signed-in user's own plan, so the proxy is not a generic pass-through: | Guardrail | Default | Option | | -------------------------- | ------------------------------------- | --------------------------------------------------- | | Rate limit | 30 requests / 60s per session | `responsesProxy.rateLimit` (`false` to disable) | | Body size | 40 MiB | `responsesProxy.maxRequestBytes` | | Model allowlist | all models | `responsesProxy.allowedModels` (array or predicate) | | Cross-origin browser calls | same-origin only | `allowedOrigins` | | Default model injection | `gpt-5.5` when the body omits `model` | `defaultModel` | ```ts title="server.ts" const auth = createChatGPTHandler({ secret: process.env.LWC_SECRET, responsesProxy: { allowedModels: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"], rateLimit: { limit: 10, windowMs: 60_000 }, }, }); ``` The rate limiter counts in memory. Behind multiple instances or serverless invocations, each instance counts separately. Pass a shared `store` in `responsesProxy.rateLimit` to enforce the limit globally. If you need custom quotas, audit logging, or stricter endpoint controls, keep the request-scoped proxy pattern and wrap `auth.proxyFetch(request)` in your own route. Raw token export is available only through the explicit dangerous escape hatch in the [server reference](/docs/reference/server#dangerouslygettokensrequest-options). ## Request normalization [#request-normalization] The ChatGPT-backed Codex endpoint is stateless and rejects some standard Responses API fields. `createCodexFetch()` from `@opencoredev/loginwithchatgpt-core` rewrites every body the same way for both the proxy and the direct provider: * Forces `store: false`. Turns are stateless. * Fills `instructions`, `reasoning` (`effort: "medium"`, `summary: "auto"`), and `text.verbosity: "medium"` when the body leaves them out. * Adds `reasoning.encrypted_content` to `include`, so multi-turn reasoning survives statelessness. * Drops `item_reference` input items and strips `id` fields from the rest. * Removes `max_output_tokens` and `max_completion_tokens`, which the endpoint rejects. ## SDK headers [#sdk-headers] Browser code cannot rewrite proxied bodies, so two narrow headers let it opt into request settings. The server validates each value, writes it into the body, and returns `400` for anything else. The values and UI patterns are in [Fast tier & thinking effort](/docs/guides/fast-and-reasoning); the exact contract is in the [routes reference](/docs/reference/routes#post-responses). One behavior worth knowing here: if upstream rejects `service_tier=fast` for the session, the handler retries once without it and adds a `x-login-with-chatgpt-service-tier-fallback: auto` response header, so the request still streams. # Security model (/docs/concepts/security) Login with ChatGPT stores refreshable credentials for a ChatGPT account and can spend that user's plan. Treat the handler as usage-sensitive infrastructure, not a harmless social login button. ## The threat model, honestly [#the-threat-model-honestly] This SDK is open source and runs on the app developer's own server. There is no hosted service in the middle. That means the SDK's guardrails protect users from buggy or careless integrations, not from a malicious developer, who controls the server and can change the code. Three things actually bound the damage: **The SDK only uses the session for model listing and AI requests.** The OAuth session belongs to the public Codex client and the SDK routes it to the ChatGPT-backed Codex endpoints it supports. Do not promise users tighter access boundaries than OpenAI grants at the token level. A malicious server controls its own code, can spend the user's plan through supported AI requests, and can read the profile fields in the id token (email, name, plan). **The user's consent is the real gate.** The default widget shows a usage-risk warning before login, always. It can be customized but not disabled. **Sessions are revocable.** Signing out deletes the app's stored session, and the app keeps nothing that works afterward. ## User consent [#user-consent] The default `` widget shows a consent step before anything touches OpenAI: a tab/window, or inline when the window is blocked. It tells users that requests use their own ChatGPT plan, that prompts and attachments pass through your server, that the app never sees their password and that this SDK uses the session for model listing and AI requests, and that signing out deletes the stored session. If you build a custom UI with `useLoginWithChatGPT()` or a `children` renderer, that obligation moves to you. Render equivalent consent before calling `login()`. Keep the copy short and specific. "I trust this app, continue" is better than a vague "Connect". ## Default guardrails [#default-guardrails] On without any configuration: * In the default configuration, refresh tokens never leave the session layer. `auth.getTokens(request)` / `auth.dangerouslyGetTokens(request)` are disabled unless the handler is configured with `dangerouslyAllowTokenExport: true`. * Normal app code can use `/responses`, `/models`, or `auth.proxyFetch(request)` without receiving raw bearer tokens. * The `/responses` proxy rate limits each session at 30 requests/minute, so runaway client code cannot quietly burn a user's plan. * Cookie-authenticated non-GET routes reject cross-origin browser requests unless the origin is in `allowedOrigins`. Third-party sites cannot ride the session cookie, even with `SameSite=None`. * Tokens are AES-GCM encrypted at rest when `secret` is set. * Sessions that never authenticate expire from the store in \~30 minutes, so unauthenticated `POST /login` calls cannot pile up long-lived entries. ## Your guardrails [#your-guardrails] The flow has no narrow scopes. There is no "read-only", "one request", or "max spend" grant, so once authorized, your server can proxy requests until the session expires or is deleted. Layer product-level limits on top: * Restrict models with `responsesProxy.allowedModels`, and tighten `responsesProxy.rateLimit` with a shared store across instances. * Cap prompt length, output length, and attachment size at your app boundary. * Keep `/responses` the only spend path. Do not expose a generic OpenAI proxy. * Do not enable `dangerouslyAllowTokenExport` unless the application server is intentionally trusted to hold raw ChatGPT bearer tokens. * Use a short `sessionTtlMs` for risky or public apps. * Add an emergency kill switch in front of the proxy route. * Log request metadata for abuse review, not prompts or file contents. * Rate limit `/login` and `/status` at the edge (WAF, CDN, or gateway). They are unauthenticated by design, because no session exists yet, so the handler cannot see a trustworthy client IP to limit them itself. Without an edge limit, a script can drive repeated upstream device-code requests against your OpenAI client. ## Sign-out and cleanup [#sign-out-and-cleanup] Give users a visible sign-out that calls the logout route. It deletes the stored session server-side, and it is the most reliable way to remove this app's credentials: OpenAI's active-sessions UI does not necessarily list third-party or device-code sessions. For users who want to review their account anyway, link: * [ChatGPT security settings](https://chatgpt.com/#settings/Security) * [Managing active sessions in ChatGPT](https://help.openai.com/en/articles/20001257-managing-active-sessions-in-chatgpt) # Sessions & tokens (/docs/concepts/sessions) A session is one browser's relationship with one ChatGPT account. The browser holds a signed cookie. Your server holds everything else. ## The session cookie [#the-session-cookie] The handler sets one cookie, `lwc_session`, containing a random 24-character id signed with HMAC-SHA256 using your `secret`. It carries no data. It is only a key into the session store. | Attribute | Default | | ---------- | ------------------------------------- | | `HttpOnly` | `true` | | `SameSite` | `Lax` | | `Secure` | `true` when the request is HTTPS | | `Path` | `/` | | `Max-Age` | derived from `sessionTtlMs` (30 days) | Override any attribute with the `cookie` option on `createChatGPTHandler()`. The handler sets `Secure` when the request URL is `https:` or when `X-Forwarded-Proto` starts with `https`. If your reverse proxy terminates TLS without forwarding that header, cookies go out without `Secure`. Fix the proxy, or force it with `cookie: { secure: true }`. ## The session store [#the-session-store] Sessions live in a `KeyValueStore`. The default is an in-process `MemoryStore`, which is fine for local development and wrong for production: it loses sessions on restart and cannot be shared across instances or serverless invocations. ```ts title="server.ts" import { createChatGPTHandler } from "@opencoredev/loginwithchatgpt-server"; const auth = createChatGPTHandler({ secret: process.env.LWC_SECRET, sessionStore: redisBackedStore, // any KeyValueStore implementation }); ``` The interface is small, so wrapping Redis, a database table, or `Bun.redis` takes a few lines: ```ts interface KeyValueStore { get(key: string): Promise | T | undefined; set(key: string, value: T, options?: { ttlMs?: number }): Promise | void; delete(key: string): Promise | void; } ``` ## Token encryption [#token-encryption] When `secret` is set, tokens are AES-GCM encrypted before entering the store, with a key derived via SHA-256 and a random IV per write. The store never sees plaintext tokens. Still treat it as sensitive: restrict access, avoid logging session objects, and delete sessions on logout. Rotating the secret invalidates all existing session cookies and makes stored token payloads undecryptable. Users just sign in again, but plan the rotation rather than improvising it. ## Refresh mechanics [#refresh-mechanics] Access tokens are short-lived. The refresh token can mint new ones until the session dies. The handler refreshes automatically: * A token counts as expired 60 seconds before its actual expiry, so proxied requests never race the deadline. * Concurrent requests that need a refresh share one in-flight refresh per session (per instance). A rotated refresh token is never invalidated by a double refresh. * When OpenAI reports the refresh token as dead (`refresh_token_expired`, `refresh_token_reused`, `refresh_token_invalidated`, or `invalid_grant`), the handler deletes the session and reports `expired`. Show the login button again. ## What app code gets [#what-app-code-gets] Normal app code should not receive bearer tokens at all. Use `/responses`, `/models`, or `auth.proxyFetch(request)` so the handler loads, refreshes, and injects credentials internally: ```ts const chatgpt = createChatGPTProxyProvider({ fetch: auth.proxyFetch(request), }); ``` Raw token export is an advanced escape hatch and is disabled by default: ```ts const tokens = await auth.dangerouslyGetTokens(request); // Requires dangerouslyAllowTokenExport: true. const exported = await auth.dangerouslyGetTokens(request, { includeRefreshToken: true, }); // Also requires dangerouslyAllowRefreshTokenExport: true. // Only for exporting a session, e.g. a store migration. Never log or forward this. ``` # Build a chat page (/docs/guides/chat-app) This guide builds the smallest useful chat page: the sign-in widget, a model picker filled from the account, a prompt box, and a streamed answer. It is a trimmed-down version of the playground that ships in the repo: The demo playground: model picker, fast toggle, thinking level, and a streamed response You need the [quickstart](/docs/quickstart) done first, so the handler is mounted at `/api/chatgpt/*`. ## The component [#the-component] ```tsx title="components/chat.tsx" "use client"; import { ChatGPTProxyError, createChatGPTProxyProvider } from "@opencoredev/loginwithchatgpt-ai"; import { LoginWithChatGPT } from "@opencoredev/loginwithchatgpt-react"; import { streamText } from "ai"; import { useEffect, useState } from "react"; // Module-level: one provider per page load. const chatgpt = createChatGPTProxyProvider(); export function Chat() { const [models, setModels] = useState([]); const [model, setModel] = useState(""); const [prompt, setPrompt] = useState(""); const [answer, setAnswer] = useState(""); const [running, setRunning] = useState(false); const [needsLogin, setNeedsLogin] = useState(false); useEffect(() => { let active = true; chatgpt .listModels() .then((list) => { if (!active) return; setModels(list); setModel(list.includes("gpt-5.5") ? "gpt-5.5" : (list[0] ?? "")); setNeedsLogin(false); }) .catch((error) => { if (!active) return; // 401 means there is no session yet. Show the sign-in button. if (error instanceof ChatGPTProxyError && error.status === 401) { setNeedsLogin(true); } }); return () => { active = false; }; }, []); async function send() { if (running || !model || !prompt.trim()) return; setRunning(true); setAnswer(""); try { const result = streamText({ model: chatgpt(model), prompt }); for await (const delta of result.textStream) { setAnswer((current) => current + delta); } } finally { setRunning(false); } } if (needsLogin) { return ( window.location.reload()} /> ); } return (