diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,173 @@
 
 ## [Unreleased]
 
+## [baikai 0.8.0.0] - 2026-10-10
+
+### Added
+
+- Catalog: `anthropic_claude_haiku_5_5` (Claude Haiku 5.5, released
+  2026-10-07). It uses Messages with adaptive thinking, drops sampling options,
+  forwards forced tool choice, and has a 1,000,000-token context and
+  128,000-token output limit. Prompts over 100,000 tokens, counting cache reads
+  and writes, are priced at $0.50/$0.05/$0.625/$2.50 per million input, cache
+  read, cache write and output tokens instead of $0.10/$0.01/$0.125/$0.50, and
+  one-hour cache writes at $1 instead of $0.20. Live text and tool cases
+  passed on 2026-10-10, and a live request confirmed forced tool choice with
+  adaptive thinking.
+
+### Changed (breaking)
+
+- `Baikai.Model.InputPriceTier` gains a third field, `longCacheWriteCost ::
+  Maybe Rational`: the one-hour cache-write rate for requests that cross that
+  tier. `resolveRates` now takes the long rate from the selected tier rather
+  than overriding every tier with the policy-level rate, and validation rejects
+  a policy whose policy-level long rate leaves any tier without one. Tier JSON
+  without the key decodes as `Nothing`. __Breaking__: positional constructions
+  of `InputPriceTier` need the new argument.
+
+### Changed
+
+- Catalog: the 2026-10-10 model refresh lowers `anthropic_claude_sonnet_5_5`'s
+  cache-read rate from $0.20 to $0.10 per million tokens, following Anthropic's
+  2026-10-07 price cut, and corrects `anthropic_claude_sonnet_4_5`'s context
+  window from 1,000,000 to 200,000 tokens (its 1M beta ended on 2026-04-30).
+  Claude Haiku 5.5 was curated separately (see Added). No OpenAI model was
+  added.
+
+- Documentation: the 2026-10-02 model refresh found no new OpenAI or Anthropic
+  models; the catalog is unchanged. `docs/user/models-and-providers.md` notes
+  Anthropic's retirement of Claude Sonnet 4.5 on 2026-11-30 (deprecation of
+  `anthropic_claude_sonnet_4_5` is planned in plan 89 for a future release;
+  it remains in 0.8.0.0),
+  why the access-gated Claude Mythos models are not curated, and how GPT-6
+  Astra's new ultrafast service tier is costed.
+
+### Fixed
+
+- The shared batch subprocess scope now owns POSIX process groups, joins pipe
+  readers, and synchronously reaps direct children before acknowledging
+  cancellation. Executable-version probes use the same scope. Darwin/Linux
+  coverage excludes processes that deliberately escape the group and adopted
+  descendant reaping; Windows retains direct-child cleanup. The provider fixes
+  ship in `baikai-claude 0.7.1.1` and `baikai-openai 0.7.1.1`; see
+  [plan 90](docs/plans/90-terminate-owned-batch-cli-process-groups-before-acknowledging-cancellation.md)
+  and [BUG-1](docs/bug-reports/batch-cli-cancellation-leaves-child-alive.md).
+
+- `baikai`'s source distribution now ships `data/models/*.json` and the test
+  suite's fixtures under `test/fixtures/`. Without them, running the suite from
+  the Hackage tarball failed 27 of 794 tests on missing files: the fixtures
+  themselves, plus the catalog round-trip test, whose `baikai-gen-models` run
+  had no model data to read.
+
+## [baikai-claude 0.7.1.1] - 2026-10-10
+
+### Fixed
+
+- Batch `claude -p` calls stop owned POSIX process-group members and join pipe
+  readers before acknowledging cancellation, including active synthetic streams
+  and evidence probes. Preserves the original asynchronous exception. Resolves
+  [BUG-1](docs/bug-reports/batch-cli-cancellation-leaves-child-alive.md).
+
+### Changed
+
+- Requires `baikai ^>=0.8.0` for shared subprocess ownership and the new pricing
+  tier field. No provider API change. Haiku 5.5 request contracts are covered
+  by offline tests.
+
+## [baikai-openai 0.7.1.1] - 2026-10-10
+
+### Fixed
+
+- Batch `codex exec` calls stop owned POSIX process-group members and join pipe
+  readers before acknowledging cancellation, including active synthetic streams,
+  repeated cancellation, and evidence probes. The temporary schema survives
+  until subprocess cleanup finishes. Resolves
+  [BUG-1](docs/bug-reports/batch-cli-cancellation-leaves-child-alive.md).
+
+### Changed
+
+- Requires `baikai ^>=0.8.0` for shared subprocess ownership and the new pricing
+  tier field. No provider API change.
+
+## [baikai-trace-otel 0.4.0.2] - 2026-10-10
+
+### Changed
+
+- Requires `baikai ^>=0.8.0` for compatibility with the core pricing tier
+  change. No package API change.
+
+## [baikai-effectful 0.4.0.3] - 2026-10-10
+
+### Changed
+
+- Requires `baikai ^>=0.8.0` for compatibility with the core pricing tier
+  change. No package API change.
+
+## [baikai-kit 0.4.0.1] - 2026-10-10
+
+### Changed
+
+- Requires `baikai ^>=0.8.0` for compatibility with the core pricing tier
+  change. No package API change.
+
+## [baikai-agent 0.2.0.2] - 2026-10-10
+
+### Changed
+
+- Requires `baikai ^>=0.8.0` for compatibility with the core pricing tier
+  change. No package API change.
+- Raises the provider bounds to `baikai-claude ^>=0.7.1.1` and
+  `baikai-openai ^>=0.7.1.1`, so installing the tool selects the fixed batch
+  adapters. Binary regressions use the shared process fixture and cover both
+  cancellation and a 1MiB output flood.
+
+## [baikai-kit 0.4.0.0] - 2026-10-02
+
+### Added
+
+- Per-item `visibility` (`tool-only` by default, or `shared`) for manifest
+  versions 1 and 2, with `kit install --shared`, `--tool-only`, and
+  `--accept-shared-codex`. Shared Claude skills and agents use tracked links;
+  tool-only Codex skills use tracked disabled entries in `config.toml`.
+- `codexSessionArgs`, required in the `extraArgs` of a consumer's own Codex
+  launches to re-enable its tool-only skills. This is the one launcher step
+  beyond a dependency bump; the standard `kitConfig`/`kitCommandParser`/`runKit`
+  integration otherwise compiles unchanged. Codex custom agents cannot be
+  isolated and require explicit acceptance, or shared visibility.
+- Requested and effective visibility in status (table and JSON), the
+  `visibility-broken` condition, and a migration note for legacy shared Codex
+  skills. Update repairs visibility even for items skipped for local edits;
+  uninstall removes only owned links and config entries.
+
+### Changed (breaking)
+
+- `SkillEntry` and `AgentEntry` gain `visibility`; `SidecarMeta` gains
+  `visibility`, `visibilitySource`, `sharedLinks`, and `codexDisabledSkills`;
+  `newSidecarMeta` takes these values. `RemovalOutcome` gains `linksRemoved`
+  and `configEntriesRemoved`; `StatusRow` gains `requestedVisibility` and
+  `effectiveVisibility`; `KitConfig` gains `confirmSharedCodex` (default
+  `Nothing` in `kitConfig`).
+- `KitInstall` gains `InstallOptions`; `installItem` and `installFrom` take
+  it as their last argument. Use `defaultInstallOptions` to follow the
+  manifest. Explicit install flags persist across updates, while new
+  manifest-driven installs follow updated defaults. Legacy placement is kept.
+- JSON adds list `visibility`, status `requestedVisibility` and
+  `effectiveVisibility`, and the new condition value. `formatVersion` stays
+  1 because all additions preserve existing keys and their meaning.
+
+### Fixed
+
+- Codex installs refuse destinations without this tool's sidecar instead of
+  overwriting user or other-tool assets. Uninstall also preserves foreign
+  Codex assets. Shared Claude names are checked before any provider write.
+- Codex config edits preserve UTF-8 text, comments and permissions, refuse
+  symlinks and invalid/conflicting config, and verify the semantic change
+  before atomic rename. Reused user-owned disabled entries survive uninstall.
+  Shared visibility refuses a user-owned entry that would keep the skill hidden.
+- The source distribution ships the test suite's manifest fixtures and JSON
+  goldens, so the test suite passes when run from the Hackage tarball. Since
+  0.3.0.0 it had failed there, because those files were missing.
+
 ## [baikai 0.7.2.0] - 2026-09-30
 
 ### Added
diff --git a/baikai.cabal b/baikai.cabal
--- a/baikai.cabal
+++ b/baikai.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.4
 name: baikai
-version: 0.7.2.0
+version: 0.8.0.0
 synopsis: Unified Haskell interface for multiple AI providers
 description:
   baikai provides a unified, provider-agnostic Haskell interface for working
@@ -18,6 +18,10 @@
 build-type: Simple
 tested-with: ghc ==9.12.4
 extra-doc-files: CHANGELOG.md
