dsh, is DeepSeek’s open agent runtime: a plugin tree where the
model adapter, the tools and the agent loop are all swappable, shipped with a
web interface, a headless mode and an ACP server. Read coding
harnesses first.
The name is a coincidence you should set aside. The harness is DeepSeek’s; the
provider you add to it here is this one, and which models you reach is decided
by the ids you pin rather than by whose harness it is.
The configuration surface is a form
Unlike every other harness on these pages, the first-class path is a settings screen rather than a file. In the web interface, open Settings then Models, and choose Add a custom provider. It asks for five things: a lowercase provider id, a display name, a base URL, an API protocol, and a credential. Then it asks for at least one model.Base URL
openai-completions as the protocol. The form offers three, and the
other two are openai-responses, which is the other surface this API serves and
Codex CLI documents, and anthropic-messages, which
this site publishes nothing about. A provider speaks one protocol, so a gateway
serving two needs two provider entries.
The provider id is permanent. Requests, saved sessions, model defaults and
credential references all use it, and renaming means adding a new provider and
deleting the old one. Pick the name you want to live with.
Model changes take effect on the next request. Nothing here needs a restart.
The file behind the form
The form writes$DSH_HOME/settings.yaml, and the section is llm-pi-ai. You
can edit it directly, and you have to for anything the form leaves out:
$DSH_HOME/settings.yaml
$DSH_HOME/.credentials.yaml and
settings keep only a reference to it. The page is write-only: after saving it
receives a redacted descriptor and never the secret again. apiKeyEnv is the
alternative and keeps the value in your environment instead. Either way the
credential is outside your repository.
Pin the model id, and expect to type it
Fetch available models asks your endpoint what it serves, using the base URL, protocol and key currently in the form, and opens a picker. This API answersGET /v1/models with every string your key can
call, so there is something true to read; whether dsh’s discovery reads this
particular listing shape is something we did not verify, and its own
documentation calls discovery a convenience rather than a guarantee. When it
returns nothing, type the ids and they work the same.
Whichever way they arrive, the ids are pinned ids, and unirouter/auto is the
one worth making the default.
One trap the project names itself: a built-in provider is always answered from
the installed catalog even when its base URL points somewhere else, so a model
list fetched through a built-in entry tells you nothing about what your endpoint
really serves. Add this API as a custom provider, not as a built-in one with the
URL changed.
Set the maximum output
The per-model form has a maximum output field, which makes dsh one of the harnesses where the shared page’s advice is actionable. A request that names a realistic ceiling holds less against your account spend rate cap and is charged exactly what it would have been anyway. See rate limits and spend controls.Images have to be declared
A model you type by hand is treated as text-only, because nothing can ask an endpoint which modalities it accepts. Attaching an image to such a model is refused before it is sent, naming the model. The form has no field for it, so it goes in the file:$DSH_HOME/settings.yaml
defaultInput on the provider does the same for every model under it. Both
state a claim about the endpoint rather than checking it, so declaring images
on a model this API routes to a text-only model is not caught here; the request
is refused further along. Check vision in the model’s capabilities block
first. Image inputs covers what this API accepts.
If the gateway refuses everything
Worth reading before you conclude something is broken, because the project documents this failure precisely. The adapter decides the shape of a request partly from the endpoint’s address, and an address it does not recognise is addressed as though it were OpenAI itself. Two consequences account for most of the resulting refusals: a model that declares reasoning has its system prompt sent with thedeveloper role,
and the output cap is sent as max_completion_tokens. A server that wants
system and max_tokens refuses both.
The switches are on the route, and a model’s own value wins field by field:
$DSH_HOME/settings.yaml
It shares its adapter with Pi
Thellm-pi-ai plugin is named after the library it wraps, which is the same
one Pi uses. The compatibility vocabulary, the protocol
names and the per-model fields are recognisably the same, so a setting you
worked out on one is usually the same setting on the other, under a slightly
different key. That is a convenience, not a guarantee: the two projects pin
their own versions and the plugin adds fields of its own.
What we did not verify
- That a dsh session against this API succeeds. Nothing was run.
- Whether model discovery reads this API’s listing shape.
- Whether a model id from the form or the file is sent as the
modelfield byte for byte. A mismatch would show as a400withmodel_id_not_pinnedor a404withmodel_not_found, neither of which costs anything. - Which
compatswitches this API needs, if any. - Whether any plugin in a default profile calls an embeddings endpoint. The project ships optional overlays including memory servers, which is the kind of feature that usually does, and we did not audit them.
- How stable any of this is. The project is a developer preview iterating quickly and says so.