Skip to content
Hoody.com

These endpoints let you browse the agent’s merged model and provider catalogue, manage provider authentication (API keys, OAuth logins, pooled OAuth accounts), tune the Jev decision subsystem, and run tool-free model completions (one shot or streamed). Use them to discover what is bindable, configure credentials, and execute ad-hoc calls against the underlying LLM providers without an active session.

All endpoints are scoped to a single container. The container hostname is {projectId}-{containerId}-agent-1.{server}.containers.hoody.com, with {projectId} and {containerId} each exactly 24 lowercase hexadecimal characters and {server} the server label (for example node-example-1).

List the merged model catalogue: every catalogued provider model (spec, display name, context window, output limit, reasoning flag, display prices) AND every selectable fusion composite. Each entry carries source:"provider" or source:"fusion" so a caller can tell them apart. A composite’s context_window is the minimum across its catalogued members and it carries no per-token price. Only SELECTABLE composites appear; a broken composite is excluded (use client.agent.fusions.list({ include_invalid: true }) to manage those). Fusion composites are cwd/config_dir scoped, so X-Hoody-Cwd and X-Hoody-Config-Dir are honored. A per-request realm header, or X-Hoody-Container (which has no model dimension), returns 400 realm_scope_unsupported.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/models?page=1&limit=50" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
pagequeryintegerNo1-based page number for pagination.
limitqueryintegerNoMaximum items per page (0 = no pagination).
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (for example POST /todos; client.agent.todos.create() also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
{
"items": [],
"meta": { "total": 0, "page": 1, "limit": 50 }
}
Error CodeTitleDescriptionResolution

Returns one catalogued model by its full spec (for example anthropic/claude-opus-4-8), OR a fusion composite by its fusion/<slug> spec. The spec itself contains a slash, so pass it URL-encoded or as the trailing path segment. A per-request realm header, or X-Hoody-Container, returns 400 realm_scope_unsupported.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/models/anthropic%2Fclaude-opus-4-8" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
specpathstringYesThe spec.
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
{
"spec": "anthropic/claude-opus-4-8",
"display_name": "Claude Opus 4.8",
"source": "provider",
"provider": "anthropic",
"context_window": 200000,
"output_limit": 32000,
"reasoning": true
}
Error CodeTitleDescriptionResolution

Lists every catalogued LLM provider (id, display name, model prefix, wire format, model count) from the merged providers.json catalogue. The catalogue is process-wide with no realm dimension, so a per-request realm header returns 400 realm_scope_unsupported.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers?page=1&limit=50" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
pagequeryintegerNo1-based page number for pagination.
limitqueryintegerNoMaximum items per page (0 = no pagination).
{
"items": [],
"meta": { "total": 0, "page": 1, "limit": 50 }
}
Error CodeTitleDescriptionResolution

Returns one provider’s metadata plus its full catalogued model list.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/anthropic" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
{
"id": "anthropic",
"display_name": "Anthropic"
}
Error CodeTitleDescriptionResolution

Adds a custom AI provider: an endpoint speaking the OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages API, with its catalogued models. Its models are selectable as <model_prefix>/<model> wherever a model is needed (createSession, PATCH /sessions/{id}/model, agents, Jev) once the provider’s key is stored. Keys never go in this body: store the provider’s key with PUT /providers/{id}/auth/api-key once the provider exists. base_url must be an https URL with no user name, password, query, or fragment, at a public address: an internal address, a name that only resolves inside a network, or a name that resolves to an internal address when a request is made is refused.

To retry a create safely, send an Idempotency-Key header: a retry with the same key and body answers 201 with the provider the first try created; the same key with another body is refused 422 idempotency_key_reused. Without a key, a retry whose first try succeeded answers 409 provider_exists.

Process-wide: a per-request realm, cwd, config-dir, or container header returns 400 realm_scope_unsupported.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <your-key-here>" \
-d '{
"id": "acme",
"display_name": "Acme AI",
"base_url": "https://api.acme.example/v1",
"wire_format": "chat_completions",
"models": [
{ "model": "acme-large", "context_window": 128000, "output_limit": 8192 }
]
}'
NameInTypeRequiredDescription
Idempotency-KeyheaderstringNoOpaque retry key (1-255 printable ASCII, no whitespace). A retry with the same key and body answers 201 with the provider the first try created; the same key with another body is 422 idempotency_key_reused. Keys are remembered for 24 hours, until the agent restarts.
NameTypeRequiredDescription
idstringYesThe provider id: 1-40 lowercase letters, digits, or inner hyphens. It cannot be changed later.
model_prefixstringNoThe prefix of the provider’s model specs (acme for acme/<model>). It must equal the id; omitted, it is the id. No provider’s id or prefix may already use it, and fusion is reserved.
display_namestringNoDisplay name. Omitted, it is the id.
wire_formatstringNoThe API the endpoint speaks: chat_completions (OpenAI-compatible Chat Completions, the default on create), responses (OpenAI Responses), or messages (Anthropic Messages).
base_urlstringYesThe endpoint’s base URL, the part before /chat/completions, /responses, or /v1/messages (for example https://api.acme.example/v1). Must be an https URL with no user name, password, query, or fragment, at a public address.
auth_schemestringNoHow the stored key is sent. It follows wire_format: bearer (Authorization: Bearer <key>) with chat_completions and responses, x-api-key with messages. Omitted, it is that value; any other pairing is refused 422 provider_invalid.
headersobjectNoStatic request headers sent on every request, chat_completions only, at most 16. Credential and transport headers (Authorization, x-api-key, api-key, Cookie, Host, Content-Type, Content-Length, Connection, and the other hop-by-hop headers) are refused. Values must not contain ${...}.
modelsarrayYesThe provider’s models, 1 to 500, each selectable as <model_prefix>/<model>.

Each item in models is an object with:

NameTypeRequiredDescription
modelstringYesThe model id the endpoint expects, as sent upstream (for example llama-3.3-70b). The model’s spec is <model_prefix>/<model>.
context_windowintegerNoInput context window in tokens. 0 or omitted means unknown, which also turns automatic compaction off for the model.
output_limitintegerNoMaximum output tokens. 0 or omitted means unknown.
reasoningbooleanNoMarks a reasoning-capable model in the catalogue. Default false.
{
"provider": {
"auth_scheme": "bearer",
"base_url": "https://api.acme.example/v1",
"display_name": "Acme AI",
"headers": {},
"id": "acme",
"model_prefix": "acme",
"models": [
{
"context_window": 128000,
"display_name": "Acme Large",
"model": "acme-large",
"output_limit": 8192,
"reasoning": false,
"spec": "acme/acme-large"
}
],
"prefix": "acme/",
"wire_format": "chat_completions"
}
}
Error CodeTitleDescriptionResolution

Changes the members the body lists, leaving omitted members as they are. models and headers replace the current list and map. id and model_prefix are fixed: a body that changes model_prefix is refused 400 provider_field_immutable. Built-in providers cannot be changed (409 provider_builtin), nor can a custom provider defined in a project’s providers file (409 provider_not_managed). The stored key is kept. The change applies at once, with no restart: new sessions, model switches, and Jev see it, and every open session using one of the provider’s models picks it up before its next turn.

base_url must be an https URL with no user name, password, query, or fragment, at a public address.

Process-wide: a per-request realm, cwd, config-dir, or container header returns 400 realm_scope_unsupported.

Terminal window
curl -X PATCH "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/acme" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"base_url": "https://eu.api.acme.example/v1"
}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.
NameTypeRequiredDescription
model_prefixstringNoFixed at creation. Accepted only when equal to the current prefix; any other value is refused 400 provider_field_immutable.
display_namestringNoDisplay name.
wire_formatstringNoThe API the endpoint speaks: chat_completions, responses, or messages.
base_urlstringNoThe endpoint’s base URL. Must be an https URL with no user name, password, query, or fragment, at a public address.
auth_schemestringNoHow the stored key is sent. It follows wire_format: bearer with chat_completions and responses, x-api-key with messages. Any other pairing is refused 422 provider_invalid.
headersobjectNoStatic request headers sent on every request, chat_completions only, at most 16. Credential and transport headers are refused. Values must not contain ${...}. On PATCH the object replaces the current headers; {} or null clears them.
modelsarrayNoThe provider’s models, 1 to 500, each selectable as <model_prefix>/<model>. On PATCH the list replaces the current one.

