pi-claude-agent-sdk 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,13 +21,13 @@ pi install npm:pi-claude-agent-sdk
21
21
 
22
22
  ## Provider
23
23
 
24
- Use `/model` to select `claude-bridge/claude-fable-5`, `claude-bridge/claude-opus-5`, `claude-bridge/claude-opus-4-8`, `claude-bridge/claude-opus-4-7`, `claude-bridge/claude-opus-4-6`, `claude-bridge/claude-sonnet-5`, `claude-bridge/claude-sonnet-4-6`, or `claude-bridge/claude-haiku-4-5`.
24
+ Use `/model` to select `claude-bridge/claude-fable-5-1`, `claude-bridge/claude-fable-5`, `claude-bridge/claude-opus-5`, `claude-bridge/claude-opus-4-8`, `claude-bridge/claude-opus-4-7`, `claude-bridge/claude-opus-4-6`, `claude-bridge/claude-sonnet-5`, `claude-bridge/claude-sonnet-4-6`, or `claude-bridge/claude-haiku-4-5`. The `fable` shortcut resolves to Fable 5.1. Fable 5.1 needs Claude Code **2.1.251 or newer** (the SDK's bundled CLI is 2.1.257). If you point `provider.pathToClaudeCodeExecutable` at an older binary, or a later model outruns the bundle, the bridge uses a current `claude` on PATH when it finds one.
25
25
 
26
26
  Behind the scenes, pi's tools are bridged to Claude Code but it should all work like normal in pi. Bash commands get a 120-second default timeout (matching Claude Code's default) since pi's bash has no timeout by default. Skills in pi are copied over to Claude Code's system prompt so should work as they would with any other pi provider. Steering works mid-turn: a message sent while Claude is running a tool reaches it at that tool boundary, not after the whole turn finishes.
27
27
 
28
28
  **Authentication:** the bridge requires an Anthropic OAuth credential (or API key) configured in Pi and uses Pi's normal token refresh. Claude Code login and inherited Claude/Anthropic authentication settings are deliberately ignored, so configure Anthropic authentication in Pi before using the provider.
29
29
 
30
- **1M Context:** Opus 5, Opus 4.8, and Opus 4.7 get 1M context by default. Opus 4.6 only gets 1M if you're on a Max plan or pay for Extra Usage. Sonnet 4.6 only gets 1M if you pay for Extra Usage. You will need to set `provider.plan` and/or `provider.longContextExtraUsage` for 1M context in Opus 4.6/Sonnet 4.6 as described in [Configuration](#configuration).
30
+ **1M Context:** Fable 5.1, Fable 5, Opus 5, Opus 4.8, and Opus 4.7 get 1M context by default. Opus 4.6 only gets 1M if you're on a Max plan or pay for Extra Usage. Sonnet 4.6 only gets 1M if you pay for Extra Usage. You will need to set `provider.plan` and/or `provider.longContextExtraUsage` for 1M context in Opus 4.6/Sonnet 4.6 as described in [Configuration](#configuration).
31
31
 
32
32
  ## Configuration
33
33
 
@@ -49,7 +49,7 @@ Config: `~/.pi/agent/claude-bridge.json` (global) or the project Pi config direc
49
49
  - `longContextExtraUsage` — set to `true` to enable 1M models that cost money through Extra Usage. It enables Sonnet 4.6 with 1M on every plan and Opus 4.6 with 1M on Pro. Not needed for Opus 4.7 or 4.8.
50
50
  - `strictMcpConfig` — block MCP servers from `~/.claude.json` / `.mcp.json` (default `true`). Cloud MCP (Gmail/Drive via claude.ai OAuth) is always blocked.
51
51
  - `autoMemoryEnabled` — enable Claude Code's auto-memory system (default `false`)
