Skip to content

Ennodia IO

Ennodia IO is a local Hypertext Transfer Protocol (HTTP) server for artificial intelligence (AI) provider options. It uses the same Ennodia Core as the Model Context Protocol (MCP) tools. It supports a bring your own key (BYOK) setup without requiring a provider application programming interface (API) key. The app can show runnable local agents in its user interface (UI). It can then send chat-style requests to one option.

It is not a hosted model router. Ennodia does not sell model access, proxy provider billing, or hide which local agents ran. IO ships as the separate @cherninlab/ennodia-io package.

When Compare is enabled, Ennodia keeps the roles explicit. The Judge evaluates candidate results. The Result Advisor combines the Judge analysis and candidate evidence into the final answer.

Terminal window
npx -y @cherninlab/ennodia-io

From a checkout:

Terminal window
bun run --cwd packages/ennodia-io start

By default IO listens on 127.0.0.1:17273. Non-loopback binding requires an API key:

Terminal window
ENNODIA_IO_API_KEY="local-secret" npx -y @cherninlab/ennodia-io --host 0.0.0.0

Flags:

FlagDefaultMeaning
--host127.0.0.1HTTP bind host.
--port17273HTTP bind port.
--api-keyENNODIA_IO_API_KEYRequire Authorization: Bearer <key>.
--max-request-body-size2097152Maximum request body bytes before a structured 413.
--max-concurrent-chat-completions4Maximum in-flight chat completions before a structured 429.

Environment variables:

VariableMeaning
ENNODIA_IO_HOSTDefault host when --host is omitted.
ENNODIA_IO_PORTDefault port when --port is omitted.
ENNODIA_IO_API_KEYAPI key required by the HTTP handler.
ENNODIA_IO_MAX_REQUEST_BODY_SIZEDefault request body byte cap.
ENNODIA_IO_MAX_CONCURRENT_CHAT_COMPLETIONSDefault in-flight chat completion cap.

IO binds to loopback by default and refuses non-loopback hosts unless an API key is configured. Bearer tokens are compared with a timing-safe comparison.

IO intentionally sends no Cross-Origin Resource Sharing (CORS) headers by default. The browser blocks local apps unless they use their own local proxy. There is no --cors-origin flag. Add one only when an app needs that trust boundary.

RouteStatusNotes
GET /healthSupportedReturns server name, status, and Ennodia version.
GET /v1/provider-optionsSupportedReturns app-facing local provider options for settings screens.
GET /v1/byok-optionsSupported aliasSame response as provider options.
GET /v1/modelsSupportedReturns local virtual model IDs for compatible clients.
POST /v1/chat/completionsSupported subsetNon-streaming text messages only.

Use provider options when building a settings screen:

Terminal window
curl http://127.0.0.1:17273/v1/provider-options

The response is intentionally app-facing:

{
"object": "list",
"defaultModel": "local/auto",
"compareModel": "local/compare",
"options": [
{
"id": "codex",
"label": "Codex CLI",
"kind": "local-agent-cli",
"status": "ready",
"available": true,
"runnable": true,
"configured": true,
"model": "local/codex",
"version": "codex-cli 0.141.0"
}
]
}

Option statuses:

StatusMeaning
readyThe local agent is runnable now.
installedAn app or partial install was detected, but Ennodia cannot run it through a supported command-line interface (CLI) surface yet.
missingThe supported local agent was not found.

Add ?includeUnavailable=false to return only ready options.

GET /v1/models returns local virtual models for OpenAI-compatible clients:

ModelMeaning
local/autoLet Ennodia choose the local harness.
local/compareRun parallel local harnesses, evaluate them with the Judge, and combine the result with the Result Advisor.
local/<harness-id>Force a specific harness, such as local/codex or local/claude-code.