Each item in models is an object with:

NameTypeRequiredDescription
modelstringYesThe model id the endpoint expects, as sent upstream. The model’s spec is <model_prefix>/<model>.
context_windowintegerNoInput context window in tokens. 0 or omitted means unknown.
output_limitintegerNoMaximum output tokens. 0 or omitted means unknown.
reasoningbooleanNoMarks a reasoning-capable model. Default false.
{
"provider": {
"auth_scheme": "bearer",
"base_url": "https://api.acme.example/v1",
"display_name": "Acme AI",
"headers": {},
"id": "acme",
"model_prefix": "acme",
"models": [
{
"context_window": 128000,
"display_name": "Acme Large",
"model": "acme-large",
"output_limit": 8192,
"reasoning": false,
"spec": "acme/acme-large"
}
],
"prefix": "acme/",
"wire_format": "chat_completions"
}
}
Error CodeTitleDescriptionResolution

Removes a custom provider and the API key stored for it. Built-in providers cannot be removed (409 provider_builtin), nor can a custom provider defined in a project’s providers file (409 provider_not_managed). An open session using one of its models becomes unconfigured: its next prompt fails with session_unconfigured naming the removed provider, and switching the session to another model (PATCH /sessions/{id}/model) recovers it. A session in a turn finishes that turn first. Agents and Jev settings that name one of its models keep the name and fail the same way until changed.