52
- - `pathToClaudeCodeExecutable` — path to the `claude` binary. Useful if your OS/filesystem has the SDK's bundled musl/glibc binaries in a place where they can't run. For example, with Nix you can set the binary to e.g. `"/home/you/.nix-profile/bin/claude"`.
52
+ - `pathToClaudeCodeExecutable` — path to the `claude` binary. Useful if your OS/filesystem has the SDK's bundled musl/glibc binaries in a place where they can't run, or to pin a specific CLI. For example, with Nix you can set the binary to e.g. `"/home/you/.nix-profile/bin/claude"`.
53
53
 
54
54
  **Extension providers and models.json:** pi's `modelOverrides` in `~/.pi/agent/models.json` do not currently apply to extension-registered providers (like claude-bridge). Overriding `contextWindow` or other fields requires editing `src/models.ts` directly.
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-agent-sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "private": false,
5
5
  "description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider.",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "type": "module",
42
42
  "dependencies": {
43
- "@anthropic-ai/claude-agent-sdk": "^0.2.141",
43
+ "@anthropic-ai/claude-agent-sdk": "^0.3.257",
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "cc-session-io": "^0.4.0",
46
46
  "change-case": "^5.4.4"
@@ -50,7 +50,7 @@
50
50
  "@earendil-works/pi-coding-agent": ">=0.82.1"
51
51
  },
