@tekmidian/pai 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +41 -0
  2. package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
  3. package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
  4. package/dist/{chain-CTtHligI.mjs → chain-DhVHVnmT.mjs} +263 -791
  5. package/dist/chain-DhVHVnmT.mjs.map +1 -0
  6. package/dist/cli/index.mjs +10 -8
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/program.mjs +10 -8
  9. package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
  10. package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
  11. package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
  12. package/dist/config-D1G9IFpn.mjs.map +1 -0
  13. package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
  14. package/dist/config-Dl8lT4Lu.mjs +527 -0
  15. package/dist/config-Dl8lT4Lu.mjs.map +1 -0
  16. package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
  17. package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
  18. package/dist/daemon/index.mjs +12 -10
  19. package/dist/daemon/index.mjs.map +1 -1
  20. package/dist/daemon-BSL9sXxm.mjs +22 -0
  21. package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
  22. package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
  23. package/dist/daemon-mcp/index.mjs +127 -6
  24. package/dist/daemon-mcp/index.mjs.map +1 -1
  25. package/dist/detector-BLVOCiJ_.mjs +5 -0
  26. package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
  27. package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
  28. package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
  29. package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
  30. package/dist/factory-C7ua_3ik.mjs +3 -0
  31. package/dist/{providers-FYkZjn_C.mjs → fallback-CdXv-Np0.mjs} +411 -51
  32. package/dist/fallback-CdXv-Np0.mjs.map +1 -0
  33. package/dist/hooks/context-compression-hook.mjs +136 -22
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/post-compact-inject.mjs +98 -7
  36. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  37. package/dist/hooks/route-agents-to-worker.mjs +75 -6
  38. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  39. package/dist/hooks/worker-proxy.mjs +75 -6
  40. package/dist/hooks/worker-proxy.mjs.map +2 -2
  41. package/dist/hooks/worker-status-line.mjs +149 -90
  42. package/dist/hooks/worker-status-line.mjs.map +4 -4
  43. package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
  44. package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
  45. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
  46. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
  47. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  48. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
  50. package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
  51. package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
  52. package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
  53. package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
  54. package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
  55. package/dist/main-resolver-E5WlilhM.mjs +6 -0
  56. package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
  57. package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
  58. package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
  59. package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
  60. package/dist/{planner-BDI7bE9B.mjs → planner-Cm3g6fWH.mjs} +5 -3
  61. package/dist/{planner-BDI7bE9B.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
  62. package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
  63. package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
  64. package/dist/{program-DXBwJV7h.mjs → program-AR0qlYRg.mjs} +334 -78
  65. package/dist/program-AR0qlYRg.mjs.map +1 -0
  66. package/dist/query-feedback-CGcEiGtY.mjs +3 -0
  67. package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
  68. package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
  69. package/dist/router-CAtyVQud.mjs +3 -0
  70. package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
  71. package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
  72. package/dist/server-BOuAOj9b.mjs +386 -0
  73. package/dist/server-BOuAOj9b.mjs.map +1 -0
  74. package/dist/skills/Worker/SKILL.md +19 -1
  75. package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
  76. package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
  77. package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
  78. package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
  79. package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
  80. package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
  81. package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
  82. package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
  83. package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
  84. package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
  85. package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
  86. package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
  87. package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
  88. package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
  89. package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
  90. package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
  91. package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
  92. package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
  93. package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
  94. package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
  95. package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
  96. package/docs/commands/README.md +5 -2
  97. package/docs/commands/worker.md +66 -5
  98. package/docs/images/provider-abstraction-live.png +0 -0
  99. package/docs/images/workers.png +0 -0
  100. package/docs/launcher-independence.md +70 -0
  101. package/docs/provider-abstraction.md +87 -0
  102. package/docs/provider-independence-details.md +330 -0
  103. package/docs/provider-independence.md +75 -0
  104. package/docs/worker.md +72 -11
  105. package/package.json +1 -1
  106. package/scripts/build-hooks.mjs +6 -1
  107. package/scripts/build-skill-stubs.mjs +17 -4
  108. package/src/hooks/ts/lib/context-fill.test.ts +158 -4
  109. package/src/hooks/ts/lib/context-fill.ts +102 -35
  110. package/statusline-command.sh +120 -21
  111. package/dist/chain-CTtHligI.mjs.map +0 -1
  112. package/dist/config-B64vFg14.mjs.map +0 -1
  113. package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
  114. package/dist/daemon-CmHHmKpg.mjs +0 -20
  115. package/dist/detector-DO730Zq0.mjs +0 -5
  116. package/dist/factory-BD-np0Vd.mjs +0 -3
  117. package/dist/main-resolver-D6IImXvF.mjs +0 -4
  118. package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
  119. package/dist/program-DXBwJV7h.mjs.map +0 -1
  120. package/dist/providers-FYkZjn_C.mjs.map +0 -1
  121. package/dist/query-feedback-DSVyHtrG.mjs +0 -3
  122. package/dist/router-Bk77E7hj.mjs +0 -3
  123. package/dist/runtime-paths-rni52zHX.mjs +0 -50
  124. package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
  125. package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
  126. package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
@@ -0,0 +1,70 @@
1
+ # Launcher Independence
2
+
3
+ > Verified 2026-09-17: the `pai` session launcher starts sessions on whichever
4
+ > worker provider is active — the claude binary, Anthropic credentials and
5
+ > `~/.claude` session state are needed only where a claude-format transcript is
6
+ > genuinely being resumed, and nowhere else.
7
+
8
+ ## The two launch paths
9
+
10
+ | Invocation | Code path | Engine decision |
11
+ |---|---|---|
12
+ | `pai` (bare, tty) | `cmdPick` → `launchInDir(dir, name, { forceFresh: true, engine })` | `resolveLaunchRoute` (src/cli/lib/launch.ts) — active provider wins; `w`/`a` keys force one engine |
13
+ | `pai <name>` | `cmdMain` → `openMatch` / registry match → `launchSession` → **`launchInDir`** | same `resolveLaunchRoute` — fresh launch follows the provider; a resumable transcript pins claude |
14
+
15
+ `resolveLaunchRoute` mirrors the Agent-routing hook's gate: routing on = one
16
+ interactive `pai worker run --label <name> --cwd <dir>`; routing off, no active
17
+ provider, or an explicit override = `claude`. Resume stays claude by design — a
18
+ claude transcript has nothing to resume on another engine.
19
+
20
+ ## The gap that was found and fixed
21
+
22
+ `launchSession` in `src/cli/commands/main-resolver.ts` spawned `claude` directly
23
+ in all three of its branches (resume, resume-fallback, fresh). Every other
24
+ launch path had been routed (6adddb5), but this one pinned `pai <name>`,
25
+ `pai <uuid>` and history-search launches to Anthropic credentials regardless of
26
+ configuration. It was also a third near-copy of the launch dance — the exact
27
+ shape that once left `probeResume` fixed in one copy and broken in two.
28
+
29
+ Fix: `launchSession` now resolves the resumable UUID, directory and display
30
+ name, then hands the launch to `launchInDir`. One router, one spawn site per
31
+ engine. Verified:
32
+
33
+ - **Before/after dry-run** (topic match over prompt history, non-resumable
34
+ session, `env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL pai <topic> <pick> --dry-run`):
35
+ before → `argv: claude --name …` (would fail to authenticate with the env
36
+ stripped); after → `argv: pai worker run --label … --cwd …`.
37
+ - **Resume path unchanged**: the same dry-run against a resumable session still
38
+ prints the probe + `claude --resume` plan — through `launchInDir` now.
39
+ - **Unit tests**: `src/cli/lib/launch.test.ts` covers every `resolveLaunchRoute`
40
+ outcome (provider active/off, empty provider map, override keys, resume pins
41
+ claude, forceFresh drops it) and that `probeResume` exists in exactly one
42
+ file; `main-resolver.test.ts` still passes.
43
+
44
+ ## Why the provider needs nothing from Anthropic
45
+
46
+ `buildRunEnv` (src/workers/run-env.ts) builds the worker child's environment
47
+ from the provider config alone: token from the provider's key file,
48
+ `ANTHROPIC_BASE_URL` from its `baseUrl`, model overrides from its model map —
49
+ and it **deletes `ANTHROPIC_API_KEY`** and disables nonessential traffic. The
50
+ `claude` binary serves as the agent runtime, but authentication is entirely the
51
+ provider's.
52
+
53
+ Verified live: a headless `pai worker run` with
54
+ `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`/`ANTHROPIC_BASE_URL` unset completed
55
+ and returned its reply — the active provider (configuration, nothing else)
56
+ supplied the credentials. `src/workers/daemon-llm.test.ts` asserts the same
57
+ rule for background spawns: the API key never reaches the child, the provider
58
+ token and base URL do.
59
+
60
+ ## What still touches claude-only state, deliberately
61
+
62
+ - **Resume** (`claude --resume <uuid>`, `pai resume`): the transcript format and
63
+ the probe's filesystem layout (`~/.claude/projects/<encoded-cwd>/`) are
64
+ claude's; the probe itself is read-only and spawns nothing.
65
+ - **Explicit claude** (`a` in the picker, `engine: "claude"`).
66
+ - **Routing off** (`pai worker off`): everything runs claude, as before.
67
+
68
+ Switching providers (`pai worker providers use <name>`) changes every fresh
69
+ launch — picker, name match, UUID, history search — without touching any of
70
+ these files again.
@@ -0,0 +1,87 @@
1
+ # Provider Abstraction
2
+
3
+ ## What this layer achieves
4
+
5
+ The entire assistant stack — interactive sessions, workers, follow panes, hooks, MCP tools and the statusline — runs on **any provider that speaks the Anthropic Messages protocol**, not just Anthropic. This has been demonstrated end-to-end on a third-party `glm` endpoint serving a 1M-context model: the interactive session, every delegated worker, the daemon-side background calls (summaries, context handovers, knowledge-graph extraction) and all instrumentation run against that provider, with nothing falling back to a vendor login.
6
+
7
+ What that buys:
8
+
9
+ - **No single-vendor lock-in.** Providers are entries in a registry (`src/workers/config.ts`): protocol (`anthropic`/`openai`), engine (`claude`/`codex`), model ids, key files, cost tiers, capability tags, quota probes. Switching is `pai worker providers use <name>`; disabling routing entirely is `pai worker off`.
10
+ - **Plan/quota-aware instrumentation.** The statusline reads the *active provider's* real plan utilization (5-hour rolling window and weekly quota with reset times), not a hardcoded vendor endpoint.
11
+ - **Cost routing per task class.** Work is dispatched by class (`draft`, `spotcheck`, `complex`, …); each class maps to a provider and model, so cheap work lands on cheap models without touching code.
12
+ - **Provider swaps are configuration, not surgery.** Every seam that used to assume one vendor — daemon LLM spawns, compaction-history family detection, context-window defaults, tier aliases — has been patched at protocol level (see below), so a new provider is a registry entry plus a key file.
13
+
14
+ OpenAI-protocol providers are reached through the translating proxy (`src/workers/proxy/`), and the Codex engine (`src/workers/codex.ts`) normalizes its events into the same shape — both are first-class in the registry.
15
+
16
+ ## Tools
17
+
18
+ ### Worker provider/model routing
19
+
20
+ `pai worker run --class <class>` picks the provider and model for a job by task class. The standard classes (`WORKER_CLASSES`, `src/workers/config.ts:126`): `draft`, `plan`, `implement`, `review`, `research`, `spotcheck`, `simple`, `complex`. `--provider` and `--model` override for a single call.
21
+
22
+ Model ids are configuration, not code. Two surfaces manage them:
23
+
24
+ - **CLI** — `pai worker model` (`src/cli/commands/worker/model.ts`): no args lists the active provider with its `default` and `fast` model ids; `model <id>` sets the default; `model fast <id>` sets the fast slot; `--provider` targets a non-active provider.
25
+ - **MCP** — the `worker_model` tool (`src/daemon-mcp/tools/worker-model.ts`): `action: get|set`, `slot: default|fast`, optional `provider`.
26
+
27
+ ```text
28
+ $ pai worker model
29
+ glm (active)
30
+ default: glm-5.3[1m]
31
+ fast: glm-5.3-flash
32
+
33
+ $ pai worker model glm-5.4
34
+ $ pai worker model fast glm-5.4-flash --provider glm
35
+ ```
36
+
37
+ The `fast` slot is what cheap classes resolve to; the runner also honors the platform's tier aliases by pinning `ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL` to the provider's models in the spawn environment (`src/workers/run-env.ts`).
38
+
39
+ ### Provider-aware statusline
40
+
41
+ `statusline-command.sh` derives everything it can from the session's model id instead of assuming one vendor:
42
+
43
+ - **Plan utilization** — glm sessions fetch the provider's plan quota (5-hour window and weekly credit window, `nextResetTime` rendered as reset times); claude models keep the existing Anthropic OAuth path. Each source has its own 60-second cache. On fetch failure the provider windows show `?` rather than silently falling back to another plan's numbers. `PAI_ZAI_QUOTA_URL` overrides the endpoint (kill switch / testing).
44
+ - **Context window from the model id** — a `[1m]` variant suffix means a 1,000,000-token window (`statusline-command.sh:126`); ids with no marker keep the previous default. The same derivation lives in TypeScript as `contextWindowFromModelId()` (`src/utils/model-window.ts`), so the session hooks and the worker meter agree with the statusline.
45
+ - **`(N% left)`** — remaining context until auto-compact, computed against that derived window.
46
+ - **Worker row** — line 4 lists the workers this session launched, running ones with their current step (`pai worker status-line`, `statusline-command.sh:634`).
47
+
48
+ ### Seam patches
49
+
50
+ The seams that previously hardcoded one vendor, now patched:
51
+
52
+ - **Daemon LLM spawns route through the provider registry** — `src/workers/daemon-llm.ts` builds the spawn plan for session summaries (`src/daemon/session-summary-worker.ts`), context handovers (`src/daemon/context-handover-worker.ts`) and KG extraction (`src/memory/kg-extraction.ts`) via `buildRunEnv()` (`src/workers/run-env.ts`): provider base URL and token, the provider's concrete model id instead of a tier alias, and class-appropriate timeouts. With no provider configured it falls back to the historical bare-alias behaviour minus the vendor API key.
53
+ - **Compaction-history family detection accepts any provider model** — `modelFamily()` (`src/hooks/ts/lib/context-fill.ts:379`) derives a stable family key from the model id itself, stripping any bracketed variant suffix (`glm-5.3[1m]` → `glm-5.3`), instead of allowlisting `claude-*` and discarding everything else as "foreign". A provider-model session keeps its measured compaction history.
54
+ - **Context-window defaults derived from the model id** — `src/utils/model-window.ts` is the single helper; consumers are the session hooks (`context-fill.ts`) and the provider schema default (`src/workers/config.ts`). The statusline derives its window the same way, instead of writing a single assumed size into the state file for every model it does not recognize.
55
+ - **Tier aliases map provider models into the cheap/standard tiers** — `modelToClass()` (`src/workers/agents.ts`) resolves an agent's model through the provider registry (`fast` → cheap tier, `default` → standard) and logs once, then falls back to the standard class, instead of silently dropping the hint for unknown ids.
56
+
57
+ ### Delegation instruments
58
+
59
+ - **Agent-tool gate** — `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts` (PreToolUse, Agent matcher) denies in-process subagents whenever worker providers are configured, so no delegated work runs on the vendor login; the deny reason tells the orchestrator to use `pai worker run` instead. `ALLOW_ANTHROPIC_AGENTS=1` bypasses it for one session. Every decision is appended to the routing ledger (`pai worker log`).
60
+ - **Worker exemption marker** — every worker spawn carries `PAI_WORKER=1` (`src/workers/run-env.ts:47`; `src/workers/codex.ts:50` for the Codex engine). `isWorkerSession()` (`src/hooks/ts/lib/worker-session.ts`) makes all per-session hooks (autosave, stop bookkeeping, compaction state) skip disposable workers, so the gate and the bookkeeping never fight the workers themselves.
61
+ - **Post-compaction reinjection** — `src/hooks/ts/session-start/post-compact-inject.ts` (SessionStart, matcher `compact`) replays the state the PreCompact hook saved, so a compacted orchestrator session resumes with its context intact. The live worker list stays visible in the statusline's worker row and via `pai worker ps` / `pai worker follow <id>`.
62
+
63
+ ### Session-level instruments (deployed config)
64
+
65
+ Two further instruments are deployed configuration in the PAI config dir rather than repo code, so a tree-only audit misses them:
66
+
67
+ - **Edit-delegation guard** — a PreToolUse hook (`route-edits-to-worker`) blocks `Edit`/`MultiEdit`/`Write` on files inside any git work tree for orchestrator sessions, while worker sessions are exempt via the `PAI_WORKER=1` marker set on every worker spawn. Code changes route through workers mechanically.
68
+ - **Compact recovery** — on `SessionStart(compact)` a hook injects the live `pai worker ps --all` output into the session context, so a session resuming from compaction immediately sees every running worker.
69
+
70
+ ### Live worker panes
71
+
72
+ `pai worker follow` (`src/cli/commands/worker/index.ts:165`) attaches a chat pane to a worker, rendered by `src/workers/viewer.ts`, `pane.ts`, `render.ts` and `chatui.ts`:
73
+
74
+ - **Transcript with stamped gutter** — every event line carries a dim `HH:MM:SS │ ` stamp from the event's own timestamp, with continuation padding for wrapped text (`render.ts:135`); the pane's bar runs unbroken down its full height (`render.ts:139`).
75
+ - **Prompt row** — `› ` with readline line editing. Plain text is *said* to a running worker; `/resume <text>` continues a finished one; `/status` prints a one-line status (`chatui.ts`).
76
+ - **Status ticker** — a fixed bottom row showing elapsed time, current intent and tool (`$ bun run test` …) plus the worker's provider/model and context meter (`tickerText()`, `render.ts:183`; meter data from `src/workers/status.ts`, whose window comes from the run's init event or the provider default). A finished worker freezes the row with ✓/✗.
77
+ - **Resize-safe redraw** — on terminal resize the viewer re-reads the geometry, clears the stale transcript and refills it from events (`viewer.ts:735`), so the fixed prompt/ticker rows and the scroll-region transcript stay consistent at any size.
78
+
79
+ ## In action
80
+
81
+ ![A live session on a third-party 1M-context model: the provider-aware statusline (plan quota windows with reset times, derived context window, worker row) above worker follow panes with stamped transcript, prompt row and status ticker.](images/provider-abstraction-live.png)
82
+
83
+ ## Current limits
84
+
85
+ The layer is proven against one non-Anthropic provider so far (the `glm` endpoint); other Anthropic-protocol providers are expected to work but have not been exercised end-to-end, and OpenAI-protocol providers additionally depend on the translating proxy. Instrumentation still special-cases per-provider quota endpoints as providers are added. The seam audit that drove these patches lives outside this repository; new seams are patched as they surface, and each patch carries a unit test pinning the provider-neutral behaviour (`src/workers/daemon-llm.test.ts`, `src/workers/model.test.ts`, `src/workers/agents.test.ts`, `src/hooks/ts/lib/context-fill.test.ts`).
86
+
87
+ For the full worker command reference see [worker.md](worker.md) and [commands/worker.md](commands/worker.md).
@@ -0,0 +1,330 @@
1
+ # Provider Independence — Details
2
+
3
+ Everything technical about provider independence: what switches, the tools,
4
+ the configuration, the internals. The human-facing page is
5
+ [provider-independence.md](provider-independence.md).
6
+
7
+ ## Switch what?
8
+
9
+ Two levels, one provider registry. Both switch by sentence or by command —
10
+ never by editing code:
11
+
12
+ | Level | What runs on the provider | Switch with |
13
+ | --- | --- | --- |
14
+ | The crew | delegated workers — research, drafting, implementation, review, spotchecks | `worker_providers` (use): "Use glm for the workers from now on." |
15
+ | The orchestrator | the session itself — the interactive chat you are typing into | start it with `glm` (the shim for `pai worker run`), or pick a project with `pai` — the picker follows the active provider |
16
+
17
+ The main session orchestrates, reviews and merges; workers do the work —
18
+ unless the session itself runs on a provider too, in which case everything is
19
+ off the Anthropic account.
20
+
21
+ ### The crew — `worker_providers` add / use
22
+
23
+ - **Add once.** "Add a worker provider named glm, base URL <url>, model
24
+ <model>, here is the key: <token>" → `worker_providers` (add). The key is
25
+ parked in `~/.config/pai/keys/<name>` (mode 0600); the config records only
26
+ the path. OpenAI-compatible endpoint? Say "upstream URL" — PAI routes it
27
+ through its local translating proxy. The first provider added also turns
28
+ worker routing on and seeds the nine classes (`addProvider`,
29
+ `src/workers/providers.ts:51`); from then a PreToolUse hook denies every
30
+ in-process subagent.
31
+ - **Use.** "Switch the fleet to glm." → `worker_providers` (use): sets
32
+ `workers.active` (`useProvider`, `src/workers/providers.ts:170`); classes
33
+ without an explicit target follow.
34
+ - **Test.** "Is glm alive?" → `worker_providers` (test): a one-word pong
35
+ probe, latency, OK/FAILED.
36
+ - **Off.** "Turn workers off." → `worker_toggle`: subagents run on Anthropic
37
+ again; the registry is kept.
38
+
39
+ ### The orchestrator — the session itself
40
+
41
+ There is no in-session switch: a session runs on a provider by being
42
+ **started that way**. The launcher is `glm` — a shim in `~/.local/bin`
43
+ written by `pai worker install` (`src/workers/install.ts:119` body
44
+ `exec pai worker run "$@"`; installed names `glm`, `glm-run`, `glm-ps`,
45
+ `glm-log`, `worker-say`, `:125`).
46
+
47
+ `pai worker run` without `-p` starts an **interactive** Claude Code session:
48
+ it spawns `claude` with the terminal inherited (`stdio: "inherit"`,
49
+ `src/workers/run.ts:452`) — that process *is* the chat pane, not a subagent
50
+ of it (`origin: "chat"`, `run.ts:394`). No MCP restriction applies.
51
+
52
+ The `pai` project picker follows the active provider the same way: its
53
+ launch keys start an interactive worker run in the chosen directory
54
+ (`w` forces it, `a` forces plain `claude`).
55
+
56
+ What actually puts it on the provider is the spawn environment
57
+ (`buildRunEnv`, `src/workers/run-env.ts`):
58
+
59
+ | Env var | Value | Where |
60
+ | --- | --- | --- |
61
+ | `ANTHROPIC_API_KEY` | deleted — the Anthropic key never reaches a provider session | `run-env.ts:19` |
62
+ | `ANTHROPIC_BASE_URL` | the provider's base URL; the local proxy's URL for OpenAI-protocol providers | `run-env.ts:25,37` |
63
+ | `ANTHROPIC_AUTH_TOKEN` | the token read from the provider's key file (proxy runs: placeholder) | `run-env.ts:40` |
64
+ | `ANTHROPIC_DEFAULT_HAIKU_MODEL` / `_SONNET_MODEL` / `_OPUS_MODEL` | the provider's fast/default models — every model slot maps to the provider | `run-env.ts:41-43` |
65
+ | `ENABLE_TOOL_SEARCH` | `true` in interactive runs; headless runs get `PAI_WORKER=1` instead | `run-env.ts:47-49` |
66
+
67
+ - **Model.** `--model <provider default>` is passed unless you name one
68
+ (`run.ts:438-439`). A `[1m]` suffix on the model id (`glm-5.3[1m]`) selects
69
+ the 1,000,000-token window variant: the suffix is read wherever a window is
70
+ needed (`contextWindowFromModelId()`, `src/utils/model-window.ts:33`) and
71
+ stripped for the family name (`stripModelVariant()`, `:19`).
72
+ - **Machine-wide instead of per-session.** `worker_fallback` (on) writes the
73
+ same env into the `env` block of `~/.claude/settings.json`
74
+ (`src/workers/fallback.ts:90-94`; path override `CLAUDE_SETTINGS_PATH` for
75
+ dry runs, `:46`), so every NEW Claude Code process — interactive,
76
+ task-bus, daemon summarizer — runs on the provider. `off` restores the
77
+ saved previous state. Running sessions keep their provider until
78
+ restarted; worker routing is unchanged while on.
79
+ - **Bypass.** `ALLOW_ANTHROPIC_AGENTS=1` in the environment re-enables the
80
+ Agent tool for one session.
81
+
82
+ ## Tools (MCP)
83
+
84
+ Everything here happens in chat: you say the sentence, the session calls the
85
+ tool — you never name the tool yourself. These are the **twelve `worker_*`
86
+ tools** the pai MCP server registers (`src/daemon-mcp/index.ts:804-1334`);
87
+ there is no worker surface outside them. Parameter names are the schema's
88
+ own.
89
+
90
+ ### At a glance
91
+
92
+ | Tool | You say | What it does |
93
+ | --- | --- | --- |
94
+ | `worker_status` | "How is the worker system doing?" | On/off, active provider, providers, today's run tally, running workers with waiting handoffs |
95
+ | `worker_providers` | "Switch the workers to glm." | Manage providers: list / add / update / remove / use / enable / disable / test |
96
+ | `worker_fallback` | "Run everything on glm." | Machine-wide: every NEW Claude Code process on a provider — or back on the Anthropic login |
97
+ | `worker_classes` | "Make spotchecks cheaper." | Map task classes to providers or routing constraints |
98
+ | `worker_model` | "Put glm on the 1M-context model." | Show or set a provider's model ids (default and fast slots) |
99
+ | `worker_run` | "Fix the black buttons bug." | Start a worker or chain; returns the id immediately |
100
+ | `worker_handoff` | (inside a worker) "Ask the parent whether …" | Send a proposal / question / blocker / result up to the parent |
101
+ | `worker_toggle` | "Turn workers off." | Route subagents to workers (on) or back to Anthropic (off) |
102
+ | `worker_ps` | "What are my workers doing?" | Running workers plus the last finished |
103
+ | `worker_replay` | "What did 3390 do?" | Transcript of one worker |
104
+ | `worker_say` | "Tell 3390 to skip the docs." | One message to a running worker, mid-run |
105
+ | `worker_resume` | "Have 3390 also update the docs." | Continue a finished worker, context intact |
106
+
107
+ ### Tool details
108
+
109
+ #### `worker_status` — the state page
110
+ No parameters. Workers on/off, active provider, provider list, today's tally
111
+ (started / ok / failed, hook denied / allowed / rerouted), running workers
112
+ with `◆N` waiting handoffs. Use it before delegating to see what routing
113
+ will choose.
114
+
115
+ #### `worker_providers` — manage providers
116
+ `action` (default `list`): **list** · **add** · **update** · **remove** ·
117
+ **use** · **enable** · **disable** · **test**. `name` is required for every
118
+ action but `list`.
119
+
120
+ - **add** — needs `name`, `model`, and `key_file` or `key`. `base_url` for an
121
+ Anthropic-protocol endpoint, or `upstream_url` with `protocol=openai`
122
+ (routed through the local proxy). Optional: `engine` (`claude`|`codex`),
123
+ `fast_model`, `context_window` (the meter; unset hides it unless the init
124
+ event announces one), `env`, `note`, `quota_probe`, `cost_tier` (1 cheapest
125
+ … 5), `tags` (`code`, `vision`, `image-gen`, `long-context`, `fast`,
126
+ `reasoning`). The first provider added also turns routing on and seeds the
127
+ classes.
128
+ - **update** — `cost_tier` and/or `tags`.
129
+ - **use** — make it active; unset classes follow it.
130
+ - **enable / disable** — in or out of auto-routing.
131
+ - **test** — one-word pong probe: latency, OK/FAILED.
132
+ - **remove** — drop it, and any classes pointing at it.
133
+
134
+ #### `worker_fallback` — the machine-wide switch
135
+ `action` (default `status`): **on** · **off** · **status**; `on` takes
136
+ `provider` (default: active). See the orchestrator section above for exactly
137
+ what it writes and restores.
138
+
139
+ #### `worker_classes` — map classes to providers
140
+ `action` (default `list`): **list** · **set** · **unset**. `set` takes
141
+ `class` plus either `target` (`<provider>` or `<provider>/fast`) or routing
142
+ constraints — `max_cost_tier` (1–5) and/or `require_tags` — with which
143
+ auto-routing picks any qualifying provider. `unset` frees a class back to
144
+ the active provider.
145
+
146
+ #### `worker_model` — model slots
147
+ `action` (default `get`): **get** — model ids of one provider (`provider`,
148
+ default active) or all when omitted; **set** — needs `model`, `slot` picks
149
+ `default` or `fast`. `[1m]` suffix = 1M window, derived from the id.
150
+
151
+ #### `worker_run` — start work
152
+ `prompt` (required — the task, self-contained). Optional: `chain`
153
+ (`draft,implement` or `draft,implement,review`), `class` (the nine below),
154
+ `label` (shown in `worker_ps` and the statusline — always give one), `cwd`,
155
+ `allowed_tools` (comma-separated allowlist), `mcp` (servers/sets the worker
156
+ may load). Returns the id at once; the run is a background task — the result
157
+ arrives later as a task notification (`result` plus a parsed `report`:
158
+ changed paths, ✓/✗ checks, open items). The orchestrator ends its turn
159
+ instead of waiting, and reviews the diff itself — workers never merge their
160
+ own work.
161
+
162
+ #### `worker_handoff` — from inside a worker
163
+ `kind` (required): `proposal` · `question` · `blocker` · `result`; `text`
164
+ (required, one paragraph); optional structured `data`. Lands in the parent's
165
+ inbox. Only works inside a worker — no sideways or downward path. Results
166
+ are sent automatically when you finish; send one yourself only for mid-run
167
+ findings.
168
+
169
+ #### `worker_toggle` — routing switch
170
+ `enabled` (required). `true`: the Agent-tool hook denies in-process
171
+ subagents and rewrites them to workers. `false`: they run on Anthropic
172
+ again. Registry kept either way.
173
+
174
+ #### `worker_ps` — the list
175
+ Optional `all`: `true` = every session's workers, default this session's.
176
+ Running workers show id, provider, age, turns, current tool; the last
177
+ finished is included.
178
+
179
+ #### `worker_replay` — one transcript
180
+ `id` (required), optional `tail` (last N rendered lines, 1–2000). Plain
181
+ text: tool calls, short outputs, result.
182
+
183
+ #### `worker_say` — talk to a running worker
184
+ `id`, `text` (both required). Lands on the worker's open stdin as a user
185
+ message, mid-run, without breaking its stream. Fails with an explanation
186
+ when the worker finished — then `worker_resume`.
187
+
188
+ #### `worker_resume` — continue a finished worker
189
+ `id`, `text` (both required). `claude --resume` on the same provider, same
190
+ session, context intact; returns the new worker id. Not for running workers.
191
+
192
+ ## Configure
193
+
194
+ Everything lives in the `workers` section of `~/.config/pai/config.json`
195
+ (`readWorkersSection` / `writeWorkersSection`, `src/workers/config.ts:619`);
196
+ every sentence and tool above writes it atomically — no hand-editing.
197
+
198
+ **Classes** (`WORKER_CLASSES`, `src/workers/config.ts:131`):
199
+
200
+ | Class | Work |
201
+ | --- | --- |
202
+ | `draft` · `plan` | spec writing, planning |
203
+ | `implement` · `complex` | code (get their own git worktree on `worker/<id>`) |
204
+ | `review` | reads the diff |
205
+ | `research` | web research (give it `WebSearch,WebFetch`) |
206
+ | `spotcheck` · `simple` | fast verification, small jobs |
207
+ | `image` | image generation |
208
+
209
+ A class maps to `<provider>`, `<provider>/fast`, or to constraints
210
+ (`maxCostTier` 1–5, `requireTags`, per-class `order`). Unset classes follow
211
+ the active provider; `active: "auto"` routes by `workers.routing.order`
212
+ instead.
213
+
214
+ **Provider fields** (registry entry, `parseProvider`,
215
+ `src/workers/config.ts:264`): protocol, base URL (or upstream URL for the
216
+ proxy), key file path, model slots (default / fast, `[1m]` suffix for the
217
+ 1M window), engine, `cost_tier`, `tags`, extra `env`, `context_window`.
218
+
219
+ **Defaults you inherit** (`src/workers/config.ts:209-226`):
220
+
221
+ | Area | Default |
222
+ | --- | --- |
223
+ | Routing | cooldown 30 min after a quota/rate failure, `retryOnQuota` true |
224
+ | Sub-worker tree | depth 2, 4 children per parent |
225
+ | Follow pane | font 13 pt, auto-exit 60 s |
226
+ | Logs | `~/.claude/logs/workers` |
227
+
228
+ **Environment variables:**
229
+
230
+ | Var | Effect |
231
+ | --- | --- |
232
+ | `PAI_WORKER=1` | carried by every headless spawn — marks the worker session, exempts it from orchestrator-only hooks (`src/workers/run-env.ts:47`) |
233
+ | `ALLOW_ANTHROPIC_AGENTS=1` | re-enables the Agent tool for one session |
234
+ | `ENABLE_TOOL_SEARCH=true` | set in interactive provider runs (`run-env.ts:49`) |
235
+ | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` | every provider spawn (`run-env.ts:45`) |
236
+ | `CLAUDE_SETTINGS_PATH` | dry-run target for `worker_fallback` |
237
+
238
+ ## How it works
239
+
240
+ One orchestrator, many workers, split made mechanical by four hooks:
241
+
242
+ | Hook | Enforces | Where |
243
+ | --- | --- | --- |
244
+ | PreToolUse (Agent) | every in-process subagent denied while providers are configured; the deny reason prints the `pai worker run` line to use instead | `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts`, deployed `${PAI_DIR}/Hooks/route-agents-to-worker.mjs` |
245
+ | PreToolUse (Edit/Write) | no code edits by the orchestrator inside a git work tree; `PAI_WORKER=1` sessions are exempt | `${PAI_DIR}/Hooks/route-edits-to-worker.mjs` (deployed config) |
246
+ | SessionStart (compact) | saved state replayed and the live worker list injected — a compacted orchestrator still sees every worker | `src/hooks/ts/session-start/post-compact-inject.ts`, `${PAI_DIR}/Hooks/post-compact-workers.mjs` |
247
+ | PreToolUse (TaskOutput) | blocking waits denied unless `block: false` — end the turn; the result arrives as a task notification | `${PAI_DIR}/Hooks/block-taskoutput-wait.mjs` |
248
+
249
+ Every routing decision lands in the routing ledger (`pai worker log`). A
250
+ chain `draft,implement[,review]` gives each stage its own worker — own id,
251
+ own follow pane, tree in the worker list; a failing stage stops the chain.
252
+ Writing classes work in their own git worktree on `worker/<id>`; merging or
253
+ discarding is a deliberate operator (or orchestrator) action.
254
+
255
+ The **statusline** carries the worker row (line 4):
256
+
257
+ ```
258
+ glm ▶1 #3390 unlabeled 30s · interactive ✓74 ✗4 today
259
+ ```
260
+
261
+ Segments: provider badge (`glm`; `workers` when mixed) · `▶N` this
262
+ terminal's running workers with a live pid · short id · label · age ·
263
+ current step · today's finished count. Full legend (`◆N` handoffs, `ctx`
264
+ meter, `↳` sub-workers): the *Status line* section of
265
+ [worker.md](worker.md).
266
+
267
+ What else follows the active provider — statusline plan windows,
268
+ daemon-side summarizer calls, compaction history, the translating proxy:
269
+ [provider-abstraction.md](provider-abstraction.md).
270
+
271
+ ### Code map
272
+
273
+ | What | Where |
274
+ | --- | --- |
275
+ | Provider registry, classes (`WORKER_CLASSES`) | `src/workers/config.ts:131` |
276
+ | Config read/write, `workers` section | `src/workers/config.ts:619` |
277
+ | Provider validation (`parseProvider`) | `src/workers/config.ts:264` |
278
+ | First-provider takeover (enabled + active + seeded classes) | `src/workers/providers.ts:51` (`addProvider`) |
279
+ | `providers use` (sets `workers.active`) | `src/workers/providers.ts:170` (`useProvider`) |
280
+ | `pai worker on/off` (`setWorkersEnabled`) | `src/workers/providers.ts:279` |
281
+ | Runner / chain / agent definitions | `src/workers/run.ts`, `src/workers/chain.ts`, `src/workers/agents.ts` |
282
+ | Spawn env — base URL, token, model pins | `src/workers/run-env.ts` |
283
+ | Machine-wide fallback (settings.json) | `src/workers/fallback.ts` |
284
+ | Shims (`glm`, `glm-run`, …) installer | `src/workers/install.ts` |
285
+ | Worktrees and merge | `src/workers/worktree.ts` |
286
+ | CLI surface (`run`, `ps`, `follow`, `replay`, `merge`, `discard`) | `src/cli/commands/worker/index.ts` |
287
+ | Providers / classes CLI | `src/cli/commands/worker/providers.ts:124,334` |
288
+ | Model slots CLI (`pai worker model`) | `src/cli/commands/worker/model.ts` |
289
+ | Statusline worker row | `src/workers/render.ts`, `statusline-command.sh` (line 4), built `${PAI_DIR}/worker-status-line.mjs` |
290
+ | Context window from model id (`[1m]`) | `src/utils/model-window.ts` |
291
+ | Agent-tool gate hook (repo) | `src/hooks/ts/pre-tool-use/route-agents-to-worker.ts` |
292
+ | Post-compact reinjection (repo) | `src/hooks/ts/session-start/post-compact-inject.ts`, `src/hooks/ts/lib/context-fill.ts` |
293
+ | Worker-session exemption | `src/hooks/ts/lib/worker-session.ts` |
294
+ | Deployed hooks (config, not repo) | `${PAI_DIR}/Hooks/route-edits-to-worker.mjs`, `block-taskoutput-wait.mjs`, `post-compact-workers.mjs` — registered in `${PAI_DIR}/settings.json` |
295
+ | MCP tool surface — all 12 `worker_*` tools | `src/daemon-mcp/index.ts:804-1334` (`worker_model` handler: `src/daemon-mcp/tools/worker-model.ts`) |
296
+ | Neighbour docs | [provider-independence.md](provider-independence.md), [provider-abstraction.md](provider-abstraction.md), [worker.md](worker.md), [commands/worker.md](commands/worker.md) |
297
+
298
+ ## CLI fallback
299
+
300
+ For scripts and terminals outside sessions — the twin of a tool above; flags
301
+ live in [commands/worker.md](commands/worker.md). (`worker_status` has no
302
+ single twin; `pai worker ps` + `pai worker log` show the same facts.)
303
+
304
+ | Command | Twin of |
305
+ | --- | --- |
306
+ | `pai worker run [claude-args…]` | `worker_run` — and the only place a one-call `--provider` override exists |
307
+ | `pai worker ps [--all]` | `worker_ps` |
308
+ | `pai worker replay <id> [--tail n]` | `worker_replay` |
309
+ | `pai worker say <id> <text>` | `worker_say` |
310
+ | `pai worker resume <id> <text>` | `worker_resume` |
311
+ | `pai worker handoff <json>` | `worker_handoff` |
312
+ | `pai worker providers <list\|add\|update\|remove\|use\|enable\|disable\|test>` | `worker_providers` — keys via `--key-file`, never pasted |
313
+ | `pai worker classes <list\|set\|unset>` | `worker_classes` |
314
+ | `pai worker model [what] [model]` | `worker_model` |
315
+ | `pai worker on` / `pai worker off` | `worker_toggle` |
316
+ | `pai worker fallback <on [provider]\|off\|status>` | `worker_fallback` |
317
+
318
+ Terminal-only (no tool equivalent):
319
+
320
+ | Command | What it does |
321
+ | --- | --- |
322
+ | `pai worker watch` | `ps` refreshed every 2 seconds |
323
+ | `pai worker follow [id]` / `pai worker pane [id]` | live transcript pane for one worker or this session's |
324
+ | `pai worker log [all\|tail\|<id>]` | the routing ledger, or one worker's raw event stream |
325
+ | `pai worker merge <id>` / `pai worker discard <id>` | land a worker's worktree branch in the original checkout, or drop it entirely |
326
+ | `pai worker controls <id> you\|me` | hand the desktop controls (clickr) to a worker, or take them back |
327
+ | `pai worker status-line [term] [cwd]` | one-line worker summary for a status bar (called by `statusline-command.sh`) |
328
+ | `pai worker proxy [stop]` | the local Anthropic↔OpenAI proxy |
329
+ | `pai worker mcp [list]` | MCP servers/sets workers may load |
330
+ | `pai worker install` | migration: Agent hook in settings.json, `~/.local/bin` shims (`glm`, …), old script cleanup |
@@ -0,0 +1,75 @@
1
+ # Provider Independence
2
+
3
+ How to free yourself from Claude — so a maxed-out subscription stops being your problem. How to run any cloud model and any local model inside your Claude Code (or OpenCode) harness. How to run models in parallel, each doing what it is best at — all of it purely local, no need for yet another Hermes-or-whatever harness. How to have workers run by default, visibly, each with its own chat you can talk to.
4
+
5
+ My journey into PAI and AIBroker started nine months ago, when OpenClawd burned through all my credits in four hours. I decided then and there that there had to be a better way: instead of buying API tokens, use the Claude Max subscription I was already paying for.
6
+
7
+ Nine months later, I hit the next wall: maxing out Claude Max itself. The next level of independence was required. I built it — in one day. Here is what happened.
8
+
9
+ I started my Thursday at 5am, which is normal, and at about 80%, which is not.
10
+
11
+ That 80% is not motivation — although that was certainly higher. It is the weekly usage of my Claude Max 200 subscription.
12
+
13
+ With two days to go until the Saturday 8am reset, I was getting worried. Last week I had already hit 100% — despite all my efficiency gains, including inventing my own "Agentish" language.
14
+
15
+ By 9am I was above 90%. There was no way I would make it to the reset.
16
+
17
+ So I decided to solve this problem instead of waiting it out. By 10pm I was at 97% — a number that has not moved since noon, because since noon, almost nothing has run on Claude.
18
+
19
+ This is how I did it.
20
+
21
+ I made myself independent of Claude and Fable. They are great — but I hate being locked in, and I suspect most of us do. I want to decide which model I use: Claude, GLM, Grok, whatever. I need to abstract away from that choice.
22
+
23
+ Fable was — and is — great, but it burns tokens. And Opus on its own is so unbelievably dumb that I had taken to letting Fable do everything, even forcing it to not use agents — burning tokens even faster.
24
+
25
+ So the requirements were:
26
+
27
+ 1. **Provider independence.** Switch to any model at any time — not only for agents, but for the main orchestrator session too.
28
+ 2. **Cloud and local.** Any cloud model, and local models.
29
+ 3. **Visibility.** Really see what each worker is doing — not Claude Code's click-into-a-worker-and-make-sense-of-it, but every worker in a side pane, automatically.
30
+ 4. **Direct chat.** Talk to any worker while it runs.
31
+ 5. **Hardening.** Each worker in its own worktree, so parallel work cannot destroy itself.
32
+ 6. **Forced orchestration.** Not just ask the orchestrator to delegate — deterministically enforce that it spawns workers.
33
+ 7. **Cost routing.** Automatically pick cheaper workers for simpler tasks.
34
+ 8. **Keep the harness.** Stay on Claude Code (I could have switched to OpenCode — turns out it was not even necessary).
35
+
36
+ The whole implementation cost 7 of my last 10 percentage points of Claude Max 200. Then I switched to GLM for everything else. And what can I say — it works beautifully.
37
+
38
+ ---
39
+
40
+ The rest of this page is the operator's guide: switching providers, starting sessions, and the three copy-paste steps to your own setup.
41
+
42
+ > My AI budget hit 91% before lunch. So I changed one rule: the expensive
43
+ > assistant keeps the thinking, a cheaper crew does the building. The feature
44
+ > shipped anyway.
45
+ >
46
+ > Your AI bill is not a fact of nature. It is a design decision.
47
+
48
+ Your assistant runs its helper crew on whichever AI provider you choose —
49
+ and so can the session itself.
50
+
51
+ ## Switching
52
+
53
+ - **The crew.** Once, at the start: "Add a worker provider named <provider> — here
54
+ is the key." Then: "Use <provider> for the workers from now on."
55
+ - **The session itself.** Start it with the `<provider>` command — the shim for
56
+ `pai worker run` — or just pick a project with `pai`: the picker follows
57
+ the active provider too, so the whole session, you included, runs on it.
58
+
59
+ Swapping back — or to any other provider — is one sentence for the crew, and
60
+ starting `claude` again for the session.
61
+
62
+ ## A day with the crew
63
+
64
+ - "Fix the login timeout bug." — a worker named *fix login timeout* starts; the status line shows its name and current step, live.
65
+ - "What are my workers doing?" — a short list, by name.
66
+ - "Tell fix login timeout to also check the retry path." — lands mid-run; the worker adapts.
67
+ - "Show me what it changed." — the diff comes back for your review.
68
+ - "Stop using workers for now." — the crew stands down.
69
+
70
+ ![The worker row live in the status line.](images/workers.png)
71
+
72
+ ## Further
73
+
74
+ - [How it works — full tool reference](provider-independence-details.md)
75
+ - [The provider layer, in depth](provider-abstraction.md)