Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

Configuration and operations1 of 9

The settings reference

About 8 minutes to read

On this page

Kotoba has exactly seventeen settings you can change while it is running. They are not a loose collection of environment variables — they are one table in the code, _RUNTIME_SPEC in api/src/kotoba/core/app_settings.py, and that table is simultaneously:

  • the list of things the Settings panel and the terminal's /set can change,
  • the allowlist for the write endpoint (anything not in it is refused with a 400),
  • the validator for both halves of a value (the saved one and the environment one),
  • and the source of every default below.

Everything else that looks like configuration is an environment variable read directly by one module. Those are in Environment variables, and none of them can be changed from the Settings panel or the terminal.

How a value is decided

Every call site reads through runtime_value(key, env_var, default), which answers in this order:

  1. the override saved in settings.yaml, if there is one and it validates;
  2. the environment variable, if it is set and it validates;
  3. the default.

Two consequences are worth stating plainly.

A saved override beats the environment. If api/.env says KOTOBA_SANDBOX=local — and the shipped example file does say exactly that, uncommented — and you then pick docker in Settings, docker wins, now and after every restart. Nothing warns you that the two disagree, because the disagreement is normal and the file is meant to lose.

An invalid value is discarded, not obeyed. Set KOTOBA_SANDBOX=rocket and the effective value is local, with one line in the log saying so. The validator runs on the environment half too, which is the whole point: an earlier version guarded only the file, so the panel drew local while the backend picked rocket, found no backend for it, and silently ran nothing.

The single exception is reasoning_effort, whose environment half is passed through raw so that its consumer can fold it down the ladder instead of treating it as unset (_CONSUMER_CLAMPS).

Changes apply on the next request, because the value is read at the moment it is used. There is no reload step and no restart.

The seventeen

Defaults are the ones the code produces on an install with no settings.yaml and no environment (measured, not transcribed).

SettingWhat it doesAllowed valuesDefault
providerWhich company answersopenai, xaiopenai
base_urlOverride the provider's endpointempty, or an http:// / https:// URLempty (the provider's own)
modelThe model of the companion turn, and the last fallback for every roleany non-empty textgpt-5.6-luna
work_modelModel for work (background) modeany text; empty inheritsempty
code_modelModel for the code roleany text; empty inheritsempty
research_modelModel for the research roleany text; empty inheritsempty
utility_modelModel for the two sidecars: emotion extraction and memory extractionany text; empty inheritsempty
reasoning_effortHow much the model thinks before it speaksoff, empty, minimal, low, medium, high, xhigh, maxlow
expressiveWhether [audio tags] are asked for and kepton / offon
elevenlabs_agent_idThe agent id used by voice_mode: agentany textempty
voice_modeVoice transportlocal, agentlocal
tts_engineWhich speech engine local voice usesexpressive, fastexpressive
sandboxWhere her shell and code runlocal, docker, nonelocal
work_timeoutWall-clock limit for one background task, in secondsa finite number ≥ 303600
work_max_iterModel round trips one background task may takean integer ≥ 140
work_max_tool_callsTool calls one background task may makean integer ≥ 140
work_fail_limitConsecutive tool failures before work gives upan integer ≥ 16

Notes that a table cannot carry:

  • base_url empty is not the empty string on the wire. It falls back to the provider's own default, which is https://api.x.ai/v1 for xAI and genuinely empty (the SDK's own endpoint) for OpenAI.

  • reasoning_effort is clamped by the provider, not refused. OpenAI offers low medium high xhigh max; xAI offers low medium high xhigh. A value on the ladder but above what the provider sells is folded down — max becomes xhigh on xAI — and minimal becomes low on OpenAI. A value that is not on the ladder at all falls to the provider's lowest, because guessing high would spend your money on a typo.

  • reasoning_effort is also ignored by non-reasoning models. The gate is the model name, not this setting: OpenAI o1/o3/o4/gpt-5* and xAI grok-4*/grok-build* take reasoning arguments; anything else gets none, whatever this says.

  • Work mode does not use reasoning_effort. It reads work_reasoning_effort (default medium), from KOTOBA_WORK_REASONING_EFFORT or a hand-written runtime.work_reasoning_effort: in settings.yaml — which wins. Neither /set nor the panel offers it.

  • work_timeout's default is 3600 seconds, except for a job launched from an ElevenLabs call, where the built-in default drops to 1500. An explicit value you set is used in both cases.

  • expressive on is not enough for tags to be performed. The one authority is audio_tags_enabled(), and it is measurable:

    expressivevoice_modetts_enginetags
    onlocalexpressiveyes
    onlocalfastno — the fast engine deletes them
    onagenteitheryes — the performer is your ElevenLabs agent
    offanythinganythingno

