Providers
An agent runs on AI Gateway unless you choose another provider:
| Provider | provider | Credentials |
|---|---|---|
| AI Gateway | omitted, or 'gateway' | An API key, or the deployment's OIDC token on Vercel |
| ChatGPT plan | 'codex' | The user's ChatGPT credentials, from libfx/chatgpt |
| Grok plan | 'grok' | The user's Grok credentials, from libfx/grok |
| Custom provider | { name, baseUrl, models } | An API key, or none |
The provider applies to the whole agent and every session it runs.
AI Gateway
libfx uses the first Gateway credential it finds: the apiKey option, then the AI_GATEWAY_API_KEY environment variable, then on Vercel the deployment's OIDC token. A browser has no environment, so pass apiKey there, and use a short-lived key.
createFxAgent({ apiKey: process.env.AI_GATEWAY_API_KEY })
ChatGPT plans
An agent can run on a user's ChatGPT plan through Codex. Set provider: 'codex' and give libfx the user's credentials:
const agent = createFxAgent({
provider: 'codex',
model: 'gpt-6.1-sol',
credentials: ({ provider, sessionId }) => tokens.load(provider, sessionId),
onCredentials: (credentials, { provider, sessionId }) =>
tokens.save(provider, sessionId, credentials),
})
libfx calls credentials() in whichever process runs the turn, with the session's id. It refreshes the credentials shortly before they expire and once after a rejected request, then passes the new ones to onCredentials for you to save. Codex doesn't accept requests from web pages, so these turns run on your server.
Sign a user in
With libfx/chatgpt, the user signs in on OpenAI's site and pastes back the address it opens:
import { startSignIn, finishSignIn } from 'libfx/chatgpt'
// One request: send the user to signIn.url and keep signIn on the server.
const signIn = await startSignIn()
// A later request: the user pastes back the address OpenAI opened.
const credentials = await finishSignIn(signIn, pastedAddress)
After the user signs in, OpenAI opens a page at localhost:1455 that doesn't load. That's expected: ask the user to copy the page's whole address and paste it back. signIn holds a secret, so keep it on the server.
If the user's account or ChatGPT workspace allows device code sign-in, show a short code instead:
import { startDeviceSignIn, completeDeviceSignIn } from 'libfx/chatgpt'
// Show device.userCode and a link to device.verificationUrl.
const device = await startDeviceSignIn()
const credentials = await completeDeviceSignIn(device)
Each step returns plain data, so a later request or function can finish it. pollDeviceSignIn(device) checks once and returns null until the user enters the code. The sign-in uses the same OpenAI client as the Codex CLI, and the user's ChatGPT plan and OpenAI's terms apply to the turns.
Grok plans
provider: 'grok' runs on a user's Grok plan, with the same credentials and onCredentials callbacks, called with provider: 'grok':
const agent = createFxAgent({
provider: 'grok',
model: 'grok-4.5',
credentials: ({ provider, sessionId }) => tokens.load(provider, sessionId),
onCredentials: (credentials, { provider, sessionId }) =>
tokens.save(provider, sessionId, credentials),
})
libfx refreshes Grok credentials the same way, and checks that refreshed credentials still belong to the same xAI account. These turns run on your server too.
libfx/grok signs a user in. Device code is the simplest:
import { startDeviceSignIn, completeDeviceSignIn } from 'libfx/grok'
// Show device.userCode and a link to device.verificationUrl.
const device = await startDeviceSignIn()
const credentials = await completeDeviceSignIn(device)
startSignIn() and finishSignIn() work as for ChatGPT, except that xAI opens a page at 127.0.0.1. The sign-in uses the same xAI client as the fx CLI, and the user's Grok plan and xAI's terms apply to the turns.
Refresh across servers
The sessions in one process share each refresh. Across servers, two can notice the same expiring credentials at once, and the provider accepts only the first refresh. Pass refreshLock so they take turns:
createFxAgent({
provider: 'codex',
credentials, // the callbacks from above
onCredentials,
refreshLock: ({ provider, sessionId }, refresh) =>
locks.withLock(`${provider}:${accountOf(sessionId)}`, refresh),
})
libfx calls it with a function to run while you hold a lock on that user's account, for example in Redis. Inside it, libfx calls credentials() again and uses what another server already saved. It works for ChatGPT and Grok plans.
Custom providers
An agent can run on a server that speaks the OpenAI Chat Completions API, such as a local model server or a router:
const agent = createFxAgent({
provider: { name: 'local', baseUrl: 'http://localhost:11434/v1' },
model: 'qwen3:8b',
})
Requests go to {baseUrl}/chat/completions, which must use HTTPS, or HTTP on localhost. A server that needs a key gets it from credentials, which libfx calls with the provider's name:
const router = {
name: 'router',
baseUrl: 'https://openrouter.ai/api/v1',
models: { 'qwen/qwen3-coder': { contextWindow: 128000, supportsToolUse: true } },
}
const agent = createFxAgent({
provider: router,
model: 'qwen/qwen3-coder',
credentials: async ({ provider, sessionId }) => ({ apiKey: await keys.load(provider, sessionId) }),
})
models tells libfx what it can't ask the server: each model's contextWindow and maxOutputTokens, and whether it supportsToolUse and supportsVision. Tool calls work when the model and server support them. A key isn't refreshed, so a rejected one ends the turn with the server's error. In a browser, the server must accept cross-origin requests, and the page can read the key.
Saved sessions follow the agent's provider: point an agent at another base URL, and its sessions continue there, history included.
Where credentials go
libfx adds ChatGPT, Grok, and custom-provider credentials to each request in JavaScript, just before it's sent, and sends them only to that provider. They never reach the engine. No credential is written to a saved session or passed to onEvent, and if a server quotes a key or token back in an error, libfx masks it before the error reaches the turn.