Skip to content

feat(backend): add OpenAI Agents SDK as fifth backend - #199

Merged
dylanneve1 merged 3 commits into
mainfrom
feat/openai-agents-backend
May 18, 2026
Merged

feat(backend): add OpenAI Agents SDK as fifth backend#199
dylanneve1 merged 3 commits into
mainfrom
feat/openai-agents-backend

Conversation

@claudiusthebot

@claudiusthebot claudiusthebot commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the OpenAI Agents SDK (@openai/agents) as Talon's fifth backend, alongside claude / kilo / opencode / codex. Selectable via backend: "openai-agents" in talon.json.

Originally filed against OpenAI's API directly; updated 2026-05-18 per Dylan's feedback to support arbitrary OpenAI-compatible endpoints out of the box — OpenRouter, Azure OpenAI, Ollama, LiteLLM, vLLM, Portkey, Together, Groq's compat surface, anything that speaks the protocol. Supersedes #202 (which was a special-cased OpenRouter redirect; now folded in here with generic naming).

Endpoint configuration

Two new optional config fields (and matching env vars), both generic — not OpenRouter-named:

  • openaiBaseUrl / OPENAI_BASE_URL — redirect target. Empty = OpenAI direct.
  • openaiApiMode / OPENAI_API_MODE"responses" (OpenAI native, default) or "chat_completions" (most third parties). Auto-defaults to "chat_completions" when openaiBaseUrl is set, because most non-OpenAI endpoints don't implement Responses. Set explicitly to "responses" only if your proxy supports it.

The @openai/agents SDK exposes setDefaultOpenAIClient(client) and setOpenAIAPI(mode)init.ts constructs a custom OpenAI({apiKey, baseURL}) client whenever either field is set and calls both globally. All subsequent new Agent({...}) instances inherit the redirect; no per-call wiring needed.

Example configs (all written to talon.json):

// OpenAI direct (default)
{ "backend": "openai-agents", "openaiApiKey": "sk-...", "model": "gpt-5.5" }

// OpenRouter
{ "backend": "openai-agents",
  "openaiApiKey": "sk-or-...",
  "openaiBaseUrl": "https://openrouter.ai/api/v1",
  "model": "meta-llama/llama-3.3-70b-instruct" }

// Local Ollama
{ "backend": "openai-agents",
  "openaiBaseUrl": "http://localhost:11434/v1",
  "model": "llama3.2:3b" }

// Azure OpenAI
{ "backend": "openai-agents",
  "openaiApiKey": "...",
  "openaiBaseUrl": "https://<resource>.openai.azure.com/openai/v1",
  "model": "gpt-4o-mini" }

Model catalog passthrough

The hardcoded catalog (gpt-5.5 / gpt-5 / gpt-5-mini / o4-mini) still works for OpenAI-native users. When openaiBaseUrl is set, resolveModel() and getModelInfo() accept arbitrary model ids as synthetic passthrough entries — so users can target meta-llama/llama-3.3-70b-instruct, qwen/qwen3-coder, llama3.2:3b, etc. without us maintaining a separate catalog per provider. Catalog hits are still preferred for OpenAI-native ids even with a baseURL set, so gpt-5.5 on a proxy that mirrors OpenAI's namespace still resolves cleanly.

formatModelError() points users at direct model: config in talon.json when a custom baseURL is set, instead of nagging about an unrelated OpenAI-flagship catalog.

CLI wizard + doctor + status

  • openai-agents branch of setup wizard prompts API key → base URL → (if base URL set) Chat Completions vs Responses.
  • talon status displays configured openaiBaseUrl + openaiApiMode.
  • talon doctor reports OpenAI Agents auth state and resolved endpoint (defaults to api.openai.com when no override).

What's in the module

src/backend/openai-agents/ (~900 LOC) — mirrors the codex backend shape with the same QueryBackend surface:

  • constants.ts — system-prompt suffix, default model (gpt-5.5), OPENAI_AGENTS_MAX_TURNS=50, agent name surfaced in OpenAI traces.
  • models.ts — catalog + passthrough resolver.
  • init.ts — endpoint resolution + setDefaultOpenAIClient / setOpenAIAPI wiring + auth-state logging.
  • mcp.tsMCPServerStdio[] from plugin map (one server per non-terminal frontend + brave-search + every plugin).
  • handler.ts — streaming agent run via run(agent, prompt, {stream: true}). Translates RunItemStreamEvent (tool_called, message_output_created) into shared stream-state. Hooks abort controller for terminator-driven cancel. Closes MCP servers in finally.
  • factory.ts — registers as openai-agents with the standard QueryBackend shape.
  • state.ts — singleton config + frontend handle.