Older aliases ennodia-auto, ennodia/auto, and ennodia/compare are accepted for compatibility. Prefer local/* IDs in new app integrations.

Minimal request:

Terminal window
curl http://127.0.0.1:17273/v1/chat/completions \
-H 'content-type: application/json' \
-d '{
"model": "local/auto",
"messages": [
{ "role": "user", "content": "Review this plan before I ship it." }
]
}'

With Ennodia options:

{
"model": "local/codex",
"messages": [
{ "role": "system", "content": "Be concise." },
{ "role": "user", "content": "Find the highest-risk issue." }
],
"ennodia": {
"category": "code",
"harnessId": "codex",
"mode": "single",
"compare": false,
"timeoutMs": 300000
}
}

Supported top-level fields:

FieldMeaning
modelRequired OpenAI-compatible routing label. Use local/auto, local/compare, or a provider option’s model.
messagesRequired array of text-only system, user, assistant, or tool messages.
streamOptional. Must be false or omitted.
temperature, top_p, max_tokens, userAccepted for client compatibility but not used for local agent routing.
ennodiaOptional Ennodia-specific routing and timeout options.

Supported ennodia fields:

FieldMeaning
categoryOptional caller-provided route category: code, research, browser, image, or general.
harnessIdForce one harness by adapter ID, such as codex or claude-code.
modeauto, single, or parallel.
compareauto, true, or false.
cwdWorking directory for child harness commands.
modelChild harness model override. Top-level model is only the HTTP routing label.
timeoutMsPer-child timeout, capped at one hour.
refreshRe-scan harnesses before planning.
judgeHarnessId, judgeModelCompare judge overrides.
advisorHarnessId, advisorModelCompare Result Advisor overrides.
synthesizerHarnessId, synthesizerModelDeprecated aliases for advisorHarnessId and advisorModel. If both forms are supplied, their values must match.
maxOutputCharsCandidate output cap for Compare.
maxWaitMsHTTP request wait cap, capped at one hour.

Chat completions are non-streaming. A request waits until the Ennodia run finishes or maxWaitMs elapses. The default is 10 minutes. Disable streaming in OpenAI-compatible clients and set ennodia.maxWaitMs when the client or proxy needs a shorter or longer hold time.

The response includes the normal OpenAI-style choices[0].message.content plus an ennodia object. The object contains the run ID, status, selected harnesses, task IDs, Compare ID when present, and budget estimate.

Limits and failures are returned as JSON with cache-control: no-store:

  • request body above the configured cap -> 413 request_too_large
  • saturated chat completions -> 429 rate_limit_error with retry-after: 1
  • client abort after the run starts -> 499 client_closed_request. IO cancels the Ennodia run when Core exposes cancellation
  • timeout, disappeared run, or start failure -> 502 ennodia_run_error
  • failed run or run with cancelled status -> 502 ennodia_run_failed plus run metadata

Apps can import the same primitives without running the HTTP server:

import { createDefaultEnnodiaCore } from "ennodia";
import { listAppProviderOptions } from "@cherninlab/ennodia-io";
const core = createDefaultEnnodiaCore();
const providerOptions = await listAppProviderOptions(core);

The app stays in control of its settings UI, labels, persistence, and consent flow. Ennodia supplies local discovery and execution primitives.

From a checkout:

Terminal window
bun run io:smoke

The smoke test starts a local IO server and checks /health, /v1/provider-options, and /v1/models without launching child agents.

IO deliberately rejects or omits:

  • streaming responses. Child CLIs return text at task boundaries, not token deltas, so clients must set stream: false or omit stream.
  • tool calls and function calls
  • multimodal content arrays, image input, audio input, and file uploads
  • embeddings, responses, assistants, batches, and fine-tuning routes
  • hosted model fallback, provider pricing controls, or provider billing
  • IO-specific history endpoints. Terminal run history is written by Core when history is enabled, but IO does not expose a read API for it.

Use MCP to inspect detailed run, task, Judge, Result Advisor, and Compare state while work is in progress. Use IO when a local app needs a small HTTP bridge.