Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,21 @@ OPENAI_API_KEY=
# ANTHROPIC_BASE_URL=
# GOOGLE_GENERATIVE_AI_BASE_URL=

# Framework Bot provider: openai, anthropic or google. It reads that provider's own
# Framework Bot provider: openai, anthropic, google or orcarouter. It reads that provider's own
# key and refuses to start without it, so a deployment on Anthropic never needs an OpenAI key for it.
# The proof-of-concept Bot is OpenAI only by construction: it speaks that API directly.
#
# orcarouter is an OpenAI-compatible gateway: it serves many providers under namespaced model names
# over the same endpoint, so the OpenAI integration carries it. Set BOT_MODEL to the catalogue name
# (for example orcarouter/fusion), and ORCAROUTER_BASE_URL if you self-host the gateway.
# BOT_PROVIDER=openai
# ANTHROPIC_API_KEY=
# GOOGLE_API_KEY=
# ORCAROUTER_API_KEY=
# ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1

# Which model the framework Bot uses. Defaults per provider: gpt-5.5, claude-sonnet-4-5,
# gemini-2.5-flash. A 5.6 tier works here: set one and the Responses API is switched on
# gemini-2.5-flash, orcarouter/fusion. A 5.6 tier works here: set one and the Responses API is switched on
# automatically. It used to answer nothing at all on those models — RUN_STARTED, RUN_FINISHED, no
# text — because that API streams content blocks rather than a string and the run read only the
# string. The default is left at 5.5 so the two shipped Bots stay comparable out of the box.
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### The LangGraph Bot has a fourth provider: orcarouter

