Skip to main content

Model ABI

There is only one model ABI:

/ctx/model/<provider>/<model> one-shot inference executable
/ctx/model/<provider>/<model>.sock optional CortexFS session socket
/ctx/model/<provider>/<model>.d/ control files
/ctx/model/main default model symlink
/ctx/model/{helper,fast,reason,code,vision}
canonical compatibility/capability aliases

<provider>/<model> is represented as two path components. For native model providers, <provider> is the original provider identity:

/ctx/model/openai/gpt-5.6
/ctx/model/anthropic/claude-sonnet-5
/ctx/model/google/gemini-3.6-flash

For a custom domain base URL without a declared original provider mapping, <provider> is the normalized host name. For example, https://models.example.test:9000/ projects models under:

/ctx/model/models.example.test/compatible-model

Address-like endpoints such as 127.0.0.1, ::1, or localhost MUST set an explicit provider name in the host-side provider config. Without that name, the config is invalid because /ctx/model/<provider> must be a stable object name, not a transport address. For example:

{
"name": "local",
"base_url": "http://127.0.0.1:8317/v1",
"default_model": "custom-model",
"enabled": true,
"formats": ["openai.chat", "openai.responses"]
}

This projects as /ctx/model/local/custom-model.

The custom base URL is provider-adapter configuration, not a root ABI namespace. It may be shown in model/<provider>/<model>.d/default for inspection, but secrets never appear in model metadata or .d/ files.

Bottom-layer AI API formats do not enter the ABI. OpenAI Responses, Anthropic Messages, Gemini GenerateContent, OpenAI-compatible chat, local runtimes, and aggregator-specific request formats are Rig or provider-adapter details. Bottom-layer stateful/stateless behavior does not enter the ABI. Rig adapts provider connections, API compatibility, and streaming into the canonical CortexFS request and JSONL event stream.

Example:

/ctx/model/
main -> /ctx/model/openai/gpt-5.6
helper -> /ctx/model/openai/gpt-5.6-sol
fast -> /ctx/model/openai/gpt-5.6
reason -> /ctx/model/openai/gpt-5.6
code -> /ctx/model/openai/gpt-5.6
vision -> /ctx/model/openai/gpt-5.6
debug/
echo
echo.d/
id
driver
cap
effort
default
fallback
limit
session
status
log
openai/
gpt-5.6
gpt-5.6.d/
id
driver
cap
effort
default
fallback
session
status
log

main, helper, fast, reason, code, and vision are the complete canonical alias set. helper remains a compatibility alias. Bootstrap may select a projected model whose provider-neutral metadata matches a capability alias; when it cannot identify one, that alias points to the selected main target. Existing valid user-managed alias symlinks are preserved.

Control files:

id provider-native model id or runtime-internal model id
driver driver route table; see below
cap capability list, one per line
effort provider-neutral reasoning effort: auto, none, low, medium, high, xhigh, or max
default default parameters, KEY=VALUE, one per line
fallback ordered fallback model chain, one provider/model name per line
limit maximum hard context size in tokens, or unknown
session none or socket
status dynamic status
log short call log or pointer to log location

Hard Context Limit

Every model control directory contains a read-only limit file:

/ctx/model/<provider>/<model>.d/limit

The file contains exactly one canonical LF-terminated line. Its value is either unknown or a positive base-10 u32 token count. Numeric values use no sign, surrounding whitespace, or leading zeroes. Zero, overflow, extra lines, and non-canonical decimal text are invalid. The number is the provider/model's hard combined context limit; it is not an output-token setting and must not be used as one.

Examples:

272000
unknown

unknown means CortexFS has no trusted maximum. It must not be rendered as zero or replaced with a guessed value. The executable model metadata field context_length contains the same canonical value as limit.

limit is an inspectable projection, never an Agent-writable control. FUSE opens and writes that request mutation must fail with EROFS, including for uid 0. Updating a limit happens only when host configuration changes or during the existing synchronous mount-start catalog refresh; there is no watcher, poller, or hot-reload path.

The resolver uses this precedence:

1. model_limits in the selected host provider config
2. a valid CortexFS-owned models.dev cache entry
3. unknown

A provider config may declare explicit limits for locally configured models without changing the backward-compatible string models list:

{
"name": "local",
"base_url": "http://127.0.0.1:8317/v1",
"models": ["custom-model"],
"model_limits": {
"custom-model": 32768
}
}

