Providers
A provider type is an AI CLI: claude, codex, gemini (provider.go:36-40). A provider instance is a configured copy of one. Multiple instances per type are supported — same claude binary, different env vars (e.g. two PATs).
Source
Code: internal/agents/provider/. UI handler: internal/tools/agents/providers.go.
Why multi-instance
The use case is mundane: you have a personal Anthropic PAT and a work one. Both target the same claude binary. You want to pick which to use per session.
Each instance carries:
| Field | Notes | Source |
|---|---|---|
Type | claude / codex / gemini. | provider.go:34 |
Name | Unique within type. Defaults to Type itself. Pick anything: work, personal, staging. | provider.go:53 |
Binary | Absolute path. Empty = let wick resolve via PATH + scan. | provider.go:54 |
ExtraArgs | Appended to every spawn argv. | provider.go:55 |
Env | Extra env vars. This is where ANTHROPIC_API_KEY goes for a per-instance PAT. | provider.go:56 |
Disabled | Toggle without deleting. | provider.go:57 |
UseAIRouter | Route this instance's CLI through an embedded AI router instead of the provider's own API. claude/codex only. See AI Router provider integration. | provider.go |
AIRouterProvider | Which registered router to route through (9router, omniroute, …). Empty = default (9router). | provider.go |
AIRouterModels | Per-slot model IDs (e.g. opus → cc/claude-opus-4-6). Slots defined by the selected router. All optional. | airouter.go |
AIRouterAPIKey | Custom router API key, stored encrypted. Empty = the router's default credential. | airouter.go |
The default seed: when the instance list is empty, Load auto-creates one default per type whose Name equals the type. So a fresh install always shows three cards (claude/claude, codex/codex, gemini/gemini).
Instance names and the type/name key
Every provider instance is identified by a type/name key — the type plus a slash plus the instance name. Examples: claude/claude (the default), claude/work, codex/fast. This key is what project defaults, the composer's provider chip, and agents.json store.
Name rules: letters, digits, and _ only. Spaces in the create/rename form auto-convert to _; any other character outside [A-Za-z0-9_] is rejected with an inline error. The name must be unique within its type.
Bare type strings (claude, codex, gemini) stored in older project defaults are promoted to the canonical default instance key (claude/claude) automatically at runtime — no migration needed.
Renaming an instance
On the provider detail page, click the pencil icon next to the instance title to rename it inline:
- Type the new name — spaces auto-convert to
_, invalid characters are flagged immediately. - Confirm: wick calls
POST /providers/rename/{type}/{old-name}with the new name. - Project defaults auto-migrate: every project whose
Defaults.Providermatched the oldtype/namekey is rewritten to the new key and saved. The response includes aprojects_migratedcount; the UI reports how many projects were updated. - Live sessions are not migrated: a session's
agents.jsonentry keeps the old provider key. The agent continues running with the old instance until it is killed and the user re-selects the provider manually when creating a new session or via the session settings. This is intentional — wick does not know which sessions are mid-conversation vs. idle.
Env / args catalog picker
The provider detail page's Env and Extra Args fields include a Browse catalog button that opens a searchable modal of known env vars and CLI flags for that provider type. Each entry shows the variable name, a description, and its default value.
How it works:
- Selecting one or more entries and clicking Add selected inserts them as rows in the KV editor below — no need to remember exact variable names.
- For the Env field, value cells for known variables render as a dropdown of accepted options (e.g.
1/0for bool flags, enum choices for model-selection vars) instead of a plain text input. Variables not in the catalog still get a free-text input. - Already-added variables appear checked and disabled in the modal (prevents duplicates).
- The catalog is fetched once per page load from
GET /providers/catalog/{type}and cached for the session. Unknown provider types return an empty catalog — the manual KV editor still works.
Catalog coverage per type:
The exact lists live in each provider subpackage's catalog.go (internal/agents/provider/{claude,codex,gemini}/catalog.go), sourced from each CLI's official docs. A summary:
| Provider | Env vars | CLI args |
|---|---|---|
claude | ANTHROPIC_API_KEY, ANTHROPIC_MODEL, MAX_THINKING_TOKENS, CLAUDE_CODE_EFFORT_LEVEL, CLAUDE_CONFIG_DIR, DISABLE_AUTOUPDATER + the rest of the CLAUDE_CODE_* feature toggles, telemetry, and rendering vars | --model, --permission-mode |
codex | CODEX_HOME, CODEX_API_KEY, CODEX_ACCESS_TOKEN, CODEX_NON_INTERACTIVE, RUST_LOG, TLS cert vars | --model, --sandbox, --ask-for-approval, --add-dir, --profile, --search, --oss, plus -c key=value config overrides (model_reasoning_effort, sandbox_mode, approval_policy, web_search, …) |
gemini | GEMINI_API_KEY, GOOGLE_API_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_GENAI_USE_VERTEXAI, GEMINI_MODEL, GEMINI_SANDBOX, trust + telemetry vars | --model, --approval-mode, --yolo |
Web UI
📸 Screenshot needed:
agents-providers-list.png— capture/tools/agents/providersshowing the three default cards (claude / codex / gemini) with version + path resolved, plus the "Add Instance" + "Rescan all" + "Auto-rescan" header. Save todocs/public/screenshots/agents-providers-list.png.
📸 Screenshot needed:
agents-provider-edit.png— open Edit on one provider card, capture the form with Binary, ExtraArgs, Env (showingANTHROPIC_API_KEY=...placeholder), Disabled toggle. Save todocs/public/screenshots/agents-provider-edit.png.
What each card shows (Status struct):
- Path resolved — where wick found the binary. Source label:
registry/path/scan/miss. - Version — first line of
<bin> --version. - Last probed — when the cache was last filled.
- Edit / Rescan / Delete buttons per card.
- Add Instance for a new named profile of the same type.
Active Processes panel
When at least one agent is running, an Active Processes table appears above the provider cards showing every live spawn: session ID (first 8 chars), agent name, PID, and lifecycle/substate badge. The count badge reads N / PoolMax. The panel is hidden when the pool is empty.
Hook actions per provider card
Each provider card's Command Gate row now includes inline Enable / Disable / Test buttons (visible only when the master gate is enabled and bypass is not locked). The badge reflects four states:
| Badge | Meaning |
|---|---|
enabled ✓ | Hook is on and the last probe verified it. |
enabled (unverified) | Hook is toggled on but no successful probe yet. |
ready | Hook is off but a prior probe succeeded — can be re-enabled quickly. |
disabled | Hook is off and has never been verified. |
Test fires a live probe for the PreToolUse hook and refreshes the card. Results are visible immediately without a page reload.
Recent Spawns list
The page also surfaces a Gate Status card and a Recent Spawns table, grouped per session — one row per session (provider, spawn count, latest status, last-started time, first message) instead of one row per spawn. Search and pagination (10/page) run server-side.
Clicking a session row opens its session detail page: every spawn of that session, newest first, each expandable inline to the full spawn detail (crash cause, injected env, repro command, log links) without leaving the page.
Backing endpoints:
GET /api/providers/sessions— grouped, paginated session summaries (scope with?type=&name=, search with?q=).GET /api/providers/sessions/{id}— every spawn of one session.GET /api/providers/spawns— the flat, searchable, paginated spawn list backing both the provider list and provider detail pages.
Binary resolution chain
Both the UI probe and the spawn site walk the same chain. First hit wins:
| Step | What it checks | Source |
|---|---|---|
| 1. registry | Instance.Binary set in the UI form. Used as-is, no PATH lookup. | provider.go:62 (Bin()) |
| 2. path | exec.LookPath(<type>) against %PATH% + PATHEXT (Windows). | |
| 3. scan | Known install locations the installer drops but doesn't always wire into PATH. | scan_unix.go, scan_windows.go |
| 4. miss | All three failed. Probe reports PathFound=false; spawn falls back to bare type name and fails at Start(). |
Why scan exists
Tray-launched wick inherits PATH from Explorer / login session, not from your shell. So installer-modified PATH (npm prefix, claude installer) is often invisible to the tray even though where claude works in your terminal. The scan step closes that gap without making you edit Binary manually.
Windows scan (scan_windows.go): npm root list (%APPDATA%\npm, C:\nvm4w\nodejs, nvm-windows, fnm, volta, Program Files\nodejs) cross-product with .cmd / .exe extensions. Plus per-type installer paths — Claude: ~/.local/bin, LOCALAPPDATA\Programs\claude, Program Files\Claude.
macOS / Linux scan (scan_unix.go): per-user bin (~/.local/bin, ~/.npm-global/bin, pnpm/yarn/volta/asdf/bun) → glob versioned dirs (~/.nvm/versions/node/*/bin, fnm Linux + macOS, asdf shims) → system bin (homebrew Apple Silicon + Intel, MacPorts, distro /usr/bin).
Order: per-user bin → versioned managers → system bin. First hit wins.
Status cache
--version probing on Node-shimmed CLIs (codex / gemini .cmd) takes 1–3 seconds because Node has to start. Three providers in sequence on a cold boot would block the Providers page for nearly 10 seconds.
Wick persists status in ~/.<app>/config.json under provider_statuses (keyed <type>/<name>). The page render path never spawns --version — it always reads the cache. Cache misses render an empty card and trigger a background rescan; the next reload shows the result.
Code reference
Cache logic: status_cache.go. The LoadCached invariant ("page render never blocks on probe") is what stopped the page-hang race that earlier in-memory caches couldn't fix on cold boot.
| Trigger | Action |
|---|---|
| Server boot | Background RescanAll (30s timeout) — primes the cache once. |
| Open Providers page | LoadCached. Miss = empty card now, fill in background. |
| Save / delete instance | Background RescanOne (10s) auto-fired by Save. |
| "Rescan all" header | Sync RescanAll (30s) + 303 redirect. |
| "Rescan" per card | Sync RescanOne (15s) + 303 redirect. |
| Auto-rescan on + entry stale > 24h | Background RescanOne; current render still uses cached value. |
auto_rescan off | No background refresh. Manual Rescan only. |
Toggle auto-rescan from the Providers page header. The wired closure pattern (provider.go:SetAutoRescanLookup) keeps the provider package zero-dep on HTTP / configs stack.
Hide console windows on Windows
Windows console-subsystem children (claude.exe, codex.exe, npm shims) spawned from a parent without an attached console (tray app) make Windows allocate a fresh console window → flash + auto-close. Solution: SysProcAttr{HideWindow: true, CreationFlags: 0x08000000} (CREATE_NO_WINDOW).
Pattern lives in two spots:
--versionprobe — provider/hide_console_windows.go- Long-lived spawn — provider/claude/hide_console_windows.go
Same pattern is used by internal/systemtray/{editor,notify}_windows.go. Dev mode (go run from a shell) has an attached console → child inherits → no flash; CREATE_NO_WINDOW is safe to apply universally.
System prompt on Windows: argv length limit
Windows caps a process command line at 32767 UTF-16 characters. Wick's default preset alone is ~28KB, so inlining it via --append-system-prompt could push a claude spawn's argv over that limit — the process then fails to start with CreateProcess's generic The filename or extension is too long, which names the binary and gives no hint that the argv (not the binary) is the problem.
The claude provider now writes the rendered preset to <session dir>/.claude/system-prompt.md (mode 0600) and passes it with --append-system-prompt-file instead, keeping the argv small regardless of preset size. If the file can't be written, it falls back to the inline flag and logs a warning.
Source: provider/claude/prompt_file.go.
Spawn log
Every spawn writes a JSONL file under ~/.<app>/agents/providers/spawns/:
<type>__<name>__<session>__<unix-ms>.jsonlTwo events per spawn: start (with PID, argv, binary, first user message) and exit (status, duration). Filename encoding lets ls filter by type / name / session without reading file bodies. Stable across restart, friendly to tar.
Source: spawnlog.go.
The Spawn detail page in the UI (opened from a session's spawn list) renders the start + exit events plus the resolved provider source label.
The detail page also shows an Injected env panel when wick added env vars to the spawn (instance env + AI-router routing overrides). Secret values (keys, tokens, passwords) are partially masked — the first and last character are visible, the middle is replaced with *. Non-secret routing vars (e.g. ANTHROPIC_BASE_URL) pass through unmasked so you can verify the routing destination.
Crash detail and spawn window
When a spawn's process ends abnormally, the detail page surfaces why: the human-readable reason sentence, the OS exit code, and a tail of captured stderr — instead of a bare "error" status. A process that disappears without ever recording an exit event (crash, OS-kill, power loss) is called out as "died, no exit event" rather than being shown as still running.
Every spawn also shows a start → end window:
| State | Meaning |
|---|---|
| Running | No exit event yet, and the PID is still alive. |
| Clean end | An exit event was recorded; window ends at that timestamp. |
| Unclean end | No exit event, but the PID is gone — window ends at the last event's timestamp (best-effort), flagged as unclean rather than "running". |
Log viewer
Each spawn's detail page has a Logs block linking to the runtime log file for each component (server, mcp, worker, app, gate, daemon) written on the day(s) the spawn ran. Opening a link goes to /providers/logs?file=<name>, a tail viewer for that log file with:
- Copy path, copy as JSON, and download for the visible tail.
- Download bundle to grab the full file.
- The spawn's start→end window highlighted so you know where to scroll.
Backing endpoints: GET /api/providers/logs/{file} (tail, admin-only) and GET /api/providers/logs/{file}/download (full file). Both validate the filename against path traversal and resolve strictly inside the app's logs/ directory.
Spawn / probe log keys
Prefix-consistent so grep "agents." against the server log traces one spawn end-to-end:
| Log key | Site | Fields |
|---|---|---|
agents.probe: resolve | provider.Probe (debug) | type, name, path, source (registry|path|scan|miss), found |
agents.probe: ok | provider.Probe (debug) | type, name, version |
agents.probe: --version failed | provider.Probe (warn) | type, name, path, err |
agents.spawn: resolve provider | pool.Build (info) | session, provider_type, provider_name, binary, source |
agents.spawn: starting | claude.Spawn (info) | bin, argv, cwd, resume |
agents.spawn: started | claude.Spawn (info) | pid, bin |
agents.spawn: start failed | claude.Spawn (error) | bin, err + hint to set Binary |
These land in ~/.<app>/logs/server-YYYY-MM-DD.log (zerolog's global logger initialized at server boot, not the tray).
Streaming responses
Both claude and codex stream assistant text as it generates so the UI bubble fills in character-by-character (matches the VSCode / TUI experience). The provider parsers normalize the CLI-specific stream shape into a single TextDelta event the rest of wick consumes.
| Provider | CLI flag / event | Wire shape | Parser path |
|---|---|---|---|
claude | --include-partial-messages (always on) | stream_event.content_block_delta.text_delta per chunk (Anthropic Messages API streaming) | claude.go — case "stream_event" |
codex | item.updated (always emitted by codex exec --json) | Snapshot of full text-so-far per update — parser diffs against last snapshot per item.id to emit only the appended tail | codex.go — case "item.updated" + diffTail |
gemini | (not yet wired — emits one batched TextDelta at end of turn) | — | — |
Dedup logic
Both CLIs emit a final "complete" frame after the deltas (assistant for claude, item.completed for codex). The parser suppresses the trailing frame's text when partial deltas were already emitted, otherwise the UI bubble would render the full text twice.
- claude —
partialTextEmittedflag tracks whether anytext_deltafired in the current turn; reset on Done/Error. When true, theassistantframe'stextblock is dropped. - codex —
agentMsgText[item.id]map carries the last text snapshot per message;item.completedemits only the delta tail (usually empty becauseitem.updatedalready streamed everything), then the entry is deleted.
Adding streaming for a new provider
If the underlying CLI exposes a delta-style stream:
- Add field(s) to the provider's
*Raw/*Itemstruct ininternal/agents/event/<provider>.gomodelling the delta wire shape. - Handle the delta event type → emit
TextDeltawith the new chunk only. - Track per-message state if the CLI sends snapshots (codex pattern) vs. true incremental chunks (claude pattern).
- Add dedup so the trailing complete frame doesn't double-emit.
- Extend
TestRealClaudePartialStreaming-style integration test under the provider's package — assert> 1TextDeltafor a long reply.
Lifecycle state machine
Every spawn carries a state.Machine (state.go) tracking two orthogonal dimensions:
| Dimension | Values | Driven by |
|---|---|---|
Lifecycle | Spawning → Working ↔ Idle → Killed | Pool (Spawning/Killed) + parser events (Working/Idle via Apply) |
State (substate) | Idle, Thinking, RunningTool, Responding | Parser event types (Thinking / ToolUse / TextDelta etc.) |
The state machine is the source of truth for the UI lifecycle badge. Transitions fire a callback (SetLifecycleHook) which the pool wires to OnLifecycle → SSE broadcast → FE badge update. Earlier inference logic in the JS was removed — frontend only listens to lifecycle SSE events.
| Trigger | Method | Effect |
|---|---|---|
| Pool starts spawn | MarkSpawning() | Lifecycle → Spawning |
| First CLI event after spawn | Apply(ev) | Spawning → Working |
Done / Error event | Apply(ev) | Working → Idle |
| Next event in same long-lived process | Apply(ev) | Idle → Working |
| Codex respawn-on-send | agent.respawnWithMessage → MarkSpawning() | Working/Idle → Spawning (badge flips to spawning on every codex turn) |
| Subprocess exits | Pool onAgentExit → MarkKilled() | any → Killed |
The state log line lifecycle transition from=spawning to=working source=Apply:session_start is the canonical trace — grep one session ID to see the full spawn → exit timeline.
In-flight persistence
Wick mirrors every in-flight event to ~/.<app>/agents/sessions/<id>/inflight.jsonl as it arrives — provider-agnostic. The file is deleted the moment the turn flushes to conversation.jsonl, so its presence on disk means "a turn was killed or the server crashed mid-stream".
Why: an assistant turn lives only in RAM (store.turnBuf + store.eventBuf) until Done arrives. Without persistence, a refresh while the agent is mid-stream loses the bubble; a server crash loses the entire partial turn.
| Layer | Source | Cleanup |
|---|---|---|
| Live SSE replay | pool.ActiveSnapshot() → entry.PartialText + entry.InFlightEvents | RAM only; freed when turn done |
| Snapshot endpoint | /stream/snapshot reads pool first, then inflight.jsonl if pool has no entry | — |
| Boot recovery | registry.Reload → store.RecoverInflight merges leftover into conversation.jsonl as truncated:true assistant turn | File deleted after successful conversation append |
InflightEntry shape (store.go):
{"type":"text_delta", "text":"chunk", "at":"..."}
{"type":"thinking", "text":"...", "at":"..."}
{"type":"tool_use", "tool_name":"Bash","tool_input":"…","tool_use_id":"...","at":"..."}
{"type":"tool_result", "tool_use_id":"...","text":"...", "is_error":false,"at":"..."}When adding a new event type at the parser level, mirror it here via store.appendInflight so refresh / crash recovery sees the full trace.
Provider feature matrix
Quick cheatsheet for what each provider supports — useful when picking a default or implementing parity for a new CLI.
| Feature | claude | codex | gemini |
|---|---|---|---|
| Long-lived process (one spawn, many turns) | ✓ | respawn per send | ✓ |
| Resume via session ID | --resume <id> | resume <id> | — |
| Text streaming (char-by-char) | ✓ via stream_event | ✓ via item.updated diff | ✗ |
| Thinking events | ✓ | — | ✗ |
| Built-in tools | Task, Bash, Read, Edit, Glob, Grep, WebFetch, WebSearch, MCP | function_call, mcp_tool_call, command_execution (Shell), web_search | (provider-defined) |
| Tool gate hook | ✓ via PreToolUse hook | — | — |
| MCP servers | ✓ | ✓ via TOML config | ✓ |
API reference
| Method | Path | Notes |
|---|---|---|
GET | /providers/catalog/{type} | Returns the curated env + args picker entries for claude, codex, or gemini. Admin only. |
POST | /providers/rename/{type}/{name} | Renames an instance. Body: new_name=<name>. Migrates project defaults; live sessions unaffected. Admin only. |
GET | /providers/options/{type}/{name}/models | Live model list for one configured instance, used by the composer's model drill-in. ?entry=<id> expands one wick live model set (see Built-in wick provider) by resolving the vendor's current models against its stored filter. |
GET | /providers/airouter/slots/{type}?router=<id> | Returns the model slots the given router exposes for a provider type. Admin only. |
POST | /providers/detail/{type}/{name}/airouter | Saves AI-router settings (toggle + selected router + model slots + API key) for one instance. Admin only. |
GET | /api/providers/spawns?type=&name=&q=&page= | Flat, searchable, paginated (10/page) spawn list backing the Recent Spawns table. Admin only. |
GET | /api/providers/sessions?type=&name=&q=&page= | Per-session spawn summaries (grouped, paginated) for the Recent Spawns list. Admin only. |
GET | /api/providers/sessions/{id} | Every spawn of one session, newest first, for the session detail page. Admin only. |
GET | /api/providers/logs/{file}?bytes= | Tails a runtime log file (server/mcp/worker/app/gate/daemon) for the log viewer. Admin only. |
GET | /api/providers/logs/{file}/download | Downloads the full runtime log file. Admin only. |
CLI model picker
claude, codex, and gemini instances have an optional Model selection card on the provider detail page — off by default, since each CLI already has its own default model.
- Allow model selection toggle turns it on. When on, the composer shows a model level under this instance in its provider picker, and the chosen id is passed to the CLI via
--modelon spawn. - Below the toggle, an editable id + description table (a
modelskvlist) lists the models to offer — e.g.opus/ "Opus 4.8 with 1M context · best for everyday, complex tasks". Each model's description renders under its name in the picker. - Load defaults replaces the table with the built-in catalog seed for that provider type (see below) — a quick way to start editing instead of typing ids from scratch.
- Leaving the table empty falls back to the catalog seed automatically; you only need to fill it in to trim the list or add a model the catalog doesn't know about.
--model is skipped when no model is pinned, when the instance routes through an AI Router (the router sets the model itself), or when ExtraArgs already has a manual --model — an explicit operator choice is never overridden.
Model catalog
The default models offered per type (claude / codex / gemini) come from a small catalog, since none of these CLIs can list their own models:
- Embedded baseline — a
models.jsoncompiled into the binary, always present. - Remote overlay — the same file fetched from
raw.githubusercontent.com/yogasw/wick/master/internal/agents/provider/models.json, refreshed lazily (every 6h, or immediately on Rescan). This lets the maintained list of ids/descriptions grow after a release without an upgrade. - Disk cache — the last successful remote fetch, persisted under
~/.<app>/. An operator can hand-edit this file to change the offered defaults without redeploying — it's treated exactly like a remote fetch result.
The three layers merge per model id: whichever copy (embedded, remote, or disk cache) has the newest updated_at wins; a model marked disabled in any copy is hidden from the picker entirely.
Built-in wick provider
wick is a fourth provider type alongside claude / codex / gemini — but instead of spawning a CLI subprocess, it runs the agent loop in-process and talks straight to a vendor's chat API: OpenAI, OpenRouter, Anthropic, Gemini, or any OpenAI-compatible endpoint (local llama.cpp, vLLM, LiteLLM, Together, Groq, …). Use it when no CLI is installed, or you want a model none of the three CLIs ship.
Unlike claude/codex/gemini (which allow multiple named instances), there is a single wick instance. Its models are managed directly on the provider detail page:
- Add a model: pick a kind (auto-fills the base URL for known vendors), paste the API key, save. Each model gets its own row with edit / set-default / test / duplicate / disable / delete actions.
- Single vs Multiple: the Add/Edit form has a Single / Multiple toggle above the model search box. Single is the classic flow — pick or type one model id. Multiple lets you register several at once from the discovered vendor list, in one of two sub-modes:
- Manual — tick individual models (or Select all matching) and save; each ticked model becomes its own regular entry.
- Live — type a filter (or leave it empty to match all of the vendor's models — stored as
*), and save one entry that stores the filter. This is a live model set: no single model id is pinned. At picker time wick re-fetches the vendor's model list and narrows it by the filter live, so the set always reflects whatever the vendor currently offers instead of a fixed snapshot.
- Sticky default within a live set: a live set can pin one vendor model as its default — the model used when the set is picked without drilling into a specific
@vendormodel. Set it from the Add/Edit live-set form (click a row in the preview list to pin/unpin it) or via the row's ⋮ → Set default model… action, which opens a dedicated picker over the vendor's current list. No pin (or a pinned model that has since disappeared from the vendor) falls back to the top of the freshly-fetched, filtered list. - Filter grammar (used by the Live mode box, and shared by the picker's own filter): space-separated terms; a bare
termmust be contained in the model id or label, a-term/!termprefix excludes it. Case-insensitive. An empty filter (or*) matches everything — the filter is optional, not required. - Editing an entry (plain model or live set) always updates it in place — switching a plain model to a live set (or back) reuses the same id instead of leaving a duplicate behind. The Add/Edit button reads "Save …" when editing an existing entry and "Add …" when creating a new one.
- Test sends a minimal 1-token ping to confirm the key + base URL work before relying on it in a session.
- Disable hides a model from the composer without deleting its config.
- Registering more than one enabled model (or a live set, even alongside a single model) surfaces the same nested provider picker (type ▸ instance ▸ model) in the composer described above — a live model set renders as one expandable row that drills into a 4th level of matching vendor models. Both the conversation composer and the new-session composer support this drill-in.
Custom HTTP headers
Each model's Add/Edit form has a collapsed Advanced options section with a Custom headers sub-section (also collapsed by default, alongside Raw model config). It accepts one Key: Value header per line — or a curl fragment pasted straight from a browser's "copy as cURL" or another client's debug log (--header 'X: y' \, -H "X: y"); the flag, quotes, and trailing \ continuation are stripped automatically, on blur, into the canonical form.
Custom headers are applied last, after everything an adapter builds for the request — including auth (Authorization / x-api-key / anthropic-version). This is deliberate: it lets a fronting proxy use its own auth scheme, or a header spoof a different client's User-Agent. A custom header with the same name as the adapter's auth header replaces it entirely.
They also apply to:
- Model discovery (the
/modelslisting used by the Add/Edit form's model picker) — typed-but-unsaved headers are sent along, so a gateway that requires a header to serve/modelsworks before the model row is saved. - Copy as curl (see Session interactions log) — the reconstructed request reflects the actual headers wick sends, including any auth override.
Loop guards & goal mode
Unlike the CLI providers, wick's agentic loop has no subprocess to fall back on if it stalls — so it carries its own no-progress guards, configurable on the Advanced section of the provider settings card:
| Setting | Default | What it does |
|---|---|---|
max_turns | 0 (unlimited) | Caps tool-call rounds in one reply. 0 means the guards below are the only brake. |
max_consec_errors | 20 | Cuts the turn after this many consecutive all-error tool rounds. A round with at least one successful call resets the counter. |
max_turn_minutes | 60 | Wall-clock ceiling for one reply. |
max_model_retries | 3 | Total attempts per failing model call (incl. the first). 1 disables retries. |
model_call_timeout_sec | 120 | Ceiling for one model-call attempt before it counts as failed and retries. |
When a cut fires, it's shown inline in the transcript as [wick] turn cut: … (max-turns cap / consecutive errors / wall-clock limit) instead of the turn silently dying.
Goal mode overrides the cuts for long-running jobs. The shared todo tool (same one used for the checklist widget) accepts an optional goal field — passing it opens a durable latch (goal.json in the session directory) with the given success criterion. While the goal is open:
- A plain-text reply does not end the turn — wick nudges the model to keep working toward the goal instead.
- Hitting the consecutive-error cap or the wall-clock cap does not end the turn either — wick emits a nudge (with the error/timeout context) and lets the model try a different approach, resetting that guard's window.
- A manual Kill always wins — it stops the session regardless of an open goal.
The model closes the latch by calling todo again with goal_done: true (success) or goal_abandon: true (giving up); either releases the force-continue behavior and normal turn-ending rules resume. Every provider writes the same goal.json file (useful for resuming after a restart), but only the in-process wick engine force-continues on it — CLI providers run their own agentic loop outside wick's control.
Session interactions log
Every model call a wick session makes is logged to <session>/wick-interactions.jsonl — request (system prompt, messages, tools) and response (text, tool calls, tokens, latency, error). The session's detail page shows this in place of the CLI providers' spawn/reproduce view, with server-side search and pagination.
Each logged interaction has a copy as curl action that reconstructs the exact HTTP request wick sent, in four formats (single-line, Bash, raw HTTP, JSON body), with an editable body preview and per-part copy. The bearer token defaults to a $WICK_MODEL_API_KEY placeholder; an admin can reveal the real key inline instead of hunting for it in the provider settings.
Live model-call observability. While a call is in flight, the "running" row at the top of the log is no longer a static "model call in progress…" label:
- It shows whether the model is actually mid-call or a tool is running instead (named from the newest logged interaction's tool calls), with a live elapsed timer either way.
- A retried call shows the attempt number and a short reason (rate limited, server error, context full, …).
- View request reconstructs the curl for the request being sent right now, before it finishes and a log record exists — useful for debugging a call that looks stuck.
- Cancel call aborts just the in-flight model call; the turn itself keeps going (the agent sees the call as failed/cancelled and can retry or continue), rather than killing the whole session.
This is backed by every wick model adapter (OpenAI/Anthropic/OpenAI-compatible and Gemini alike) now sharing one retry policy: transient failures (timeout, connection reset, 429, 5xx) retry with backoff under a bounded per-attempt timeout, while fatal errors (bad key/model, other 4xx) fail fast without retrying.
Context compaction
A wick session's history is bounded by a context budget. When it nears the limit, wick asks the model to summarize the oldest turns (decisions, facts, file paths, done vs. pending) and continues with the summary in place of the raw turns — so long tasks don't hit a hard context-window error. This also runs a heavier pass automatically if a request still overflows the vendor's window. Type /compact in a session to trigger it manually.
Long-running tool calls
Shell commands the wick agent runs don't block on a fixed wall-clock deadline — a long command (installs, builds, crawls) can run as a background job that the agent polls for status/log instead of stalling the turn. This mirrors the same command-gate and approval flow as any other Wick tool call.
Skills
wick is a first-class skill provider, on equal footing with claude/codex/gemini: its own skill directory (~/.<app>/skills) is created automatically and registered alongside the others, so it shows up in the Skills Manager UI, the sync/upload flows, and the wick session's / menu — no manual folder setup needed. See Skills Manager for the shared sync mechanics.
Since a wick session runs in-process (no CLI to hand --add-dir to and let it load skills itself), wick injects a compact skill catalog into its own system prompt: one line per skill (name, one-line description, path to its SKILL.md), capped so a large skill library can't blow the prompt budget. The agent reads a skill's full SKILL.md on demand via its file-read tool when it actually needs to follow one — the catalog itself never contains the full skill body.
See also
- Projects —
default_providerfield per project; how project defaults auto-migrate on rename. - Pool & Sessions — how
provider_type/provider_nameare forwarded to the spawner. - AI Router — routing provider spawns through an embedded AI router (9router / OmniRoute).
- Command Gate — gate sidecar lives next to the main binary, separate from providers.
- Skills Manager — shared skill directories, sync, and the file browser UI.