- OpenAIUpdated
- Anthropic
- Google Gemini
- Google Vertex AI
- Ollama
- Grok (xAI)
- GroqUpdated
- Mistral
- Cohere
- TypeSafe
- Ollaya
- ElevenLabs
- fal.ai
- Reactor
- World Labs
- OpenRouter AdapterUpdated
- Perplexity Search
- Vercel AI Gateway
- Lovable AI Gateway
- OpenAI-CompatibleUpdated
- LLM Gateway
- CloudflareUpdated
- Claude Code
- Codex
- OpenCode
- Grok Build
- ACP-Compatible
- Amazon BedrockUpdated
- BytePlus
Codex
The Codex adapter runs OpenAI Codex (via the @openai/codex-sdk) as a chat backend. Unlike HTTP provider adapters, this is a harness adapter: Codex runs its own agent loop and executes its own tools — shell commands, file changes, web search — locally on your server, inside its sandbox. Each chat() call runs one full harness turn; the harness's tool activity streams back as already-resolved tool-call events your UI can render.
Server-only. The harness spawns the Codex runtime (bundled with the SDK) as a subprocess, so this adapter only works in a Node.js server environment — never in the browser. The sandbox mode is the safety boundary; configure it deliberately.
Installation#
npm i @tanstack/ai-codexpnpm add @tanstack/ai-codexyarn add @tanstack/ai-codexbun add @tanstack/ai-codexA runnable demo lives at examples/sandbox-cloudflare — pick Claude Code, Codex, or Grok Build in the UI, with session resume, the harness tool timeline, and tool bridging, wired into a TanStack Start app on Workers. For the same wiring on plain Node with durable, refresh-surviving runs (Claude Code on Docker), see examples/sandbox-web — swapping in this adapter is a one-line change (src/sandbox-agent.ts).
Authentication#
Your laptop can already have codex login. A CI runner only has CODEX_API_KEY. The default authMode is 'api-key'. Set 'host' when you want codex login. See Harness Auth.
import { codexText } from "@tanstack/ai-codex"
codexText("gpt-5.5")
codexText("gpt-5.5", { authMode: "host" })- 'api-key' (default): expect CODEX_API_KEY (or pass apiKey).
- 'host': use codex login. Do not inject CODEX_API_KEY.
Basic Usage#
import { chat } from "@tanstack/ai";
import { codexText } from "@tanstack/ai-codex";
const stream = chat({
adapter: codexText("gpt-5.1-codex", {
cwd: "/path/to/project",
sandboxMode: "workspace-write",
}),
messages: [{ role: "user", content: "Fix the failing test in utils.test.ts" }],
});Configuration#
| Option | Description |
|---|---|
| cwd | Working directory for the harness session. Defaults to process.cwd(). |
| sandboxMode | Codex sandbox: 'read-only', 'workspace-write', or 'danger-full-access'. Default is 'workspace-write' on local-process and Docker. Default is 'danger-full-access' on Daytona and Cloudflare, because those providers cannot create a nested bubblewrap namespace. Isolation is then the outer VM plus defineSandboxPolicy. |
| approvalPolicy | Codex approval policy. Defaults to 'never' — headless runs have no approval UI, so anything else can stall a turn. |
| modelReasoningEffort | The default effort when a call sets no reasoning: 'minimal' | 'low' | 'medium' | 'high'. |
| skipGitRepoCheck | Skip the harness's git-repo safety check. Defaults to true (server adapters routinely point at scratch directories). |
| networkAccessEnabled | Allow network access inside the workspace-write sandbox. |
| webSearchMode | 'disabled' | 'cached' | 'live'. |
| additionalDirectories | Extra writable directories beyond cwd. |
| authMode | 'api-key' (default) expects CODEX_API_KEY. 'host' uses codex login. See Harness Auth. |
| apiKey | OpenAI API key for the harness subprocess. |
| baseUrl | Override the Codex backend base URL. |
| codexPathOverride | Use a specific codex executable instead of the SDK's bundled binary. |
| env | Environment variables for the subprocess. When set, process.env is not inherited (Codex SDK semantics). |
| config | Extra --config key=value overrides passed to the Codex CLI (e.g. additional mcp_servers entries). |
Per-call overrides go through modelOptions: sessionId, sandboxMode, approvalPolicy, workingDirectory, skipGitRepoCheck, and authMode. Set the effort per call with reasoning on chat(). See Reasoning.
Reasoning#
Set reasoning on chat(). Codex gets it as model_reasoning_effort:
import { chat } from "@tanstack/ai";
import { codexText } from "@tanstack/ai-codex";
const stream = chat({
adapter: codexText("gpt-5.3-codex"),
messages: [{ role: "user", content: "Fix the failing test in utils.test.ts" }],
reasoning: "xhigh",
});- gpt-5.3-codex: off, low to xhigh. gpt-5.2-codex: low to xhigh.
- gpt-5.1-codex, gpt-5.1-codex-mini: low, medium, high. gpt-5.1: off, low, medium, high.
- A call without reasoning uses modelReasoningEffort from the config.
Stateful Sessions#
Codex threads are stateful — the harness keeps the full working context (files read, commands run, conclusions reached) between turns. The adapter surfaces the thread id of every fresh run as a custom stream event named codex.session-id; thread it back via modelOptions.sessionId to resume. When resuming, only the latest user message is sent — the harness already holds the prior context.
Server endpoint:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { codexText } from "@tanstack/ai-codex";
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
// Extra fields the client puts in the connection `body` arrive here.
const sessionId =
typeof params.forwardedProps.sessionId === "string"
? params.forwardedProps.sessionId
: undefined;
const stream = chat({
adapter: codexText("gpt-5.1-codex", {
cwd: "/path/to/project",
sandboxMode: "workspace-write",
}),
messages: params.messages,
modelOptions: { sessionId },
});
return toServerSentEventsResponse(stream);
}Client (React) — capture the session id from the custom event and send it back on subsequent requests:
import { useState } from "react";
import { useChat } from "@tanstack/ai-react";
import { fetchServerSentEvents } from "@tanstack/ai-client";
function CodingAssistant() {
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat", () => ({
body: { sessionId },
})),
onCustomEvent: (name, value) => {
if (
name === "codex.session-id" &&
typeof value === "object" &&
value !== null &&
"sessionId" in value &&
typeof value.sessionId === "string"
) {
setSessionId(value.sessionId);
}
},
});
// ... render messages; harness tool activity (command_execution,
// file_change, ...) arrives as regular tool-call parts with results.
}Sessions are stored on the machine that ran them (~/.codex/sessions/), so resuming only works on the same server instance.
Tools#
Two kinds of tools flow through this adapter:
Built-in harness tools are executed by Codex itself and stream back as tool-call events with results already attached: command_execution (shell), file_change (patches), web_search, and todo_list (the agent's running plan). Your code never executes them.
Your TanStack tools are bridged into the harness: the adapter starts a short-lived Streamable-HTTP MCP server on 127.0.0.1 for the duration of the turn and points Codex at it. Define tools as usual with toolDefinition().server(); tool-call events come back under the names you registered.
import { z } from "zod";
import { chat, toolDefinition } from "@tanstack/ai";
import { codexText } from "@tanstack/ai-codex";
const lookupTicket = toolDefinition({
name: "lookup_ticket",
description: "Look up an issue ticket by id",
inputSchema: z.object({ ticketId: z.string() }),
}).server(async ({ ticketId }) => {
return { ticketId, status: "open", title: "Crash on startup" };
});
const stream = chat({
adapter: codexText("gpt-5.1-codex"),
messages: [{ role: "user", content: "What's the status of ticket T-123?" }],
tools: [lookupTicket],
});Client-side and approval-gated tools are not supported. The harness executes tools inside a live subprocess, which cannot pause across HTTP requests to wait for a browser round-trip or a human approval. Passing a tool without a server execute() implementation — or one marked needsApproval — fails fast with a descriptive error. Run those tools outside the harness with a regular provider adapter.
Tool choice#
You want Codex to skip your tools, or to see just one of them. Pass toolChoice to chat():
const noBridgedTools = chat({
adapter: codexText("gpt-5.3-codex"),
messages: [{ role: "user", content: "Explain the bug in src/app.ts." }],
tools: [lookupTicket],
toolChoice: "none",
});toolChoice limits only the tools that the adapter bridges into Codex. The built-in Codex tools always stay on, and Codex decides when it calls a tool.
| Value | What the adapter does |
|---|---|
| 'auto' | Bridges all of your tools. |
| 'none' | Bridges none of your tools. |
| { type: 'tool', name } | Bridges only the named tool. Codex does not have to call it. |
| 'required' | Bridges all of your tools. Codex does not have to call one. |
Each value except 'auto' logs a warning the first time an adapter instance gets it.
For the values on other adapters, see Choose when the model calls a tool.
Structured Output#
Pass outputSchema on chat(). Codex runs one harness turn and constrains the last message with --output-schema. Tool activity and assistant text stream as Codex writes them. The last message is also parsed as the schema object and arrives as structured-output.complete.
import { chat } from "@tanstack/ai"
import { codexText } from "@tanstack/ai-codex"
import { defineSandbox, withSandbox } from "@tanstack/ai-sandbox"
import { dockerSandbox } from "@tanstack/ai-sandbox-docker"
import { z } from "zod"
const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
})
const sandbox = defineSandbox({
id: "repo-report",
provider: dockerSandbox({ image: "node:22" }),
})
const report = await chat({
adapter: codexText("gpt-5.3-codex"),
messages: [{ role: "user", content: "Review this repo." }],
outputSchema: Report,
middleware: [withSandbox(sandbox)],
})
report.summaryOn the client, pass the same schema to useChat and read final. partial stays empty until the end.
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react"
import { z } from "zod"
const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
})
function ReportView() {
const { final, isLoading } = useChat({
connection: fetchServerSentEvents("/api/repo-report"),
outputSchema: Report,
})
if (isLoading) return <p>The agent is inspecting the repo.</p>
if (!final) return null
return <p>{final.summary}</p>
}If you only need to extract JSON from a prompt and do not need a sandbox, use @tanstack/ai-openai. That path is faster.
Full walkthrough, including the client: Harness Agents.
Limitations#
- No token-level text streaming. The Codex SDK reports assistant text and reasoning only as completed items, so text arrives message-at-a-time. Tool activity (commands starting/finishing) still streams live, which keeps the UI feeling alive during long turns.
- Server-only (Node). The harness spawns a subprocess.
- The harness owns the agent loop. TanStack's agent-loop strategies and per-iteration middleware don't apply inside a harness turn.
- No sampling controls. temperature-style options don't exist here.
- Sessions are machine-local. Resume requires hitting the same server instance.
- Cold starts. Each call spawns a harness turn; expect higher first-token latency than HTTP adapters.
This adapter ignores wrapFetch, because the harness process sends the model requests.