+extra-source-files:
+  data/models/*.json
+  test/fixtures/*.json
+  test/fixtures/*.jsonl
 
 common common-options
   ghc-options:
@@ -85,6 +89,7 @@
     Baikai.Prelude
     Baikai.Provider
     Baikai.Provider.Cli.Internal
+    Baikai.Provider.Cli.Process.Internal
     Baikai.Provider.Internal.StreamWorker
     Baikai.Provider.Registry
     Baikai.Provider.Transport.Classify
@@ -138,6 +143,10 @@
     unliftio-core ^>=0.2,
     vector ^>=0.13,
 
+  if !os(windows)
+    build-depends: unix ^>=2.8
+    c-sources: cbits/cli_group_anchor.c
+
 executable baikai-gen-models
   import: common-options
   hs-source-dirs: gen
@@ -191,11 +200,14 @@
     gen
 
   main-is: Main.hs
+  ghc-options: -threaded
   other-modules:
     AgentAssetsSpec
     AgentSpec
     CatalogSpec
     CliInternalSpec
+    CliProcessFixture
+    CliProcessSpec
     ContextSpec
     CostSpec
     EmbeddingSpec
@@ -248,3 +260,6 @@
     time,
     tls,
     vector,
+
+  if !os(windows)
+    build-depends: unix ^>=2.8
diff --git a/cbits/cli_group_anchor.c b/cbits/cli_group_anchor.c
new file mode 100644
--- /dev/null
+++ b/cbits/cli_group_anchor.c
@@ -0,0 +1,49 @@
+/* Reserve a process group after its CLI leader has been reaped. The parent
+ * establishes membership before returning; it must not reap the anchor until
+ * its final group signal/observation. A dead, unreaped anchor reserves it too.
+ * The child runs only async-signal-safe operations: no Haskell or allocation. */
+#include <errno.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#ifdef __linux__
+#include <sys/syscall.h>
+#endif
+
+int baikai_cli_group_anchor(int group) {
+    long limit = sysconf(_SC_OPEN_MAX);
+    if (limit < 0) { errno = EINVAL; return -1; }
+    pid_t child = fork();
+    if (child < 0) return -1;
+    if (child == 0) {
+        struct sigaction ignore;
+        sigset_t empty;
+        ignore.sa_handler = SIG_IGN;
+        ignore.sa_flags = 0;
+        sigemptyset(&ignore.sa_mask);
+        sigemptyset(&empty);
+        if (sigaction(SIGINT, &ignore, 0) < 0 ||
+            sigaction(SIGTERM, &ignore, 0) < 0 ||
+            sigprocmask(SIG_SETMASK, &empty, 0) < 0) _exit(127);
+        /* Inherited descriptors are never used and are closed before parking.
+         * Cancellation may kill this child while it is closing descriptors;
+         * its unreaped membership still protects the group's identity. */
+#ifdef __linux__
+#ifdef SYS_close_range
+        if (syscall(SYS_close_range, 0U, ~0U, 0U) < 0)
+#endif
+#endif
+            for (long fd = 0; fd < limit; ++fd) close((int)fd);
+        for (;;) pause();
+    }
+    /* The anchor never execs, so the parent can set its process group here.
+     * The CLI leader is still unreaped and no callback yet owns its handle. */
+    if (setpgid(child, (pid_t)group) < 0) {
+        int saved = errno;
+        kill(child, SIGKILL);
+        while (waitpid(child, 0, 0) < 0 && errno == EINTR) {}
+        errno = saved; return -1;
+    }
+    return (int)child;
+}
diff --git a/data/models/anthropic.json b/data/models/anthropic.json
new file mode 100644
--- /dev/null
+++ b/data/models/anthropic.json
@@ -0,0 +1,325 @@
+{
+  "provider": "anthropic",
+  "baseUrl": "https://api.anthropic.com",
+  "api": "anthropic-messages",
+  "compat": "auto",
+  "models": [
+    {
+      "id": "claude-fable-5",
+      "name": "Claude Fable 5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 10.0,
+        "output": 50.0,
+        "cacheRead": 1.0,
+        "cacheWrite": 12.5
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-fable-5-1",
+      "name": "Claude Fable 5.1",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 10.0,
+        "output": 50.0,
+        "cacheRead": 0.25,
+        "cacheWrite": 12.5
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "pricingPolicy": {"inputTiers": [], "longCacheWriteCost": 20.0},
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": false
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-haiku-4-5",
+      "name": "Claude Haiku 4.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 1.0,
+        "output": 5.0,
+        "cacheRead": 0.1,
+        "cacheWrite": 1.25
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 64000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "budget",
+        "supportsSamplingParameters": true,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-haiku-5-5",
+      "name": "Claude Haiku 5.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.1,
+        "output": 0.5,
+        "cacheRead": 0.01,
+        "cacheWrite": 0.125
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "pricingPolicy": {"inputTiers": [{"inputAbove": 100000, "rates": {"input": 0.5, "output": 2.5, "cacheRead": 0.05, "cacheWrite": 0.625}, "longCacheWriteCost": 1.0}], "longCacheWriteCost": 0.2},
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-opus-4-5",
+      "name": "Claude Opus 4.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 5.0,
+        "output": 25.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 6.25
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 64000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "budget",
+        "supportsSamplingParameters": true,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-opus-4-6",
+      "name": "Claude Opus 4.6",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 5.0,
+        "output": 25.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 6.25
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": true,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-opus-4-7",
+      "name": "Claude Opus 4.7",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 5.0,
+        "output": 25.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 6.25
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-opus-4-8",
+      "name": "Claude Opus 4.8",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 5.0,
+        "output": 25.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 6.25
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "fastModeCost": {"input": 10.0, "output": 50.0, "cacheRead": 1.0, "cacheWrite": 12.5},
+      "pricingPolicy": {"inputTiers": [], "longCacheWriteCost": 10.0},
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": true,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-opus-5",
+      "name": "Claude Opus 5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 5.0,
+        "output": 25.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 6.25
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "fastModeCost": {"input": 10.0, "output": 50.0, "cacheRead": 1.0, "cacheWrite": 12.5},
+      "pricingPolicy": {"inputTiers": [], "longCacheWriteCost": 10.0},
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": true,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-opus-5-5",
+      "name": "Claude Opus 5.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 4.0,
+        "output": 20.0,
+        "cacheRead": 0.2,
+        "cacheWrite": 5.0
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "fastModeCost": {"input": 8.0, "output": 40.0, "cacheRead": 0.4, "cacheWrite": 10.0},
+      "pricingPolicy": {"inputTiers": [], "longCacheWriteCost": 8.0},
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": true,
+        "supportsForcedToolChoice": false
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-sonnet-4-5",
+      "name": "Claude Sonnet 4.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 3.0,
+        "output": 15.0,
+        "cacheRead": 0.3,
+        "cacheWrite": 3.75
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 64000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "budget",
+        "supportsSamplingParameters": true,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-sonnet-4-6",
+      "name": "Claude Sonnet 4.6",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 3.0,
+        "output": 15.0,
+        "cacheRead": 0.3,
+        "cacheWrite": 3.75
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": true,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-sonnet-5",
+      "name": "Claude Sonnet 5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 10.0,
+        "cacheRead": 0.2,
+        "cacheWrite": 2.5
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": true
+      },
+      "enabled": true
+    },
+    {
+      "id": "claude-sonnet-5-5",
+      "name": "Claude Sonnet 5.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 10.0,
+        "cacheRead": 0.1,
+        "cacheWrite": 2.5
+      },
+      "contextWindow": 1000000,
+      "maxOutputTokens": 128000,
+      "pricingPolicy": {"inputTiers": [], "longCacheWriteCost": 4.0},
+      "compat": {
+        "kind": "anthropic-messages",
+        "thinkingStyle": "adaptive",
+        "supportsSamplingParameters": false,
+        "supportsFastMode": false,
+        "supportsForcedToolChoice": false
+      },
+      "enabled": true
+    }
+  ]
+}
diff --git a/data/models/deepseek.json b/data/models/deepseek.json
new file mode 100644
--- /dev/null
+++ b/data/models/deepseek.json
@@ -0,0 +1,38 @@
+{
+  "provider": "deepseek",
+  "baseUrl": "https://api.deepseek.com",
+  "api": "openai-chat-completions",
+  "compat": "auto",
+  "models": [
+    {
+      "id": "deepseek-chat",
+      "name": "DeepSeek Chat",
+      "reasoning": false,
+      "input": ["text"],
+      "cost": {
+        "input": 0.27,
+        "output": 1.1,
+        "cacheRead": 0.07,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 64000,
+      "maxOutputTokens": 8192,
+      "enabled": true
+    },
+    {
+      "id": "deepseek-reasoner",
+      "name": "DeepSeek Reasoner",
+      "reasoning": true,
+      "input": ["text"],
+      "cost": {
+        "input": 0.55,
+        "output": 2.19,
+        "cacheRead": 0.14,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 64000,
+      "maxOutputTokens": 8192,
+      "enabled": true
+    }
+  ]
+}
diff --git a/data/models/openai.json b/data/models/openai.json
new file mode 100644
--- /dev/null
+++ b/data/models/openai.json
@@ -0,0 +1,434 @@
+{
+  "provider": "openai",
+  "baseUrl": "https://api.openai.com",
+  "api": "openai-chat-completions",
+  "compat": "auto",
+  "models": [
+    {
+      "id": "gpt-4.1",
+      "name": "GPT-4.1",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 8.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 1047576,
+      "maxOutputTokens": 32768,
+      "enabled": true
+    },
+    {
+      "id": "gpt-4.1-mini",
+      "name": "GPT-4.1 mini",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.4,
+        "output": 1.6,
+        "cacheRead": 0.1,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 1047576,
+      "maxOutputTokens": 32768,
+      "enabled": true
+    },
+    {
+      "id": "gpt-4.1-nano",
+      "name": "GPT-4.1 nano",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.1,
+        "output": 0.4,
+        "cacheRead": 0.025,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 1047576,
+      "maxOutputTokens": 32768,
+      "enabled": true
+    },
+    {
+      "id": "gpt-4o",
+      "name": "GPT-4o",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.5,
+        "output": 10.0,
+        "cacheRead": 1.25,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 128000,
+      "maxOutputTokens": 16384,
+      "enabled": true
+    },
+    {
+      "id": "gpt-4o-mini",
+      "name": "GPT-4o mini",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.15,
+        "output": 0.6,
+        "cacheRead": 0.075,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 128000,
+      "maxOutputTokens": 16384,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5",
+      "name": "GPT-5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 1.25,
+        "output": 10.0,
+        "cacheRead": 0.125,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5-mini",
+      "name": "GPT-5 Mini",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.25,
+        "output": 2.0,
+        "cacheRead": 0.025,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5-nano",
+      "name": "GPT-5 Nano",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.05,
+        "output": 0.4,
+        "cacheRead": 0.005,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.1",
+      "name": "GPT-5.1",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 1.25,
+        "output": 10.0,
+        "cacheRead": 0.125,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.2",
+      "name": "GPT-5.2",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 1.75,
+        "output": 14.0,
+        "cacheRead": 0.175,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.4",
+      "name": "GPT-5.4",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.5,
+        "output": 15.0,
+        "cacheRead": 0.25,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.4-mini",
+      "name": "GPT-5.4 mini",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.75,
+        "output": 4.5,
+        "cacheRead": 0.075,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.4-nano",
+      "name": "GPT-5.4 nano",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.2,
+        "output": 1.25,
+        "cacheRead": 0.02,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 400000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.5",
+      "name": "GPT-5.5",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 5.0,
+        "output": 30.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.6",
+      "name": "GPT-5.6",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 4.0,
+        "output": 20.0,
+        "cacheRead": 0.4,
+        "cacheWrite": 5.0
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.6-luna",
+      "name": "GPT-5.6 Luna",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.2,
+        "output": 1.2,
+        "cacheRead": 0.02,
+        "cacheWrite": 0.25
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.6-sol",
+      "name": "GPT-5.6 Sol",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 4.0,
+        "output": 20.0,
+        "cacheRead": 0.4,
+        "cacheWrite": 5.0
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-5.6-terra",
+      "name": "GPT-5.6 Terra",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 12.0,
+        "cacheRead": 0.2,
+        "cacheWrite": 2.5
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "enabled": true
+    },
+    {
+      "id": "gpt-6-astra",
+      "name": "GPT-6 Astra",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 10.0,
+        "output": 50.0,
+        "cacheRead": 1.0,
+        "cacheWrite": 12.5
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "api": "openai-responses",
+      "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 20.0, "output": 75.0, "cacheRead": 2.0, "cacheWrite": 25.0}}]},
+      "compat": {
+        "kind": "openai-responses",
+        "supportsSamplingParameters": false,
+        "supportsLongCacheRetention": false,
+        "supportsPromptCacheOptions": true,
+        "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"]
+      },
+      "enabled": true
+    },
+    {
+      "id": "gpt-6-luna",
+      "name": "GPT-6 Luna",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.1,
+        "output": 0.5,
+        "cacheRead": 0.01,
+        "cacheWrite": 0.125
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "api": "openai-responses",
+      "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 0.2, "output": 0.75, "cacheRead": 0.02, "cacheWrite": 0.25}}]},
+      "compat": {
+        "kind": "openai-responses",
+        "supportsSamplingParameters": false,
+        "supportsLongCacheRetention": false,
+        "supportsPromptCacheOptions": true,
+        "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"]
+      },
+      "enabled": true
+    },
+    {
+      "id": "gpt-6-sol",
+      "name": "GPT-6 Sol",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 10.0,
+        "cacheRead": 0.2,
+        "cacheWrite": 2.5
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "api": "openai-responses",
+      "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 4.0, "output": 15.0, "cacheRead": 0.4, "cacheWrite": 5.0}}]},
+      "compat": {
+        "kind": "openai-responses",
+        "supportsSamplingParameters": false,
+        "supportsLongCacheRetention": false,
+        "supportsPromptCacheOptions": true,
+        "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"]
+      },
+      "enabled": true
+    },
+    {
+      "id": "gpt-6.1-sol",
+      "name": "GPT-6.1 Sol",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 10.0,
+        "cacheRead": 0.1,
+        "cacheWrite": 2.5
+      },
+      "contextWindow": 1050000,
+      "maxOutputTokens": 128000,
+      "api": "openai-responses",
+      "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 4.0, "output": 15.0, "cacheRead": 0.2, "cacheWrite": 5.0}}]},
+      "compat": {
+        "kind": "openai-responses",
+        "supportsSamplingParameters": false,
+        "supportsLongCacheRetention": false,
+        "supportsPromptCacheOptions": true,
+        "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"]
+      },
+      "enabled": true
+    },
+    {
+      "id": "o1",
+      "name": "o1",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 15.0,
+        "output": 60.0,
+        "cacheRead": 7.5,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 100000,
+      "enabled": true
+    },
+    {
+      "id": "o3",
+      "name": "o3",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 2.0,
+        "output": 8.0,
+        "cacheRead": 0.5,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 100000,
+      "enabled": true
+    },
+    {
+      "id": "o3-mini",
+      "name": "o3-mini",
+      "reasoning": true,
+      "input": ["text"],
+      "cost": {
+        "input": 1.1,
+        "output": 4.4,
+        "cacheRead": 0.55,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 100000,
+      "enabled": true
+    },
+    {
+      "id": "o4-mini",
+      "name": "o4-mini",
+      "reasoning": true,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 1.1,
+        "output": 4.4,
+        "cacheRead": 0.275,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 100000,
+      "enabled": true
+    }
+  ]
+}
diff --git a/data/models/openrouter.json b/data/models/openrouter.json
new file mode 100644
--- /dev/null
+++ b/data/models/openrouter.json
@@ -0,0 +1,38 @@
+{
+  "provider": "openrouter",
+  "baseUrl": "https://openrouter.ai/api",
+  "api": "openai-chat-completions",
+  "compat": "auto",
+  "models": [
+    {
+      "id": "openai/gpt-4o-mini",
+      "name": "GPT-4o mini via OpenRouter",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 0.15,
+        "output": 0.6,
+        "cacheRead": 0.075,
+        "cacheWrite": 0.0
+      },
+      "contextWindow": 128000,
+      "maxOutputTokens": 16384,
+      "enabled": true
+    },
+    {
+      "id": "anthropic/claude-sonnet-4",
+      "name": "Claude Sonnet 4 via OpenRouter",
+      "reasoning": false,
+      "input": ["text", "image"],
+      "cost": {
+        "input": 3.0,
+        "output": 15.0,
+        "cacheRead": 0.3,
+        "cacheWrite": 3.75
+      },
+      "contextWindow": 200000,
+      "maxOutputTokens": 8192,
+      "enabled": true
+    }
+  ]
+}
diff --git a/fetch/FetchModelsCore.hs b/fetch/FetchModelsCore.hs
--- a/fetch/FetchModelsCore.hs
+++ b/fetch/FetchModelsCore.hs
@@ -330,6 +330,15 @@
 -- generator refuses an @anthropic-messages@ entry that reaches it
 -- without a @compat@ block, so a hand edit cannot quietly drop one
 -- back to host auto-detection.