`BOT_PROVIDER=orcarouter` points `agent-langgraph` at [OrcaRouter](https://www.orcarouter.ai), an
OpenAI-compatible gateway that fronts many providers behind one key. It is wired exactly like the
existing `openai` provider — the same integration, a key of its own (`ORCAROUTER_API_KEY`), a
namespaced `BOT_MODEL` such as `orcarouter/fusion`, and a default `ORCAROUTER_BASE_URL` of
`https://api.orcarouter.ai/v1` that a self-hosted gateway can override. Existing providers are
unchanged.

### A Bot's shell can no longer reach the embedded database without a password

In the all-in-one image the cluster was `trust`-auth on loopback, and the Bot's shell runs in the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui),
- Docker, for PostgreSQL and the shipped Bots.
- [Bun](https://bun.sh) 1.3+, for the app and API server.
- A CopilotKit Intelligence project and license. A free plan is available, and Intelligence can be self-hosted.
- A model key. The proof-of-concept Bot uses OpenAI; the LangGraph Bot can use OpenAI, Anthropic, or Google.
- A model key. The proof-of-concept Bot uses OpenAI; the LangGraph Bot can use OpenAI, Anthropic, Google, or OrcaRouter.

## Quick start

Expand Down
28 changes: 26 additions & 2 deletions agent-langgraph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL?.trim() || undefined;
const ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL?.trim() || undefined;
const GOOGLE_BASE_URL =
process.env.GOOGLE_GENERATIVE_AI_BASE_URL?.trim() || undefined;
/**
* OrcaRouter's endpoint. It speaks the OpenAI API, so this is the same shape as `OPENAI_BASE_URL`,
* with a default that names the public gateway rather than OpenAI. A self-hosted gateway can point
* it somewhere else, which is why it is a variable at all.
*/
const ORCAROUTER_BASE_URL =
process.env.ORCAROUTER_BASE_URL?.trim() || "https://api.orcarouter.ai/v1";

/**
* OpenAI only, and Responses API only: how hard this Bot is allowed to think.
Expand All @@ -120,7 +127,7 @@ if (REASONING_PROBLEM) {
* configuration that goes nowhere is worse than configuration that is absent, because the Bot looks
* configured either way. Both messages name the variable that would make it work.
*/
if (REASONING_EFFORT && PROVIDER !== "openai") {
if (REASONING_EFFORT && PROVIDER !== "openai" && PROVIDER !== "orcarouter") {
console.error(
`BOT_REASONING_EFFORT is OpenAI's setting, and BOT_PROVIDER=${PROVIDER}. Unset it, or set BOT_PROVIDER=openai.`,
);
Expand All @@ -136,6 +143,7 @@ if (REASONING_EFFORT && !USE_RESPONSES_API) {
function defaultModelFor(provider: string): string {
if (provider === "anthropic") return "claude-sonnet-4-5";
if (provider === "google") return "gemini-2.5-flash";
if (provider === "orcarouter") return "orcarouter/fusion";
return "gpt-5.5";
}

Expand All @@ -150,12 +158,13 @@ const KEY_VARIABLE: Record<string, string> = {
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_API_KEY",
orcarouter: "ORCAROUTER_API_KEY",
};

const keyVariable = KEY_VARIABLE[PROVIDER];
if (!keyVariable) {
console.error(
`BOT_PROVIDER=${PROVIDER} is not one this Bot knows. Use openai, anthropic or google.`,
`BOT_PROVIDER=${PROVIDER} is not one this Bot knows. Use openai, anthropic, google or orcarouter.`,
);
process.exit(1);
}
Expand Down Expand Up @@ -191,6 +200,21 @@ function toBoundTools(input: RunAgentInput) {
* rest of this file does not know which one it got.
*/
function buildModel() {
if (PROVIDER === "orcarouter") {
/*
* OpenAI-compatible, like the `openai` branch: OrcaRouter exposes a provider/model namespace
* over the same endpoint, so the same integration serves it. `ORCAROUTER_API_KEY` is its own
* key, and `BOT_MODEL` carries the namespaced name the catalogue publishes.
*/
return new ChatOpenAI({
model: MODEL,
apiKey: API_KEY,
streaming: true,
configuration: { baseURL: ORCAROUTER_BASE_URL },
...(USE_RESPONSES_API ? { useResponsesApi: true } : {}),
...(REASONING_EFFORT ? { reasoning: { effort: REASONING_EFFORT } } : {}),
});
}
if (PROVIDER === "anthropic") {
return new ChatAnthropic({
model: MODEL,
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ services:
ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-}
GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
GOOGLE_GENERATIVE_AI_BASE_URL: ${GOOGLE_GENERATIVE_AI_BASE_URL:-}
ORCAROUTER_API_KEY: ${ORCAROUTER_API_KEY:-}
ORCAROUTER_BASE_URL: ${ORCAROUTER_BASE_URL:-https://api.orcarouter.ai/v1}
# gpt-5.5, to stay comparable with agent-bot above rather than because 5.6 does not work:
# set BOT_MODEL to one and the Responses API is switched on for it automatically.
BOT_MODEL: ${BOT_MODEL:-gpt-5.5}
Expand Down
6 changes: 5 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ at `agent-langgraph` on a laptop.
| `DEPLOYMENT_ID` | the tenant package's id | Names this deployment inside a shared Intelligence project. |
| `OPENAI_API_KEY` | unset | Default model key for built-in agents and both shipped Bots. |
| `OPENAI_BASE_URL` | unset | OpenAI-compatible endpoint that key is spent against. See below. |
| `BOT_PROVIDER` | `openai` | Provider for `agent-langgraph`: `openai`, `anthropic`, or `google`. |
| `BOT_PROVIDER` | `openai` | Provider for `agent-langgraph`: `openai`, `anthropic`, `google`, or `orcarouter`. |
| `ANTHROPIC_API_KEY` | unset | Anthropic key when `BOT_PROVIDER=anthropic`. |
| `ANTHROPIC_BASE_URL` | unset | Anthropic-compatible endpoint that key is spent against. |
| `GOOGLE_API_KEY` | unset | Google key when `BOT_PROVIDER=google`. |
| `GOOGLE_GENERATIVE_AI_BASE_URL` | unset | Google-compatible endpoint that key is spent against. |
| `ORCAROUTER_API_KEY` | unset | OrcaRouter key when `BOT_PROVIDER=orcarouter`. |
| `ORCAROUTER_BASE_URL`| `https://api.orcarouter.ai/v1` | OrcaRouter endpoint that key is spent against. |
| `BOT_MODEL` | provider default from Bot code/env | Model used by the shipped Bots. |
| `BOT_RESPONSES_API` | `false` | Makes `agent-langgraph` use the OpenAI Responses API. |
| `AGENT_STALL_TIMEOUT_MS` | unset (off) | How long a Bot's stream may produce nothing before the turn is ended for it. |
Expand Down Expand Up @@ -111,6 +113,8 @@ It moves the whole deployment rather than one Bot. The API server reads it for p

The other two providers work the same way under their own names, because they are different APIs rather than different URLs for this one: `ANTHROPIC_BASE_URL` and `GOOGLE_GENERATIVE_AI_BASE_URL`. All three are the names the API server already reads, so one line moves the built-in agents and the Bots together and a deployment cannot end up with half of itself pointed somewhere else.

`orcarouter` is a fourth `BOT_PROVIDER` for `agent-langgraph`, and it is the first one that is not a model vendor. OrcaRouter is an OpenAI-compatible gateway that fronts many providers behind one key, so the `openai` integration carries it and the endpoint has a default rather than being unset: `ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1`. Set `BOT_PROVIDER=orcarouter` with `ORCAROUTER_API_KEY` and a namespaced `BOT_MODEL` the catalogue publishes, for example `orcarouter/fusion`.

Model names travel verbatim, so use whatever the endpoint publishes. An endpoint that namespaces its catalogue wants both halves of the name, in `BOT_MODEL` and in the tenant package's `default_model` alike.

A gateway that fronts several providers behind one key is addressed the usual way:
Expand Down