English | 简体中文 | 繁體中文 | 日本語 | Русский
devo onboard is the recommended setup path. For manual configuration, Devo
merges settings in this order:
- Built-in defaults
DEVO_HOME/config.toml- user-level app config, defaulting to~/.devo/config.tomlon macOS/Linux andC:\Users\yourname\.devo\config.tomlon WindowsDEVO_HOME/providers.json- user provider connections and model selections<workspace>/.devo/config.toml- project-level app config<workspace>/.devo/providers.json- project provider/model overlay- CLI flags
Provider API keys live in the user-scoped auth.json. providers.json stores
only the credential id that points to the secret; the catalog itself is safe to
track and share.
Minimal shape (built-in catalog model + provider connection):
{
"model": "deepseek/deepseek-v4-flash",
"provider": {
"deepseek": {
"name": "DeepSeek",
"base_url": "https://api.deepseek.com/anthropic",
"credential": "deepseek_api_key",
"wire_api": "anthropic_messages",
"models": {
"deepseek-v4-flash": {
"name": "DeepSeek V4 Flash"
}
}
}
}
}The stable model identity is always provider/model: the provider map key is
the provider id and the nested model map key is the provider-facing model id.
There is no separate binding id, local slug, request model, or model
description to keep synchronized. Provider and model metadata are optional;
the tracked catalog supplies defaults and user/workspace files can add or
override entries.
Provider connections and model catalog entries are kept together in the JSON shape. Secrets remain in the separate user-scoped auth file:
{
"provider": {
"local": {
"name": "Local Gateway",
"base_url": "http://127.0.0.1:8000/v1",
"wire_api": "openai_chat_completions",
"credential": "local_gateway_api_key",
"models": {
"my-model": {
"name": "My Model",
"context_window": 131072
}
}
}
},
"model": "local/my-model"
}The tracked built-in directory is crates/core/providers.json. It is packaged
into Devo and should be changed through git. DEVO_HOME/providers.json and
<workspace>/.devo/providers.json are user/workspace overlays and may define
arbitrary custom providers and models.
The top-level model is the default primary provider/model used for normal
turns. The optional small_model is used for lightweight background work such
as session-title generation. When it is absent, Devo first looks for a
recognizably lightweight model in the same provider (for example flash,
nano, haiku, mini, small, or a small parameter-count model), then falls
back to the primary model. An invalid explicit small_model is ignored and
follows the same automatic fallback. The built-in directory leaves both
selections unset so it does not choose a provider for the user.
The directory currently includes Kimi (kimi-k3, kimi-k2.7-code,
kimi-k2.6), Z.ai and China BigModel/Zhipu AI (each with glm-5.3 and
glm-5.3-flash),
DeepSeek, Qwen, MiniMax,
Xiaomi MiMo, and Tencent Hunyuan. DeepSeek uses its official Anthropic-compatible
endpoint by default. It also includes a local ollama provider template
(http://localhost:11434/v1) with an empty model directory; after connecting,
use Discover (Ollama /api/tags or OpenAI-compatible /v1/models) to load
models installed locally.
The catalog is a curated starting point, not a closed allowlist. Add any
provider or model by repeating the same nested shape in a user or workspace
providers.json overlay.
Providers in the directory are read-only templates, not logged-in providers.
An embedded provider supplies its name, default base URL, wire API, and model
directory. Confirming it in onboarding creates a user Connection in the
user-level providers.json:
- Selecting an unconnected built-in provider opens the Connection settings
page. The template Base URL is the default and can be overridden before
connect; enter an API key (when required) to create the Connection. The key
is stored in
auth.json. - Selecting an already Connected built-in provider opens that Connection's saved model list. Choose an existing model to configure it, use the custom model card to add another model, or select a saved model and press d/Delete to remove it from this Connection. The provider template and its built-in directory remain unchanged.
- A Connected provider's API key and Base URL cannot be edited from this flow. To replace the key or endpoint, disconnect the Connection and connect the template again.
- Selecting a custom provider opens editable Connection settings, because its name, endpoint, protocol, models, and credential are user-owned.
- The provider picker marks user Connections as
Connectedand untouched directory entries asTemplate. Select a Connected entry and pressdorDeleteto confirm disconnection. This removes the user provider overlay and a credential that is not shared by another Connection; the built-in template remains available. Add custom providercreates a custom Connection. Its provider id, endpoint, protocol, models, and credential are user-owned. Disconnect it with the samed/Deleteaction; do not remove the tracked directory.
The provider directory and provider Connections are therefore separate
concepts: the directory can be tracked with git, while Connections and
auth.json remain user configuration.
The old TOML provider shape remains readable for migration, but all new
onboarding and provider/model writes use providers.json.
Legacy equivalent (read-only compatibility):
[providers."api.deepseek.com/anthropic"]
enabled = true
name = "api.deepseek.com/anthropic"
base_url = "https://api.deepseek.com/anthropic"
credential = "api_deepseek_com_anthropic_api_key"
wire_apis = ["anthropic_messages"]Put the credential id in providers.json and the actual key in the user-scoped
auth.json:
{
"provider": {
"my-provider": {
"base_url": "https://api.example.com/v1",
"credential": "my_provider_api_key",
"models": {
"my-model": {"name": "My Model"}
}
}
},
"model": "my-provider/my-model"
}~/.devo/auth.json (or C:\Users\yourname\.devo\auth.json on Windows):
{
"version": 1,
"credentials": {
"my_provider_api_key": {
"kind": "api_key",
"value": "sk-your-key"
}
}
}devo onboard and the Desktop/TUI provider flows write both files. The
credential id must match exactly in the two files. Do not put apiKey,
api_key, or a secret in providers.json; those fields are not part of the
canonical provider schema. auth.json is user-scoped and must not be committed.
auth.json fields are:
| Field | JSON type | Meaning |
|---|---|---|
version |
integer | Credential file schema version; currently 1. |
credentials |
object | Map of credential ids to credential records. |
credentials.<id>.kind |
enum | Currently only api_key is supported. |
credentials.<id>.value |
string | The secret API key. |
If onboarding receives an API key without an explicit credential id, it uses a
stable id based on the provider id, such as deepseek_api_key. A missing
auth.json behaves like an empty credential file; a referenced but missing id is
an error.
The following pairs a custom DeepSeek model (Anthropic Messages), a provider
endpoint, and an API key stored in auth.json.
~/.devo/providers.json (or C:\Users\yourname\.devo\providers.json on Windows):
{
"model": "deepseek/my-deepseek",
"provider": {
"deepseek": {
"name": "DeepSeek Anthropic Compatible",
"base_url": "https://api.deepseek.com/anthropic",
"credential": "deepseek_api_key",
"wire_api": "anthropic_messages",
"models": {
"my-deepseek": {
"name": "DeepSeek V4 Flash",
"channel": "Custom",
"context_window": 200000,
"effective_context_window_percent": 95,
"max_tokens": 8192,
"temperature": 0.2,
"reasoning_capability": {"togglewithlevels": ["high", "max"]},
"reasoning_implementation": "request_parameter",
"input_modalities": ["text"]
}
}
}
}
}~/.devo/auth.json:
{
"version": 1,
"credentials": {
"deepseek_api_key": {
"kind": "api_key",
"value": "sk-deepseek-your-api-key"
}
}
}Rules:
provider.<id>.credentialis the reference; the secret value belongs only inauth.jsonunder the same id.- Keep
auth.jsonout of git. The tracked built-incrates/core/providers.jsoncontains no user credentials.
The canonical file is JSON. Its root object has these fields:
| Field | JSON type | Default | Meaning |
|---|---|---|---|
model |
string | first enabled model | Active model in provider/model form. |
small_model |
string | automatic same-provider model, then model |
Lower-cost model for lightweight background work, such as session titles. Invalid values use the same fallback. |
reasoning_effort |
string | model default | Global logical selection: default, off, on, or one of the effort values supported by the selected model. Legacy disabled/enabled normalize to off/on. |
provider |
object | {} |
Map of provider ids to provider records. The canonical key is singular provider; providers is accepted only as a read compatibility alias. |
All fields inside a provider record and model record are optional. A selected custom model can therefore start with only its map key; Devo supplies safe runtime defaults. The provider id and model id are map keys, not duplicated fields:
{
"model": "my-provider/my-model",
"provider": {
"my-provider": {
"models": {
"my-model": {}
}
}
}
}Provider records live at provider.<provider-id>:
The <provider-id> map key is the stable provider identity used in
provider/model; name is only its display label. Renaming a provider should
not change its id.
| Field | JSON type | Default | Meaning |
|---|---|---|---|
name |
string | provider id | Display name for the provider. |
base_url |
string | none | API endpoint base URL. Use the endpoint form required by the selected wire API. |
credential |
string | none | Credential id looked up in the user-scoped auth.json. The secret value is not stored in providers.json. |
headers |
object of string-to-string | none | Literal HTTP headers sent to the provider. Do not put API keys here. |
options |
JSON object | none | Provider-specific options. Object keys are forwarded to the built-in adapter request body unless a more specific model/variant value overrides them. |
request |
JSON object | none | Provider-level request-body defaults. Merged recursively before model and variant values. |
wire_api |
enum | openai_chat_completions |
Default request protocol for all models in this provider. |
enabled |
boolean | true |
Whether the provider and its models can be selected. |
env |
array of strings | [] |
Environment variable names that integrations may use for provider credentials. |
web_search |
object | none | Provider-level web-search capability configuration; see the modes below. |
web_fetch |
object | none | Provider-level URL-fetch capability configuration; see the modes below. |
models |
object | {} |
Map of provider-facing model ids to model records. |
Provider headers is a JSON object whose keys and values are both strings, for
example { "X-Organization": "my-team" }.
env records names for integrations; the normal provider resolver does not
automatically read those names as API keys.
Inside a provider record, web-search and URL-fetch configuration uses these fields:
{
"web_search": {
"mode": "provider"
},
"web_fetch": {
"mode": "local"
}
}web_search.mode is disabled, provider, or local; its optional
local_provider selects a named local search service and local_providers
defines those services. web_fetch.mode is disabled, provider, or local.
The default search mode is provider; the default fetch mode is local.
Models live at provider.<provider-id>.models.<model-id>. The nested map key
is the model id sent to the provider and is also the second half of the public
provider/model reference. There is intentionally no model_slug,
model_name, model_id, or model description field in the canonical format.
| Field | JSON type | Default | Meaning |
|---|---|---|---|
name |
string | model id | Human-readable label in the model picker. |
wire_api |
enum | provider wire_api |
Request protocol override for this model. |
context_window |
integer | runtime default | Maximum context window in tokens. |
effective_context_window_percent |
number | runtime default | Percentage of context_window treated as usable (may be fractional). |
max_tokens |
integer | runtime default | Default response-output limit. |
temperature |
number | none | Sampling randomness. |
top_p |
number | none | Nucleus-sampling probability mass. |
top_k |
number | none | Candidate-token cap. |
reasoning_capability |
enum or object | unsupported |
Reasoning choices shown to the user. |
reasoning_implementation |
enum or object | request_parameter when reasoning is supported |
How the selected reasoning choice changes the request. |
default_reasoning_effort |
enum | none | Initial effort for level-capable reasoning. |
base_instructions |
string | built-in/default instructions | Model-specific base instructions. An explicit empty string disables them. |
input_modalities |
array of enums | ["text"] |
Accepted input types: text and/or image. |
channel |
string | none | Optional grouping label in the model picker. |
truncation_policy |
object | {"mode":"bytes","limit":8000} |
Limit for oversized tool-result content. |
supports_image_detail_original |
boolean | false |
Whether original-resolution image detail is supported. |
enabled |
boolean | true |
Whether this model can be selected. |
priority |
integer | 0 |
Higher values are listed/preferred first when no explicit model is selected. |
Additional model metadata and request controls are available:
| Field | JSON type | Meaning |
|---|---|---|
family |
string | Model family used for grouping and future capability heuristics. |
release_date |
string | Provider catalog release date, normally ISO-8601 text. |
status |
string | Provider-reported availability label, for example active, deprecated, or preview. |
cost |
object | Open-ended pricing metadata; Devo preserves it without interpreting provider-specific keys. |
metadata |
object | Open-ended catalog metadata, including fields returned by dynamic discovery. |
options |
object | Arbitrary provider/SDK options. These are merged into request defaults for the built-in HTTP adapters. |
request |
object | Arbitrary request-body fields. These override the same model/provider option keys. |
headers |
object of string-to-string | Per-model HTTP headers layered over provider headers. API keys still belong in auth.json. |
variants |
object | Named variant map. For effort encodings, keys should be logical selections (off/on/levels). Each value may contain label, disabled, request_model, options, request, and headers. |
default_variant |
string | Variant key applied when a turn does not choose a variant explicitly and no effort-keyed variant matches. |
Variants use this shape:
{
"models": {
"reasoning-model": {
"family": "reasoning",
"variants": {
"fast": {
"label": "Fast",
"options": {"thinking": {"budget": 1024}},
"request": {"speed": "fast"},
"headers": {"X-Mode": "fast"}
}
},
"default_variant": "fast"
}
}
}The merge order is provider options → provider request → model
options → model request → variant options → variant request.
Headers use the same specificity order. disabled: true keeps a variant in
the directory for reproducibility but prevents it from being selected by a
future variant-aware client.
The effective context formula is
context_window * effective_context_window_percent / 100. That value is the
applied usable window for occupancy and auto-compact (default percent is
95 when unset).
Keep these two numbers distinct:
| Concept | Where you edit it | Example (DeepSeek V4 Flash) |
|---|---|---|
Model hard context_window |
Catalog / discovery (not edited as a separate knob) | 1000000 (1M) |
| Usable Context window | Desktop / TUI model settings → Context window | User enters 250000 → stored as percent 25 → applied 250000 |
Desktop / TUI model editors show an absolute token count for the usable window.
On save, the hard context_window is left unchanged and
effective_context_window_percent is set to
clamp(user_tokens × 100 / hard, 1..=100) (fractional values allowed). Clearing the field removes
the percent overlay so the default 95% applies again. For a custom model with
no hard window yet, entering a value sets context_window to that amount and
percent to 100.
A legacy compaction_token_limit in config.toml is still loaded for
compatibility but is ignored for occupancy and auto-compact. There is no
separate Auto-compact threshold UI.
wire_api may be set on a provider or model. A model value overrides its
provider value. These are the only supported values:
| Value | Request family | Use when |
|---|---|---|
openai_chat_completions |
OpenAI-compatible Chat Completions | The endpoint accepts chat-completions requests. |
openai_responses |
OpenAI-compatible Responses | The endpoint accepts Responses API requests. |
anthropic_messages |
Anthropic-compatible Messages | The endpoint accepts Anthropic Messages requests. |
If omitted, Devo uses openai_chat_completions. Choose the value from the
provider's API documentation; the URL alone does not determine the protocol.
The Native provider/discover method refreshes the model directory for an
existing Connection. It reads the credential referenced by credential from
the user-scoped auth.json, then tries the Connection base URL's /models
endpoint and compatible /v1/models forms. A successful OpenAI-style
{"data":[...]} or provider-style {"models":[...]} response is normalized
into the model map and persisted to the user providers.json overlay. Pass
{"forceRefresh":true} to bypass Devo's short in-process cache.
Discovery is additive: it updates the returned model records and preserves the
raw provider entry in metadata, while the git-tracked built-in directory is
never modified. Common id, name, family, status, release date, context
limit, output limit, cost, reasoning, and input-modality fields are normalized
when present. Providers without a model endpoint can use an explicit custom
models map instead.
reasoning_capability controls which choices the UI exposes. It has exactly
reasoning_capability uses three JSON forms:
| JSON value | Meaning |
|---|---|
"unsupported" |
Do not expose reasoning controls. |
"toggle" |
Expose off and on. |
{ "levels": ["off", "low", "high"] } |
Expose exactly the listed chips. Include off to allow disabling; omit off when reasoning cannot be turned off. |
Legacy {"toggle_with_levels":[...]} still reads and migrates to
levels with a leading off.
The allowed effort strings are none, minimal, low, medium, high,
xhigh, and max. The array should contain only values supported by the
provider model. default_reasoning_effort is one of the same effort strings;
it is not used with unsupported. default_reasoning_selection stores the
exact logical selection (off, on, or a level). Legacy literals
disabled/enabled are accepted on read and normalized to off/on.
Session and composer UIs always pick a logical selection from
reasoning_capability. How that selection is encoded on the wire is
per model on a Connection, so the same upstream model can differ across
deployments:
| Mode | When | Behavior |
|---|---|---|
| Adapter | No catalog variants key matches the selection |
Built-in adapters fill first-class thinking / reasoning_effort fields. |
| CatalogVariant | variants contains a key equal to the selection (off/on/levels; legacy disabled/enabled keys also match) |
First-class thinking/effort fields are cleared; that variant’s request / options / headers / optional request_model are merged into the outbound request. |
Name variant keys after logical selections. Example custom gateway that encodes effort only in JSON:
{
"reasoning_capability": {"levels": ["low", "medium", "high"]},
"default_reasoning_selection": "medium",
"variants": {
"low": {"request": {"ext": {"effort": "L"}}},
"medium": {"request": {"ext": {"effort": "M"}}},
"high": {"request": {"ext": {"effort": "H"}}}
}
}Example slug switch via request_model (replaces legacy
reasoning_implementation: model_variant):
{
"reasoning_capability": "toggle",
"variants": {
"off": {"request_model": "deepseek-chat"},
"on": {"request_model": "deepseek-reasoner"}
}
}reasoning_implementation is retained only for old TOML migration and is
projected into variants when the variants map is empty. New JSON
configuration should use reasoning_capability for the reasoning selector and
the named variants map for encodings. Desktop and TUI both author these
model fields; day-to-day pickers only show capability-derived chips.
Desktop SDK note: the chat composer’s synthetic variants list is the set of
logical effort option values (from availableEfforts), not the catalog
variants map. Catalog encodings stay on the model record.
truncation_policy uses this exact shape:
{
"truncation_policy": {
"mode": "tokens",
"limit": 12000
}
}mode is either bytes or tokens; limit is an integer.
User and workspace files overlay the git-tracked directory in order. Repeating
the same provider/model key partially overrides only the fields present in the
higher-priority file. Adding a new provider key creates a custom provider;
adding a new nested model key creates a custom model with safe defaults. The
new model is selected by its provider/model reference:
{
"model": "example/custom",
"provider": {
"example": {
"name": "Example Gateway",
"base_url": "https://api.example.com/v1",
"credential": "example_api_key",
"wire_api": "openai_chat_completions",
"models": {
"custom": {
"name": "Example Custom Model",
"context_window": 128000,
"input_modalities": ["text"]
}
}
}
}
}Omitting model metadata is valid. Omitted built-in fields remain unchanged when overriding a built-in entry; omitted custom-model fields use Devo's defaults. The old TOML scalar, provider, binding, and model-override fields remain readable only for migration.
Top-level keys in DEVO_HOME/config.toml also store a few UI preferences:
theme = "aurora"
collapse_reasoning = truethemeselects the TUI color theme (also set via Settings › Appearance).collapse_reasoningcontrols reasoning display (also set via/show-reasoning):true(default): while streaming, show only the latest 3 lines; when finished, keep short reasoning in full and collapse longer reasoning to a one-lineThought · …summary (full text remains available in Ctrl+T).false: show full reasoning while streaming and after it finishes.
- Legacy
compaction_token_limit, if present, is ignored. Set each model's usable Context window in Settings › Models instead.
The git-tracked crates/core/providers.json is Devo's built-in provider and
model directory. On startup, when Devo loads a user or workspace
config.toml, it automatically migrates legacy provider, model, binding, and
model-selection settings into the matching providers.json overlay before
resolving the active model:
- User settings move from
DEVO_HOME/config.tomltoDEVO_HOME/providers.json. - Workspace settings move from
<workspace>/.devo/config.tomlto<workspace>/.devo/providers.json. - Existing JSON values win over legacy TOML values, so a newer JSON configuration is never overwritten by an older one.
- Legacy API keys are copied to user-scoped
auth.jsonand referenced fromproviders.jsonby credential id. API key values are never written to the provider catalog. - Only provider-owned TOML keys are removed. Unrelated application settings
remain in
config.toml. A legacy[model.<name>]entry is kept when Devo cannot safely associate it with a provider model.
The migration is idempotent: after the first successful startup, subsequent
starts use the JSON catalog directly. New onboarding and provider/model writes
also use providers.json.
Devo connects to Model Context Protocol
servers configured in user or workspace config.toml under [mcp]. Each server
is one entry in the servers array, and its transport table selects how Devo
connects. Supported transports are stdio, streamable_http, and the deprecated
sse.
You can configure MCP either by editing config.toml or with the CLI
(devo mcp …). Prefer the CLI for day-to-day add / enable / disable / remove;
edit TOML when you need transport details, env vars, or headers.
Devo supports an optional semantic search MCP binary. It is not installed or
enabled by default. Use the installer with --with-code-search to install
the MCP binary and local model. The config entry is injected when missing and
stays disabled until you enable it:
[[mcp.servers]]
id = "code_search"
display_name = "Code Search"
enabled = false
startup_policy = "lazy"
[mcp.servers.transport]
kind = "stdio"
command = ["devo-code-search-mcp"]devo mcp enable code_search
# or, in an interactive session: /mcps → Code Search → EnableWhen enabled, the model-facing tool name is mcp__code_search__code_search.
If the devo-code-search-mcp binary is not installed, enabling the server will
fail until you install the optional code-search bundle.
Manage user-level MCP servers (~/.devo/config.toml) with devo mcp:
# List configured servers (effective / user config)
devo mcp list
# Add a stdio server (command + args after --)
devo mcp add time -- docker run -i --rm mcp/time
devo mcp add filesystem --env HOME=/tmp -- npx -y @modelcontextprotocol/server-filesystem .
# Add Streamable HTTP (`--transport http` writes kind = "streamable_http")
devo mcp add --transport http hello-mcp http://localhost:8080/mcp
devo mcp add --transport http github --bearer-token "$TOKEN" https://api.githubcopilot.com/mcp/
# Add legacy SSE
devo mcp add --transport sse legacy-mcp https://example.com/mcp/sse
# Enable / disable / remove by server id
devo mcp enable time
devo mcp disable time
devo mcp remove timeCLI devo mcp enable|disable writes user config.toml for offline use. An
already-running interactive session applies enable/disable live through the TUI
/mcps path (mcp/set_enabled RPC).
Verify configuration in the TUI with /mcps (interactive server list → detail →
tools). Clients can also call mcp/list, mcp/tools, and mcp/set_enabled.
Stdio example:
[mcp]
auto_start = true
[[mcp.servers]]
id = "filesystem"
display_name = "Filesystem"
enabled = true
startup_policy = "lazy" # eager | lazy | manual
trust_policy = "user" # user | workspace | untrusted
allowed_capabilities = ["tools", "resources", "prompts"]
roots_policy = "workspace" # none | workspace | custom
[mcp.servers.transport]
kind = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "."]
# cwd = "/path/to/workdir"
# env = { MY_VAR = "value" }
# env_vars = ["HOME", "PATH"]Streamable HTTP with a bearer token:
[[mcp.servers]]
id = "github"
display_name = "GitHub"
startup_policy = "lazy"
[mcp.servers.transport]
kind = "streamable_http"
url = "https://api.githubcopilot.com/mcp/"
auth = { kind = "bearer_token", token = "replace-me" }
http_headers = { "X-Custom" = "static-value" }
env_http_headers = { "Authorization" = "GITHUB_TOKEN" }Legacy SSE transport:
[mcp.servers.transport]
kind = "sse"
url = "https://example.com/mcp/sse"Field notes:
auto_startdefaults totrue. Enable/disable of MCP servers in a running session is applied live viamcp/set_enabled(TUI/mcps).startup_policycontrols when an enabled server starts:eagerduring bootstrap,lazyon first use, ormanualonly by explicit request.- For stdio,
envprovides literal values andenv_varslists names inherited from the local environment;{ name = "X", source = "remote" }is not supported for stdio. - For HTTP transports,
http_headersprovides literal headers andenv_http_headersmaps a header name to the environment variable that supplies its value. - Empty
allowed_capabilitiesmeans no restriction. The runtime currently focuses ontools; resource reads are not wired yet. output_limitssetsmax_tool_output_bytes(default 1 MiB) andmax_resource_bytes(default 10 MiB).- Top-level
mcp_oauth_credentials_storeisauto(default),file, orkeyringand selects where OAuth credentials are stored. - Prefer environment-injected headers or values over hard-coding tokens into
config.toml.auth_refexists on each server record but is not wired to the runtime yet.
Merge behavior: [mcp] is merged field-wise like other tables, but servers is
an array. A project-level [[mcp.servers]] list therefore replaces the
user-level list instead of merging by id.