52
52
  "devDependencies": {
53
- "@anthropic-ai/sdk": "^0.73.0",
53
+ "@anthropic-ai/sdk": "^0.93.0",
54
54
  "@earendil-works/pi-ai": "^0.83.0",
55
55
  "@earendil-works/pi-coding-agent": "^0.83.0",
56
56
  "@types/node": "^24.13.2",
@@ -0,0 +1,149 @@
1
+ // Pick a Claude Code CLI that can actually serve the selected model.
2
+ // The Agent SDK bundles its own binary (`claudeCodeVersion` in its package.json).
3
+ // Some models (Fable 5.1) 400 against an older CLI; when the bundle is below
4
+ // the model's minimum we use a current `claude` on PATH, matching what the
5
+ // README already told people to set via pathToClaudeCodeExecutable.
6
+
7
+ import { spawnSync } from "node:child_process";
8
+ import { accessSync, constants, readFileSync } from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import { delimiter, dirname, join } from "node:path";
11
+ import { minClaudeCodeVersionForModel } from "./models.js";
12
+
13
+ const require = createRequire(import.meta.url);
14
+
15
+ export type ClaudeVersion = { major: number; minor: number; patch: number };
16
+
17
+ export type ClaudeExecutableSource = "configured" | "bundled" | "path";
18
+
19
+ export type ClaudeExecutableResolution = {
20
+ /** Absolute path for the SDK, or undefined to use the bundled CLI. */
21
+ path?: string;
22
+ source: ClaudeExecutableSource;
23
+ /** Set when no available CLI meets the model's minimum version. */
24
+ error?: string;
25
+ };
26
+
27
+ export type ClaudeExecutableDeps = {
28
+ bundledVersion?: () => ClaudeVersion | undefined;
29
+ findOnPath?: () => string | undefined;
30
+ readVersion?: (executable: string) => ClaudeVersion | undefined;
31
+ };
32
+
33
+ const versionCache = new Map<string, ClaudeVersion | undefined>();
34
+
35
+ export function parseClaudeVersion(text: string): ClaudeVersion | undefined {
36
+ const match = text.match(/(\d+)\.(\d+)\.(\d+)/);
37
+ if (!match) return undefined;
38
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
39
+ }
40
+
41
+ export function formatClaudeVersion(version: ClaudeVersion): string {
42
+ return `${version.major}.${version.minor}.${version.patch}`;
43
+ }
44
+
45
+ export function compareClaudeVersion(a: ClaudeVersion, b: ClaudeVersion): number {
46
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
47
+ }
48
+
49
+ export function readBundledClaudeCodeVersion(): ClaudeVersion | undefined {
50
+ try {
51
+ // The SDK does not export ./package.json; read the file next to its entry.
52
+ const pkgPath = join(dirname(require.resolve("@anthropic-ai/claude-agent-sdk")), "package.json");
53
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { claudeCodeVersion?: string };
54
+ return parseClaudeVersion(pkg.claudeCodeVersion ?? "");
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ export function findExecutableOnPath(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
61
+ const pathEnv = env.PATH ?? env.Path;
62
+ if (!pathEnv) return undefined;
63
+ const exts = process.platform === "win32"
64
+ ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
65
+ : [""];
66
+ const names = process.platform === "win32" && !exts.some((ext) => name.toUpperCase().endsWith(ext.toUpperCase()))
67
+ ? [name, ...exts.map((ext) => name + ext)]
68
+ : [name];
69
+ for (const dir of pathEnv.split(delimiter)) {
70
+ if (!dir) continue;
71
+ for (const candidateName of names) {
72
+ const candidate = join(dir, candidateName);
73
+ try {
74
+ accessSync(candidate, constants.X_OK);
75
+ return candidate;
76
+ } catch {}
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ export function readClaudeCliVersion(executable: string): ClaudeVersion | undefined {
83
+ if (versionCache.has(executable)) return versionCache.get(executable);
84
+ try {
85
+ const result = spawnSync(executable, ["--version"], {
86
+ encoding: "utf8",
87
+ timeout: 8000,
88
+ env: process.env,
89
+ windowsHide: true,
90
+ });
91
+ const version = parseClaudeVersion(`${result.stdout ?? ""}\n${result.stderr ?? ""}`);
92
+ versionCache.set(executable, version);
93
+ return version;
94
+ } catch {
95
+ versionCache.set(executable, undefined);
96
+ return undefined;
97
+ }
98
+ }
99
+
100
+ function tooOldError(modelId: string, min: ClaudeVersion, found: string): string {
101
+ return `${modelId} requires Claude Code ${formatClaudeVersion(min)} or newer (${found}). Install a current claude on PATH, or set provider.pathToClaudeCodeExecutable in ~/.pi/agent/claude-bridge.json.`;
102
+ }
103
+
104
+ export function resolveClaudeCodeExecutable(
105
+ modelId: string,
106
+ configured?: string,
107
+ deps: ClaudeExecutableDeps = {},
108
+ ): ClaudeExecutableResolution {
109
+ const bundledVersion = deps.bundledVersion ?? readBundledClaudeCodeVersion;
110
+ const findOnPath = deps.findOnPath ?? (() => findExecutableOnPath("claude"));
111
+ const readVersion = deps.readVersion ?? readClaudeCliVersion;
112
+ const minText = minClaudeCodeVersionForModel(modelId);
113
+ const min = minText ? parseClaudeVersion(minText) : undefined;
114
+
115
+ if (configured) {
116
+ if (min) {
117
+ const version = readVersion(configured);
118
+ if (version && compareClaudeVersion(version, min) < 0) {
119
+ return {
120
+ source: "configured",
121
+ error: tooOldError(modelId, min, `provider.pathToClaudeCodeExecutable is ${formatClaudeVersion(version)} at ${configured}`),
122
+ };
123
+ }
124
+ }
125
+ return { path: configured, source: "configured" };
126
+ }
127
+
128
+ if (!min) return { source: "bundled" };
129
+
130
+ const bundled = bundledVersion();
131
+ if (bundled && compareClaudeVersion(bundled, min) >= 0) {
132
+ return { source: "bundled" };
133
+ }
134
+
135
+ const pathClaude = findOnPath();
136
+ if (pathClaude) {
137
+ const version = readVersion(pathClaude);
138
+ if (version && compareClaudeVersion(version, min) >= 0) {
139
+ return { path: pathClaude, source: "path" };
140
+ }
141
+ }
142
+
143
+ if (bundled) {
144
+ return { source: "bundled", error: tooOldError(modelId, min, `bundled CLI is ${formatClaudeVersion(bundled)}`) };
145
+ }
146
+
147
+ // Bundle version unknown and PATH missing/unreadable: let the SDK try.
148
+ return { source: "bundled" };
149
+ }
package/src/convert.ts CHANGED
@@ -116,10 +116,16 @@ export type DroppedContent = {
116
116
  other: Map<string, number>;
117
117
  };
118
118
 
119
- /** Convert pi message array to Anthropic API format. */
119
+ /** Convert pi message array to Anthropic API format.
120
+ *
121
+ * `dropThinking` strips every thinking block, including ones we minted. Fable 5.1
122
+ * binds thinking to the conversation prefix, so a rebuild that keeps the blocks
123
+ * and rewrites system/tools is a 400. Removing a leading run of thinking is the
124
+ * documented-safe alternative. */
120
125
  export function convertPiMessages(
121
126
  messages: PiMessage[],
122
127
  customToolNameToSdk?: Map<string, string>,
128
+ dropThinking = false,
123
129
  ): { anthropicMessages: SessionMessage[]; sanitizedIds: Map<string, string>; dropped: DroppedContent } {
124
130
  const anthropicMessages = [];
125
131
  const sanitizedIds = new Map();
@@ -160,7 +166,7 @@ export function convertPiMessages(
160
166
  // by any other provider — including pi's own Anthropic provider — is
161
167
  // not ours to hand back, and Anthropic rejects ones it can't verify.
162
168
  const sig = block.thinkingSignature;
163
- if (msg.provider === PROVIDER_ID && sig) {
169
+ if (!dropThinking && msg.provider === PROVIDER_ID && sig) {
164
170
  blocks.push({ type: "thinking", thinking: block.thinking ?? "", signature: sig });
165
171
  } else {
166
172
  dropped.thinking++;
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ import { appendFileSync, mkdirSync, realpathSync, statSync } from "fs";
9
9
  import { homedir } from "os";
10
10
  import { dirname, join } from "path";
11
11
  import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
12
- import { applyLongContext, buildModels, claudeCodeModelId, type LongContextSettings } from "./models.js";
12
+ import { adaptiveThinkingAlwaysOn, applyLongContext, buildModels, claudeCodeModelId, thinkingBoundToPrefix, type LongContextSettings } from "./models.js";
13
13
  import { MCP_SERVER_NAME, MCP_TOOL_PREFIX } from "./skills.js";
14
14
  import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
15
15
  import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
@@ -24,6 +24,7 @@ import {
24
24
  import { collectCarriedAttachments, placeCarriedAttachments, type CarriedAttachment } from "./attachments.js";
25
25
  import { createToolServer } from "./mcp-server.js";
26
26
  import { CC_CHILD_ENV, resolveClaudeChildEnv, type AnthropicAuthRegistry } from "./child-env.js";
27
+ import { resolveClaudeCodeExecutable } from "./claude-executable.js";
27
28
 
28
29
  // Compat (#2): use factory if available (pi-ai ≥0.66), else fall back to constructor (gsd-pi etc.)
29
30
  const _piAi = piAi as any;
@@ -87,7 +88,7 @@ function debug(...args: unknown[]) {
87
88
  // Per-query CLI debug capture. When CLAUDE_BRIDGE_DEBUG=1, ask the Claude Code
88
89
  // CLI subprocess to write its own debug log to a file we choose, and also
89
90
  // forward its stderr into our debug stream. Drops straight into the real SDK's
90
- // Options — see @anthropic-ai/claude-agent-sdk sdk.d.ts:1245 (debug, debugFile,
91
+ // Options — see @anthropic-ai/claude-agent-sdk sdk.d.ts:2091 (debug, debugFile,
91
92
  // stderr). Without this, CC's internal view of the world is invisible to us
92
93
  // and "No conversation found" / empty-error reports are unactionable.
93
94
  let nextCliDebugSeq = 1;
@@ -215,8 +216,9 @@ function convertAndImportMessages(
215
216
  messages: Context["messages"],
216
217
  customToolNameToSdk?: Map<string, string>,
217
218
  carried?: readonly CarriedAttachment[],
219
+ dropThinking = false,
218
220
  ): void {
219
- const { anthropicMessages, sanitizedIds, dropped } = convertPiMessages(messages, customToolNameToSdk);
221
+ const { anthropicMessages, sanitizedIds, dropped } = convertPiMessages(messages, customToolNameToSdk, dropThinking);
220
222
 
221
223
  debug(`convertAndImportMessages: ${messages.length} pi msgs → ${anthropicMessages.length} anthropic msgs`);
222
224
  debug(`convertAndImportMessages: imported roles:`, anthropicMessages.map((m, i) => {
@@ -429,7 +431,9 @@ async function runIsolatedSummary(
429
431
  const promptText = extractIsolatedSummaryPrompt(context.messages);
430
432
  const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
431
433
  const compactProviderSettings = loadConfig(cwd).provider;
432
- const claudeExecutable = compactProviderSettings?.pathToClaudeCodeExecutable;
434
+ const claudeExecutableResolution = resolveClaudeCodeExecutable(model.id, compactProviderSettings?.pathToClaudeCodeExecutable);
435
+ if (claudeExecutableResolution.error) throw new Error(claudeExecutableResolution.error);
436
+ const claudeExecutable = claudeExecutableResolution.path;
433
437
  const cliModel = claudeCodeModelId(model, longContextSettings);
434
438
  debug(`compact summary: spawn model=${cliModel} registeredModel=${model.id} promptLen=${promptText.length}`);
435
439
  const childEnv = await resolveClaudeChildEnv(piModelRegistry);
@@ -676,7 +680,7 @@ function syncSharedSession(
676
680
  ...(preserveId ? { sessionId: previousSessionId } : {}),
677
681
  ...(modelId ? { model: modelId } : {}),
678
682
  });
679
- convertAndImportMessages(session, priorMessages, customToolNameToSdk, carried);
683
+ convertAndImportMessages(session, priorMessages, customToolNameToSdk, carried, thinkingBoundToPrefix(modelId ?? ""));
680
684
  session.save();
681
685
  // records, not messages: `messages` filters out the attachment records that
682
686
  // carrying an `@file` expansion across a rebuild writes into the same file.
@@ -1491,6 +1495,19 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
1491
1495
  const queryCtx = isReentrant ? new QueryContext() : ctx();
1492
1496
  debug(`provider: fresh query setup, isReentrant=${isReentrant}, activeContexts=${activeQueryContexts.size}`);
1493
1497
 
1498
+ // Fail before claiming a stream if this model needs a newer CLI than we have.
1499
+ const claudeExecutableResolution = resolveClaudeCodeExecutable(model.id, providerSettings.pathToClaudeCodeExecutable);
1500
+ if (claudeExecutableResolution.error) {
1501
+ debug(`provider: ${claudeExecutableResolution.error}`);
1502
+ stream.push({ type: "error", reason: "error", error: newAssistantOutput(model, "", "error", claudeExecutableResolution.error) });
1503
+ stream.end();
1504
+ return stream;
1505
+ }
1506
+ const claudeExecutable = claudeExecutableResolution.path;
1507
+ if (claudeExecutableResolution.source === "path") {
1508
+ debug(`provider: using PATH claude ${claudeExecutable} (bundled CLI is too old for ${model.id})`);
1509
+ }
1510
+
1494
1511
  // Resolved first: an unaccountable system prompt throws, and doing that before
1495
1512
  // anything is claimed or reset leaves no half-built query behind — in particular
1496
1513
  // no stream claimed on the shared context that nobody will ever end.
@@ -1560,7 +1577,6 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
1560
1577
  // programmatically and ignore filesystem MCP entries — applied unconditionally because
1561
1578
  // settingSources is left at CC's default, which loads all sources.
1562
1579
  const strictMcpConfigEnabled = providerSettings.strictMcpConfig !== false;
1563
- const claudeExecutable = providerSettings.pathToClaudeCodeExecutable;
1564
1580
 
1565
1581
  // Prefer the model's own thinkingLevelMap when present (pi-ai 0.72+ ships
1566
1582
  // per-model overrides — e.g. opus-4-7 wants xhigh→xhigh, not xhigh→max).
@@ -1574,7 +1590,9 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
1574
1590
  if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
1575
1591
  // Opus 4.7 defaults thinking.display to "omitted" (empty thinking text in stream).
1576
1592
  // Force summarized so thinking_delta events arrive. See anthropics/claude-agent-sdk-python#830.
1577
- if (effort) extraArgs["thinking-display"] = "summarized";
1593
+ // Fable 5 / 5.1 always think and also default to omitted, even when we pass no effort
1594
+ // (CC then uses the model default, high).
1595
+ if (effort || adaptiveThinkingAlwaysOn(model.id)) extraArgs["thinking-display"] = "summarized";
1578
1596
 
1579
1597
  // Suppress claude.ai cloud MCP servers (Figma/Canva/etc. auto-discovered via OAuth
1580
1598
  // when the user is logged into Anthropic). These are a separate code path from
package/src/models.ts CHANGED
@@ -2,14 +2,42 @@
2
2
  // `resolveModel` returns the first partial match, so `opus` resolves to the first-listed opus entry.
3
3
  // Extracted from index.ts so tests can import without activating the extension.
4
4
 
5
- export const MODEL_IDS_IN_ORDER = ["claude-fable-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-5", "claude-sonnet-4-6", "claude-haiku-4-5"];
5
+ export const MODEL_IDS_IN_ORDER = ["claude-fable-5-1", "claude-fable-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-5", "claude-sonnet-4-6", "claude-haiku-4-5"];
6
+
7
+ const TWO_HUNDRED_K_CONTEXT = 200_000;
8
+ const ONE_M_CONTEXT = 1_000_000;
9
+
10
+ /** Catalog stubs for IDs Claude Code already serves that the installed pi-ai has
11
+ * not listed yet. Prefer pi-ai when it has the entry. Fable 5.1 shipped
12
+ * 2026-09-01; pi-ai 0.84.4 (2026-08-28) does not include it. */
13
+ export const FALLBACK_MODELS: Record<string, {
14
+ id: string;
15
+ name: string;
16
+ reasoning: boolean;
17
+ input: string[];
18
+ contextWindow: number;
19
+ maxTokens: number;
20
+ thinkingLevelMap?: Record<string, string | null>;
21
+ }> = {
22
+ "claude-fable-5-1": {
23
+ id: "claude-fable-5-1",
24
+ name: "Claude Fable 5.1",
25
+ reasoning: true,
26
+ input: ["text", "image"],
27
+ contextWindow: ONE_M_CONTEXT,
28
+ maxTokens: 128_000,
29
+ // Same shape as pi-ai's claude-fable-5: adaptive thinking, xhigh visible.
30
+ thinkingLevelMap: { off: null, xhigh: "xhigh", max: "max" },
31
+ },
32
+ };
6
33
 
7
34
  // Project pi-ai's model entries down to the fields pi's registerProvider expects,
8
- // and keep MODEL_IDS_IN_ORDER ordering. IDs missing from pi-ai are silently dropped.
9
- // Context-dependent display labels are applied after plan/long-context config is known.
35
+ // and keep MODEL_IDS_IN_ORDER ordering. IDs missing from pi-ai are silently dropped
36
+ // unless FALLBACK_MODELS has a stub. Context-dependent display labels are applied
37
+ // after plan/long-context config is known.
10
38
  export function buildModels<T extends { id: string; [key: string]: any }>(piAiModels: T[]) {
11
39
  return MODEL_IDS_IN_ORDER
12
- .map((id) => piAiModels.find((m) => m.id === id))
40
+ .map((id) => piAiModels.find((m) => m.id === id) ?? FALLBACK_MODELS[id])
13
41
  .filter((m) => m != null)
14
42
  // Forward thinkingLevelMap so pi-ai's per-model overrides (e.g. opus-4-8
15
43
  // mapping xhigh→xhigh and max→max) are visible to the effort lookup.
@@ -32,9 +60,6 @@ export type ClaudeCodeRuntimeModel = {
32
60
  contextWindow: number;
33
61
  };
34
62
 
35
- const TWO_HUNDRED_K_CONTEXT = 200_000;
36
- const ONE_M_CONTEXT = 1_000_000;
37
-
38
63
  // Measured Claude Agent SDK subscription/OAuth behavior. Do not infer this from
39
64
  // pi-ai's advertised contextWindow: bare Opus 4.7 serves 1M, bare Opus 4.8 does
40
65
  // not, and [1m] entitlement differs by model. See diag/CONTEXT-SIZE.md.
@@ -53,6 +78,11 @@ export function resolveClaudeCodeRuntimeModel(modelId: string, settings: LongCon
53
78
  contextWindow: useOneM ? ONE_M_CONTEXT : TWO_HUNDRED_K_CONTEXT,
54
79
  };
55
80
  }
81
+ case "claude-fable-5-1":
82
+ // 1M is the default and the maximum, billed at standard rates across the
83
+ // whole window (no Extra Usage). CC still takes the [1m] suffix to request
84
+ // that window, same as Fable 5 / Opus 5.
85
+ return { cliModelId: "claude-fable-5-1[1m]", contextWindow: ONE_M_CONTEXT };
56
86
  case "claude-fable-5":
57
87
  return { cliModelId: "claude-fable-5[1m]", contextWindow: ONE_M_CONTEXT };
58
88
  case "claude-sonnet-5":
@@ -74,9 +104,35 @@ export function claudeCodeModelId(model: { id: string }, settings: LongContextSe
74
104
  return resolveClaudeCodeRuntimeModel(model.id, settings).cliModelId;
75
105
  }
76
106
 
107
+ /** Adaptive thinking is always on. `thinking: enabled` with budget_tokens and
108
+ * `disabled` both 400; omit thinking or send adaptive. `thinking.display`
109
+ * defaults to omitted, so the stream has no thinking text unless we ask. */
110
+ export function adaptiveThinkingAlwaysOn(modelId: string): boolean {
111
+ return modelId === "claude-fable-5-1" || modelId.startsWith("claude-fable-5-1[")
112
+ || modelId === "claude-fable-5" || modelId.startsWith("claude-fable-5[");
113
+ }
114
+
115
+ /** Fable 5.1 binds each thinking block to the conversation prefix. Replaying a
116
+ * block after a rebuild (new system prompt or tools) 400s with "The block is
117
+ * bound to a different conversation". Resume is fine; rebuilds must drop
118
+ * thinking. https://platform.claude.com/docs/en/models/fable-5-1/whats-new-fable-5-1#editing-earlier-turns-invalidates-thinking-blocks */
119
+ export function thinkingBoundToPrefix(modelId: string): boolean {
120
+ return modelId === "claude-fable-5-1" || modelId.startsWith("claude-fable-5-1[");
121
+ }
122
+
123
+ /** Minimum Claude Code CLI version that will accept this model. Undefined
124
+ * means the SDK's bundled CLI is fine. Fable 5.1 400s on 2.1.141 with
125
+ * "version 2.1.251 or newer is required". */
126
+ export function minClaudeCodeVersionForModel(modelId: string): string | undefined {
127
+ if (modelId === "claude-fable-5-1" || modelId.startsWith("claude-fable-5-1[")) return "2.1.251";
128
+ return undefined;
129
+ }
130
+
77
131
  export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
78
132
  const lower = input.toLowerCase();
79
- return models.find((m) => m.id === lower || m.id.includes(lower));
133
+ // Exact match first: otherwise `claude-fable-5` would hit `claude-fable-5-1`
134
+ // via includes() when the newer id is listed first for the `fable` shortcut.
135
+ return models.find((m) => m.id === lower) ?? models.find((m) => m.id.includes(lower));
80
136
  }
81
137
 
82
138
  // Produce the model metadata registered with pi. The registered contextWindow must