Process-wide: a per-request realm, cwd, config-dir, or container header returns 400 realm_scope_unsupported.

Terminal window
curl -X DELETE "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/acme" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
{
"id": "acme",
"deleted": true,
"api_key_removed": true
}
Error CodeTitleDescriptionResolution

Reports a provider’s stored-credential state: whether an API key or OAuth login is stored, the effective default method, the passwordless (no-auth) flag, and the secret-free OAuth account pool.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/anthropic/auth" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
{
"id": "anthropic",
"default": "api_key",
"passwordless": false
}
Error CodeTitleDescriptionResolution

GET /api/v1/agent/providers/{id}/auth/accounts

Section titled “GET /api/v1/agent/providers/{id}/auth/accounts”

Lists the secret-free OAuth account pool for a provider: key, label, active flag, cooldown. Never a token. 422 oauth_unsupported when the provider has no OAuth flow.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/accounts?page=1&limit=50" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
pagequeryintegerNo1-based page number for pagination.
limitqueryintegerNoMaximum items per page (0 = no pagination).
{
"items": [],
"meta": { "total": 0, "page": 1, "limit": 50 }
}
Error CodeTitleDescriptionResolution

POST /api/v1/agent/providers/{id}/auth/accounts

Section titled “POST /api/v1/agent/providers/{id}/auth/accounts”

Begins an OAuth login that ADDS to the provider’s account pool. Returns a {job_id}; drive it with the same poll and submit endpoints as POST /providers/{id}/auth/oauth. 422 oauth_unsupported when the provider has no OAuth flow.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/accounts" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.

This endpoint accepts an empty JSON object. No fields are required; the login flow is started for the {id} provider.

{}
{
"job_id": "job-01HZX2EXAMPLE",
"verification_uri": "https://openrouter.ai/auth?code=ABC123",
"user_code": "ABC123"
}
Error CodeTitleDescriptionResolution

POST /api/v1/agent/providers/{id}/auth/oauth

Section titled “POST /api/v1/agent/providers/{id}/auth/oauth”