+--
+-- 2026-10-02: @claude-mythos-5@ and @claude-mythos-5-1@ are deliberately
+-- absent. Anthropic offers them only to approved Project Glasswing
+-- customers, so no general key can prove them live, and models.dev omits
+-- both from its @anthropic@ provider. Mythos 5.1 shares Fable 5.1's API
+-- facts and standard prices; callers with access can record-update
+-- @Baikai.Models.Generated.anthropic_claude_fable_5_1@ with the Mythos id.
+-- https://platform.claude.com/docs/en/models/fable-5-1/migration-guide
+-- https://platform.claude.com/docs/en/about-claude/pricing
 anthropicInclude :: Map Text AnthropicGenerationFacts
 anthropicInclude =
   Map.fromList
@@ -376,6 +385,14 @@
       ("claude-sonnet-4-5", budgetWithSampling),
       -- 2026-08-27: budget shape, sampling parameters accepted — same source.
       ("claude-haiku-4-5", budgetWithSampling),
+      -- 2026-10-10: adaptive thinking on by default, and manual budget
+      -- thinking returns 400; nondefault sampling returns 400. Forced tool
+      -- choice is accepted, and that response starts with the tool call
+      -- and has no thinking block. No fast mode.
+      -- https://platform.claude.com/docs/en/models/haiku-5-5/overview
+      -- https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide
+      -- https://platform.claude.com/docs/en/build-with-claude/effort
+      ("claude-haiku-5-5", adaptiveNoSampling),
       -- 2026-08-27: adaptive-only, sampling parameters rejected — same source.
       ("claude-fable-5", adaptiveNoSampling),
       -- 2026-09-07: always-on adaptive thinking; omit sampling parameters.
@@ -662,25 +679,31 @@
 -- https://platform.claude.com/docs/en/models/fable-5-1/overview
 -- https://platform.claude.com/docs/en/models/opus-5-5/overview
 -- https://platform.claude.com/docs/en/build-with-claude/fast-mode
+-- Claude Haiku 5.5 verified 2026-10-10: prompts over 100,000 tokens,
+-- counting cache reads and writes, pay the higher rates on every category.
+-- https://platform.claude.com/docs/en/about-claude/pricing#long-context-pricing
 pricingPolicies :: Map (Text, Text) Model.PricingPolicy
 pricingPolicies =
   Map.fromList
     [ (("anthropic", "claude-opus-5-5"), Model.PricingPolicy [] (Just 8)),
       (("anthropic", "claude-opus-5"), Model.PricingPolicy [] (Just 10)),
       (("anthropic", "claude-opus-4-8"), Model.PricingPolicy [] (Just 10)),
-      (("openai", "gpt-6-astra"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25)] Nothing),
+      (("openai", "gpt-6-astra"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25) Nothing] Nothing),
       (("anthropic", "claude-sonnet-5-5"), Model.PricingPolicy [] (Just 4)),
