Psi supports custom LLM providers through models.edn files.
This lets you add providers such as MiniMax, Ollama, LM Studio, vLLM, llama.cpp, or any other service that exposes an OpenAI-compatible or Anthropic-compatible API.
You can define custom providers in either or both of these files:
- user-global:
~/.psi/agent/models.edn - project-local:
<worktree>/.psi/models.edn
If the same custom provider/model pair appears in both places, the project-local entry wins.
Built-in models remain available alongside custom ones.
Each provider entry defines:
- a provider id, such as
"minimax"or"ollama" :base-url— the API root for that provider:api— which wire protocol psi should use- optional
:authsettings - one or more
:models
Supported custom-provider API protocols are:
:openai-completions:anthropic-messages:openai-codex-responses
In practice, most custom hosted providers fit the first two.
Custom model definitions may opt into structured-output requests with a model-level capability map:
{:capabilities
{:structured-output
{:supported? true
:strategies [:prompted-json]
:native-mechanism nil
:notes "Use adapter-owned JSON-only prompt fallback."}}}Omitting :capabilities :structured-output is valid and normalizes to unsupported. Psi will not inject prompted-JSON fallback instructions for omitted legacy/custom models; add :strategies [:prompted-json] when that behavior is wanted.
Native capability declarations should only be used when the configured transport is known to support the provider mechanism:
:openai-completionsmay use:native-mechanism :openai/chat-completions-json-schema-response-formatwhen the compatible API supports Chat Completionsresponse_formatJSON Schema.:anthropic-messagesmay use:native-mechanism :anthropic/json-schema-outputwhen the compatible API supports Anthropic Messagesoutput_formatJSON Schema plus thestructured-outputs-2025-11-13beta/header. This is the preferred Anthropic native mechanism for supported models.:anthropic-messagesmay use:native-mechanism :anthropic/forced-tool-usewhen the compatible API supports forced tool choice withinput_schema. This is a separate native tool-use mechanism, not the only Anthropic structured-output path.:openai-codex-responsesmay use:native-mechanism :openai/responses-text-format-json-schemafor the ChatGPT/Codex OAuth transport when the backend supports streaming Responses-styletext.formatJSON Schema. This mechanism is distinct from Chat Completionsresponse_format; non-streaming Codex structured output is not established by Psi's current contract.
Structured-output requests must supply an explicit :json-schema; Psi does not convert Malli/domain schemas in the AI adapter. Prompted JSON remains fallback only. Local workflow/runtime validation remains mandatory after provider generation, and OAuth/API tokens must not be written into docs, task files, fixtures, logs, or commits.
Illustrative example: confirm the provider's current base URL and model ids in
its own docs, then place a definition like this in ~/.psi/agent/models.edn or
.psi/models.edn:
{:version 1
:providers
{"minimax"
{:base-url "https://api.minimax.chat/v1"
:api :openai-completions
:auth {:api-key "env:MINIMAX_API_KEY"}
:models [{:id "MiniMax-M1"
:name "MiniMax M1"
:supports-reasoning true
:supports-text true
:context-window 128000
:max-tokens 16384
:latency-tier :medium
:cost-tier :medium}]}}}Then export your key:
export MINIMAX_API_KEY=...Notes:
- the provider id here is
minimax - psi will route requests through its OpenAI-compatible transport because
:apiis:openai-completions - you can define multiple models under the same provider
If a provider exposes an Anthropic Messages-compatible API, configure it the
same way but set :api to :anthropic-messages.
{:version 1
:providers
{"my-anthropic-proxy"
{:base-url "https://example.com/anthropic"
:api :anthropic-messages
:auth {:api-key "env:MY_PROXY_API_KEY"}
:models [{:id "proxy-sonnet"
:name "Proxy Sonnet"
:supports-reasoning true
:supports-text true
:context-window 200000
:max-tokens 8192}]}}}For Anthropic-compatible providers, psi uses the Anthropic transport and will send the configured key through the compatible auth path.
Custom providers do not define their own proxy fields. When a custom provider
uses psi's built-in OpenAI-compatible or Anthropic-compatible transport path, it
inherits the same environment-driven outbound proxy behavior documented in
doc/configuration.md.
The :auth map supports more than just an API key:
{:auth {:api-key "env:LOCAL_LLM_KEY"
:auth-header? false
:headers {"X-Client" "psi"}}}Use cases:
:api-key— literal key or"env:VAR_NAME":auth-header? false— omit the normal auth header for servers that reject it:headers— add custom request headers
A common use for :auth-header? false is an OpenAI-compatible local server that
accepts requests without a bearer token and rejects unexpected auth headers.
For local :openai-completions models, psi also projects the normal session
/thinking control onto a local-only compatibility extension when thinking is
set to off: the request body includes
{:chat_template_kwargs {:enable_thinking false}}. This helps local
OpenAI-compatible servers that expose hidden reasoning via a nonstandard
chat_template_kwargs.enable_thinking flag.
If psi is already running, reload the definitions after editing either models file:
/reload-models
That reloads:
~/.psi/agent/models.edn<worktree>/.psi/models.edn
After reloading, use the normal model-selection surface.
In-session:
/model minimax MiniMax-M1
or, for the Anthropic-compatible example:
/model my-anthropic-proxy proxy-sonnet
Once selected, the custom model behaves like any other model in psi.
You can define multiple providers in the same file, for example:
minimaxollamastaging-openaicompany-anthropic-proxy
This already satisfies the issue's requested workflow of configuring multiple providers in a config file and switching between them at runtime.
- If psi does not see a newly added provider, run
/reload-models. - If a models file is malformed, psi logs a warning and keeps built-in models available.
- If a custom provider uses the same
(provider, model-id)as a built-in model, the custom definition is skipped to avoid shadowing built-ins. - If a project-local and user-global definition use the same
(provider, model-id), the project-local definition wins.