Begins an interactive OAuth login for a provider and returns {success, job_id, verification_uri, user_code?}. Drive the flow with GET .../oauth/{job} (poll) and, for a manual or PKCE code, POST .../oauth/{job}/code (submit). The daemon’s own admin check may still refuse with 403 admin_unauthorized when the daemon was started with a separate administrator token, which an HTTP request cannot supply. The job carries only the non-secret verification URL and user code. 422 oauth_unsupported when the provider has no OAuth flow.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/oauth" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"add_account": true}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.
NameTypeRequiredDescription
add_accountbooleanNoWhen true, the login ADDS to the provider’s OAuth account pool instead of replacing the primary login.
{
"success": true,
"job_id": "job-01HZX2EXAMPLE",
"verification_uri": "https://openrouter.ai/auth?code=ABC123",
"user_code": "ABC123"
}
Error CodeTitleDescriptionResolution

GET /api/v1/agent/providers/{id}/auth/oauth/{job}

Section titled “GET /api/v1/agent/providers/{id}/auth/oauth/{job}”

Polls an in-flight OAuth login job. Reports state:"pending" until the user authorizes, then state:"complete" (with the now-current secret-free auth status) or state:"error" (with a scrubbed error message).

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/oauth/job-01HZX2EXAMPLE" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
jobpathstringYesThe job.
{
"state": "pending",
"verification_uri": "https://openrouter.ai/auth?code=ABC123",
"user_code": "ABC123",
"needs_code": false
}
Error CodeTitleDescriptionResolution

POST /api/v1/agent/providers/{id}/auth/oauth/{job}/code

Section titled “POST /api/v1/agent/providers/{id}/auth/oauth/{job}/code”

Supplies the authorization code (or full redirect URL) a blocked OAuth flow is waiting for. Idempotent-safe: a second submit on an already-fed job is a no-op (submitted:false). 404 job_not_found when the job is unknown or expired.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/oauth/job-01HZX2EXAMPLE/code" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"code": "AUTH_CODE_FROM_PROVIDER"}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.
jobpathstringYesThe job.
NameTypeRequiredDescription
codestringYesThe authorization code (or the full redirect URL) to complete the exchange.
{
"submitted": true,
"message": "code accepted"
}
Error CodeTitleDescriptionResolution

PUT /api/v1/agent/providers/{id}/auth/api-key

Section titled “PUT /api/v1/agent/providers/{id}/auth/api-key”

Stores a provider’s API key in the agent’s owner-only credential file inside the container. The reply carries only the secret-free auth status (10-character prefix).

422 auth_method_unsupported for a passwordless provider.

Terminal window
curl -X PUT "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/anthropic/auth/api-key" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"api_key": "sk-ant-EXAMPLE-KEY"}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.
NameTypeRequiredDescription
api_keystringYesThe provider API key. Stored in the agent’s owner-only credential file; the reply echoes only a prefix.
{
"auth": {
"id": "anthropic",
"default": "api_key",
"api_key": { "set": true, "prefix": "sk-ant-EXA1" }
}
}
Error CodeTitleDescriptionResolution

DELETE /api/v1/agent/providers/{id}/auth/api-key

Section titled “DELETE /api/v1/agent/providers/{id}/auth/api-key”

Removes a provider’s stored API key and its default-method marker. Idempotent. Returns the resulting secret-free auth status.

Terminal window
curl -X DELETE "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/anthropic/auth/api-key" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
{
"auth": {
"id": "anthropic",
"default": null,
"api_key": { "set": false }
}
}
Error CodeTitleDescriptionResolution

PUT /api/v1/agent/providers/{id}/auth/default

Section titled “PUT /api/v1/agent/providers/{id}/auth/default”

Sets the effective default credential method for a provider (api_key or oauth). The daemon validates that the method is available AND that a credential of that kind is actually stored (else 422). 422 oauth_unsupported, auth_method_unsupported, or credential_not_stored as appropriate.