-      (("openai", "gpt-6.1-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.2 5)] Nothing),
-      (("openai", "gpt-6-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5)] Nothing),
-      (("openai", "gpt-6-luna"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25)] Nothing),
-      (("anthropic", "claude-fable-5-1"), Model.PricingPolicy [] (Just 20))
+      (("openai", "gpt-6.1-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.2 5) Nothing] Nothing),
+      (("openai", "gpt-6-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5) Nothing] Nothing),
+      (("openai", "gpt-6-luna"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25) Nothing] Nothing),
+      (("anthropic", "claude-fable-5-1"), Model.PricingPolicy [] (Just 20)),
+      (("anthropic", "claude-haiku-5-5"), Model.PricingPolicy [Model.InputPriceTier 100000 (Model.ModelCost 0.5 2.5 0.05 0.625) (Just 1)] (Just 0.2))
     ]
 
 renderPricingPolicy :: Model.PricingPolicy -> Text
-renderPricingPolicy p = "{\"inputTiers\": [" <> Text.intercalate ", " (map tier (Model.inputTiers p)) <> "]" <> maybe "" (\r -> ", \"longCacheWriteCost\": " <> num r) (Model.longCacheWriteCost p) <> "}"
+renderPricingPolicy p = "{\"inputTiers\": [" <> Text.intercalate ", " (map tier (Model.inputTiers p)) <> "]" <> long policyLong <> "}"
   where
+    Model.PricingPolicy {Model.longCacheWriteCost = policyLong} = p
     num = renderNum . fst . fromRationalRepetendUnlimited
-    tier t = "{\"inputAbove\": " <> Text.pack (show (Model.inputAbove t)) <> ", \"rates\": " <> rates (Model.rates t) <> "}"
+    long = maybe "" (\r -> ", \"longCacheWriteCost\": " <> num r)
+    tier (Model.InputPriceTier above tierRates tierLong) = "{\"inputAbove\": " <> Text.pack (show above) <> ", \"rates\": " <> rates tierRates <> long tierLong <> "}"
     rates c = "{\"input\": " <> num (Model.inputCost c) <> ", \"output\": " <> num (Model.outputCost c) <> ", \"cacheRead\": " <> num (Model.cacheReadCost c) <> ", \"cacheWrite\": " <> num (Model.cacheWriteCost c) <> "}"
 
 -- | Render the per-model @compat@ block, if the provider spec supplied
diff --git a/gen/GenModelsCore.hs b/gen/GenModelsCore.hs
--- a/gen/GenModelsCore.hs
+++ b/gen/GenModelsCore.hs
@@ -305,7 +305,10 @@
   either (fail . Text.unpack) (const (pure policy)) (Model.validatePricingPolicy policy)
   where
     parseTier = Aeson.withObject "InputPriceTier" $ \o ->
-      Model.InputPriceTier <$> o .: "inputAbove" <*> (o .: "rates" >>= parseCompleteRates)
+      Model.InputPriceTier
+        <$> o .: "inputAbove"
+        <*> (o .: "rates" >>= parseCompleteRates)
+        <*> (fmap toRational <$> (o .:? "longCacheWriteCost" :: Parser (Maybe Scientific)))
     parseCompleteRates = Aeson.withObject "Complete tier rates" $ \o ->
       toModelCost <$> (CostEntry <$> o .: "input" <*> o .: "output" <*> o .: "cacheRead" <*> o .: "cacheWrite")
 
@@ -602,9 +605,10 @@
 
 renderPolicy :: Maybe Model.PricingPolicy -> Text
 renderPolicy Nothing = "Nothing"
-renderPolicy (Just p) = "Just (PricingPolicy [" <> Text.intercalate ", " (map tier (Model.inputTiers p)) <> "] " <> maybe "Nothing" (\r -> "(Just (" <> renderRational r <> "))") (Model.longCacheWriteCost p) <> ")"
+renderPolicy (Just (Model.PricingPolicy tiers policyLong)) = "Just (PricingPolicy [" <> Text.intercalate ", " (map tier tiers) <> "] " <> long policyLong <> ")"
   where
-    tier t = "InputPriceTier " <> Text.pack (show (Model.inputAbove t)) <> " (" <> rates (Model.rates t) <> ")"
+    tier (Model.InputPriceTier above tierRates tierLong) = "InputPriceTier " <> Text.pack (show above) <> " (" <> rates tierRates <> ") " <> long tierLong
+    long = maybe "Nothing" (\r -> "(Just (" <> renderRational r <> "))")
     rates c = "ModelCost " <> Text.unwords (map (\r -> "(" <> renderRational r <> ")") [Model.inputCost c, Model.outputCost c, Model.cacheReadCost c, Model.cacheWriteCost c])
 
 renderRational :: Rational -> Text
diff --git a/src/Baikai/Cost/Pricing.hs b/src/Baikai/Cost/Pricing.hs
--- a/src/Baikai/Cost/Pricing.hs
+++ b/src/Baikai/Cost/Pricing.hs
@@ -68,7 +68,7 @@
   Nothing -> estimateCost [UnsupportedSpeed "fast"] (computeCostWith duration m u)
   Just fast ->
     let resolved = do
-          validatePricingPolicy (PricingPolicy [InputPriceTier 0 fast] Nothing)
+          validatePricingPolicy (PricingPolicy [InputPriceTier 0 fast Nothing] Nothing)
           standard <- resolveRates duration m u
           let base = m ^. #cost
               -- Apply each premium rate's ratio to the resolved policy once.
@@ -88,7 +88,7 @@
 -- | Price one resolved rate record exactly once.
 computeCostAtRates :: ModelCost -> Usage -> Cost
 computeCostAtRates rates u =
-  let resolved = validatePricingPolicy (PricingPolicy [InputPriceTier 0 rates] Nothing) >> pure rates
+  let resolved = validatePricingPolicy (PricingPolicy [InputPriceTier 0 rates Nothing] Nothing) >> pure rates
       computed = priceUsage resolved u
    in computed & #basis . #sources .~ Set.singleton ResolvedTokenRates
 
@@ -138,16 +138,21 @@
 
 -- | Choose one complete rate record. Thresholds are exclusive and use
 -- disjoint normalized input categories, including both cache counters.
+-- A shaped long-duration request takes its cache-write rate from the same
+-- tier that supplied the other rates, or from the policy below every tier.
 resolveRates :: Maybe CacheRetention -> Model -> Usage -> Either Text ModelCost
 resolveRates duration m u = do
-  validatePricingPolicy (PricingPolicy [InputPriceTier 0 (m ^. #cost)] Nothing)
+  validatePricingPolicy (PricingPolicy [InputPriceTier 0 (m ^. #cost) Nothing] Nothing)
   case m ^. #pricingPolicy of
     Nothing -> pure (m ^. #cost)
-    Just policy -> do
+    Just policy@(PricingPolicy tiers policyLong) -> do
       validatePricingPolicy policy
       let totalInput = (u ^. #inputTokens) + (u ^. #cacheReadTokens) + (u ^. #cacheWriteTokens)
-          selected = foldl' (\current tier -> if totalInput > inputAbove tier then rates tier else current) (m ^. #cost) (inputTiers policy)
-      pure $ case (duration, longCacheWriteCost policy) of
+          select current (InputPriceTier above tierRates tierLong)
+            | totalInput > above = (tierRates, tierLong)
+            | otherwise = current
+          (selected, long) = foldl' select (m ^. #cost, policyLong) tiers
+      pure $ case (duration, long) of
         (Just CacheRetentionLong, Just price) -> selected {cacheWriteCost = price}
         _ -> selected
 
diff --git a/src/Baikai/Model.hs b/src/Baikai/Model.hs
--- a/src/Baikai/Model.hs
+++ b/src/Baikai/Model.hs
@@ -68,6 +68,7 @@
     genericToJSON,
     withObject,
     (.!=),
+    (.:),
     (.:?),
   )
 import Data.List (nub, sort)
@@ -97,16 +98,26 @@
 
 -- | An exclusive input-context threshold. Its complete rate record
 -- applies to every token category in the call once total input exceeds it.
+-- Its long cache-write rate is the absolute per-million price of a
+-- long-duration cache write in such a call. JSON without that key decodes
+-- to 'Nothing'.
 data InputPriceTier = InputPriceTier
   { inputAbove :: !Natural,
-    rates :: !ModelCost
+    rates :: !ModelCost,
+    longCacheWriteCost :: !(Maybe Rational)
   }
   deriving stock (Eq, Show, Generic)
-  deriving anyclass (FromJSON, ToJSON)
+  deriving anyclass (ToJSON)
 
+instance FromJSON InputPriceTier where
+  parseJSON = withObject "InputPriceTier" $ \o ->
+    InputPriceTier <$> o .: "inputAbove" <*> o .: "rates" <*> o .:? "longCacheWriteCost"
+
 -- | Optional catalog policy layered over Model.cost. Long cache-write
 -- pricing is an absolute per-million rate, selected only for a shaped
--- long-duration request. It overrides the selected tier's write rate.
+-- long-duration request. The policy-level rate applies while no tier is
+-- crossed; a crossed tier supplies its own. A policy with a long rate
+-- must therefore give every tier one.
 data PricingPolicy = PricingPolicy
   { inputTiers :: ![InputPriceTier],
     longCacheWriteCost :: !(Maybe Rational)
@@ -120,11 +131,13 @@
     either (fail . show) (const (pure p)) (validatePricingPolicy p)
 
 validatePricingPolicy :: PricingPolicy -> Either Text ()
-validatePricingPolicy p = do
-  let thresholds = map inputAbove (inputTiers p)
+validatePricingPolicy (PricingPolicy tiers policyLong) = do
+  let thresholds = map inputAbove tiers
       validRates r = all (>= 0) [inputCost r, outputCost r, cacheReadCost r, cacheWriteCost r]
+      tierLongs = [long | InputPriceTier {longCacheWriteCost = long} <- tiers]
   unless (thresholds == sort (nub thresholds)) (Left "Pricing thresholds must be strictly increasing")
-  unless (all (validRates . rates) (inputTiers p) && maybe True (>= 0) (longCacheWriteCost p)) (Left "Pricing rates must be nonnegative")
+  unless (all (validRates . rates) tiers && all (maybe True (>= 0)) (policyLong : tierLongs)) (Left "Pricing rates must be nonnegative")
+  unless (policyLong == Nothing || Nothing `notElem` tierLongs) (Left "Every input tier must state its long cache-write rate when the policy has one")
 
 -- | Per-API compatibility shim. 'CompatNone' tells the provider to
 -- pick a sensible record by inspecting 'baseUrl'; the two real
diff --git a/src/Baikai/Models/Generated.hs b/src/Baikai/Models/Generated.hs
--- a/src/Baikai/Models/Generated.hs
+++ b/src/Baikai/Models/Generated.hs
@@ -170,6 +170,41 @@
             }
     }
 
+anthropic_claude_haiku_5_5 :: Model
+anthropic_claude_haiku_5_5 =
+  emptyModel
+    { modelId = "claude-haiku-5-5",
+      name = "Claude Haiku 5.5",
+      api = AnthropicMessages,
+      provider = "anthropic",
+      baseUrl = "https://api.anthropic.com",
+      reasoning = True,
+      input = [InputText, InputImage],
+      cost =
+        ModelCost
+          { inputCost = 1 % 10,
+            outputCost = 1 % 2,
+            cacheReadCost = 1 % 100,
+            cacheWriteCost = 1 % 8
+          },
+      fastModeCost = Nothing,
+      pricingPolicy = Just (PricingPolicy [InputPriceTier 100000 (ModelCost (1 % 2) (5 % 2) (1 % 20) (5 % 8)) (Just (1 % 1))] (Just (1 % 5))),
+      contextWindow = 1000000,
+      maxOutputTokens = 128000,
+      headers = Map.empty,
+      compat =
+        CompatAnthropicMessages
+          defaultAnthropicMessagesCompat
+            { supportsLongCacheRetention = True,
+              supportsCacheControlOnTools = True,
+              sendSessionAffinityHeaders = False,
+              thinkingStyle = AnthropicThinkingAdaptive,
+              supportsSamplingParameters = False,
+              supportsFastMode = False,
+              supportsForcedToolChoice = True
+            }
+    }
+
 anthropic_claude_opus_4_5 :: Model
 anthropic_claude_opus_4_5 =
   emptyModel
@@ -423,7 +458,7 @@
           },
       fastModeCost = Nothing,
       pricingPolicy = Nothing,
-      contextWindow = 1000000,
+      contextWindow = 200000,
       maxOutputTokens = 64000,
       headers = Map.empty,
       compat =
@@ -523,7 +558,7 @@
         ModelCost
           { inputCost = 2 % 1,
             outputCost = 10 % 1,
-            cacheReadCost = 1 % 5,
+            cacheReadCost = 1 % 10,
             cacheWriteCost = 5 % 2
           },
       fastModeCost = Nothing,
@@ -1062,7 +1097,7 @@
             cacheWriteCost = 5 % 2
           },
       fastModeCost = Nothing,
-      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (4 % 1) (15 % 1) (1 % 5) (5 % 1))] Nothing),
+      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (4 % 1) (15 % 1) (1 % 5) (5 % 1)) Nothing] Nothing),
       contextWindow = 1050000,
       maxOutputTokens = 128000,
       headers = Map.empty,
@@ -1094,7 +1129,7 @@
             cacheWriteCost = 25 % 2
           },
       fastModeCost = Nothing,
-      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (20 % 1) (75 % 1) (2 % 1) (25 % 1))] Nothing),
+      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (20 % 1) (75 % 1) (2 % 1) (25 % 1)) Nothing] Nothing),
       contextWindow = 1050000,
       maxOutputTokens = 128000,
       headers = Map.empty,
@@ -1126,7 +1161,7 @@
             cacheWriteCost = 1 % 8
           },
       fastModeCost = Nothing,
-      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (1 % 5) (3 % 4) (1 % 50) (1 % 4))] Nothing),
+      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (1 % 5) (3 % 4) (1 % 50) (1 % 4)) Nothing] Nothing),
       contextWindow = 1050000,
       maxOutputTokens = 128000,
       headers = Map.empty,
@@ -1158,7 +1193,7 @@
             cacheWriteCost = 5 % 2
           },
       fastModeCost = Nothing,
-      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (4 % 1) (15 % 1) (2 % 5) (5 % 1))] Nothing),
+      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (4 % 1) (15 % 1) (2 % 5) (5 % 1)) Nothing] Nothing),
       contextWindow = 1050000,
       maxOutputTokens = 128000,
       headers = Map.empty,
@@ -1328,6 +1363,7 @@
   [ anthropic_claude_fable_5,
     anthropic_claude_fable_5_1,
     anthropic_claude_haiku_4_5,
+    anthropic_claude_haiku_5_5,
     anthropic_claude_opus_4_5,
     anthropic_claude_opus_4_6,
     anthropic_claude_opus_4_7,
diff --git a/src/Baikai/Provider/Cli/Internal.hs b/src/Baikai/Provider/Cli/Internal.hs
--- a/src/Baikai/Provider/Cli/Internal.hs
+++ b/src/Baikai/Provider/Cli/Internal.hs
@@ -52,6 +52,7 @@
     ToolResultPayload (..),
     UserPayload (..),
   )
+import Baikai.Provider.Cli.Process.Internal (withOwnedProcess, withOwnedWorker)
 import Baikai.StopReason (StopReason (..))
 import Baikai.Usage (Usage (..))
 import Control.Applicative ((<|>))
@@ -694,9 +695,25 @@
 
 probeVersion :: FilePath -> IO (Maybe Text)
 probeVersion path = do
-  outcome <- trySync (timeout versionProbeMicros (Process.readProcessWithExitCode path ["--version"] ""))
+  outcome <- trySync
+    $ timeout versionProbeMicros
+    $ withOwnedProcess
+      (Process.proc path ["--version"])
+        { Process.std_in = Process.NoStream,
+          Process.std_out = Process.CreatePipe,
+          Process.std_err = Process.CreatePipe
+        }
+    $ \_ output errors ph -> case (output, errors) of
+      (Just out, Just err) ->
+        withOwnedWorker (BS.hGetContents out) $ \joinOut ->
+          withOwnedWorker (BS.hGetContents err) $ \joinErr -> do
+            stdoutBytes <- joinOut
+            stderrBytes <- joinErr
+            code <- Process.waitForProcess ph
+            pure (code, stdoutBytes, stderrBytes)
+      _ -> ioError (userError "version probe: capture handles missing")
   pure $ case outcome of
-    Right (Just (ExitSuccess, out, _)) -> firstNonBlankLine (Text.pack out)
+    Right (Just (ExitSuccess, out, _)) -> firstNonBlankLine (decodeUtf8Lenient out)
     _ -> Nothing
 
 -- | Five seconds.