Each model_limits key must be a model listed by default_model or models, and each value must be in 1..=4294967295. Invalid local limit declarations make that provider config invalid; they are not silently ignored. A local entry overrides catalog data for the same projected model.

Provider configuration may also override stable semantic capabilities for individual declared models:

{
"name": "local",
"base_url": "http://127.0.0.1:8317/v1",
"models": ["text-model", "vision-model"],
"model_capabilities": {
"text-model": ["chat", "stream"],
"vision-model": ["chat", "stream", "vision"]
}
}

Each key must name default_model or an entry in models. Values must be unique stable capability words from the list below. Provider-private, unknown, or duplicate words make the provider configuration invalid. An explicit empty list is valid and projects an empty cap file. Models without an override use the adapter-derived capability projection.

CortexFS obtains catalog limits through the external models-dev library. Catalog provider and model map keys are matched exactly to the projected <provider>/<model> identity; transport hosts and aggregator names are not guessed as original providers. Only stable CortexFS provider/model names and positive limits enter the cache.

The host cache is bounded, versioned data with this shape:

{
"schema": "cortexfs.model-limits/v1",
"models": {
"openai/gpt-5.6": 272000
}
}

The cache is atomically replaced only after a complete successful online response has been parsed and validated. A timeout, network error, invalid or oversized response, empty validated result, or failed durable write preserves the last valid cache unchanged. A missing, malformed, oversized, wrong-schema, or unsafe cache supplies no limit. Catalog cache content contains no provider credentials and is backend state, not a new /ctx namespace.

fallback is a model fallback chain, not a transport route. It lives next to the selected model in model/<provider>/<model>.d/fallback; each non-comment line is another stable provider/model reference, for example:

openai/gpt-5.6
models.example.test/compatible-model

When the selected model is unavailable or fails before producing a successful answer, the runtime tries fallback models in order. Each candidate still uses the normal provider registry, secret lookup, and /ctx/model/route egress rules.

driver may be a legacy single driver name:

debug

or a route table:

default=openai-chat
exec=openai-chat
socket=openai-chat
agent=openai-responses,openai-chat

Route keys:

default fallback route
exec direct one-shot model file execution
socket direct model socket calls
agent agent-owned model calls

Each value is a comma-separated priority list. Runtime selection checks the use-case route first, then default. This lets direct model usage choose a classic chat driver while agents prefer a richer Responses-style driver with a chat fallback. Driver names are adapter names, not stable model names.

Secrets are never stored in model files or .d/ control files. Provider credentials use this priority:

root-owned CortexFS system secret store
unconfigured

The API key is read from /var/lib/cortexfs/secrets/provider/<provider>/<slot>. Provider JSON must not declare API-key environment variable names, and API keys must not be placed in process environments. If the system secret is absent, the model is not configured and must return a stable error unless the endpoint supports unauthenticated requests.

OAuth providers use the same rule: access tokens are bearer credentials and remain provider-runtime state, not model ABI state. A provider config may declare OAuth Authorization Code + PKCE metadata:

{
"base_url": "https://api.example.com/v1",
"oauth": {
"client_id": "cortexfs-local",
"auth_url": "https://auth.example.com/oauth/authorize",
"token_url": "https://auth.example.com/oauth/token",
"redirect_uri": "http://127.0.0.1:8765/callback",
"scopes": ["model.read", "offline_access"],
"device": {
"request_url": "https://auth.example.com/device/code",
"token_url": "https://auth.example.com/device/token",
"verification_uri": "https://auth.example.com/device"
}
}
}

OAuth token environment names are generated from provider identity, for example CTX_EXAMPLE_OAUTH_ACCESS_TOKEN and CTX_EXAMPLE_OAUTH_REFRESH_TOKEN; users do not configure those names in provider JSON. If the generated access-token variable is absent or empty, the runtime looks up service=cortexfs:<provider> account=oauth:access. Refresh tokens, when used by a provider adapter or CLI wrapper, use account=oauth:refresh by default. PKCE uses S256; the verifier and callback state are short-lived local flow state and must not be written into /ctx/model. ctx provider oauth login PROVIDER is the host-side helper that performs this PKCE login flow and writes tokens to the system keychain.

Provider Authentication Framework

Provider JSON may advertise more than one authentication method without coupling a model to a provider-specific login command:

{
"base_url": "https://api.example.com/v1",
"auth": [
{"type": "api_key", "slot": "default"},
{"type": "oauth", "flow": "authorization_code", "slot": "subscription"}
],
"oauth": {
"client_id": "cortexfs-example",
"auth_url": "https://auth.example.com/authorize",
"token_url": "https://auth.example.com/token",
"redirect_uri": "http://127.0.0.1:8765/callback",
"scopes": ["model.read", "offline_access"],
"device": {
"request_url": "https://auth.example.com/device/code",
"token_url": "https://auth.example.com/device/token",
"verification_uri": "https://auth.example.com/device"
}
}
}

type is api_key or oauth; OAuth flow is authorization_code or device_code. slot is a logical credential slot and is not a keychain account name. When auth is absent, CortexFS retains the compatibility defaults of an API-key default slot plus an authorization-code OAuth method when the legacy oauth block is present. Invalid slots or an OAuth method without OAuth metadata fail closed during provider snapshot loading.

Adapters implement one provider-neutral boundary (id, supported methods, authorization URL, login, device challenge, refresh, persistence, and model listing) and return the normalized credential shape. The host can inject the HTTP transport, clock, challenge notifier, and sleep callback for deterministic tests; Agents never receive that transport or provider-native response types. The built-in registry provides concrete OpenAI/Codex and Anthropic/Claude adapters, plus a GitHub Copilot adapter when the host supplies its OAuth app metadata. Claude and Copilot client registrations remain host configuration; no provider client id is compiled into the Agent path.

{
"type": "oauth",
"provider": "example",
"access_token": "…",
"refresh_token": "…",
"expires_at": 123456789,
"scopes": ["model.read"]
}

API-key credentials use type: "api_key", provider, and key. These are in-memory adapter envelopes only. Raw credentials never enter /ctx, model objects, .d/ controls, or model history; the existing root-owned secret store remains the persistence boundary. Inspect the declared methods with:

ctx provider auth methods PROVIDER

The command prints method<TAB>flow<TAB>slot and never prints secret material. Model listing remains provider-neutral and feeds the existing model projection and bounded host caches; it does not create an /ctx/identity namespace. The existing hardened host discovery request is issued through the selected adapter's model transport and parser, so provider-specific model envelopes do not leak into the model ABI. device_code is part of the shared declaration grammar. An OAuth device block supplies standard device-code endpoints for host-configured adapters. The built-in GitHub Copilot adapter supplies its documented defaults when that block is omitted; api.githubcopilot.com also maps to the stable github-copilot provider name when no explicit host name is supplied. Adapters implement the standard device challenge, bounded polling, and normalized credential persistence. The CLI prints the verification URI and user code but never stores the device code in /ctx.

Provider Presets

Provider presets are host-side JSON file templates. They install under /etc/cortexfs/providers.d/ and do not create a /ctx/provider namespace:

ctx provider preset list
ctx provider preset show openai|codex|anthropic|google
ctx provider preset install openai|codex|anthropic|google

Canonical provider names:

openai OpenAI API with `/v1/responses` for agent calls and
`/v1/chat/completions` fallback; `codex` is an alias
anthropic Claude Messages API
google Gemini through Google's OpenAI-compatible endpoint; `gemini` is an alias

The codex alias installs the OpenAI preset and projects Codex-recommended OpenAI models under the canonical provider path, for example /ctx/model/openai/gpt-5.6. It does not create /ctx/model/codex or a second provider namespace.

The Google preset uses Gemini's OpenAI-compatible endpoint. The Anthropic preset uses anthropic.messages, so the runner sends POST /v1/messages with the required Anthropic version header.

One-Shot Exec

/ctx/model/<provider>/<model> is a read-only executable object. Reading it returns CortexFS metadata text for that model. Executing it performs one-shot inference through CortexFS/Rust runtime code or a provider adapter; model objects must not be shell-script implementations.

The first metadata keys mirror Rig 0.39 model listing fields:

id
name
description
type
created_at
owned_by
context_length