Terminal window
curl -X PUT "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/default" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"default": "oauth"}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.
NameTypeRequiredDescription
defaultstringYesThe default method: api_key or oauth. Must be a method the provider supports AND has a stored credential for.
{
"auth": {
"id": "openrouter",
"default": "oauth",
"oauth": { "set": true }
}
}
Error CodeTitleDescriptionResolution

PUT /api/v1/agent/providers/{id}/auth/accounts/{key}/active

Section titled “PUT /api/v1/agent/providers/{id}/auth/accounts/{key}/active”

Makes one pooled OAuth account active for a provider. The {key} path value names the account. Returns the resulting secret-free account pool. 422 oauth_unsupported when the provider has no OAuth flow.

Terminal window
curl -X PUT "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/accounts/acct_01/active" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{}'
NameInTypeRequiredDescription
idpathstringYesThe provider id.
keypathstringYesThe key.

This endpoint accepts an empty JSON object. No body fields are required; the account is identified by the {key} path value.

{}
{
"accounts": [
{ "key": "acct_01", "label": "Personal", "active": true, "cooldown": null }
]
}
Error CodeTitleDescriptionResolution

DELETE /api/v1/agent/providers/{id}/auth/accounts/{key}

Section titled “DELETE /api/v1/agent/providers/{id}/auth/accounts/{key}”

Drops one pooled OAuth account for a provider. The {key} path value names the account. Returns the resulting secret-free account pool. 422 oauth_unsupported when the provider has no OAuth flow.

Terminal window
curl -X DELETE "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/accounts/acct_01" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
keypathstringYesThe key.
{
"accounts": []
}
Error CodeTitleDescriptionResolution

DELETE /api/v1/agent/providers/{id}/auth/oauth

Section titled “DELETE /api/v1/agent/providers/{id}/auth/oauth”

Removes a provider’s stored OAuth credentials. Idempotent. Returns the resulting secret-free auth status. 422 oauth_unsupported when the provider has no OAuth flow.

Terminal window
curl -X DELETE "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/providers/openrouter/auth/oauth" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
idpathstringYesThe provider id.
{
"auth": {
"id": "openrouter",
"default": null,
"oauth": { "set": false }
}
}
Error CodeTitleDescriptionResolution

Jev evaluates state against named questions and returns calibrated answers. Question types are noul (yes/no, returning {noul: P(yes)}), choice (1 to 255 labels, returning {choice, confidence, probabilities}), and score (1 to 10 levels low to high, returning {score, confidence, probabilities, legend}).

Lists every Jev model spec per provider, plus each provider’s quirks. models is [{spec, display_name, provider, default, key_set, active}] and providers is [{id, display_name, key_set, key_env_var, default_model, quirks}]. quirks records each provider’s differences: nullable_instructions, partial_noul_criteria, routing_extras (OpenRouter’s provider, session_id, trace, user, x_session_id), cost_in_body, request_id_header, accepted_models, rejected_models, and verified. effective_model names the model in use.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/jev/models" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
{
"models": [],
"providers": [],
"effective_model": ""
}
Error CodeTitleDescriptionResolution

Returns the Jev settings of this install and the usage since the agent started. model is the explicit jev.model ("" when unset); effective_model is the model calls use, with source "explicit" or "derived" (derived = the default of the first provider with a key, preferring typesafe, then openrouter, then vercel). provider and key_set describe the effective model’s provider. usage totals every Jev call this agent made: calls, errors, attempts, cost_usd (null once any call’s spend is unknown), known_cost_usd (the proven subtotal), unknown_cost_calls, tokens, and the last call’s latency, model, and error code.

X-Hoody-Config-Dir selects the install; a realm or container header is rejected.

Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/jev/settings" \
-H "Authorization: Bearer <token>"
NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
{
"enabled": true,
"model": "",
"effective_model": "typesafe/jev-latest",
"source": "derived"
}
Error CodeTitleDescriptionResolution

Patches jev.enabled, jev.model, and/or jev.timeout_ms. An omitted field is left unchanged, and at least one is required. model is a provider/model spec from GET /jev/models, or any model id the provider accepts (a new Jev version needs no agent update). Model ids a provider is known to reject are refused (OpenRouter rejects jev-1.13.0, jev-preview, and jev-1.12). "" clears the model so it is derived again. Returns the updated settings, as GET /jev/settings does.