diff --git a/src/Baikai/Provider/Cli/Process/Internal.hs b/src/Baikai/Provider/Cli/Process/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Baikai/Provider/Cli/Process/Internal.hs
@@ -0,0 +1,241 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+-- | Internal batch-process ownership shared by the vendor adapters. Not
+-- covered by the public PVP interface. POSIX groups contain inherited children,
+-- not processes deliberately escaping into another group or session.
+module Baikai.Provider.Cli.Process.Internal
+  ( withOwnedProcess,
+    withOwnedWorker,
+  )
+where
+
+import Control.Concurrent (forkIO, forkIOWithUnmask, killThread, threadDelay)
+import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar, tryReadMVar)
+import Control.Exception
+  ( SomeAsyncException,
+    SomeException,
+    catch,
+    displayException,
+    finally,
+    fromException,
+    mask,
+    onException,
+    throwIO,
+    try,
+  )
+import Control.Monad (forM_, void)
+import Data.ByteString qualified as BS
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as Text
+import System.IO (Handle, hClose, stderr)
+import System.Process qualified as P
+#ifndef mingw32_HOST_OS
+import Data.ByteString.Char8 qualified as BS8
+import Data.Maybe (mapMaybe)
+import Foreign.C.Error (throwErrnoIfMinus1)
+import Foreign.C.Types (CInt (..))
+import GHC.Clock (getMonotonicTimeNSec)
+import System.Exit (ExitCode (..))
+import System.Posix.Process (getProcessStatus)
+import System.Posix.Signals (Signal, sigINT, sigTERM, sigKILL, signalProcess, signalProcessGroup)
+import System.IO.Error (isDoesNotExistError)
+import System.Posix.Types (ProcessID, ProcessGroupID)
+import Text.Read (readMaybe)
+#endif
+
+-- | Run a callback with ordinary interruptibility, then finish release in a
+-- private masked worker. Repeated cancellation interrupts only the join, never
+-- release. Every worker exit publishes completion. Preserve the first async
+-- exception, including one delivered after a successful callback.
+ownedScope :: IO resource -> (resource -> IO ()) -> (resource -> IO a) -> IO a
+ownedScope acquire release use = mask $ \restore -> do
+  done <- newEmptyMVar
+  resource <- acquire
+  outcome <- try (restore (use resource))
+  _ <- forkIO $ try (release resource) >>= putMVar done
+  let initial = case outcome of
+        Left e | isAsync e -> Just e
+        _ -> Nothing
+      join first =
+        ((,first) <$> readMVar done) `catch` \e ->
+          if isAsync e
+            then join (case first of Nothing -> Just e; Just _ -> first)
+            else throwIO e
+  (cleaned, cancellation) <- join initial
+  case cancellation of
+    Just e -> do
+      -- Cancellation remains asynchronous even if cleanup diagnosed a failure.
+      -- Make that exceptional failure visible rather than silently losing it.
+      case cleaned of
+        Left failure -> void (try (BS.hPut stderr (Text.encodeUtf8 (Text.pack ("baikai CLI cleanup failed: " <> displayException failure <> "\n")))) :: IO (Either SomeException ()))
+        Right () -> pure ()
+      throwIO e
+    Nothing -> case cleaned of
+      Left e -> throwIO (e :: SomeException)
+      Right () -> either throwIO pure outcome
+  where
+    isAsync e = case fromException e :: Maybe SomeAsyncException of
+      Just _ -> True
+      Nothing -> False
+
+-- | The supplied join action returns the reader's value or exception. On every
+-- scope exit an unfinished reader is cancelled and joined before pipe closure.
+withOwnedWorker :: forall a b. IO a -> (IO a -> IO b) -> IO b
+withOwnedWorker action use = ownedScope acquire release $ \(_, done) ->
+  use (readMVar done >>= either throwIO pure)
+  where
+    acquire = do
+      done <- newEmptyMVar
+      tid <- forkIOWithUnmask $ \unmask -> do
+        result <- try (unmask action)
+        putMVar done (result :: Either SomeException a)
+      pure (tid, done)
+    release (tid, done) = do
+      completed <- tryReadMVar done
+      case completed of
+        Nothing -> killThread tid
+        Just _ -> pure ()
+      void (readMVar done)
+
+-- | Own handles, group identity, and synchronous direct-child reaping. Reader
+-- scopes must be nested inside this callback, so no reader retains a handle lock
+-- when release closes the pipes. Windows has direct-child-only cleanup.
+withOwnedProcess ::
+  P.CreateProcess ->
+  (Maybe Handle -> Maybe Handle -> Maybe Handle -> P.ProcessHandle -> IO a) ->
+  IO a
+withOwnedProcess spec use = ownedScope (acquireProcess spec) releaseProcess $ \(handles, _) ->
+  let (input, output, err, ph) = handles in use input output err ph
+
+type ProcessHandles = (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle)
+
+#ifdef mingw32_HOST_OS
+type GroupIdentity = ()
+
+acquireProcess :: P.CreateProcess -> IO (ProcessHandles, GroupIdentity)
+acquireProcess spec = do
+  handles <- P.createProcess spec {P.create_group = True}
+  pure (handles, ())
+
+releaseProcess :: (ProcessHandles, GroupIdentity) -> IO ()
+releaseProcess (handles@(_, _, _, ph), _) =
+  (P.terminateProcess ph >> void (P.waitForProcess ph)) `finally` closeHandles handles
+#else
+type GroupIdentity = (ProcessGroupID, ProcessID)
+
+acquireProcess :: P.CreateProcess -> IO (ProcessHandles, GroupIdentity)
+acquireProcess spec = do
+  handles@(_, _, _, ph) <- P.createProcess spec
+    {P.create_group = True, P.new_session = False, P.delegate_ctlc = False}
+  -- No callback can reap the leader before the anchor has joined. Even a
+  -- quickly exiting leader is still an unreaped group member at this point.
+  anchored <- try $ do
+    group <- P.getPid ph >>= maybe (ioError (userError "CLI leader PID unavailable")) pure
+    anchor <- throwErrnoIfMinus1 "CLI group anchor" (c_anchor (fromIntegral group))
+    pure (group, fromIntegral anchor)
+  case anchored of
+    Right identity -> pure (handles, identity)
+    Left (e :: SomeException) ->
+      -- The unreaped leader reserves the group during acquisition failure.
+      -- This nested scope also protects failed-acquisition cleanup from repeats.
+      ownedScope (pure handles) cleanupFailure (\_ -> throwIO e)
+  where
+    cleanupFailure handles@(_, _, _, ph) =
+      (do
+        pid <- P.getPid ph
+        forM_ pid $ \group -> terminateGroup group (-1) `onException` signalGroup sigKILL group
+        ) `finally` (void (P.waitForProcess ph) `finally` closeHandles handles)
+
+releaseProcess :: (ProcessHandles, GroupIdentity) -> IO ()
+releaseProcess (handles@(_, _, _, ph), (group, anchor)) =
+  ((terminateGroup group anchor `onException` signalGroup sigKILL group)
+    `finally` void (P.waitForProcess ph))
+    `finally` (finishAnchor `finally` closeHandles handles)
+  where
+    finishAnchor = do
+      -- Darwin reports EPERM when signalling a zombie-only group/PID.
+      -- Signalling is already complete, so we may now reap the anchor.
+      status <- getProcessStatus False False anchor
+      case status of
+        Just _ -> pure ()
+        Nothing -> signalPid sigKILL anchor >> void (getProcessStatus True False anchor)
+#endif
+
+closeHandles :: ProcessHandles -> IO ()
+closeHandles (input, output, err, _) =
+  -- finally ensures one failed close cannot skip the other owned descriptors.
+  forM_ input hClose `finally` (forM_ output hClose `finally` forM_ err hClose)
+
+#ifndef mingw32_HOST_OS
+foreign import ccall safe "baikai_cli_group_anchor" c_anchor :: CInt -> IO CInt
+
+signalGroup :: Signal -> ProcessGroupID -> IO ()
+signalGroup signal group = absentOnly (signalProcessGroup signal group)
+
+signalPid :: Signal -> ProcessID -> IO ()
+signalPid signal pid = absentOnly (signalProcess signal pid)
+
+-- Only ESRCH is benign. EPERM and all other observation/signal errors propagate.
+absentOnly :: IO () -> IO ()
+absentOnly action = action `catch` \e -> do
+  if isDoesNotExistError e then pure () else throwIO (e :: IOError)
+
+terminateGroup :: ProcessGroupID -> ProcessID -> IO ()
+terminateGroup group anchor = do
+  live <- runningMembers group anchor
+  if null live then pure () else do
+    signalGroup sigINT group
+    interrupted <- settle 100000
+    if interrupted then pure () else do
+      signalGroup sigTERM group
+      terminated <- settle 500000
+      if terminated then pure () else do
+        -- The anchor is killed too, but stays unreaped until all observations
+        -- and group signals finish. Its zombie membership reserves the PGID.
+        signalGroup sigKILL group
+        killed <- settle 1000000
+        if killed then pure () else ioError (userError "CLI group has live survivors after SIGKILL")
+  where
+    settle :: Int -> IO Bool
+    settle micros = do
+      start <- getMonotonicTimeNSec
+      let deadline = start + fromIntegral micros * 1000
+          poll = do
+            members <- runningMembers group anchor
+            if null members then pure True else do
+              now <- getMonotonicTimeNSec
+              if now >= deadline then pure False else threadDelay 10000 >> poll
+      poll
+
+-- Darwin and Linux both expose PID, PGID, and process state via POSIX ps.
+-- A zombie is no longer running; adopted grandchildren are reaped elsewhere.
+-- Never infer successful cleanup from kill(0), which includes zombies.
+runningMembers :: ProcessGroupID -> ProcessID -> IO [ProcessID]
+runningMembers group anchor = do
+  (code, bytes) <- P.withCreateProcess
+    (P.proc "/bin/ps" ["-axo", "pid=,pgid=,stat="])
+      { P.std_in = P.NoStream, P.std_out = P.CreatePipe, P.std_err = P.CreatePipe }
+    $ \_ output err ph -> case (output, err) of
+      (Just out, Just errors) -> withOwnedWorker (BS.hGetContents errors) $ \joinErr -> do
+        captured <- BS.hGetContents out
+        void joinErr
+        status <- P.waitForProcess ph
+        pure (status, captured)
+      _ -> ioError (userError "ps capture handles unavailable")
+  case code of
+    ExitFailure n -> ioError (userError ("CLI group observation failed: ps exit " <> show n))
+    ExitSuccess -> do
+      rows <- traverse parseRow (BS8.lines bytes)
+      pure (mapMaybe running rows)
+  where
+    parseRow row = case BS8.words row of
+      [pid, pgid, state] -> case (readMaybe (BS8.unpack pid), readMaybe (BS8.unpack pgid)) of
+        (Just p, Just g) -> pure (p, g, state)
+        _ -> invalid
+      _ -> invalid
+      where invalid = ioError (userError "CLI group observation: malformed ps row")
+    running (pid, pgid, state)
+      | pgid == group && pid /= anchor && not (BS8.elem 'Z' state) = Just pid
+      | otherwise = Nothing
+#endif
diff --git a/test/CatalogSpec.hs b/test/CatalogSpec.hs
--- a/test/CatalogSpec.hs
+++ b/test/CatalogSpec.hs
@@ -111,6 +111,7 @@
   [ ("claude-fable-5", (AnthropicThinkingAdaptive, False, True, False)),
     ("claude-fable-5-1", (AnthropicThinkingAdaptive, False, False, False)),
     ("claude-haiku-4-5", (AnthropicThinkingBudget, True, True, False)),
+    ("claude-haiku-5-5", (AnthropicThinkingAdaptive, False, True, False)),
     ("claude-opus-4-5", (AnthropicThinkingBudget, True, True, False)),
     ("claude-opus-4-6", (AnthropicThinkingAdaptive, True, True, False)),
     ("claude-opus-4-7", (AnthropicThinkingAdaptive, False, True, False)),
diff --git a/test/CliProcessFixture.hs b/test/CliProcessFixture.hs
new file mode 100644
--- /dev/null
+++ b/test/CliProcessFixture.hs
@@ -0,0 +1,158 @@
+{-# LANGUAGE CPP #-}
+
+-- | Real, offline subprocess fixtures shared by core and adapter regressions.
+-- Keep the copies in each package identical: each source distribution must
+-- contain its own test dependencies.
+module CliProcessFixture
+  ( Fixture (..),
+    withFixture,
+    awaitReady,
+    assertStopped,
+    cancelAndJoin,
+    processState,
+    writeExecutable,
+  )
+where
+
+import Control.Concurrent (forkFinally, forkIO, killThread, threadDelay)
+import Control.Concurrent.MVar (readMVar)
+import Control.Concurrent.MVar qualified
+import Control.Exception (SomeAsyncException, bracket, finally, fromException, try)
+import Control.Monad (forM_, unless, void)
+import Data.List (isInfixOf)
+import System.Directory (doesFileExist, getPermissions, setOwnerExecutable, setPermissions)
+import System.Exit (ExitCode (..))
+import System.FilePath ((</>))
+import System.IO.Temp (withSystemTempDirectory)
+import System.Process qualified as P
+import System.Timeout (timeout)
+import Test.Tasty.HUnit (assertBool, assertFailure)
+#ifndef mingw32_HOST_OS
+import System.Posix.Signals (sigKILL, signalProcess)
+#endif
+
+data Fixture = Fixture
+  { directory :: FilePath,
+    executable :: FilePath,
+    parentFile :: FilePath,
+    childFile :: FilePath,
+    readyFile :: FilePath
+  }
+
+-- The optional prefix can implement a successful normal invocation and hang
+-- only in --version. Every fixture sleeps 30s so success cannot be natural expiry.
+withFixture :: Bool -> Bool -> String -> (Fixture -> IO a) -> IO a
+withFixture resistant early prefix use = withSystemTempDirectory "baikai-cli-cancellation" $ \dir -> do
+  let sleeper = dir </> "sleeper"
+      parent = dir </> "parent"
+      child = dir </> "child"
+      ready = dir </> "ready"
+      body =
+        unlines $
+          ["#!/bin/sh", prefix]
+            <> ["trap '' INT TERM" | resistant]
+            <> [ "echo $$ > '" <> parent <> "'",
+                 "sh -c 'echo $$ > \"" <> child <> "\"; exec \"" <> sleeper <> "\" 30' &",
+                 "while [ ! -s '" <> child <> "' ]; do sleep 0.01; done",
+                 "echo ready > '" <> ready <> "'"
+               ]
+            <> [if early then "exit 0" else "wait"]
+  P.callProcess "/bin/ln" ["-s", "/bin/sleep", sleeper]
+  exe <- writeExecutable dir "vendor" body
+  let fixture = Fixture dir exe parent child ready
+  -- Capture process identities before use returns/fails; identity comparison in
+  -- emergency cleanup avoids signalling a PID reused after a fixture exits.
+  bracket (pure fixture) cleanupFixture use
+
+cleanupFixture :: Fixture -> IO ()
+cleanupFixture fixture = forM_ [childFile fixture, parentFile fixture] $ \file -> do
+  exists <- doesFileExist file
+  if not exists
+    then pure ()
+    else do
+      pid <- read <$> readFile file
+      state <- processState pid
+      -- Both the script and sleeper executable have this invocation's unique
+      -- directory in their command line, including after exec. A reused PID
+      -- belonging to a different invocation cannot match that identity.
+      (_, identity, _) <- P.readProcessWithExitCode "/bin/ps" ["-p", show pid, "-o", "lstart=,command="] ""
+      let ours = (directory fixture <> "/") `isInfixOf` identity
+      unless (null state || 'Z' `elem` state || not ours) $ do
+        (_, current, _) <- P.readProcessWithExitCode "/bin/ps" ["-p", show pid, "-o", "lstart=,command="] ""
+        if current /= identity
+          then pure ()
+          else do
+            killFixturePid pid
+
+killFixturePid :: Int -> IO ()
+#ifndef mingw32_HOST_OS
+killFixturePid pid = void (try (signalProcess sigKILL (fromIntegral pid)) :: IO (Either IOError ()))
+#else
+killFixturePid _ = pure ()
+#endif
+
+writeExecutable :: FilePath -> String -> String -> IO FilePath
+writeExecutable dir name body = do
+  let path = dir </> name
+  writeFile path body
+  perms <- getPermissions path
+  setPermissions path (setOwnerExecutable True perms)
+  pure path
+
+awaitReady :: Fixture -> IO (Int, Int)
+awaitReady fixture = do
+  ready <- timeout 2000000 poll
+  case ready of
+    Nothing -> assertFailure "fixture did not publish readiness within two seconds"
+    Just () -> (,) <$> (read <$> readFile (parentFile fixture)) <*> (read <$> readFile (childFile fixture))
+  where
+    poll = do
+      exists <- doesFileExist (readyFile fixture)
+      if exists then pure () else threadDelay 10000 >> poll
+
+processState :: Int -> IO String
+processState pid = do
+  (code, out, err) <- P.readProcessWithExitCode "/bin/ps" ["-p", show pid, "-o", "stat="] ""
+  case code of
+    ExitSuccess -> pure out
+    ExitFailure 1 | null out && null err -> pure ""
+    _ -> assertFailure ("ps observation failed: " <> show (code, out, err))
+
+assertStopped :: (Int, Int) -> IO ()
+assertStopped (parent, child) = do
+  parentState <- processState parent
+  assertBool ("direct child remains: " <> parentState) (null parentState)
+  childState <- processState child
+  assertBool ("descendant is running: " <> childState) (null childState || 'Z' `elem` childState)
+  -- Removal of an adopted zombie is diagnostic, distinct from termination.
+  unless (null childState) $ do
+    removed <- timeout 200000 (let poll = processState child >>= \s -> if null s then pure () else threadDelay 10000 >> poll in poll)
+    case removed of
+      Nothing -> putStrLn "fixture descendant stopped; adoptive parent has not yet reaped zombie"
+      Just () -> pure ()
+
+cancelAndJoin :: Bool -> IO a -> Fixture -> ((Int, Int) -> IO ()) -> IO ()
+cancelAndJoin repeated action fixture after = do
+  done <- Control.Concurrent.MVar.newEmptyMVar
+  tid <- forkFinally (void action) (Control.Concurrent.MVar.putMVar done)
+  let stop = void (forkIO (killThread tid))
+  ( do
+      pids <- awaitReady fixture
+      stop
+      if repeated then void (forkIO (threadDelay 40000 >> killThread tid)) else pure ()
+      -- Keep the original 200ms observation without using it as acknowledgement.
+      threadDelay 200000
+      diagnostic <- processState (snd pids)
+      putStrLn ("descendant state at 200ms: " <> show diagnostic)
+      terminal <- timeout 2800000 (readMVar done)
+      case terminal of
+        Just (Left e) | Just _ <- (fromException e :: Maybe SomeAsyncException) -> after pids
+        other -> assertFailure ("expected acknowledged async cancellation within 3s, got " <> show other)
+    )
+    `finally` do
+      stop
+      cleanupFixture fixture
+      joined <- timeout 3000000 (readMVar done)
+      case joined of
+        Nothing -> assertFailure "fixture worker failed to finish after emergency cleanup"
+        Just _ -> pure ()
diff --git a/test/CliProcessSpec.hs b/test/CliProcessSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/CliProcessSpec.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE CPP #-}
+
+module CliProcessSpec (tests) where
+
+import Baikai.Provider.Cli.Internal (ExecutableIdentity (..), executableIdentity)
+import Baikai.Provider.Cli.Process.Internal
+import CliProcessFixture
+import Control.Concurrent (forkFinally, forkIO, killThread, threadDelay, throwTo)
+import Control.Concurrent.MVar
+import Control.Exception (AsyncException (..), SomeException, finally, fromException, throwIO, try)
+import Control.Monad (void)
+import Data.ByteString qualified as BS
+import System.Exit (ExitCode (..))
+import System.FilePath ((</>))
+import System.IO (BufferMode (..), hSetBuffering)
+import System.Process qualified as P
+import System.Timeout (timeout)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+
+tests :: TestTree
+#ifndef mingw32_HOST_OS
+tests = testGroup "CliProcessSpec: shared owned process scopes"
+  [ testCase "sleeping descendant" $ cancelled False False False,
+    testCase "leader exits with descendant holding pipes" $ cancelled False True False,
+    testCase "callback reaps leader before cancellation" $ cancelled False True True,
+    testCase "SIGKILL-resistant group and repeated cancellation" $ cancelled True False False,
+    testCase "normal process exit and direct-child reaping" $ do
+      pidCell <- newEmptyMVar
+      anchors <- newEmptyMVar
+      code <- withOwnedProcess (P.proc "/bin/sh" ["-c", "exit 0"]) $ \_ _ _ ph -> do
+        pid <- P.getPid ph
+        putMVar pidCell pid
+        members <- maybe (assertFailure "missing leader PID") (groupPids . fromIntegral) pid
+        putMVar anchors [p | p <- members, Just (fromIntegral p) /= pid]
+        P.waitForProcess ph
+      code @?= ExitSuccess
+      readMVar pidCell >>= maybe (assertFailure "missing leader PID") (\pid -> processState (fromIntegral pid) >>= (@?= ""))
+      recordedAnchors <- readMVar anchors
+      length recordedAnchors @?= 1
+      mapM_ (\pid -> processState pid >>= (@?= "")) recordedAnchors,
+    testCase "synchronous callback failure still terminates descendants" $ withFixture False False "" $ \fixture -> do
+      pidsCell <- newEmptyMVar
+      result <- try $ withOwnedProcess (spec fixture) $ \_ _ _ _ -> do
+        awaitReady fixture >>= putMVar pidsCell
+        throwIO (userError "callback failed")
+      case result :: Either SomeException () of
+        Left _ -> readMVar pidsCell >>= assertStopped
+        Right () -> assertFailure "callback exception swallowed",
+    testCase "successful callback still cleans resistant descendants" $ withFixture True False "" $ \fixture -> do
+      pids <- withOwnedProcess (spec fixture) $ \_ _ _ _ -> awaitReady fixture
+      assertStopped pids,
+    testCase "first cancellation during successful-callback cleanup survives repeats" $
+      withFixture True False "" $ \fixture -> do
+        returned <- newEmptyMVar
+        done <- newEmptyMVar
+        let action = withOwnedProcess (spec fixture) $ \_ _ _ _ -> do
+              pids <- awaitReady fixture
+              putMVar returned pids
+        tid <- forkFinally action (putMVar done)
+        (do
+          ready <- timeout 2000000 (readMVar returned)
+          pids <- maybe (assertFailure "callback did not publish readiness within two seconds") pure ready
+          threadDelay 50000
+          _ <- forkIO (throwTo tid UserInterrupt)
+          _ <- forkIO (threadDelay 50000 >> killThread tid)
+          terminal <- timeout 3000000 (readMVar done)
+          case terminal of
+            Just (Left e) -> (fromException e :: Maybe AsyncException) @?= Just UserInterrupt
+            _ -> assertFailure "first cancellation during cleanup was lost"
+          assertStopped pids
+          ) `finally` do
+            _ <- forkIO (killThread tid)
+            void (timeout 3000000 (readMVar done)),
+    testCase "cleanup failure publishes completion and preserves cancellation" $ do
+      result <- timeout 2000000 $ try $ withOwnedProcess
+        (P.proc "/bin/sh" ["-c", "exit 0"]) {P.std_in = P.CreatePipe}
+        $ \input _ _ ph -> case input of
+          Nothing -> assertFailure "missing stdin pipe"
+          Just pipe -> do
+            hSetBuffering pipe (BlockBuffering (Just 4096))
+            BS.hPut pipe "pending buffered input"
+            void (P.waitForProcess ph)
+            -- Releasing stdin flushes to a dead child and fails with EPIPE.
+            -- The finalizer must still publish completion and retain cancellation.
+            throwIO ThreadKilled
+      case result :: Maybe (Either SomeException ()) of
+        Just (Left e) -> (fromException e :: Maybe AsyncException) @?= Just ThreadKilled
+        _ -> assertFailure "cleanup failure lost cancellation or completion",
+    testCase "failed spawn is reported" $ do
+      result <- try (withOwnedProcess (P.proc "/nonexistent/baikai-fixture" []) (\_ _ _ _ -> pure ()))
+      case result :: Either SomeException () of
+        Left _ -> pure ()
+        Right () -> assertFailure "spawn unexpectedly succeeded",
+    testCase "blocked owned reader is joined before release returns" $ do
+      started <- newEmptyMVar
+      finished <- newEmptyMVar
+      gate <- newEmptyMVar
+      withOwnedWorker ((putMVar started () >> readMVar gate) `finally` putMVar finished ()) $ \_ -> readMVar started
+      tryReadMVar finished >>= (@?= Just ()),
+    testCase "reader error reaches join" $ do
+      result <- try (withOwnedWorker (throwIO (userError "reader failed") :: IO ()) id)
+      case result :: Either SomeException () of
+        Left _ -> pure ()
+        Right () -> assertFailure "reader failure swallowed",
+    testCase "unrelated sentinel survives group cancellation" $
+      withOwnedProcess (P.proc "/bin/sleep" ["30"]) $ \_ _ _ sentinel -> do
+        sentinelPid <- P.getPid sentinel
+        cancelled False False False
+        maybe (assertFailure "sentinel PID missing") (\pid -> processState (fromIntegral pid) >>= assertBool "sentinel stopped" . not . null) sentinelPid,
+    testCase "version-probe cancellation owns descendants" $ withFixture False False "" $ \fixture ->
+      cancelAndJoin False (executableIdentity (executable fixture)) fixture assertStopped,
+    testCase "version-probe timeout owns descendants and reports no version" $ withFixture False False "" $ \fixture -> do
+      bounded <- timeout 8000000 (executableIdentity (executable fixture))
+      case bounded of
+        Nothing -> assertFailure "version timeout failed to finish"
+        Just identity -> version identity @?= Nothing
+      awaitReady fixture >>= assertStopped
+  ]
+  where
+    spec fixture = (P.proc (executable fixture) [])
+      {P.std_in = P.NoStream, P.std_out = P.CreatePipe, P.std_err = P.CreatePipe}
+    cancelled resistant early reap = withFixture resistant early "" $ \fixture -> do
+      anchorCell <- newEmptyMVar
+      let action = withOwnedProcess (spec fixture) $ \_ output err ph -> case (output, err) of
+            (Just out, Just errors) -> withOwnedWorker (BS.hGetContents errors) $ \joinErr -> do
+              if reap then do
+                group <- P.getPid ph >>= maybe (assertFailure "missing group PID") (pure . fromIntegral)
+                (_, child) <- awaitReady fixture
+                void (P.waitForProcess ph)
+                members <- groupPids group
+                let anchors = filter (/= child) members
+                length anchors @?= 1
+                putMVar anchorCell anchors
+                writeFile (directory fixture </> "reaped") "ready"
+              else pure ()
+              _ <- BS.hGetContents out
+              void joinErr
+            _ -> assertFailure "missing capture handles"
+      let readyFixture = if reap then fixture {readyFile = directory fixture </> "reaped"} else fixture
+      cancelAndJoin resistant action readyFixture $ \pids -> do
+        assertStopped pids
+        if reap then readMVar anchorCell >>= mapM_ (\pid -> processState pid >>= (@?= "")) else pure ()
+
+-- Inspect only this invocation's group, and record the anchor while its owner
+-- still holds it. Checking it after return proves the private direct child was
+-- reaped too, even when the callback reaped the CLI leader itself.
+groupPids :: Int -> IO [Int]
+groupPids group = do
+  (code, out, err) <- P.readProcessWithExitCode "/bin/ps" ["-axo", "pid=,pgid="] ""
+  code @?= ExitSuccess
+  err @?= ""
+  pure [read pid | row <- lines out, [pid, pgid] <- [words row], read pgid == group]
+#else
+tests = testGroup "CliProcessSpec: Windows direct-child cleanup"
+  [testCase "normal direct-child exit" $ do
+    code <- withOwnedProcess (P.proc "cmd" ["/c", "exit", "0"]) (\_ _ _ ph -> P.waitForProcess ph)
+    code @?= ExitSuccess]
+#endif
diff --git a/test/CostSpec.hs b/test/CostSpec.hs
--- a/test/CostSpec.hs
+++ b/test/CostSpec.hs
@@ -290,11 +290,11 @@
   testGroup
     "fast pricing"
     [ testCase "fast premiums compose with context tiers before pricing" $ do
-        let m = Models.anthropic_claude_opus_5 & #pricingPolicy .~ Just (PricingPolicy [InputPriceTier 1000 (ModelCost 10 50 1 12.5)] Nothing)
+        let m = Models.anthropic_claude_opus_5 & #pricingPolicy .~ Just (PricingPolicy [InputPriceTier 1000 (ModelCost 10 50 1 12.5) Nothing] Nothing)
         Cost.usd (computeCostAtSpeed m SpeedFast u) @?= 2 * Cost.usd (computeCost m u),
       testCase "invalid premium rates and undefined policy ratios are explicit" $ do
         let negative = knownModel & #fastModeCost .~ Just (ModelCost (-1) 10 0.2 2.5)
-            undefinedRatio = knownModel & #cost .~ ModelCost 0 5 0.1 1.25 & #fastModeCost .~ Just (ModelCost 10 10 0.2 2.5) & #pricingPolicy .~ Just (PricingPolicy [InputPriceTier 1 (ModelCost 5 5 0.1 1.25)] Nothing)
+            undefinedRatio = knownModel & #cost .~ ModelCost 0 5 0.1 1.25 & #fastModeCost .~ Just (ModelCost 10 10 0.2 2.5) & #pricingPolicy .~ Just (PricingPolicy [InputPriceTier 1 (ModelCost 5 5 0.1 1.25) Nothing] Nothing)
         mapM_ (\m -> Set.member Cost.InvalidPricingPolicy (Cost.estimateReasons (Cost.basis (computeCostAtSpeed m SpeedFast u))) @?= True) [negative, undefinedRatio],
       testCase "contradictory speed observations remain an explicit standard estimate" $ do
         let usage = Usage.observeBilling [Usage.BillingSpeed "fast", Usage.BillingSpeed "standard"] u
diff --git a/test/FetchModelsSpec.hs b/test/FetchModelsSpec.hs
--- a/test/FetchModelsSpec.hs
+++ b/test/FetchModelsSpec.hs
@@ -179,9 +179,9 @@
         upstream <- loadUpstream
         let sample = (upstream Map.! "openai") Map.! "gpt-5.4"
         forM_
-          [ (openaiSpec, "gpt-6-astra", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25)] Nothing),
-            (openaiSpec, "gpt-6-sol", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5)] Nothing),
-            (openaiSpec, "gpt-6-luna", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25)] Nothing),
+          [ (openaiSpec, "gpt-6-astra", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25) Nothing] Nothing),
+            (openaiSpec, "gpt-6-sol", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5) Nothing] Nothing),
+            (openaiSpec, "gpt-6-luna", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25) Nothing] Nothing),
             (anthropicSpec, "claude-fable-5-1", Model.PricingPolicy [] (Just 20)),
             (anthropicSpec, "claude-opus-5-5", Model.PricingPolicy [] (Just 8))
           ]
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -10,6 +10,7 @@
 import Baikai.Prelude
 import CatalogSpec qualified
 import CliInternalSpec qualified
