diff --git a/.cspell.json b/.cspell.json index 4165054..58a85d9 100644 --- a/.cspell.json +++ b/.cspell.json @@ -100,7 +100,10 @@ "bucketings", "bucketers", "consoledonottrack", - "hosters" + "hosters", + "Gemini", + "OpenAI", + "OpenRouter" ], "ignoreRegExpList": ["https?://[^\\s)]+", "`[^`]*`"] } diff --git a/content/docs/cli/configuration.mdx b/content/docs/cli/configuration.mdx index ae8f83e..8b8ccb9 100644 --- a/content/docs/cli/configuration.mdx +++ b/content/docs/cli/configuration.mdx @@ -14,11 +14,13 @@ state: dir: ./.reactor # durable state (receipts, world-models, IR cache) model: - provider: openrouter + provider: openrouter # openrouter (default) | openai | anthropic | google | render_model: google/gemini-3.5-flash compile_model: google/gemini-3.5-flash temperature: 0 max_turns: 200 + # base_url: ... # optional: override the provider's endpoint + # api_key_env: ... # optional: read the key from a different env var sandbox: mode: none # none (default) | docker @@ -45,14 +47,76 @@ reactors: [] # optional: a multi-reactor host (see below) | Key | Default | Meaning | | --- | --- | --- | -| `model.provider` | `openrouter` | The model provider. | +| `model.provider` | `openrouter` | The model provider. A built-in (`openrouter`, `openai`, `anthropic`, `google`) supplies its own endpoint + key env; any other name requires `base_url` + `api_key_env`. | | `model.render_model` | `google/gemini-3.5-flash` | The model used for renders at run/serve time. | | `model.compile_model` | `google/gemini-3.5-flash` | The model used for the compile sessions. It is part of the IR cache key. | | `model.temperature` | `0` | Sampling temperature. | | `model.max_turns` | `200` | The per-session turn ceiling. | +| `model.base_url` | (provider default) | Override the OpenAI-compatible base URL. Optional for a built-in; required (with `api_key_env`) for a custom vendor. | +| `model.api_key_env` | (provider default) | The env var holding the API key (e.g. `ANTHROPIC_API_KEY`). Optional for a built-in; set it to read from a non-default variable. | The compile model is a component of the content-addressed cache key `(contract-set fingerprint, SDK version, model id)`. Changing it invalidates the cache and forces a recompile. +### Choosing a model provider + +By default Reactor talks to models through OpenRouter, but it is **not** bound to +it. The CLI drives one bounded `@openai/agents` session per render/compile step, +and any vendor with an **OpenAI-compatible Chat Completions** endpoint plugs in by +naming it in `model:`. Point at OpenAI, Anthropic, or Google **directly** -- no +OpenRouter account required: + +```yaml +# Anthropic, directly (set ANTHROPIC_API_KEY in your env or a .env) +model: + provider: anthropic + render_model: claude-haiku-4-5 + compile_model: claude-haiku-4-5 +``` + +Each built-in provider resolves to an endpoint + a key env var: + +| `provider` | Endpoint | Key env var | Example model id | +| --- | --- | --- | --- | +| `openrouter` (default) | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | `google/gemini-3.5-flash` | +| `openai` | `https://api.openai.com/v1` | `OPENAI_API_KEY` | `gpt-4o-mini` | +| `anthropic` | `https://api.anthropic.com/v1/` | `ANTHROPIC_API_KEY` | `claude-haiku-4-5` | +| `google` | `https://generativelanguage.googleapis.com/v1beta/openai/` | `GEMINI_API_KEY` | `gemini-2.5-flash` | + +For any other OpenAI-compatible vendor (or a self-hosted gateway), name it freely +and supply both `base_url` and `api_key_env`: + +```yaml +model: + provider: together + base_url: https://api.together.xyz/v1 + api_key_env: TOGETHER_API_KEY + render_model: meta-llama/Llama-3.3-70B-Instruct-Turbo + compile_model: meta-llama/Llama-3.3-70B-Instruct-Turbo +``` + +`api_key_env` also overrides a built-in's key var -- handy when, say, your +OpenRouter key lives under a different name. Run `reactor doctor` to confirm the +configured provider's key is visible (it reports the exact env var, never printing +the value), and `reactor doctor --live` to drive one real round-trip against it. + + +The key is read from the named env var first, then a `.env` discovered by walking +up from the project directory. A missing key fails `compile`/`run`/`serve` with a +**non-zero** exit and a message naming the exact variable to set -- it never +silently misdirects you to OpenRouter, and never exits 0 on an auth dead-end. + + + +**Claude models: prefer `openrouter` over `anthropic` for now.** Compile + render +use a JSON-schema response format, and Anthropic's direct OpenAI-compatible +endpoint rejects it unless `strict: true` (`400 response_format.json_schema.strict`). +OpenRouter, OpenAI, and Google accept it. To run Claude today, use +`provider: openrouter` with a `render_model`/`compile_model` like +`anthropic/claude-haiku-4-5`. `provider: anthropic` (direct) is wired and +authenticates, but the structured compile/run path is blocked by that endpoint +requirement. + + ## sandbox The `sandbox` block is the render threat-model knob. @@ -96,7 +160,8 @@ Each entry is isolated: its own contracts directory, state directory, substrate, | Variable | Meaning | | --- | --- | -| `OPENROUTER_API_KEY` | The live key for the model surface. Required by `compile`, `run`, `serve`, and `trigger`. Read from the process env first, then a `.env` file discovered from the working directory upward. | +| `OPENROUTER_API_KEY` | The live key for the **default** provider. Required by `compile`, `run`, `serve`, and `trigger` when `model.provider` is `openrouter`. Read from the process env first, then a `.env` file discovered from the working directory upward. | +| `` | The live key for a **non-default** provider -- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or whatever `model.api_key_env` names. Resolved the same way (env first, then a discoverable `.env`). See [Choosing a model provider](#choosing-a-model-provider). | | `REACTOR_OFFLINE` | Set to `1` (or `true`) to force offline mode. Equivalent to the `--offline` flag. | ## Global flags diff --git a/content/docs/sdk/agents.mdx b/content/docs/sdk/agents.mdx index 374fdf1..8ddf3c2 100644 --- a/content/docs/sdk/agents.mdx +++ b/content/docs/sdk/agents.mdx @@ -218,6 +218,80 @@ const render: RenderOptions = { }; ``` +## Bring your own LLM provider + +Reactor is **not** bound to OpenRouter. The default render points at OpenRouter's +OpenAI-compatible surface only because it is a cheap, broad gateway -- but the +`provider` field is plain `@openai/agents` configuration, so you point it +anywhere the way **any** `@openai/agents` consumer would: build a scoped +`OpenAIProvider` at the base URL of your choice and hand it in. Nothing about the +harness is OpenRouter-specific. + +```ts +import { reactor } from "@openprose/reactor"; +import { OpenAIProvider } from "@openai/agents"; + +// Anthropic, directly -- not through OpenRouter. (OpenAI and Google work the same +// way; only the base URL, key, and model id change -- see the table below.) +const provider = new OpenAIProvider({ + apiKey: process.env.ANTHROPIC_API_KEY!, + baseURL: "https://api.anthropic.com/v1/", + useResponses: false, // Chat Completions -- the surface these vendors share +}); + +const { reactor: r } = await reactor("./my-project", { + directory: "./state", + render: { provider, model: "claude-haiku-4-5" }, // run-phase renders + compile: { options: { provider, model: "claude-haiku-4-5" } }, // compile sessions +}); +await r.ingest("source", { wake: { source: "external", refs: [] } }); +``` + +The same scoped `provider` flows to **both** phases: `render.provider` drives the +run-phase renders, and `compile.options.provider` drives the compile sessions +(they are the same bounded-session machinery). Pass it in only one place and the +other still defaults to OpenRouter, so set both when you mean to switch wholesale. + +Any vendor with an OpenAI-compatible Chat Completions endpoint drops straight in: + +| Vendor | `baseURL` | Key | Example model id | +| --- | --- | --- | --- | +| OpenRouter (default) | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | `google/gemini-3.5-flash` | +| OpenAI | `https://api.openai.com/v1` | `OPENAI_API_KEY` | `gpt-4o-mini` | +| Anthropic | `https://api.anthropic.com/v1/` | `ANTHROPIC_API_KEY` | `claude-haiku-4-5` | +| Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai/` | `GEMINI_API_KEY` | `gemini-2.5-flash` | + + +**`useResponses: false` is the safe default for a non-OpenAI host.** It selects +Chat Completions, the surface every vendor above implements; the newer Responses +API is OpenAI-only and 404s elsewhere. Against OpenAI's own host you may leave it +unset to use Responses. + + +Because `provider` is a **scoped** `ModelProvider`, this never mutates the +`@openai/agents` process-global default client -- two reactors in one process can +target two different vendors. The wiring is exercised live (OpenRouter + OpenAI + +Anthropic) in +`packages/reactor/src/adapters/agent-render/__tests__/provider-byo.live.test.ts`. + + +**Anthropic's direct endpoint and structured outputs.** Compile sessions and the +render's done/failed signal use a JSON-schema `response_format`. Anthropic's +OpenAI-compatible endpoint requires `strict: true` on that schema and rejects the +default with `400 response_format.json_schema.strict`. OpenAI and Google (and +every model **via OpenRouter**, including Anthropic's) accept it as-is. So for +**Claude models today, route through OpenRouter** (`provider` pointed at +`https://openrouter.ai/api/v1`, model `anthropic/claude-...`); direct +`api.anthropic.com` works for plain-text use but not yet for the structured +compile/render path. + + +For an API that is **not** OpenAI-compatible at all (a native Anthropic Messages +client, a local model, a bespoke gateway), drop one level to the +[`RenderBackend` injection seam](#the-renderbackend-injection-seam) below: you +own the whole session and the harness keeps its instruction composition, tools, +harvest, and cost capture. + ## The `RenderBackend` injection seam The Tier-C factories let you rebuild the `Agent`/`Runner` while staying inside the