Keys are not set here; use PUT /providers/{id}/auth/api-key.

Terminal window
curl -X PUT "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/jev/settings" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "model": "typesafe/jev-latest", "timeout_ms": 15000}'
NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
NameTypeRequiredDescription
enabledbooleanNoWhether Jev calls are allowed (default true). When false every decide/test call fails with jev_disabled.
modelstringNoProvider/model spec, for example typesafe/jev-latest or openrouter/typesafe/jev-1.13. "" returns to the derived default.
timeout_msintegerNoBudget in milliseconds for one whole call, the one retry included: 100 to 120000 (default 15000).
{
"enabled": true,
"model": "typesafe/jev-latest",
"effective_model": "typesafe/jev-latest",
"source": "explicit"
}
Error CodeTitleDescriptionResolution

Evaluates state against named questions and returns calibrated answers. The body is the provider request, passed through without narrowing. state is a string, a JSON object, or a JSON array (never null, a number, or a boolean). questions maps a name to {type:"noul", instructions, criteria?}, {type:"choice", instructions, criteria}, or {type:"score", instructions, criteria}. Optional: model, timeout_ms (100 to 120000), and OpenRouter’s provider, session_id, trace, user, x_session_id.

Checks run before any network call (label and level limits, per-provider nullability). A transport failure, 5xx, or 429 is retried once (a 429 only when its Retry-After is at most 5 s and fits the budget). Every X-Hoody-Realm / ?realm= (including global), X-Hoody-Cwd, X-Hoody-Container, and X-Hoody-Config-Dir is refused with 400 realm_scope_unsupported.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/jev/decide" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"state": { "ticket": "I was charged twice for my order." },
"questions": {
"billing": { "type": "noul", "instructions": "Is this about billing?" },
"urgency": { "type": "score", "instructions": "How urgent is it?", "criteria": ["low", "medium", "high"] }
}
}'
NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
NameTypeRequiredDescription
statestring | object | arrayYesWhat the questions are about: a string, a JSON object, or a JSON array (never null, a number, or a boolean). Passed to the provider as is.
questionsobjectYesQuestion name to {type: noul|choice|score, instructions, criteria}. At least one.
modelstringNoProvider/model spec to use instead of the effective model.
timeout_msintegerNoBudget for this call in milliseconds (100 to 120000), the one retry included.
providerobjectNoOpenRouter only: provider routing preferences.
session_idstringNoOpenRouter only: session grouping id (up to 256 characters).
traceobjectNoOpenRouter only: trace configuration.
userstringNoOpenRouter only: end-user id (up to 256 characters).
x_session_idstringNoOpenRouter only: sent as the x-session-id header.
{
"answers": {},
"model": "typesafe/jev-latest"
}
Error CodeTitleDescriptionResolution

Sends one small yes/no question (about 280 input tokens, a fraction of a cent) to the effective model, or to model when given, and returns {ok, latency_ms, model, provider, answer, usage}. A failure returns the typed jev_* error, so this is how to check a key and model before relying on them. The call counts toward the usage in GET /jev/settings.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/jev/test" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"model": "typesafe/jev-latest"}'
NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
NameTypeRequiredDescription
modelstringNoA provider/model spec to test instead of the effective model.
{
"ok": true,
"latency_ms": 280,
"model": "typesafe/jev-latest",
"provider": "typesafe",
"answer": { "noul": 0.5 },
"usage": { "input_tokens": 280, "output_tokens": 1, "cost_usd": 0.00012 }
}
Error CodeTitleDescriptionResolution

POST /api/v1/agent/sessions/{id}/jev/decide

Section titled “POST /api/v1/agent/sessions/{id}/jev/decide”