+import CliProcessSpec qualified
 import ContextSpec qualified
 import Control.Monad (forM_)
 import CostSpec qualified
@@ -109,6 +110,7 @@
         AgentSpec.tests,
         CatalogSpec.tests,
         CliInternalSpec.tests,
+        CliProcessSpec.tests,
         ContextSpec.tests,
         CostSpec.tests,
         EmbeddingSpec.tests,
diff --git a/test/PricingPolicySpec.hs b/test/PricingPolicySpec.hs
--- a/test/PricingPolicySpec.hs
+++ b/test/PricingPolicySpec.hs
@@ -43,8 +43,21 @@
             u = U.zeroUsage & #cacheWriteTokens .~ 1000000
         (computeCostWith (Just CacheRetentionShort) m u).usd @?= 5 / 2
         (computeCostWith (Just CacheRetentionLong) m u).usd @?= 4
-        (computeCost m (U.zeroUsage & #cacheReadTokens .~ 1000000)).usd @?= 1 / 5
+        -- 2026-10-07: Anthropic lowered Sonnet 5.5 cache reads from $0.20 to $0.10/M.
+        -- https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching
+        (computeCost m (U.zeroUsage & #cacheReadTokens .~ 1000000)).usd @?= 1 / 10
         Set.member (C.UnsupportedSpeed "fast") (computeCostAtSpeed m SpeedFast u).basis.estimateReasons @?= True,
+      testCase "a crossed tier prices long cache writes at its own rate" $ do
+        let writes n = U.zeroUsage & #cacheWriteTokens .~ n
+            prompt n = U.zeroUsage & #inputTokens .~ n & #cacheReadTokens .~ 20000 & #outputTokens .~ 1000
+        (computeCostWith (Just CacheRetentionLong) haikuShaped (writes 1000000)).usd @?= 1
+        (computeCostWith (Just CacheRetentionShort) haikuShaped (writes 1000000)).usd @?= 5 / 8
+        (computeCostWith (Just CacheRetentionLong) haikuShaped (writes 50000)).usd @?= 1 / 100
+        (computeCostWith (Just CacheRetentionShort) haikuShaped (writes 50000)).usd @?= 1 / 160
+        (computeCost haikuShaped (prompt 90000)).usd @?= 97 / 2000
+        (computeCost haikuShaped (prompt 80000)).usd @?= 87 / 10000
+        Models.anthropic_claude_haiku_5_5.cost @?= haikuShaped.cost
+        Models.anthropic_claude_haiku_5_5.pricingPolicy @?= haikuShaped.pricingPolicy,
       testCase "requested tiers never substitute for observed service" $ do
         let unknown = N.normalizeUsage N.InclusiveInput (N.ReportedUsage (Just 1000) (Just 0) (Just 0) (Just 0) Nothing)
             standard = U.observeBilling [U.BillingServiceTier "default"] unknown
@@ -95,11 +108,21 @@
         let flat = astra & #pricingPolicy .~ Nothing
         resolveRates (Just CacheRetentionLong) flat (U.zeroUsage & #inputTokens .~ 900000) @?= Right base,
       testCase "duplicate, unordered and negative policy data are rejected" $ do
-        forM_ [M.PricingPolicy [M.InputPriceTier 2 high, M.InputPriceTier 2 base] Nothing, M.PricingPolicy [M.InputPriceTier 2 high, M.InputPriceTier 1 base] Nothing, M.PricingPolicy [] (Just (-1)), M.PricingPolicy [M.InputPriceTier 1 (base & #inputCost .~ (-1))] Nothing] $ \p -> do
+        forM_ [M.PricingPolicy [M.InputPriceTier 2 high Nothing, M.InputPriceTier 2 base Nothing] Nothing, M.PricingPolicy [M.InputPriceTier 2 high Nothing, M.InputPriceTier 1 base Nothing] Nothing, M.PricingPolicy [] (Just (-1)), M.PricingPolicy [M.InputPriceTier 1 (base & #inputCost .~ (-1)) Nothing] Nothing] $ \p -> do
           assertBool "pure validation rejects" (case M.validatePricingPolicy p of Left _ -> True; _ -> False)
           case Aeson.fromJSON (Aeson.toJSON p) :: Aeson.Result M.PricingPolicy of Aeson.Error _ -> pure (); _ -> assertFailure "invalid policy decoded"
         let negative = Aeson.object ["inputTiers" Aeson..= [Aeson.object ["inputAbove" Aeson..= (-1 :: Int), "rates" Aeson..= base]]]
         case Aeson.fromJSON negative :: Aeson.Result M.PricingPolicy of Aeson.Error _ -> pure (); _ -> assertFailure "negative threshold decoded",
+      testCase "a policy-level long rate requires a long rate on every tier" $ do
+        let missing = M.PricingPolicy [M.InputPriceTier 2 high Nothing] (Just 1)
+        M.validatePricingPolicy missing @?= Left "Every input tier must state its long cache-write rate when the policy has one"
+        case Aeson.fromJSON (Aeson.toJSON missing) :: Aeson.Result M.PricingPolicy of Aeson.Error _ -> pure (); _ -> assertFailure "policy with an unpriced tier decoded"
+        assertBool "negative tier long rate rejects" (M.validatePricingPolicy (M.PricingPolicy [M.InputPriceTier 2 high (Just (-1))] (Just 1)) /= Right ())
+        M.validatePricingPolicy (M.PricingPolicy [M.InputPriceTier 2 high (Just 2)] Nothing) @?= Right (),
+      testCase "a tier without a long-rate key decodes to Nothing" $ do
+        let old = Aeson.object ["inputTiers" Aeson..= [Aeson.object ["inputAbove" Aeson..= (272000 :: Int), "rates" Aeson..= high]]]
+        Aeson.fromJSON old @?= Aeson.Success (M.PricingPolicy [M.InputPriceTier 272000 high Nothing] Nothing)
+        case Aeson.fromJSON (Aeson.toJSON haikuShaped) of Aeson.Success m -> (m :: M.Model) @?= haikuShaped; Aeson.Error err -> assertFailure err,
       testCase "old model JSON without a policy decodes and new policy round trips" $ do
         let old = case Aeson.toJSON M.emptyModel of Aeson.Object o -> Aeson.Object (KM.delete "pricingPolicy" o); v -> v
         case Aeson.fromJSON old of Aeson.Success m -> (m :: M.Model).pricingPolicy @?= Nothing; Aeson.Error err -> assertFailure err
@@ -131,3 +154,11 @@
 
 fable :: M.Model
 fable = Models.anthropic_claude_fable_5_1
+
+-- | Claude Haiku 5.5's published rates, built by hand so the pricing rule
+-- is tested independently of the catalog entry.
+haikuShaped :: M.Model
+haikuShaped =
+  M.emptyModel
+    & #cost .~ M.ModelCost (1 / 10) (1 / 2) (1 / 100) (1 / 8)
+    & #pricingPolicy .~ Just (M.PricingPolicy [M.InputPriceTier 100000 (M.ModelCost (1 / 2) (5 / 2) (1 / 20) (5 / 8)) (Just 1)] (Just (1 / 5)))
diff --git a/test/fixtures/claude-cli-result.json b/test/fixtures/claude-cli-result.json
new file mode 100644
--- /dev/null
+++ b/test/fixtures/claude-cli-result.json
@@ -0,0 +1,58 @@
+[
+  {
+    "type": "system",
+    "subtype": "init",
+    "session_id": "01890000-0000-4000-8000-000000000001",
+    "model": "claude-opus-5[1m]",
+    "claude_code_version": "2.1.222"
+  },
+  {
+    "type": "assistant",
+    "session_id": "01890000-0000-4000-8000-000000000001",
+    "message": {
+      "id": "msg_recorded_0001",
+      "model": "claude-opus-5",
+      "role": "assistant",
+      "content": [{ "type": "text", "text": "ok" }],
+      "usage": {
+        "input_tokens": 2,
+        "cache_creation_input_tokens": 7455,
+        "cache_read_input_tokens": 15185,
+        "output_tokens": 6
+      }
+    }
+  },
+  {
+    "type": "result",
+    "subtype": "success",
+    "is_error": false,
+    "result": "ok",
+    "session_id": "01890000-0000-4000-8000-000000000001",
+    "uuid": "01890000-0000-4000-8000-000000000002",
+    "stop_reason": "end_turn",
+    "duration_ms": 4212,
+    "duration_api_ms": 3890,
+    "num_turns": 1,
+    "total_cost_usd": 0.0823025,
+    "permission_denials": [],
+    "usage": {
+      "input_tokens": 2,
+      "cache_creation_input_tokens": 7455,
+      "cache_read_input_tokens": 15185,
+      "output_tokens": 6,
+      "service_tier": "standard",
+      "server_tool_use": { "web_search_requests": 0, "web_fetch_requests": 0 }
+    },
+    "modelUsage": {
+      "claude-opus-5[1m]": {
+        "inputTokens": 2,
+        "outputTokens": 6,
+        "cacheReadInputTokens": 15185,
+        "cacheCreationInputTokens": 7455,
+        "costUSD": 0.0823025,
+        "canonicalModel": "claude-opus-5",
+        "provider": "firstParty"
+      }
+    }
+  }
+]
diff --git a/test/fixtures/codex-events.jsonl b/test/fixtures/codex-events.jsonl
new file mode 100644
--- /dev/null
+++ b/test/fixtures/codex-events.jsonl
@@ -0,0 +1,4 @@
+{"type":"thread.started","thread_id":"019fd471-4a48-7c83-be67-6b7c49646e43"}
+{"type":"turn.started"}
+{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"ok"}}
+{"type":"turn.completed","usage":{"input_tokens":16071,"cached_input_tokens":6912,"cache_write_input_tokens":0,"output_tokens":5,"reasoning_output_tokens":0}}
diff --git a/test/fixtures/evidence-request.json b/test/fixtures/evidence-request.json
new file mode 100644
--- /dev/null
+++ b/test/fixtures/evidence-request.json
@@ -0,0 +1,86 @@
+{
+  "model": "claude-opus-4-6",
+  "max_tokens": 4096,
+  "temperature": 0.7,
+  "top_p": 0.95,
+  "stream": true,
+  "thinking": {
+    "type": "enabled",
+    "budget_tokens": 16384
+  },
+  "metadata": {
+    "user_id": "u-123"
+  },
+  "extra_headers": {
+    "x-api-key": "sk-baikai-fixture-secret-key"
+  },
+  "system": "You are a careful assistant. SYSTEM-PROMPT-BODY-MARKER.",
+  "messages": [
+    {
+      "role": "user",
+      "content": "PROMPT-BODY-MARKER: summarise the attached quarterly report."
+    },
+    {
+      "role": "assistant",
+      "content": [
+        {
+          "type": "thinking",
+          "thinking": "REASONING-TEXT-MARKER: the user wants a short summary."
+        },
+        {
+          "type": "tool_use",
+          "id": "toolu_1",
+          "name": "fetch_report",
+          "input": {
+            "query": "TOOL-PAYLOAD-MARKER"
+          }
+        }
+      ]
+    }
+  ],
+  "tools": [
+    {
+      "name": "fetch_report",
+      "description": "Fetch a quarterly report by identifier.",
+      "input_schema": {
+        "type": "object",
+        "properties": {
+          "query": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  ],
+  "output_config": {
+    "effort": "high",
+    "format": {
+      "type": "json_schema",
+      "schema": {
+        "type": "object",
+        "description": "OUTPUT-SCHEMA-MARKER: the shape a quarterly report answer must take.",
+        "properties": {
+          "revenue": {
+            "type": "number"
+          }
+        }
+      }
+    }
+  },
+  "response_format": {
+    "type": "json_schema",
+    "json_schema": {
+      "name": "quarterly_report",
+      "strict": true,
+      "schema": {
+        "type": "object",
+        "description": "RESPONSE-SCHEMA-MARKER: the same shape, spelled the OpenAI way.",
+        "properties": {
+          "revenue": {
+            "type": "number"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/models-dev-sample.json b/test/fixtures/models-dev-sample.json
new file mode 100644
--- /dev/null
+++ b/test/fixtures/models-dev-sample.json
@@ -0,0 +1,59 @@
+{
+  "openai": {
+    "id": "openai",
+    "name": "OpenAI",
+    "models": {
+      "gpt-5.4": {
+        "id": "gpt-5.4",
+        "name": "GPT-5.4",
+        "tool_call": true,
+        "reasoning": true,
+        "limit": { "context": 1050000, "output": 128000 },
+        "cost": { "input": 2.5, "output": 15, "cache_read": 0.25, "cache_write": 0 },
+        "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
+      },
+      "gpt-5-nano": {
+        "id": "gpt-5-nano",
+        "name": "GPT-5 Nano",
+        "tool_call": true,
+        "reasoning": true,
+        "limit": { "context": 400000, "output": 128000 },
+        "cost": { "input": 0.05, "output": 0.4 },
+        "modalities": { "input": ["text", "image"], "output": ["text"] }
+      },
+      "gpt-legacy-nontool": {
+        "id": "gpt-legacy-nontool",
+        "name": "Legacy non-tool model",
+        "tool_call": false,
+        "reasoning": false,
+        "limit": { "context": 8192, "output": 4096 },
+        "cost": { "input": 1, "output": 2 },
+        "modalities": { "input": ["text"], "output": ["text"] }
+      },
+      "gpt-5-pro": {
+        "id": "gpt-5-pro",
+        "name": "GPT-5 Pro",
+        "tool_call": true,
+        "reasoning": true,
+        "limit": { "context": 400000, "output": 128000 },
+        "cost": { "input": 15, "output": 120 },
+        "modalities": { "input": ["text", "image"], "output": ["text"] }
+      }
+    }
+  },
+  "anthropic": {
+    "id": "anthropic",
+    "name": "Anthropic",
+    "models": {
+      "claude-opus-4-5": {
+        "id": "claude-opus-4-5",
+        "name": "Claude Opus 4.5 (latest)",
+        "tool_call": true,
+        "reasoning": true,
+        "limit": { "context": 200000, "output": 64000 },
+        "cost": { "input": 5, "output": 25, "cache_read": 1.5, "cache_write": 6.25 },
+        "modalities": { "input": ["text", "image"], "output": ["text"] }
+      }
+    }
+  }
+}
diff --git a/test/fixtures/trace-opt-out.jsonl b/test/fixtures/trace-opt-out.jsonl
new file mode 100644
--- /dev/null
+++ b/test/fixtures/trace-opt-out.jsonl
@@ -0,0 +1,2 @@
+{"kind":"call_started","eventId":"<id>","timestamp":"<ts>","provider":"stub.trace","model":"stub-1","maxTokens":16,"promptSummary":"hello"}
+{"kind":"call_finished","eventId":"<id>","timestamp":"<ts>","provider":"stub.trace","model":"stub-1","latencyMs":0,"inputTokens":0,"outputTokens":0,"cachedInputTokens":0,"cacheWriteTokens":0,"totalTokens":0,"usd":0}
