pi-claude-agent-sdk 0.8.2 → 0.8.4

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**; if the SDK's bundled CLI is older, set `provider.pathToClaudeCodeExecutable` to a current `claude` binary.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-agent-sdk",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "private": false,
5
5
  "description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider.",
6
6
  "keywords": [
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";
@@ -17,6 +17,7 @@ import { QueryContext, ctx } from "./query-state.js";
17
17
  import { makePromptStream, userMessage, type PromptStream } from "./prompt-stream.js";
18
18
  import { claudeCodeSettings, loadConfig, markStartupNoticeShown, type Config } from "./config.js";
19
19
  import {
20
+ getSharedPromptCaptures,
20
21
  projectPromptCapture,
21
22
  PromptCaptures,
22
23
  } from "./prompt-capture.js";
@@ -133,6 +134,10 @@ function diagDump(label: string, data: Record<string, unknown>) {
133
134
  //
134
135
  // On session_shutdown (including /reload), clearSession() resets this so a fresh
135
136
  // registration can occur for the next session.
137
+ //
138
+ // The prompt-capture table is shared the same way (PROMPT_CAPTURES_KEY): skipping
139
+ // re-registration is not enough when the first copy's before_agent_start handler
140
+ // is dropped and a second copy records into a different Map.
136
141
  const ACTIVE_STREAM_SIMPLE_KEY = Symbol.for("claude-bridge:activeStreamSimple");
137
142
 
138
143
  // MODELS is buildModels(getModels("anthropic")) — projection kept in models.js.
@@ -210,8 +215,9 @@ function convertAndImportMessages(
210
215
  messages: Context["messages"],
211
216
  customToolNameToSdk?: Map<string, string>,
212
217
  carried?: readonly CarriedAttachment[],
218
+ dropThinking = false,
213
219
  ): void {
214
- const { anthropicMessages, sanitizedIds, dropped } = convertPiMessages(messages, customToolNameToSdk);
220
+ const { anthropicMessages, sanitizedIds, dropped } = convertPiMessages(messages, customToolNameToSdk, dropThinking);
215
221
 
216
222
  debug(`convertAndImportMessages: ${messages.length} pi msgs → ${anthropicMessages.length} anthropic msgs`);
217
223
  debug(`convertAndImportMessages: imported roles:`, anthropicMessages.map((m, i) => {
@@ -671,7 +677,7 @@ function syncSharedSession(
671
677
  ...(preserveId ? { sessionId: previousSessionId } : {}),
672
678
  ...(modelId ? { model: modelId } : {}),
673
679
  });
674
- convertAndImportMessages(session, priorMessages, customToolNameToSdk, carried);
680
+ convertAndImportMessages(session, priorMessages, customToolNameToSdk, carried, thinkingBoundToPrefix(modelId ?? ""));
675
681
  session.save();
676
682
  // records, not messages: `messages` filters out the attachment records that
677
683
  // carrying an `@file` expansion across a rebuild writes into the same file.
@@ -790,8 +796,10 @@ function showStartupNoticeOnce(): void {
790
796
  }
791
797
 
792
798
  // Captures of what pi assembled per agent; see src/prompt-capture.ts for why this
793
- // is keyed rather than held in a single slot.
794
- const promptCaptures = new PromptCaptures(256, (diagnostic) => {
799
+ // is keyed rather than held in a single slot. Process-wide: a second evaluation
800
+ // of this module (user vs project package root, or a subagent) must record into
801
+ // the same table the first evaluation's streamSimple reads.
802
+ const promptCaptures = getSharedPromptCaptures(() => new PromptCaptures(256, (diagnostic) => {
795
803
  const first = diagnostic.matches[0];
796
804
  debug(
797
805
  `prompt-capture: no match for ${diagnostic.systemPrompt.length}-char system prompt. `
@@ -801,7 +809,7 @@ const promptCaptures = new PromptCaptures(256, (diagnostic) => {
801
809
  : "no known captures to compare against."
802
810
  ) + ` known keys=${diagnostic.matches.length}`,
803
811
  );
804
- });
812
+ }));
805
813
 
806
814
  /** Whatever a settled session left behind, named in one greppable line.
807
815
  *
@@ -1567,7 +1575,9 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
1567
1575
  if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
1568
1576
  // Opus 4.7 defaults thinking.display to "omitted" (empty thinking text in stream).
1569
1577
  // Force summarized so thinking_delta events arrive. See anthropics/claude-agent-sdk-python#830.
1570
- if (effort) extraArgs["thinking-display"] = "summarized";
1578
+ // Fable 5 / 5.1 always think and also default to omitted, even when we pass no effort
1579
+ // (CC then uses the model default, high).
1580
+ if (effort || adaptiveThinkingAlwaysOn(model.id)) extraArgs["thinking-display"] = "summarized";
1571
1581
 
1572
1582
  // Suppress claude.ai cloud MCP servers (Figma/Canva/etc. auto-discovered via OAuth
1573
1583
  // 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,27 @@ 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
+
77
123
  export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
78
124
  const lower = input.toLowerCase();
79
- return models.find((m) => m.id === lower || m.id.includes(lower));
125
+ // Exact match first: otherwise `claude-fable-5` would hit `claude-fable-5-1`
126
+ // via includes() when the newer id is listed first for the `fable` shortcut.
127
+ return models.find((m) => m.id === lower) ?? models.find((m) => m.id.includes(lower));
80
128
  }
81
129
 
82
130
  // Produce the model metadata registered with pi. The registered contextWindow must
@@ -162,7 +162,10 @@ export class PromptCaptures {
162
162
  + `Claude Code would receive none of this turn's context files, skills or custom instructions. `
163
163
  + `The usual cause is an extension loaded after claude-bridge that rewrites the system prompt from before_agent_start — `
164
164
  + `one that wraps it is fine, one that rebuilds or strips it leaves nothing to match. `
165
- + `(Also possible: pi rebuilt the prompt outside before_agent_start — a late-registered tool or fresh resource discovery.)`,
165
+ + `(Also possible: pi rebuilt the prompt outside before_agent_start — a late-registered tool or fresh resource discovery.)`
166
+ + (this.captures.size === 0
167
+ ? ` Zero known also means before_agent_start never recorded into this table — often a second copy of this extension loaded from another package root after the first registered the provider.`
168
+ : ""),
166
169
  );
167
170
  }
168
171
 
@@ -232,6 +235,30 @@ export class PromptCaptures {
232
235
  }
233
236
  }
234
237
 
238
+ /** Process-wide slot for the capture table.
239
+ *
240
+ * `src/index.ts` is evaluated once per package root. Pi's pre-trust pass loads
241
+ * the user install, which registers the provider; the post-trust pass then
242
+ * loads the project install as a different module and drops the first copy's
243
+ * event handlers. A per-module Map means `before_agent_start` records into a
244
+ * table the live `streamSimple` never reads.
245
+ *
246
+ * Symbol.for shares one table across those evaluations, the same way
247
+ * `ACTIVE_STREAM_SIMPLE_KEY` shares the stream. Do not use `instanceof
248
+ * PromptCaptures` to recognize the stored value: two package roots evaluate
249
+ * two copies of the class, so a cross-realm check would replace the table
250
+ * the first copy's stream already closed over. */
251
+ export const PROMPT_CAPTURES_KEY = Symbol.for("claude-bridge:promptCaptures");
252
+
253
+ export function getSharedPromptCaptures(create: () => PromptCaptures): PromptCaptures {
254
+ const g = globalThis as Record<symbol, unknown>;
255
+ const existing = g[PROMPT_CAPTURES_KEY];
256
+ if (existing) return existing as PromptCaptures;
257
+ const created = create();
258
+ g[PROMPT_CAPTURES_KEY] = created;
259
+ return created;
260
+ }
261
+
235
262
  export function projectPromptCapture(
236
263
  capture: PromptCapture,
237
264
  options: { skillReadTool: SkillReadTool },