feat(backend): add OpenAI Agents SDK as fifth backend - #199
Conversation
|
CI fix pushed ( Root cause of the all-jobs failure: Fix: manually restored the two entries from main's lockfile. |
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
ed9eb0e to
ceacb9e
Compare
|
Iterated on this per your feedback. Closed #202 — it was over-specified as an "OpenRouter redirect" when the New commit
Rebased onto current main while I was here (PR was Examples in the updated PR description:
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. |
Summary
Adds the OpenAI Agents SDK (
@openai/agents) as Talon's fifth backend, alongside claude / kilo / opencode / codex. Selectable viabackend: "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"whenopenaiBaseUrlis set, because most non-OpenAI endpoints don't implement Responses. Set explicitly to"responses"only if your proxy supports it.The
@openai/agentsSDK exposessetDefaultOpenAIClient(client)andsetOpenAIAPI(mode)—init.tsconstructs a customOpenAI({apiKey, baseURL})client whenever either field is set and calls both globally. All subsequentnew Agent({...})instances inherit the redirect; no per-call wiring needed.Example configs (all written to
talon.json):Model catalog passthrough
The hardcoded catalog (
gpt-5.5/gpt-5/gpt-5-mini/o4-mini) still works for OpenAI-native users. WhenopenaiBaseUrlis set,resolveModel()andgetModelInfo()accept arbitrary model ids as synthetic passthrough entries — so users can targetmeta-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, sogpt-5.5on a proxy that mirrors OpenAI's namespace still resolves cleanly.formatModelError()points users at directmodel:config in talon.json when a custom baseURL is set, instead of nagging about an unrelated OpenAI-flagship catalog.CLI wizard + doctor + status
openai-agentsbranch of setup wizard prompts API key → base URL → (if base URL set) Chat Completions vs Responses.talon statusdisplays configuredopenaiBaseUrl+openaiApiMode.talon doctorreports 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 sameQueryBackendsurface: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/setOpenAIAPIwiring + auth-state logging.mcp.ts—MCPServerStdio[]from plugin map (one server per non-terminal frontend + brave-search + every plugin).handler.ts— streaming agent run viarun(agent, prompt, {stream: true}). TranslatesRunItemStreamEvent(tool_called,message_output_created) into shared stream-state. Hooks abort controller for terminator-driven cancel. Closes MCP servers infinally.factory.ts— registers asopenai-agentswith the standardQueryBackendshape.state.ts— singleton config + frontend handle.Wiring:
src/bootstrap.tsextra side-effect import,src/util/config.tsadds the enum entry + new fields,src/cli.tsnew 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:getOpenAIBaseUrlenv-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)
Sessionabstraction but Talon's session model is per-chat numeric ids; wiring is a future PR.Verified
npx tsc --noEmitcleannpm test— 2449 passing, 12 skipped (live), 2 pre-existing opencode-integration timeouts unrelated to this changenpm run lint— 11 pre-existing warnings, 0 errorsnpx prettier --checkcleanLive verification path with a custom endpoint: set
OPENAI_API_KEY+OPENAI_BASE_URLin env (or the matching fields in talon.json), flipbackend: "openai-agents", send a message.Out of scope (future PRs)
Backend Live (openai-agents, *)CI matrix job. Needs an API key secret; SDK won't smoke-test without one.Sessionabstraction.onStreamDeltacallbacks for typing-indicator UX.🤖 Generated with Claude Code