Provider adapters may populate those fields from ModelListingClient::list_models() / ModelList. Built-in debug/* models are local debug metadata and do not imply a provider default.

/ctx/model/debug/echo "hello"
echo "hello" | /ctx/model/openai/gpt-5.6
echo '{"messages":[{"role":"user","content":"hello"}]}' | /ctx/model/openai/gpt-5.6

Semantics:

one invocation
no durable session mutation
stdout is the canonical JSONL event stream
exit code is the process-level summary
file content is inspectable metadata, not provider code or secrets

Even if the underlying provider has native state, /ctx/model/<provider>/<model> behaves as a stateless single call.

Global Model Route

Model proxying is not an agent and is not stored in provider JSON. The single global egress route table is:

/ctx/model/route

The file is ordinary CortexFS state. It is read only when a model request is made; if the file is absent, the projected default is fallback: direct.

Rules are evaluated top to bottom. A rule selects a group; a group selects both transport and an optional credential slot. Secrets are never written into the route file or provider JSON. key(NAME) selects /var/lib/cortexfs/secrets/provider/<provider>/NAME from the CortexFS system secret store. API keys are not placed in process environments. Without key(...), the default credential slot is default.

group(proxy) -> http(http://127.0.0.1:8080/v1), key(office)
group(local-socket) -> unix(/run/user/1000/cortexfs/proxy/openai.sock), key(local)

dip(198.51.100.45) -> direct
# dip(203.0.113.43) -> JP
domain(bestproxy.com) -> proxy
pname(NetworkManager, systemd-resolved, dnsmasq) -> must_direct
dip(geoip:private) -> direct
dip(geoip:cn) -> direct
domain(geosite:cn) -> direct
model(embedding-*) -> local-socket
fallback: proxy

Built-in group names:

direct use the provider base_url and default credential slot
must_direct same transport as direct, intended for policy readability

Custom groups use:

group(NAME) -> direct[, key(SLOT)]
group(NAME) -> http(BASE_URL)[, key(SLOT)]
group(NAME) -> unix(SOCKET_PATH[, BASE_URL])[, key(SLOT)]

Matchers currently include domain(...), dip(...), pname(...), provider(...), and model(...). model(...) and provider(...) accept exact names and trailing * prefixes.

Model Socket

/ctx/model/<provider>/<model>.sock is the only multi-turn model entry. It uses the shared JSONL socket protocol from object-abi.md.

Examples:

{"op":"send","id":"msg-1","session":"default","input":"hello"}
{"op":"resume","session":"default","after":"event-123"}
{"op":"cancel","id":"run-1"}
{"op":"ping"}

A model socket session is CortexFS session semantics, not provider-native session semantics. Native threads, response ids, context caches, and simulated message logs are hidden behind the canonical protocol.

model/<provider>/<model>.d/session has only two stable values:

none no /ctx/model/<provider>/<model>.sock
socket /ctx/model/<provider>/<model>.sock exists and supports CortexFS sessions

The value never describes provider-native state.

Capabilities

Use stable semantic capability words:

chat
stream
session
vision
audio_input
audio_output
json_schema
tool_call_syntax
reasoning
embedding
rerank

Provider-private or API-format-private capability words are forbidden in stable ABI:

openai_responses
anthropic_messages
gemini_generate_content
native_thread
native_stateful
native_stateless

tool_call_syntax only means the model event stream may contain tool-call-shaped events. It does not mean the model can execute tools. It grants no tool permission.

Canonical Event Stream

Model and agent streams use these event types:

start
delta
message
reasoning_delta
reasoning_message
tool_call
usage
error
done

Example:

{"type":"start","run":"r1","model":"debug/echo"}
{"type":"delta","run":"r1","text":"hello"}
{"type":"message","run":"r1","role":"assistant","content":[{"type":"text","text":"hello"}]}
{"type":"usage","run":"r1","input_tokens":10,"output_tokens":1}
{"type":"done","run":"r1","status":"ok"}

usage requires input_tokens and output_tokens. When reported by a provider, optional cached_tokens and cache_write_tokens record cache reads and writes without changing those totals.

Error example:

{"type":"error","run":"r1","code":"EACCES","message":"permission denied"}
{"type":"done","run":"r1","status":"error"}

code uses stable errno names. Clients must not parse message.

Native Diagnostics

model/<provider>/<model>.d/native may exist for diagnostics only:

native is diagnostic only
native is not stable ABI
strict clients must not depend on it

Tool Boundary

Model execution is not tool execution.

Hard rule:

model may emit tool_call events
model must not execute tools
agent decides whether to execute tools
agent policy decides whether execution is allowed

Model processes must not receive project mounts, tool credentials, or write access outside runtime-owned cache. Provider tool calling must not become a backdoor around agent policy.