Wiring: src/bootstrap.ts extra side-effect import, src/util/config.ts adds the enum entry + new fields, src/cli.ts new menu/wizard/doctor branches.

Tests (+35 cases total)

  • openai-agents-models.test.ts — 27 catalog tests (resolve, getModelInfo, providers, presentation, error formatting, listModels) + 8 new custom-endpoint tests: getOpenAIBaseUrl env-vs-config priority, custom-baseURL passthrough resolution, catalog hits still preferred, synthetic getModelInfo entries, baseURL-aware formatModelError.
  • openai-agents-backend.test.ts — constants, state lifecycle, factory registration.
  • backend-registry-parity.test.ts — five-backends parity assertions.

Acknowledged debt (callouts in the handler header)

  • No session persistence — the Agents SDK has a Session abstraction but Talon's session model is per-chat numeric ids; wiring is a future PR.
  • No progress-text emission before tool calls — the SDK's raw-stream events don't cleanly split into pre-tool text segments the way Claude SDK's do.
  • No flow-violation reminder retry — single-pass for now. Base retry ladder (session_expired / context_length / fallback_model) still runs.

Verified

  • npx tsc --noEmit clean
  • npm test — 2449 passing, 12 skipped (live), 2 pre-existing opencode-integration timeouts unrelated to this change
  • npm run lint — 11 pre-existing warnings, 0 errors
  • npx prettier --check clean

Live verification path with a custom endpoint: set OPENAI_API_KEY + OPENAI_BASE_URL in env (or the matching fields in talon.json), flip backend: "openai-agents", send a message.

Out of scope (future PRs)

  • A Backend Live (openai-agents, *) CI matrix job. Needs an API key secret; SDK won't smoke-test without one.
  • Session persistence wiring via the SDK's Session abstraction.
  • Streaming onStreamDelta callbacks for typing-indicator UX.

🤖 Generated with Claude Code

@claudiusthebot

Copy link
Copy Markdown
Collaborator Author

CI fix pushed (ed9eb0e).

Root cause of the all-jobs failure: @emnapi/core@1.10.0 and @emnapi/runtime@1.10.0 were missing from the lockfile. These are optional dev transitive deps (via @openai/codex) that exist in main's lockfile, but the lockfile was generated on an arm64/platform that doesn't need them — so they were dropped when npm install ran locally. CI (ubuntu-latest x64) considers them required and fails npm ci with EUSAGE immediately, which is why every job failed in under 10 seconds.

Fix: manually restored the two entries from main's lockfile. npm ci --dry-run now exits 0. CI re-running on the new commit.

claudiusthebot added a commit that referenced this pull request May 17, 2026
When `openrouterApiKey` (or OPENROUTER_API_KEY env) is set, init injects
a custom OpenAI client via setDefaultOpenAIClient() pointed at OpenRouter's
OpenAI-compatible API. No handler changes needed — the SDK picks up the
global client automatically.

