# Harness adapters (/brain/docs/adapters/harness) A **harness adapter** runs the model loop, manages conversation context and compaction, and writes native session state inside the session's `native/` directory while Brain coordinates requests, tool dispatch, event logs, and checkpoints. ## Go lifecycle (`harness.Factory` and `harness.Runner`) [#go-lifecycle-harnessfactory-and-harnessrunner] `Factory.Open(ctx, opts)` receives a `harness.Options` struct containing the session ID, the `native/` working directory (`Dir`), the immutable session `Config`, enabled `Tools` schemas, resolved `Provider` credentials (`APIKey`, `BaseURL`, `API`), and two callbacks: `Event` (for emitting `assistant.delta` and `assistant.message`) and `Call` (for executing bound tools). The application package exports these contracts as `brain.HarnessFactory`, `brain.Harness`, and `brain.HarnessOptions`. `Open` returns a runner with five methods (see [Go interfaces](/brain/docs/adapters/interfaces)): | Method | Purpose | | --- | --- | | `Prompt(ctx, text)` | Runs one user turn to completion (including any model-initiated tool calls and follow-up generations) and returns when the turn settles. | | `Steer(ctx, text)` | Steers an active turn if `Factory.SupportsSteering()` is enabled. | | `Interrupt(ctx)` | Aborts the active turn cleanly when the caller sends `user.interrupt` or closes the session. | | `Checkpoint(ctx)` | Returns the list of `core.NativeFile` entries (`path`, `size`, `append_only`) relative to `Dir` required to restore the conversation. | | `Close()` | Terminates the runner and releases its resources. | ### Core runner invariants [#core-runner-invariants] - **Passive open:** Opening an existing session directory must load saved native state without calling the model, re-running tools, or starting background cache-warming timers. - **Settled return:** `Prompt` blocks until the turn finishes or is interrupted. - **Clean checkpoints:** `Checkpoint` is called only after `Prompt` settles and must return regular, append-only relative file paths and exact byte sizes in `Dir`, excluding any provider credentials. ## Child process stdio protocol (`harness.ProcessFactory`) [#child-process-stdio-protocol-harnessprocessfactory] You do not need to write Go code to add a harness runner. Every entry in the `"harnesses"` configuration block uses `harness.ProcessFactory` (`internal/harness/process.go`) to spawn a child process (such as `node runners/pi/dist/main.js`) and communicate over newline-delimited JSON frames (`RunnerFrame`) on `stdin` and `stdout`. - **Frame limit:** Each line is capped at `4 MiB` (`MaxFrame = 4 << 20`) with strict JSON decoding (`DisallowUnknownFields`). - **Standard streams:** `stdout` is reserved strictly for `RunnerFrame` JSON lines. Brain suppresses runner diagnostic payloads from logs. - **Startup timeout:** The initial `open` request during `Factory.Open` has a 30-second timeout. ### Brain → Runner requests (`stdin`) [#brain--runner-requests-stdin] Brain sends RPC requests with `attachment`, `id`, `method`, and `params`. An attachment identifies one native session within a shared host. `prompt`, `steer`, `interrupt`, events, and tool callbacks also carry the active `turn` identity; replies echo the request's attachment and turn. Open, checkpoint, and detach have no active turn. ```json {"attachment":"ATTACHMENT_ID","id":"REQUEST_ID","method":"open","params":{"session_id":"SESSION_ID","dir":"/path/to/native","config":{"model":"openai/gpt-4.1-mini","tools":["echo"]},"tools":[{"name":"echo","description":"Echo text","input_schema":{"type":"object"}}],"provider":{"api_key":"PROVIDER_TOKEN"}}} {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"REQUEST_ID","method":"prompt","params":{"text":"Hello"}} {"attachment":"ATTACHMENT_ID","id":"REQUEST_ID","method":"checkpoint","params":null} ``` The runner replies on `stdout` with a matching `id` and either `result` or `error` (`code: "provider_error"` or `"brain_error"`): ```json {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"REQUEST_ID","result":{}} {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"REQUEST_ID","error":{"code":"provider_error","message":"upstream rate limit exceeded"}} ``` | Method | `params` | `result` | | --- | --- | --- | | `open` | `harness.Options` (`session_id`, `dir`, `config`, `tools`, `provider`) | `{}` (or diagnostic metadata such as `{"tools": [...]}`) | | `prompt` | `{"text": "..."}` (`core.Text`) | `{}` (or `{"interrupted": true}`) | | `steer` | `{"text": "..."}` (`core.Text`) | `{}` | | `interrupt` | `null` | `{}` | | `checkpoint` | `null` | `[{"path": "session/file.jsonl", "size": 1024, "append_only": true}]` (`[]core.NativeFile`) | | `detach` | `null` | `{}`; release this settled attachment without stopping peers. | ### Runner → Brain events and tool calls (`stdout`) [#runner--brain-events-and-tool-calls-stdout] While `prompt` is running, the runner can emit streaming events and reverse `tool.call` requests (up to 32 concurrent tool calls per runner): ```json {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","event":"assistant.delta","data":{"text":"Hello"}} {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"CALL_FRAME_ID","method":"tool.call","params":{"id":"TOOL_CALL_ID","name":"echo","arguments":{"value":"hi"}}} {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","event":"assistant.message","data":{"text":"Hello!","usage":{"input":12,"output":4},"stop_reason":"stop"}} ``` For each `tool.call`, Brain validates that the tool is bound to the session, appends a `tool.call` event, dispatches the call to the external tool service, appends a `tool.result` event, and writes the correlated response frame back to the runner's `stdin`: ```json {"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"CALL_FRAME_ID","result":{"content":[{"type":"text","text":"hi"}],"is_error":false}} ``` Wire types for this protocol are generated in `internal/harness` and `runners/pi/src/generated.ts` via `go run ./cmd/generate`. ## Isolation and verification [#isolation-and-verification] A harness adapter must disable all built-in local tools (such as local shell execution or file editors), ambient plugins, and background cache warmers so the model can only invoke tools explicitly bound in `config.tools`. `harness.ProcessFactory` passes only minimal OS environment variables (`PATH`, `SystemRoot`, `WINDIR`, `COMSPEC`, `PATHEXT`, `TMP`, `TEMP`, `TMPDIR`, `LANG`, `LC_ALL`, and `NO_COLOR=1`) and supplies provider credentials exclusively through the `open` frame. When testing a new harness adapter, verify: 1. **Passive open:** Reopening a saved directory makes zero model or tool calls until the next `Prompt`. 2. **Checkpoint fidelity:** Restoring a checkpoint into a fresh directory produces the exact same prompt context on the next turn. 3. **Failure separation:** Tool errors (`is_error: true`) return to the model as tool results, whereas broken stdio framing, unexpected exits, or disk failures abort the turn with `brain_error`. 4. **Clean interruption:** Interrupting a turn while a tool call is waiting stops execution cleanly and leaves prior checkpoint files intact. # Adapter overview (/brain/docs/adapters) Brain keeps its session coordinator (`internal/session`) small by delegating model execution, tool execution, and external storage to explicit extension boundaries. Implement a custom agent loop over `harness.Factory` / `harness.Runner` or the stdio JSON protocol. Store immutable checkpoint blocks and session discovery indexes via `persistence.Objects` and `persistence.Metadata`. Read the generated Go interface and struct definitions from `internal/harness` and `internal/persistence`. ## Extension boundaries at a glance [#extension-boundaries-at-a-glance] | Boundary | Go interface / protocol | When to build one | Built-in implementations | | --- | --- | --- | --- | | **Harness** | `harness.Factory` and `harness.Runner` (`internal/harness`), or newline-delimited JSON over `stdin`/`stdout` (`harness.ProcessFactory`). | You want to run a different agent loop, prompt compaction strategy, or runtime behind Brain's session API. | `pi` (`runners/pi`, a Node.js child process wrapping `@earendil-works/pi-coding-agent` via `harness.ProcessFactory`). | | **Tool service** | [HTTP JSON / SSE tool protocol](/brain/docs/tools) and [MCP Streamable HTTP](/brain/docs/tools/mcp) (`internal/tools`). | You want to expose external tools, sandboxes, resource gates, or operator approvals in any language without changing Brain. | `tools.Registry` (`internal/tools/registry.go`, with HTTP and MCP transports), plus reference services in `examples/tools`, `services/sandbox`, and `services/resources`. | | **Persistence** | `persistence.Objects` and `persistence.Metadata` (`internal/persistence`). | You want to publish checkpoint blocks or index session metadata in a storage backend not covered by the built-in drivers. | **Checkpoint object stores (`persistence.Objects`):** `filesystem`, `s3`, `r2`, and `gcs`.
**External metadata index (`persistence.Metadata`):** PostgreSQL (`postgres_env`). | ## Coordinator contracts [#coordinator-contracts] `session.Repository`, `session.ToolExecutor`, and `session.Checkpoints` contain only operations used by the session owner. `cmd/brain` constructs concrete implementations and owns their shutdown. The HTTP API depends only on `api.Catalog` for tool discovery. These Go extension points live under `internal/` and require a source build. `tools.Registry` owns authorization, schemas, deadlines, inspection delegation, and output policy. `tools.Transport` owns one call's wire protocol and connection cleanup. `tools.NewWithTransports` accepts compiled transport implementations; adding a protocol does not change scheduling. External tools still use HTTP/SSE/MCP without changing Go code. ## How each boundary plugs in [#how-each-boundary-plugs-in] 1. **External tool services** run out of process over HTTP JSON, `text/event-stream`, or MCP Streamable HTTP (`mcp_tool`). You bind them directly in the `"tools"` array of your configuration file without modifying or recompiling Brain. 2. **Harness runners** can be plugged in as external child processes under `"harnesses"` (speaking the `RunnerFrame` stdio protocol implemented by `harness.ProcessFactory`) or compiled into the binary by implementing `harness.Factory` in `internal/harness`. 3. **Persistence adapters** live in `internal/persistence` and are wired into `persistence.NewObjects` and `persistence.NewPostgres` from the `"persistence"` configuration block. The default local database is SQLite (`store.SQLite`); a compiled replacement implements `store.Metadata` and is supplied through `store.New`. Append-only `events.jsonl` and `native/` files remain in the local `data_dir`. See [Harness adapters](/brain/docs/adapters/harness), [Persistence adapters](/brain/docs/adapters/persistence), and [Go interfaces](/brain/docs/adapters/interfaces) for implementation details and contract requirements. # Go interfaces (/brain/docs/adapters/interfaces) Extracted directly from Go source during the documentation build. Application code imports public aliases from `github.com/aexhq/brain` for harnesses, providers, handlers, and session contracts. The public `Server` owns stock persistence and transport implementations. Repository and checkpoint adapter seams remain internal. See the [Go application SDK](/brain/docs/sdks/application). ## harness [#harness] ### Factory [#factory] ```go package harness // import "github.com/aexhq/brain/internal/harness" type Factory interface { Open(context.Context, Options) (Runner, error) SupportsSteering() bool } ``` ### Runner [#runner] ```go package harness // import "github.com/aexhq/brain/internal/harness" type Runner interface { Prompt(context.Context, string) error Steer(context.Context, string) error Interrupt(context.Context) error Checkpoint(context.Context) ([]core.NativeFile, error) Close() error } ``` ### Options [#options] ```go package harness // import "github.com/aexhq/brain/internal/harness" type Options struct { Lifetime context.Context `json:"-"` SessionID string `json:"session_id"` Dir string `json:"dir"` Config core.Config `json:"config"` Tools []core.Tool `json:"tools"` Provider Provider `json:"provider"` Event func(string, json.RawMessage) error `json:"-"` Call func(context.Context, core.Call) (core.Result, error) `json:"-"` } ``` ### RunnerFrame [#runnerframe] ```go package harness // import "github.com/aexhq/brain/internal/harness" type RunnerFrame struct { Attachment string `json:"attachment"` Turn string `json:"turn,omitempty"` ID string `json:"id,omitempty"` Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` Result json.RawMessage `json:"result,omitempty"` Error *core.Error `json:"error,omitempty"` Event string `json:"event,omitempty"` Data json.RawMessage `json:"data,omitempty"` } ``` ## persistence [#persistence] ### Objects [#objects] ```go package persistence // import "github.com/aexhq/brain/internal/persistence" type Objects interface { Put(context.Context, string, []byte, bool) error Get(context.Context, string) ([]byte, error) } func NewObjects(ctx context.Context, s Settings) (Objects, error) ``` ### Metadata [#metadata] ```go package persistence // import "github.com/aexhq/brain/internal/persistence" // Metadata is an eventual discovery index, never a session ownership lock. type Metadata interface { Put(context.Context, core.Session, string) error Get(context.Context, string, string) (core.Session, string, error) List(context.Context, string, string, int) ([]core.Session, error) Close() error } ``` ## session [#session] ### Repository [#repository] ```go package session // import "github.com/aexhq/brain/internal/session" // Repository is the local state consumed by the session owner. Metadata writes // are durable before returning. Root and Dir identify the local workspace used // by native harnesses and staged recovery; remote databases do not replace it. // The composition root owns Close, after Engine.Shutdown has drained executions. type Repository interface { Create(context.Context, core.Session) error Get(context.Context, string) (core.Session, error) Save(context.Context, core.Session) error List(context.Context, string, string, int) ([]core.Session, error) Scan(context.Context, string, int) ([]core.Session, error) Import(context.Context, core.Session) error Root() string Dir(string) (string, error) } ``` ### ToolExecutor [#toolexecutor] ```go package session // import "github.com/aexhq/brain/internal/session" // ToolExecutor applies authorization and dispatch policy outside scheduling. // Calls honor cancellation, do not retry, and return model-visible failures. type ToolExecutor interface { Bind(context.Context, *core.Session) error Describe(context.Context, core.Session) ([]core.Tool, error) NeedsInspection(string) bool Invoke(context.Context, core.Session, core.ToolScope, core.Call, func(json.RawMessage)) (core.Result, error) } ``` ### Checkpoints [#checkpoints] ```go package session // import "github.com/aexhq/brain/internal/session" // Checkpoints publishes and restores immutable snapshots without inference. // List is eventual discovery; it must never be used as an ownership lock. type Checkpoints interface { Publish(context.Context, persistence.Snapshot) (persistence.Manifest, error) Restore(context.Context, string, string, string, string) (persistence.Manifest, string, error) List(context.Context, string, string, int) ([]core.Session, error) } ``` ## store [#store] ### Metadata [#metadata-1] ```go package store // import "github.com/aexhq/brain/internal/store" // Metadata is the authoritative local session database, not an eventual index. // Writes are durable on return. Save and Import must never change a tenant. // List and Scan return ascending IDs strictly after the supplied cursor. // Missing Get/Save returns core.NotFound; conflicting Import returns core.Forbidden. // Implementations must support concurrent calls and context cancellation. type Metadata interface { Create(context.Context, core.Session) error Get(context.Context, string) (core.Session, error) Save(context.Context, core.Session) error List(context.Context, string, string, int) ([]core.Session, error) Scan(context.Context, string, int) ([]core.Session, error) Import(context.Context, core.Session) error Close() error } ``` ## tools [#tools] ### Transport [#transport] ```go package tools // import "github.com/aexhq/brain/internal/tools" // Transport executes a single authorized, validated call. It must honor ctx, // bound protocol reads, preserve outcome ambiguity, and never retry an action. // The registry owns transports after NewWithTransports succeeds. type Transport interface { Call(context.Context, Binding, Request, func(json.RawMessage)) core.Result Close() } ``` ### Binding [#binding] ```go package tools // import "github.com/aexhq/brain/internal/tools" type Binding struct { Handler Handler `json:"-"` Credential string `json:"-"` Stdio *StdioConfig `json:"-"` core.Tool Transport string `json:"transport,omitempty"` URL string `json:"url"` MCPTool string `json:"mcp_tool,omitempty"` TokenEnv string `json:"token_env,omitempty"` Tenants []string `json:"tenants"` TimeoutSeconds int `json:"timeout_seconds,omitempty"` MaxOutputBytes int64 `json:"max_output_bytes,omitempty"` InspectSession bool `json:"inspect_session,omitempty"` } func DiscoverMCP(ctx context.Context, binding Binding, names []string) ([]Binding, error) ``` ### Request [#request] ```go package tools // import "github.com/aexhq/brain/internal/tools" type Request struct { core.ToolScope Arguments json.RawMessage `json:"arguments"` } ``` # Persistence adapters (/brain/docs/adapters/persistence) Persistence adapters implement two small Go interfaces in `internal/persistence`: `Objects` for immutable checkpoint blocks and manifests, and `Metadata` for optional cross-instance session discovery (see [Go interfaces](/brain/docs/adapters/interfaces)). ## Authoritative local database [#authoritative-local-database] `store.Metadata` is separate from the eventual `persistence.Metadata` index. Its `Create`, `Get`, `Save`, `List`, `Scan`, and `Import` operations must preserve durable writes and tenant ownership. `List` and host-only `Scan` page by ascending ID. `Save` cannot create a missing record; `Import` cannot transfer a session to another tenant. All operations honor context cancellation. `store.SQLite` is the built-in implementation. `store.New(root, metadata)` combines an adapter with local session files and takes ownership of its lifecycle. It prepares native directories before creating metadata; a database failure propagates to the caller. Run `internal/store/metadata_test.go` against each implementation, plus the restart/recovery journeys. A remote SQL database alone does not provide session placement, distributed ownership, or remote native files. ## Built-in implementations [#built-in-implementations] | Interface | Purpose | Built-in implementations | | --- | --- | --- | | `persistence.Objects` | Stores 64 KiB immutable file blocks, bounded tail objects, checkpoint manifests, and per-session `latest` pointers. | `FileObjects` (`filesystem`), `S3Objects` (`s3` and `r2` in `internal/persistence/objects.go`), `GCSObjects` (`gcs` in `internal/persistence/gcs.go`). | | `persistence.Metadata` | Maintains an eventually consistent tenant-scoped index of sessions and their latest checkpoint IDs. | `Postgres` (`postgres_env` in `internal/persistence/metadata.go`). | ## `Objects` interface [#objects-interface] `Objects` provides `Put(ctx, key, bytes, immutable)` and `Get(ctx, key)`: - **Immutable writes:** When `immutable` is `true`, `Put` must never overwrite an existing object with different content (`FileObjects` uses hard links; `S3Objects` sends `If-None-Match: "*"`; `GCSObjects` sets `DoesNotExist: true`). If the key already exists from an earlier upload attempt, returning `nil` is safe because block and manifest keys are generation- and checkpoint-scoped. - **Error distinction:** Missing objects on `Get` must wrap `os.ErrNotExist` so `persistence.Archive` can distinguish missing checkpoints (`404 not_found`) from transient network or storage failures (`500 brain_error`). - **Scoping and bounds:** Implementations must validate keys with `validKey` (rejecting empty keys, leading slashes, backslashes, or path traversal), prepend the configured `prefix`, and cap individual object sizes at `2 MiB` (`MaxObjectBytes = 2 << 20`). ### How `Archive` publishes and restores checkpoints [#how-archive-publishes-and-restores-checkpoints] `persistence.Archive` (`internal/persistence/archive.go`) coordinates incremental uploads of `events.jsonl` and append-only `native/` files (up to 128 files per checkpoint): Complete `64 KiB` (`BlockBytes = 64 << 10`) blocks are uploaded once under `sessions//blocks//` (`immutable: true`). Subsequent checkpoints reuse the same `generation` ID and upload only newly appended full blocks plus the bounded remainder tail under `sessions//tails//` (`immutable: true`). Only after every referenced block and tail object is stored does `Archive.Publish` write `sessions//checkpoints/` (`immutable: true`). A failed upload can leave unreachable blocks, never a partially readable checkpoint. `Archive.Publish` updates the mutable pointer `sessions//latest` (`immutable: false`), updates `Metadata.Put` if `postgres_env` is configured, and saves `uploaded.json` in the local session directory. ## `Metadata` interface [#metadata-interface] `Metadata` provides `Put(ctx, session, checkpointID)`, `Get(ctx, tenant, id)`, `List(ctx, tenant, after, limit)`, and `Close()`: - `Get` and `List` are strictly scoped by `tenant`, and `List` returns sessions ordered by `id` ascending (`id > after`, `limit` `1`–`1000`). - `Put` upserts `(id, tenant, data, checkpoint)` and rejects cross-tenant collisions with `403 forbidden` (`metadata tenant mismatch`). - External metadata is an eventually consistent discovery index updated after checkpoint publication; it is never used for locking or leader election. ## Testing an adapter [#testing-an-adapter] Every storage adapter should pass the contract tests in `internal/persistence/`: - Immutable block writes (`immutable: true`) and idempotent retries when a block or manifest already exists - Interrupted manifest publication (unreachable blocks must never expose a half-written checkpoint) - Missing object detection (`errors.Is(err, os.ErrNotExist)`) and tenant/path isolation - Restoring into an empty local cache and verifying the next Pi model turn preserves conversation history # List available tools (/brain/docs/api/generated/catalog) Lists tool descriptors available to the caller's tenant. Endpoint URLs and credentials are omitted. ## GET /catalog Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/catalog": { "get": { "description": "Lists tool descriptors available to the caller's tenant. Endpoint URLs and credentials are omitted.", "operationId": "catalog", "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToolCatalog" } } }, "description": "List available tools" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "List available tools" } } }, "components": { "schemas": { "ToolCatalog": { "additionalProperties": false, "properties": { "tools": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "tools" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Close a session (/brain/docs/api/generated/close) Permanently closes a session and stops any active or queued work. Requires a management token. Event history remains readable. ## POST /sessions/{id}/close Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/close": { "post": { "description": "Permanently closes a session and stops any active or queued work. Requires a management token. Event history remains readable.", "operationId": "close", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "202": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Status" } } }, "description": "Close a session" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Close a session" } } }, "components": { "schemas": { "Status": { "additionalProperties": false, "properties": { "status": { "type": "string" } }, "required": [ "status" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Create a session (/brain/docs/api/generated/create) Creates a session for the caller's tenant with the given harness, model, instructions, reasoning effort, and tools. Requires a management token. Creating a session records its metadata locally; the harness process starts when the first message is sent. ## POST /sessions Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions": { "post": { "description": "Creates a session for the caller's tenant with the given harness, model, instructions, reasoning effort, and tools. Requires a management token. Creating a session records its metadata locally; the harness process starts when the first message is sent.", "operationId": "create", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSession" } } }, "required": true }, "responses": { "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } }, "description": "Create a session" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Create a session" } } }, "components": { "schemas": { "CreateSession": { "additionalProperties": false, "properties": { "config": { "$ref": "#/components/schemas/SessionConfig" }, "credentials": { "items": { "$ref": "#/components/schemas/CredentialInput" }, "type": "array" }, "harness": { "type": "string" } }, "required": [ "harness", "config" ], "type": "object" }, "SessionConfig": { "additionalProperties": false, "properties": { "bindings": { "items": { "$ref": "#/components/schemas/ToolBinding" }, "type": "array" }, "instructions": { "type": "string" }, "model": { "type": "string" }, "reasoning_effort": { "type": "string" }, "tools": { "items": { "type": "string" }, "type": "array" } }, "required": [ "model", "tools" ], "type": "object" }, "ToolBinding": { "additionalProperties": false, "properties": { "alias": { "type": "string" }, "executor": { "type": "string" }, "operations": { "items": { "type": "string" }, "type": "array" }, "parameters": {}, "provider": { "type": "string" }, "runtime": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "alias" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "CredentialInput": { "additionalProperties": false, "properties": { "binding": { "type": "string" }, "value": {} }, "required": [ "binding", "value" ], "type": "object" }, "Session": { "additionalProperties": false, "properties": { "checkpoint": { "$ref": "#/components/schemas/Checkpoint" }, "config": { "$ref": "#/components/schemas/SessionConfig" }, "created_at": { "format": "date-time", "type": "string" }, "credential_bindings": { "items": { "type": "string" }, "type": "array" }, "descriptors": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" }, "harness": { "type": "string" }, "id": { "type": "string" }, "status": { "type": "string" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "harness", "config", "status", "created_at", "updated_at", "descriptors" ], "type": "object" }, "Checkpoint": { "additionalProperties": false, "properties": { "id": { "type": "string" }, "offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "id", "offset", "time" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Read or stream events (/brain/docs/api/generated/events) Returns a page of retained events as JSON, or streams history and live updates when called with Accept: text/event-stream. Pass after (or Last-Event-ID) with the last retained event's cursor to resume from a known position. Returns 409 cursor_unavailable if the cursor points to an unsynced tail lost during restore. ## GET /sessions/{id}/events Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/events": { "get": { "description": "Returns a page of retained events as JSON, or streams history and live updates when called with Accept: text/event-stream. Pass after (or Last-Event-ID) with the last retained event's cursor to resume from a known position. Returns 409 cursor_unavailable if the cursor points to an unsynced tail lost during restore.", "operationId": "events", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } }, { "description": "Opaque cursor from the last retained event.", "in": "query", "name": "after", "schema": { "type": "string" } }, { "description": "Maximum number of items to return (JSON mode).", "in": "query", "name": "limit", "schema": { "default": 100, "maximum": 1000, "minimum": 1, "type": "integer" } }, { "description": "SSE reconnect cursor (used when the after query parameter is omitted).", "in": "header", "name": "Last-Event-ID", "schema": { "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EventList" } }, "text/event-stream": { "example": "id: OPAQUE_CURSOR\nevent: assistant.message\ndata: {\"id\":\"EVENT_ID\",\"type\":\"assistant.message\",\"time\":\"2026-10-15T12:00:00Z\",\"input_id\":\"INPUT_ID\",\"data\":{\"text\":\"Hello\"},\"cursor\":\"OPAQUE_CURSOR\"}\n\n", "schema": { "type": "string" } } }, "description": "Read or stream events" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Read or stream events" } } }, "components": { "schemas": { "EventList": { "additionalProperties": false, "properties": { "events": { "items": { "$ref": "#/components/schemas/Event" }, "type": "array" }, "next_cursor": { "type": "string" } }, "required": [ "events", "next_cursor" ], "type": "object" }, "Event": { "additionalProperties": false, "properties": { "cursor": { "type": "string" }, "data": {}, "id": { "type": "string" }, "input_id": { "type": "string" }, "time": { "format": "date-time", "type": "string" }, "type": { "type": "string" } }, "required": [ "id", "type", "time", "data" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Connect a runtime executor (/brain/docs/api/generated/executorCommands) Requires the executor credential. Returns non-replayable SSE commands and a ready connection identity. A dropped connection never replays calls. ## GET /executors/{id}/commands Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/executors/{id}/commands": { "get": { "description": "Requires the executor credential. Returns non-replayable SSE commands and a ready connection identity. A dropped connection never replays calls.", "operationId": "executorCommands", "parameters": [ { "description": "Opaque executor ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "200": { "content": { "text/event-stream": { "example": "data: {\"type\":\"ready\",\"connection\":\"CONNECTION_ID\"}\n\n", "schema": { "type": "string" } } }, "description": "Connect a runtime executor" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Connect a runtime executor" } } }, "components": { "schemas": { "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Connect bidirectional runtime RPC (/brain/docs/api/generated/executorRPC) WebSocket upgrade using brain-rpc and brain-auth.TOKEN subprotocols. Commands and replies use the same contract as SSE/HTTP. Browser origins are rejected. ## GET /executors/{id}/rpc Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/executors/{id}/rpc": { "get": { "description": "WebSocket upgrade using brain-rpc and brain-auth.TOKEN subprotocols. Commands and replies use the same contract as SSE/HTTP. Browser origins are rejected.", "operationId": "executorRPC", "parameters": [ { "description": "Opaque executor ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "101": { "content": {}, "description": "Connect bidirectional runtime RPC" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Connect bidirectional runtime RPC" } } }, "components": { "schemas": { "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Submit runtime progress or result (/brain/docs/api/generated/executorReply) Requires the executor credential and current connection/call identity. Stale, duplicate and unsolicited results fail. ## POST /executors/{id}/responses Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/executors/{id}/responses": { "post": { "description": "Requires the executor credential and current connection/call identity. Stale, duplicate and unsolicited results fail.", "operationId": "executorReply", "parameters": [ { "description": "Opaque executor ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Reply" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Status" } } }, "description": "Submit runtime progress or result" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Submit runtime progress or result" } } }, "components": { "schemas": { "Reply": { "additionalProperties": false, "properties": { "call_id": { "type": "string" }, "connection": { "type": "string" }, "progress": {}, "result": { "$ref": "#/components/schemas/Result" } }, "required": [ "connection", "call_id" ], "type": "object" }, "Result": { "additionalProperties": false, "properties": { "content": { "items": { "$ref": "#/components/schemas/Content" }, "type": "array" }, "is_error": { "type": "boolean" }, "structured_content": {}, "unknown_outcome": { "type": "boolean" } }, "required": [ "content" ], "type": "object" }, "Content": { "additionalProperties": false, "properties": { "data": { "type": "string" }, "mime_type": { "type": "string" }, "text": { "type": "string" }, "type": { "type": "string" } }, "required": [ "type" ], "type": "object" }, "Status": { "additionalProperties": false, "properties": { "status": { "type": "string" } }, "required": [ "status" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Flush external state (/brain/docs/api/generated/flush) Uploads local event history and the latest completed checkpoint to external storage and returns the resulting checkpoint reference. Requires a management token and configured persistence. ## POST /sessions/{id}/flush Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/flush": { "post": { "description": "Uploads local event history and the latest completed checkpoint to external storage and returns the resulting checkpoint reference. Requires a management token and configured persistence.", "operationId": "flush", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SyncStatus" } } }, "description": "Flush external state" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Flush external state" } } }, "components": { "schemas": { "SyncStatus": { "additionalProperties": false, "properties": { "checkpoint": { "type": "string" }, "error": { "type": "string" }, "history_bytes": { "type": "integer" }, "native_checkpoint": { "type": "string" }, "pending": { "type": "boolean" }, "resume_offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "history_bytes", "resume_offset", "pending" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Get a session (/brain/docs/api/generated/get) Returns metadata and current status for a session. If the session is not on local disk and external storage is configured, Brain loads the latest published checkpoint. ## GET /sessions/{id} Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}": { "get": { "description": "Returns metadata and current status for a session. If the session is not on local disk and external storage is configured, Brain loads the latest published checkpoint.", "operationId": "get", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } }, "description": "Get a session" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Get a session" } } }, "components": { "schemas": { "Session": { "additionalProperties": false, "properties": { "checkpoint": { "$ref": "#/components/schemas/Checkpoint" }, "config": { "$ref": "#/components/schemas/SessionConfig" }, "created_at": { "format": "date-time", "type": "string" }, "credential_bindings": { "items": { "type": "string" }, "type": "array" }, "descriptors": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" }, "harness": { "type": "string" }, "id": { "type": "string" }, "status": { "type": "string" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "harness", "config", "status", "created_at", "updated_at", "descriptors" ], "type": "object" }, "Checkpoint": { "additionalProperties": false, "properties": { "id": { "type": "string" }, "offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "id", "offset", "time" ], "type": "object" }, "SessionConfig": { "additionalProperties": false, "properties": { "bindings": { "items": { "$ref": "#/components/schemas/ToolBinding" }, "type": "array" }, "instructions": { "type": "string" }, "model": { "type": "string" }, "reasoning_effort": { "type": "string" }, "tools": { "items": { "type": "string" }, "type": "array" } }, "required": [ "model", "tools" ], "type": "object" }, "ToolBinding": { "additionalProperties": false, "properties": { "alias": { "type": "string" }, "executor": { "type": "string" }, "operations": { "items": { "type": "string" }, "type": "array" }, "parameters": {}, "provider": { "type": "string" }, "runtime": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "alias" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # List sessions (/brain/docs/api/generated/list) Lists sessions belonging to the caller's tenant in ascending ID order. Pass the last session ID as after to fetch the next page. ## GET /sessions Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions": { "get": { "description": "Lists sessions belonging to the caller's tenant in ascending ID order. Pass the last session ID as after to fetch the next page.", "operationId": "list", "parameters": [ { "description": "Last session ID from the previous page.", "in": "query", "name": "after", "schema": { "type": "string" } }, { "description": "Maximum number of items to return (JSON mode).", "in": "query", "name": "limit", "schema": { "default": 100, "maximum": 1000, "minimum": 1, "type": "integer" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionList" } } }, "description": "List sessions" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "List sessions" } } }, "components": { "schemas": { "SessionList": { "additionalProperties": false, "properties": { "sessions": { "items": { "$ref": "#/components/schemas/Session" }, "type": "array" } }, "required": [ "sessions" ], "type": "object" }, "Session": { "additionalProperties": false, "properties": { "checkpoint": { "$ref": "#/components/schemas/Checkpoint" }, "config": { "$ref": "#/components/schemas/SessionConfig" }, "created_at": { "format": "date-time", "type": "string" }, "credential_bindings": { "items": { "type": "string" }, "type": "array" }, "descriptors": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" }, "harness": { "type": "string" }, "id": { "type": "string" }, "status": { "type": "string" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "harness", "config", "status", "created_at", "updated_at", "descriptors" ], "type": "object" }, "Checkpoint": { "additionalProperties": false, "properties": { "id": { "type": "string" }, "offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "id", "offset", "time" ], "type": "object" }, "SessionConfig": { "additionalProperties": false, "properties": { "bindings": { "items": { "$ref": "#/components/schemas/ToolBinding" }, "type": "array" }, "instructions": { "type": "string" }, "model": { "type": "string" }, "reasoning_effort": { "type": "string" }, "tools": { "items": { "type": "string" }, "type": "array" } }, "required": [ "model", "tools" ], "type": "object" }, "ToolBinding": { "additionalProperties": false, "properties": { "alias": { "type": "string" }, "executor": { "type": "string" }, "operations": { "items": { "type": "string" }, "type": "array" }, "parameters": {}, "provider": { "type": "string" }, "runtime": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "alias" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Register runtime tools (/brain/docs/api/generated/registerExecutor) Requires management authority. Connect the returned executor before creating the session. Rebinding supplies the saved session and matching descriptors; functions remain in the client. ## POST /executors Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/executors": { "post": { "description": "Requires management authority. Connect the returned executor before creating the session. Rebinding supplies the saved session and matching descriptors; functions remain in the client.", "operationId": "registerExecutor", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecutorRegistration" } } }, "required": true }, "responses": { "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExecutorCredential" } } }, "description": "Register runtime tools" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Register runtime tools" } } }, "components": { "schemas": { "ExecutorRegistration": { "additionalProperties": false, "properties": { "binding": { "type": "string" }, "session": { "type": "string" }, "tools": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "binding", "tools" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "ExecutorCredential": { "additionalProperties": false, "properties": { "id": { "type": "string" }, "token": { "type": "string" } }, "required": [ "id", "token" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Restore a checkpoint (/brain/docs/api/generated/restore) Replaces local session state with a specific checkpoint from external storage. Requires a management token and settled local execution. ## POST /sessions/{id}/restore Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/restore": { "post": { "description": "Replaces local session state with a specific checkpoint from external storage. Requires a management token and settled local execution.", "operationId": "restore", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Restore" } } }, "required": true }, "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } }, "description": "Restore a checkpoint" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Restore a checkpoint" } } }, "components": { "schemas": { "Restore": { "additionalProperties": false, "properties": { "checkpoint": { "type": "string" } }, "required": [ "checkpoint" ], "type": "object" }, "Session": { "additionalProperties": false, "properties": { "checkpoint": { "$ref": "#/components/schemas/Checkpoint" }, "config": { "$ref": "#/components/schemas/SessionConfig" }, "created_at": { "format": "date-time", "type": "string" }, "credential_bindings": { "items": { "type": "string" }, "type": "array" }, "descriptors": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" }, "harness": { "type": "string" }, "id": { "type": "string" }, "status": { "type": "string" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "harness", "config", "status", "created_at", "updated_at", "descriptors" ], "type": "object" }, "Checkpoint": { "additionalProperties": false, "properties": { "id": { "type": "string" }, "offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "id", "offset", "time" ], "type": "object" }, "SessionConfig": { "additionalProperties": false, "properties": { "bindings": { "items": { "$ref": "#/components/schemas/ToolBinding" }, "type": "array" }, "instructions": { "type": "string" }, "model": { "type": "string" }, "reasoning_effort": { "type": "string" }, "tools": { "items": { "type": "string" }, "type": "array" } }, "required": [ "model", "tools" ], "type": "object" }, "ToolBinding": { "additionalProperties": false, "properties": { "alias": { "type": "string" }, "executor": { "type": "string" }, "operations": { "items": { "type": "string" }, "type": "array" }, "parameters": {}, "provider": { "type": "string" }, "runtime": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "alias" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Resume a session (/brain/docs/api/generated/resume) Moves an interrupted or failed session back to idle from its last completed checkpoint. Requires a management token. Send a new user.message after resuming to continue the conversation. ## POST /sessions/{id}/resume Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/resume": { "post": { "description": "Moves an interrupted or failed session back to idle from its last completed checkpoint. Requires a management token. Send a new user.message after resuming to continue the conversation.", "operationId": "resume", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } }, "description": "Resume a session" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Resume a session" } } }, "components": { "schemas": { "Session": { "additionalProperties": false, "properties": { "checkpoint": { "$ref": "#/components/schemas/Checkpoint" }, "config": { "$ref": "#/components/schemas/SessionConfig" }, "created_at": { "format": "date-time", "type": "string" }, "credential_bindings": { "items": { "type": "string" }, "type": "array" }, "descriptors": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" }, "harness": { "type": "string" }, "id": { "type": "string" }, "status": { "type": "string" }, "updated_at": { "format": "date-time", "type": "string" } }, "required": [ "id", "harness", "config", "status", "created_at", "updated_at", "descriptors" ], "type": "object" }, "Checkpoint": { "additionalProperties": false, "properties": { "id": { "type": "string" }, "offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "id", "offset", "time" ], "type": "object" }, "SessionConfig": { "additionalProperties": false, "properties": { "bindings": { "items": { "$ref": "#/components/schemas/ToolBinding" }, "type": "array" }, "instructions": { "type": "string" }, "model": { "type": "string" }, "reasoning_effort": { "type": "string" }, "tools": { "items": { "type": "string" }, "type": "array" } }, "required": [ "model", "tools" ], "type": "object" }, "ToolBinding": { "additionalProperties": false, "properties": { "alias": { "type": "string" }, "executor": { "type": "string" }, "operations": { "items": { "type": "string" }, "type": "array" }, "parameters": {}, "provider": { "type": "string" }, "runtime": { "items": { "$ref": "#/components/schemas/Tool" }, "type": "array" } }, "required": [ "alias" ], "type": "object" }, "Tool": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "input_schema": {}, "name": { "type": "string" }, "output_schema": {} }, "required": [ "name", "description", "input_schema" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Send input (/brain/docs/api/generated/send) Queues a user.message (with text and an optional idempotency key) or requests an interruption with user.interrupt. Returns 202 Accepted once the input is recorded locally. ## POST /sessions/{id}/events Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/events": { "post": { "description": "Queues a user.message (with text and an optional idempotency key) or requests an interruption with user.interrupt. Returns 202 Accepted once the input is recorded locally.", "operationId": "send", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Input" } } }, "required": true }, "responses": { "202": { "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/Event" }, { "$ref": "#/components/schemas/Status" } ] } } }, "description": "Send input" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Send input" } } }, "components": { "schemas": { "Input": { "additionalProperties": false, "properties": { "delivery": { "type": "string" }, "key": { "type": "string" }, "text": { "type": "string" }, "type": { "type": "string" } }, "required": [ "type" ], "type": "object" }, "Event": { "additionalProperties": false, "properties": { "cursor": { "type": "string" }, "data": {}, "id": { "type": "string" }, "input_id": { "type": "string" }, "time": { "format": "date-time", "type": "string" }, "type": { "type": "string" } }, "required": [ "id", "type", "time", "data" ], "type": "object" }, "Status": { "additionalProperties": false, "properties": { "status": { "type": "string" } }, "required": [ "status" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Read synchronization status (/brain/docs/api/generated/sync) Returns the latest published checkpoint, pending upload state, and any sync error for the session. Requires configured persistence. ## GET /sessions/{id}/sync Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/sessions/{id}/sync": { "get": { "description": "Returns the latest published checkpoint, pending upload state, and any sync error for the session. Requires configured persistence.", "operationId": "sync", "parameters": [ { "description": "Opaque session ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SyncStatus" } } }, "description": "Read synchronization status" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Read synchronization status" } } }, "components": { "schemas": { "SyncStatus": { "additionalProperties": false, "properties": { "checkpoint": { "type": "string" }, "error": { "type": "string" }, "history_bytes": { "type": "integer" }, "native_checkpoint": { "type": "string" }, "pending": { "type": "boolean" }, "resume_offset": { "type": "integer" }, "time": { "format": "date-time", "type": "string" } }, "required": [ "history_bytes", "resume_offset", "pending" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # Close a runtime executor (/brain/docs/api/generated/unregisterExecutor) Requires its executor credential. Pending dispatched calls receive unknown outcomes; future calls are unavailable. ## POST /executors/{id}/close Bearer authentication is required. ```json { "openapi": "3.1.0", "info": { "description": "HTTP API for the Brain agent session server.", "title": "Brain", "version": "current" }, "servers": [ { "description": "Local development", "url": "http://127.0.0.1:8080" } ], "paths": { "/executors/{id}/close": { "post": { "description": "Requires its executor credential. Pending dispatched calls receive unknown outcomes; future calls are unavailable.", "operationId": "unregisterExecutor", "parameters": [ { "description": "Opaque executor ID.", "in": "path", "name": "id", "required": true, "schema": { "pattern": "^[a-fA-F0-9]{32}$", "type": "string" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Status" } } }, "description": "Close a runtime executor" }, "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Error. 400 invalid_request; 401 missing/invalid Bearer token; 403 forbidden; 404 not_found; 409 conflict or cursor_unavailable; 413 body limit; 422 unsupported; 429 overloaded (Retry-After: 1); 500 brain_error. X-Request-ID is present on every response." } }, "security": [ { "bearer": [] } ], "summary": "Close a runtime executor" } } }, "components": { "schemas": { "Status": { "additionalProperties": false, "properties": { "status": { "type": "string" } }, "required": [ "status" ], "type": "object" }, "ErrorResponse": { "additionalProperties": false, "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" }, "request_id": { "type": "string" } }, "required": [ "error", "request_id" ], "type": "object" }, "ErrorDetail": { "additionalProperties": false, "properties": { "code": { "type": "string" }, "message": { "type": "string" } }, "required": [ "code", "message" ], "type": "object" } }, "securitySchemes": { "bearer": { "scheme": "bearer", "type": "http" } } } } ``` [Complete OpenAPI document](/brain/openapi.json) # HTTP API (/brain/docs/api) Brain exposes a small JSON and server-sent events (SSE) HTTP API for creating sessions, sending messages, streaming events, and managing checkpoints. Every endpoint page is generated directly from Brain's Go route declarations and wire types (`internal/api/contract.go` and `internal/core/types.go`), and you can download the full [OpenAPI 3.1 contract](/brain/openapi.json). ## Base URL and authentication [#base-url-and-authentication] By default, a local Brain server listens on `http://127.0.0.1:8080`. - **Session and catalog endpoints:** Send `Authorization: Bearer ` on every request. Each token is bound to one tenant and optional session or permission scope (`manage` or `read_only`). Missing or invalid tokens return `401 forbidden` with `WWW-Authenticate: Bearer`. See [Authentication & tenants](/brain/docs/guides/authentication). - **Health endpoints:** `GET /health/live` and `GET /health/ready` are public and require no authentication. See [Operations & observability](/brain/docs/guides/operations). ```sh curl -sS http://127.0.0.1:8080/sessions \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "tools": [] } }' ``` ## Request and response rules [#request-and-response-rules] - **Content types:** Send `Content-Type: application/json` on `POST` requests with a body. Standard responses return `Content-Type: application/json`. Streaming responses from `GET /sessions/{id}/events` with `Accept: text/event-stream` return `Content-Type: text/event-stream`. - **Strict JSON decoding:** Request bodies must contain a single JSON object. Unknown JSON fields and trailing data after the root object are rejected with `400 invalid_request`. - **Body size limits:** Request bodies are capped at `limits.input_bytes + 16 KiB` (default `65536 + 16384` bytes). Larger payloads fail with `413 invalid_request`. - **Request IDs and tracing:** Every HTTP response includes an `X-Request-ID` header (and `X-Content-Type-Options: nosniff`). Error responses also echo `request_id` in the JSON body. If you send W3C `traceparent` headers, Brain propagates the trace context through session execution and outbound tool calls. ## Session endpoints [#session-endpoints] | Operation | Method & path | Auth scope | Status | Description | | --- | --- | --- | --- | --- | | [Create a session](/brain/docs/api/generated/create) | `POST /sessions` | `manage: true` (tenant-wide) | `201` | Creates a session with the specified harness, model, instructions, reasoning effort, and tool list. | | [List sessions](/brain/docs/api/generated/list) | `GET /sessions` | Any valid token | `200` | Lists sessions belonging to the caller's tenant in ascending ID order. | | [Get a session](/brain/docs/api/generated/get) | `GET /sessions/{id}` | Any valid token | `200` | Returns session metadata and current status (`idle`, `running`, `interrupted`, `failed`, or `closed`). Loads from external storage on a local cache miss. | | [Send input](/brain/docs/api/generated/send) | `POST /sessions/{id}/events` | Write token (`read_only: false`) | `202` | Queues a `user.message` (returning the recorded `Event`) or requests an interruption with `user.interrupt` (returning `{"status": "interrupt_requested"}`). | | [Read or stream events](/brain/docs/api/generated/events) | `GET /sessions/{id}/events` | Any valid token | `200` | Returns a JSON page of retained events, or streams retained history and live events over SSE when called with `Accept: text/event-stream`. | | [Resume a session](/brain/docs/api/generated/resume) | `POST /sessions/{id}/resume` | `manage: true` | `200` | Moves an `interrupted` or `failed` session back to `idle` from its last completed checkpoint. | | [Close a session](/brain/docs/api/generated/close) | `POST /sessions/{id}/close` | `manage: true` | `202` | Permanently closes a session and stops active or queued work. Event history remains readable. | ## Persistence and tool endpoints [#persistence-and-tool-endpoints] | Operation | Method & path | Auth scope | Status | Description | | --- | --- | --- | --- | --- | | [Flush external state](/brain/docs/api/generated/flush) | `POST /sessions/{id}/flush` | `manage: true` | `200` | Forces an immediate upload of local event history and the latest completed checkpoint to external storage and returns `SyncStatus`. | | [Read synchronization status](/brain/docs/api/generated/sync) | `GET /sessions/{id}/sync` | Any valid token | `200` | Returns the latest published checkpoint reference, byte offsets, pending upload state, and any sync error. | | [Restore a checkpoint](/brain/docs/api/generated/restore) | `POST /sessions/{id}/restore` | `manage: true` | `200` | Replaces settled local session state with a specific checkpoint from external storage. | | [List available tools](/brain/docs/api/generated/catalog) | `GET /catalog` | Any valid token | `200` | Lists tool descriptors (`name`, `description`, `input_schema`, `output_schema`) enabled for the caller's tenant. | ## Pagination [#pagination] `GET /sessions` and JSON requests to `GET /sessions/{id}/events` accept two query parameters: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `limit` | integer | `100` | Maximum items to return (`1`–`1000`). Values outside this range return `400 invalid_request`. | | `after` | string | `""` | Cursor for the next page. On `GET /sessions`, pass the last session's `id`. On `GET /sessions/{id}/events`, pass `next_cursor` (or the last retained event's `cursor`). | ## Streaming and errors [#streaming-and-errors] - **Server-sent events:** Send `Accept: text/event-stream` to `GET /sessions/{id}/events` to replay retained history and stay connected for live events (`assistant.delta`, `tool.progress`, and subsequent turns). To reconnect after a network drop, pass `?after=CURSOR` or `Last-Event-ID: CURSOR` (`after` takes precedence when both are present). See [Events & streaming](/brain/docs/reference/events) for the event envelope and SSE wire format. - **Errors:** Failed HTTP requests return a structured JSON body (`{"error": {"code": "...", "message": "..."}, "request_id": "..."}`). See [Errors](/brain/docs/reference/errors) for status codes, retry headers, tool errors, and session error events. # Architecture (/brain/docs/architecture) Brain sits between your application and the systems that do the work. Your application talks to Brain, Brain runs a harness for each active session, and the harness's tool calls go to implementations you authorize. This page follows one message through Brain, then explains how Brain stores and recovers sessions and which component owns what. ## How a message flows [#how-a-message-flows] ### Your backend sends a message [#your-backend-sends-a-message] Your backend sends a message to a session with an API token. Brain checks the token, records the message in the session's history, and queues it. Each session runs one turn at a time, and further messages wait in order. ### The harness runs the turn [#the-harness-runs-the-turn] Brain starts the session's harness, or reuses the one already running. The harness, such as Pi, builds prompts, calls the model provider, manages context, and decides when the turn is finished. It runs with only the provider credentials it needs. ### Brain calls your tools [#brain-calls-your-tools] When the model calls a tool, the harness hands the call to Brain. Brain checks that the tool is enabled for the session, records the call, validates the arguments, and dispatches it to your implementation together with the session's identity. Brain records the result and returns it to the harness. ### Brain streams events [#brain-streams-events] As the turn runs, Brain records each event in the session's history and streams it to connected clients, along with live progress such as partial messages and tool progress. ### Brain saves a checkpoint [#brain-saves-a-checkpoint] When the turn completes, Brain records a checkpoint: the position in the session's history and the harness's own state. If you configure external storage, Brain uploads the checkpoint in the background. ## How Brain stores and recovers sessions [#how-brain-stores-and-recovers-sessions] Each server keeps its sessions on local disk, so a running turn never waits on remote storage. External storage is optional. When you configure it, Brain copies each session's history and checkpoints there in the background, and any server with access to that storage can load the session. When a request names a session that the server doesn't have, Brain loads the session's latest checkpoint from external storage. The loaded session waits for you to resume it before it runs again. You can also force an upload, check upload progress, or restore a specific checkpoint. If a machine fails before a background upload finishes, only the events since the last completed upload are lost. Restoring from external storage always lands on the latest complete checkpoint. After a restart, Brain loads each session from local disk the next time a request uses it. A session whose turn was running when Brain stopped comes back interrupted; resume it to continue. See [Persistence & recovery](/brain/docs/guides/persistence) to configure storage and hand a session to another server. ## Who owns what [#who-owns-what] | Component | Owns | | --- | --- | | **Brain** | The API, authentication and tenant isolation, message queueing, event streaming, tool calls, session storage, checkpoint uploads, and telemetry. | | **Harness** | Model requests, prompts, context management, and its own session state. | | **Tool service** | Tool execution, sandboxes, external jobs, human approvals, retries, and resource cleanup. | | **Host platform** | End-user authentication, routing each session to a single server, scaling, and failover. | ## How errors are handled [#how-errors-are-handled] Brain separates three kinds of errors, so a failure in one layer doesn't look like a failure in another: - **Request errors:** Brain rejects an invalid request and leaves the session unchanged. - **Tool failures:** Denials, errors, timeouts, and dropped connections return to the model as tool results, so it can react. Brain sends each tool call at most once; your tool service owns retries and cleanup. - **Execution failures:** A model provider error, a harness crash, or a failed disk write stops the turn and reports a session error. The session stays stopped until you resume it. ## Design principles [#design-principles] - **Keep the core small:** Harnesses own inference and context; applications install tools and own approvals. - **Fail explicitly:** Tool errors become tool results, and Brain errors stop the affected turn. Missing or damaged state returns an error instead of silently starting a fresh session. - **Bound every queue:** Message queues, event streams, and storage retries all have limits. At capacity, Brain turns away new work instead of slowing down running turns, and health checks and stream keepalives never call the model. ## Storage details [#storage-details] You don't need these details to use Brain. They show how Brain keeps the guarantees above. - **Local state:** SQLite stores each session's record, including its owner and configuration. Append-only files store the session's event history and the harness's native state. - **External checkpoints:** A background worker uploads the event history and the harness's native files as immutable 64 KiB blocks to a local directory, Amazon S3, Cloudflare R2, or Google Cloud Storage. A checkpoint becomes visible only after all of its blocks are in place, and later checkpoints reuse blocks that are already uploaded. - **Session index:** An optional, eventually consistent PostgreSQL index lets any server find sessions stored by the others. [Persistence & recovery](/brain/docs/guides/persistence) covers storage configuration, flushes, and handoff. [Harness adapters](/brain/docs/adapters/harness) explains how Brain runs a harness, and the ADR below records the design decisions. ## Design references [#design-references] - [ADR 0001: Local-first ephemeral session server](https://github.com/aexhq/brain/blob/main/docs/adr/0001-local-first-session-server.md) - [Roadmap](/brain/docs/roadmap) - [Implementation plan](https://github.com/aexhq/brain/blob/main/docs/implementation-plan.md) ## Next steps [#next-steps] } title="Session lifecycle" href="/docs/guides/sessions">Send messages, stream events, and handle interruptions. } title="Persistence & recovery" href="/docs/guides/persistence">Configure external checkpoints and move sessions between servers. } title="Adapters" href="/docs/adapters">Add a harness or a storage backend through small Go interfaces. # Development & verification (/brain/docs/development) This guide is for contributors. It covers how to set up a local development environment for Brain, run the full verification suite, regenerate the OpenAPI document and SDK wire types, preview the documentation site, and how CI tests and publishes each release. ## Prerequisites [#prerequisites] - **Go `1.26+`** (`go.mod` declares `go 1.26.0`) - **Node.js `24+`** and `npm` - **Python `3.11+`**, available as `python`, to build and test the Python SDK packages ## 1. Install workspace dependencies [#1-install-workspace-dependencies] Install the locked Node dependencies for the Pi runner, TypeScript SDK, sandbox service, and documentation site: ```sh node scripts/setup.mjs ``` The script runs `npm ci --ignore-scripts` in `runners/pi`, `sdks/typescript`, `services/sandbox`, and `website`. ## 2. Run all checks [#2-run-all-checks] Run `node scripts/check.mjs` before you commit: ```sh node scripts/check.mjs ``` It runs these steps in order and stops at the first failure: 1. `npm --prefix runners/pi run build`: compiles the TypeScript Pi runner (`runners/pi/dist/main.js`). 2. `npm --prefix sdks/typescript run build`: compiles the TypeScript SDK (`sdks/typescript/dist/`). 3. `go run ./cmd/generate -check`: verifies that `api/openapi.json`, `runners/pi/src/generated.ts`, `sdks/typescript/src/generated.ts`, and `sdks/python/brain/types.py` match the Go source. 4. `node scripts/package-sdks.mjs`: packs the npm package, builds the Python wheel and source distribution, and installs all three in isolated environments. It then runs the TypeScript client tests and a strict type check against the installed npm package, and the Python client tests against both Python installs. 5. `go test -count=1 ./...`: runs all Go unit and integration tests across `cmd/` and `internal/`. 6. `go vet ./...`: runs Go static analysis. 7. `npm --prefix runners/pi test`: runs the Pi runner unit and integration tests against a local recording provider. 8. `npm --prefix sdks/typescript test`: runs the TypeScript SDK test suite. 9. `npm --prefix services/sandbox test`: runs the sandbox tool service test suite. 10. The Python unit suite runs against the installed wheel from `bin/sdk-consumer/python` using `bin/sdk-python`. 11. `node --test tests/journeys/journeys.test.mjs`: runs the [end-to-end journeys](https://github.com/aexhq/brain/blob/main/tests/journeys/README.md) through the installed clients. 12. `node --test tests/platforms.test.mjs`: validates the platform deployment examples under `examples/platforms/`. 13. `node --test tests/registry-artifacts.test.mjs`: tests the SDK release workflow's admission checks and registry publication, including partial uploads, registry outages, and version reuse. 14. `npm --prefix website run build`: generates the derived docs pages and builds the static Next.js / Fumadocs export. 15. `npm --prefix website test`: runs `website/scripts/site.test.mjs`. ## 3. Regenerate contracts and docs [#3-regenerate-contracts-and-docs] When you edit Go route declarations, wire structs, or shared Markdown sources, regenerate the derived artifacts: ```sh # Regenerate api/openapi.json, runners/pi/src/generated.ts, # sdks/typescript/src/generated.ts, and sdks/python/brain/types.py go run ./cmd/generate # Regenerate website/content/docs/** generated pages and website/public/openapi.json npm --prefix website run generate ``` ## 4. Preview the documentation site [#4-preview-the-documentation-site] Regenerate the derived pages and start the local Next.js development server: ```sh npm --prefix website run dev ``` Open `http://127.0.0.1:3000/docs`. To test the static production export locally, build it, run the site tests, and serve `website/out` on port 3000: ```sh npm --prefix website run build npm --prefix website test npm --prefix website start ``` ## Where to edit what [#where-to-edit-what] Don't hand-edit generated files (`api/openapi.json`, `runners/pi/src/generated.ts`, `sdks/typescript/src/generated.ts`, `sdks/python/brain/types.py`, `website/public/openapi.json`, or generated MDX pages). Edit the source of truth and run the corresponding generator: | Area | Source of truth | Regenerate with | | --- | --- | --- | | HTTP routes, summaries, and wire types | `internal/api/contract.go` & `internal/core/types.go` | `go run ./cmd/generate` then `npm --prefix website run generate` | | Harness & storage Go interfaces | `internal/harness/harness.go`, `internal/harness/process.go`, `internal/persistence/*.go` | `npm --prefix website run generate` | | Model catalog | Pinned `@earendil-works/pi-coding-agent` in `runners/pi/package.json` (`runners/pi/scripts/model-catalog.mjs`) | `npm --prefix website run generate` | | Persistence guide | `docs/running-persistence.md` | `npm --prefix website run generate` | | Local tools walkthrough | `examples/tools/README.md` | `npm --prefix website run generate` | | Sandbox service guide | `services/sandbox/README.md` | `npm --prefix website run generate` | | Resource service guide | `services/resources/README.md` | `npm --prefix website run generate` | | Platform examples guide | `examples/platforms/README.md` | `npm --prefix website run generate` | | Roadmap | `docs/roadmap.md` | `npm --prefix website run generate` | | Docs pages & guides | `website/content/docs/**/*.mdx` | `npm --prefix website run build` | Follow the [writing guide](https://github.com/aexhq/brain/blob/main/docs/writing.md) when you change the README, a docs page, or a shared Markdown source. ## Design documents [#design-documents] The [roadmap](/brain/docs/roadmap) lists release stages, planned work, and integration status, and links every design document. Start with the [architecture ADR](https://github.com/aexhq/brain/blob/main/docs/adr/0001-local-first-session-server.md) for ownership boundaries and design decisions, and the [implementation plan](https://github.com/aexhq/brain/blob/main/docs/implementation-plan.md) for how the work is split across runtime, persistence, harnesses, tools, API, and operations. ## CI and releases [#ci-and-releases] The [CI workflow](https://github.com/aexhq/brain/blob/main/.github/workflows/ci.yml) runs on every push and pull request, and weekly: - **Checks:** `node scripts/check.mjs` on Linux and Windows. On Linux, the job also builds the Brain and sandbox tool images and smoke-tests both containers. - **Race detection:** `go test -race ./...` on Linux. - **PostgreSQL:** the session index tests against a real PostgreSQL server. - **Docs:** builds the site for its GitHub Pages path and runs the site tests. - **CodeQL:** code scanning for Go, JavaScript/TypeScript, Python, and GitHub Actions. Nothing publishes unless every job passes. After that, each push to `main`: 1. Publishes the tested images to `ghcr.io/aexhq/brain` and `ghcr.io/aexhq/brain-sandbox-tools`, tagged `sha-` and `main`, with build provenance attestations. 2. Deploys the tested docs site to GitHub Pages, then smoke-tests the published site. 3. Starts the SDK release, which publishes the SDK archives tested on Linux to npm and PyPI through registry trusted publishing, without rebuilding them, and attaches the same archives to a GitHub release. SDK 2.0.0 is the client for the current server and replaces the predecessor's 0.x API. The server's routes and local state have no versioning or compatibility layer, so pin production images by digest and use the SDK version that matches your server commit. [SDK delivery](https://github.com/aexhq/brain/blob/main/docs/sdk-releases.md) covers versioning, the release workflow, and registry setup. # Authentication & tenants (/brain/docs/guides/authentication) Brain authenticates every session and catalog request with a bearer token. Each token in the host config maps to a grant: the tenant the token acts for and the operations it may perform. Your backend authenticates end users, then calls Brain with the token for the user's tenant. Health endpoints are public. ## Send a token [#send-a-token] Send the token in the `Authorization` header of every request to `/sessions` and `/catalog`: ```sh curl http://127.0.0.1:8080/sessions \ -H "Authorization: Bearer $BRAIN_API_KEY" ``` The SDKs take the token as their second argument: `new Brain(url, token)` in TypeScript and `Brain(url, token)` in Python. | Status | Cause | | --- | --- | | `401` | The header is missing, doesn't use the `Bearer` scheme, is longer than 1024 characters, or names an unknown token. The error code is `forbidden`, and the response includes `WWW-Authenticate: Bearer`. | | `403` | The token is `read_only` and the request isn't a `GET`, or the token's grant doesn't allow the operation. | | `404` | The session doesn't exist or belongs to another tenant. | ## Configure tokens [#configure-tokens] Declare tokens in the `tokens` list of the host config. Each entry names the environment variable that holds the token value and the grant that goes with it: ```json { "tokens": [ { "token_env": "BRAIN_API_KEY", "tenant": "local", "manage": true }, { "token_env": "BRAIN_READER_KEY", "tenant": "local", "read_only": true } ] } ``` | Field | Required | Description | | --- | --- | --- | | `token_env` | Yes | The environment variable that holds the token. The value must be at least 24 bytes. | | `tenant` | Yes | The tenant the token acts for, up to 128 characters. | | `manage` | No | Allows creating, resuming, closing, flushing, and restoring sessions. | | `read_only` | No | Allows `GET` requests only. | | `session` | No | Limits the token to one session ID. A session-scoped token sees only its session and can't create sessions. | Brain reads token values once, at startup, and fails to start if a variable is missing, a value is shorter than 24 bytes, or two entries share a value. It keeps only SHA-256 hashes of the values in memory. To rotate a token, change the environment variable and restart Brain. Generate each token from a cryptographically secure random source, for example `openssl rand -hex 32`. Keep tokens in a secret store, not in the host config file or source control. ## Choose what each token can do [#choose-what-each-token-can-do] | Operation | Token requirement | | --- | --- | | List and read sessions, events, sync status, and the tool catalog | Any token for the tenant | | Send a message or interrupt a turn | Not `read_only` | | Create a session | `manage`, and no `session` limit | | Resume or close a session | `manage`, and not `read_only` | | Flush a session to external storage or restore a checkpoint | `manage`, and not `read_only` | A typical setup gives your backend one `manage` token per tenant. Give components that only display sessions, such as a dashboard backend, a `read_only` token. ## Keep tenants separate [#keep-tenants-separate] - **Sessions:** A request for another tenant's session returns `404`, the same response as a session that doesn't exist. Lists include only the token's tenant. - **Tools:** Each tool binding lists the tenants allowed to use it. `GET /catalog` returns only those tools, and Brain refuses to enable or call a tool for any other tenant. - **Telemetry:** Metrics carry no tenant or session labels. ## Verify calls to your tools [#verify-calls-to-your-tools] When Brain calls a tool, it identifies itself and the session the call belongs to: - **Credential:** If the tool binding sets `token_env`, Brain sends that variable's value as a bearer token. Your tool service should reject requests without it. - **Identity:** The request body carries the authenticated `tenant_id`, `session_id`, and `call_id` at the top level, next to the model's `arguments`. Authorize with these fields, never with values the model put in `arguments`. - **Session access:** If the binding sets `inspect_session: true`, Brain adds an `inspection_token` and `brain_url` (the host config's `public_url`). The token is read-only, limited to that session, and revoked as soon as the tool call returns. MCP tools don't receive one. ```json { "tenant_id": "local", "session_id": "7d1c9e04b3a24f6e8c5a0b2d9e3f4a61", "call_id": "c41f8a2e6b0d4e9f8a1c3b5d7e9f0a2b", "inspection_token": "TOKEN_FOR_THIS_CALL", "brain_url": "http://127.0.0.1:8080", "arguments": { "question": "Approve the deployment?" } } ``` See [Distributed tools](/brain/docs/tools) for the full request and response format. ## Secure the network [#secure-the-network] - **Use TLS:** Brain serves plain HTTP. Outside your own machine, put it behind a load balancer or reverse proxy that terminates TLS. - **Call Brain from your backend:** Brain authenticates with API tokens. Keep tokens on your servers and in CLIs, and give browsers access through your own backend. - **Limit exposure:** The default listen address, `127.0.0.1:8080`, accepts only local connections. Listen on `0.0.0.0` only inside a container or a private network. ## Next steps [#next-steps] } title="Session lifecycle" href="/docs/guides/sessions">Create, stream, interrupt, and resume sessions. } title="Distributed tools" href="/docs/tools">Configure tool bindings and their tenant allowlists. } title="Host configuration" href="/docs/reference/configuration">Every host config field and default. # Operations & observability (/brain/docs/guides/operations) This guide covers what you need to run Brain as a service: health checks, capacity limits, logs, OpenTelemetry traces and metrics, the container image, and request routing when you run more than one instance. ## Check health [#check-health] Health endpoints need no token and never call a model. | Endpoint | Response | Use it for | | --- | --- | --- | | `GET /health/live` | `200` with `{"status":"ok"}` while the HTTP server is running. | Liveness probes. | | `GET /health/ready` | `200` with `{"status":"ready"}` when Brain can accept new work, otherwise `503` with `{"status":"not_ready"}`. | Readiness probes and load balancer health checks. | `/health/ready` returns `503` while Brain is shutting down. With external persistence configured, it also returns `503` until Brain has checked which local sessions still need uploading after startup, and whenever the upload backlog is full. ## Set capacity limits [#set-capacity-limits] Every queue and buffer in Brain has a limit. Set them in the `limits` object of the [host config](/brain/docs/reference/configuration): | Limit | Default | What it bounds | When the limit is reached | | --- | --- | --- | --- | | `active` | 16 | Turns that run at the same time, and harness processes kept running between turns. | New turns wait for a slot. Brain stops an idle harness process to start a new one. | | `loaded` | 1024 | Sessions held in memory. Must be at least `active`. | Brain unloads an idle session. If none is idle, the request gets `429`. | | `queue` | 16 | Messages waiting in one session, from 1 to 1024. | Sending returns `429`. | | `input_bytes` | 64 KiB | Message text and session instructions, up to 1 MiB. Request bodies may be 16 KiB larger. | Oversized text returns `400`; an oversized body returns `413`. | | `session_bytes` | 1 GiB | One session's event log, at least 16 MiB. | Sending returns `429` near the limit. | | `subscribers` | 128 | Open SSE streams on the server. | A new stream returns `429`. | | `subscriber_events` | 128 | Events buffered for one SSE stream. | Brain disconnects the slow client. | | `subscriber_bytes` | 2 MiB | Bytes buffered for one SSE stream, at least 4096. | Brain disconnects the slow client. | When Brain is out of capacity, it returns `429` with code `overloaded` and `Retry-After: 1` instead of slowing down turns that are already running. Retry with backoff. Brain also returns `429` while it shuts down and, with external persistence, while the upload backlog is full. ## Read logs [#read-logs] Brain writes structured JSON logs, one object per line, to stderr. Logs don't include request bodies or credentials. Every HTTP response has an `X-Request-ID` header, and every error response body includes the same value as `request_id`: ```json { "error": { "code": "not_found", "message": "session not found" }, "request_id": "5f0c1e7a9b2d4c6e8f1a3b5c7d9e0f2a" } ``` Brain logs each `500` response with its `request_id`, and each stopped turn with its session ID and error. Quote the request ID when you report a problem. ## Export traces and metrics [#export-traces-and-metrics] Set `OTEL_EXPORTER_OTLP_ENDPOINT` to export OpenTelemetry traces and metrics over OTLP/HTTP. Brain reports as service `brain` and exports metrics every 15 seconds. Without the variable, Brain exports nothing. ```sh export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" ``` | Metric | Measures | | --- | --- | | `brain.http.requests`, `brain.http.duration` | Requests and their latency, by route and method. SSE streams are counted but not timed. | | `brain.http.rejected` | Error responses, by error code. | | `brain.executions.active` | Number of running turns. | | `brain.input.queue_delay` | Time a message waited before its turn started. | | `brain.assistant.first_output` | Time from accepting a message to the first assistant output for it. | | `brain.runner.startup` | Time to start a harness process, by harness. | | `brain.tool.calls`, `brain.tool.duration` | Tool calls and their latency, by tool and outcome (`success` or `tool_error`). | | `brain.transcript.bytes`, `brain.native.appended_bytes` | Growth of event logs and native harness state. | | `brain.sync.pending`, `brain.sync.duration`, `brain.sync.failures` | External storage upload backlog, upload time, and failures. | Metrics carry no tenant or session labels, so their cardinality stays bounded as sessions grow. Brain records a `session.execute` span for each turn, with the session ID and harness name, and a `tool.call` span for each tool call, with the tool name. Brain propagates W3C trace context: if the request that sends a message has a `traceparent` header, the turn joins that trace, and Brain forwards `traceparent` to HTTP tools so their spans join it too. ## Run Brain in a container [#run-brain-in-a-container] The repository's `Dockerfile` builds an image with the Brain server at `/app/brain`, the Pi runner at `/app/runners/pi`, and the resource tool service at `/app/resource-tools`. The image runs as the unprivileged `node` user (uid 1000), exposes port 8080, and starts Brain with `-config /config/brain.json`. To run the image: 1. **Mount your host config** at `/config/brain.json`. 2. **Listen on all interfaces:** Set `"listen": "0.0.0.0:8080"` in the config. 3. **Store state on a volume:** Set `"data_dir": "/data"` and mount a volume at `/data`. 4. **Pass secrets as environment variables:** For example `BRAIN_API_KEY` and your provider keys. ```sh docker build --tag brain:local-dev . docker run --rm -p 127.0.0.1:8080:8080 \ --mount type=bind,source="$PWD/brain.json",target=/config/brain.json,readonly \ --mount type=volume,source=brain-data,target=/data \ -e BRAIN_API_KEY -e OPENAI_API_KEY \ brain:local-dev ``` To check a locally built image, run `node scripts/container-smoke.mjs`. It starts `brain:local-dev` with a recording model provider, so it needs no model credentials. CI publishes tested images for every commit to `main` as `ghcr.io/aexhq/brain` and `ghcr.io/aexhq/brain-sandbox-tools`, tagged `sha-` and `main`, with provenance attestations. Pin production images by digest, and use the SDK version that matches your server commit. ## Route sessions across instances [#route-sessions-across-instances] Each session must have exactly one owning Brain instance at a time. Your routing layer sends every request for a session to its owner and handles failover. On `SIGINT` or `SIGTERM`, Brain stops accepting new work, interrupts running turns, ends open event streams, and exits within about 20 seconds. Sessions whose turns were interrupted need a resume before they accept new messages. To move sessions to another instance, upload their checkpoints first; see [Persistence & recovery](/brain/docs/guides/persistence) for the handoff steps. Load-test with your own workload before you size a fleet. ## Next steps [#next-steps] } title="Host configuration" href="/docs/reference/configuration">Every config field, default, and range. } title="Persistence & recovery" href="/docs/guides/persistence">Configure external storage and hand sessions between instances. } title="Authentication & tenants" href="/docs/guides/authentication">Tokens, grants, and network security. # Persistence & recovery (/brain/docs/guides/persistence) External persistence is an optional background archive that uploads local session checkpoints and event logs to a secondary filesystem directory, AWS S3, Cloudflare R2, or Google Cloud Storage (`persistence` in `brain.json`). Brain always keeps active session state on local disk first (`metadata.sqlite` for session records, plus append-only `events.jsonl` and `native/` files in `data_dir`, `.brain/` by default) so turns never block on remote storage. > **Note:** Because uploads run asynchronously in the background, an ungraceful machine crash can lose events written after the last uploaded checkpoint. ## Before you begin [#before-you-begin] - A working Brain configuration file (`brain.json`). - Write access to a local archive directory or a bucket in AWS S3, Cloudflare R2, or Google Cloud Storage. - Optional: a PostgreSQL connection string (`postgres_env`) if you need cross-instance cold-session discovery (`GET /sessions`). ## Configure a storage provider [#configure-a-storage-provider] Add a `persistence` block to your host configuration file. Using the `filesystem` provider is the fastest way to test checkpointing and recovery on a single machine: ```json { "provider": "filesystem", "directory": ".brain-archive", "interval_seconds": 5, "queue": 128 } ``` ### Object storage providers [#object-storage-providers] | Provider | Required `persistence` fields | Optional `persistence` fields | Authentication & notes | | --- | --- | --- | --- | | `filesystem` | `provider`, `directory` | `prefix`, `postgres_env`, `interval_seconds` (`5`), `queue` (`128`) | Writes immutable hard-linked blocks and manifests to a local or mounted directory. | | `s3` | `provider`, `bucket` | `region`, `endpoint`, `prefix`, `access_key_env`, `secret_key_env`, `postgres_env`, `interval_seconds`, `queue` | Uses the standard AWS SDK credential chain unless both `access_key_env` and `secret_key_env` are set. | | `r2` | `provider`, `bucket`, `endpoint`, `access_key_env`, `secret_key_env` | `region` (defaults to `"auto"`), `prefix`, `postgres_env`, `interval_seconds`, `queue` | Set `endpoint` to `https://ACCOUNT_ID.r2.cloudflarestorage.com`. | | `gcs` | `provider`, `bucket` | `prefix`, `postgres_env`, `interval_seconds`, `queue` | Uses Google Application Default Credentials (workload identity on Google Cloud or `GOOGLE_APPLICATION_CREDENTIALS` locally). Rejects `region`, `endpoint`, `access_key_env`, and `secret_key_env`. | - **Least-privilege permissions:** The Brain server only needs object read/write (`GET` and `PUT`, or object read/create/update on GCS) under its configured `prefix`; no bucket administration permissions are required. - **Bucket isolation:** Keep internal checkpoint storage separate from any S3/R2 buckets exposed to agents as tools. - **No credential archiving:** Brain never archives provider API keys or host credential files. When a session is restored on a new server, model and tool credentials come from the new host's configuration. > **Note:** Before you rely on a store, verify credentials, IAM permissions, and bucket access in your own account, and run a restore drill. ### Optional PostgreSQL discovery index (`postgres_env`) [#optional-postgresql-discovery-index-postgres_env] Set `postgres_env` to the name of an environment variable holding a PostgreSQL connection string (`postgres://...`) to maintain an eventually consistent session index across instances: - **Hosted PostgreSQL (Neon, Supabase, RDS):** Put the provider's connection string in the environment variable named by `postgres_env` with TLS enabled. Use a direct connection or session pool initially, because transaction-pool prepared-statement behavior varies by provider. Brain bounds each process to at most 4 open database connections. - **Discovery only:** PostgreSQL stores an eventually consistent session index for `GET /sessions` and cold-session lookup; it does not lock or coordinate session ownership. ## How checkpoints work [#how-checkpoints-work] Brain uploads `events.jsonl` and append-only `native/` harness files in immutable `64 KiB` blocks alongside bounded tail segments. It writes the checkpoint manifest (`sessions//checkpoints/`) only after every referenced block is in place, then updates the mutable `sessions//latest` pointer (using generation preconditions on immutable objects and replaceable writes on `latest`) and optional PostgreSQL index. Already uploaded 64 KiB blocks are reused on subsequent turns instead of re-uploading the full conversation history. On startup, Brain scans local disk and queues any unsynced sessions. Transient storage failures retry up to 5 times with exponential backoff, while permanent configuration or credential errors stop immediately. If the sync queue (`queue`, default `128`) fills up, Brain rejects new writes with `429 overloaded` and marks `GET /health/ready` as `503 not_ready` until the backlog drains. ### Flush and inspect sync status [#flush-and-inspect-sync-status] - **`GET /sessions/{id}/sync`** returns the latest published checkpoint status, pending upload state, and any sync error. - **`POST /sessions/{id}/flush`** (requires `manage: true`) forces an immediate upload of local event history and the latest completed native checkpoint and returns a `SyncStatus` object: | Field | Description | | --- | --- | | `checkpoint` | Immutable 32-character hex checkpoint reference for handoff or restore. | | `history_bytes` | Uploaded `events.jsonl` byte offset. | | `resume_offset` | Event log byte offset corresponding to the committed native checkpoint boundary. | | `native_checkpoint` | Identifier of the committed native harness checkpoint. | | `time` | UTC publication timestamp of the checkpoint manifest. | | `pending` | `true` if newer local state still needs to be uploaded. | | `error` | Last background sync error message, if any. | > **Note:** If you call `POST /sessions/{id}/flush` while a turn is still running, `history_bytes` may include events written beyond `resume_offset`. Flushing does not pause or trigger model execution. ## Move a session to another server [#move-a-session-to-another-server] Brain does not run distributed consensus or merge writes from two servers running the same session. Your platform must stop the old owner before starting the new one: ### 1. Quiesce the old instance [#1-quiesce-the-old-instance] Stop routing new input to the old instance and let the current turn finish (or send `user.interrupt` via `POST /sessions/{id}/events` and wait for the session to settle). ### 2. Flush and record the checkpoint ID [#2-flush-and-record-the-checkpoint-id] Call `POST /sessions/{id}/flush` on the old instance and save the returned `checkpoint` reference from the response. ### 3. Re-route to the new instance [#3-re-route-to-the-new-instance] Stop the old owner and route the session to the new instance. The new instance must have the same runtime version, harness configuration, tool bindings, and external storage access. ### 4. Restore the explicit checkpoint [#4-restore-the-explicit-checkpoint] Call `POST /sessions/{id}/restore` on the new instance with `{"checkpoint": "RETURNED_REFERENCE"}`. ### 5. Resume the session [#5-resume-the-session] Call `POST /sessions/{id}/resume` to move the restored session from `interrupted` to `idle`, then send new input. When a server receives `GET /sessions/{id}` or `GET /sessions/{id}/events` for a session that is not on local disk, it automatically loads the latest published checkpoint from external storage. During a planned handoff, always call `POST /sessions/{id}/restore` with the explicit `checkpoint` ID from step 2 so a stale local cache or delayed upload cannot pick an older boundary. > **Note:** > - When `restore` replaces an existing local session directory, Brain renames the old directory to `SESSION_ID-replaced-ID` so operators can inspect any unsynced local tail before deleting it. > - Restoring a session recovers its transcript and native model context; it does not roll back or restart external tool jobs, sandboxes, or browser sessions. > - Configure bucket object lifecycle policies so they never expire 64 KiB blocks referenced by retained checkpoints. ## Next steps [#next-steps] - [Host configuration reference](https://aexhq.github.io/brain/docs/reference/configuration) - [Persistence adapters](https://aexhq.github.io/brain/docs/adapters/persistence) - [Operations & observability](https://aexhq.github.io/brain/docs/guides/operations) - [HTTP API reference](https://aexhq.github.io/brain/docs/api) # Platform examples (/brain/docs/guides/platforms) `examples/platforms` provides starting deployment configurations for running a single-owner Brain server and its companion tool services across Docker Compose, Fly.io, AWS ECS/Fargate, and Cloudflare. > **Note:** Keep API keys in environment variables or your platform's secret store, and pin deployed container images by digest from a passing CI run. ## Summary of platform templates [#summary-of-platform-templates] | Platform | Path | Role | Storage | | --- | --- | --- | --- | | **Docker Compose** | `examples/platforms/docker/compose.json` | Brain server + `resources` tool service on a private Compose network | Named local volume (`brain-data`) for local cache; add `persistence` for external backups. | | **Fly.io** | `examples/platforms/fly/fly.toml`, `fly/brain.json` | Single Fly Machine (`--ha=false`, count `1`, `immediate` strategy) | Ephemeral local cache backed by S3/R2 checkpoints; explicit flush/restore handoff on replacement. | | **AWS ECS/Fargate** | `examples/platforms/ecs/task-definition.json`, `ecs/brain.json` | Single Fargate task (`desiredCount: 1`, `minimumHealthyPercent: 0`, `maximumPercent: 100`) | Disposable task storage backed by S3 checkpoints and optional PostgreSQL discovery index. | | **Cloudflare Worker** | `examples/platforms/cloudflare/worker.mjs`, `wrangler.json` | Tenant-scoped R2 HTTP tool service (`/read`, `/write`) | Stateless tool endpoint for Brain; Brain itself still runs in a container or VM. | ## Docker Compose [#docker-compose] Set `BRAIN_API_KEY`, `OPENAI_API_KEY`, and `TOOL_API_KEY` (each at least 24 bytes for Brain/tool tokens), then start the stack from the repository root: ```sh docker compose -f examples/platforms/docker/compose.json up --build ``` - Brain listens on `http://localhost:8080` and calls the `resources` tool service over the internal Compose network (`http://resources:8092`). - Enable `web_fetch` in `config.tools` when creating a session. - The named volume (`brain-data`, mounted as Brain's `data_dir` at `/data`) survives `docker compose down` unless you pass `-v`. A mounted local volume isn't an external backup. Configure `persistence` to copy session checkpoints to external storage. ## Fly.io [#flyio] Edit `examples/platforms/fly/brain.json` for your public app URL and checkpoint archive bucket. From `examples/platforms/fly`, create your Fly app, set `BRAIN_API_KEY`, `OPENAI_API_KEY`, `ARCHIVE_ACCESS_KEY`, and `ARCHIVE_SECRET_KEY` via `fly secrets set`, and deploy a single Machine: ```sh fly deploy --config fly.toml --ha=false fly scale count 1 ``` - **Single-owner settings:** The configuration uses an ephemeral local cache plus external S3/R2 checkpoints, disables idle auto-stop (`auto_stop_machines = "off"`), allows a 30-second `kill_timeout`, and uses `strategy = "immediate"` so deployments replace the single owner instead of overlapping two writers. - **Planned replacement:** Before replacing the Machine, stop accepting new work, let active turns settle (or interrupt them), call `POST /sessions/{id}/flush`, stop the old owner, then `POST /sessions/{id}/restore` and `POST /sessions/{id}/resume` on the new owner. - **Volumes and routing:** A Fly volume is optional and local to one Machine, not cross-host replication. Running multiple Machines requires an application placement layer; `fly-replay` can route HTTP requests to a chosen Machine, while your placement layer must prevent two servers from writing the same session. ## AWS ECS/Fargate [#aws-ecsfargate] 1. Edit `examples/platforms/ecs/brain.json`, build a configuration image on top of a verified Brain image digest, and push it to your container registry. 2. Replace the account ID, image digest, IAM role ARN, and Secrets Manager ARN placeholders in `examples/platforms/ecs/task-definition.json`, and pre-create the `/brain` CloudWatch log group. 3. Grant the **execution role** permission to pull the image and read `BRAIN_API_KEY` and `OPENAI_API_KEY` from Secrets Manager; grant the **task role** S3 read/write (`GetObject`, `PutObject`) under only the configured checkpoint prefix. 4. Run a single task behind a private network and HTTPS load balancer with `desiredCount: 1`, `minimumHealthyPercent: 0`, and `maximumPercent: 100` to prevent overlapping writers, and follow the same explicit flush → stop → restore → resume handoff when replacing tasks. > **Note:** Fargate task storage is a disposable local cache. Add `postgres_env` if your application needs cold-session discovery across task replacements. Load-balancer stickiness alone does not make multi-writer execution safe. ## Cloudflare [#cloudflare] `examples/platforms/cloudflare/worker.mjs` is a complete Cloudflare Worker implementing a tenant-scoped R2 **tool** service with `/read` and `/write` endpoints, request size bounds, bearer token authentication (`TOOL_API_KEY`), and automatic per-tenant key prefixes (`tools//`). 1. Edit the R2 bucket binding in `examples/platforms/cloudflare/wrangler.json`, set `TOOL_API_KEY` with `wrangler secret put TOOL_API_KEY`, and deploy with Wrangler. Leave `ALLOW_WRITE` set to `"false"` for read-only access. 2. Bind HTTP tools in Brain's `tools` config pointing to the Worker's `/read` (`{ "key": "..." }`) and `/write` (`{ "key": "...", "text": "..." }`) URLs. Never point model-accessible tools at Brain's checkpoint bucket. > **Note:** Brain runs in a container or VM because the Go binary starts the Pi harness as a Node.js child process. The Cloudflare Worker hosts stateless HTTP tool services. ## Application backends [#application-backends] Keep Brain management credentials on your backend server: - A Next.js route handler, Python backend, or API gateway should authenticate its own end user, verify which session IDs that user owns, and proxy only permitted operations to Brain. - Use the TypeScript or Python SDKs (`examples/session.mjs` and `examples/session.py`) on your backend for `create`, `send`, `observe`, `interrupt`, `flush`, and `resume`; never expose a `manage: true` token directly to browser clients. Sources: [Fly configuration](https://docs.fly.io/reference/configuration), [Fly volumes](https://docs.fly.io/volumes/overview), [ECS task parameters](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html). ## Next steps [#next-steps] - [Operations & observability](https://aexhq.github.io/brain/docs/guides/operations) - [Persistence & recovery](https://aexhq.github.io/brain/docs/guides/persistence) - [Authentication & tenants](https://aexhq.github.io/brain/docs/guides/authentication) - [Host configuration reference](https://aexhq.github.io/brain/docs/reference/configuration) - [Integration status](https://aexhq.github.io/brain/docs/roadmap#integration-status) # Session lifecycle (/brain/docs/guides/sessions) A session is one agent conversation that belongs to one tenant. You [create a session](#create-a-session) with its harness, model, and tools, then [send messages](#send-a-message) and [stream events](#stream-events) while each turn runs. When a turn needs to stop, you [interrupt it](#interrupt-a-turn), then [resume](#resume-after-an-interruption-or-failure) or [close](#close-a-session) the session. ## Before you begin [#before-you-begin] The examples use a client for the `local` tenant from the [Quickstart](/brain/docs/quickstart). Install the JavaScript/TypeScript or Python package as described there. ```ts tab="TypeScript" import { Brain } from "@aexhq/brain"; const brain = new Brain("http://127.0.0.1:8080", process.env.BRAIN_API_KEY!); ``` ```python tab="Python" import os from brain import Brain brain = Brain("http://127.0.0.1:8080", os.environ["BRAIN_API_KEY"]) ``` ## Session statuses [#session-statuses] | Status | Meaning | What you can do next | | --- | --- | --- | | `idle` | No turn is running. | Send a message or close the session. | | `running` | A turn is running, or messages are waiting in the queue. | Send more messages (they queue), interrupt, or close. | | `interrupted` | A turn stopped before it finished: you interrupted it, the model provider returned an error, Brain stopped during the turn, or Brain loaded the session from external storage. | Resume, then send a message. Or close. | | `failed` | Brain stopped the turn because of its own failure, such as a crashed harness process or a failed disk write. | Resume, then send a message. Or close. | | `closed` | The session is permanently closed. | Read its event history. | ## Create a session [#create-a-session] Create a session with the harness, model, instructions, and tools the agent should use. Brain saves the session right away and starts the harness when you send the first message. See [Create a session](/brain/docs/api/generated/create) for every field and status code. | Field | Required | Description | | --- | --- | --- | | `harness` | Yes | A harness name from the host config, such as `pi`. | | `config.model` | Yes | A model in `provider/model` form, such as `openai/gpt-4.1-mini`, up to 256 characters. The provider must be configured in the host config, and its API key must be set. | | `config.tools` | Yes | Names of the tools to enable, up to 128. Pass `[]` for none. Each tool must be available to your tenant. | | `config.instructions` | No | The system prompt, up to `input_bytes` (64 KiB by default). Pi uses a generic assistant prompt when you omit it. | | `config.reasoning_effort` | No | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Pi defaults to `off`. Choose a level the model supports; otherwise the first turn fails with `session.error`. | ```sh tab="cURL" curl http://127.0.0.1:8080/sessions \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"harness":"pi","config":{"model":"openai/gpt-4.1-mini","instructions":"Answer in one paragraph.","tools":[]}}' ``` ```ts tab="TypeScript" const session = await brain.create({ harness: "pi", config: { model: "openai/gpt-4.1-mini", instructions: "Answer in one paragraph.", tools: [] }, }); ``` ```python tab="Python" session = brain.create({ "harness": "pi", "config": {"model": "openai/gpt-4.1-mini", "instructions": "Answer in one paragraph.", "tools": []}, }) ``` Brain returns `201 Created` with the session in status `idle`: ```json { "id": "7d1c9e04b3a24f6e8c5a0b2d9e3f4a61", "harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "instructions": "Answer in one paragraph.", "tools": [] }, "status": "idle", "created_at": "2025-01-01T12:00:00Z", "updated_at": "2025-01-01T12:00:00Z", "checkpoint": { "id": "0f8e2a6c1d3b4e5f9a7c6b5d4e3f2a1b", "offset": 0, "time": "2025-01-01T12:00:00Z" } } ``` Creating a session needs a token with `manage` that isn't limited to one session; other tokens get `403`. An unknown harness returns `422`. A malformed model, a provider that isn't configured, an invalid reasoning effort, more than 128 tools, or a duplicate tool returns `400`. A tool that isn't available to your tenant returns `403`. ## Send a message [#send-a-message] `POST /sessions/{id}/events` with a `user.message` adds the message to the session's queue and returns `202 Accepted` with the saved event. The event's `id` is the `input_id` on every event of the turn it starts. Any token for the session's tenant that isn't read-only can send messages; `manage` isn't required. | Field | Required | Description | | --- | --- | --- | | `type` | Yes | `user.message`. | | `text` | Yes | The message. It must not be blank and is limited to `input_bytes` (64 KiB by default). | | `key` | No | An idempotency key, up to 128 bytes. | | `delivery` | No | `queue`, the default: the message runs after the active turn and any messages queued before it. | ```sh tab="cURL" curl http://127.0.0.1:8080/sessions/SESSION_ID/events \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type":"user.message","text":"Summarize our plan.","key":"msg-0001"}' ``` ```ts tab="TypeScript" const input = await brain.send(session.id, { type: "user.message", text: "Summarize our plan.", key: "msg-0001" }); ``` ```python tab="Python" message = brain.send(session["id"], {"type": "user.message", "text": "Summarize our plan.", "key": "msg-0001"}) ``` A session runs one turn at a time. Messages sent while a turn runs wait in order, up to `queue` messages (16 by default). ### Retry safely with idempotency keys [#retry-safely-with-idempotency-keys] Send a `key` with each message so you can retry after a timeout or a dropped connection: - **Same key, same message:** Brain returns the original event and doesn't queue the message again. - **Same key, different message:** Brain returns `409 conflict`. - **Rejected request:** A key is recorded only when Brain accepts the message, so a request rejected with `429` doesn't use up its key. Keys are stored with the event log and still apply after Brain restarts. ### Handle send errors [#handle-send-errors] | Status | Code | Cause | | --- | --- | --- | | `400` | `invalid_request` | Blank or oversized `text`, a `key` over 128 bytes, or an unknown `delivery`. | | `409` | `conflict` | The key was used with a different message, the session is closed, or the session is `interrupted` or `failed`. Resume it before sending. | | `429` | `overloaded` | The queue is full, the session's event log is near its `session_bytes` limit, the external storage upload backlog is full, or Brain is shutting down. Retry after the `Retry-After` delay. | ## Stream events [#stream-events] `GET /sessions/{id}/events` returns the session's events in two ways: - **JSON pages:** By default, Brain returns `{"events": [...], "next_cursor": "..."}` with up to `limit` retained events (1 to 1000, default 100). Pass `next_cursor` as `after` to read the next page. - **Server-sent events:** With `Accept: text/event-stream`, Brain replays retained events after the `after` cursor (or from the start of the session), then sends new events as they happen. Each SSE message has an `event:` line with the event type and a `data:` line with the event as JSON. Retained events also have an `id:` line with their cursor. Brain sends a `: keepalive` comment every 15 seconds while the stream is idle. | Event | Retained | Description | | --- | --- | --- | | `user.message` | Yes | A message you sent. Its `id` is the `input_id` for the turn it starts. | | `session.status` | Yes | The session's new status, such as `running` or `idle`. | | `assistant.delta` | No | A chunk of assistant text while the model writes. | | `assistant.message` | Yes | A complete assistant message. A turn can contain several. | | `tool.call` | Yes | A tool call, with the `id` Brain assigned, the tool name, and arguments. | | `tool.progress` | No | A progress update from a streaming tool. | | `tool.result` | Yes | The tool's result, including errors. | | `session.error` | Yes | Why a turn or a queued message stopped. | Live-only events (`assistant.delta` and `tool.progress`) reach only clients connected when they happen; they never appear in JSON pages or replays. See [Events & streaming](/brain/docs/reference/events) for each event's payload. ```sh tab="cURL" curl -N http://127.0.0.1:8080/sessions/SESSION_ID/events \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Accept: text/event-stream" ``` ```ts tab="TypeScript" let cursor = ""; for await (const event of brain.observe(session.id, { after: cursor })) { if (event.cursor) cursor = event.cursor; // save for reconnecting if (event.type === "assistant.delta") process.stdout.write((event.data as { text: string }).text); if (event.input_id !== input.id) continue; if (event.type === "session.error") throw new Error((event.data as { message: string }).message); if (event.type === "session.status" && (event.data as { status: string }).status === "idle") break; } ``` ```python tab="Python" cursor = "" stream = brain.observe(session["id"], after=cursor) try: for event in stream: cursor = event.get("cursor") or cursor # save for reconnecting if event["type"] == "assistant.delta": print(event["data"]["text"], end="", flush=True) if event.get("input_id") != message["id"]: continue if event["type"] == "session.error": raise RuntimeError(event["data"]["message"]) if event["type"] == "session.status" and event["data"]["status"] == "idle": break finally: stream.close() ``` ### Know when a turn ends [#know-when-a-turn-ends] Brain emits `session.status` with `running` and the message's `input_id` when a turn starts. A turn ends in one of three ways: - **It completes and the queue is empty:** `session.status` with `idle` and the same `input_id`. - **It completes and another message is queued:** `session.status` with `running` and the next message's `input_id`. There's no `idle` in between. - **It stops early:** `session.error` with the message's `input_id`, followed by `session.status` with `interrupted`, `failed`, or `closed`. Each queued message also gets a `session.error` with code `conflict`. ### Reconnect after a disconnect [#reconnect-after-a-disconnect] Disconnecting doesn't stop the turn. To resume streaming after a disconnect, pass the cursor of the last retained event you processed as `{ after: cursor }` in TypeScript, `after=cursor` in Python, or the `after` query parameter or `Last-Event-ID` header over HTTP. Brain buffers up to `subscriber_events` events (128) and `subscriber_bytes` (2 MiB) for each stream. A client that falls further behind is disconnected and must reconnect from its last cursor. When `subscribers` streams (128) are already open, a new stream gets `429`. A cursor outside the session's retained history returns `409 cursor_unavailable`; this can happen after a restore drops an unsynced tail. ## Interrupt a turn [#interrupt-a-turn] Send `{"type":"user.interrupt"}` to the events endpoint to stop the running turn. Brain returns `202 Accepted` with `{"status":"interrupt_requested"}`. If no turn is running, the request does nothing. Like sending, interrupting doesn't require `manage`. ```sh tab="cURL" curl http://127.0.0.1:8080/sessions/SESSION_ID/events \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type":"user.interrupt"}' ``` ```ts tab="TypeScript" await brain.interrupt(session.id); ``` ```python tab="Python" brain.interrupt(session["id"]) ``` Brain asks the harness to stop, cancels the turn, and emits `session.error` with code `conflict` for the turn and for each queued message. The session then moves to `interrupted`. An interrupt request accepts no other fields. Interrupting cancels in-flight tool requests, though a tool service may already have started work, such as a sandbox command or an external job. Brain sends each tool call at most once; your tool service owns retries and cleanup. ## Resume after an interruption or failure [#resume-after-an-interruption-or-failure] `POST /sessions/{id}/resume` moves an `interrupted` or `failed` session back to `idle`. Brain restores the harness's native state and the session config from the last completed turn's checkpoint, then emits `session.status` with `idle` and no `input_id`. Resuming doesn't call the model or re-run tools. Events from the stopped turn stay in the history, but the harness no longer sees them. ```sh tab="cURL" curl -X POST http://127.0.0.1:8080/sessions/SESSION_ID/resume \ -H "Authorization: Bearer $BRAIN_API_KEY" ``` ```ts tab="TypeScript" await brain.resume(session.id); ``` ```python tab="Python" brain.resume(session["id"]) ``` Resuming needs a token with `manage` and returns the session. An `idle` session is returned unchanged. A running or closed session returns `409`. After resuming, send a new message to continue. ## Close a session [#close-a-session] `POST /sessions/{id}/close` permanently closes a session and returns `202 Accepted` with `{"status":"close_requested"}`. An idle session closes immediately. If a turn is running, Brain interrupts it and fails the queued messages first. Closing needs a token with `manage`. ```sh tab="cURL" curl -X POST http://127.0.0.1:8080/sessions/SESSION_ID/close \ -H "Authorization: Bearer $BRAIN_API_KEY" ``` ```ts tab="TypeScript" await brain.close(session.id); ``` ```python tab="Python" brain.close(session["id"]) ``` The event history stays readable after you close a session; new messages return `409`. Resources that tools created, such as sandboxes, belong to the tool services, which clean them up. ## List and inspect sessions [#list-and-inspect-sessions] - **List:** `GET /sessions` returns `{"sessions": [...]}` for your tenant, sorted by ID. Pass the last ID as `after` to get the next page, with `limit` from 1 to 1000 (default 100). With a PostgreSQL index configured, the list includes sessions stored by other Brain instances. - **Inspect:** `GET /sessions/{id}` returns the session with its current status and latest checkpoint. A session that belongs to another tenant returns `404`, the same as a session that doesn't exist. A token limited to one session sees only that session. The SDKs expose these routes as `list(after, limit)` and `get(id)`. ## Next steps [#next-steps] } title="Events & streaming" href="/docs/reference/events">Payloads for every event type. } title="Distributed tools" href="/docs/tools">Give sessions tools that run in your services. } title="Persistence & recovery" href="/docs/guides/persistence">Upload checkpoints and restore sessions on another server. } title="HTTP API" href="/docs/api">Every route, request body, and status code. # Introduction (/brain/docs) Brain is a single application you deploy as a service to run the agents behind your products. It's an open-source alternative to Claude Managed Agents and the OpenAI Agents SDK, running on your own infrastructure with the models you choose. | Approach | What it is | Best for | | --- | --- | --- | | **Model API** | Direct requests to a model provider. You write the agent loop, tool dispatch, and conversation storage. | Single completions, or full control over every model call. | | **Agent library** | An agent loop that runs inside your application process, such as the OpenAI Agents SDK. | Agents that one process can run from start to finish. | | **Managed agent service** | A hosted agent loop and session service that runs on the provider's infrastructure, such as Claude Managed Agents. | Agent tasks that run in the background, without operating your own infrastructure. | | **Brain** | An open-source agent server you deploy yourself. It runs sessions for your applications: the agent loop, the event history, tool calls, and recovery. | Agents that serve many customers and outlast any one process, with tools in your own services and state on your own infrastructure. | Brain gives you session-oriented primitives for building agents. Your backend creates a session, sends messages, and streams events. Brain runs the harness (the agent loop that calls the model and manages context), records every event, and sends tool calls to services you run. Sessions outlive client connections and server restarts, so you can reconnect to a session, interrupt it, and resume it later. } title="Quickstart" href="/docs/quickstart">Start Brain on your machine and run your first session. } title="Sessions" href="/docs/guides/sessions">Send messages, stream events, interrupt turns, and resume. } title="Tools" href="/docs/tools">Connect HTTP, SSE, and MCP tools to your sessions. } title="API reference" href="/docs/api">Routes, payloads, and status codes. ## Core concepts [#core-concepts] | Concept | Description | | --- | --- | | **Session** | An agent conversation that outlives client connections and server restarts. You choose its harness, model, instructions, and tools, then exchange messages and events with it over time. | | **Turn** | The work Brain does for one user message: model calls and tool calls until the harness finishes. A session runs one turn at a time and queues further messages in order. | | **Event** | A typed record in a session's stream, such as a message, a tool call, or a status change. Follow events live, or replay the history from the last event you read. | | **Harness** | The agent loop that calls the model, manages context, and decides when a turn is done. Brain includes the Pi harness, and you can [add your own](/brain/docs/adapters/harness). | | **Tool** | A capability implemented in application code, a client function, or a service. Brain calls it when the model asks, and each session uses exactly the tools you enable for it. | | **Tenant** | The isolation boundary for sessions, tokens, and tools. | | **Host config** | The JSON file that configures a Brain server: its API tokens, harnesses, model providers, tools, limits, and storage. | | **Checkpoint** | Saved session state that Brain records after each completed turn, for recovering a session or moving it to another server. | ## How it works [#how-it-works] ### Configure and start Brain [#configure-and-start-brain] Declare your API tokens, harnesses, model providers, and tools in a host config, then start the server. ### Create a session [#create-a-session] Choose a harness, a model, instructions, and the tools the agent can use. Brain saves the session and returns its ID. ### Send a message [#send-a-message] Brain records the message, queues it, and starts a turn. The harness calls the model. When the model calls a tool, Brain sends the call to your tool service together with the session's identity, then returns the result to the harness. ### Stream events [#stream-events] Follow the session's events live, or page through its history. The turn keeps running when a client disconnects, and the client can reconnect from the last event it processed. ### Interrupt, resume, or close [#interrupt-resume-or-close] Stop a running turn, resume an interrupted session from its last completed turn, or close the session. With external storage configured, another server can restore the session from its checkpoints. ## When to use Brain [#when-to-use-brain] - **Background agents:** Sessions outlive client connections and server restarts. Clients reconnect and continue from the last event they processed. - **Multi-tenant products:** Each tenant's sessions, tokens, and tools stay separate. Tools receive the session's identity from Brain rather than from the model, so they can authorize every call. - **Tools in other systems:** Sandboxes, storage, and human approvals run as services that you deploy and scale on their own, in any language. - **State on your infrastructure:** You run the open-source server. Sessions stay on your disks and, if you configure it, in your own object storage. ### Brain or an embedded agent library? [#brain-or-an-embedded-agent-library] **Use an agent library** when one process can run the whole agent: your application owns the loop, the tools, and the conversation state, and nothing else needs to reach the session. **Use Brain** when the agent runs as its own service: several clients or customers reach its sessions over an API, tools live in other systems, and sessions outlast client connections and server restarts. With Brain, your application stays in charge of what each agent is. You choose every session's instructions, model, and tools, and you coordinate any work that spans several agents. ## What's included [#whats-included] - **Pi harness:** Brain includes the Pi harness, an agent loop that works with models from the providers you configure. Every tool is an implementation you control, and the model sees exactly the tools you enable for its session, so you decide everything the agent can do. - **[Tool connections](/brain/docs/tools):** Answer tool calls over HTTP, stream progress with server-sent events, or connect tools from a remote [MCP server](/brain/docs/tools/mcp). - **Tool services:** A [sandbox service](/brain/docs/tools/sandboxes) for Modal, E2B, and Daytona; a [storage and web service](/brain/docs/tools/resources) for S3 or R2 storage, Brave or Tavily search, and public web fetch; and an [example tool server](/brain/docs/tools/local-tools) with an operator approval flow. - **[Session storage](/brain/docs/guides/persistence):** Sessions live on local disk and can sync checkpoints to a directory, Amazon S3, Cloudflare R2, or Google Cloud Storage. An optional PostgreSQL index lets servers find each other's sessions. - **SDKs:** Compose your service with the [Go application SDK](/brain/docs/sdks/application), or connect with [TypeScript](/brain/docs/sdks/typescript), [Python](/brain/docs/sdks/python), and the [HTTP API](/brain/docs/api). - **[Operations](/brain/docs/guides/operations):** Health checks, structured logs, OpenTelemetry traces and metrics, and a published container image. ## Choose your path [#choose-your-path] | Goal | Start here | | --- | --- | | Start the server and send a message | [Quickstart](/brain/docs/quickstart) | | Understand how Brain runs a session | [Architecture](/brain/docs/architecture) | | Connect from your backend | [TypeScript SDK](/brain/docs/sdks/typescript) or [Python SDK](/brain/docs/sdks/python) | | Stream, interrupt, and resume sessions | [Session lifecycle](/brain/docs/guides/sessions) | | Inspect routes and event payloads | [HTTP API](/brain/docs/api) and [Events & streaming](/brain/docs/reference/events) | | Configure models and API keys | [Models & providers](/brain/docs/reference/models) | | Build a tool service | [Distributed tools](/brain/docs/tools) and [Local tools & approvals](/brain/docs/tools/local-tools) | | Connect a remote MCP server | [MCP tools](/brain/docs/tools/mcp) | | Back up sessions and move them between servers | [Persistence & recovery](/brain/docs/guides/persistence) | | Deploy and monitor Brain | [Operations & observability](/brain/docs/guides/operations) | | Write a harness or storage adapter | [Adapter overview](/brain/docs/adapters) and [Go interfaces](/brain/docs/adapters/interfaces) | | See what's coming next | [Roadmap](/brain/docs/roadmap) | ## Docs for coding agents [#docs-for-coding-agents] These docs are also available as plain text for LLMs and coding agents. [`/llms.txt`](/brain/llms.txt) lists every page, and [`/llms-full.txt`](/brain/llms-full.txt) contains the full text of all pages. # Quickstart (/brain/docs/quickstart) This guide walks you through running Brain on your machine and sending your first message. You start the server with the example host config, create a session that uses the Pi harness and an OpenAI model, and stream the reply with the TypeScript or Python SDK. | Concept | In this guide | | --- | --- | | **Host config** | `examples/local.json`: one API token for the `local` tenant, the `pi` harness, the OpenAI provider, and no tools. | | **Session** | One conversation that uses the `pi` harness and the `openai/gpt-4.1-mini` model. | | **Turn** | The work Brain runs for your message: model calls until Pi finishes its reply. | | **Event** | Records such as `assistant.message` and `session.status` that the example reads from the session's stream. | ## Prerequisites [#prerequisites] - **Go 1.26 or newer** to run the server. - **Node.js 24 or newer** to run the Pi runner and JavaScript/TypeScript client. - **Python 3.11 or newer**, only if you use the Python SDK or run the full test suite. - An **OpenAI API key**. To use another provider, see [Models & providers](/brain/docs/reference/models). The SDKs install from npm and PyPI. This guide builds the server and Pi runner locally; [platform examples](/brain/docs/guides/platforms) use the published container image. ### Get the code and build the Pi runner [#get-the-code-and-build-the-pi-runner] Clone the repository and build the Pi runner. Brain starts shared runner hosts when sessions begin their first turn. ```sh git clone https://github.com/aexhq/brain.git cd brain npm --prefix runners/pi ci --ignore-scripts npm --prefix runners/pi run build ``` ### Set your keys and start Brain [#set-your-keys-and-start-brain] Brain reads two secrets from the environment: - `BRAIN_API_KEY` is the token your application sends to Brain. It must be at least 24 bytes. Use a long random value anywhere other than your own machine. - `OPENAI_API_KEY` is the model provider key that Brain passes to the Pi runner. ```sh tab="macOS / Linux" export BRAIN_API_KEY="replace-with-a-random-token-of-at-least-24-bytes" export OPENAI_API_KEY="your-provider-key" go run ./cmd/brain -config examples/local.json ``` ```powershell tab="Windows" $env:BRAIN_API_KEY = "replace-with-a-random-token-of-at-least-24-bytes" $env:OPENAI_API_KEY = "your-provider-key" go run ./cmd/brain -config examples/local.json ``` When the server is ready, it writes a JSON log line with `"msg":"brain listening"` to stderr. With `examples/local.json`, Brain listens on `http://127.0.0.1:8080`, writes local state to `.brain/`, grants `BRAIN_API_KEY` the `local` tenant with permission to manage sessions, registers the `pi` harness, and enables no tools. Copy `examples/local.json` to `brain.json` to change settings without editing tracked files. `brain.json` is gitignored, and Brain loads it when you omit `-config`. Add `-check` to validate a config, including the environment variables it names, and exit without starting the server. ### Open a second terminal [#open-a-second-terminal] Leave the server running. Open a second terminal in the repository root and set the same `BRAIN_API_KEY`. Install the Python package if you use the Python example. ```sh tab="macOS / Linux" export BRAIN_API_KEY="replace-with-a-random-token-of-at-least-24-bytes" python -m pip install aex-brain==2.0.0 ``` ```powershell tab="Windows" $env:BRAIN_API_KEY = "replace-with-a-random-token-of-at-least-24-bytes" python -m pip install aex-brain==2.0.0 ``` ### Run a session [#run-a-session] Run the example script for your language. Install the published SDK for your language. If your system has no `python` command, use `python3`. ```sh tab="TypeScript" npm install --no-save --package-lock=false @aexhq/brain@2.0.0 node examples/session.mjs "Explain what makes a good agent tool." ``` ```sh tab="Python" python examples/session.py "Explain what makes a good agent tool." ``` The script prints the session ID and the assistant's reply, then exits when the turn ends. Example output: ```text Session: 7d1c9e04b3a24f6e8c5a0b2d9e3f4a61 A good agent tool does one clearly named job, ... ``` ## How the example works [#how-the-example-works] Both scripts create a session, send one message, and read events until the turn for that message ends. Here are the same steps with comments: ```ts tab="TypeScript" import { Brain } from "@aexhq/brain"; const token = process.env.BRAIN_API_KEY; if (!token) throw new Error("Set BRAIN_API_KEY before running this example"); // Connect with the token that the host config grants to the "local" tenant. const brain = new Brain("http://127.0.0.1:8080", token); // Create a session with the harness, model, and tools the agent should use. const session = await brain.create({ harness: "pi", config: { model: "openai/gpt-4.1-mini", tools: [] } }); console.log(`Session: ${session.id}`); // Send a message. The random key makes the request safe to retry. const input = await brain.send(session.id, { type: "user.message", text: "Say hello.", key: crypto.randomUUID() }); // Read events until the turn for this message ends. for await (const event of brain.observe(session.id)) { const data = event.data as { text?: string; message?: string; status?: string }; if (event.type === "assistant.message") console.log(data.text); if (event.input_id !== input.id) continue; if (event.type === "session.error") throw new Error(data.message); if (event.type === "session.status" && data.status === "idle") break; } ``` ```python tab="Python" import os import uuid from brain import Brain # Connect with the token that the host config grants to the "local" tenant. brain = Brain("http://127.0.0.1:8080", os.environ["BRAIN_API_KEY"]) # Create a session with the harness, model, and tools the agent should use. session = brain.create({"harness": "pi", "config": {"model": "openai/gpt-4.1-mini", "tools": []}}) print("Session:", session["id"]) # Send a message. The random key makes the request safe to retry. message = brain.send(session["id"], {"type": "user.message", "text": "Say hello.", "key": str(uuid.uuid4())}) # Read events until the turn for this message ends. stream = brain.observe(session["id"]) try: for event in stream: if event["type"] == "assistant.message": print(event["data"]["text"]) if event.get("input_id") != message["id"]: continue if event["type"] == "session.error": raise RuntimeError(event["data"]["message"]) if event["type"] == "session.status" and event["data"]["status"] == "idle": break finally: stream.close() ``` The scripts in `examples/` take the prompt from their arguments. `examples/session.mjs` also reads a comma-separated list of tool names from `BRAIN_TOOLS`, and `examples/session.py` reads `BRAIN_URL` and `BRAIN_MODEL`. Ending the loop doesn't close the session; it stays available for more messages. ## Call the HTTP API with cURL [#call-the-http-api-with-curl] The SDKs wrap Brain's HTTP API. You can make the same calls with cURL from a macOS or Linux shell. Create a session. Brain returns `201 Created` with the session; save its `id`. ```sh curl http://127.0.0.1:8080/sessions \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"harness":"pi","config":{"model":"openai/gpt-4.1-mini","tools":[]}}' ``` Send a message using the returned session `id`: ```sh curl http://127.0.0.1:8080/sessions/SESSION_ID/events \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type":"user.message","text":"Hello","key":"first-message"}' ``` `POST /sessions/{id}/events` returns `202 Accepted` with the saved `user.message` event once the message is stored and queued. The event's `id` is the `input_id` on the events of the turn it starts. Stream the session's events: ```sh curl -N http://127.0.0.1:8080/sessions/SESSION_ID/events \ -H "Authorization: Bearer $BRAIN_API_KEY" \ -H "Accept: text/event-stream" ``` The stream replays the session's retained events, then follows new events as they happen. Without `Accept: text/event-stream`, the same URL returns a JSON page of retained events. See [Events & streaming](/brain/docs/reference/events) for every event type. ## What happened [#what-happened] 1. **Brain loaded the host config.** It checked that `BRAIN_API_KEY` and `OPENAI_API_KEY` are set, registered the `pi` harness, and opened local state in `.brain/`. 2. **The script created a session.** Brain stored it with status `idle` and returned `201 Created`. 3. **The script sent a message.** Brain saved a `user.message` event, queued it, and returned `202 Accepted`. 4. **Brain ran a turn.** It emitted `session.status` with `running`, started the Pi runner, and streamed `assistant.delta` events while the model wrote. The complete reply arrived as `assistant.message`. 5. **The session went idle.** A `session.status` event with `idle` and the message's `input_id` ended the turn. The session stays open for more messages until you close it. ## Next steps [#next-steps] } title="Add a tool" href="/docs/tools/local-tools">Run the local tool server with `echo`, `inspect_session`, and `ask_approval`. } title="Session lifecycle" href="/docs/guides/sessions">Queue messages, interrupt turns, resume, and close sessions. } title="TypeScript SDK" href="/docs/sdks/typescript">Call Brain from a Node.js or TypeScript backend. } title="Python SDK" href="/docs/sdks/python">Call Brain from a Python backend. } title="Persistence & recovery" href="/docs/guides/persistence">Back up checkpoints and restore sessions on another server. } title="HTTP API" href="/docs/api">Every route, request body, and response. # Host configuration (/brain/docs/reference/configuration) Brain reads its host configuration from a JSON file passed with `-config` (defaults to `brain.json`). You can validate the configuration file and all referenced environment variables without starting the HTTP server by passing `-check`: ```sh tab="macOS / Linux" go run ./cmd/brain -config brain.json -check ``` ```powershell tab="Windows" go run ./cmd/brain -config brain.json -check ``` Configuration files are limited to 4 MiB. Unknown JSON fields and trailing JSON data are rejected at startup. Relative paths (such as `data_dir` or `persistence.directory`) resolve from the working directory of the Brain process. ## Minimal example [#minimal-example] ```json { "listen": "127.0.0.1:8080", "public_url": "http://127.0.0.1:8080", "data_dir": ".brain", "tokens": [ { "token_env": "BRAIN_API_KEY", "tenant": "local", "manage": true } ], "harnesses": { "pi": { "command": ["node", "runners/pi/dist/main.js"] } }, "providers": { "openai": { "api_key_env": "OPENAI_API_KEY" } }, "tools": [] } ``` ## Top-level fields [#top-level-fields] | Field | Type | Default | Description | | --- | --- | --- | --- | | `listen` | `string` | `"127.0.0.1:8080"` | TCP `host:port` address to bind. Validated with `net.SplitHostPort`. | | `public_url` | `string` | `"http://127.0.0.1:8080"` | Base `http` or `https` URL passed as `brain_url` to tools with `inspect_session: true`. Must not contain credentials, query parameters, or fragments. | | `data_dir` | `string` | `".brain"` | Local directory for `metadata.sqlite` and per-session directories (`sessions//`). Must be non-empty. | | `tokens` | `Token[]` | Required | Non-empty array of API token grants (see [`tokens`](#authentication-tokens-tokens)). | | `harnesses` | `Record` | Required | Map of harness name (`1`–`64` characters) to child process command (see [`harnesses`](#harnesses-harnesses)). At least one harness is required. | | `providers` | `Record` | `{}` | Map of model provider prefix to credentials and optional transport overrides (see [`providers`](#model-providers-providers)). | | `tools` | `Binding[]` | `[]` | Array of external HTTP, SSE, or MCP tool bindings (see [`tools`](#external-tools-tools)). | | `limits` | `Limits` | See below | In-memory, disk, queue, and SSE subscriber bounds (see [`limits`](#resource-limits-limits)). | | `persistence` | `Settings` | `null` | Optional background checkpoint storage and session index settings (see [`persistence`](#external-persistence-persistence)). | | `credential_keys` | `object` | `null` | Independent encryption key configuration for private session overrides. | ## Authentication tokens (`tokens`) [#authentication-tokens-tokens] Each entry in `tokens` binds an environment variable containing a bearer secret to a tenant and permission scope. Duplicate token values across entries are rejected at startup. See [Authentication & tenants](/brain/docs/guides/authentication). | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `token_env` | `string` | Yes | — | Name of the environment variable holding the bearer token. The environment variable must be set at startup and contain at least 24 bytes. | | `tenant` | `string` | Yes | — | Tenant identifier (`1`–`128` bytes). All operations using this token are isolated to this tenant. | | `session` | `string` | No | `""` | Optional 32-character hex session ID. When set, the token can only access that single session and cannot create new sessions. | | `manage` | `boolean` | No | `false` | Permits management operations (`POST /sessions`, `resume`, `close`, `flush`, and `restore`). | | `read_only` | `boolean` | No | `false` | Restricts the credential to `GET` requests (`list`, `get`, `events`, `sync`, and `catalog`). | ## Harnesses (`harnesses`) [#harnesses-harnesses] Each key in `harnesses` is the harness name clients pass in `POST /sessions` (for example, `"pi"`). | Field | Type | Required | Description | | --- | --- | --- | --- | | `command` | `string[]` | Yes | Non-empty command and arguments used to spawn the harness child process (for example, `["node", "runners/pi/dist/main.js"]`). `command[0]` must be non-empty. | | `max_shared` | `integer` | No | Session attachments per host; default `8`, range `1`–`1024`. | | `max_active_turns` | `integer` | No | Simultaneous turns per host; defaults to `max_shared`, range `1`–`max_shared`. | | `max_hosts` | `integer` | No | Maximum live hosts for this harness; default `16`, minimum `1`. | | `max_starting` | `integer` | No | Concurrent session opens; default `4`, minimum `1`. | Hosts use private working and home directories. Linux process guardians and Windows Job Objects own descendant lifetimes. Idle session eviction detaches only that session; the last detach closes the host. Global `limits.active` also bounds resident native session attachments. ## Private credential keys [#private-credential-keys] ```json {"credential_keys":{"active":"key-2026","keys":{"key-2026":"BRAIN_CREDENTIAL_KEY"}}} ``` `active` selects a key ID from `keys`. Each value names an environment variable containing a base64-encoded 32-byte AES key. Every configured key must be available at startup. Keep retired keys available while retained checkpoints still reference them. Application code may instead supply `Options.Keys` implementing `CredentialKeys`. Private overrides are authenticated encrypted SQL records, bound to tenant, session, and alias. Creation rejects overrides without configured keys. Public JSON never contains ciphertext or plaintext values. [Credential administration](/brain/docs/sdks/application#persist-private-overrides) covers rotation, revocation, and checkpoint retention. ## Model providers (`providers`) [#model-providers-providers] Each key in `providers` matches the `provider` prefix in `config.model` (`provider/model`). Every configured provider must have its `api_key_env` environment variable set and non-empty when Brain starts. See [Models & providers](/brain/docs/reference/models). | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `api_key_env` | `string` | Yes | — | Environment variable containing the provider API key. Passed to the harness runner via stdio `open` options. | | `base_url` | `string` | No | `""` | Optional `http` or `https` base URL override for proxies or compatible endpoints (must not include user credentials). | | `api` | `string` | No | `""` | Optional transport override in the Pi model registry (for example, `"openai-completions"`). | ## External tools (`tools`) [#external-tools-tools] Each element in `tools` registers one tool that sessions belonging to authorized `tenants` can enable in `config.tools`. See [External tools](/brain/docs/tools) and [MCP tools](/brain/docs/tools/mcp) for protocol and discovery details. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | `string` | Yes | — | Unique tool name matching `^[a-zA-Z][a-zA-Z0-9_]{0,63}$`. | | `description` | `string` | Yes | — | Non-empty description shown to the model. | | `input_schema` | `object` | Yes | — | JSON Schema for tool arguments. External `$ref` URLs are rejected. | | `output_schema` | `object` | No | `null` | JSON Schema for successful `structured_content`. Error results bypass the success schema, while content and size limits still apply. | | `url` | `string` | Yes | — | Target `http` or `https` endpoint URL (no credentials or URL fragments). | | `tenants` | `string[]` | Yes | — | Non-empty list of tenant IDs permitted to see and enable this tool. | | `token_env` | `string` | No | `""` | Optional environment variable holding the bearer token Brain sends in `Authorization: Bearer `. Must be non-empty at startup when specified. | | `transport` | `string` | No | `"http"`, or `"mcp"` when `mcp_tool` is set | Transport registered at startup. Unknown names fail config validation. Built-ins are `http` (JSON/SSE) and `mcp`. | | `mcp_tool` | `string` | No | `""` | Remote MCP tool name. When set, Brain calls `url` using the MCP Streamable HTTP transport instead of the native HTTP/SSE tool protocol. | | `timeout_seconds` | `integer` | No | `300` | Per-call deadline in seconds. Allowed range: `1`–`86400` (24 hours). | | `max_output_bytes` | `integer` | No | `1048576` | Maximum response body size in bytes. Allowed range: `1`–`2097152` (2 MiB). | | `inspect_session` | `boolean` | No | `false` | When `true`, Brain issues a short-lived, read-only session token (`inspection_token`) and `brain_url` for the duration of each call. Cannot be combined with `mcp_tool`. | ## Resource limits (`limits`) [#resource-limits-limits] Every field is optional. Fields you omit keep the defaults below, so you can override only the limits you need. Brain validates the combined values at startup. | Field | Type | Default | Allowed range | Description | | --- | --- | ---: | --- | --- | | `active` | `integer` | `16` | `>= 1` | Maximum concurrent running turns and resident native session attachments across the server. Hosts may share several attachments. | | `loaded` | `integer` | `1024` | `>= active` | Maximum sessions held in memory at once. Idle unreferenced sessions are evicted when full. | | `queue` | `integer` | `16` | `1`–`1024` | Maximum queued `user.message` inputs per session. | | `input_bytes` | `integer` | `65536` | `1`–`1048576` (1 MiB) | Maximum byte length of `user.message` `text` or session `instructions`. | | `session_bytes` | `integer` | `1073741824` | `>= 16777216` (16 MiB) | Maximum local event log (`events.jsonl`) and checkpoint artifact size per session (default 1 GiB). | | `subscribers` | `integer` | `128` | `>= 1` | Maximum concurrent SSE connections across the server. | | `subscriber_events` | `integer` | `128` | `>= 1` | Maximum buffered live events per SSE subscriber before Brain disconnects the slow client. | | `subscriber_bytes` | `integer` | `2097152` | `>= 4096` | Maximum buffered live event bytes per SSE subscriber before disconnect (default 2 MiB; each event accounts for `len(data) + 256` bytes). | ## External persistence (`persistence`) [#external-persistence-persistence] Configure `persistence` to upload immutable 64 KiB transcript blocks and native checkpoints in the background. See [Persistence & recovery](/brain/docs/guides/persistence) for provider setup and handoff steps. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `provider` | `string` | Yes | — | Storage backend: `"filesystem"`, `"s3"`, `"r2"`, or `"gcs"`. | | `directory` | `string` | When `filesystem` | `""` | Target directory on disk for `"filesystem"` checkpoints. | | `bucket` | `string` | When `s3`, `r2`, or `gcs` | `""` | Object storage bucket name. | | `region` | `string` | No | `"auto"` for `r2` | AWS/S3 region. Rejected when `provider` is `"gcs"`. | | `endpoint` | `string` | When `r2` | `""` | Custom `http` or `https` S3/R2 endpoint URL (enables path-style addressing). Rejected when `provider` is `"gcs"`. | | `prefix` | `string` | No | `""` | Optional relative object key prefix (must not start with `/`, contain `\`, or traverse `..`). | | `access_key_env` | `string` | When `r2` | `""` | Environment variable holding the S3/R2 access key ID. Optional for `"s3"` (which falls back to the default AWS credential chain); rejected for `"gcs"`. Both `access_key_env` and `secret_key_env` must be set together. | | `secret_key_env` | `string` | When `r2` | `""` | Environment variable holding the S3/R2 secret access key. Rejected for `"gcs"`. | | `postgres_env` | `string` | No | `""` | Optional environment variable containing a PostgreSQL connection string (`postgres://...`) for cross-instance session discovery. Must be non-empty at startup when specified. | | `interval_seconds` | `integer` | No | `5` | Background sync scan interval in seconds (`1`–`3600`). | | `queue` | `integer` | No | `128` | Maximum pending dirty sessions in the background sync queue (`1`–`10000`). When full, Brain rejects new writes with `429 overloaded` and marks `/health/ready` as `503`. | # Errors (/brain/docs/reference/errors) Brain separates failures into three distinct error layers so your application can tell whether an HTTP request was rejected, a tool call failed during a turn, or the turn itself stopped: - **Client request errors (`4xx` / `500` HTTP responses):** Reject bad inputs, missing permissions, or capacity limits without breaking a healthy session. - **Tool errors (`tool.result` events):** Tool failures become tool results (`result.is_error: true`) so the model can inspect the error and decide how to proceed. - **Execution errors (`session.error` events):** Brain errors and model provider failures stop the affected execution and move the session to `interrupted` or `failed`. ## HTTP error shape [#http-error-shape] Every failed HTTP request returns a JSON error envelope containing `error.code`, `error.message`, and `request_id`: ```json { "error": { "code": "conflict", "message": "resume the interrupted session before sending input" }, "request_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90" } ``` In the TypeScript and Python SDKs, non-2xx HTTP responses raise `BrainError` with `status`, `code`, `message`, and `requestId` / `request_id`. ## HTTP status codes [#http-status-codes] | HTTP status | `error.code` | Meaning | What to do | | --- | --- | --- | --- | | `400` | `invalid_request` | Malformed JSON, unknown fields, trailing data, invalid session config, invalid pagination `limit`, missing/invalid checkpoint reference, or unconfigured model provider. | Fix the request body or query parameters before retrying. | | `401` | `forbidden` | Missing or unrecognized `Authorization: Bearer ` header (also sets `WWW-Authenticate: Bearer`). | Supply a valid bearer token configured in `tokens`. | | `403` | `forbidden` | Token is `read_only`, lacks `manage: true` for a management route, is scoped to a different session, or requests a tool not authorized for the tenant. | Use a token with the required tenant and management permissions. | | `404` | `not_found` | Unknown route, unknown session ID, session belongs to another tenant, or external checkpoint was not found. | Verify the URL path, session ID, checkpoint ID, and tenant token. | | `409` | `conflict` | Session is in the wrong state (for example, `interrupted`, `failed`, `closed`, or actively running during `restore`), or an idempotency `key` was reused with different input. | Call `POST /sessions/{id}/resume` before sending input, wait for active work to settle before restoring, or use a new idempotency `key`. | | `409` | `cursor_unavailable` | The `after` or `Last-Event-ID` cursor is malformed, exceeds 512 bytes, or points to an unsynced event tail lost after a checkpoint restore. | Reload retained event history from the beginning (`after=""`) to resync client state. | | `413` | `invalid_request` | Request body exceeds `limits.input_bytes + 16 KiB`. | Reduce the message `text` or `instructions` length, or increase `limits.input_bytes`. | | `422` | `unsupported` | Requested harness is not configured, `delivery` is the reserved value `"steer"`, or `flush`/`sync`/`restore` was called without `persistence` configured. | Use a configured harness, `"queue"` delivery, or enable `persistence` in the host config. | | `429` | `overloaded` | Session input queue (`limits.queue`), session log size (`limits.session_bytes`), loaded sessions (`limits.loaded`), SSE subscribers (`limits.subscribers`), or persistence sync queue (`persistence.queue`) is full, or the server is draining. | Wait for the `Retry-After: 1` header window (or flush external storage if the sync backlog is full) and retry. | | `500` | `brain_error` | Unexpected local storage, runner, or internal server failure (`message` is `"internal server error"` unless a typed internal error is returned). | Inspect the server's structured JSON logs using `request_id`. | ## Request IDs [#request-ids] Every HTTP response includes an `X-Request-ID` header (a 32-character hex ID). When a request fails, the same value is returned in the JSON body's `request_id` field, and `500` responses log `"request failed"` with `request_id` to `stderr`. Include `request_id` whenever diagnosing failed requests. ## Retries and `Retry-After` [#retries-and-retry-after] - **HTTP client requests:** Neither the TypeScript SDK nor the Python SDK retries failed HTTP requests automatically. When Brain returns `429 overloaded`, it sets the `Retry-After: 1` response header. When retrying `POST /sessions/{id}/events`, pass the same idempotency `key` on your `user.message` so duplicate submissions return the originally recorded event instead of queueing a second turn. - **Tool dispatches:** Brain never retries tool calls automatically. A transient network failure or timeout on a tool call is reported to the model so the harness or tool service can decide whether retrying is safe. ## Tool errors (`tool.result`) [#tool-errors-toolresult] Tool errors do not fail the HTTP request or stop the turn. When a tool call is denied, fails input/output schema validation, returns a non-2xx HTTP status, outputs invalid JSON, exceeds `max_output_bytes`, or times out, Brain records a `tool.result` event with `result.is_error: true` and passes the error text back to the harness: ```json { "call_id": "CALL_ID", "result": { "content": [{ "type": "text", "text": "tool transport failed; remote outcome may be unknown" }], "is_error": true, "unknown_outcome": true } } ``` - **`unknown_outcome: false` (omitted):** The failure happened before the tool service received the request (for example, unauthorized tool, schema validation error, missing local credential, or MCP connection failure before dispatch). No remote side effect occurred. - **`unknown_outcome: true`:** The failure happened after Brain sent the request (for example, network drop, timeout, HTTP 5xx, oversized response, or broken SSE stream). The remote side effect may have already happened. ## Session errors (`session.error`) [#session-errors-sessionerror] When an execution failure stops a turn (or cancels a queued message), Brain emits a retained `session.error` event with `input_id` set to the affected `user.message` `id`, followed by a `session.status` event: | `data.code` | Resulting session status | When it fires | | --- | --- | --- | | `provider_error` | `interrupted` | The model provider rejected the request or failed mid-stream. | | `conflict` | `interrupted` or `closed` | The active turn was interrupted (`user.interrupt`, session close, or server shutdown), or a queued `user.message` was cancelled before it could run (`"queued input interrupted before execution"`). | | `brain_error` (or other internal codes) | `failed` | The harness process crashed, violated the stdio framing protocol, selected an unsupported model or reasoning effort, or local disk writes failed. | To continue a session after it enters `interrupted` or `failed`, call `POST /sessions/{id}/resume` (`brain.resume(id)`) to reset the session to `idle` at its last completed checkpoint before sending new input. # Events & streaming (/brain/docs/reference/events) Every turn in a session produces a sequence of typed events. Brain appends retained events to the session's local log (`events.jsonl`) and broadcasts both retained and live-only events over server-sent events (SSE). ## Event envelope [#event-envelope] Every event returned by `POST /sessions/{id}/events` or `GET /sessions/{id}/events` shares the same outer JSON envelope: ```json { "id": "8f14e45fceea167a5a36dedd4bea2543", "type": "assistant.message", "time": "2026-10-15T12:00:00Z", "input_id": "c9f0f895fb98ab9159f51fd0297e236d", "data": { "text": "Append-only logs write new records at the end of a file without modifying existing bytes.", "usage": { "input": 42, "output": 19 }, "stop_reason": "stop" }, "cursor": "eyJvIjoxMjgsImkiOiI4ZjE0ZTQ1ZmNlZWExNjdhNWEzNmRlZGQ0YmVhMjU0MyJ9" } ``` | Field | Type | Present on | Description | | --- | --- | --- | --- | | `id` | `string` | All events | Unique 32-character hex identifier for this event. | | `type` | `string` | All events | Event type name in `{domain}.{action}` format (see [Event types](#event-types)). | | `time` | `string` | All events | UTC RFC 3339 timestamp when Brain recorded or published the event. | | `input_id` | `string` | Turn events | The `id` of the `user.message` event that triggered this event. Omitted on `user.message` itself and on session-level status changes (such as `close`). | | `data` | `object` | All events | Type-specific payload (see [Event types](#event-types)). | | `cursor` | `string` | Retained events | Opaque position token encoding the byte offset and event ID in `events.jsonl`. Omitted on live-only events (`assistant.delta` and `tool.progress`). | Always pass the exact `cursor` string from a retained event (or `next_cursor` from a history page) as `after`. Passing an event `id` or a fabricated string returns `409 cursor_unavailable`. ## Event types [#event-types] Brain emits eight event types. Six are saved to disk (`events.jsonl`) and replayed on history/stream requests; two are live-only progress signals sent to connected SSE subscribers. | Type | Saved to history? | `data` fields | Description | | --- | --- | --- | --- | | `user.message` | Yes | `type` (`"user.message"`), `text`, `delivery` (`"queue"`), optional `key` | User message accepted by `POST /sessions/{id}/events`. | | `assistant.delta` | No (live only) | `text` | Incremental text chunk streamed while the model generates a response. | | `assistant.message` | Yes | `text`, plus `usage` and `stop_reason` from Pi | Complete assistant message emitted at the end of a model generation step. | | `tool.call` | Yes | `id`, `name`, `arguments` | Tool invocation requested by the model before Brain dispatches the HTTP, SSE, or MCP call. | | `tool.progress` | No (live only) | `call_id`, `data` | Intermediate JSON progress frame emitted by an SSE tool (`event: progress`) or MCP progress notification. | | `tool.result` | Yes | `call_id`, `result` (`content[]`, optional `is_error`, optional `unknown_outcome`) | Final outcome of a tool call returned to the harness. Each item in `content` has `type: "text"` (`text`) or `type: "image"` (`data`, `mime_type`). | | `session.status` | Yes | `status` (`"running"`, `"idle"`, `"interrupted"`, `"failed"`, or `"closed"`) | Lifecycle transition for the session. When a turn finishes and the input queue is empty, Brain emits `status: "idle"` with `input_id` set to that turn's `user.message` `id`. | | `session.error` | Yes | `code` (`"provider_error"`, `"brain_error"`, `"conflict"`, etc.), `message` | Fatal turn error or cancelled queued input. See [Errors](/brain/docs/reference/errors#session-errors-sessionerror). | Each retained record in `events.jsonl` is capped at 4 MiB, and the total log file is capped at `limits.session_bytes` (default 1 GiB). ## SSE wire format [#sse-wire-format] Send `GET /sessions/{id}/events` with `Accept: text/event-stream` to open an SSE stream: ```text id: eyJvIjowLCJpIjoiYzlmMGY4OTVmYjk4YWI5MTU5ZjUxZmQwMjk3ZTIzNmQifQ event: user.message data: {"id":"c9f0f895fb98ab9159f51fd0297e236d","type":"user.message","time":"2026-10-15T12:00:00Z","data":{"type":"user.message","text":"Hello","delivery":"queue"},"cursor":"eyJvIjowLCJpIjoiYzlmMGY4OTVmYjk4YWI5MTU5ZjUxZmQwMjk3ZTIzNmQifQ"} event: assistant.delta data: {"id":"45c48cce2e2d7fbdea1afc51c7c6ad26","type":"assistant.delta","time":"2026-10-15T12:00:01Z","input_id":"c9f0f895fb98ab9159f51fd0297e236d","data":{"text":"Hello"}} id: eyJvIjoxMjgsImkiOiI4ZjE0ZTQ1ZmNlZWExNjdhNWEzNmRlZGQ0YmVhMjU0MyJ9 event: assistant.message data: {"id":"8f14e45fceea167a5a36dedd4bea2543","type":"assistant.message","time":"2026-10-15T12:00:01Z","input_id":"c9f0f895fb98ab9159f51fd0297e236d","data":{"text":"Hello"},"cursor":"eyJvIjoxMjgsImkiOiI4ZjE0ZTQ1ZmNlZWExNjdhNWEzNmRlZGQ0YmVhMjU0MyJ9"} : keepalive ``` - **Framing:** Every SSE frame includes `event: ` and `data: `, followed by a blank line (`\n\n`). - **Event IDs:** Retained events include `id: ` before `event:`. Live-only events (`assistant.delta` and `tool.progress`) omit the `id:` line so standard `EventSource` clients never overwrite their last valid reconnection cursor. - **Keepalives and deadlines:** Brain writes `: keepalive\n\n` every 15 seconds while idle and applies a 15-second write deadline per frame or keepalive. Keepalives never touch model state or trigger LLM calls. ## Reconnecting with cursors [#reconnecting-with-cursors] When subscribing over SSE, Brain captures the current log boundary under lock, replays all retained events after your cursor in batches of 100, and then switches to live events without gaps or duplicates. 1. Record `event.cursor` whenever it is non-empty. 2. Reconnect with `GET /sessions/{id}/events?after=CURSOR` or the `Last-Event-ID: CURSOR` header (`?after=` takes precedence if both are sent; cursors longer than 512 bytes are rejected). 3. If the session was restored from an older external checkpoint and your cursor points into an unsynced tail that no longer exists, Brain responds with `409 cursor_unavailable`. Reset your local state and read from `after=""`. ## Stream lifecycle and backpressure [#stream-lifecycle-and-backpressure] - **Multi-turn streams:** SSE streams stay open across `idle` transitions and future turns until the client disconnects, the server shuts down, or the session is closed. - **Client disconnects:** Closing an SSE connection never interrupts a running turn on the server. - **Slow-consumer eviction:** Each SSE subscriber has a bounded in-memory buffer (`limits.subscriber_events`, default `128`, and `limits.subscriber_bytes`, default `2 MiB`). If a slow client falls behind either limit during live streaming, Brain closes that subscriber's connection without affecting the turn or other subscribers; the client can immediately reconnect with its last `cursor`. # Model catalog (/brain/docs/reference/model-catalog) Extracted from the pinned Pi `1.2.0` model registry (`runners/pi/scripts/model-catalog.mjs`). > **Note:** This list is an offline catalog inventory, **not live-provider qualification**. Brain authenticates each provider with the API key configured in `providers`. See [Models & providers](/brain/docs/reference/models) for provider configuration, reasoning effort, and custom endpoints. ## amazon-bedrock [#amazon-bedrock] | Model ID | Transport | Context window | | --- | --- | ---: | | `amazon-bedrock/amazon.nova-2-lite-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/amazon.nova-lite-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/amazon.nova-micro-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/amazon.nova-pro-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/anthropic.claude-fable-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-fable-5-1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/anthropic.claude-haiku-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-opus-4-1-20250805-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/anthropic.claude-opus-4-5-20251101-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/anthropic.claude-opus-4-6-v1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-opus-4-7` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-opus-4-8` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-opus-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/anthropic.claude-sonnet-4-6` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/anthropic.claude-sonnet-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/apac.amazon.nova-lite-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/apac.amazon.nova-micro-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/apac.amazon.nova-pro-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/apac.anthropic.claude-sonnet-4-20250514-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/au.anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/au.anthropic.claude-haiku-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-opus-4-6-v1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-opus-4-7` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-opus-4-8` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-opus-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-opus-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-sonnet-4-5-20250929-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/au.anthropic.claude-sonnet-4-6` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/au.anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/ca.amazon.nova-lite-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/deepseek.v3-v1:0` | `bedrock-converse-stream` | 163840 | | `amazon-bedrock/deepseek.v3.2` | `bedrock-converse-stream` | 163840 | | `amazon-bedrock/eu.amazon.nova-2-lite-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.amazon.nova-lite-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/eu.amazon.nova-micro-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/eu.amazon.nova-pro-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/eu.anthropic.claude-fable-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/eu.anthropic.claude-haiku-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-opus-4-5-20251101-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/eu.anthropic.claude-opus-4-6-v1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-opus-4-7` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-opus-4-8` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-opus-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-opus-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-sonnet-4-20250514-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/eu.anthropic.claude-sonnet-4-5-20250929-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/eu.anthropic.claude-sonnet-4-6` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.anthropic.claude-sonnet-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/eu.mistral.pixtral-large-2502-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/global.amazon.nova-2-lite-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-fable-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-fable-5-1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/global.anthropic.claude-haiku-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-opus-4-5-20251101-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/global.anthropic.claude-opus-4-6-v1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-opus-4-7` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-opus-4-8` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-opus-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-opus-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-sonnet-4-20250514-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/global.anthropic.claude-sonnet-4-6` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.anthropic.claude-sonnet-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/global.moonshotai.kimi-k3` | `bedrock-converse-stream` | 1048576 | | `amazon-bedrock/global.openai.gpt-5.6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.openai.gpt-5.6-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.openai.gpt-5.6-terra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.openai.gpt-6-astra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.openai.gpt-6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.openai.gpt-6-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.openai.gpt-6.1-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/global.xai.grok-4.6` | `bedrock-converse-stream` | 500000 | | `amazon-bedrock/global.xai.grok-4.7` | `bedrock-converse-stream` | 500000 | | `amazon-bedrock/global.zai.glm-5.3` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/google.gemma-4-26b-a4b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/google.gemma-4-31b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/google.gemma-4-e2b` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/in.anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/in.anthropic.claude-opus-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/in.anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/in.openai.gpt-5.6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/in.openai.gpt-5.6-terra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/jp.amazon.nova-2-lite-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/jp.anthropic.claude-haiku-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-opus-4-7` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-opus-4-8` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-opus-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-opus-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-sonnet-4-5-20250929-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/jp.anthropic.claude-sonnet-4-6` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/jp.anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/meta.llama3-1-70b-instruct-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/meta.llama3-1-8b-instruct-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/meta.llama3-3-70b-instruct-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/meta.llama4-maverick-17b-instruct-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/meta.llama4-scout-17b-instruct-v1:0` | `bedrock-converse-stream` | 10000000 | | `amazon-bedrock/minimax.minimax-m2` | `bedrock-converse-stream` | 204608 | | `amazon-bedrock/minimax.minimax-m2.1` | `bedrock-converse-stream` | 196608 | | `amazon-bedrock/minimax.minimax-m2.5` | `bedrock-converse-stream` | 196608 | | `amazon-bedrock/mistral.devstral-2-123b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/mistral.magistral-small-2509` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/mistral.ministral-3-14b-instruct` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/mistral.ministral-3-3b-instruct` | `bedrock-converse-stream` | 256000 | | `amazon-bedrock/mistral.ministral-3-8b-instruct` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/mistral.mistral-large-3-675b-instruct` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/mistral.pixtral-large-2502-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/mistral.voxtral-mini-3b-2507` | `bedrock-converse-stream` | 32768 | | `amazon-bedrock/mistral.voxtral-small-24b-2507` | `bedrock-converse-stream` | 32768 | | `amazon-bedrock/moonshot.kimi-k2-thinking` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/moonshotai.kimi-k2.5` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/nvidia.nemotron-nano-12b-v2` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/nvidia.nemotron-nano-3-30b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/nvidia.nemotron-nano-9b-v2` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/nvidia.nemotron-super-3-120b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/openai.gpt-5.4` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/openai.gpt-5.5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/openai.gpt-5.6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-5.6-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-5.6-terra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-6-astra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-6-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-6.1-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/openai.gpt-oss-120b` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/openai.gpt-oss-120b-1:0` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/openai.gpt-oss-20b` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/openai.gpt-oss-20b-1:0` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/openai.gpt-oss-safeguard-120b` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/openai.gpt-oss-safeguard-20b` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/qwen.qwen3-235b-a22b-2507-v1:0` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/qwen.qwen3-32b-v1:0` | `bedrock-converse-stream` | 32768 | | `amazon-bedrock/qwen.qwen3-coder-30b-a3b-v1:0` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/qwen.qwen3-coder-480b-a35b-v1:0` | `bedrock-converse-stream` | 131072 | | `amazon-bedrock/qwen.qwen3-coder-next` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/qwen.qwen3-next-80b-a3b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/qwen.qwen3-vl-235b-a22b` | `bedrock-converse-stream` | 262144 | | `amazon-bedrock/us-gov.openai.gpt-oss-120b-1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us-gov.openai.gpt-oss-20b-1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us.amazon.nova-2-lite-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.amazon.nova-lite-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/us.amazon.nova-micro-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us.amazon.nova-premier-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.amazon.nova-pro-v1:0` | `bedrock-converse-stream` | 300000 | | `amazon-bedrock/us.anthropic.claude-fable-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-fable-5-1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/us.anthropic.claude-haiku-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-opus-4-1-20250805-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/us.anthropic.claude-opus-4-5-20251101-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/us.anthropic.claude-opus-4-6-v1` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-opus-4-7` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-opus-4-8` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-opus-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-opus-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0` | `bedrock-converse-stream` | 200000 | | `amazon-bedrock/us.anthropic.claude-sonnet-4-6` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-sonnet-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.anthropic.claude-sonnet-5-5` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.meta.llama3-1-70b-instruct-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us.meta.llama3-1-8b-instruct-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us.meta.llama3-3-70b-instruct-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us.meta.llama4-maverick-17b-instruct-v1:0` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/us.meta.llama4-scout-17b-instruct-v1:0` | `bedrock-converse-stream` | 10000000 | | `amazon-bedrock/us.mistral.pixtral-large-2502-v1:0` | `bedrock-converse-stream` | 128000 | | `amazon-bedrock/us.moonshotai.kimi-k3` | `bedrock-converse-stream` | 1048576 | | `amazon-bedrock/us.openai.gpt-5.6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.openai.gpt-5.6-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.openai.gpt-5.6-terra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.openai.gpt-6-astra` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.openai.gpt-6-luna` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.openai.gpt-6-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.openai.gpt-6.1-sol` | `bedrock-converse-stream` | 1050000 | | `amazon-bedrock/us.writer.palmyra-x4-v1:0` | `bedrock-converse-stream` | 122880 | | `amazon-bedrock/us.writer.palmyra-x5-v1:0` | `bedrock-converse-stream` | 1040000 | | `amazon-bedrock/us.xai.grok-4.6` | `bedrock-converse-stream` | 500000 | | `amazon-bedrock/us.xai.grok-4.7` | `bedrock-converse-stream` | 500000 | | `amazon-bedrock/us.zai.glm-5.3` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/writer.palmyra-x4-v1:0` | `bedrock-converse-stream` | 122880 | | `amazon-bedrock/writer.palmyra-x5-v1:0` | `bedrock-converse-stream` | 1040000 | | `amazon-bedrock/xai.grok-4.3` | `bedrock-converse-stream` | 1000000 | | `amazon-bedrock/xai.grok-4.6` | `bedrock-converse-stream` | 500000 | | `amazon-bedrock/zai.glm-4.7` | `bedrock-converse-stream` | 202752 | | `amazon-bedrock/zai.glm-4.7-flash` | `bedrock-converse-stream` | 202752 | | `amazon-bedrock/zai.glm-5` | `bedrock-converse-stream` | 202752 | ## ant-ling [#ant-ling] | Model ID | Transport | Context window | | --- | --- | ---: | | `ant-ling/Ling-2.6-1T` | `openai-completions` | 262144 | | `ant-ling/Ling-2.6-flash` | `openai-completions` | 262144 | | `ant-ling/Ring-2.6-1T` | `openai-completions` | 262144 | ## anthropic [#anthropic] | Model ID | Transport | Context window | | --- | --- | ---: | | `anthropic/claude-fable-5` | `anthropic-messages` | 1000000 | | `anthropic/claude-fable-5-1` | `anthropic-messages` | 1000000 | | `anthropic/claude-haiku-4-5` | `anthropic-messages` | 200000 | | `anthropic/claude-haiku-4-5-20251001` | `anthropic-messages` | 200000 | | `anthropic/claude-haiku-5-5` | `anthropic-messages` | 1000000 | | `anthropic/claude-opus-4-5` | `anthropic-messages` | 200000 | | `anthropic/claude-opus-4-5-20251101` | `anthropic-messages` | 200000 | | `anthropic/claude-opus-4-6` | `anthropic-messages` | 1000000 | | `anthropic/claude-opus-4-7` | `anthropic-messages` | 1000000 | | `anthropic/claude-opus-4-8` | `anthropic-messages` | 1000000 | | `anthropic/claude-opus-5` | `anthropic-messages` | 1000000 | | `anthropic/claude-opus-5-5` | `anthropic-messages` | 1000000 | | `anthropic/claude-sonnet-4-5` | `anthropic-messages` | 200000 | | `anthropic/claude-sonnet-4-5-20250929` | `anthropic-messages` | 200000 | | `anthropic/claude-sonnet-4-6` | `anthropic-messages` | 1000000 | | `anthropic/claude-sonnet-5` | `anthropic-messages` | 1000000 | | `anthropic/claude-sonnet-5-5` | `anthropic-messages` | 1000000 | ## azure [#azure] | Model ID | Transport | Context window | | --- | --- | ---: | | `azure/deepseek-v4-pro` | `openai-completions` | 1000000 | | `azure/gpt-4` | `azure-openai-responses` | 8192 | | `azure/gpt-4-turbo` | `azure-openai-responses` | 128000 | | `azure/gpt-4.1` | `azure-openai-responses` | 1047576 | | `azure/gpt-4.1-mini` | `azure-openai-responses` | 1047576 | | `azure/gpt-4.1-nano` | `azure-openai-responses` | 1047576 | | `azure/gpt-4o` | `azure-openai-responses` | 128000 | | `azure/gpt-4o-2024-05-13` | `azure-openai-responses` | 128000 | | `azure/gpt-4o-2024-08-06` | `azure-openai-responses` | 128000 | | `azure/gpt-4o-2024-11-20` | `azure-openai-responses` | 128000 | | `azure/gpt-4o-mini` | `azure-openai-responses` | 128000 | | `azure/gpt-5` | `azure-openai-responses` | 400000 | | `azure/gpt-5-chat-latest` | `azure-openai-responses` | 128000 | | `azure/gpt-5-mini` | `azure-openai-responses` | 400000 | | `azure/gpt-5-nano` | `azure-openai-responses` | 400000 | | `azure/gpt-5-pro` | `azure-openai-responses` | 400000 | | `azure/gpt-5.1` | `azure-openai-responses` | 400000 | | `azure/gpt-5.2` | `azure-openai-responses` | 400000 | | `azure/gpt-5.2-chat-latest` | `azure-openai-responses` | 128000 | | `azure/gpt-5.2-pro` | `azure-openai-responses` | 400000 | | `azure/gpt-5.3-chat-latest` | `azure-openai-responses` | 128000 | | `azure/gpt-5.3-codex` | `azure-openai-responses` | 400000 | | `azure/gpt-5.3-codex-spark` | `azure-openai-responses` | 128000 | | `azure/gpt-5.4` | `azure-openai-responses` | 1050000 | | `azure/gpt-5.4-mini` | `azure-openai-responses` | 400000 | | `azure/gpt-5.4-nano` | `azure-openai-responses` | 400000 | | `azure/gpt-5.4-pro` | `azure-openai-responses` | 1050000 | | `azure/gpt-5.5` | `azure-openai-responses` | 1050000 | | `azure/gpt-5.5-pro` | `azure-openai-responses` | 1050000 | | `azure/gpt-5.6-luna` | `azure-openai-responses` | 1050000 | | `azure/gpt-5.6-sol` | `azure-openai-responses` | 1050000 | | `azure/gpt-5.6-terra` | `azure-openai-responses` | 1050000 | | `azure/gpt-6-astra` | `azure-openai-responses` | 272000 | | `azure/gpt-6-luna` | `azure-openai-responses` | 272000 | | `azure/gpt-6-sol` | `azure-openai-responses` | 272000 | | `azure/gpt-6.1-sol` | `azure-openai-responses` | 272000 | | `azure/gpt-daybreak-blue-latest` | `azure-openai-responses` | 1050000 | | `azure/gpt-daybreak-red-latest` | `azure-openai-responses` | 400000 | | `azure/gpt-realtime-2.1` | `azure-openai-responses` | 128000 | | `azure/o1` | `azure-openai-responses` | 200000 | | `azure/o1-pro` | `azure-openai-responses` | 200000 | | `azure/o3` | `azure-openai-responses` | 200000 | | `azure/o3-mini` | `azure-openai-responses` | 200000 | | `azure/o3-pro` | `azure-openai-responses` | 200000 | | `azure/o4-mini` | `azure-openai-responses` | 200000 | ## baseten [#baseten] | Model ID | Transport | Context window | | --- | --- | ---: | | `baseten/deepseek-ai/DeepSeek-V4-Flash-0731` | `openai-completions` | 1048576 | | `baseten/deepseek-ai/DeepSeek-V4-Pro` | `openai-completions` | 1048576 | | `baseten/deepseek-ai/DeepSeek-V4-Pro-0813` | `openai-completions` | 1048576 | | `baseten/deepseek-ai/DeepSeek-V4.1-Flash` | `openai-completions` | 1048576 | | `baseten/deepseek-ai/DeepSeek-V4.1-Flash-Fast` | `openai-completions` | 1048576 | | `baseten/moonshotai/Kimi-K2.5` | `openai-completions` | 262000 | | `baseten/moonshotai/Kimi-K2.6` | `openai-completions` | 262000 | | `baseten/moonshotai/Kimi-K2.7-Code` | `openai-completions` | 262000 | | `baseten/moonshotai/Kimi-K3` | `openai-completions` | 1048576 | | `baseten/nvidia/Nemotron-120B-A12B` | `openai-completions` | 202800 | | `baseten/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B` | `openai-completions` | 202800 | | `baseten/openai/gpt-oss-120b` | `openai-completions` | 128072 | | `baseten/thinkingmachines/inkling` | `openai-completions` | 1048576 | | `baseten/thinkingmachines/inkling-small` | `openai-completions` | 1048576 | | `baseten/zai-org/GLM-4.7` | `openai-completions` | 200000 | | `baseten/zai-org/GLM-5` | `openai-completions` | 202800 | | `baseten/zai-org/GLM-5.1` | `openai-completions` | 202800 | | `baseten/zai-org/GLM-5.2` | `openai-completions` | 1048576 | | `baseten/zai-org/GLM-5.2-Fast` | `openai-completions` | 1048576 | | `baseten/zai-org/GLM-5.3` | `openai-completions` | 1048576 | | `baseten/zai-org/GLM-5.3-Fast` | `openai-completions` | 1048576 | | `baseten/zai-org/GLM-5.3-Flash` | `openai-completions` | 1048576 | ## cerebras [#cerebras] | Model ID | Transport | Context window | | --- | --- | ---: | | `cerebras/gpt-oss-120b` | `openai-completions` | 131072 | | `cerebras/qwen-3.8-27b` | `openai-completions` | 131072 | ## cloudflare-ai-gateway [#cloudflare-ai-gateway] | Model ID | Transport | Context window | | --- | --- | ---: | | `cloudflare-ai-gateway/claude-fable-5` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-fable-5-1` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-haiku-4-5` | `anthropic-messages` | 200000 | | `cloudflare-ai-gateway/claude-haiku-5-5` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-opus-4-5` | `anthropic-messages` | 200000 | | `cloudflare-ai-gateway/claude-opus-4-6` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-opus-4-7` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-opus-4-8` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-opus-5` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-opus-5-5` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-sonnet-4-5` | `anthropic-messages` | 200000 | | `cloudflare-ai-gateway/claude-sonnet-4-6` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-sonnet-5` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/claude-sonnet-5-5` | `anthropic-messages` | 1000000 | | `cloudflare-ai-gateway/gpt-4.1` | `openai-responses` | 1047576 | | `cloudflare-ai-gateway/gpt-4.1-mini` | `openai-responses` | 1047576 | | `cloudflare-ai-gateway/gpt-4.1-nano` | `openai-responses` | 1000000 | | `cloudflare-ai-gateway/gpt-4o` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-4o-mini` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5-mini` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5-nano` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5.1` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5.4` | `openai-responses` | 1000000 | | `cloudflare-ai-gateway/gpt-5.4-mini` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5.4-nano` | `openai-responses` | 128000 | | `cloudflare-ai-gateway/gpt-5.4-pro` | `openai-responses` | 1000000 | | `cloudflare-ai-gateway/gpt-5.5` | `openai-responses` | 1000000 | | `cloudflare-ai-gateway/gpt-5.5-pro` | `openai-responses` | 1000000 | | `cloudflare-ai-gateway/gpt-5.6-luna` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/gpt-5.6-sol` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/gpt-5.6-terra` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/gpt-6-astra` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/gpt-6-luna` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/gpt-6-sol` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/gpt-6.1-sol` | `openai-responses` | 1050000 | | `cloudflare-ai-gateway/o3` | `openai-responses` | 200000 | | `cloudflare-ai-gateway/o3-mini` | `openai-responses` | 200000 | | `cloudflare-ai-gateway/o4-mini` | `openai-responses` | 200000 | | `cloudflare-ai-gateway/workers-ai/@cf/deepseek-ai/deepseek-v4-flash-0731` | `openai-completions` | 1048576 | | `cloudflare-ai-gateway/workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813` | `openai-completions` | 1048576 | | `cloudflare-ai-gateway/workers-ai/@cf/google/gemma-4-26b-a4b-it` | `openai-completions` | 256000 | | `cloudflare-ai-gateway/workers-ai/@cf/ibm-granite/granite-4.0-h-micro` | `openai-completions` | 131000 | | `cloudflare-ai-gateway/workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast` | `openai-completions` | 24000 | | `cloudflare-ai-gateway/workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct` | `openai-completions` | 131000 | | `cloudflare-ai-gateway/workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct` | `openai-completions` | 128000 | | `cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6` | `openai-completions` | 262144 | | `cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.7-code` | `openai-completions` | 262144 | | `cloudflare-ai-gateway/workers-ai/@cf/nvidia/nemotron-3-120b-a12b` | `openai-completions` | 256000 | | `cloudflare-ai-gateway/workers-ai/@cf/openai/gpt-oss-120b` | `openai-completions` | 128000 | | `cloudflare-ai-gateway/workers-ai/@cf/openai/gpt-oss-20b` | `openai-completions` | 128000 | | `cloudflare-ai-gateway/workers-ai/@cf/qwen/qwen3-30b-a3b-fp8` | `openai-completions` | 32768 | | `cloudflare-ai-gateway/workers-ai/@cf/qwen/qwen3.8-27b` | `openai-completions` | 262144 | | `cloudflare-ai-gateway/workers-ai/@cf/zai-org/glm-4.7-flash` | `openai-completions` | 131072 | | `cloudflare-ai-gateway/workers-ai/@cf/zai-org/glm-5.2` | `openai-completions` | 262144 | | `cloudflare-ai-gateway/workers-ai/@cf/zai-org/glm-5.3` | `openai-completions` | 1048576 | | `cloudflare-ai-gateway/workers-ai/@cf/zai-org/glm-5.3-flash` | `openai-completions` | 1048576 | ## cloudflare-workers-ai [#cloudflare-workers-ai] | Model ID | Transport | Context window | | --- | --- | ---: | | `cloudflare-workers-ai/@cf/deepseek-ai/deepseek-v4-flash-0731` | `openai-completions` | 1048576 | | `cloudflare-workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813` | `openai-completions` | 1048576 | | `cloudflare-workers-ai/@cf/google/gemma-4-26b-a4b-it` | `openai-completions` | 256000 | | `cloudflare-workers-ai/@cf/ibm-granite/granite-4.0-h-micro` | `openai-completions` | 131000 | | `cloudflare-workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast` | `openai-completions` | 24000 | | `cloudflare-workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct` | `openai-completions` | 131000 | | `cloudflare-workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct` | `openai-completions` | 128000 | | `cloudflare-workers-ai/@cf/moonshotai/kimi-k2.6` | `openai-completions` | 262144 | | `cloudflare-workers-ai/@cf/moonshotai/kimi-k2.7-code` | `openai-completions` | 262144 | | `cloudflare-workers-ai/@cf/nvidia/nemotron-3-120b-a12b` | `openai-completions` | 256000 | | `cloudflare-workers-ai/@cf/openai/gpt-oss-120b` | `openai-completions` | 128000 | | `cloudflare-workers-ai/@cf/openai/gpt-oss-20b` | `openai-completions` | 128000 | | `cloudflare-workers-ai/@cf/qwen/qwen3-30b-a3b-fp8` | `openai-completions` | 32768 | | `cloudflare-workers-ai/@cf/qwen/qwen3.8-27b` | `openai-completions` | 262144 | | `cloudflare-workers-ai/@cf/zai-org/glm-4.7-flash` | `openai-completions` | 131072 | | `cloudflare-workers-ai/@cf/zai-org/glm-5.2` | `openai-completions` | 262144 | | `cloudflare-workers-ai/@cf/zai-org/glm-5.3` | `openai-completions` | 1048576 | | `cloudflare-workers-ai/@cf/zai-org/glm-5.3-flash` | `openai-completions` | 1048576 | ## deepseek [#deepseek] | Model ID | Transport | Context window | | --- | --- | ---: | | `deepseek/deepseek-flash` | `openai-completions` | 1000000 | | `deepseek/deepseek-v4-pro` | `openai-completions` | 1000000 | ## fireworks [#fireworks] | Model ID | Transport | Context window | | --- | --- | ---: | | `fireworks/accounts/fireworks/models/deepseek-v4p1-flash` | `anthropic-messages` | 1000000 | | `fireworks/accounts/fireworks/models/ember-1` | `anthropic-messages` | 1048576 | | `fireworks/accounts/fireworks/models/glm-5p3` | `openai-completions` | 1048573 | | `fireworks/accounts/fireworks/models/glm-5p3-flash` | `openai-completions` | 1048573 | | `fireworks/accounts/fireworks/models/gpt-oss-120b` | `anthropic-messages` | 131072 | | `fireworks/accounts/fireworks/models/inkling` | `anthropic-messages` | 1048576 | | `fireworks/accounts/fireworks/models/kimi-k3` | `openai-completions` | 1048576 | | `fireworks/accounts/fireworks/models/minimax-m3` | `anthropic-messages` | 512000 | | `fireworks/accounts/fireworks/models/nemotron-3-ultra-nvfp4` | `anthropic-messages` | 262144 | | `fireworks/accounts/fireworks/models/nemotron-lightning-3p5-30b-a3b` | `anthropic-messages` | 262144 | | `fireworks/accounts/fireworks/models/qwen3p8-2p4t-a95b` | `anthropic-messages` | 262144 | | `fireworks/accounts/fireworks/models/qwen3p8-max` | `anthropic-messages` | 262144 | | `fireworks/accounts/fireworks/routers/deepseek-flash-latest` | `anthropic-messages` | 1000000 | | `fireworks/accounts/fireworks/routers/glm-5p3-fast` | `openai-completions` | 1048572 | | `fireworks/accounts/fireworks/routers/glm-fast-latest` | `openai-completions` | 1048572 | | `fireworks/accounts/fireworks/routers/glm-flash-latest` | `openai-completions` | 1048573 | | `fireworks/accounts/fireworks/routers/glm-latest` | `openai-completions` | 1048573 | | `fireworks/accounts/fireworks/routers/kimi-fast-latest` | `anthropic-messages` | 1048576 | | `fireworks/accounts/fireworks/routers/kimi-k3-fast` | `openai-completions` | 1048576 | | `fireworks/accounts/fireworks/routers/kimi-latest` | `anthropic-messages` | 1048576 | | `fireworks/accounts/fireworks/routers/minimax-latest` | `anthropic-messages` | 512000 | | `fireworks/accounts/fireworks/routers/qwen-max-latest` | `anthropic-messages` | 262144 | ## github-copilot [#github-copilot] | Model ID | Transport | Context window | | --- | --- | ---: | | `github-copilot/claude-fable-5` | `anthropic-messages` | 1000000 | | `github-copilot/claude-fable-5.1` | `anthropic-messages` | 1000000 | | `github-copilot/claude-haiku-4.5` | `anthropic-messages` | 200000 | | `github-copilot/claude-haiku-5.5` | `anthropic-messages` | 1000000 | | `github-copilot/claude-opus-4.7` | `anthropic-messages` | 1000000 | | `github-copilot/claude-opus-4.8` | `anthropic-messages` | 1000000 | | `github-copilot/claude-opus-5` | `anthropic-messages` | 1000000 | | `github-copilot/claude-opus-5.5` | `anthropic-messages` | 1000000 | | `github-copilot/claude-sonnet-4.6` | `anthropic-messages` | 1000000 | | `github-copilot/claude-sonnet-5` | `anthropic-messages` | 1000000 | | `github-copilot/claude-sonnet-5.5` | `anthropic-messages` | 1000000 | | `github-copilot/gemini-3.5-flash` | `openai-completions` | 200000 | | `github-copilot/gemini-3.6-flash` | `openai-completions` | 1000000 | | `github-copilot/gemini-3.7-flash` | `openai-completions` | 1000000 | | `github-copilot/gemini-3.8-flash` | `openai-completions` | 1000000 | | `github-copilot/gpt-5-mini` | `openai-responses` | 264000 | | `github-copilot/gpt-5.3-codex` | `openai-responses` | 1000000 | | `github-copilot/gpt-5.4` | `openai-responses` | 1000000 | | `github-copilot/gpt-5.4-mini` | `openai-responses` | 400000 | | `github-copilot/gpt-5.4-nano` | `openai-responses` | 400000 | | `github-copilot/gpt-5.5` | `openai-responses` | 1000000 | | `github-copilot/gpt-5.6-luna` | `openai-responses` | 1050000 | | `github-copilot/gpt-5.6-sol` | `openai-responses` | 1050000 | | `github-copilot/gpt-5.6-terra` | `openai-responses` | 1050000 | | `github-copilot/gpt-6-astra` | `openai-responses` | 1000000 | | `github-copilot/gpt-6-luna` | `openai-responses` | 1000000 | | `github-copilot/gpt-6-sol` | `openai-responses` | 1000000 | | `github-copilot/gpt-6.1-sol` | `openai-responses` | 1050000 | | `github-copilot/grok-4.5` | `openai-responses` | 500000 | | `github-copilot/grok-4.6` | `openai-responses` | 500000 | | `github-copilot/grok-4.7` | `openai-responses` | 500000 | | `github-copilot/kimi-k2.7-code` | `openai-completions` | 256000 | | `github-copilot/kimi-k3` | `openai-completions` | 1048576 | | `github-copilot/mai-code-1-flash-picker` | `openai-responses` | 256000 | | `github-copilot/mai-code-1.1-flash` | `openai-responses` | 256000 | ## google-vertex [#google-vertex] | Model ID | Transport | Context window | | --- | --- | ---: | | `google-vertex/gemini-2.5-flash` | `google-vertex` | 1048576 | | `google-vertex/gemini-2.5-flash-lite` | `google-vertex` | 1048576 | | `google-vertex/gemini-2.5-pro` | `google-vertex` | 1048576 | | `google-vertex/gemini-3-flash-preview` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.1-flash-lite` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.1-flash-lite-image` | `google-vertex` | 65536 | | `google-vertex/gemini-3.1-pro-preview` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.1-pro-preview-customtools` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.5-flash` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.5-flash-lite` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.6-flash` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.7-flash` | `google-vertex` | 1048576 | | `google-vertex/gemini-3.8-flash` | `google-vertex` | 1048576 | ## google [#google] | Model ID | Transport | Context window | | --- | --- | ---: | | `google/deep-research-max-preview-04-2026` | `google-generative-ai` | 131072 | | `google/deep-research-preview-04-2026` | `google-generative-ai` | 131072 | | `google/gemini-2.5-computer-use-preview-10-2025` | `google-generative-ai` | 128000 | | `google/gemini-2.5-flash` | `google-generative-ai` | 1048576 | | `google/gemini-2.5-flash-lite` | `google-generative-ai` | 1048576 | | `google/gemini-2.5-pro` | `google-generative-ai` | 1048576 | | `google/gemini-3-flash-preview` | `google-generative-ai` | 1048576 | | `google/gemini-3.1-flash-lite` | `google-generative-ai` | 1048576 | | `google/gemini-3.1-flash-lite-image` | `google-generative-ai` | 65536 | | `google/gemini-3.1-flash-lite-preview` | `google-generative-ai` | 1048576 | | `google/gemini-3.1-flash-live-preview` | `google-generative-ai` | 131072 | | `google/gemini-3.1-pro-preview` | `google-generative-ai` | 1048576 | | `google/gemini-3.1-pro-preview-customtools` | `google-generative-ai` | 1048576 | | `google/gemini-3.5-flash` | `google-generative-ai` | 1048576 | | `google/gemini-3.5-flash-lite` | `google-generative-ai` | 1048576 | | `google/gemini-3.6-flash` | `google-generative-ai` | 1048576 | | `google/gemini-3.7-flash` | `google-generative-ai` | 1048576 | | `google/gemini-3.8-flash` | `google-generative-ai` | 1048576 | | `google/gemini-flash-latest` | `google-generative-ai` | 1048576 | | `google/gemini-flash-lite-latest` | `google-generative-ai` | 1048576 | | `google/gemma-4-26b-a4b-it` | `google-generative-ai` | 262144 | | `google/gemma-4-31b-it` | `google-generative-ai` | 262144 | ## groq [#groq] | Model ID | Transport | Context window | | --- | --- | ---: | | `groq/llama-3.1-8b-instant` | `openai-completions` | 131072 | | `groq/llama-3.3-70b-versatile` | `openai-completions` | 131072 | | `groq/openai/gpt-oss-120b` | `openai-completions` | 131072 | | `groq/openai/gpt-oss-20b` | `openai-completions` | 131072 | | `groq/openai/gpt-oss-safeguard-20b` | `openai-completions` | 131072 | | `groq/qwen/qwen3.6-27b` | `openai-completions` | 131072 | | `groq/qwen/qwen3.8-27b` | `openai-completions` | 131042 | ## huggingface [#huggingface] | Model ID | Transport | Context window | | --- | --- | ---: | | `huggingface/deepseek-ai/DeepSeek-R1` | `openai-completions` | 64000 | | `huggingface/deepseek-ai/DeepSeek-R1-0528` | `openai-completions` | 163840 | | `huggingface/deepseek-ai/DeepSeek-V3` | `openai-completions` | 64000 | | `huggingface/deepseek-ai/DeepSeek-V3-0324` | `openai-completions` | 163840 | | `huggingface/deepseek-ai/DeepSeek-V3.1` | `openai-completions` | 131072 | | `huggingface/deepseek-ai/DeepSeek-V3.2` | `openai-completions` | 163840 | | `huggingface/deepseek-ai/DeepSeek-V4-Flash` | `openai-completions` | 1048576 | | `huggingface/deepseek-ai/DeepSeek-V4-Flash-0731` | `openai-completions` | 1048576 | | `huggingface/deepseek-ai/DeepSeek-V4-Flash-Vision-Exp` | `openai-completions` | 1048576 | | `huggingface/deepseek-ai/DeepSeek-V4-Pro` | `openai-completions` | 1048576 | | `huggingface/deepseek-ai/DeepSeek-V4-Pro-0813` | `openai-completions` | 1000000 | | `huggingface/deepseek-ai/DeepSeek-V4.1-Flash` | `openai-completions` | 1048576 | | `huggingface/google/gemma-3-12b-it` | `openai-completions` | 131072 | | `huggingface/google/gemma-3-27b-it` | `openai-completions` | 131072 | | `huggingface/google/gemma-3-4b-it` | `openai-completions` | 131072 | | `huggingface/google/gemma-4-26B-A4B-it` | `openai-completions` | 262144 | | `huggingface/google/gemma-4-31B-it` | `openai-completions` | 262144 | | `huggingface/meta-llama/Llama-3.1-8B-Instruct` | `openai-completions` | 131072 | | `huggingface/meta-llama/Llama-3.3-70B-Instruct` | `openai-completions` | 131072 | | `huggingface/MiniMaxAI/MiniMax-M1-80k` | `openai-completions` | 1000000 | | `huggingface/MiniMaxAI/MiniMax-M2` | `openai-completions` | 204800 | | `huggingface/MiniMaxAI/MiniMax-M2.1` | `openai-completions` | 204800 | | `huggingface/MiniMaxAI/MiniMax-M2.5` | `openai-completions` | 204800 | | `huggingface/MiniMaxAI/MiniMax-M2.7` | `openai-completions` | 204800 | | `huggingface/MiniMaxAI/MiniMax-M3` | `openai-completions` | 524288 | | `huggingface/moonshotai/Kimi-K2-Instruct` | `openai-completions` | 131072 | | `huggingface/moonshotai/Kimi-K2-Instruct-0905` | `openai-completions` | 262144 | | `huggingface/moonshotai/Kimi-K2-Thinking` | `openai-completions` | 262144 | | `huggingface/moonshotai/Kimi-K2.5` | `openai-completions` | 262144 | | `huggingface/moonshotai/Kimi-K2.6` | `openai-completions` | 262144 | | `huggingface/moonshotai/Kimi-K2.7-Code` | `openai-completions` | 262144 | | `huggingface/moonshotai/Kimi-K3` | `openai-completions` | 1000000 | | `huggingface/openai/gpt-oss-120b` | `openai-completions` | 131072 | | `huggingface/openai/gpt-oss-20b` | `openai-completions` | 131072 | | `huggingface/Qwen/Qwen2.5-Coder-32B-Instruct` | `openai-completions` | 131072 | | `huggingface/Qwen/Qwen3-235B-A22B` | `openai-completions` | 40960 | | `huggingface/Qwen/Qwen3-235B-A22B-Instruct-2507` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-235B-A22B-Thinking-2507` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-30B-A3B` | `openai-completions` | 40960 | | `huggingface/Qwen/Qwen3-32B` | `openai-completions` | 131072 | | `huggingface/Qwen/Qwen3-Coder-30B-A3B-Instruct` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-Coder-480B-A35B-Instruct` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-Coder-Next` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-Next-80B-A3B-Instruct` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-Next-80B-A3B-Thinking` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3-VL-235B-A22B-Instruct` | `openai-completions` | 131072 | | `huggingface/Qwen/Qwen3-VL-235B-A22B-Thinking` | `openai-completions` | 131072 | | `huggingface/Qwen/Qwen3-VL-30B-A3B-Instruct` | `openai-completions` | 131072 | | `huggingface/Qwen/Qwen3.5-122B-A10B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.5-27B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.5-35B-A3B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.5-397B-A17B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.5-9B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.6-27B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.6-35B-A3B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.8-2.4T-A95B` | `openai-completions` | 262144 | | `huggingface/Qwen/Qwen3.8-27B` | `openai-completions` | 262144 | | `huggingface/stepfun-ai/Step-3.5-Flash` | `openai-completions` | 262144 | | `huggingface/stepfun-ai/Step-3.7-Flash` | `openai-completions` | 262144 | | `huggingface/tencent/Hy3` | `openai-completions` | 262144 | | `huggingface/tencent/Hy4-preview` | `openai-completions` | 1000000 | | `huggingface/thinkingmachines/Inkling` | `openai-completions` | 1048576 | | `huggingface/thinkingmachines/Inkling-Small` | `openai-completions` | 524288 | | `huggingface/XiaomiMiMo/MiMo-V2-Flash` | `openai-completions` | 262144 | | `huggingface/XiaomiMiMo/MiMo-V2.5` | `openai-completions` | 262144 | | `huggingface/XiaomiMiMo/MiMo-V2.5-Pro` | `openai-completions` | 1048576 | | `huggingface/zai-org/GLM-4.5` | `openai-completions` | 131072 | | `huggingface/zai-org/GLM-4.5-Air` | `openai-completions` | 131072 | | `huggingface/zai-org/GLM-4.5V` | `openai-completions` | 65536 | | `huggingface/zai-org/GLM-4.6` | `openai-completions` | 204800 | | `huggingface/zai-org/GLM-4.6V-Flash` | `openai-completions` | 131072 | | `huggingface/zai-org/GLM-4.7` | `openai-completions` | 204800 | | `huggingface/zai-org/GLM-4.7-Flash` | `openai-completions` | 200000 | | `huggingface/zai-org/GLM-5` | `openai-completions` | 202752 | | `huggingface/zai-org/GLM-5.1` | `openai-completions` | 202752 | | `huggingface/zai-org/GLM-5.2` | `openai-completions` | 262144 | | `huggingface/zai-org/GLM-5.3` | `openai-completions` | 1048576 | | `huggingface/zai-org/GLM-5.3-Flash` | `openai-completions` | 1048576 | ## kimi-coding [#kimi-coding] | Model ID | Transport | Context window | | --- | --- | ---: | | `kimi-coding/k3` | `anthropic-messages` | 1048576 | | `kimi-coding/k3-256k` | `anthropic-messages` | 262144 | | `kimi-coding/kimi-for-coding` | `anthropic-messages` | 1048576 | | `kimi-coding/kimi-for-coding-highspeed` | `anthropic-messages` | 262144 | ## meta [#meta] | Model ID | Transport | Context window | | --- | --- | ---: | | `meta/muse-spark-1.1` | `openai-responses` | 1048576 | | `meta/muse-spark-1.2` | `openai-responses` | 1048576 | | `meta/muse-spark-1.2-contributor` | `openai-responses` | 1048576 | | `meta/muse-spark-1.3` | `openai-responses` | 1048576 | | `meta/muse-spark-1.3-contributor` | `openai-responses` | 1048576 | | `meta/muse-wonder-1.0` | `openai-responses` | 1048576 | ## minimax-cn [#minimax-cn] | Model ID | Transport | Context window | | --- | --- | ---: | | `minimax-cn/MiniMax-M2.7` | `anthropic-messages` | 204800 | | `minimax-cn/MiniMax-M2.7-highspeed` | `anthropic-messages` | 204800 | | `minimax-cn/MiniMax-M3` | `anthropic-messages` | 1000000 | ## minimax [#minimax] | Model ID | Transport | Context window | | --- | --- | ---: | | `minimax/MiniMax-M2.7` | `anthropic-messages` | 204800 | | `minimax/MiniMax-M2.7-highspeed` | `anthropic-messages` | 204800 | | `minimax/MiniMax-M3` | `anthropic-messages` | 1000000 | ## mistral [#mistral] | Model ID | Transport | Context window | | --- | --- | ---: | | `mistral/codestral-2508` | `mistral-conversations` | 256000 | | `mistral/codestral-latest` | `mistral-conversations` | 256000 | | `mistral/devstral-2512` | `mistral-conversations` | 262144 | | `mistral/devstral-latest` | `mistral-conversations` | 262144 | | `mistral/devstral-medium-2507` | `mistral-conversations` | 128000 | | `mistral/devstral-medium-latest` | `mistral-conversations` | 262144 | | `mistral/devstral-small-2505` | `mistral-conversations` | 128000 | | `mistral/devstral-small-2507` | `mistral-conversations` | 128000 | | `mistral/glm-5-2` | `mistral-conversations` | 1048576 | | `mistral/labs-devstral-small-2512` | `mistral-conversations` | 256000 | | `mistral/labs-leanstral-1-5-1` | `mistral-conversations` | 262144 | | `mistral/magistral-medium-latest` | `mistral-conversations` | 262144 | | `mistral/ministral-14b-2512` | `mistral-conversations` | 262144 | | `mistral/ministral-3b-2512` | `mistral-conversations` | 131072 | | `mistral/ministral-3b-latest` | `mistral-conversations` | 131072 | | `mistral/ministral-8b-2512` | `mistral-conversations` | 262144 | | `mistral/ministral-8b-latest` | `mistral-conversations` | 262144 | | `mistral/mistral-large-2411` | `mistral-conversations` | 131072 | | `mistral/mistral-large-2512` | `mistral-conversations` | 262144 | | `mistral/mistral-large-4` | `mistral-conversations` | 1048576 | | `mistral/mistral-large-latest` | `mistral-conversations` | 262144 | | `mistral/mistral-medium-2505` | `mistral-conversations` | 131072 | | `mistral/mistral-medium-2508` | `mistral-conversations` | 262144 | | `mistral/mistral-medium-2604` | `mistral-conversations` | 262144 | | `mistral/mistral-medium-3.5` | `mistral-conversations` | 262144 | | `mistral/mistral-medium-latest` | `mistral-conversations` | 262144 | | `mistral/mistral-nemo` | `mistral-conversations` | 128000 | | `mistral/mistral-small-2506` | `mistral-conversations` | 128000 | | `mistral/mistral-small-2603` | `mistral-conversations` | 262144 | | `mistral/mistral-small-latest` | `mistral-conversations` | 262144 | | `mistral/open-mistral-7b` | `mistral-conversations` | 8000 | | `mistral/open-mistral-nemo` | `mistral-conversations` | 128000 | | `mistral/open-mixtral-8x22b` | `mistral-conversations` | 64000 | | `mistral/open-mixtral-8x7b` | `mistral-conversations` | 32000 | | `mistral/pixtral-12b` | `mistral-conversations` | 128000 | | `mistral/pixtral-large-latest` | `mistral-conversations` | 128000 | | `mistral/voxtral-small-2507` | `mistral-conversations` | 32768 | | `mistral/voxtral-small-latest` | `mistral-conversations` | 32768 | | `mistral/zai-glm-5-2` | `mistral-conversations` | 1048576 | | `mistral/zai-glm-5-3` | `mistral-conversations` | 1048576 | ## moonshotai-cn [#moonshotai-cn] | Model ID | Transport | Context window | | --- | --- | ---: | | `moonshotai-cn/kimi-k2.6` | `openai-completions` | 262144 | | `moonshotai-cn/kimi-k2.7-code` | `openai-completions` | 262144 | | `moonshotai-cn/kimi-k2.7-code-highspeed` | `openai-completions` | 262144 | | `moonshotai-cn/kimi-k3` | `openai-completions` | 1048576 | ## moonshotai [#moonshotai] | Model ID | Transport | Context window | | --- | --- | ---: | | `moonshotai/kimi-k2.6` | `openai-completions` | 262144 | | `moonshotai/kimi-k2.7-code` | `openai-completions` | 262144 | | `moonshotai/kimi-k2.7-code-highspeed` | `openai-completions` | 262144 | | `moonshotai/kimi-k3` | `openai-completions` | 1048576 | ## nvidia [#nvidia] | Model ID | Transport | Context window | | --- | --- | ---: | | `nvidia/deepseek-ai/deepseek-v4.1-flash` | `openai-completions` | 1000000 | | `nvidia/google/diffusiongemma-26b-a4b-it` | `openai-completions` | 250000 | | `nvidia/google/gemma-3-12b-it` | `openai-completions` | 131072 | | `nvidia/google/gemma-3-4b-it` | `openai-completions` | 131072 | | `nvidia/meta/llama-3.2-11b-vision-instruct` | `openai-completions` | 128000 | | `nvidia/meta/llama-3.2-90b-vision-instruct` | `openai-completions` | 128000 | | `nvidia/meta/muse-glimmer-30b` | `openai-completions` | 131072 | | `nvidia/mistralai/mistral-7b-instruct-v0.3` | `openai-completions` | 65536 | | `nvidia/moonshotai/kimi-k2.6` | `openai-completions` | 262144 | | `nvidia/moonshotai/kimi-k3` | `openai-completions` | 1048576 | | `nvidia/nvidia/cosmos-reason2-8b` | `openai-completions` | 131072 | | `nvidia/nvidia/llama-3.1-nemotron-70b-instruct` | `openai-completions` | 128000 | | `nvidia/nvidia/llama-3.1-nemotron-ultra-253b-v1` | `openai-completions` | 128000 | | `nvidia/nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` | `openai-completions` | 256000 | | `nvidia/nvidia/nemotron-3-super-120b-a12b` | `openai-completions` | 262144 | | `nvidia/nvidia/nemotron-3-ultra-550b-a55b` | `openai-completions` | 1000000 | | `nvidia/nvidia/nemotron-3.5-lightning-30b-a3b` | `openai-completions` | 262144 | | `nvidia/openai/gpt-oss-20b` | `openai-completions` | 131072 | | `nvidia/poolside/laguna-xs-2.1` | `openai-completions` | 262144 | | `nvidia/z-ai/glm-5.3` | `openai-completions` | 1000000 | | `nvidia/z-ai/glm-5.3-flash` | `openai-completions` | 1000000 | ## openai-codex [#openai-codex] | Model ID | Transport | Context window | | --- | --- | ---: | | `openai-codex/gpt-5.3-codex-spark` | `openai-codex-responses` | 128000 | | `openai-codex/gpt-5.5` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-5.6-luna` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-5.6-sol` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-5.6-terra` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-6-astra` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-6-luna` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-6-sol` | `openai-codex-responses` | 272000 | | `openai-codex/gpt-6.1-sol` | `openai-codex-responses` | 272000 | ## openai [#openai] | Model ID | Transport | Context window | | --- | --- | ---: | | `openai/gpt-4` | `openai-responses` | 8192 | | `openai/gpt-4-turbo` | `openai-responses` | 128000 | | `openai/gpt-4.1` | `openai-responses` | 1047576 | | `openai/gpt-4.1-mini` | `openai-responses` | 1047576 | | `openai/gpt-4.1-nano` | `openai-responses` | 1047576 | | `openai/gpt-4o` | `openai-responses` | 128000 | | `openai/gpt-4o-2024-05-13` | `openai-responses` | 128000 | | `openai/gpt-4o-2024-08-06` | `openai-responses` | 128000 | | `openai/gpt-4o-2024-11-20` | `openai-responses` | 128000 | | `openai/gpt-4o-mini` | `openai-responses` | 128000 | | `openai/gpt-5` | `openai-responses` | 400000 | | `openai/gpt-5-chat-latest` | `openai-responses` | 128000 | | `openai/gpt-5-mini` | `openai-responses` | 400000 | | `openai/gpt-5-nano` | `openai-responses` | 400000 | | `openai/gpt-5-pro` | `openai-responses` | 400000 | | `openai/gpt-5.1` | `openai-responses` | 400000 | | `openai/gpt-5.2` | `openai-responses` | 400000 | | `openai/gpt-5.2-chat-latest` | `openai-responses` | 128000 | | `openai/gpt-5.2-pro` | `openai-responses` | 400000 | | `openai/gpt-5.3-chat-latest` | `openai-responses` | 128000 | | `openai/gpt-5.3-codex` | `openai-responses` | 400000 | | `openai/gpt-5.3-codex-spark` | `openai-responses` | 128000 | | `openai/gpt-5.4` | `openai-responses` | 272000 | | `openai/gpt-5.4-mini` | `openai-responses` | 400000 | | `openai/gpt-5.4-nano` | `openai-responses` | 400000 | | `openai/gpt-5.4-pro` | `openai-responses` | 1050000 | | `openai/gpt-5.5` | `openai-responses` | 272000 | | `openai/gpt-5.5-pro` | `openai-responses` | 1050000 | | `openai/gpt-5.6-luna` | `openai-responses` | 272000 | | `openai/gpt-5.6-sol` | `openai-responses` | 272000 | | `openai/gpt-5.6-terra` | `openai-responses` | 272000 | | `openai/gpt-6-astra` | `openai-responses` | 272000 | | `openai/gpt-6-luna` | `openai-responses` | 272000 | | `openai/gpt-6-sol` | `openai-responses` | 272000 | | `openai/gpt-6.1-sol` | `openai-responses` | 272000 | | `openai/gpt-daybreak-blue-latest` | `openai-responses` | 1050000 | | `openai/gpt-daybreak-red-latest` | `openai-responses` | 400000 | | `openai/gpt-realtime-2.1` | `openai-responses` | 128000 | | `openai/o1` | `openai-responses` | 200000 | | `openai/o1-pro` | `openai-responses` | 200000 | | `openai/o3` | `openai-responses` | 200000 | | `openai/o3-mini` | `openai-responses` | 200000 | | `openai/o3-pro` | `openai-responses` | 200000 | | `openai/o4-mini` | `openai-responses` | 200000 | ## opencode-go [#opencode-go] | Model ID | Transport | Context window | | --- | --- | ---: | | `opencode-go/claude-haiku-5-5` | `anthropic-messages` | 1000000 | | `opencode-go/deepseek-v4-flash` | `openai-completions` | 1000000 | | `opencode-go/deepseek-v4-flash-vision-exp` | `openai-completions` | 1000000 | | `opencode-go/deepseek-v4-pro` | `openai-completions` | 1000000 | | `opencode-go/deepseek-v4.1-flash` | `openai-completions` | 1000000 | | `opencode-go/glm-5.2` | `openai-completions` | 1000000 | | `opencode-go/glm-5.3` | `openai-completions` | 1000000 | | `opencode-go/glm-5.3-flash` | `openai-completions` | 1000000 | | `opencode-go/gpt-5.6-luna` | `openai-responses` | 1050000 | | `opencode-go/gpt-6-luna` | `openai-responses` | 1050000 | | `opencode-go/grok-4.6` | `openai-responses` | 500000 | | `opencode-go/grok-4.7` | `openai-responses` | 500000 | | `opencode-go/hy3` | `openai-completions` | 256000 | | `opencode-go/hy4-preview` | `openai-completions` | 1024000 | | `opencode-go/kimi-k2.7-code` | `openai-completions` | 262144 | | `opencode-go/kimi-k3` | `openai-completions` | 1048576 | | `opencode-go/longcat-2.0` | `openai-completions` | 1000000 | | `opencode-go/longcat-2.5-preview-free` | `openai-completions` | 1000000 | | `opencode-go/mimo-v2.5` | `openai-completions` | 1000000 | | `opencode-go/mimo-v2.5-pro` | `openai-completions` | 1048576 | | `opencode-go/mimo-v2.6-flash` | `openai-completions` | 1048576 | | `opencode-go/mimo-v2.6-pro` | `openai-completions` | 1048576 | | `opencode-go/minimax-m2.7` | `openai-completions` | 204800 | | `opencode-go/minimax-m3` | `anthropic-messages` | 1000000 | | `opencode-go/muse-spark-1.2-contributor` | `openai-responses` | 1048576 | | `opencode-go/muse-spark-1.3-contributor` | `openai-responses` | 1048576 | | `opencode-go/qwen3.7-plus` | `anthropic-messages` | 1000000 | | `opencode-go/qwen3.8-flash` | `anthropic-messages` | 1000000 | | `opencode-go/qwen3.8-max` | `anthropic-messages` | 1000000 | | `opencode-go/space-bunny` | `openai-completions` | 1048576 | | `opencode-go/step-5-preview-free` | `openai-completions` | 1000000 | ## opencode [#opencode] | Model ID | Transport | Context window | | --- | --- | ---: | | `opencode/big-pickle` | `openai-completions` | 200000 | | `opencode/claude-fable-5` | `anthropic-messages` | 1000000 | | `opencode/claude-fable-5-1` | `anthropic-messages` | 1000000 | | `opencode/claude-haiku-4-5` | `anthropic-messages` | 200000 | | `opencode/claude-haiku-5-5` | `anthropic-messages` | 1000000 | | `opencode/claude-opus-4-5` | `anthropic-messages` | 200000 | | `opencode/claude-opus-4-6` | `anthropic-messages` | 1000000 | | `opencode/claude-opus-4-7` | `anthropic-messages` | 1000000 | | `opencode/claude-opus-4-8` | `anthropic-messages` | 1000000 | | `opencode/claude-opus-5` | `anthropic-messages` | 1000000 | | `opencode/claude-opus-5-5` | `anthropic-messages` | 1000000 | | `opencode/claude-sonnet-4` | `anthropic-messages` | 200000 | | `opencode/claude-sonnet-4-5` | `anthropic-messages` | 200000 | | `opencode/claude-sonnet-4-6` | `anthropic-messages` | 1000000 | | `opencode/claude-sonnet-5` | `anthropic-messages` | 1000000 | | `opencode/claude-sonnet-5-5` | `anthropic-messages` | 1000000 | | `opencode/deepseek-v4-flash` | `openai-completions` | 1000000 | | `opencode/deepseek-v4-flash-vision-exp` | `openai-completions` | 1000000 | | `opencode/deepseek-v4-pro` | `openai-completions` | 1000000 | | `opencode/deepseek-v4.1-flash` | `openai-completions` | 1000000 | | `opencode/exo-free` | `openai-completions` | 1048576 | | `opencode/gemini-3-flash` | `google-generative-ai` | 1048576 | | `opencode/gemini-3.1-pro` | `google-generative-ai` | 1048576 | | `opencode/gemini-3.5-flash` | `google-generative-ai` | 1048576 | | `opencode/gemini-3.5-flash-lite` | `google-generative-ai` | 1048576 | | `opencode/gemini-3.6-flash` | `google-generative-ai` | 1048576 | | `opencode/gemini-3.7-flash` | `google-generative-ai` | 1048576 | | `opencode/gemini-3.8-flash` | `google-generative-ai` | 1048576 | | `opencode/glm-5` | `openai-completions` | 204800 | | `opencode/glm-5.1` | `openai-completions` | 204800 | | `opencode/glm-5.2` | `openai-completions` | 1000000 | | `opencode/glm-5.3` | `openai-completions` | 1000000 | | `opencode/glm-5.3-flash` | `openai-completions` | 1000000 | | `opencode/gpt-5` | `openai-responses` | 400000 | | `opencode/gpt-5-codex` | `openai-responses` | 400000 | | `opencode/gpt-5-nano` | `openai-responses` | 400000 | | `opencode/gpt-5.1` | `openai-responses` | 400000 | | `opencode/gpt-5.1-codex` | `openai-responses` | 400000 | | `opencode/gpt-5.1-codex-max` | `openai-responses` | 400000 | | `opencode/gpt-5.1-codex-mini` | `openai-responses` | 400000 | | `opencode/gpt-5.2` | `openai-responses` | 400000 | | `opencode/gpt-5.2-codex` | `openai-responses` | 400000 | | `opencode/gpt-5.3-codex` | `openai-responses` | 400000 | | `opencode/gpt-5.4` | `openai-responses` | 272000 | | `opencode/gpt-5.4-mini` | `openai-responses` | 400000 | | `opencode/gpt-5.4-nano` | `openai-responses` | 400000 | | `opencode/gpt-5.4-pro` | `openai-responses` | 1050000 | | `opencode/gpt-5.5` | `openai-responses` | 1050000 | | `opencode/gpt-5.5-pro` | `openai-responses` | 1050000 | | `opencode/gpt-5.6-luna` | `openai-responses` | 1050000 | | `opencode/gpt-5.6-sol` | `openai-responses` | 1050000 | | `opencode/gpt-5.6-terra` | `openai-responses` | 1050000 | | `opencode/gpt-6-astra` | `openai-responses` | 1050000 | | `opencode/gpt-6-luna` | `openai-responses` | 1050000 | | `opencode/gpt-6-sol` | `openai-responses` | 1050000 | | `opencode/gpt-6.1-sol` | `openai-responses` | 1050000 | | `opencode/grok-4.5` | `openai-responses` | 500000 | | `opencode/grok-4.6` | `openai-responses` | 500000 | | `opencode/grok-4.7` | `openai-responses` | 500000 | | `opencode/grok-build-0.1` | `openai-responses` | 256000 | | `opencode/kimi-k2.5` | `openai-completions` | 262144 | | `opencode/kimi-k2.6` | `openai-completions` | 262144 | | `opencode/kimi-k2.7-code` | `openai-completions` | 262144 | | `opencode/kimi-k3` | `openai-completions` | 1048576 | | `opencode/ling-3.0-flash-fin-free` | `openai-completions` | 262144 | | `opencode/ling-3.1-flash-free` | `openai-completions` | 262144 | | `opencode/longcat-2.5-preview-free` | `openai-completions` | 1000000 | | `opencode/mimo-v2.6-flash-free` | `openai-completions` | 200000 | | `opencode/minimax-m2.5` | `openai-completions` | 204800 | | `opencode/minimax-m2.7` | `openai-completions` | 204800 | | `opencode/minimax-m3` | `openai-completions` | 512000 | | `opencode/mistral-large-4` | `openai-completions` | 524288 | | `opencode/muse-spark-1.2` | `openai-responses` | 1048576 | | `opencode/muse-spark-1.3` | `openai-responses` | 1048576 | | `opencode/muse-spark-1.3-contributor-free` | `openai-responses` | 1048576 | | `opencode/nemotron-3-ultra-free` | `openai-completions` | 1000000 | | `opencode/nemotron-3.5-lightning-free` | `openai-completions` | 262144 | | `opencode/qwen3.5-plus` | `anthropic-messages` | 262144 | | `opencode/qwen3.6-plus` | `anthropic-messages` | 262144 | | `opencode/qwen3.8-flash` | `anthropic-messages` | 1000000 | | `opencode/qwen3.8-max` | `openai-completions` | 262144 | | `opencode/space-bunny-free` | `openai-completions` | 1048576 | | `opencode/step-5-preview-free` | `openai-completions` | 1000000 | ## openrouter [#openrouter] | Model ID | Transport | Context window | | --- | --- | ---: | | `openrouter/~anthropic/claude-fable-latest` | `openai-completions` | 1000000 | | `openrouter/~anthropic/claude-haiku-latest` | `openai-completions` | 1000000 | | `openrouter/~anthropic/claude-opus-latest` | `openai-completions` | 1000000 | | `openrouter/~anthropic/claude-sonnet-latest` | `openai-completions` | 1000000 | | `openrouter/~deepseek/deepseek-flash-latest` | `openai-completions` | 1048576 | | `openrouter/~deepseek/deepseek-pro-latest` | `openai-completions` | 1048576 | | `openrouter/~deepseek/deepseek-v4-flash-latest` | `openai-completions` | 1048576 | | `openrouter/~google/gemini-flash-latest` | `openai-completions` | 1048576 | | `openrouter/~google/gemini-pro-latest` | `openai-completions` | 1048576 | | `openrouter/~moonshotai/kimi-latest` | `openai-completions` | 1048576 | | `openrouter/~openai/gpt-astra-latest` | `openai-completions` | 1050000 | | `openrouter/~openai/gpt-luna-latest` | `openai-completions` | 1050000 | | `openrouter/~openai/gpt-mini-latest` | `openai-completions` | 400000 | | `openrouter/~openai/gpt-sol-latest` | `openai-completions` | 1050000 | | `openrouter/~openai/gpt-terra-latest` | `openai-completions` | 1050000 | | `openrouter/~x-ai/grok-latest` | `openai-completions` | 500000 | | `openrouter/~z-ai/glm-flash-latest` | `openai-completions` | 1048576 | | `openrouter/~z-ai/glm-latest` | `openai-completions` | 1048576 | | `openrouter/aion-labs/aion-2.0` | `openai-completions` | 131072 | | `openrouter/aion-labs/aion-3.0` | `openai-completions` | 131072 | | `openrouter/aion-labs/aion-3.0-mini` | `openai-completions` | 131072 | | `openrouter/aion-labs/aion-3.5` | `openai-completions` | 262144 | | `openrouter/aion-labs/aion-3.5-mini` | `openai-completions` | 262144 | | `openrouter/amazon/nova-2-lite-v1` | `openai-completions` | 1000000 | | `openrouter/amazon/nova-lite-v1` | `openai-completions` | 300000 | | `openrouter/amazon/nova-micro-v1` | `openai-completions` | 128000 | | `openrouter/amazon/nova-premier-v1` | `openai-completions` | 1000000 | | `openrouter/amazon/nova-pro-v1` | `openai-completions` | 300000 | | `openrouter/anthropic/claude-fable-5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-fable-5:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-fable-5.1` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-fable-5.1:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-haiku-4.5` | `anthropic-messages` | 200000 | | `openrouter/anthropic/claude-haiku-4.5:batch` | `openai-completions` | 200000 | | `openrouter/anthropic/claude-haiku-5.5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-haiku-5.5:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-opus-4.1` | `anthropic-messages` | 200000 | | `openrouter/anthropic/claude-opus-4.1:batch` | `openai-completions` | 200000 | | `openrouter/anthropic/claude-opus-4.5` | `anthropic-messages` | 200000 | | `openrouter/anthropic/claude-opus-4.5:batch` | `openai-completions` | 200000 | | `openrouter/anthropic/claude-opus-4.6` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-opus-4.6:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-opus-4.7` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-opus-4.7:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-opus-4.8` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-opus-4.8:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-opus-5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-opus-5:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-opus-5.5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-opus-5.5:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-sonnet-4` | `anthropic-messages` | 200000 | | `openrouter/anthropic/claude-sonnet-4.5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-sonnet-4.5:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-sonnet-4.6` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-sonnet-4.6:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-sonnet-5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-sonnet-5:batch` | `openai-completions` | 1000000 | | `openrouter/anthropic/claude-sonnet-5.5` | `anthropic-messages` | 1000000 | | `openrouter/anthropic/claude-sonnet-5.5:batch` | `openai-completions` | 1000000 | | `openrouter/apodex/apodex-1.1-mini:free` | `openai-completions` | 262144 | | `openrouter/arcee-ai/trinity-large-thinking` | `openai-completions` | 262144 | | `openrouter/auto` | `openai-completions` | 2000000 | | `openrouter/bytedance-seed/seed-1.6` | `openai-completions` | 262144 | | `openrouter/bytedance-seed/seed-1.6-flash` | `openai-completions` | 262144 | | `openrouter/bytedance-seed/seed-2-1-turbo` | `openai-completions` | 262144 | | `openrouter/bytedance-seed/seed-2.0-code` | `openai-completions` | 262144 | | `openrouter/bytedance-seed/seed-2.0-lite` | `openai-completions` | 262144 | | `openrouter/bytedance-seed/seed-2.0-mini` | `openai-completions` | 262144 | | `openrouter/cohere/command-a-plus` | `openai-completions` | 192000 | | `openrouter/cohere/command-r-08-2024` | `openai-completions` | 128000 | | `openrouter/cohere/command-r-plus-08-2024` | `openai-completions` | 128000 | | `openrouter/cohere/north-mini-code:free` | `openai-completions` | 256000 | | `openrouter/deepseek/deepseek-chat` | `openai-completions` | 163840 | | `openrouter/deepseek/deepseek-chat-v3-0324` | `openai-completions` | 128000 | | `openrouter/deepseek/deepseek-chat-v3.1` | `openai-completions` | 163840 | | `openrouter/deepseek/deepseek-r1` | `openai-completions` | 64000 | | `openrouter/deepseek/deepseek-r1-0528` | `openai-completions` | 163840 | | `openrouter/deepseek/deepseek-v3.1-terminus` | `openai-completions` | 163840 | | `openrouter/deepseek/deepseek-v3.2` | `openai-completions` | 163840 | | `openrouter/deepseek/deepseek-v3.2-exp` | `openai-completions` | 163840 | | `openrouter/deepseek/deepseek-v4-flash` | `openai-completions` | 1048576 | | `openrouter/deepseek/deepseek-v4-flash-0731` | `openai-completions` | 1048576 | | `openrouter/deepseek/deepseek-v4-flash-vision-exp` | `openai-completions` | 1048576 | | `openrouter/deepseek/deepseek-v4-pro` | `openai-completions` | 1024000 | | `openrouter/deepseek/deepseek-v4-pro-0813` | `openai-completions` | 1048576 | | `openrouter/deepseek/deepseek-v4.1-flash` | `openai-completions` | 1048576 | | `openrouter/deepseek/deepseek-v4.1-flash:batch` | `openai-completions` | 1048576 | | `openrouter/dots-studio/dots-3-note-preview:free` | `openai-completions` | 512000 | | `openrouter/fireworks/ember-1` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-flash` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-flash-lite` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-flash-lite:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-flash:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-pro` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-pro-preview` | `openai-completions` | 1048576 | | `openrouter/google/gemini-2.5-pro:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3-flash-preview` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3-flash-preview:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3-pro-image` | `openai-completions` | 65536 | | `openrouter/google/gemini-3.1-flash-lite` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.1-flash-lite-preview` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.1-flash-lite:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.1-pro-preview` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.1-pro-preview-customtools` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.1-pro-preview:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.5-flash` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.5-flash-lite` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.5-flash-lite:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.5-flash:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.6-flash` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.6-flash:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.7-flash` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.7-flash:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.8-flash` | `openai-completions` | 1048576 | | `openrouter/google/gemini-3.8-flash:batch` | `openai-completions` | 1048576 | | `openrouter/google/gemini-nano-banana-2.1` | `openai-completions` | 65536 | | `openrouter/google/gemma-3-12b-it` | `openai-completions` | 131072 | | `openrouter/google/gemma-3-27b-it` | `openai-completions` | 131072 | | `openrouter/google/gemma-4-26b-a4b-it` | `openai-completions` | 262144 | | `openrouter/google/gemma-4-26b-a4b-it:free` | `openai-completions` | 262144 | | `openrouter/google/gemma-4-31b-it` | `openai-completions` | 262144 | | `openrouter/google/gemma-4-31b-it:free` | `openai-completions` | 262144 | | `openrouter/ibm-granite/granite-4.2-8b` | `openai-completions` | 131072 | | `openrouter/inception/mercury-2` | `openai-completions` | 128000 | | `openrouter/inception/mercury-2.5` | `openai-completions` | 260000 | | `openrouter/inclusionai/ling-3.0-flash` | `openai-completions` | 262144 | | `openrouter/inclusionai/ling-3.0-flash-fin` | `openai-completions` | 262144 | | `openrouter/inclusionai/ling-3.0-flash-sante` | `openai-completions` | 262144 | | `openrouter/inclusionai/ling-3.0-flash-vl` | `openai-completions` | 262144 | | `openrouter/inclusionai/ling-3.1-flash` | `openai-completions` | 262144 | | `openrouter/liquid/lfm-2.5-2.6b:free` | `openai-completions` | 65536 | | `openrouter/meituan/longcat-2.0` | `openai-completions` | 1048756 | | `openrouter/meta-llama/llama-3.1-70b-instruct` | `openai-completions` | 131072 | | `openrouter/meta-llama/llama-3.1-8b-instruct` | `openai-completions` | 131072 | | `openrouter/meta-llama/llama-3.3-70b-instruct` | `openai-completions` | 131072 | | `openrouter/meta-llama/llama-4-maverick` | `openai-completions` | 128000 | | `openrouter/meta-llama/llama-4-scout` | `openai-completions` | 131072 | | `openrouter/meta/muse-glimmer-30b` | `openai-completions` | 131072 | | `openrouter/meta/muse-spark-1.1` | `openai-completions` | 1048576 | | `openrouter/meta/muse-spark-1.2` | `openai-completions` | 1048576 | | `openrouter/meta/muse-spark-1.2-contributor` | `openai-completions` | 1048576 | | `openrouter/meta/muse-spark-1.3` | `openai-completions` | 1048576 | | `openrouter/meta/muse-spark-1.3-contributor` | `openai-completions` | 1048576 | | `openrouter/meta/muse-wonder` | `openai-completions` | 1048576 | | `openrouter/minimax/minimax-m1` | `openai-completions` | 1000000 | | `openrouter/minimax/minimax-m2` | `openai-completions` | 196608 | | `openrouter/minimax/minimax-m2.1` | `openai-completions` | 204800 | | `openrouter/minimax/minimax-m2.5` | `openai-completions` | 200000 | | `openrouter/minimax/minimax-m2.7` | `openai-completions` | 196608 | | `openrouter/minimax/minimax-m3` | `openai-completions` | 524288 | | `openrouter/mistralai/codestral-2508` | `openai-completions` | 256000 | | `openrouter/mistralai/codestral-2508:batch` | `openai-completions` | 256000 | | `openrouter/mistralai/devstral-2512` | `openai-completions` | 262144 | | `openrouter/mistralai/ministral-14b-2512` | `openai-completions` | 262144 | | `openrouter/mistralai/ministral-3b-2512` | `openai-completions` | 131072 | | `openrouter/mistralai/ministral-8b-2512` | `openai-completions` | 262144 | | `openrouter/mistralai/ministral-8b-2512:batch` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-large` | `openai-completions` | 128000 | | `openrouter/mistralai/mistral-large-2407` | `openai-completions` | 131072 | | `openrouter/mistralai/mistral-large-2512` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-large-2512:batch` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-large-4-0` | `openai-completions` | 1048576 | | `openrouter/mistralai/mistral-medium-3` | `openai-completions` | 131072 | | `openrouter/mistralai/mistral-medium-3-5` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-medium-3-5:batch` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-medium-3.1` | `openai-completions` | 131072 | | `openrouter/mistralai/mistral-medium-3.1:batch` | `openai-completions` | 131072 | | `openrouter/mistralai/mistral-nemo` | `openai-completions` | 131072 | | `openrouter/mistralai/mistral-saba` | `openai-completions` | 32768 | | `openrouter/mistralai/mistral-small-2603` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-small-2603:batch` | `openai-completions` | 262144 | | `openrouter/mistralai/mistral-small-3.1-24b-instruct` | `openai-completions` | 128000 | | `openrouter/mistralai/mistral-small-3.2-24b-instruct` | `openai-completions` | 256000 | | `openrouter/mistralai/mixtral-8x22b-instruct` | `openai-completions` | 65536 | | `openrouter/mistralai/voxtral-small-24b-2507` | `openai-completions` | 32768 | | `openrouter/moonshotai/kimi-k2` | `openai-completions` | 131072 | | `openrouter/moonshotai/kimi-k2-0905` | `openai-completions` | 262144 | | `openrouter/moonshotai/kimi-k2-thinking` | `openai-completions` | 262144 | | `openrouter/moonshotai/kimi-k2.5` | `openai-completions` | 262144 | | `openrouter/moonshotai/kimi-k2.6` | `openai-completions` | 262144 | | `openrouter/moonshotai/kimi-k2.7-code` | `openai-completions` | 262144 | | `openrouter/moonshotai/kimi-k3` | `openai-completions` | 1048576 | | `openrouter/moonshotai/kimi-k3:batch` | `openai-completions` | 1048576 | | `openrouter/nex-agi/nex-n2.5-pro` | `openai-completions` | 262144 | | `openrouter/nvidia/nemotron-3-nano-30b-a3b` | `openai-completions` | 262144 | | `openrouter/nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free` | `openai-completions` | 256000 | | `openrouter/nvidia/nemotron-3-super-120b-a12b` | `openai-completions` | 262144 | | `openrouter/nvidia/nemotron-3-super-120b-a12b:free` | `openai-completions` | 262144 | | `openrouter/nvidia/nemotron-3-ultra-550b-a55b` | `openai-completions` | 202800 | | `openrouter/nvidia/nemotron-3-ultra-550b-a55b:free` | `openai-completions` | 1000000 | | `openrouter/nvidia/nemotron-3.5-lightning` | `openai-completions` | 262144 | | `openrouter/nvidia/nemotron-3.5-lightning:free` | `openai-completions` | 1000000 | | `openrouter/openai/gpt-3.5-turbo` | `openai-completions` | 16385 | | `openrouter/openai/gpt-3.5-turbo-0613` | `openai-completions` | 4095 | | `openrouter/openai/gpt-3.5-turbo-16k` | `openai-completions` | 16385 | | `openrouter/openai/gpt-3.5-turbo:batch` | `openai-completions` | 16385 | | `openrouter/openai/gpt-4` | `openai-completions` | 8191 | | `openrouter/openai/gpt-4-turbo` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4-turbo:batch` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4.1` | `openai-completions` | 1047576 | | `openrouter/openai/gpt-4.1-mini` | `openai-completions` | 1047576 | | `openrouter/openai/gpt-4.1-mini:batch` | `openai-completions` | 1047576 | | `openrouter/openai/gpt-4.1-nano` | `openai-completions` | 1047576 | | `openrouter/openai/gpt-4.1-nano:batch` | `openai-completions` | 1047576 | | `openrouter/openai/gpt-4.1:batch` | `openai-completions` | 1047576 | | `openrouter/openai/gpt-4o` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o-2024-05-13` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o-2024-08-06` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o-2024-11-20` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o-mini` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o-mini-2024-07-18` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o-mini:batch` | `openai-completions` | 128000 | | `openrouter/openai/gpt-4o:batch` | `openai-completions` | 128000 | | `openrouter/openai/gpt-5` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5-mini` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5-mini:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5-nano` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5-nano:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5-pro` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5-pro:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.1` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.1-codex` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.1-codex-max` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.1-codex-mini` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.1:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.2` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.2-chat` | `openai-completions` | 128000 | | `openrouter/openai/gpt-5.2-codex` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.2-pro` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.2-pro:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.2:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.3-codex` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.4` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.4-mini` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.4-mini:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.4-nano` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.4-nano:batch` | `openai-completions` | 400000 | | `openrouter/openai/gpt-5.4-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.4-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.4:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.5` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.5-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.5-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.5:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-luna` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-luna-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-luna-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-luna:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-sol` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-sol-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-sol-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-sol:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-terra` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-terra-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-terra-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-5.6-terra:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-astra` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-astra-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-astra-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-astra:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-luna` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-luna-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-luna-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-luna:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-sol` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-sol-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-sol-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6-sol:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6.1-sol` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6.1-sol-pro` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6.1-sol-pro:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-6.1-sol:batch` | `openai-completions` | 1050000 | | `openrouter/openai/gpt-audio` | `openai-completions` | 128000 | | `openrouter/openai/gpt-audio-mini` | `openai-completions` | 128000 | | `openrouter/openai/gpt-chat-latest` | `openai-completions` | 400000 | | `openrouter/openai/gpt-oss-120b` | `openai-completions` | 131072 | | `openrouter/openai/gpt-oss-120b:batch` | `openai-completions` | 131072 | | `openrouter/openai/gpt-oss-20b` | `openai-completions` | 131072 | | `openrouter/openai/gpt-oss-20b:batch` | `openai-completions` | 131072 | | `openrouter/openai/gpt-oss-safeguard-20b` | `openai-completions` | 131072 | | `openrouter/openai/o1` | `openai-completions` | 200000 | | `openrouter/openai/o3` | `openai-completions` | 200000 | | `openrouter/openai/o3-mini` | `openai-completions` | 200000 | | `openrouter/openai/o3-mini-high` | `openai-completions` | 200000 | | `openrouter/openai/o3-mini:batch` | `openai-completions` | 200000 | | `openrouter/openai/o3-pro` | `openai-completions` | 200000 | | `openrouter/openai/o3:batch` | `openai-completions` | 200000 | | `openrouter/openai/o4-mini` | `openai-completions` | 200000 | | `openrouter/openai/o4-mini-high` | `openai-completions` | 200000 | | `openrouter/openai/o4-mini:batch` | `openai-completions` | 200000 | | `openrouter/openrouter/auto` | `openai-completions` | 2000000 | | `openrouter/openrouter/auto-beta` | `openai-completions` | 2000000 | | `openrouter/openrouter/free` | `openai-completions` | 200000 | | `openrouter/openrouter/fusion` | `openai-completions` | 1000000 | | `openrouter/perceptron/perceptron-mk1.5` | `openai-completions` | 36864 | | `openrouter/poolside/laguna-s-2.1` | `openai-completions` | 1048576 | | `openrouter/poolside/laguna-s-2.1:free` | `openai-completions` | 262144 | | `openrouter/poolside/laguna-xs-2.1` | `openai-completions` | 262144 | | `openrouter/poolside/laguna-xs-2.1:free` | `openai-completions` | 262144 | | `openrouter/prism-ml/ternary-bonsai-2-27b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen-2.5-72b-instruct` | `openai-completions` | 32768 | | `openrouter/qwen/qwen-2.5-7b-instruct` | `openai-completions` | 32768 | | `openrouter/qwen/qwen-plus` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3-14b` | `openai-completions` | 40960 | | `openrouter/qwen/qwen3-235b-a22b-2507` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-235b-a22b-thinking-2507` | `openai-completions` | 128000 | | `openrouter/qwen/qwen3-30b-a3b` | `openai-completions` | 40960 | | `openrouter/qwen/qwen3-30b-a3b-instruct-2507` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-32b` | `openai-completions` | 40960 | | `openrouter/qwen/qwen3-coder` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-coder-30b-a3b-instruct` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-coder-flash` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3-coder-next` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-next-80b-a3b-instruct` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-next-80b-a3b-thinking` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-vl-235b-a22b-instruct` | `openai-completions` | 131072 | | `openrouter/qwen/qwen3-vl-30b-a3b-instruct` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-vl-30b-a3b-thinking` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3-vl-8b-instruct` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.5-122b-a10b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.5-27b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.5-35b-a3b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.5-397b-a17b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.5-9b` | `openai-completions` | 256000 | | `openrouter/qwen/qwen3.5-flash-02-23` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.5-plus-02-15` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.5-plus-20260420` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.6-27b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.6-35b-a3b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.6-flash` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.6-plus` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.7-flash` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.7-max` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.7-plus` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.8-2.4t-a95b` | `openai-completions` | 1048576 | | `openrouter/qwen/qwen3.8-27b` | `openai-completions` | 262144 | | `openrouter/qwen/qwen3.8-flash` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.8-max-0902` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.8-max-prime` | `openai-completions` | 1000000 | | `openrouter/qwen/qwen3.8-omni-flash` | `openai-completions` | 1000000 | | `openrouter/rekaai/reka-edge` | `openai-completions` | 16384 | | `openrouter/relace/relace-search` | `openai-completions` | 256000 | | `openrouter/sakana/fugu-max` | `openai-completions` | 1000000 | | `openrouter/sakana/fugu-ultra` | `openai-completions` | 1000000 | | `openrouter/sakana/fugu-ultra-v2` | `openai-completions` | 1000000 | | `openrouter/sakana/sakana-namazu` | `openai-completions` | 262144 | | `openrouter/stepfun/step-3.5-flash` | `openai-completions` | 262144 | | `openrouter/stepfun/step-3.7-flash` | `openai-completions` | 256000 | | `openrouter/stepfun/step-5-preview` | `openai-completions` | 1000000 | | `openrouter/tencent/hy3` | `openai-completions` | 262144 | | `openrouter/tencent/hy3-preview` | `openai-completions` | 262144 | | `openrouter/tencent/hy4-preview` | `openai-completions` | 1048576 | | `openrouter/thinkingmachines/inkling` | `openai-completions` | 524288 | | `openrouter/thinkingmachines/inkling-small` | `openai-completions` | 524288 | | `openrouter/thinkingmachines/inkling-small:free` | `openai-completions` | 1048576 | | `openrouter/thinkingmachines/inkling:free` | `openai-completions` | 1048576 | | `openrouter/typesafe/jev-router` | `openai-completions` | 1000000 | | `openrouter/unbiased/pareto` | `openai-completions` | 262144 | | `openrouter/unbiased/pareto-26.10-preview` | `openai-completions` | 1048576 | | `openrouter/upstage/solar-mini4` | `openai-completions` | 524288 | | `openrouter/upstage/solar-pro-3` | `openai-completions` | 131072 | | `openrouter/upstage/solar-pro4` | `openai-completions` | 524288 | | `openrouter/x-ai/grok-4.20` | `openai-completions` | 2000000 | | `openrouter/x-ai/grok-4.3` | `openai-completions` | 1000000 | | `openrouter/x-ai/grok-4.3:batch` | `openai-completions` | 1000000 | | `openrouter/x-ai/grok-4.5` | `openai-completions` | 500000 | | `openrouter/x-ai/grok-4.6` | `openai-completions` | 500000 | | `openrouter/x-ai/grok-4.7` | `openai-completions` | 500000 | | `openrouter/x-ai/grok-build-0.1` | `openai-completions` | 256000 | | `openrouter/xiaomi/mimo-v2.5` | `openai-completions` | 1048576 | | `openrouter/xiaomi/mimo-v2.5-pro` | `openai-completions` | 1048576 | | `openrouter/xiaomi/mimo-v2.6-flash` | `openai-completions` | 1048576 | | `openrouter/xiaomi/mimo-v2.6-pro` | `openai-completions` | 1048576 | | `openrouter/xiaomi/mimo-v2.6-pro-ultraspeed` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-4.5` | `openai-completions` | 131072 | | `openrouter/z-ai/glm-4.5-air` | `openai-completions` | 131072 | | `openrouter/z-ai/glm-4.5v` | `openai-completions` | 65536 | | `openrouter/z-ai/glm-4.6` | `openai-completions` | 202752 | | `openrouter/z-ai/glm-4.6v` | `openai-completions` | 131072 | | `openrouter/z-ai/glm-4.7` | `openai-completions` | 202752 | | `openrouter/z-ai/glm-4.7-flash` | `openai-completions` | 131072 | | `openrouter/z-ai/glm-5` | `openai-completions` | 198000 | | `openrouter/z-ai/glm-5-turbo` | `openai-completions` | 202752 | | `openrouter/z-ai/glm-5.1` | `openai-completions` | 200000 | | `openrouter/z-ai/glm-5.2` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-5.3` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-5.3-flash` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-5.3-flash:batch` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-5.3-flashx` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-5.3-prime` | `openai-completions` | 1000000 | | `openrouter/z-ai/glm-5.3:batch` | `openai-completions` | 1048576 | | `openrouter/z-ai/glm-5v-turbo` | `openai-completions` | 202752 | ## qwen-token-plan-cn [#qwen-token-plan-cn] | Model ID | Transport | Context window | | --- | --- | ---: | | `qwen-token-plan-cn/deepseek-v3.2` | `openai-completions` | 131072 | | `qwen-token-plan-cn/deepseek-v4-flash` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/deepseek-v4-flash-0731` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/deepseek-v4-pro` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/deepseek-v4-pro-0813` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/deepseek-v4.1-flash` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/glm-5` | `openai-completions` | 202752 | | `qwen-token-plan-cn/glm-5.1` | `openai-completions` | 202752 | | `qwen-token-plan-cn/glm-5.2` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/glm-5.3` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/kimi-k2.5` | `openai-completions` | 262144 | | `qwen-token-plan-cn/kimi-k2.6` | `openai-completions` | 262144 | | `qwen-token-plan-cn/kimi-k2.7-code` | `openai-completions` | 262144 | | `qwen-token-plan-cn/MiniMax-M2.5` | `openai-completions` | 196608 | | `qwen-token-plan-cn/qwen3.6-flash` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/qwen3.6-plus` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/qwen3.7-max` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/qwen3.7-plus` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/qwen3.8-flash` | `openai-completions` | 1000000 | | `qwen-token-plan-cn/qwen3.8-max` | `openai-completions` | 1000000 | ## qwen-token-plan-individual [#qwen-token-plan-individual] | Model ID | Transport | Context window | | --- | --- | ---: | | `qwen-token-plan-individual/deepseek-v4-flash-0731` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/deepseek-v4-pro` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/deepseek-v4-pro-0813` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/glm-5.2` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/qwen3.6-flash` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/qwen3.7-max` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/qwen3.7-plus` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/qwen3.8-flash` | `openai-completions` | 1000000 | | `qwen-token-plan-individual/qwen3.8-max` | `openai-completions` | 1000000 | ## qwen-token-plan [#qwen-token-plan] | Model ID | Transport | Context window | | --- | --- | ---: | | `qwen-token-plan/deepseek-v3.2` | `openai-completions` | 131072 | | `qwen-token-plan/deepseek-v4-flash` | `openai-completions` | 1000000 | | `qwen-token-plan/deepseek-v4-flash-0731` | `openai-completions` | 1000000 | | `qwen-token-plan/deepseek-v4-pro` | `openai-completions` | 1000000 | | `qwen-token-plan/deepseek-v4-pro-0813` | `openai-completions` | 1000000 | | `qwen-token-plan/deepseek-v4.1-flash` | `openai-completions` | 1000000 | | `qwen-token-plan/glm-5` | `openai-completions` | 202752 | | `qwen-token-plan/glm-5.1` | `openai-completions` | 202752 | | `qwen-token-plan/glm-5.2` | `openai-completions` | 1000000 | | `qwen-token-plan/glm-5.3` | `openai-completions` | 1000000 | | `qwen-token-plan/kimi-k2.5` | `openai-completions` | 262144 | | `qwen-token-plan/kimi-k2.6` | `openai-completions` | 262144 | | `qwen-token-plan/kimi-k2.7-code` | `openai-completions` | 262144 | | `qwen-token-plan/MiniMax-M2.5` | `openai-completions` | 196608 | | `qwen-token-plan/qwen3.6-flash` | `openai-completions` | 1000000 | | `qwen-token-plan/qwen3.6-plus` | `openai-completions` | 1000000 | | `qwen-token-plan/qwen3.7-max` | `openai-completions` | 1000000 | | `qwen-token-plan/qwen3.7-plus` | `openai-completions` | 1000000 | | `qwen-token-plan/qwen3.8-flash` | `openai-completions` | 1000000 | | `qwen-token-plan/qwen3.8-max` | `openai-completions` | 1000000 | ## radius [#radius] | Model ID | Transport | Context window | | --- | --- | ---: | | `radius/balanced` | `pi-messages` | 1048576 | | `radius/cheap` | `pi-messages` | 1000000 | | `radius/claude-fable-5` | `pi-messages` | 1000000 | | `radius/claude-fable-5-1` | `pi-messages` | 1000000 | | `radius/claude-haiku-4-5` | `pi-messages` | 200000 | | `radius/claude-haiku-5-5` | `pi-messages` | 1000000 | | `radius/claude-opus-4-5` | `pi-messages` | 200000 | | `radius/claude-opus-4-8` | `pi-messages` | 1000000 | | `radius/claude-opus-5` | `pi-messages` | 1000000 | | `radius/claude-opus-5-5` | `pi-messages` | 1000000 | | `radius/claude-sonnet-4-5` | `pi-messages` | 1000000 | | `radius/claude-sonnet-5` | `pi-messages` | 1000000 | | `radius/claude-sonnet-5-5` | `pi-messages` | 1000000 | | `radius/deepseek-v4.1-flash` | `pi-messages` | 1000000 | | `radius/glm-5.3` | `pi-messages` | 1000000 | | `radius/glm-5.3-flash` | `pi-messages` | 1000000 | | `radius/gpt-5.3-codex` | `pi-messages` | 400000 | | `radius/gpt-5.4` | `pi-messages` | 272000 | | `radius/gpt-5.4-mini` | `pi-messages` | 400000 | | `radius/gpt-5.5` | `pi-messages` | 272000 | | `radius/gpt-5.6-luna` | `pi-messages` | 272000 | | `radius/gpt-5.6-sol` | `pi-messages` | 272000 | | `radius/gpt-5.6-terra` | `pi-messages` | 272000 | | `radius/gpt-6-astra` | `pi-messages` | 272000 | | `radius/gpt-6-luna` | `pi-messages` | 1050000 | | `radius/gpt-6-sol` | `pi-messages` | 1050000 | | `radius/grok-4.7` | `pi-messages` | 500000 | | `radius/kimi-k3` | `pi-messages` | 1048576 | | `radius/precise` | `pi-messages` | 272000 | ## together [#together] | Model ID | Transport | Context window | | --- | --- | ---: | | `together/deepseek-ai/DeepSeek-V4-Flash-0731` | `openai-completions` | 1048576 | | `together/deepseek-ai/DeepSeek-V4-Pro-0813` | `openai-completions` | 1048576 | | `together/deepseek-ai/DeepSeek-V4.1-Flash` | `openai-completions` | 1048576 | | `together/meta-llama/Llama-3.3-70B-Instruct-Turbo` | `openai-completions` | 131072 | | `together/MiniMaxAI/MiniMax-M2.7` | `openai-completions` | 196608 | | `together/MiniMaxAI/MiniMax-M3` | `openai-completions` | 524288 | | `together/moonshotai/Kimi-K3` | `openai-completions` | 1048576 | | `together/nvidia/nemotron-3-ultra-550b-a55b` | `openai-completions` | 512300 | | `together/openai/gpt-oss-120b` | `openai-completions` | 131072 | | `together/Qwen/Qwen2.5-7B-Instruct-Turbo` | `openai-completions` | 32768 | | `together/Qwen/Qwen3.5-9B` | `openai-completions` | 262144 | | `together/Qwen/Qwen3.6-Plus` | `openai-completions` | 1000000 | | `together/Qwen/Qwen3.7-Max` | `openai-completions` | 1000000 | | `together/thinkingmachines/Inkling` | `openai-completions` | 524288 | | `together/zai-org/GLM-5.2` | `openai-completions` | 1048575 | | `together/zai-org/GLM-5.3` | `openai-completions` | 1048576 | | `together/zai-org/GLM-5.3-Flash` | `openai-completions` | 1048575 | ## vercel-ai-gateway [#vercel-ai-gateway] | Model ID | Transport | Context window | | --- | --- | ---: | | `vercel-ai-gateway/alibaba/qwen-3-14b` | `anthropic-messages` | 40960 | | `vercel-ai-gateway/alibaba/qwen-3-235b` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen-3-30b` | `anthropic-messages` | 40960 | | `vercel-ai-gateway/alibaba/qwen-3-32b` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/alibaba/qwen3-coder` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen3-coder-30b-a3b` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen3-coder-next` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/alibaba/qwen3-max-thinking` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/alibaba/qwen3-next-80b-a3b-instruct` | `anthropic-messages` | 262114 | | `vercel-ai-gateway/alibaba/qwen3-next-80b-a3b-thinking` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen3-vl-instruct` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen3.5-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/alibaba/qwen3.5-plus` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/alibaba/qwen3.6-27b` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/alibaba/qwen3.6-plus` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/alibaba/qwen3.7-flash` | `anthropic-messages` | 991000 | | `vercel-ai-gateway/alibaba/qwen3.7-max` | `anthropic-messages` | 991000 | | `vercel-ai-gateway/alibaba/qwen3.7-plus` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/alibaba/qwen3.8-2.4t-a95b` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen3.8-27b` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/alibaba/qwen3.8-flash` | `anthropic-messages` | 991000 | | `vercel-ai-gateway/alibaba/qwen3.8-max` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/alibaba/qwen3.8-max-0902` | `anthropic-messages` | 991000 | | `vercel-ai-gateway/alibaba/qwen3.8-max-prime` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/alibaba/qwen3.8-omni-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/amazon/nova-2-lite` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/amazon/nova-lite` | `anthropic-messages` | 300000 | | `vercel-ai-gateway/amazon/nova-micro` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/amazon/nova-pro` | `anthropic-messages` | 300000 | | `vercel-ai-gateway/anthropic/claude-fable-5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-fable-5.1` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-haiku-4.5` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/anthropic/claude-haiku-5.5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-4.5` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/anthropic/claude-opus-4.6` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-4.7` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-4.8` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-4.8-fast` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-5-fast` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-5.5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-opus-5.5-fast` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-sonnet-4` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-sonnet-4.5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-sonnet-4.6` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-sonnet-5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/anthropic/claude-sonnet-5.5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/arcee-ai/trinity-large-thinking` | `anthropic-messages` | 262100 | | `vercel-ai-gateway/bytedance/seed-1.6` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/bytedance/seed-1.8` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/bytedance/seed-2.1-turbo` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/callstack/apex` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/cohere/command-a` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/deepseek/deepseek-r1` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/deepseek/deepseek-v3.1` | `anthropic-messages` | 163840 | | `vercel-ai-gateway/deepseek/deepseek-v3.2` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/deepseek/deepseek-v3.2-thinking` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/deepseek/deepseek-v4-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/deepseek/deepseek-v4-flash-0731` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/deepseek/deepseek-v4-flash-vision-exp` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/deepseek/deepseek-v4-pro` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/deepseek/deepseek-v4-pro-0813` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/deepseek/deepseek-v4.1-flash` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/deepseek/deepseek-v4.1-flash-fast` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/fireworks/ember-1` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/google/gemini-2.5-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-2.5-flash-lite` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/google/gemini-2.5-pro` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/google/gemini-3-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.1-flash-lite` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.1-pro-preview` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.5-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.5-flash-lite` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.6-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.7-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemini-3.8-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/google/gemma-4-26b-a4b-it` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/google/gemma-4-31b-it` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/inception/mercury-2` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/inception/mercury-2.5` | `anthropic-messages` | 260000 | | `vercel-ai-gateway/inception/mercury-coder-small` | `anthropic-messages` | 32000 | | `vercel-ai-gateway/inclusionai/ling-3.0-flash` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/inclusionai/ling-3.0-flash-fin` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/inclusionai/ling-3.0-flash-sante` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/inclusionai/ling-3.0-flash-vl` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/inclusionai/ling-3.1-flash` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/inclusionai/ling-3.1-flash-free` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/interfaze/interfaze-beta` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/meituan/longcat-2.5-preview` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/meta/llama-3.1-70b` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/meta/llama-3.1-8b` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/meta/llama-3.3-70b` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/meta/llama-4-maverick` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/meta/llama-4-scout` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/meta/muse-glimmer-30b` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/meta/muse-spark-1.1` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/meta/muse-spark-1.2` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/meta/muse-spark-1.2-contributor` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/meta/muse-spark-1.3` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/meta/muse-spark-1.3-contributor` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/meta/muse-wonder-1.0` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/minimax/minimax-m2` | `anthropic-messages` | 205000 | | `vercel-ai-gateway/minimax/minimax-m2.1` | `anthropic-messages` | 204800 | | `vercel-ai-gateway/minimax/minimax-m2.1-lightning` | `anthropic-messages` | 204800 | | `vercel-ai-gateway/minimax/minimax-m2.5` | `anthropic-messages` | 204800 | | `vercel-ai-gateway/minimax/minimax-m2.5-highspeed` | `anthropic-messages` | 204800 | | `vercel-ai-gateway/minimax/minimax-m2.7` | `anthropic-messages` | 204800 | | `vercel-ai-gateway/minimax/minimax-m2.7-highspeed` | `anthropic-messages` | 204800 | | `vercel-ai-gateway/minimax/minimax-m3` | `anthropic-messages` | 512000 | | `vercel-ai-gateway/mistral/codestral` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/mistral/ministral-14b` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/mistral/ministral-3b` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/mistral/ministral-8b` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/mistral/mistral-large-3` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/mistral/mistral-large-4` | `anthropic-messages` | 524288 | | `vercel-ai-gateway/mistral/mistral-medium-3.5` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/mistral/mistral-nemo` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/mistral/mistral-small` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/mixedbread/toast-1` | `anthropic-messages` | 131000 | | `vercel-ai-gateway/moonshotai/kimi-k2` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/moonshotai/kimi-k2.5` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/moonshotai/kimi-k2.6` | `anthropic-messages` | 262000 | | `vercel-ai-gateway/moonshotai/kimi-k2.7-code` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/moonshotai/kimi-k2.7-code-highspeed` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/moonshotai/kimi-k3` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/moonshotai/kimi-k3-fast` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/nvidia/nemotron-3-nano-30b-a3b` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/nvidia/nemotron-3-super-120b-a12b` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/nvidia/nemotron-3-ultra-550b-a55b` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/nvidia/nemotron-3.5-lightning` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/nvidia/nemotron-nano-12b-v2-vl` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/nvidia/nemotron-nano-9b-v2` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/openai/gpt-3.5-turbo` | `anthropic-messages` | 16385 | | `vercel-ai-gateway/openai/gpt-4-turbo` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/gpt-4.1` | `anthropic-messages` | 1047576 | | `vercel-ai-gateway/openai/gpt-4.1-fast` | `anthropic-messages` | 1047576 | | `vercel-ai-gateway/openai/gpt-4.1-mini` | `anthropic-messages` | 1047576 | | `vercel-ai-gateway/openai/gpt-4.1-mini-fast` | `anthropic-messages` | 1047576 | | `vercel-ai-gateway/openai/gpt-4.1-nano` | `anthropic-messages` | 1047576 | | `vercel-ai-gateway/openai/gpt-4.1-nano-fast` | `anthropic-messages` | 1047576 | | `vercel-ai-gateway/openai/gpt-4o` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/gpt-4o-fast` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/gpt-4o-mini` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/gpt-4o-mini-fast` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/gpt-5` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5-codex` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5-fast` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5-mini` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5-mini-fast` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5-nano` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5-pro` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.1-codex` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.1-codex-max` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.1-codex-mini` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.1-thinking` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.1-thinking-fast` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.2` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.2-codex` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.2-fast` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.2-pro` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.3-codex` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.3-codex-fast` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.4` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.4-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.4-mini` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.4-mini-fast` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.4-nano` | `anthropic-messages` | 400000 | | `vercel-ai-gateway/openai/gpt-5.4-pro` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.5` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/openai/gpt-5.5-fast` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/openai/gpt-5.5-pro` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/openai/gpt-5.6-luna` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.6-luna-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.6-sol` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.6-sol-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.6-terra` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-5.6-terra-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6-astra` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6-astra-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6-luna` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6-luna-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6-sol` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6-sol-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6.1-sol` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-6.1-sol-fast` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/openai/gpt-oss-120b` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/openai/gpt-oss-20b` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/openai/gpt-oss-safeguard-120b` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/gpt-oss-safeguard-20b` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/openai/o1` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/openai/o3` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/openai/o3-fast` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/openai/o3-mini` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/openai/o3-pro` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/openai/o4-mini` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/openai/o4-mini-fast` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/poolside/laguna-s-2.1` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/poolside/laguna-s-2.1-free` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/quiverai/arrow-2` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/quiverai/arrow-2-telos` | `anthropic-messages` | 131072 | | `vercel-ai-gateway/sakana/fugu-max` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/sakana/fugu-ultra` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/sakana/fugu-ultra-v2` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/sakana/namazu` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/spacexai/grok-4.1-fast-non-reasoning` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/spacexai/grok-4.1-fast-reasoning` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/spacexai/grok-4.20-multi-agent` | `anthropic-messages` | 2000000 | | `vercel-ai-gateway/spacexai/grok-4.20-multi-agent-beta` | `anthropic-messages` | 2000000 | | `vercel-ai-gateway/spacexai/grok-4.20-non-reasoning` | `anthropic-messages` | 2000000 | | `vercel-ai-gateway/spacexai/grok-4.20-non-reasoning-beta` | `anthropic-messages` | 2000000 | | `vercel-ai-gateway/spacexai/grok-4.20-reasoning` | `anthropic-messages` | 2000000 | | `vercel-ai-gateway/spacexai/grok-4.20-reasoning-beta` | `anthropic-messages` | 2000000 | | `vercel-ai-gateway/spacexai/grok-4.3` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/spacexai/grok-4.5` | `anthropic-messages` | 500000 | | `vercel-ai-gateway/spacexai/grok-4.6` | `anthropic-messages` | 500000 | | `vercel-ai-gateway/spacexai/grok-4.7` | `anthropic-messages` | 500000 | | `vercel-ai-gateway/spacexai/grok-build-0.1` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/stealth/glyph-cluster` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/stepfun/step-3.7-flash` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/tencent/hy3` | `anthropic-messages` | 262144 | | `vercel-ai-gateway/tencent/hy4-preview` | `anthropic-messages` | 1024000 | | `vercel-ai-gateway/thinkingmachines/inkling` | `anthropic-messages` | 256000 | | `vercel-ai-gateway/thinkingmachines/inkling-small` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/xiaomi/mimo-v2.5` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/xiaomi/mimo-v2.5-pro` | `anthropic-messages` | 1050000 | | `vercel-ai-gateway/xiaomi/mimo-v2.6-flash` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/xiaomi/mimo-v2.6-pro` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/xiaomi/mimo-v2.6-pro-ultraspeed` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/zai/glm-4.5` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/zai/glm-4.5-air` | `anthropic-messages` | 128000 | | `vercel-ai-gateway/zai/glm-4.5v` | `anthropic-messages` | 66000 | | `vercel-ai-gateway/zai/glm-4.6` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/zai/glm-4.7` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/zai/glm-4.7-flash` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/zai/glm-4.7-flashx` | `anthropic-messages` | 200000 | | `vercel-ai-gateway/zai/glm-5` | `anthropic-messages` | 202800 | | `vercel-ai-gateway/zai/glm-5-turbo` | `anthropic-messages` | 202800 | | `vercel-ai-gateway/zai/glm-5.1` | `anthropic-messages` | 202800 | | `vercel-ai-gateway/zai/glm-5.2` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/zai/glm-5.2-fast` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/zai/glm-5.3` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/zai/glm-5.3-fast` | `anthropic-messages` | 1048576 | | `vercel-ai-gateway/zai/glm-5.3-flash` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/zai/glm-5.3-flashx` | `anthropic-messages` | 1000000 | | `vercel-ai-gateway/zai/glm-5v-turbo` | `anthropic-messages` | 200000 | ## xai [#xai] | Model ID | Transport | Context window | | --- | --- | ---: | | `xai/grok-4.3` | `openai-responses` | 1000000 | | `xai/grok-4.5` | `openai-responses` | 500000 | | `xai/grok-4.6` | `openai-responses` | 500000 | | `xai/grok-4.7` | `openai-responses` | 500000 | ## xiaomi-token-plan-ams [#xiaomi-token-plan-ams] | Model ID | Transport | Context window | | --- | --- | ---: | | `xiaomi-token-plan-ams/mimo-v2.5` | `openai-completions` | 1048576 | | `xiaomi-token-plan-ams/mimo-v2.5-pro` | `openai-completions` | 1048576 | | `xiaomi-token-plan-ams/mimo-v2.6-flash` | `openai-completions` | 1048576 | | `xiaomi-token-plan-ams/mimo-v2.6-pro` | `openai-completions` | 1048576 | ## xiaomi-token-plan-cn [#xiaomi-token-plan-cn] | Model ID | Transport | Context window | | --- | --- | ---: | | `xiaomi-token-plan-cn/mimo-v2.5` | `openai-completions` | 1048576 | | `xiaomi-token-plan-cn/mimo-v2.5-pro` | `openai-completions` | 1048576 | | `xiaomi-token-plan-cn/mimo-v2.6-flash` | `openai-completions` | 1048576 | | `xiaomi-token-plan-cn/mimo-v2.6-pro` | `openai-completions` | 1048576 | ## xiaomi-token-plan-sgp [#xiaomi-token-plan-sgp] | Model ID | Transport | Context window | | --- | --- | ---: | | `xiaomi-token-plan-sgp/mimo-v2.5` | `openai-completions` | 1048576 | | `xiaomi-token-plan-sgp/mimo-v2.5-pro` | `openai-completions` | 1048576 | | `xiaomi-token-plan-sgp/mimo-v2.6-flash` | `openai-completions` | 1048576 | | `xiaomi-token-plan-sgp/mimo-v2.6-pro` | `openai-completions` | 1048576 | ## xiaomi [#xiaomi] | Model ID | Transport | Context window | | --- | --- | ---: | | `xiaomi/mimo-v2.5` | `openai-completions` | 1048576 | | `xiaomi/mimo-v2.5-pro` | `openai-completions` | 1048576 | | `xiaomi/mimo-v2.5-pro-ultraspeed` | `openai-completions` | 1048576 | | `xiaomi/mimo-v2.6-flash` | `openai-completions` | 1048576 | | `xiaomi/mimo-v2.6-pro` | `openai-completions` | 1048576 | | `xiaomi/mimo-v2.6-pro-ultraspeed` | `openai-completions` | 1048576 | ## zai-coding-cn [#zai-coding-cn] | Model ID | Transport | Context window | | --- | --- | ---: | | `zai-coding-cn/glm-4.6v` | `openai-completions` | 128000 | | `zai-coding-cn/glm-5.3` | `openai-completions` | 1000000 | | `zai-coding-cn/glm-5.3-flash` | `openai-completions` | 1000000 | | `zai-coding-cn/glm-5.3-highspeed` | `openai-completions` | 1000000 | ## zai [#zai] | Model ID | Transport | Context window | | --- | --- | ---: | | `zai/glm-4.7` | `openai-completions` | 204800 | | `zai/glm-5-turbo` | `openai-completions` | 200000 | | `zai/glm-5.2` | `openai-completions` | 1000000 | | `zai/glm-5.2-highspeed` | `openai-completions` | 1000000 | | `zai/glm-5.3` | `openai-completions` | 1000000 | | `zai/glm-5.3-flash` | `openai-completions` | 1000000 | | `zai/glm-5.3-highspeed` | `openai-completions` | 1000000 | # Models & providers (/brain/docs/reference/models) Brain delegates model inference to the harness runner while keeping provider credentials and transport configuration under host control. When you create a session, you set its model and optional reasoning effort in `config`. ## Model ID format and validation [#model-id-format-and-validation] Pass `config.model` in `provider/model` format (`1`–`256` characters) when calling `POST /sessions`: ```json { "harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "reasoning_effort": "low", "tools": [] } } ``` Model validation happens in two steps: 1. **Session creation (`POST /sessions`):** Brain splits `config.model` on the first `/`, verifies that the `provider` prefix exists in your host config's `providers` map, and checks that the configured `api_key_env` environment variable is non-empty. Missing slashes, unknown providers, or missing credentials fail immediately with `400 invalid_request`. 2. **Turn execution (Pi runner startup):** When the first `user.message` starts the Pi runner, the runner looks up `provider` and `model` in its pinned offline [Model catalog](/brain/docs/reference/model-catalog) (`allowModelNetwork: false`, `refreshOnCreate: false`). If the model ID is not in the catalog or the harness falls back to a different model, the turn fails with a `session.error` (`brain_error`) event. Brain authenticates providers with API keys. It passes the resolved key to the runner over stdio in an in-memory credential store (`InMemoryCredentialStore`) and strips ambient host environment variables when spawning child processes. ## Configure providers [#configure-providers] Declare each provider prefix in the `providers` map of your host configuration file (`brain.json`): ```json { "providers": { "openai": { "api_key_env": "OPENAI_API_KEY" }, "anthropic": { "api_key_env": "ANTHROPIC_API_KEY" } } } ``` Every provider entry requires `api_key_env`, and that environment variable must be set and non-empty when `brain` starts (or when running `brain -check`). If a provider's API key changes in the host environment while Brain is running, Brain recycles any idle runner using the old credential before the next turn. ## Reasoning effort [#reasoning-effort] Set `config.reasoning_effort` when creating a session to control extended thinking or reasoning tokens: | Value | Description | | --- | --- | | `"off"` | Default when `reasoning_effort` is omitted or `""`. Disables extended reasoning. | | `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` | Requests the corresponding thinking level from the Pi harness. | - Any value outside this set is rejected at session creation with `400 invalid_request`. - When the Pi runner opens the session, it checks `session.thinkingLevel === options.config.reasoning_effort`. If the selected model does not support the requested reasoning effort, the runner fails the turn explicitly with `session.error` (`"requested reasoning effort is unsupported by the model"`) rather than silently downgrading it. ## Custom endpoints and proxies [#custom-endpoints-and-proxies] Each entry in `providers` accepts optional `base_url` and `api` fields to route a catalog provider through an HTTP(S) proxy or OpenAI-compatible gateway: ```json { "providers": { "openai": { "api_key_env": "OPENAI_API_KEY", "base_url": "https://my-proxy.example.com/v1", "api": "openai-completions" } } } ``` - **`base_url`:** Must be a valid `http` or `https` URL with a host and no embedded user credentials. - **`api`:** Overrides the transport protocol used by the Pi runtime for that provider (for example, `"openai-completions"` or `"openai-responses"`). - **Catalog scope:** `base_url` and `api` override transport settings for models that already exist under that provider in the [Model catalog](/brain/docs/reference/model-catalog); they do not register arbitrary new model IDs. ## Prompt caching [#prompt-caching] Brain and the Pi runner are structured to keep provider prompt prefixes stable across turns, evictions, and checkpoint restores: - **Stable prompt prefixes:** The system prompt (`config.instructions`) and tool definitions (`config.tools`) stay constant while a turn runs so provider prompt prefixes remain cacheable across model calls. - **Clean system prompts:** The Pi runner overrides `before_agent_start` so Pi uses your exact `config.instructions` (defaulting to `"You are a helpful assistant. Use the available tools when needed."`) without appending the runner's private local working directory path, which would invalidate prompt caches when a session moves to another server. - **Native state restoration:** Reloading an evicted session or restoring an external checkpoint opens Pi's native `.jsonl` session file (`SessionManager.open`) instead of rebuilding prompts from flattened event text. - **No background cache warming:** Upstream background cache-warming timers are disabled (`cacheWarming: "off"`) so idle sessions never make background LLM calls or incur unexpected token charges. # Roadmap (/brain/docs/roadmap) Brain is in active development. This page lists what you can use today, what's coming next with today's workaround for each item, and how each integration has been verified. To ask about an item or suggest one, open an [issue](https://github.com/aexhq/brain/issues). ## Available today [#available-today] - **[Sessions with the Pi harness](https://aexhq.github.io/brain/docs/guides/sessions/):** create sessions, stream events, interrupt and resume turns, and recover sessions after a restart. - **SDKs:** the [TypeScript SDK](https://aexhq.github.io/brain/docs/sdks/typescript/) on npm and the [Python SDK](https://aexhq.github.io/brain/docs/sdks/python/) on PyPI. - **Tools:** [HTTP and SSE tools](https://aexhq.github.io/brain/docs/tools/) in your own services, and [MCP tools](https://aexhq.github.io/brain/docs/tools/mcp/) from remote MCP servers. - **[Persistence](https://aexhq.github.io/brain/docs/guides/persistence/):** checkpoint archives in a local directory, Amazon S3, Cloudflare R2, or Google Cloud Storage, and an optional PostgreSQL session index. - **Tool services:** [sandboxes](https://aexhq.github.io/brain/docs/tools/sandboxes/) on Modal, E2B, and Daytona; [storage and web tools](https://aexhq.github.io/brain/docs/tools/resources/) for S3 or R2 storage, Brave or Tavily search, and public web fetch; and an [example tool server](https://aexhq.github.io/brain/docs/tools/local-tools/) with operator approvals. - **[Platform examples](https://aexhq.github.io/brain/docs/guides/platforms/):** templates for Docker Compose, Fly.io, AWS ECS/Fargate, and a Cloudflare Worker tool service. - **[Operations](https://aexhq.github.io/brain/docs/guides/operations/):** health checks, structured logs, OpenTelemetry traces and metrics, and a published container image. ## Next up [#next-up] ### Application SDK and session-bound integrations [#application-sdk-and-session-bound-integrations] - **Status:** Available for Go application composition, authorized provider bindings, encrypted session credentials, TypeScript/Python runtime functions, direct HTTP/SSE, reverse SSE/HTTP, WebSocket RPC, MCP HTTP/stdio, and optional authorized session reads. [Plan 11](https://github.com/aexhq/brain/blob/main/docs/plans/11-application-sdk.md) records implementation evidence and remaining adapter gates. - **Today:** Use the [Go application SDK](https://aexhq.github.io/brain/docs/sdks/application/) to install providers; declare runtime functions in a backend client and keep its callback connection alive. Browser authentication and automatic reconnection remain separate work. ### Local harness-host supervision [#local-harness-host-supervision] - **Status:** Available for Pi on Linux and Windows. Hosts have separate session occupancy and active-turn caps, private homes, session-local cancellation and eviction, and owned process trees. Tests cover eight concurrent native sessions with exact catalogs and separate context, bounded concurrent admission, detachment while peers continue, shared host crashes, and abrupt parent death. These are local correctness tests, not fleet capacity measurements. - **Today:** Configure `max_shared`, `max_active_turns`, `max_hosts`, and `max_starting` alongside global limits. Other harnesses must pass the same recovery and isolation gates; each session still has one owning Brain instance. ### Dynamic session configuration [#dynamic-session-configuration] Change a session's model, instructions, reasoning effort, and tools between turns. - **Status:** Planned. The [proposed design](https://github.com/aexhq/brain/blob/main/docs/plans/05-api-and-sdks.md#future-configuration-updates-proposed) applies a configuration event while the session is idle, and [plan 08](https://github.com/aexhq/brain/blob/main/docs/plans/08-integration-expansion.md) keeps it as follow-up work. - **Today:** A session's configuration is fixed when you create it. To change it, create a new session with the new configuration. ### More harnesses: OpenCode and Codex [#more-harnesses-opencode-and-codex] Choose OpenCode or Codex for each session through the same API you use for Pi. Each adapter ships once it passes the same tests as Pi: recovery without model or tool calls, an exact tool set, recovery after context compaction, and storage that grows only with new data. - **Status:** Planned. Targeted local probes verified exclusion of the previously observed extra native tools; complete adapters still require recovery, persistence, and isolation qualification. The [harness probes](https://github.com/aexhq/brain/blob/main/docs/research/harness-probes.md) record that evidence and the remaining changes, and [plan 03](https://github.com/aexhq/brain/blob/main/docs/plans/03-harness-adapters.md) tracks the work. - **Today:** Use the Pi harness, or run your own agent loop through the [harness process contract](https://aexhq.github.io/brain/docs/adapters/harness/). ### Tool catalog: browser and Fly Machines tools [#tool-catalog-browser-and-fly-machines-tools] Attach ready-made tools that work in an isolated browser, or that start, inspect, and stop Fly Machines for compute. - **Status:** Planned. [Plan 08](https://github.com/aexhq/brain/blob/main/docs/plans/08-integration-expansion.md#i4-external-tool-catalog) describes both services and their safety boundaries, and [plan 04](https://github.com/aexhq/brain/blob/main/docs/plans/04-distributed-tools.md) tracks the tool catalog. Release stage [R3](#release-stages). - **Today:** Serve your own tools over HTTP, SSE, or MCP. The [sandbox](https://aexhq.github.io/brain/docs/tools/sandboxes/) and [storage and web](https://aexhq.github.io/brain/docs/tools/resources/) tool services are available. ### Fleet-scale operations [#fleet-scale-operations] Size a fleet from published evidence: multi-instance load and soak benchmarks with their hardware and workload details, plus handoff and disaster-recovery drills. - **Status:** Planned. [Plan 08](https://github.com/aexhq/brain/blob/main/docs/plans/08-integration-expansion.md#i6-performance-and-operational-evidence) targets 1,000 retained sessions, then 1,000 active sessions across several servers. [Plan 06](https://github.com/aexhq/brain/blob/main/docs/plans/06-observability-and-operations.md) and [plan 07](https://github.com/aexhq/brain/blob/main/docs/plans/07-verification-and-release.md) own the operations work and release gates. Release stage [R4](#release-stages). Local tests don't establish thousand-session capacity, and the project doesn't claim it. - **Today:** Load-test with your own workload before you size a deployment, and route each session to a single owner. [Operations](https://aexhq.github.io/brain/docs/guides/operations/) covers capacity limits and routing across servers. ### Steering a running turn [#steering-a-running-turn] Add guidance to a turn while it runs. The API reserves `delivery: "steer"` for this. - **Status:** Planned. No design document yet. - **Today:** Queue your next message, which runs after the current turn, or interrupt the turn and send a new message. ### SDK auto-reconnect [#sdk-auto-reconnect] SDK event streams reconnect on their own after a dropped connection. - **Status:** Planned. No design document yet. - **Today:** Reconnect by passing the cursor of the last event you processed. See [Session lifecycle](https://aexhq.github.io/brain/docs/guides/sessions/). ### Browser clients [#browser-clients] Call Brain directly from browser apps, with cross-origin (CORS) support and browser-safe authentication. - **Status:** Planned. No design document yet. - **Today:** Proxy requests through your backend, so the Brain token stays on your server. ### OAuth for MCP servers and model providers [#oauth-for-mcp-servers-and-model-providers] Connect MCP servers and model providers that require OAuth. - **Status:** Planned. No design document yet. - **Today:** Use API keys and tokens from environment variables. See [Models & providers](https://aexhq.github.io/brain/docs/reference/models/) and [MCP tools](https://aexhq.github.io/brain/docs/tools/mcp/). ## Integration status [#integration-status] Local tests and live runs are separate evidence. **Verified against live services** means the integration passed against a real provider account; **verified with local tests** means it passed with the provider's official SDK or local fixtures, without a live account. The [integration evidence](https://github.com/aexhq/brain/blob/main/docs/research/qualification.md) records exactly what each run covered. | Integration | Status | Notes | | --- | --- | --- | | Amazon S3 archive | Verified against live services | Passed live cold-restore qualification in AWS eu-west-2, including immutable writes, restore into an empty directory, and older-checkpoint restore. This covers the tested standard bucket and IAM setup, not every S3-compatible service or policy. | | Cloudflare R2 archive | Verified with local tests | The real AWS SDK transport passes the archive fixtures with R2 settings. Not yet run against a live R2 account. | | Google Cloud Storage archive | Verified with local tests | The official SDK passes multipart, precondition, read, and shared archive contract tests. Not yet run against a live GCS account. | | PostgreSQL session index | Verified with local tests | CI runs the index tests against a PostgreSQL 18 server. Hosted services such as Neon, Supabase, and RDS haven't been qualified separately. | | MCP tools | Verified with local tests | Official SDK HTTP and owned-stdio fixtures cover explicit discovery, process loss, progress, structured output, tenant isolation, limits, cancellation, and no replay. No third-party MCP server has been tested live. | | Modal, E2B, and Daytona sandboxes | Verified with local tests | Pinned SDK type checks, an HTTP tool journey, signed sandbox handles that stay valid across service restarts, and cancellation and cleanup fixtures. A live script, `npm --prefix services/sandbox run live -- PROVIDER`, needs your provider credentials. | | Storage and web tools | Verified with local tests | An AWS SDK and HTTP registry journey covers authentication, tenant prefixes, read-only policy, and deletes, alongside Brave and Tavily request fixtures and public-network and HTML extraction tests. Live search and R2 tool accounts are unqualified. | | Platform templates | Verified with local tests | `tests/platforms.test.mjs` validates the Docker Compose, Fly.io, AWS ECS/Fargate, and Cloudflare Worker templates. They aren't evidence of a live deployment on each platform. | | Cloudflare Containers as a Brain runtime | Experimental | Cloudflare Containers run container images with Durable Objects for routing. This runtime stays experimental until container lifecycle and handoff tests pass. A standard Cloudflare Worker can host tool services, not Brain. | | Google Cloud Run as a Brain runtime | Not validated | Turns keep running after the send-message request (`POST /sessions/{id}/events`) returns `202 Accepted`, so request-based CPU allocation throttles them. Instance-based CPU avoids that, but termination windows, request timeouts, and single-owner routing still need validation. Cloud Run's in-memory file system also needs conservative cache limits. | ## Release stages [#release-stages] Brain is built in stages: a solid local session server first, then portable session state, more harnesses, a catalog of ready-made tools, and evidence for running a fleet. | Stage | Goal | Scope | Verification gate | Status | | --- | --- | --- | --- | --- | | **R0: Local preview** | Run and manage local Pi sessions through one server. | Session records in SQLite, append-only event and harness-state files, the session and streaming API, interrupt and resume, HTTP and SSE tools, approval and inspection examples, the TypeScript SDK, OpenTelemetry, and bounded queues. | End-to-end Pi lifecycle, restart recovery, separate tool and session errors, responsiveness during tool waits, tenant isolation, stream reconnects, and overload checks. | Available | | **R1: Portable state** | Move or recover sessions across servers with external object storage. | S3, R2, and filesystem checkpoint stores, an optional PostgreSQL session index, background batching and retries, and explicit flush, sync, and restore. | PostgreSQL and S3/R2 contract suites, restore into an empty cache with the model's context intact, interrupted-upload tests, and backpressure tests during archive outages. | Available | | **R2: Multi-harness beta** | Choose Pi, OpenCode, or Codex per session through the same API. | Pinned OpenCode and Codex adapters, the MCP tool transport, native resume and rewind where a harness supports them, and the Python SDK. | Conformance tests for next-turn context after restore, input queueing during tool waits, error boundaries, and fully disabling a harness's own local tools. | Partly available: MCP tools and the Python SDK. OpenCode and Codex are [next up](#more-harnesses-opencode-and-codex). | | **R3: Tool catalog beta** | Attach ready-made storage, web research, browser, and compute tools. | External tool services for S3 and R2 buckets, Fly and Modal sandboxes, code execution, browser bridges, and web search and fetch. | Isolation, quota, approval, and cleanup tests for each service against real providers. | Partly available: storage, web search and fetch, and Modal, E2B, and Daytona sandboxes, verified with local tests. Browser and Fly Machines tools are [next up](#tool-catalog-browser-and-fly-machines-tools). | | **R4: Operational release** | Run Brain as a predictable compute unit in a multi-tenant fleet. | Multi-instance load and soak benchmarks, long-session compaction and checkpoint measurements, and handoff and disaster-recovery drills. | Every advertised adapter passes credentialed CI, soak suites, and restore drills on fresh infrastructure, with published hardware and workload numbers. | Planned | ## Design documents [#design-documents] These documents are for contributors. They record how Brain is designed and how the work is split up. - [ADR 0001: Local-first ephemeral session server](https://github.com/aexhq/brain/blob/main/docs/adr/0001-local-first-session-server.md) records the architecture, ownership boundaries, and design decisions. - [ADR 0002: Subsystem contracts and published SDKs](https://github.com/aexhq/brain/blob/main/docs/adr/0002-subsystem-contracts-and-sdk-delivery.md) extends it with subsystem contracts and SDK delivery. - [ADR 0003: Application SDK, session bindings, and harness integration](https://github.com/aexhq/brain/blob/main/docs/adr/0003-application-sdk-and-session-bindings.md) proposes application composition and session-bound integrations, with the consequential alternatives still open. - The [implementation plan](https://github.com/aexhq/brain/blob/main/docs/implementation-plan.md) breaks the work down across runtime, persistence, harnesses, tools, API, and operations. - Research: [integration options](https://github.com/aexhq/brain/blob/main/docs/research/integration-options.md), [harness feasibility probes](https://github.com/aexhq/brain/blob/main/docs/research/harness-probes.md), [application SDK architecture spikes](https://github.com/aexhq/brain/blob/main/docs/research/application-sdk-spikes.md), and [integration evidence](https://github.com/aexhq/brain/blob/main/docs/research/qualification.md). [Plan 10: Published clients and subsystem boundaries](https://github.com/aexhq/brain/blob/main/docs/plans/10-modernization.md) is implemented and deployed. [Plan 08: Integration expansion](https://github.com/aexhq/brain/blob/main/docs/plans/08-integration-expansion.md) and [plan 09: Delivery, documentation, and platform examples](https://github.com/aexhq/brain/blob/main/docs/plans/09-delivery-and-examples.md) have landed their first cuts. Their remaining work, including dynamic configuration, browser and Fly Machines tools, and fleet benchmarks, is listed under [Next up](#next-up). | Area | Plan | Target stages | | --- | --- | --- | | Server runtime and local state | [01 Local runtime](https://github.com/aexhq/brain/blob/main/docs/plans/01-local-runtime.md) | R0 | | Object checkpoints and PostgreSQL index | [02 External persistence](https://github.com/aexhq/brain/blob/main/docs/plans/02-external-persistence.md) | R1 | | Pi, OpenCode, and Codex runners | [03 Harness adapters](https://github.com/aexhq/brain/blob/main/docs/plans/03-harness-adapters.md) | R0 (Pi), R2 (OpenCode, Codex) | | HTTP/SSE, MCP, and external tool catalog | [04 Distributed tools](https://github.com/aexhq/brain/blob/main/docs/plans/04-distributed-tools.md) | R0 (HTTP/SSE), R2 (MCP), R3 (catalog) | | HTTP contract and TypeScript/Python SDKs | [05 API and SDKs](https://github.com/aexhq/brain/blob/main/docs/plans/05-api-and-sdks.md) | R0–R2 | | Telemetry, limits, Docker, and capacity | [06 Observability and operations](https://github.com/aexhq/brain/blob/main/docs/plans/06-observability-and-operations.md) | R0–R4 | | Test suites and release gates | [07 Verification and release](https://github.com/aexhq/brain/blob/main/docs/plans/07-verification-and-release.md) | R0–R4 | | Cloud storage, MCP, tool catalog, harnesses, and capacity evidence | [08 Integration expansion](https://github.com/aexhq/brain/blob/main/docs/plans/08-integration-expansion.md) | R1–R4 | # Go application SDK (/brain/docs/sdks/application) Build your application with `github.com/aexhq/brain`. The stock `cmd/brain` executable uses this package too. Pin the module to the commit recorded in your [release](https://github.com/aexhq/brain/releases): ```sh go get github.com/aexhq/brain@COMMIT ``` ## Run an application [#run-an-application] Start with the [complete greeting application](https://github.com/aexhq/brain/blob/main/examples/application/main.go). It loads `brain.json`, installs a `greetings` provider, authorizes the `local` tenant, and owns shutdown through `Server.Run`. ```sh go run ./examples/application ``` Create a session that selects the installed operation and its resource parameters: ```json { "harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "tools": [], "bindings": [{ "alias": "welcome", "provider": "greetings", "operations": ["hello"], "parameters": {"prefix": "Hello, "} }] } } ``` The model sees `welcome_hello`. Bind the provider again with another alias to use different parameters in the same session. Keep action execution inside `Handler`; `Authorize` and `Bind` resolve permitted resources and descriptors without performing an action. Handlers receive trusted tenant, session, and call identity separately from model arguments. Honor the supplied context for cancellation and deadlines. Return text or image blocks in `Result.Content`, and the successful JSON value in `Result.StructuredContent`. An `output_schema` validates that value. Return `brain.ToolError(message, unknownOutcome)` for failures; mark the outcome unknown when the action may already have happened. ## Connect managed services [#connect-managed-services] Import `github.com/aexhq/brain/integrations` for `HTTP`, `MCP`, and `Sessions` constructors. Supply a parameter schema, an authorization function, and a resolver that returns only application-approved endpoints. Select MCP names explicitly. MCP stdio commands and environment values are installed by application code; clients select bindings, never commands. Each MCP discovery or invocation owns its connection. Stdio uses a private directory and an owned process tree that closes after the operation. HTTP connections are pooled; MCP sessions are separate per operation. Changed discovered descriptors reject a saved session before an action runs. Use `integrations.Sessions` with a `SessionReader` and an explicit target authorization function to expose public session metadata and bounded event pages. The authenticated binding supplies the tenant. Authorize each target independently; the operation returns neither private credentials nor native files. ## Persist private overrides [#persist-private-overrides] Provide `Options.Keys` or configure [`credential_keys`](/brain/docs/reference/configuration#private-credential-keys) before accepting direct credentials. Keep keys independently of the SQL database and checkpoint archive. ```json { "harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "tools": [], "bindings": [{"alias":"records","provider":"installed_data","operations":["read"],"parameters":{"resource":"permitted-resource"}}] }, "credentials": [{"binding":"records","value":{"token":"SESSION_RESOURCE_TOKEN"}}] } ``` The installed provider authorizes and interprets the private value through `BindingContext.Credential`. Use deployment defaults only when the value is absent. SQL stores authenticated encrypted records, and checkpoints carry encrypted recovery records. Public session data exposes only the aliases that require overrides. A missing key, missing record, or failed authentication stops execution; it never switches credentials. Call `Server.RotateCredentials` to re-encrypt a session with the active key, or `Server.RevokeCredential` to retain a revocation tombstone. Both mark checkpoint state as changed. Retain old keys until their retained checkpoints expire. Existing local private records win over older restored records. Restoring an old backup into an empty deployment restores that backup's credential state, so apply your revocation inventory before resuming it. ## Own the server lifecycle [#own-the-server-lifecycle] Use `brain.Validate` for startup validation before opening storage. `brain.New` returns an owned server. `Run` opens the configured listener; `Serve` accepts a listener you supply. Both drain on context cancellation. When embedding `Handler` in your own HTTP server, always call `Shutdown`. Install custom `HarnessFactory` implementations through `Options.Harnesses`, or use configured process commands. Follow the [harness contract](/brain/docs/adapters/harness) for passive recovery and exact tool catalogs. [Runtime functions](/brain/docs/sdks/typescript#declare-runtime-tools) execute in the client that declares them. # Python SDK (/brain/docs/sdks/python) `Brain` is a synchronous Python 3.11+ client built entirely on the Python standard library (`urllib.request`, `json`). It requires no third-party packages. ## Install [#install] ```sh python -m pip install aex-brain==2.0.0 ``` ## Declare runtime tools [#declare-runtime-tools] Keep your Python process alive while the session uses its functions. The SDK owns a reverse SSE connection and sends results over HTTP. ```python import os from brain import Brain, RuntimeTool client = Brain("http://127.0.0.1:8080", os.environ["BRAIN_API_KEY"]) def lookup(arguments, context): if context.cancelled.is_set(): raise RuntimeError("cancelled") context.progress({"stage": "reading"}) return {"value": "application data for " + context.scope["tenant_id"]} tools = [RuntimeTool("lookup", "Read application data", {"type": "object"}, lookup)] session = client.create({"harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "tools": [] }}, runtime_tools=tools) client.send(session["id"], {"type": "user.message", "text": "Use runtime_lookup."}) stream = client.observe(session["id"]) try: for event in stream: print(event) if event["type"] == "session.status" and event["data"]["status"] == "idle": break finally: stream.close() client.close(session["id"]) client.dispose() ``` Return a JSON value to populate `structured_content`; raise an exception for a tool error. Functions run on bounded callback threads and must cooperate with `context.cancelled`. `context.scope` is authenticated host identity, separate from arguments. Function source and captured credentials are never registered with Brain. `client.dispose()` closes callback connections without closing sessions. Reattach matching descriptors after restart with `client.rebind(session_id, tools)`, then explicitly resume and send a new input. A disconnected callback returns an unavailable or unknown-outcome tool error according to whether dispatch occurred; the SDK never resends an action. The distribution is named `aex-brain`; the import package is `brain`. Wheels and source distributions are published on PyPI and include generated types and the PEP 561 `py.typed` marker. No checkout or `PYTHONPATH` setting is needed. Pin the client and matching server commit from the release workflow. ## Create a client [#create-a-client] Import `Brain` from `brain` and pass the server base URL, your bearer token, and an optional socket `timeout` in seconds (default `30`): ```python import os from brain import Brain brain = Brain("http://127.0.0.1:8080", os.environ["BRAIN_API_KEY"], timeout=30) ``` Generated `TypedDict` definitions for request and response payloads (`Session`, `SessionConfig`, `CreateSession`, `Input`, `Event`, `EventList`, `SessionList`, `Tool`, `ToolCatalog`, `Call`, `Content`, `Result`, `ToolResult`, `Checkpoint`, `Restore`, `Status`, `SyncStatus`, `ErrorDetail`, `ErrorResponse`) live in `brain.types`. ## Create a session and send a message [#create-a-session-and-send-a-message] Call `create()` with a harness name and the session configuration: `model`, `tools`, and optional `instructions` and `reasoning_effort`. Then call `send()` to queue a `user.message`: ```python session = brain.create({ "harness": "pi", "config": { "model": "openai/gpt-4.1-mini", "instructions": "Answer in two sentences or fewer.", "reasoning_effort": "low", "tools": [], }, }) accepted = brain.send(session["id"], { "type": "user.message", "text": "Explain append-only storage.", "key": "message-1", }) ``` `send()` returns the recorded `user.message` event once Brain writes it to disk (`202 Accepted`). Keep `accepted["id"]` to correlate events emitted during that turn. ## Stream events [#stream-events] Call `observe(session_id)` to receive a synchronous generator over SSE frames. Always close the generator in a `finally` block (or with `contextlib.closing`) when you break out of the loop early so the underlying HTTP socket closes cleanly. Run this script while Brain is running (see `examples/session.py` for the CLI version): ```python import os from brain import Brain brain = Brain("http://127.0.0.1:8080", os.environ["BRAIN_API_KEY"]) session = brain.create({ "harness": "pi", "config": {"model": "openai/gpt-4.1-mini", "tools": []}, }) accepted = brain.send(session["id"], { "type": "user.message", "text": "Explain append-only storage.", "key": "message-1", }) stream = brain.observe(session["id"]) try: for event in stream: if event["type"] == "assistant.delta": print(event["data"]["text"], end="", flush=True) if event["type"] == "assistant.message": print(event["data"]["text"]) if event.get("input_id") == accepted["id"]: if event["type"] == "session.error": raise RuntimeError(event["data"]["message"]) if (event["type"] == "session.status" and event["data"]["status"] in {"idle", "failed", "interrupted", "closed"}): break finally: stream.close() ``` Calling `stream.close()` only closes the client's HTTP connection. The session remains open on the server, and any running turn continues unless you call `brain.interrupt(session["id"])`. ## Reconnect with cursors [#reconnect-with-cursors] Every retained event carries an opaque `cursor` string. Save the latest non-empty `event.get("cursor")` and pass it as `after` to `observe()` to resume streaming from that position: ```python cursor = "" stream = brain.observe(session["id"], after=cursor) try: for event in stream: if event.get("cursor"): cursor = event["cursor"] if event["type"] == "session.status" and event["data"]["status"] == "idle": break finally: stream.close() ``` Live-only events (`assistant.delta` and `tool.progress`) do not include a `cursor`. To read retained events in batches without opening an SSE stream, call `brain.history(session["id"], after=cursor, limit=100)`. ## Handle errors and timeouts [#handle-errors-and-timeouts] Non-2xx HTTP responses raise `BrainError`: ```python from brain import Brain, BrainError try: brain.send(session["id"], {"type": "user.message", "text": "Continue."}) except BrainError as err: print(err.status, err.code, err, err.request_id) raise ``` - **`BrainError` attributes:** `status` (HTTP status int), `code` (machine-readable error code such as `"conflict"` or `"overloaded"`, falling back to `"http_error"`), `request_id` (`str | None`), and the error message via `str(err)`. - **Tool failures vs. HTTP errors:** Failed or denied tool calls do not raise `BrainError`; they appear in the event stream as `tool.result` events with `is_error: True`. Model provider or harness failures appear as `session.error` events. See [Errors](/brain/docs/reference/errors). - **Timeouts:** `Brain(url, token, timeout=30)` sets the socket read/connect timeout in seconds for all calls on that client instance, including `flush()`, `restore()`, and individual socket reads in `observe()` (where keepalives arrive roughly every 15 seconds). Increase `timeout` (for example, `timeout=60`) when calling `flush()` or `restore()` on large checkpoints. `observe()` raises `ValueError` if a single SSE event exceeds 4 MiB or the stream closes mid-frame. - **No automatic retries or redirects:** The client installs a custom `HTTPRedirectHandler` that rejects all HTTP redirects and never retries failed requests automatically. ## Methods reference [#methods-reference] | Method | Signature | Returns | Description | | --- | --- | --- | --- | | `create` | `create(config: CreateSession)` | `Session` | Creates a session (`POST /sessions`). | | `get` | `get(session_id: str)` | `Session` | Fetches session metadata and status (`GET /sessions/{id}`). | | `list` | `list(after: str = "", limit: int = 100)` | `dict[str, Any]` | Returns `{"sessions": [...]}` in ascending ID order (`GET /sessions`). | | `send` | `send(session_id: str, event: Input)` | `Event` | Queues a `user.message` (or sends `user.interrupt`) (`POST /sessions/{id}/events`). | | `interrupt` | `interrupt(session_id: str)` | `None` | Sends `{"type": "user.interrupt"}` to stop the active turn (`POST /sessions/{id}/events`). | | `history` | `history(session_id: str, after: str = "", limit: int = 100)` | `dict[str, Any]` | Returns `{"events": [...], "next_cursor": "..."}` (`GET /sessions/{id}/events`). | | `observe` | `observe(session_id: str, after: str = "")` | `Iterator[Event]` | Yields retained and live events over SSE (`GET /sessions/{id}/events`). | | `resume` | `resume(session_id: str)` | `Session` | Moves an `interrupted` or `failed` session back to `idle` (`POST /sessions/{id}/resume`). | | `close` | `close(session_id: str)` | `None` | Permanently closes a session (`POST /sessions/{id}/close`). | | `flush` | `flush(session_id: str)` | `SyncStatus` | Uploads local state to external storage immediately (`POST /sessions/{id}/flush`). | | `sync` | `sync(session_id: str)` | `SyncStatus` | Reads background checkpoint sync status (`GET /sessions/{id}/sync`). | | `restore` | `restore(session_id: str, checkpoint: str)` | `Session` | Replaces local session state with a specific external checkpoint (`POST /sessions/{id}/restore`). | | `catalog` | `catalog()` | `dict[str, Any]` | Returns `{"tools": [...]}` available to the caller's tenant (`GET /catalog`). | # TypeScript SDK (/brain/docs/sdks/typescript) `Brain` is a zero-dependency TypeScript client for the Brain HTTP API and server-sent event (SSE) streams. It runs on Node.js 24+ using standard `fetch` and `AsyncGenerator` streams. ## Install [#install] ```sh npm install --save-exact @aexhq/brain@2.0.0 ``` ## Declare runtime tools [#declare-runtime-tools] Pass ordinary functions when creating a session. The SDK registers descriptors and opens a callback connection before admitting the session. Functions and captured credentials stay in your Node process. ```typescript import { Brain } from "@aexhq/brain"; const client = new Brain("http://127.0.0.1:8080", process.env.BRAIN_API_KEY!); const session = await client.create({ harness: "pi", config: { model: "openai/gpt-4.1-mini", tools: [] }, runtime_transport: "sse", runtime_tools: [{ name: "lookup", description: "Read application data", input_schema: { type: "object", additionalProperties: false }, output_schema: { type: "object", required: ["value"] }, async run(_args, { signal, scope, progress }) { signal.throwIfAborted(); await progress({ stage: "reading" }); return { value: `data authorized for ${scope.tenant_id}` }; }, }], }); await client.send(session.id, { type: "user.message", text: "Use runtime_lookup." }); for await (const event of client.observe(session.id)) { console.log(event); if (event.type === "session.status" && (event.data as { status: string }).status === "idle") break; } await client.close(session.id); await client.dispose(); ``` The alias `runtime` prefixes each function name. Return a JSON value; the SDK places it in `structured_content` and renders it as text. Throw an error to return a tool failure. Honor `signal` to stop local work after cancellation. Use `runtime_transport: "rpc"` for the WebSocket carrier. Keep the client process alive while callbacks are needed. `dispose()` closes its executors without closing saved sessions. After restart or restore, call `client.rebind(sessionID, matchingTools, "sse")` and explicitly resume/send as required by the session status. Rebinding changes the connection, never replays an action. An unavailable executor fails before dispatch; a connection lost after dispatch reports an unknown outcome. Reading history or observing events cannot execute callbacks. JavaScript and TypeScript use the same ESM package, including generated type declarations. No checkout or compilation is needed. Version 1 replaces the predecessor's incompatible 0.x SDK; use it with the current Go session server. Pin the client and matching server commit from the release workflow. ## Create a client [#create-a-client] Pass the Brain server base URL and your bearer token to `new Brain(url, token, fetchImpl?)`: ```ts import { Brain } from '@aexhq/brain'; const brain = new Brain('http://127.0.0.1:8080', process.env.BRAIN_API_KEY!); ``` You can pass a custom `fetch` implementation as the third argument to add custom headers, logging, or test doubles. ## Create a session and send a message [#create-a-session-and-send-a-message] Call `create()` with a harness name and the session configuration: `model`, `tools`, and optional `instructions` and `reasoning_effort`. Then call `send()` to queue a `user.message`: ```ts const session = await brain.create({ harness: 'pi', config: { model: 'openai/gpt-4.1-mini', instructions: 'Answer in two sentences or fewer.', reasoning_effort: 'low', tools: [], }, }); const input = await brain.send(session.id, { type: 'user.message', text: 'Explain append-only storage.', key: 'message-1', }); ``` `send()` returns the recorded `user.message` event once Brain writes it to disk (`202 Accepted`). Keep `input.id` to match status updates and errors produced during that turn. ## Stream events [#stream-events] Call `observe(session.id)` to iterate over the session's SSE stream. Brain replays retained events first, then streams live events across turns. The stream stays open until you break out of the loop or abort the signal, even after the session closes, so stop when you see the status you're waiting for. Run this `.mjs` script from the repository root while Brain is running (see `examples/session.mjs` for the CLI version): ```js import { Brain } from '@aexhq/brain'; const brain = new Brain('http://127.0.0.1:8080', process.env.BRAIN_API_KEY); const session = await brain.create({ harness: 'pi', config: { model: 'openai/gpt-4.1-mini', tools: [] }, }); const input = await brain.send(session.id, { type: 'user.message', text: 'Explain append-only storage.', key: 'message-1', }); for await (const event of brain.observe(session.id)) { if (event.type === 'assistant.delta') { process.stdout.write(event.data.text); } if (event.type === 'assistant.message') { console.log(event.data.text); } if (event.input_id === input.id && event.type === 'session.error') { throw new Error(event.data.message); } if ( event.input_id === input.id && event.type === 'session.status' && ['idle', 'failed', 'interrupted', 'closed'].includes(event.data.status) ) { break; } } ``` Exiting the `for await` loop cancels the client's HTTP stream without stopping the session on the server. To stop an active turn, call `await brain.interrupt(session.id)`. In TypeScript, `event.data` is typed as `unknown`. Check `event.type`, then cast `event.data` to that event's payload from [Events & streaming](/brain/docs/reference/events). All wire types (`Session`, `SessionConfig`, `CreateSession`, `Input`, `Event`, `EventList`, `SessionList`, `Tool`, `ToolCatalog`, `Call`, `Content`, `Result`, `ToolResult`, `Checkpoint`, `Restore`, `Status`, `SyncStatus`, `ErrorDetail`, `ErrorResponse`) are re-exported from the package entrypoint. ## Reconnect with cursors [#reconnect-with-cursors] Every retained event includes an opaque `event.cursor`. Save the latest non-empty cursor as you consume the stream, and pass it in `{ after }` to resume from that exact log position without re-reading earlier history: ```ts let cursor: string | undefined; const controller = new AbortController(); for await (const event of brain.observe(session.id, { after: cursor, signal: controller.signal, })) { if (event.cursor) cursor = event.cursor; if (event.input_id !== input.id) continue; if (event.type === 'session.status' && (event.data as { status: string }).status === 'idle') break; } ``` Live-only events (`assistant.delta` and `tool.progress`) do not carry a `cursor`. You can also fetch paginated retained history without opening an SSE stream by calling `await brain.history(session.id, after, limit)`. ## Handle errors and timeouts [#handle-errors-and-timeouts] Non-2xx HTTP responses throw `BrainError`: ```ts import { Brain, BrainError } from '@aexhq/brain'; try { await brain.send(session.id, { type: 'user.message', text: 'Continue.' }); } catch (err) { if (err instanceof BrainError) { console.error(err.status, err.code, err.message, err.requestId); } throw err; } ``` - **`BrainError` fields:** `status` (HTTP status number), `code` (machine-readable string such as `"conflict"` or `"overloaded"`, falling back to `"http_error"`), `message`, and `requestId` (from the response body's `request_id`). - **Tool failures vs. HTTP errors:** Failed or denied tool calls do not throw `BrainError`; they arrive in the event stream as `tool.result` events with `result.is_error: true`. Model provider or harness crashes arrive as `session.error` events. See [Errors](/brain/docs/reference/errors). - **Timeouts:** Standard requests default to `AbortSignal.timeout(30_000)` (30 seconds). `flush()` and `restore()` default to `AbortSignal.timeout(60_000)` (60 seconds). `observe()` has no overall wall-clock timeout and rejects individual SSE events larger than 4 MiB or streams that end mid-frame. - **No automatic retries or redirects:** The client sets `redirect: "error"` and never retries failed requests automatically. ## Methods reference [#methods-reference] | Method | Signature | Returns | Description | | --- | --- | --- | --- | | `create` | `create(input: CreateSession, signal?: AbortSignal)` | `Promise` | Creates a session (`POST /sessions`). Default timeout: 30s. | | `get` | `get(id: string, signal?: AbortSignal)` | `Promise` | Fetches session metadata and status (`GET /sessions/{id}`). Default timeout: 30s. | | `list` | `list(after = "", limit = 100)` | `Promise<{ sessions: Session[] }>` | Lists tenant sessions in ascending ID order (`GET /sessions`). | | `send` | `send(id: string, input: Input, signal?: AbortSignal)` | `Promise` | Queues a `user.message` and returns the recorded event (`POST /sessions/{id}/events`). Default timeout: 30s. | | `interrupt` | `interrupt(id: string)` | `Promise` | Sends `{"type": "user.interrupt"}` to stop the active turn (`POST /sessions/{id}/events`). | | `history` | `history(id: string, after = "", limit = 100)` | `Promise<{ events: Event[]; next_cursor: string }>` | Returns a page of retained events and the next cursor (`GET /sessions/{id}/events`). | | `observe` | `observe(id: string, options?: { after?: string; signal?: AbortSignal })` | `AsyncGenerator` | Streams retained and live events over SSE (`GET /sessions/{id}/events`). | | `resume` | `resume(id: string)` | `Promise` | Moves an `interrupted` or `failed` session back to `idle` (`POST /sessions/{id}/resume`). | | `close` | `close(id: string)` | `Promise` | Permanently closes a session (`POST /sessions/{id}/close`). | | `flush` | `flush(id: string, signal?: AbortSignal)` | `Promise` | Uploads local state to external storage immediately (`POST /sessions/{id}/flush`). Default timeout: 60s. | | `sync` | `sync(id: string)` | `Promise` | Reads background checkpoint sync status (`GET /sessions/{id}/sync`). | | `restore` | `restore(id: string, checkpoint: string, signal?: AbortSignal)` | `Promise` | Replaces local session state with a specific external checkpoint (`POST /sessions/{id}/restore`). Default timeout: 60s. | | `catalog` | `catalog()` | `Promise<{ tools: Tool[] }>` | Lists tools available to the caller's tenant (`GET /catalog`). | # Distributed tools (/brain/docs/tools) Tools let the agent act outside the conversation: run code in a sandbox, read and write storage, search the web, or ask a person for approval. In Brain, every tool runs outside the server, in a service you deploy and write in any language. You declare each tool in the host config, enable tools for a session by name, and Brain calls the tool over HTTP when the model asks for it. The model sees exactly the tools you enable for its session, so you decide everything the agent can do. ## Ways to provide tools [#ways-to-provide-tools] } title="HTTP JSON" href="#return-a-json-result">Answer each call with one JSON result. } title="Streaming SSE" href="#stream-progress-with-sse">Send progress events, then the result, over server-sent events. } title="MCP tools" href="/docs/tools/mcp">Connect tools from a remote MCP server over Streamable HTTP. } title="Local tools & approvals" href="/docs/tools/local-tools">Run the example tool server with echo, session inspection, and operator approval. } title="Sandboxes" href="/docs/tools/sandboxes">Run commands in Modal, E2B, or Daytona sandboxes. } title="Storage & web" href="/docs/tools/resources">Use S3 or R2 storage, Brave or Tavily search, and public web fetch. The local tools, sandbox, and storage services ship with this repository. Run each one as its own service alongside Brain, with its provider keys in its own configuration. | If your tool needs to… | Use | | --- | --- | | Return a result within a few seconds | [HTTP JSON](#return-a-json-result) | | Report progress, or wait for a person | [Streaming SSE](#stream-progress-with-sse) | | Reuse tools from an existing MCP server | [MCP tools](/brain/docs/tools/mcp) | | Run work that can outlast `timeout_seconds` | A tool that starts a job and returns its ID, plus status and cancel tools | ## Configure a tool [#configure-a-tool] Add a binding to the `tools` array of the host config. This binding declares the `echo` tool from the example tool server in `examples/tools`, with the default timeout and output limit written out: ```json { "name": "echo", "description": "Echo text through an external tool service.", "input_schema": { "type": "object", "properties": { "text": { "type": "string", "maxLength": 4096 } }, "required": ["text"], "additionalProperties": false }, "url": "http://127.0.0.1:8081/tools/echo", "token_env": "BRAIN_TOOLS_KEY", "tenants": ["local"], "timeout_seconds": 300, "max_output_bytes": 1048576 } ``` | Field | Required | Description | | --- | --- | --- | | `name` | Yes | The name the model sees. It starts with a letter and has up to 64 letters, digits, or underscores. Names are unique within the host config. | | `description` | Yes | What the tool does. The model reads it to decide when to call the tool. | | `input_schema` | Yes | A JSON Schema for the arguments. Brain validates every call against it, including `format` keywords, before sending the request. External `$ref` references aren't loaded. | | `output_schema` | No | JSON Schema for successful `structured_content`. Tool errors retain their outcome classification; all results still obey content and size limits. | | `url` | Yes | The `http` or `https` endpoint that Brain posts calls to. It can't contain credentials or a fragment. | | `tenants` | Yes | The tenants allowed to use the tool. | | `token_env` | No | An environment variable whose value Brain sends as a bearer token. The variable must be set when Brain starts. | | `timeout_seconds` | No | The time limit for the whole call, from 1 to 86400. Defaults to 300. | | `max_output_bytes` | No | The largest response Brain accepts, up to 2 MiB. Defaults to 1 MiB. | | `inspect_session` | No | Send a read-only token for the session with each call. See [Authentication & tenants](/brain/docs/guides/authentication). | | `mcp_tool` | No | The remote tool name, for tools served by an MCP server. See [MCP tools](/brain/docs/tools/mcp). | Brain checks every binding when it starts and doesn't start if one is invalid. Run `go run ./cmd/brain -config brain.json -check` to validate a config without starting the server. ## Enable tools for a session [#enable-tools-for-a-session] `GET /catalog` lists the tools available to your tenant, with their names, descriptions, and schemas. It never returns a tool's `url` or `token_env`. To give a session tools, pass their names in `config.tools` when you create it. ```sh tab="cURL" curl http://127.0.0.1:8080/catalog \ -H "Authorization: Bearer $BRAIN_API_KEY" ``` ```ts tab="TypeScript" const { tools } = await brain.catalog(); const session = await brain.create({ harness: "pi", config: { model: "openai/gpt-4.1-mini", tools: ["echo"] } }); ``` ```python tab="Python" tools = brain.catalog()["tools"] session = brain.create({"harness": "pi", "config": {"model": "openai/gpt-4.1-mini", "tools": ["echo"]}}) ``` To try tools without writing a service, follow [Local tools & approvals](/brain/docs/tools/local-tools). It generates a host config for the example tool server and runs `examples/session.mjs` with `BRAIN_TOOLS` set to the tools to enable. ## Handle a tool call [#handle-a-tool-call] When the model calls a tool, Brain assigns a call ID, records a `tool.call` event, validates the arguments, and sends a `POST` request to the tool's `url` with these headers: - `Content-Type: application/json` - `Accept: application/json, text/event-stream` - `Authorization: Bearer TOKEN`, when the binding sets `token_env` - `traceparent`, when the request belongs to a trace The body carries the session's identity next to the model's arguments: ```json { "tenant_id": "local", "session_id": "SESSION_ID", "call_id": "CALL_ID", "arguments": { "text": "hello" } } ``` `tenant_id` and `session_id` come from the authenticated session, not from the model. With `inspect_session: true`, the body also has an `inspection_token` and a `brain_url`. Here is a minimal tool service in Node.js: ```ts import http from "node:http"; const token = process.env.BRAIN_TOOLS_KEY; http.createServer(async (req, res) => { if (req.headers.authorization !== `Bearer ${token}`) { res.writeHead(401).end(); return; } let body = ""; for await (const chunk of req) body += chunk; const { tenant_id, session_id, call_id, arguments: args } = JSON.parse(body); // Authorize with tenant_id and session_id. Log call_id to match Brain's tool.call event. res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ content: [{ type: "text", text: args.text }] })); }).listen(8081, "127.0.0.1"); ``` ### Return a JSON result [#return-a-json-result] Respond with status `200`, `Content-Type: application/json`, and a result object: ```json { "content": [{ "type": "text", "text": "hello" }] } ``` | Field | Description | | --- | --- | | `content` | One or more content blocks. A text block is `{"type":"text","text":"..."}`. An image block is `{"type":"image","data":"BASE64","mime_type":"image/png"}`. | | `is_error` | Set to `true` when the call failed or was denied. The model sees the content as an error and decides what to do next. | | `structured_content` | JSON value returned by a successful call. When configured, `output_schema` validates this value. A structured-only result is rendered as a text block for the harness. | | `unknown_outcome` | Set when an action may have happened but its completion is unknown. | Brain rejects a result with neither content nor structured content, an unknown content type, or unknown fields. Error results bypass the success schema while retaining content and size checks. ### Stream progress with SSE [#stream-progress-with-sse] Respond with `Content-Type: text/event-stream` to send progress while the tool works. Send zero or more `progress` events, then exactly one `result` event whose data is a result object: ```text event: progress data: {"status":"waiting_for_operator"} event: result data: {"content":[{"type":"text","text":"Operator answered yes."}]} ``` Each `progress` payload must be JSON. Brain forwards it to connected clients as a live-only `tool.progress` event with the call's `call_id`. Brain ignores comment lines that start with `:`. Any other event name, or a stream that ends without a `result`, fails the call. ## Handle errors, timeouts, and long jobs [#handle-errors-timeouts-and-long-jobs] Every tool call ends with a `tool.result` event. When a call fails, the result has `is_error: true`, and Brain adds `unknown_outcome: true` when the tool may have done the work anyway: | What happened | `is_error` | `unknown_outcome` | | --- | --- | --- | | The tool returned a result with `is_error: true` | Yes | No | | Brain refused the call before sending it: the tool isn't allowed for the tenant, the arguments fail validation, or the credential is missing | Yes | No | | The connection failed, timed out, or dropped | Yes | Yes | | The tool returned a non-2xx status | Yes | Yes | | The response had the wrong content type, an invalid result, too much output, or no `result` event | Yes | Yes | Brain sends each tool call at most once; your tool service owns retries and cleanup. The model sees the error, and with an unknown outcome it can check what happened, for example by calling a status tool. Report failures your tool handles with status `200` and `is_error: true`. Brain ignores the body of a non-2xx response and records the call as an unknown outcome. Brain enforces two time limits on every call: - **Response headers within 30 seconds.** For longer work, send the response headers right away: start an SSE response and send `progress` events until the result is ready. - **The whole call within `timeout_seconds`.** For work that can run longer, return a job ID right away and provide separate tools to check status and cancel the job. Interrupting or closing a session cancels its in-flight tool requests. Work that already started in your service keeps running unless your service stops it, so your service owns cleanup. ## Secure your tool service [#secure-your-tool-service] - **Check the token:** Set `token_env` on every binding and reject requests without the matching bearer token. - **Authorize with the session's identity:** Use the top-level `tenant_id` and `session_id` to decide what the call may access. Never trust IDs that the model put in `arguments`. - **Protect inspection tokens:** An `inspection_token` can read the session's events until the call returns. Don't log or store it. - **Enforce approvals where the action happens:** An approval tool records a person's answer; the service that performs the action must still check its own authorization. - **Expect no redirects:** Brain doesn't follow redirects from tools. It sends tool requests through the proxy in the standard `HTTPS_PROXY` and `HTTP_PROXY` variables when they're set. ## Next steps [#next-steps] } title="Local tools & approvals" href="/docs/tools/local-tools">Run the example tool server and approve a call by hand. } title="MCP tools" href="/docs/tools/mcp">Discover and connect tools from a remote MCP server. } title="Sandboxes" href="/docs/tools/sandboxes">Give the agent a sandbox to run code in. # Local tools & approvals (/brain/docs/tools/local-tools) `examples/tools` is a standalone Go service (`127.0.0.1:8081` by default) that demonstrates Brain's HTTP JSON and SSE tool protocol, delegated session inspection, and operator approval gates. | Tool | Response mode | Description | | --- | --- | --- | | `echo` | HTTP JSON (`application/json`) | Returns the caller's input text. | | `inspect_session` | HTTP JSON (`application/json`) | Reads the calling session's status and recent event history using a short-lived, read-only `inspection_token` and `brain_url` delegated by Brain (`inspect_session: true`). | | `ask_approval` | Streaming SSE (`text/event-stream`) | Emits `event: progress` frames while waiting up to 5 minutes for an operator to approve or deny the tool call over HTTP. | ## Before you begin [#before-you-begin] - Go `1.26+` and Node.js `24+` installed (`npm --prefix runners/pi run build` and `npm --prefix sdks/typescript run build` completed). - An LLM provider API key (for example, `OPENAI_API_KEY`). - Three distinct random tokens of at least 24 bytes each: - `BRAIN_API_KEY` — authenticates client requests to Brain. - `BRAIN_TOOLS_KEY` — authenticates tool dispatch requests from Brain to `examples/tools`. - `BRAIN_APPROVAL_KEY` — authenticates operator approval requests to `examples/tools`. ## 1. Start the tool service [#1-start-the-tool-service] Set `BRAIN_TOOLS_KEY` and `BRAIN_APPROVAL_KEY`, generate a ready-to-use `brain.json` with `-print-config` (which binds all three tools and configures a local `.brain-archive` checkpoint directory), and start the tool server: ```powershell $env:BRAIN_TOOLS_KEY = "a-distinct-random-tool-token-at-least-24-bytes" $env:BRAIN_APPROVAL_KEY = "a-distinct-random-operator-token-at-least-24-bytes" go run ./examples/tools -print-config | Set-Content -Encoding utf8 brain.json go run ./examples/tools ``` ## 2. Start Brain [#2-start-brain] In a second terminal, set `BRAIN_API_KEY`, `OPENAI_API_KEY`, and the same `BRAIN_TOOLS_KEY`, then start Brain with the generated `brain.json`: ```powershell $env:BRAIN_API_KEY = "replace-with-a-random-token-of-at-least-24-bytes" $env:OPENAI_API_KEY = "your-provider-key" $env:BRAIN_TOOLS_KEY = "a-distinct-random-tool-token-at-least-24-bytes" go run ./cmd/brain -config brain.json ``` ## 3. Run a session with tools [#3-run-a-session-with-tools] Pass the tool names in `config.tools` when creating a session (or call `GET /catalog` to inspect available tools). Using the runnable TypeScript example script: ```powershell $env:BRAIN_API_KEY = "replace-with-a-random-token-of-at-least-24-bytes" $env:BRAIN_TOOLS = "echo,inspect_session,ask_approval" node examples/session.mjs "Ask me whether to continue, then report my answer." ``` ## 4. Approve or deny a pending request [#4-approve-or-deny-a-pending-request] When the model calls `ask_approval`, the tool service holds the SSE stream open (up to 5 minutes, maximum 32 concurrent pending requests). From a third terminal, use `BRAIN_APPROVAL_KEY` to list pending requests and approve or deny the call by `CALL_ID`: ```powershell $headers = @{ Authorization = "Bearer $env:BRAIN_APPROVAL_KEY" } Invoke-RestMethod http://127.0.0.1:8081/approvals -Headers $headers Invoke-RestMethod http://127.0.0.1:8081/approvals/CALL_ID -Method Post -Headers $headers -ContentType application/json -Body '{"approved":true}' ``` > **Note:** If an operator denies the request (`{"approved":false}`) or it expires after 5 minutes, the tool service returns an ordinary `is_error: true` tool result to the model so the turn can continue cleanly. Interrupting or closing the session drops the SSE connection and clears the pending request automatically. ## Next steps [#next-steps] - [External tools protocol](https://aexhq.github.io/brain/docs/tools) - [Sandboxes tool service](https://aexhq.github.io/brain/docs/tools/sandboxes) - [Storage & web resource tools](https://aexhq.github.io/brain/docs/tools/resources) - [MCP servers](https://aexhq.github.io/brain/docs/tools/mcp) # MCP tools (/brain/docs/tools/mcp) The Model Context Protocol (MCP) is an open protocol for serving tools to AI applications. Brain connects to remote MCP servers over Streamable HTTP with the official MCP Go SDK (pinned in `go.mod`), so you can give sessions tools from an MCP server you already run. MCP tools go through the same tenant allowlist, input validation, timeout, and output limits as [HTTP tools](/brain/docs/tools). ## What Brain supports [#what-brain-supports] - **Transport:** Remote MCP servers over Streamable HTTP, or installed stdio commands through the [Go application SDK](/brain/docs/sdks/application#connect-managed-services). - **Authentication:** API keys sent as bearer tokens from the environment variable named in `token_env`. - **Results:** Tool calls that return text, images, or structured content. - **Progress:** Progress notifications streamed during a call as `tool.progress` events. ## Discover tools [#discover-tools] Run `cmd/mcp-tools` to turn tools from an MCP server into Brain tool bindings: ```sh go run ./cmd/mcp-tools -url https://YOUR_SERVER/mcp -token-env MCP_API_KEY -tenant local -tools search,fetch ``` | Flag | Description | | --- | --- | | `-url` | The server's Streamable HTTP endpoint. | | `-token-env` | The environment variable that holds the server's API key. The command uses it to connect, and the bindings tell Brain to send it as a bearer token. | | `-tenant` | The tenant allowed to use the tools. | | `-tools` | A comma-separated list of 1 to 128 remote tool names to include. | The command connects to the server, reads its tool list, and prints a JSON array with one binding for each named tool. It fails if a named tool doesn't exist or if discovery takes longer than 30 seconds. The output looks like this: ```json [ { "name": "search", "description": "Search the knowledge base.", "input_schema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }, "url": "https://YOUR_SERVER/mcp", "mcp_tool": "search", "token_env": "MCP_API_KEY", "tenants": ["local"], "timeout_seconds": 300, "max_output_bytes": 1048576 } ] ``` Review the descriptions and schemas, then copy the bindings into the `tools` array of your host config. Bindings are static: a tool's description and schema change only when you edit the config and restart Brain, and discovery never changes the tools of a running session. ## Configure an MCP binding [#configure-an-mcp-binding] An MCP binding is a regular tool binding with one extra field, `mcp_tool`: - **`name`** is the name the model sees. Rename tools to avoid collisions, for example `docs_search` for a `search` tool. - **`mcp_tool`** is the tool's name on the MCP server. - **`url`** is the server's MCP endpoint. - **`token_env`** names the environment variable with the server's API key. Brain sends it as a bearer token on every request to the server. `tenants`, `timeout_seconds`, `max_output_bytes`, and `output_schema` work as they do for [HTTP tools](/brain/docs/tools). `inspect_session` applies to HTTP and SSE tools; Brain rejects a host config that sets it on an MCP binding. ## How Brain calls an MCP tool [#how-brain-calls-an-mcp-tool] 1. **Check the call.** Brain confirms that the tool is allowed for the session's tenant and validates the arguments against `input_schema`. 2. **Open an MCP session.** Brain starts a new MCP session with the server for this call, over pooled HTTP connections. A separate MCP session per call keeps server-side session state from crossing tenants or calls, at the cost of one initialization round trip per call. 3. **Call the tool.** Brain sends the arguments and puts the session's identity in `_meta["brain.dev/scope"]` as `tenant_id`, `session_id`, and `call_id`. The call ID is also the progress token. 4. **Stream progress.** Each progress notification from the server becomes a live-only `tool.progress` event. 5. **Return the result.** Brain converts the result into a `tool.result` event, returns it to the harness, and closes the MCP session. The `_meta["brain.dev/scope"]` values tell your server which tenant and session a call belongs to. Your server must still authenticate Brain's API key and enforce access to its own resources. Brain never sends session inspection tokens to MCP servers. ## Results and errors [#results-and-errors] Brain converts MCP results into Brain content blocks: | MCP result | Brain result | | --- | --- | | Text content | A text block. | | Image content | An image block with base64 `data` and `mime_type`. | | Structured content | `structured_content` plus a text block containing the JSON. Successful output schemas validate the structured value. | | `isError: true` | `is_error: true`. The model sees the error as a tool result. | | Any other content type | The call fails. | When a call fails, the `tool.result` has `is_error: true`: | What happened | `unknown_outcome` | | --- | --- | | Brain refused the call or couldn't connect to the server, so the tool was never called | No | | The call failed after Brain sent it: the connection dropped, the call timed out, or the server returned a protocol error | Yes | | The server asked for user input | Yes. Handle approval inside the tool instead. | | The result had an unsupported content type, exceeded `max_output_bytes`, or didn't match `output_schema` | Yes | Brain sends each MCP call at most once. With an unknown outcome, the model can use another tool to check the resource. Interrupting the session or reaching the timeout cancels the request; whether the work stops on the server depends on the server. ## Next steps [#next-steps] } title="Distributed tools" href="/docs/tools">Tool bindings, request payloads, and result formats. } title="Authentication & tenants" href="/docs/guides/authentication">How tools receive the session's identity. } title="Host configuration" href="/docs/reference/configuration">Every host config field and default. # Storage & web (/brain/docs/tools/resources) `cmd/resource-tools` (`services/resources`) is a standalone Go HTTP tool service that provides public web fetching (`web_fetch`), web search (`web_search`), and tenant-scoped S3/R2 text storage (`storage_list`, `storage_read`, `storage_write`, `storage_delete`). It shares Brain's wire types and AWS SDK dependency while running outside the session server. ## Before you begin [#before-you-begin] - Go `1.26+`. - `TOOL_API_KEY` set to a random secret of at least 24 characters (shared with Brain as `RESOURCE_TOOL_TOKEN`). - Optional search credentials (`SEARCH_PROVIDER` and `SEARCH_API_KEY`) or S3/R2 storage settings (`STORAGE_BUCKET`, `STORAGE_REGION`). ## 1. Start the service and print descriptors [#1-start-the-service-and-print-descriptors] Start `cmd/resource-tools` (listening on `127.0.0.1:8092` by default) and generate the tool descriptors for your configured capabilities: ```sh # Set TOOL_API_KEY to a random secret of at least 24 characters. go run ./cmd/resource-tools go run ./cmd/resource-tools -catalog http://127.0.0.1:8092 -tenant local ``` ## 2. Register the tools in Brain [#2-register-the-tools-in-brain] Copy the printed JSON descriptors into the `tools` array of your Brain configuration file (`brain.json`), and set `RESOURCE_TOOL_TOKEN` in Brain's environment to match the service's `TOOL_API_KEY`. The service binds to loopback (`127.0.0.1:8092`) by default; pass `-listen 0.0.0.0:8092` only behind TLS or private networking. ## Tools and configuration [#tools-and-configuration] | Tool | Configuration | Behavior | | --- | --- | --- | | `web_fetch` | No provider key | Public HTTP(S) on ports 80/443. DNS addresses are checked and pinned before dialing (rejecting private/loopback/link-local ranges), and redirects are validated at each hop. HTML is parsed into plain text. Upstream responses are capped at **2 MiB**; returned text is truncated at **128 KiB** with an explicit truncation flag. | | `web_search` | `SEARCH_PROVIDER=brave` or `tavily`, `SEARCH_API_KEY` | Returns up to 10 results with titles, snippets, and source URLs. Uses standard HTTP without third-party search SDK dependencies. | | `storage_list`, `storage_read` | `STORAGE_BUCKET`, `STORAGE_REGION`, optional `STORAGE_PREFIX` | Each tenant gets its own key prefix (`STORAGE_PREFIX//`), assigned from the authenticated Brain scope. Lists paginate at 100 keys; reads accept UTF-8 text up to **128 KiB**. | | `storage_write`, `storage_delete` | Also `STORAGE_ALLOW_WRITE=true` | Write and delete tools are disabled and omitted from `-catalog` output unless `STORAGE_ALLOW_WRITE=true` is explicitly set. | ### S3 and Cloudflare R2 storage settings [#s3-and-cloudflare-r2-storage-settings] - **AWS S3:** Uses the default AWS credential chain unless `STORAGE_ACCESS_KEY` and `STORAGE_SECRET_KEY` are set. - **Cloudflare R2:** Set `STORAGE_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com`, `STORAGE_REGION=auto`, `STORAGE_ACCESS_KEY`, and `STORAGE_SECRET_KEY`. - **Bucket isolation:** Keep this bucket separate from Brain's checkpoint storage bucket. ## Security, scoping, and limits [#security-scoping-and-limits] - **Tenant isolation:** Object keys are validated relative paths. Storage is shared across sessions belonging to the same tenant (`tenant_id`), whereas sandbox handles and session-inspection tools use narrower per-session scope. The tool service trusts `tenant_id` only from the authenticated Brain request envelope, never from model-supplied arguments—the model cannot choose a bucket, credential, or another tenant's prefix. - **Concurrency and timeouts:** The service runs at most **32** concurrent operations, and each request expires after **25 seconds**. - **Failure semantics:** The service never retries model actions. An ambiguous `storage_write` or `storage_delete` transport failure returns a tool error (`is_error: true`) with `unknown_outcome: true`; ordinary search, fetch, or read failures return tool errors without implying a remote mutation. Sources: [Brave API](https://api-dashboard.search.brave.com/app/documentation/web-search/get-started), [Tavily API](https://docs.tavily.com/documentation/api-reference/endpoint/search), [R2 S3 compatibility](https://developers.cloudflare.com/r2/api/s3/api/). ## Next steps [#next-steps] - [External tools protocol](https://aexhq.github.io/brain/docs/tools) - [Sandboxes tool service](https://aexhq.github.io/brain/docs/tools/sandboxes) - [Local tools & approvals](https://aexhq.github.io/brain/docs/tools/local-tools) - [Host configuration reference](https://aexhq.github.io/brain/docs/reference/configuration) - [Integration status](https://aexhq.github.io/brain/docs/roadmap#integration-status) # Sandboxes (/brain/docs/tools/sandboxes) `services/sandbox` is an independent Node.js 24 HTTP tool service that wraps Modal, E2B, and Daytona behind four standard Brain tools: `sandbox_create`, `sandbox_exec`, `sandbox_inspect`, and `sandbox_terminate`. Provider identifiers are wrapped in HMAC-signed handles scoped to the calling tenant and session. Keep `SANDBOX_SIGNING_KEY` stable across service restarts; no external ownership database is needed. ## Before you begin [#before-you-begin] - Node.js `24+` and `npm`. - `TOOL_API_KEY` set to a random secret of at least 24 characters (shared with Brain as `SANDBOX_TOOL_TOKEN`). - `SANDBOX_SIGNING_KEY` set to a random secret of at least 32 characters. - Credentials for at least one sandbox provider (`modal`, `e2b`, or `daytona`). ## 1. Install and start the service [#1-install-and-start-the-service] Install dependencies, start the service (listening on `127.0.0.1:8091` by default), and print the tool descriptors for the providers you configured: ```sh npm --prefix services/sandbox ci --ignore-scripts # Set TOOL_API_KEY (24+ characters), SANDBOX_SIGNING_KEY (32+ characters), # and credentials for at least one provider below. npm --prefix services/sandbox start npm --prefix services/sandbox run catalog -- http://127.0.0.1:8091 local modal,e2b,daytona ``` ## 2. Register the tools in Brain [#2-register-the-tools-in-brain] Copy the generated JSON descriptors into the `tools` array of your Brain configuration file (`brain.json`) and set `SANDBOX_TOOL_TOKEN` in Brain's environment to match the service's `TOOL_API_KEY`. - **Advertise only configured providers:** Pass only the providers you have configured to `npm --prefix services/sandbox run catalog`. - **Network binding:** The service binds to loopback (`127.0.0.1:8091`) by default; set `HOST=0.0.0.0` only behind TLS or private networking. - **Container image:** To build the standalone container image from the repository root, run `docker build -f services/sandbox/Dockerfile -t brain-sandbox-tools .`. ## Providers and lifecycles [#providers-and-lifecycles] | Provider | Required secrets | Optional host settings | Lifecycle | | --- | --- | --- | --- | | Modal | `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET` | `MODAL_APP` (`brain-tools`), `MODAL_IMAGE` (`python:3.13-slim`) | Five-minute maximum lifetime; one CPU, 512 MiB. Uses the official JS SDK. | | E2B | `E2B_API_KEY` | `E2B_TEMPLATE` (`code-interpreter-v1`) | Five-minute sandbox timeout. Inspect does not reconnect or resume it. | | Daytona | `DAYTONA_API_KEY` | `DAYTONA_API_URL`, `DAYTONA_TARGET` (`us`) | Five-minute inactivity stop, then automatic deletion. Preview access is private. | - **Handle lifetime:** Signed handles authorize `sandbox_exec` for 5 minutes. `sandbox_inspect` and `sandbox_terminate` remain available afterward so callers can clean up expired sandboxes. - **Explicit provider lifecycles:** Daytona's provider timeout is an inactivity limit rather than a wall-clock lifetime. No tool resumes stopped sandboxes or extends handle lifetimes. ## Execution limits and failure semantics [#execution-limits-and-failure-semantics] - **Command execution (`sandbox_exec`):** Commands run inside the remote sandbox through a Python 3 helper with a **30-second** deadline and **64 KiB** combined `stdout`/`stderr` cap. On timeout or excess output, the helper kills the process group. Results include the exit status plus explicit truncation and timeout flags. - **No automatic retries:** Provider failures and lost responses become tool errors (`is_error: true`) with `unknown_outcome: true` when the remote outcome is ambiguous; the service never retries a model action. - **Disconnect behavior:** Disconnecting a client does not guarantee remote cancellation: an in-flight command retains its 30-second deadline, and the provider's sandbox expiry (see the table above) is the final cleanup bound. - **Concurrency and isolation:** The service runs at most **16** concurrent HTTP requests. Keep provider account quotas and host CPU/memory limits appropriate for your workload. This common interface exposes no GPUs, public port tunnels, persistent disks, arbitrary container images, or cross-provider snapshots, and injects no credentials into sandboxes. ## Verification and security notes [#verification-and-security-notes] `npm --prefix services/sandbox test` runs the local contract tests (SDK types, HTTP lifecycle, authentication, cross-session isolation, handle tampering, service restarts, expiry, ambiguous failures, cleanup, and execution limits): ```sh npm --prefix services/sandbox test ``` To test against a live provider (`modal`, `e2b`, or `daytona`), configure that provider's credentials and run: ```sh npm --prefix services/sandbox run live -- modal ``` The `live` script creates a sandbox, executes a command, inspects it, and terminates it in a `finally` block. Missing credentials fail explicitly (no silent skips), and running against a live account can incur provider charges. > **Note:** Daytona `0.224.1` carries an upstream `braces` advisory through its file-glob dependencies (`GHSA-vfj7-8cjw-p6xm`), with no published fix at review time. This service never calls the Daytona SDK's upload/glob APIs or passes model input as glob patterns. Track the upstream fix and do not expand to those APIs without addressing it. Sources: [Modal SDK](https://modal.com/docs/sdk/js), [E2B sandbox API](https://docs.e2b.dev/sdk-reference/js-sdk/v2.9.0/sandbox), [Daytona SDK](https://www.daytona.io/docs/en/typescript-sdk/). ## Next steps [#next-steps] - [External tools protocol](https://aexhq.github.io/brain/docs/tools) - [Local tools & approvals](https://aexhq.github.io/brain/docs/tools/local-tools) - [Storage & web resource tools](https://aexhq.github.io/brain/docs/tools/resources) - [Host configuration reference](https://aexhq.github.io/brain/docs/reference/configuration) - [Integration status](https://aexhq.github.io/brain/docs/roadmap#integration-status)