Skip to content

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).

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:

FieldNotesSource
Typeclaude / codex / gemini.provider.go:34
NameUnique within type. Defaults to Type itself. Pick anything: work, personal, staging.provider.go:53
BinaryAbsolute path. Empty = let wick resolve via PATH + scan.provider.go:54
ExtraArgsAppended to every spawn argv.provider.go:55
EnvExtra env vars. This is where ANTHROPIC_API_KEY goes for a per-instance PAT.provider.go:56
DisabledToggle without deleting.provider.go:57
UseAIRouterRoute 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
AIRouterProviderWhich registered router to route through (9router, omniroute, …). Empty = default (9router).provider.go
AIRouterModelsPer-slot model IDs (e.g. opus → cc/claude-opus-4-6). Slots defined by the selected router. All optional.airouter.go
AIRouterAPIKeyCustom 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.Provider matched the old type/name key is rewritten to the new key and saved. The response includes a projects_migrated count; the UI reports how many projects were updated.
  • Live sessions are not migrated: a session's agents.json entry 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 / 0 for 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:

ProviderEnv varsCLI args
claudeANTHROPIC_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
codexCODEX_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, …)
geminiGEMINI_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/providers showing the three default cards (claude / codex / gemini) with version + path resolved, plus the "Add Instance" + "Rescan all" + "Auto-rescan" header. Save to docs/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 (showing ANTHROPIC_API_KEY=... placeholder), Disabled toggle. Save to docs/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:

BadgeMeaning
enabled ✓Hook is on and the last probe verified it.
enabled (unverified)Hook is toggled on but no successful probe yet.
readyHook is off but a prior probe succeeded — can be re-enabled quickly.
disabledHook 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:

StepWhat it checksSource
1. registryInstance.Binary set in the UI form. Used as-is, no PATH lookup.provider.go:62 (Bin())
2. pathexec.LookPath(<type>) against %PATH% + PATHEXT (Windows).
3. scanKnown install locations the installer drops but doesn't always wire into PATH.scan_unix.go, scan_windows.go
4. missAll 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.

TriggerAction
Server bootBackground RescanAll (30s timeout) — primes the cache once.
Open Providers pageLoadCached. Miss = empty card now, fill in background.
Save / delete instanceBackground RescanOne (10s) auto-fired by Save.
"Rescan all" headerSync RescanAll (30s) + 303 redirect.
"Rescan" per cardSync RescanOne (15s) + 303 redirect.
Auto-rescan on + entry stale > 24hBackground RescanOne; current render still uses cached value.
auto_rescan offNo 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:

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>.jsonl

Two 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:

StateMeaning
RunningNo exit event yet, and the PID is still alive.
Clean endAn exit event was recorded; window ends at that timestamp.
Unclean endNo 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 keySiteFields
agents.probe: resolveprovider.Probe (debug)type, name, path, source (registry|path|scan|miss), found
agents.probe: okprovider.Probe (debug)type, name, version
agents.probe: --version failedprovider.Probe (warn)type, name, path, err
agents.spawn: resolve providerpool.Build (info)session, provider_type, provider_name, binary, source
agents.spawn: startingclaude.Spawn (info)bin, argv, cwd, resume
agents.spawn: startedclaude.Spawn (info)pid, bin
agents.spawn: start failedclaude.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.

ProviderCLI flag / eventWire shapeParser path
claude--include-partial-messages (always on)stream_event.content_block_delta.text_delta per chunk (Anthropic Messages API streaming)claude.gocase "stream_event"
codexitem.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 tailcodex.gocase "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.

  • claudepartialTextEmitted flag tracks whether any text_delta fired in the current turn; reset on Done/Error. When true, the assistant frame's text block is dropped.
  • codexagentMsgText[item.id] map carries the last text snapshot per message; item.completed emits only the delta tail (usually empty because item.updated already streamed everything), then the entry is deleted.

Adding streaming for a new provider

If the underlying CLI exposes a delta-style stream:

  1. Add field(s) to the provider's *Raw / *Item struct in internal/agents/event/<provider>.go modelling the delta wire shape.
  2. Handle the delta event type → emit TextDelta with the new chunk only.
  3. Track per-message state if the CLI sends snapshots (codex pattern) vs. true incremental chunks (claude pattern).
  4. Add dedup so the trailing complete frame doesn't double-emit.
  5. Extend TestRealClaudePartialStreaming-style integration test under the provider's package — assert > 1 TextDelta for a long reply.

Lifecycle state machine

Every spawn carries a state.Machine (state.go) tracking two orthogonal dimensions:

DimensionValuesDriven by
LifecycleSpawningWorkingIdleKilledPool (Spawning/Killed) + parser events (Working/Idle via Apply)
State (substate)Idle, Thinking, RunningTool, RespondingParser 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.

TriggerMethodEffect
Pool starts spawnMarkSpawning()Lifecycle → Spawning
First CLI event after spawnApply(ev)Spawning → Working
Done / Error eventApply(ev)Working → Idle
Next event in same long-lived processApply(ev)Idle → Working
Codex respawn-on-sendagent.respawnWithMessageMarkSpawning()Working/Idle → Spawning (badge flips to spawning on every codex turn)
Subprocess exitsPool onAgentExitMarkKilled()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.

LayerSourceCleanup
Live SSE replaypool.ActiveSnapshot()entry.PartialText + entry.InFlightEventsRAM only; freed when turn done
Snapshot endpoint/stream/snapshot reads pool first, then inflight.jsonl if pool has no entry
Boot recoveryregistry.Reloadstore.RecoverInflight merges leftover into conversation.jsonl as truncated:true assistant turnFile deleted after successful conversation append

InflightEntry shape (store.go):

json
{"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.

Featureclaudecodexgemini
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 toolsTask, Bash, Read, Edit, Glob, Grep, WebFetch, WebSearch, MCPfunction_call, mcp_tool_call, command_execution (Shell), web_search(provider-defined)
Tool gate hook✓ via PreToolUse hook
MCP servers✓ via TOML config

API reference

MethodPathNotes
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}/modelsLive 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}/airouterSaves 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}/downloadDownloads 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 --model on spawn.
  • Below the toggle, an editable id + description table (a models kvlist) 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:

  1. Embedded baseline — a models.json compiled into the binary, always present.
  2. 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.
  3. 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 @vendor model. 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 term must be contained in the model id or label, a -term / !term prefix 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 /models listing 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 /models works 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:

SettingDefaultWhat it does
max_turns0 (unlimited)Caps tool-call rounds in one reply. 0 means the guards below are the only brake.
max_consec_errors20Cuts the turn after this many consecutive all-error tool rounds. A round with at least one successful call resets the counter.
max_turn_minutes60Wall-clock ceiling for one reply.
max_model_retries3Total attempts per failing model call (incl. the first). 1 disables retries.
model_call_timeout_sec120Ceiling 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

  • Projectsdefault_provider field per project; how project defaults auto-migrate on rename.
  • Pool & Sessions — how provider_type / provider_name are 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.
Built with ❤️ by a developer, for developers.