Skip to content

Shared configuration

AnyInfer has one JSON configuration format for every integration method. Load it from Python, pass it to anyinfer run, or start the OpenAI-compatible sidecar with it. Provider identity, credentials, endpoint overrides, and the default route therefore do not drift between deployments.

{
  "format_version": 1,
  "providers": [
    {"id": "anthropic", "api_key": "env://ANTHROPIC_API_KEY"},
    {"id": "ollama"}
  ],
  "default_route": ["ollama:qwen3:8b", "anthropic:claude-sonnet-4-5"]
}
import anyinfer as ai

config = ai.load_config("anyinfer.json")
with ai.Client(config.providers, route=config.route) as client:
    result = client.generate("Explain consistent hashing.")
anyinfer run "Explain consistent hashing." --config anyinfer.json
anyinfer serve --config anyinfer.json
# or, from a standalone download:
anyinfer-serve --config anyinfer.json

The current format_version is 1. Omitting it is accepted for files written before the field was introduced. Unknown versions, unknown keys, duplicate instance ids, invalid types, and files larger than 1 MiB fail with ConfigError before a client is created. The one exception: an entry with enabled: false is skipped without validating its other keys, so a disabled entry can hold settings for a provider that is not installed.

Provider settings

ai.ProviderSettings.of(
    "openai",
    alias=None,                             # instance id; defaults to the provider id
    base_url="https://api.openai.com/v1",   # provider default when omitted
    api_key="env://OPENAI_API_KEY",         # literal, env://, or credential://
    api_version=None,                       # Azure and Anthropic
    headers={},                             # extra request headers
    options={},                             # adapter-specific settings
    timeout_s=120.0,                        # default per-request timeout
)

The order you list providers is the preference order for alias resolution.

Configuring one engine more than once

alias gives an entry its own identity, so the same engine can be configured several times — two Azure tenants, a local and a remote Ollama — each with its own endpoint and credentials. The alias is what a target string names:

client = ai.Client([
    ai.ProviderSettings.of("azure-foundry", alias="work",
                            base_url="https://work.openai.azure.com",
                            api_key="env://WORK_KEY"),
    ai.ProviderSettings.of("azure-foundry", alias="lab",
                            base_url="https://lab.openai.azure.com",
                            api_key="env://LAB_KEY"),
])

client.generate(messages, target="work:gpt-4o")   # the work tenant, not the lab one

Each alias becomes its own adapter with its own connection pool. Omitting alias is the ordinary single-instance case, where the provider id is the instance id. Two entries sharing an instance id is a ConfigError, as is an alias that would shadow a registered provider id.

Providers that take a choice of credential

api_key is the top-level slot for the usual credential. A provider that accepts more than one kind declares each as its own setup field and reads the extra ones from options. Anthropic is the case in point: a console API key and a claude.ai subscription token authenticate with different headers, so they are separate fields rather than two spellings of one.

# An Anthropic API key — sent as x-api-key.
ai.ProviderSettings.of("anthropic", api_key="env://ANTHROPIC_API_KEY")

# A claude.ai OAuth token — sent as a bearer token, with the beta flag the API requires.
# Obtain one with: ant auth print-credentials --access-token
ai.ProviderSettings.of("anthropic", options={"oauth_token": "env://ANTHROPIC_OAUTH_TOKEN"})

Supply one or the other; if both are set the OAuth token wins.

Option values for fields a provider declares as secret go through the same credential resolver as api_key, so they accept env:// and credential:// references and are registered for redaction. Bedrock's explicit AWS credentials work the same way:

ai.ProviderSettings.of("bedrock", options={
    "aws_access_key_id": "AKIA…",                        # an identifier, passed through
    "aws_secret_access_key": "env://AWS_SECRET_ACCESS_KEY",  # resolved and redacted
})

Fields a provider declares as anything other than secret are passed through verbatim — resolving them would corrupt any literal value that merely looked like a reference.

To discover what a provider accepts without hardcoding it, read its setup spec — the any_of groups are the ones where one of several fields will do:

setup = ai.default_registry.get("anthropic").setup
[(f.key, f.required) for f in setup.fields]  # declared fields
setup.any_of                                 # (('api_key', 'oauth_token'),)
setup.requirement_note                       # why, in one line

Which fields to actually ask for

Not every declared field is a question. A provider knows its own endpoint, its API version, and where AWS keeps its credentials; what it cannot know is your key or your account. The spec draws that line itself, so an application prompting for setup does not have to infer it from help text:

setup = ai.default_registry.get("openai").setup
[f.key for f in setup.essential_fields]  # ['api_key']  — ask for these
[f.key for f in setup.advanced_fields]   # ['base_url'] — offer these, folded away

An advanced field is never required and never part of an any_of group, so a form built from essential_fields alone can always be saved. Each one carries the value it falls back to in SetupField.default_value (https://api.openai.com/v1 here), which is what lets a collapsed field still say what it will do. Render that value rather than pre-filling the editor with it: a saved copy of today's default keeps overriding the real default long after it has moved on.

The two extremes are worth knowing. ollama, vllm, and the other local engines have no essential fields at all — there is nothing to fill in. azure-foundry, runpod, and anything else whose URL embeds an account or endpoint id keeps base_url essential, because no default could be right.

Client settings

ai.Client(
    providers,
    registry=None,              # defaults to the process-wide registry
    catalog=None,               # defaults to the bundled catalog
    route=None,                 # default route when a call names no target
    observers=[],               # telemetry sinks, registered payload-free
    resolver=None,              # credential resolver chain
    retain_raw=False,           # keep raw provider payloads on results
    repair=None,                # default repair budget
    use_default_catalog=True,   # False disables alias resolution entirely
    estimator=None,             # token counting; defaults to the byte heuristic
    context_gate=True,          # refuse requests that provably cannot fit pre-dispatch
    pricing_table=None,         # defaults to the bundled table; see fetch_pricing()
    capability_overrides=None,  # "provider:model"-keyed corrections, strongest layer
    model_dir=None,             # where acquired model weights are stored
)

retain_raw is off by default because raw payloads carry response text that payload-free telemetry deliberately omits.

Per-request options

client.generate(
    messages,
    target="medium",                # or route=
    schema=None,
    tools=(),
    tool_choice="auto",             # "auto" | "none" | "required" | a tool name
    sampling=ai.Sampling(...),
    reasoning=None,                 # "minimal" | "low" | "medium" | "high"
    timeout_s=None,                 # per attempt; defaults to 120
    repair=None,
    provider_options={},            # namespaced escape hatch
    metadata={},                    # opaque, echoed in telemetry
    max_response_bytes=1_048_576,
)

Sampling fields default to None, meaning provider default. AnyInfer never invents a temperature — an unset value is omitted from the wire request entirely.

Environment variables

Variable Effect
ANYINFER_MODEL_DIR Override where downloaded models are stored (also Client(model_dir=...)).
ANYINFER_RUNTIME_DIR Override where llama.cpp runtime variants are installed.
ANYINFER_HARDWARE_CACHE_BYPASS Skip the hardware cache entirely, read and write.
ANYINFER_HARDWARE_CACHE_REFRESH Ignore a cached profile, re-probe, and rewrite it.
ANYINFER_SERVE_TOKEN Bearer token for anyinfer serve.
COPILOT_CLI_PATH Override Copilot CLI discovery.

Credential references (env://NAME) read any variable you name; there are no magic credential variable names.

File format

{
  "format_version": 1,
  "providers": [
    {"id": "anthropic", "api_key": "env://ANTHROPIC_API_KEY"},
    {"id": "ollama"},
    {
      "id": "llama-cpp",
      "options": {"posture": "balanced", "binary": "/usr/local/bin/llama-server"}
    }
  ],
  "default_route": ["anthropic:claude-sonnet-4-5", "ollama:qwen3:8b"]
}

Each provider entry needs an id. Top-level provider settings are adapter, base_url, api_key, api_version, headers, timeout_s, and options. Setup fields declared by that provider may also be written directly, or grouped under a values object (the shape setup UIs write); unrecognized fields fail validation. Two more keys exist for compatibility: provider_id is the legacy spelling of adapter, and alias, when present, must simply restate the entry's id. Credential references such as env://ANTHROPIC_API_KEY are resolved only when the adapter is first used, so parsing a config never prints or expands a secret.

Set enabled to false to keep a provider entry in a file without loading it. The demo app uses that facility and writes its own UI fields alongside the shared fields; the SDK, CLI, and sidecar deliberately ignore those known demo-only fields (at the root: targets, system_prompt, theme, and context_window_tokens).

The adapter key

id is the instance id used in target strings. The optional adapter key names the engine behind it, which is what lets one engine be configured more than once:

{
  "providers": [
    {"id": "openai", "api_key": "env://OPENAI_API_KEY"},
    {
      "id": "work-azure",
      "adapter": "azure-foundry",
      "base_url": "https://wumbo.openai.azure.com",
      "api_key": "env://WUMBO_KEY"
    },
    {
      "id": "ollama-local",
      "adapter": "ollama",
      "base_url": "http://127.0.0.1:11434"
    }
  ],
  "default_route": ["openai:gpt-5", "work-azure:gpt-4o"]
}

Omitting adapter keeps the single-instance spelling exactly as before: the id is both the engine selector and the instance id. A duplicate id fails fast with a ConfigError.

The sidecar can advertise instance-scoped targets from /v1/models by writing them in instance terms:

anyinfer serve --config anyinfer.json --expose work-azure:gpt-4o

CLI

anyinfer serve --host 127.0.0.1 --port 8080 --config anyinfer.json
anyinfer serve --token SECRET --host 0.0.0.0 --allow-remote-exposure
anyinfer run "PROMPT" --config anyinfer.json   # one prompt, then exit
anyinfer doctor [--json]        # detected hardware, recommended tier
anyinfer providers [--json]     # every registered provider and what it needs

run reads the same config file as serve, so one file drives both. See run a prompt from the shell for its flags.

A non-loopback bind requires both --allow-remote-exposure and a token. The CLI refuses otherwise, because an unauthenticated LLM gateway on a network is a credential laundering service.

Cache and data locations

Purpose Windows macOS Linux
Hardware cache %LOCALAPPDATA%\anyinfer ~/Library/Caches/anyinfer $XDG_CACHE_HOME/anyinfer
Model artifacts %LOCALAPPDATA%\anyinfer\models ~/Library/Application Support/anyinfer/models $XDG_DATA_HOME/anyinfer/models

Override the model directory with options={"model_dir": Path(...)} on the llama-cpp provider.