A provider is a .py file in providers/ (repo) or ~/.config/leancoder/providers/
with a module-level PROVIDER dict, toggled with /provider enable|disable. The
ProviderManager registers it; the file does not call register_provider. Bundled
providers: providers/README.md. Template:
examples/providers/example.py.
PROVIDER = {
"name": "cloud",
"description": "my hosted backend", # /provider menu
"make_client": lambda cfg: ApiClient(cfg), # required
"list_models": lambda: [...ids...], # required; safe unauthed, never throws
"context_window": lambda model: 200000, # required
# optional, absent-safe:
"capabilities": lambda model: {"thinking": ["off", "max"], "effort": ["low", "high"]},
"available": lambda: True, # offered only when True (default True)
"autostart": lambda: False, # activate on launch when True
"on_activate": lambda agent, cfg: ...,
"on_deactivate": lambda agent, cfg: ...,
"usage": lambda agent, cfg: {...}, # quota meters (below)
"status_line": lambda agent, cfg: "raw", # used only if usage is absent
"login": lambda agent, cfg: True, # enables /provider login; truthy on success
"clear": lambda agent, cfg: None, # enables /provider clear
"detail": lambda agent, cfg: "text", # /usage full view
"tag": "cloud", # /model row label (default: name)
"warm_models": lambda: ["m1"], # loaded models (green dot)
"model_status": lambda model: None, # hint in /model, e.g. "pull"
"endpoints": lambda: ["http://host:11434"],
"restore_settings": lambda agent, cfg: ..., # re-sync module state after /load
"worker_inherit": ("account",), # settings keys workers inherit
"location": lambda cfg: "box (http://...)", # status label
"prepare": lambda agent, cfg: None, # runs before activation
"on_conn_error": lambda agent, cfg, err: False, # True = fixed, retry the turn
"system_addendum": lambda agent, cfg: "", # extra system-prompt text, deterministic per model
"login_files": "auth*.json", # listed by /push and /pull
}
def setup(lc, cfg): # optional, helpers only
_lc.update(lc)
lc["register_command"]("/cloud_push", _push)restore_settings must not write config.toml; settings are session-scoped.
A read-only mapping; unknown names raise KeyError. Bundled providers use only these
(a test enforces it).
| Group | Names |
|---|---|
| Display | green red yellow dim bold blue glyph GLYPH Spinner THINK_FRAMES MarkdownStream |
| Streaming | consume_openai_stream upstream_error_of stream_tiered open_tiered StreamStall parse_text_tool_calls messages_tokens encode_image_block |
| Config + limits | CONFIG_DIR save_config autosave_config sampling_for hosted_sampling RateLedger log_system_activity |
| Commands + prompts | register_command register_provider pick_one pick_model_menu model_available handle_model_command handle_sync_command ask menu_index visible_wait TurnAbort TTY |
| Hosts | resolve_host host_label valid_host norm_host |
| Live state | active_composer() |
- Implement
login/clear. Read secrets withgetpass, storechmod 600underlc["CONFIG_DIR"], never inconfig.toml. Resolve an env var first, then the keyfile. - On a missing or rejected key, raise from
chat()aRuntimeErrorwhose text containsno API key,API key,401,403orunauthorized. Core then offers login and retries the turn once.
if not key:
raise RuntimeError("no API key - use /provider login")Returned by make_client(cfg):
chat(messages, tools, should_abort) -> (assistant_dict, prompt_eval|None, aborted)(required). Pollshould_abort()while streaming.uncapped_ctx = True- opt out of the 32k local OOM cap (hosted clients).list_models()/running_models()- optional.- After each call, optionally set
last_out_tokens,last_cache_read,last_cache_write. A caching backend must set the cache counts: context fill isprompt_eval + last_cache_read + last_cache_write.
Read cfg.active_model() and cfg.setting(key) at send time. Do not mutate cfg.
For OpenAI-style SSE (data: {...} ... data: [DONE]):
return lc["consume_openai_stream"](client, resp, should_abort, tools,
on_line=None, on_obj=None, on_done=None,
text_fallback=False, split_think=False)Sets client.last_finish, last_truncated, last_out_tokens; reasoning is shown, not stored.
text_fallback- parse text-encoded tool calls (local servers only).split_think- move inline<think>tags to the reasoning line (local servers only).on_line(line, obj, st)- each line; returnTrueto stop, raise to fail. Use withlc["upstream_error_of"](obj).on_obj(obj, st)- each parsed chunk, after core.on_done(st)- adjustst["prompt_total"],st["out_tokens"],st["finish"].
st keys: finish, content_parts, prompt_total, out_tokens, aborted,
printed, printed_think, chunks, tool_acc.
capabilities(model) returns {key: [choices]}. thinking and effort map to
/think and /effort; other keys appear under /provider set. Undeclared keys can be
set free-form with /provider set <key> <value>. Read with cfg.setting(key).
provider- active backend (default"ollama").[providers.<name>]- per-providermodel,thinking,effort.- Ollama transport keys (
host,hosts,[machines],num_ctx) stay top-level. num_ctxcomes fromcontext_window(model); providers never write it.
def usage(agent, cfg):
return {"meters": [{"label": "5h", "pct": 12, "resets_at": "2026-06-27T20:00:00Z"}],
"note": ""}Core renders colours and reset times. None / {} keeps the plain context meter.
Session token totals are tracked by core.
Call lc["sampling_for"](cfg, model, server_card, "<source>") and send only the
returned send dict (server_card = the server's defaults, or None). Store
{"params": effective, "source": source, "sent": send} on client.last_sampling.
Hosted providers send no sampling parameters.