The same decision as POST /jev/decide (same body, reply, and jev_* errors), made for a LIVE session: the Jev settings and keys come from the session’s own config root, and the call’s usage is emitted as event.jev_usage on the session’s stream and added to the session’s cost, as well as to the totals in GET /jev/settings. The body’s session_id stays OpenRouter’s grouping id; the session is the path’s {id}. A session that is not live or not visible to this caller is a plain 404. X-Hoody-Realm or ?realm= names the realm the session is looked up in (global or a 24-hex realm id; omitted, the agent’s current realm); a session in another realm, or in a realm this login does not serve, is the plain 404. The session’s scope is frozen: an X-Hoody-Container, X-Hoody-Cwd or X-Hoody-Config-Dir header returns 400 realm_scope_unsupported.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/sessions/sess-01HZX2EXAMPLE/jev/decide" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"state": { "ticket": "I was charged twice for my order." },
"questions": {
"billing": { "type": "noul", "instructions": "Is this about billing?" }
}
}'
NameInTypeRequiredDescription
idpathstringYesThe session id.
X-Hoody-RealmheaderstringNoPer-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
NameTypeRequiredDescription
statestring | object | arrayYesWhat the questions are about: a string, a JSON object, or a JSON array.
questionsobjectYesQuestion name to {type: noul|choice|score, instructions, criteria}. At least one.
modelstringNoProvider/model spec to use instead of the effective model.
timeout_msintegerNoBudget for this call in milliseconds (100 to 120000).
providerobjectNoOpenRouter only: provider routing preferences.
session_idstringNoOpenRouter only: session grouping id (up to 256 characters).
traceobjectNoOpenRouter only: trace configuration.
userstringNoOpenRouter only: end-user id (up to 256 characters).
x_session_idstringNoOpenRouter only: sent as the x-session-id header.
{
"answers": {},
"model": "typesafe/jev-latest"
}
Error CodeTitleDescriptionResolution

Sends messages (and an optional system prompt) to model once, with no tools, no session, and no transcript, and answers the whole reply as JSON. settings sets per-call thinking, temperature, max_tokens, and response_format; a setting the model or its adapter cannot honour is refused with 400 unsupported_setting before any network call, never dropped. applied reports what was actually sent. A reply that stops on max_tokens, refusal, or content_filter is still a 200 carrying the text so far (read stop_reason); a tool call or an error stop is 502 upstream_error. json_valid (present when response_format is set) is a syntax check of the text, not a schema check. OpenAI-style json_object mode needs the word “json” in the prompt.

Usage is recorded in the usage log whenever the model was called, success or not. This route takes no stream slot: each client IP may hold up to the gateway’s completion cap (default 32) in flight, above which it answers 503 service_unavailable with Retry-After; the request-rate limit applies as usual. Fan-out clients should use this route rather than client.agent.completions.create() with stream: true.

Every X-Hoody-Realm / ?realm= (including global), X-Hoody-Cwd, X-Hoody-Container, and X-Hoody-Config-Dir is refused with 400 realm_scope_unsupported: a completion has no scope.

Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/completions" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"model": "xiaomi-token-plan-sgp/mimo-v2.5",
"system": "Reply with a JSON object.",
"messages": [{ "role": "user", "content": "List three primary colours as json." }],
"settings": {
"thinking": { "type": "disabled" },
"temperature": 0.2,
"max_tokens": 2048,
"response_format": { "type": "json_object" }
},
"timeout_ms": 120000
}'

This endpoint takes no parameters.

NameTypeRequiredDescription
modelstringYesA provider/model spec, for example xiaomi-token-plan-sgp/mimo-v2.5. The provider prefix must be catalogued. An unknown provider or an empty model name is 422 model_unavailable (reason unknown_model); a fusion/ composite is 422 model_unavailable (reason fusion).
systemstringNoOptional system prompt.
messagesarrayYes1 to 1000 turns, oldest first; the last must be a user turn.
settingsobjectNoOptional per-call model settings. A setting the model cannot honour is 400 unsupported_setting, never dropped.
timeout_msintegerNoDeadline for the whole call, 1 to 600000 ms (default 120000). Reaching it is 504 timeout.
max_tokensintegerNoAlias of settings.max_tokens (output-token cap, 1 to 1000000). Sending both with different values is 400 bad_request (details.field max_tokens).