Config additions:
  - openrouterApiKey — the OR API key
  - openrouterBaseUrl — optional override (defaults to OR's v1 endpoint)

Recommended free-tier model for heartbeats (set as `model` in talon.json):
  meta-llama/llama-3.3-70b-instruct

Depends on PR #199 (openai-agents backend). Rebase onto main after that
PR merges before merging this one.
Dylan asked for OpenAI Agents SDK support as a possible new backend
(`https://developers.openai.com/api/docs/guides/agents`). This adds
it alongside claude / kilo / opencode / codex, registered as
`backend: "openai-agents"` in talon.json.

## What's in scope (v0)

The backend speaks to OpenAI's Responses API via
`@openai/agents@^0.11.4` (the official Node SDK). Talon uses it as
a single-agent backend (no handoffs, no guardrails) with MCP servers
wired from the plugin system.

  - **`src/backend/openai-agents/`** — new module, ~900 LOC, mirrors
    the `codex` backend's shape with the same `QueryBackend` surface.
    - `constants.ts` — system-prompt suffix + default model
      (`gpt-5.5`, the broadest-access flagship), max turns (50),
      agent name surfaced in OpenAI traces.
    - `models.ts` — hand-maintained catalog (`gpt-5.5`, `gpt-5`,
      `gpt-5-mini`, `o4-mini`). `gpt-5-codex` is intentionally NOT
      in this catalog — that model is Codex-CLI-only.
    - `auth` via `init.ts`: `OPENAI_API_KEY` env > `openaiApiKey`
      in config > startup warning. No ChatGPT-OAuth fallback —
      the Responses API requires a paid API key.
    - `mcp.ts` — builds `MCPServerStdio[]` from Talon's plugin map.
      One server per non-terminal frontend + brave-search + every
      configured plugin. Parallel `connect()`; close-all on error.
    - `handler.ts` — streaming agent run via `run(agent, prompt,
      {stream: true})`. Translates `RunItemStreamEvent` events
      (`tool_called`, `message_output_created`) into the shared
      stream-state. Hooks the abort controller for terminator-
      driven cancellation. Closes MCP servers in `finally` to avoid
      subprocess leaks.
    - `factory.ts` — registers as `openai-agents` with the standard
      `QueryBackend` shape.
    - `state.ts` — singleton per-process config + frontend handle.

  - **Bootstrap wiring** (`src/bootstrap.ts`): one extra
    side-effect import, additive only.
  - **Config schema** (`src/util/config.ts`): adds `"openai-agents"`
    to the `backend` enum.
  - **CLI setup wizard** (`src/cli.ts`): new menu entry +
    `OPENAI_API_KEY` prompt. The label note explicitly calls out
    "no ChatGPT-OAuth fallback" so users with only `codex login`
    auth don't pick this backend and then 401 on first turn.

## Tests (+27 cases, 2423 → 2435 total)

  - `openai-agents-models.test.ts` (19 tests) — catalog presence,
    `resolveModel` / `getModelInfo` / `getSettingsPresentation` /
    providers / `listModels` / error formatting.
  - `openai-agents-backend.test.ts` (8 tests) — constants, state
    lifecycle, factory registration.
  - `backend-registry-parity.test.ts` updated from "four backends"
    → "five backends" with the same per-id assertions.

## Acknowledged debt (callouts in the handler header)

  - **No session persistence.** The Agents SDK has a `Session`
    abstraction but Talon's session model is per-chat numeric ids;
    wiring is a future PR.
  - **No progress-text emission** before tool calls. The Agents
    SDK's raw-stream events don't cleanly split into pre-tool
    text segments the way Claude SDK's do.
  - **No flow-violation reminder retry** — single-pass for now.
    The base retry ladder (session_expired / context_length /
    fallback_model) does run via the per-backend catch block;
    this PR doesn't depend on PR #196's `applyRetryDecision`
    extraction to stay independently mergeable.

## Verified

  - `npx tsc --noEmit` clean
  - `npm test` — 2423 passing, 12 skipped (live), 0 failing
    (was 2396 — +27 new)
  - `npm run lint` — 11 warnings, 0 errors (all pre-existing in
    `src/frontend/discord/`)
  - `npm run format:check` clean
  - Live SDK probe: `@openai/agents` constructs + calls `run()`
    cleanly (verified without API key by mocking; full live test
    deferred to a follow-up Backend Live job).

## Out of scope (future)

  - A `Backend Live (openai-agents, *)` CI job in the matrix.
    Needs an `OPENAI_API_KEY` secret in repo settings; the SDK
    won't smoke-test without one (unlike codex which has a
    ChatGPT-OAuth fallback path).
  - Session persistence wiring via the SDK's `Session` abstraction.
  - Streaming `onStreamDelta` callbacks for typing-indicator UX.
These are optional dev transitive dependencies (via @openai/codex) that
exist in main's lockfile but were dropped when the local npm install
generated the lockfile on an arm64 platform that doesn't need them.
CI (ubuntu-latest x64) requires them to be present via npm ci.