Where you change each one

Three surfaces write the same file.

The browser — Settings:

SectionSettings in it
Personalityvoice_mode, tts_engine, elevenlabs_agent_id (plus her name, language, voice and avatar, which are not runtime settings)
Brainprovider, base_url, model, work_model, code_model, research_model, utility_model, reasoning_effort, expressive
Securitysandbox
Advanced (work)work_timeout, work_max_iter, work_max_tool_calls, work_fail_limit

The terminal — /settings lists them and /set <key> <value> writes one. Its sections are grouped differently: BRAIN, VOICE (which is where expressive lives here), WORK, SECURITY.

The API — POST /api/settings/runtime with {"key": …, "value": …}. It takes any of the seventeen and refuses everything else.

The two that are gated

sandbox and provider change the trust model — one moves where her commands run, the other sends everything you say to a different company under a different privacy policy — so the terminal names them GATED_KEYS and asks you to confirm, with the consequence spelled out, before writing either.

Be exact about what that gate is and is not:

  • It exists in the terminal only. The browser's Sandbox control writes immediately; it shows a warning line when the value is none, and no confirmation dialog for any value.
  • It is not an authorisation check. POST /api/settings/runtime accepts sandbox and provider with no confirmation at all. Whoever can reach /api/* can change them — which is the real reason KOTOBA_WEB_PASSWORD matters on anything but a private machine.
  • The browser panel does not send provider or model to the runtime endpoint. It uses /api/setup/provider and /api/setup/model, which pin the pair together, because choosing xAI while the model still reads gpt-5.6-luna produces a 404 on the next turn and a perfectly good key reported as bad.

Two things the panel shows but cannot change

trust and browser_cdp are read straight from the environment (KOTOBA_TRUST, default workspace; KOTOBA_BROWSER_CDP, default empty) and are display-only. They are deliberately absent from the settable table: changing browser_cdp under an already-launched browser would leave the two out of step, and trust has no enforcement behind it yet — nothing but the panel and /settings reads it. The terminal refuses them by name: "it comes from the environment."

The file it writes

~/.kotoba/settings.yaml, or wherever KOTOBA_SETTINGS points. It holds two keys and nothing else:

yaml
disabled_toolsets:
- browser
runtime:
  sandbox: docker
  work_timeout: 45.0

runtime is the overrides above. disabled_toolsets is the tool families switched off in Settings → Capabilities; the current family names are browser, code, core, cron, discord, file, mcp, memory, report, skills, subagent, terminal, web, plus plugin:<name> for each community plugin. That list is read before anything is imported, which is the only moment a disabled plugin's module body can still be stopped from running.

Every write takes a cross-process lock and is published atomically, so two Kotoba processes — the terminal beside the server — cannot lose each other's changes.

If the file will not parse

Measured on a settings.yaml with a stray tab in it:

  • Every read logs … cannot be parsed (…) — using defaults and refusing to overwrite it and then falls back. sandbox: docker in the file reads as local, and toolsets you had switched off come back on.
  • Every write raises SettingsUnreadable rather than clobbering the file, and the file is left byte for byte as it was. POST /api/settings/runtime catches only ValueError, so from the panel this arrives as a 500 rather than a message.

So a broken file does not lose your settings, but it does mean Kotoba runs on defaults until you fix it. The log line is the only warning, and it is easy to miss under kotoba serve, where it arrives as an [api] line among the startup noise. Fix the YAML, or move the file aside and set what you need again.