Each message in messages is an object with:

NameTypeRequiredDescription
rolestringYesWho spoke. One of: user, assistant.
contentstringYesNon-empty text.

The settings object supports:

NameTypeRequiredDescription
thinkingobjectNoReasoning: {type: enabled|disabled, effort?} — {low, medium, high, xhigh, max}. effort is allowed only with enabled. Absent keeps the model’s default reasoning.
temperaturenumberNoSampling temperature, 0 to 2 (the model’s own range applies; many models accept it only with thinking disabled).
max_tokensintegerNoOutput cap, 1 to 1000000, clamped to the model’s output limit.
response_formatobjectNo{type: json_object}, or {type: json_schema, schema: {...}, name?, strict?}. name matches ^[A-Za-z0-9_-]{1,64}$ and defaults to "response".
{
"status": "ok",
"model": "xiaomi-token-plan-sgp/mimo-v2.5",
"text": "ok",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 1,
"output_tokens": 1,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"cost_usd": 0.0,
"cost_source": "provider",
"applied": {
"thinking": "disabled",
"max_tokens": 2048
},
"duration_ms": 10
}
Error CodeTitleDescriptionResolution

The same call as client.agent.completions.create() without stream, streamed as Server-Sent Events: start, then delta (text) and thinking frames as they arrive, then exactly one result (the body that call returns) or error, then end. Nothing is written before the model call starts: a failure before start (validation, unsupported_setting, model_unavailable, timeout, the stream cap) is an ordinary JSON error with its HTTP status. After start a failure is an error frame {code, message, details?} carrying the same codes (upstream_error, timeout, internal_error). A : ping comment is sent every 25 s while the stream is open. There is no retry: hint and the stream must not be replayed automatically: every call is billed. Read it with fetch and an AbortController; aborting cancels the model call.

Each open stream holds one of the client IP’s stream slots (shared with session streams, default 16); over the cap is 503 service_unavailable with Retry-After. Fan-out clients should use client.agent.completions.create() without stream.

Every X-Hoody-Realm / ?realm= (including global), X-Hoody-Cwd, X-Hoody-Container, and X-Hoody-Config-Dir is refused with 400 realm_scope_unsupported.

Terminal window
curl -N -X POST "https://{projectId}-{containerId}-agent-1.node-example-1.containers.hoody.com/api/v1/agent/completions/stream" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"model": "xiaomi-token-plan-sgp/mimo-v2.5",
"messages": [{ "role": "user", "content": "List three primary colours as json." }]
}'

This endpoint takes no parameters.

NameTypeRequiredDescription
modelstringYesA provider/model spec, for example xiaomi-token-plan-sgp/mimo-v2.5. The provider prefix must be catalogued. An unknown provider or an empty model name is 422 model_unavailable (reason unknown_model); a fusion/ composite is 422 model_unavailable (reason fusion).
systemstringNoOptional system prompt.
messagesarrayYes1 to 1000 turns, oldest first; the last must be a user turn.
settingsobjectNoOptional per-call model settings. A setting the model cannot honour is 400 unsupported_setting, never dropped.
timeout_msintegerNoDeadline for the whole call, 1 to 600000 ms (default 120000). Reaching it is 504 timeout (an error frame once streaming).
max_tokensintegerNoAlias of settings.max_tokens (output-token cap, 1 to 1000000). Sending both with different values is 400 bad_request (details.field max_tokens).
event: start
data: {"model":"xiaomi-token-plan-sgp/mimo-v2.5"}
event: delta
data: {"text":"..."}
event: result
data: {"status":"ok","model":"...","text":"...","stop_reason":"end_turn","usage":{...},"cost_usd":0.0,"cost_source":"provider","applied":{...},"duration_ms":10}
event: end
data: {}

The body is a Server-Sent Events stream, not JSON.

Error CodeTitleDescriptionResolution