Restores the entries from main's lockfile — no behavior change.
Dylan flagged that the original v0 of this backend hardcoded to OpenAI's
production API, and a follow-up PR (#202) tried to add OpenRouter as a
special-cased redirect. The @openai/agents SDK already supports arbitrary
custom OpenAI-compatible endpoints via setDefaultOpenAIClient() — so the
right design is generic baseURL config on this backend, not an OpenRouter-
named adapter. PR #202 is closed; folding the support in here properly.

## What this adds

Two new optional config fields (plus matching env vars), both generic:

  - `openaiBaseUrl` / `OPENAI_BASE_URL`
      Redirects the SDK at any OpenAI-compatible service. Works for
      OpenRouter, Azure OpenAI, local Ollama, LiteLLM, Portkey, vLLM,
      Together, Groq's OpenAI-compat surface — anything that speaks the
      protocol.
  - `openaiApiMode` / `OPENAI_API_MODE`
      Which OpenAI API surface to target: "responses" (OpenAI native) or
      "chat_completions" (most third parties). Defaults to "responses"
      with no baseURL, and "chat_completions" automatically when a
      custom baseURL is set — since most non-OpenAI endpoints don't
      implement the Responses API.

`init.ts` constructs a custom `OpenAI({apiKey, baseURL})` client and
calls `setDefaultOpenAIClient()` whenever either field is configured.
`setOpenAIAPI()` is called unconditionally with the resolved mode so the
startup log line accurately reflects which surface subsequent `run()`
calls hit.

## Model catalog passthrough

`resolveModel()` and `getModelInfo()` previously rejected anything not in
the hardcoded `gpt-5.5 / gpt-5 / gpt-5-mini / o4-mini` catalog. Now: when
`openaiBaseUrl` is set, any unknown model id is accepted as a synthetic
passthrough entry — so users can target `meta-llama/llama-3.3-70b-instruct`
on OpenRouter, `llama3.2:3b` on Ollama, etc. without us maintaining a
separate catalog per third-party provider. Exact catalog hits still win
for OpenAI-native models even with a baseURL set.

## CLI wizard + doctor

- `openai-agents` branch of setup wizard prompts for API key, then base
  URL, then (if a base URL is set) Chat Completions vs Responses.
- `talon status` now shows configured `openaiBaseUrl` + `openaiApiMode`.
- `talon doctor` adds an `openai-agents` branch reporting auth state and
  endpoint config (defaults to api.openai.com when none).

## Tests

8 new cases in openai-agents-models.test.ts covering the passthrough
path: env-vs-config priority, custom-baseURL resolution, catalog hits
still preferred, getModelInfo synthetic entries, formatModelError
pointing users at direct config. 35 / 35 openai-agents tests pass; full
suite at 2449 / 2463 (2 pre-existing opencode integration-test
timeouts unrelated to this change).

Verified:
  - npx tsc --noEmit clean
  - npm run lint 0 errors, 11 pre-existing warnings
  - npx prettier --check clean
  - vitest 35/35 backend tests pass
@dylanneve1
dylanneve1 force-pushed the feat/openai-agents-backend branch from ed9eb0e to ceacb9e Compare May 18, 2026 10:06
@claudiusthebot

Copy link
Copy Markdown
Collaborator Author

Iterated on this per your feedback. Closed #202 — it was over-specified as an "OpenRouter redirect" when the @openai/agents SDK already supports arbitrary custom endpoints via setDefaultOpenAIClient(new OpenAI({apiKey, baseURL})). Folded the support into this PR directly with generic naming so it works with any OpenAI-compatible service, not just OpenRouter.

New commit ceacb9e on top of the existing two:

  1. openaiBaseUrl + openaiApiMode config fields (also OPENAI_BASE_URL / OPENAI_API_MODE env). Generic, not OR-specific.
  2. init.ts constructs new OpenAI({apiKey, baseURL}), calls setDefaultOpenAIClient() and setOpenAIAPI(mode). Auto-defaults to chat_completions when baseURL is set since most non-OpenAI endpoints don't implement Responses.
  3. Model resolver accepts arbitrary ids when a custom baseURL is set (passthrough) — so meta-llama/llama-3.3-70b-instruct / llama3.2:3b / qwen/qwen3-coder etc work without needing a per-provider catalog. Catalog hits are still preferred for OpenAI-native ids.
  4. CLI: wizard prompts API key → base URL → Chat Completions vs Responses; talon status shows the new fields; talon doctor reports auth + endpoint state for the openai-agents backend.
  5. 8 new tests in openai-agents-models.test.ts covering the passthrough path. 35 / 35 openai-agents tests green.

Rebased onto current main while I was here (PR was behind before). Force-pushed with --force-with-lease against the pre-rebase tip.

Examples in the updated PR description:

  • OpenAI direct (status quo)
  • OpenRouter (https://openrouter.ai/api/v1)
  • Local Ollama (http://localhost:11434/v1)
  • Azure OpenAI (https://<resource>.openai.azure.com/openai/v1)

After June 1 (Max end), this gives a clean path off Anthropic billing: free-tier OpenRouter models for heartbeat/dream, anything else you want for chat. Or stay on OpenAI direct with a paid key. Or local Ollama if you want zero marginal cost and don't mind the latency. Same backend either way.

@dylanneve1
dylanneve1 enabled auto-merge (squash) May 18, 2026 10:11
@dylanneve1
dylanneve1 merged commit d9a6e90 into main May 18, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants