rath.config#

Persistent local configuration for LLM providers, embedding/VLM provider selection, MCP servers, memory stores, and backend presets. v1.3.0 separates routing config from credentials and centralizes environment-variable declarations.

Source#

Module

Source

rath.config.paths

src/rath/config/paths.py

rath.config.schema

src/rath/config/schema.py

rath.config.secrets

src/rath/config/secrets.py

rath.config.credentials

src/rath/config/credentials.py

rath.config.env

src/rath/config/env.py

rath.config.store

src/rath/config/store.py

Public contract#

OpenRath resolves config in this order:

OpenRath configuration resolution stack

Config resolution starts with explicit Provider fields, then environment variables, and finally the resolved .openrath/config.json store.#

Location

When used

$OPENRATH_HOME/config.json

Explicit override.

./.openrath/config.json

Project-local marker directory exists.

~/.openrath/config.json

Default user config.

The routing file is JSON. Unknown fields round-trip through the Pydantic models so newer OpenRath or third-party tools can add sections without losing data. API keys live in a sibling credentials.json after save.

config.json:

{
  "version": 1,
  "llm": {
    "default_provider": "openai-main",
    "providers": {
      "openai-main": {
        "provider_kind": "openai",
        "model": "gpt-5.5",
        "base_url": "https://api.openai.com/v1"
      },
      "claude": {
        "provider_kind": "anthropic",
        "model": "claude-sonnet-4-5"
      },
      "gemini": {
        "provider_kind": "litellm",
        "model": "gemini/gemini-2.0-flash"
      }
    }
  },
  "mcp": {
    "default_enabled": ["filesystem"],
    "servers": {
      "filesystem": {
        "command": ["python", "-m", "mcp_server_filesystem"],
        "env": {}
      }
    }
  },
  "memory": {
    "default_provider": "local-main",
    "providers": {
      "local-main": {
        "backend_kind": "local",
        "path": ".openrath/memory",
        "embedding_provider": "openai-main",
        "chat_provider": "openai-main"
      }
    }
  },
  "backend": {
    "default_provider": "sandbox-main",
    "providers": {
      "sandbox-main": {
        "backend_kind": "opensandbox",
        "domain": "127.0.0.1:8080",
        "options": {}
      }
    }
  }
}

credentials.json (mode 0600 on POSIX):

{
  "version": 1,
  "llm": {
    "providers": {
      "openai-main": "sk-...",
      "claude": "sk-ant-..."
    }
  },
  "backend": {
    "providers": {
      "sandbox-main": "sandbox-key"
    }
  }
}

Legacy inline api_key values still load and take precedence over the sidecar. The next save() migrates them. When another non-empty credential causes the sidecar to be rewritten, keys set to None are omitted. If the resulting credentials payload is empty, v1.3.0 does not remove or rewrite an existing sidecar; remove that file or entry explicitly when revoking the last key.

OpenSandbox config boundary

In v1.3.0, backend domain participates in availability/config resolution but is not injected into the SDK connection created by the backend, and backend api_key has no runtime consumer. Configure actual connections with OPEN_SANDBOX_DOMAIN and OPEN_SANDBOX_API_KEY (or the SDK’s ~/.sandbox.toml).

Store helpers#

API

Behavior

ConfigStore.load()

Loads the resolved default path or seeds an empty config.

store.save()

Splits secrets, atomically replaces each written file independently, sets user-only permissions on POSIX, writes .gitignore guards, and refreshes the root manifest. This is not a two-file transaction.

store.get_llm_provider(name)

Returns a named provider, or llm.default_provider when name=None.

store.find_provider_by_kind(kind)

Finds the default matching provider, then the first matching provider.

store.get_memory_provider(name)

Returns a named local memory provider, or memory.default_provider when name=None.

store.get_backend_provider(name)

Returns a named backend provider, or backend.default_provider when name=None.

store.get_mcp_server(name)

Returns one MCP server entry.

store.enabled_mcp_servers()

Resolves every name in mcp.default_enabled.

Consumers#

Consumer

Config behavior

Provider.from_config(name=None, **overrides)

Builds a Provider from llm.providers; explicit overrides win.

EmbeddingProvider.from_config(name=None, **overrides)

Uses llm.embedding_provider first, then chat default credentials with a safe embedding model.

VLMProvider.from_config(name=None, **overrides)

Uses llm.vlm_provider or an explicit name; no chat fallback is assumed.

MemoryStoreSpec.from_config(name=None, **overrides)

Builds local memory store options from memory.providers.

RathOpenAIChatClient

Falls back to the first provider_kind="openai" config entry after Provider kwargs and environment variables.

RathAnthropicChatClient

Falls back to the first provider_kind="anthropic" config entry after Provider kwargs and environment variables.

RathLiteLLMChatClient

Does not scan OpenRath config directly; use Provider.from_config(...) before client dispatch. Requires openrath[litellm].

mcp_tools_from_config(name=None)

Builds MCP tool wrappers from one configured stdio server.

Environment registry#

rath.config.env is the central registry for provider and OpenSandbox environment variables. (OPENRATH_HOME remains path configuration.) It preserves the existing precedence: explicit field → environment → config.

API

Behavior

get_env_spec(name)

Returns one declared EnvSpec or raises on a typo.

env_value(name)

Returns a stripped value/default or None.

env_flag(name)

Interprets 1, true, yes, or on.

resolve_env(name, *explicit)

Returns the first explicit non-empty value, then the environment tier.

all_env_specs()

Returns sorted declarations.

env_reference_rows() / env_reference_markdown()

Produces stable documentation data without secret values.

Autodoc#

rath.config.resolve_config_dir() Path[source]#

Return the directory that holds config.json.

Raises FileNotFoundError only when OPENRATH_HOME is set but points at a non-directory path that already exists (e.g. a regular file). A missing target is fine — the caller will create it on first save.

rath.config.resolve_config_path() Path[source]#

Return the full path to config.json under the resolved config dir.

rath.config.is_project_local(config_dir: Path) bool[source]#

Return whether config_dir is the project-local ./.openrath/.

Used by rath.config.secrets to decide whether to also append .openrath/ to the surrounding project’s .gitignore on save.

class rath.config.RathConfig(*, version: int = 1, llm: ~rath.config.schema.LLMConfig = <factory>, mcp: ~rath.config.schema.MCPConfig = <factory>, memory: ~rath.config.schema.MemoryConfig = <factory>, backend: ~rath.config.schema.BackendConfig = <factory>, **extra_data: ~typing.Any)[source]#

Top-level on-disk schema.

Sections currently in use: llm, mcp, memory, and backend. Unknown/future sections are preserved on round-trip because extra="allow".

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.LLMConfig(*, default_provider: str | None = None, embedding_provider: str | None = None, vlm_provider: str | None = None, providers: dict[str, ~rath.config.schema.LLMProviderConfig] = <factory>, **extra_data: ~typing.Any)[source]#

The llm section: named providers + which one is the default.

default_provider is the chat fallback. embedding_provider and vlm_provider are independent overrides used by rath.llm.embedding.EmbeddingProvider and rath.llm.vlm.VLMProvider; when unset, those clients fall back to default_provider’s api_key/base_url with a sensible default model.

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.LLMProviderConfig(*, provider_kind: Literal['openai', 'anthropic', 'litellm'] = 'openai', model: str | None = None, api_key: str | None = None, base_url: str | None = None, temperature: float | None = None, max_tokens: int | None = None, **extra_data: Any)[source]#

One named entry under llm.providers.

Mirrors the most common Provider fields. Less-common knobs (frequency_penalty, logit_bias, …) stay on explicit Provider(...) kwargs — adding fields here later is non-breaking thanks to extra="allow".

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.MCPConfig(*, default_enabled: list[str] = <factory>, servers: dict[str, ~rath.config.schema.MCPServerConfig] = <factory>, **extra_data: ~typing.Any)[source]#

The mcp section: named server defs + which are enabled by default.

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.MCPServerConfig(*, command: list[str], env: dict[str, str] = <factory>, **extra_data: ~typing.Any)[source]#

One named entry under mcp.servers.

command is the full argv list passed to the stdio MCP server (the OpenRath adapter never shells out, so no string-form). env is merged into the subprocess environment by the adapter.

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.MemoryConfig(*, default_provider: str | None = None, providers: dict[str, ~rath.config.schema.MemoryProviderConfig] = <factory>, **extra_data: ~typing.Any)[source]#

The memory section: named local store presets.

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.MemoryProviderConfig(*, backend_kind: Literal['local'] = 'local', path: str | None = None, embedding_provider: str | None = None, chat_provider: str | None = None, **extra_data: Any)[source]#

One named entry under memory.providers (local backend only).

embedding_provider and chat_provider name entries under llm.providers used by LocalMemoryBackend for vector search and commit-time memo extraction respectively. OpenViking connection settings stay on MemoryStoreSpec.options or environment variables — they are not modeled here.

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.schema.BackendConfig(*, default_provider: str | None = None, providers: dict[str, ~rath.config.schema.BackendProviderConfig] = <factory>, **extra_data: ~typing.Any)[source]#

The backend section: named sandbox-backend presets + the default.

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.schema.BackendProviderConfig(*, backend_kind: ~typing.Literal['local', 'opensandbox'] = 'opensandbox', domain: str | None = None, api_key: str | None = None, options: dict[str, object] = <factory>, **extra_data: ~typing.Any)[source]#

One named entry under backend.providers.

Gives sandbox backends a config home parallel to llm/memory instead of relying on environment variables and ~/.sandbox.toml alone. domain and api_key route the opensandbox backend; api_key is a secret and is externalized to credentials.json on save (see rath.config.credentials). Backend-specific knobs (image, timeout, …) stay on options and round-trip via extra="allow".

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class rath.config.env.EnvSpec(name: str, kind: ~rath.config.env.EnvKind, consumers: str, default: str | None = None, aliases: tuple[str, ...] = <factory>)[source]#

Declaration of a single environment variable.

rath.config.env.get_env_spec(name: str) EnvSpec[source]#

Return the declared EnvSpec, or raise KeyError.

rath.config.env.env_value(name: str, *, environ: dict[str, str] | None = None) str | None[source]#

Return the stripped value of name, or its default, or None.

Raises KeyError if name is not declared (typo guard). Whitespace-only values are treated as unset.

rath.config.env.env_flag(name: str, *, environ: dict[str, str] | None = None) bool[source]#

Interpret name as a boolean flag (1/true/yes/on → True).

rath.config.env.resolve_env(name: str, *explicit: str | None) str[source]#

First non-empty among explicit candidates, then the env var.

Mirrors rath.llm.credentials.resolve_credential(), preserving the documented explicit > env precedence. Returns "" when nothing qualifies; callers decide whether that is an error. (Config-file fallback stays in the caller — the registry only owns the env tier.)

rath.config.env.env_reference_markdown() str[source]#

Render the env reference as a stable markdown table (feeds the docs).

Secrets never print a default value (they have none), so the Default column stays blank for them — no secret material can leak into docs.

class rath.config.ConfigStore(path: Path | None = None)[source]#

Round-trip the config file at path.

The constructor immediately reads the file (or seeds defaults when it does not exist), so callers do not need to guard against FileNotFoundError separately. Subsequent reads should mutate config directly; call save() to persist.

exception rath.config.ConfigError[source]#

Raised on schema-validation failure or corrupt JSON.

The string carries a human-readable summary; the original json.JSONDecodeError or pydantic.ValidationError is available via __cause__.

← API Reference