@vanillagreen/pi-claude-bridge 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/bundle/index.js +89 -5
- package/package.json +1 -1
- package/src/config.ts +39 -2
- package/src/index.ts +28 -3
- package/src/models.ts +49 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Forked from [`elidickinson/pi-claude-bridge`](https://github.com/elidickinson/pi
|
|
|
9
9
|
|
|
10
10
|
## Highlights
|
|
11
11
|
|
|
12
|
-
- `claude-bridge/claude-
|
|
12
|
+
- `claude-bridge/claude-fable-5`, Opus 4.8, Opus 4.7, Sonnet, and Haiku in `/model`.
|
|
13
13
|
- Pi tool calls run on Pi; Claude Code handles reasoning.
|
|
14
14
|
- Tool-use turns block until Pi-delivered tool results reach Claude Code, including persistent subagent panes.
|
|
15
15
|
- Session continuity across normal turns, `/compact`, tree navigation, and abort recovery.
|
|
@@ -45,6 +45,8 @@ Extra Pi context is off by default. Enable per item in the extension manager whe
|
|
|
45
45
|
|
|
46
46
|
Open `/extensions:settings`; settings appear under the **Claude Bridge** tab.
|
|
47
47
|
|
|
48
|
+
Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, vstack Pi extensions read user/global settings only.
|
|
49
|
+
|
|
48
50
|
### General
|
|
49
51
|
|
|
50
52
|
| Setting | What it does |
|
|
@@ -90,6 +92,10 @@ Pi does not have a native `max` thinking level; it exposes up to `xhigh`, and ea
|
|
|
90
92
|
|
|
91
93
|
Keys may be bare model IDs (`claude-opus-4-8`), `claude-bridge/<id>`, or `*` for all bridge models. Values are `low`, `medium`, `high`, `xhigh`, or `max`.
|
|
92
94
|
|
|
95
|
+
### Fable 5 caveat
|
|
96
|
+
|
|
97
|
+
The bridge registers `claude-bridge/claude-fable-5` and `claude-bridge/claude-opus-4-8` even when Pi's Anthropic model registry has not shipped those entries yet. For Fable 5, the bridge asks Claude Code to use Opus 4.8 as the availability fallback and preserves Claude Code's content-safety fallback events so Pi labels rerouted turns as Opus 4.8. Content-safety fallback still depends on Claude Code's own Fable 5 support; use Claude Code 2.1.170 or newer, and set `ANTHROPIC_DEFAULT_FABLE_MODEL` / `ANTHROPIC_DEFAULT_OPUS_MODEL` yourself when routing provider-specific model IDs through Bedrock, Vertex, or Foundry.
|
|
98
|
+
|
|
93
99
|
## Extra usage and rate limits
|
|
94
100
|
|
|
95
101
|
Claude Code's `/extra-usage` local command works through the Claude Agent SDK. In Pi, use `/claude-bridge:extra` to run that flow from claude-bridge. Persist automatic launch on extra-usage errors with **Allow extra usage helper** in `/extensions:settings`.
|
|
@@ -98,7 +104,7 @@ When Claude Code emits rate-limit reset metadata, the bridge shows one red ASCII
|
|
|
98
104
|
|
|
99
105
|
Allowed-warning rate-limit events are filtered before user notification. The bridge normalizes unambiguous numeric utilization (`0 < value < 1` as fractional, `1 < value <= 100` as percent), suppresses low or unit-ambiguous values such as exact `1`, and only shows a neutral warning at 80%+ instead of claiming an unverified `% used` value. Check Claude Code `/usage` for exact allowed-warning utilization.
|
|
100
106
|
|
|
101
|
-
If Claude Code accepts a turn but produces no assistant/tool output, the bridge treats that stream-idle stall as a retryable overload/rate-limit failure: it closes the stalled Claude Code subprocess, emits a normal assistant error with a backoff hint, and lets
|
|
107
|
+
If Claude Code accepts a turn but produces no assistant/tool output, the bridge treats that stream-idle stall as a retryable overload/rate-limit failure: it closes the stalled Claude Code subprocess, emits a normal assistant error with a backoff hint, and lets `pi-agents-tmux` reuse its existing rate-limit retry ladder. Tune the first-output timeout with `CLAUDE_BRIDGE_STREAM_IDLE_TIMEOUT` (bare numbers are seconds; suffixes `ms`, `s`, and `m` are accepted). Default: `90s`; set `0` to disable.
|
|
102
108
|
|
|
103
109
|
## Debugging
|
|
104
110
|
|
package/bundle/index.js
CHANGED
|
@@ -22237,9 +22237,41 @@ function convertPiMessages(messages, customToolNameToSdk) {
|
|
|
22237
22237
|
}
|
|
22238
22238
|
|
|
22239
22239
|
// src/models.ts
|
|
22240
|
-
var
|
|
22240
|
+
var FABLE_MODEL_ID = "claude-fable-5";
|
|
22241
|
+
var FABLE_FALLBACK_MODEL_ID = "claude-opus-4-8";
|
|
22242
|
+
function fallbackModelForPrimaryModel(modelId) {
|
|
22243
|
+
return modelId === FABLE_MODEL_ID ? FABLE_FALLBACK_MODEL_ID : void 0;
|
|
22244
|
+
}
|
|
22245
|
+
var MODEL_IDS_IN_ORDER = [
|
|
22246
|
+
FABLE_MODEL_ID,
|
|
22247
|
+
FABLE_FALLBACK_MODEL_ID,
|
|
22248
|
+
"claude-opus-4-7",
|
|
22249
|
+
"claude-opus-4-6",
|
|
22250
|
+
"claude-sonnet-4-6",
|
|
22251
|
+
"claude-haiku-4-5"
|
|
22252
|
+
];
|
|
22253
|
+
var FALLBACK_MODELS = {
|
|
22254
|
+
[FABLE_MODEL_ID]: {
|
|
22255
|
+
id: FABLE_MODEL_ID,
|
|
22256
|
+
name: "Claude Fable 5",
|
|
22257
|
+
reasoning: true,
|
|
22258
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
22259
|
+
input: ["text", "image"],
|
|
22260
|
+
contextWindow: 1e6,
|
|
22261
|
+
maxTokens: 128e3
|
|
22262
|
+
},
|
|
22263
|
+
[FABLE_FALLBACK_MODEL_ID]: {
|
|
22264
|
+
id: FABLE_FALLBACK_MODEL_ID,
|
|
22265
|
+
name: "Claude Opus 4.8",
|
|
22266
|
+
reasoning: true,
|
|
22267
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
22268
|
+
input: ["text", "image"],
|
|
22269
|
+
contextWindow: 1e6,
|
|
22270
|
+
maxTokens: 128e3
|
|
22271
|
+
}
|
|
22272
|
+
};
|
|
22241
22273
|
function buildModels(piAiModels) {
|
|
22242
|
-
return MODEL_IDS_IN_ORDER.map((id) => piAiModels.find((m4) => m4.id === id)).filter((m4) => m4 != null).map(({ id, name, reasoning, input, contextWindow, maxTokens, thinkingLevelMap }) => ({
|
|
22274
|
+
return MODEL_IDS_IN_ORDER.map((id) => piAiModels.find((m4) => m4.id === id) ?? FALLBACK_MODELS[id]).filter((m4) => m4 != null).map(({ id, name, reasoning, input, contextWindow, maxTokens, thinkingLevelMap }) => ({
|
|
22243
22275
|
id,
|
|
22244
22276
|
name,
|
|
22245
22277
|
reasoning,
|
|
@@ -22638,8 +22670,34 @@ function projectSettingsPath(cwd) {
|
|
|
22638
22670
|
current = parent;
|
|
22639
22671
|
}
|
|
22640
22672
|
}
|
|
22673
|
+
var PROJECT_TRUST_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.project-trust");
|
|
22674
|
+
function projectTrustRegistry() {
|
|
22675
|
+
const host = globalThis;
|
|
22676
|
+
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
22677
|
+
if (existing) return existing;
|
|
22678
|
+
const created = {};
|
|
22679
|
+
host[PROJECT_TRUST_SYMBOL] = created;
|
|
22680
|
+
return created;
|
|
22681
|
+
}
|
|
22682
|
+
function recordProjectTrust(ctx2) {
|
|
22683
|
+
if (!ctx2.cwd) return;
|
|
22684
|
+
let trusted = true;
|
|
22685
|
+
try {
|
|
22686
|
+
trusted = ctx2.isProjectTrusted?.() === true;
|
|
22687
|
+
} catch {
|
|
22688
|
+
trusted = false;
|
|
22689
|
+
}
|
|
22690
|
+
const registry2 = projectTrustRegistry();
|
|
22691
|
+
if (!registry2.projectSettings) registry2.projectSettings = /* @__PURE__ */ new Map();
|
|
22692
|
+
registry2.projectSettings.set(projectSettingsPath(ctx2.cwd), trusted);
|
|
22693
|
+
}
|
|
22694
|
+
function projectSettingsTrusted(settingsPath) {
|
|
22695
|
+
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
22696
|
+
}
|
|
22641
22697
|
function settingsPaths(cwd) {
|
|
22642
|
-
|
|
22698
|
+
const user = join2(piUserDir(), "settings.json");
|
|
22699
|
+
const project = projectSettingsPath(cwd);
|
|
22700
|
+
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
22643
22701
|
}
|
|
22644
22702
|
function tryParseJson(path) {
|
|
22645
22703
|
if (!existsSync3(path)) return {};
|
|
@@ -22747,7 +22805,9 @@ function managerToConfig(raw) {
|
|
|
22747
22805
|
}
|
|
22748
22806
|
function loadConfig(cwd) {
|
|
22749
22807
|
const global2 = tryParseJson(join2(piUserDir(), "claude-bridge.json"));
|
|
22750
|
-
const
|
|
22808
|
+
const projectSettings = projectSettingsPath(cwd);
|
|
22809
|
+
const trustedProject = projectSettingsTrusted(projectSettings);
|
|
22810
|
+
const project = trustedProject ? tryParseJson(join2(dirname2(projectSettings), "claude-bridge.json")) : {};
|
|
22751
22811
|
const manager = managerToConfig(readManagerConfig(cwd));
|
|
22752
22812
|
const provider = normalizeProviderConfig({ ...global2.provider, ...project.provider, ...manager.provider });
|
|
22753
22813
|
return {
|
|
@@ -38549,6 +38609,13 @@ function finalizeCurrentStream(stopReason) {
|
|
|
38549
38609
|
ctx().currentPiStream.end();
|
|
38550
38610
|
ctx().currentPiStream = null;
|
|
38551
38611
|
}
|
|
38612
|
+
function updateTurnOutputModel(modelId) {
|
|
38613
|
+
const c2 = ctx();
|
|
38614
|
+
if (typeof modelId !== "string" || !modelId || !c2.turnOutput) return;
|
|
38615
|
+
if (c2.turnOutput.model === modelId) return;
|
|
38616
|
+
debug(`provider: active Claude model changed ${c2.turnOutput.model} -> ${modelId}`);
|
|
38617
|
+
c2.turnOutput.model = modelId;
|
|
38618
|
+
}
|
|
38552
38619
|
function processStreamEvent(message, customToolNameToPi, model) {
|
|
38553
38620
|
const c2 = ctx();
|
|
38554
38621
|
if (!c2.currentPiStream || !c2.turnOutput) return;
|
|
@@ -38560,6 +38627,7 @@ function processStreamEvent(message, customToolNameToPi, model) {
|
|
|
38560
38627
|
}
|
|
38561
38628
|
if (event?.type === "message_start") {
|
|
38562
38629
|
c2.resetToolTracking();
|
|
38630
|
+
updateTurnOutputModel(event.message?.model);
|
|
38563
38631
|
if (event.message?.usage) updateUsage(c2.turnOutput, event.message.usage, model);
|
|
38564
38632
|
return;
|
|
38565
38633
|
}
|
|
@@ -38698,6 +38766,7 @@ function processAssistantMessage(message, model, customToolNameToPi) {
|
|
|
38698
38766
|
const c2 = ctx();
|
|
38699
38767
|
const assistantMsg = message.message;
|
|
38700
38768
|
if (!assistantMsg?.content) return;
|
|
38769
|
+
updateTurnOutputModel(assistantMsg.model);
|
|
38701
38770
|
if (c2.turnSawStreamEvent) {
|
|
38702
38771
|
if (appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi)) {
|
|
38703
38772
|
c2.turnSawToolCall = true;
|
|
@@ -38744,6 +38813,8 @@ function processAssistantMessage(message, model, customToolNameToPi) {
|
|
|
38744
38813
|
const toolBlock = c2.turnBlocks[idx];
|
|
38745
38814
|
c2.currentPiStream?.push({ type: "toolcall_start", contentIndex: idx, partial: c2.turnOutput });
|
|
38746
38815
|
c2.currentPiStream?.push({ type: "toolcall_end", contentIndex: idx, toolCall: toolBlock, partial: c2.turnOutput });
|
|
38816
|
+
} else if (block.type === "fallback") {
|
|
38817
|
+
updateTurnOutputModel(block.to?.model);
|
|
38747
38818
|
} else {
|
|
38748
38819
|
debug("processAssistantMessage: unhandled block type", block.type);
|
|
38749
38820
|
}
|
|
@@ -38795,6 +38866,14 @@ async function consumeQuery(sdkQuery, customToolNameToPi, model, cwd, bridgeConf
|
|
|
38795
38866
|
case "system":
|
|
38796
38867
|
if (message.subtype === "init" && message.session_id) {
|
|
38797
38868
|
capturedSessionId = message.session_id;
|
|
38869
|
+
} else if (message.subtype === "model_refusal_fallback") {
|
|
38870
|
+
const originalModel = message.original_model;
|
|
38871
|
+
const fallbackModel = message.fallback_model;
|
|
38872
|
+
updateTurnOutputModel(fallbackModel);
|
|
38873
|
+
debug("consumeQuery: model_refusal_fallback", JSON.stringify({ originalModel, fallbackModel }));
|
|
38874
|
+
if (originalModel === FABLE_MODEL_ID && fallbackModel === FABLE_FALLBACK_MODEL_ID) {
|
|
38875
|
+
safeNotify("Claude bridge switched Fable 5 to Opus 4.8 after Claude Code safety fallback.", "info");
|
|
38876
|
+
}
|
|
38798
38877
|
}
|
|
38799
38878
|
break;
|
|
38800
38879
|
case "user":
|
|
@@ -38949,16 +39028,19 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
38949
39028
|
const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
38950
39029
|
const requestedEffort = options?.reasoning ? model.thinkingLevelMap?.[options.reasoning] ?? REASONING_TO_EFFORT[options.reasoning] : void 0;
|
|
38951
39030
|
const effort = resolveConfiguredEffort(model.id, requestedEffort, providerSettings);
|
|
38952
|
-
const extraArgs = {
|
|
39031
|
+
const extraArgs = {};
|
|
38953
39032
|
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
38954
39033
|
if (effort) extraArgs["thinking-display"] = "summarized";
|
|
39034
|
+
const fallbackModel = fallbackModelForPrimaryModel(model.id);
|
|
38955
39035
|
const childEnv = { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: "0", DISABLE_AUTO_COMPACT: "1" };
|
|
38956
39036
|
const queryOptions = {
|
|
38957
39037
|
cwd,
|
|
39038
|
+
model: model.id,
|
|
38958
39039
|
env: childEnv,
|
|
38959
39040
|
...CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
38960
39041
|
permissionMode: "bypassPermissions",
|
|
38961
39042
|
includePartialMessages: true,
|
|
39043
|
+
...fallbackModel ? { fallbackModel } : {},
|
|
38962
39044
|
...providerSettings.fastMode ? { settings: { fastMode: true } } : {},
|
|
38963
39045
|
systemPrompt: {
|
|
38964
39046
|
type: "preset",
|
|
@@ -38978,6 +39060,7 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
38978
39060
|
"provider: fresh query",
|
|
38979
39061
|
`model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
38980
39062
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
39063
|
+
`fallback=${fallbackModel ?? "none"}`,
|
|
38981
39064
|
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled} fastMode=${providerSettings.fastMode === true}`,
|
|
38982
39065
|
`claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
|
|
38983
39066
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`
|
|
@@ -39238,6 +39321,7 @@ function index_default(pi) {
|
|
|
39238
39321
|
}
|
|
39239
39322
|
};
|
|
39240
39323
|
pi.on("session_start", (event, ctx2) => {
|
|
39324
|
+
recordProjectTrust(ctx2);
|
|
39241
39325
|
piUI = ctx2.ui;
|
|
39242
39326
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
39243
39327
|
clearSession(`session_start:${event.reason}`);
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -76,8 +76,43 @@ function projectSettingsPath(cwd: string): string {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
|
|
80
|
+
|
|
81
|
+
interface ProjectTrustRegistry {
|
|
82
|
+
projectSettings?: Map<string, boolean>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function projectTrustRegistry(): ProjectTrustRegistry {
|
|
86
|
+
const host = globalThis as unknown as Record<PropertyKey, ProjectTrustRegistry | undefined>;
|
|
87
|
+
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
88
|
+
if (existing) return existing;
|
|
89
|
+
const created: ProjectTrustRegistry = {};
|
|
90
|
+
host[PROJECT_TRUST_SYMBOL] = created;
|
|
91
|
+
return created;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function recordProjectTrust(ctx: { cwd?: string; isProjectTrusted?: () => boolean }): void {
|
|
95
|
+
if (!ctx.cwd) return;
|
|
96
|
+
let trusted = true;
|
|
97
|
+
try {
|
|
98
|
+
trusted = ctx.isProjectTrusted?.() === true;
|
|
99
|
+
} catch {
|
|
100
|
+
trusted = false;
|
|
101
|
+
}
|
|
102
|
+
const registry = projectTrustRegistry();
|
|
103
|
+
if (!registry.projectSettings) registry.projectSettings = new Map();
|
|
104
|
+
registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function projectSettingsTrusted(settingsPath: string): boolean {
|
|
108
|
+
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
79
112
|
function settingsPaths(cwd: string): string[] {
|
|
80
|
-
|
|
113
|
+
const user = join(piUserDir(), "settings.json");
|
|
114
|
+
const project = projectSettingsPath(cwd);
|
|
115
|
+
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
export function tryParseJson(path: string): Partial<Config> {
|
|
@@ -202,7 +237,9 @@ function managerToConfig(raw: SettingsRecord): Partial<Config> {
|
|
|
202
237
|
|
|
203
238
|
export function loadConfig(cwd: string): Config {
|
|
204
239
|
const global = tryParseJson(join(piUserDir(), "claude-bridge.json"));
|
|
205
|
-
const
|
|
240
|
+
const projectSettings = projectSettingsPath(cwd);
|
|
241
|
+
const trustedProject = projectSettingsTrusted(projectSettings);
|
|
242
|
+
const project = trustedProject ? tryParseJson(join(dirname(projectSettings), "claude-bridge.json")) : {};
|
|
206
243
|
const manager = managerToConfig(readManagerConfig(cwd));
|
|
207
244
|
const provider = normalizeProviderConfig({ ...global.provider, ...project.provider, ...manager.provider });
|
|
208
245
|
return {
|
package/src/index.ts
CHANGED
|
@@ -11,13 +11,13 @@ import { resolve as pathResolve } from "path";
|
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { delimiter, dirname, join } from "path";
|
|
13
13
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
14
|
-
import { buildModels } from "./models.js";
|
|
14
|
+
import { FABLE_FALLBACK_MODEL_ID, FABLE_MODEL_ID, buildModels, fallbackModelForPrimaryModel } from "./models.js";
|
|
15
15
|
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, extractSkillsBlock } from "./skills.js";
|
|
16
16
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
17
17
|
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
|
|
18
18
|
import { QueryContext, ctx, stackDepth, pushContext, popContext } from "./query-state.js";
|
|
19
19
|
import { findUnpairedToolUses, summarizeMissingToolNames, type MissingToolResult } from "./tool-pairing-audit.js";
|
|
20
|
-
import { loadConfig, normalizeEffortLevel, type Config } from "./config.js";
|
|
20
|
+
import { loadConfig, normalizeEffortLevel, recordProjectTrust, type Config } from "./config.js";
|
|
21
21
|
import { extractAgentsAppend } from "./agents-md.js";
|
|
22
22
|
import { buildPromptContextAppend } from "./prompt-context.js";
|
|
23
23
|
import { jsonSchemaToZodShape } from "./typebox-to-zod.js";
|
|
@@ -1401,6 +1401,14 @@ function finalizeCurrentStream(stopReason?: string): void {
|
|
|
1401
1401
|
ctx().currentPiStream = null;
|
|
1402
1402
|
}
|
|
1403
1403
|
|
|
1404
|
+
function updateTurnOutputModel(modelId: unknown): void {
|
|
1405
|
+
const c = ctx();
|
|
1406
|
+
if (typeof modelId !== "string" || !modelId || !c.turnOutput) return;
|
|
1407
|
+
if (c.turnOutput.model === modelId) return;
|
|
1408
|
+
debug(`provider: active Claude model changed ${c.turnOutput.model} -> ${modelId}`);
|
|
1409
|
+
c.turnOutput.model = modelId;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1404
1412
|
/** Maps Anthropic stream events to pi stream events (text, thinking, toolcall).
|
|
1405
1413
|
* On message_stop with tool_use: ends currentPiStream so pi can execute the tool. */
|
|
1406
1414
|
export function processStreamEvent(
|
|
@@ -1419,6 +1427,7 @@ export function processStreamEvent(
|
|
|
1419
1427
|
|
|
1420
1428
|
if (event?.type === "message_start") {
|
|
1421
1429
|
c.resetToolTracking();
|
|
1430
|
+
updateTurnOutputModel(event.message?.model);
|
|
1422
1431
|
if (event.message?.usage) updateUsage(c.turnOutput, event.message.usage, model);
|
|
1423
1432
|
return;
|
|
1424
1433
|
}
|
|
@@ -1579,6 +1588,7 @@ export function processAssistantMessage(message: SDKMessage, model: Model<any>,
|
|
|
1579
1588
|
const c = ctx();
|
|
1580
1589
|
const assistantMsg = (message as any).message;
|
|
1581
1590
|
if (!assistantMsg?.content) return;
|
|
1591
|
+
updateTurnOutputModel(assistantMsg.model);
|
|
1582
1592
|
if (c.turnSawStreamEvent) {
|
|
1583
1593
|
// Claude Agent SDK can yield the completed assistant message before (or
|
|
1584
1594
|
// instead of) a stream_event message_stop for a tool-use turn. Treat that
|
|
@@ -1630,6 +1640,8 @@ export function processAssistantMessage(message: SDKMessage, model: Model<any>,
|
|
|
1630
1640
|
const toolBlock = c.turnBlocks[idx];
|
|
1631
1641
|
c.currentPiStream?.push({ type: "toolcall_start", contentIndex: idx, partial: c.turnOutput });
|
|
1632
1642
|
c.currentPiStream?.push({ type: "toolcall_end", contentIndex: idx, toolCall: toolBlock as any, partial: c.turnOutput });
|
|
1643
|
+
} else if (block.type === "fallback") {
|
|
1644
|
+
updateTurnOutputModel(block.to?.model);
|
|
1633
1645
|
} else {
|
|
1634
1646
|
debug("processAssistantMessage: unhandled block type", block.type);
|
|
1635
1647
|
}
|
|
@@ -1698,6 +1710,14 @@ async function consumeQuery(
|
|
|
1698
1710
|
case "system":
|
|
1699
1711
|
if ((message as any).subtype === "init" && (message as any).session_id) {
|
|
1700
1712
|
capturedSessionId = (message as any).session_id;
|
|
1713
|
+
} else if ((message as any).subtype === "model_refusal_fallback") {
|
|
1714
|
+
const originalModel = (message as any).original_model;
|
|
1715
|
+
const fallbackModel = (message as any).fallback_model;
|
|
1716
|
+
updateTurnOutputModel(fallbackModel);
|
|
1717
|
+
debug("consumeQuery: model_refusal_fallback", JSON.stringify({ originalModel, fallbackModel }));
|
|
1718
|
+
if (originalModel === FABLE_MODEL_ID && fallbackModel === FABLE_FALLBACK_MODEL_ID) {
|
|
1719
|
+
safeNotify("Claude bridge switched Fable 5 to Opus 4.8 after Claude Code safety fallback.", "info");
|
|
1720
|
+
}
|
|
1701
1721
|
}
|
|
1702
1722
|
break;
|
|
1703
1723
|
case "user":
|
|
@@ -1908,11 +1928,12 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1908
1928
|
: undefined;
|
|
1909
1929
|
const effort = resolveConfiguredEffort(model.id, requestedEffort, providerSettings);
|
|
1910
1930
|
|
|
1911
|
-
const extraArgs: Record<string, string | null> = {
|
|
1931
|
+
const extraArgs: Record<string, string | null> = {};
|
|
1912
1932
|
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
1913
1933
|
// Opus 4.7 defaults thinking.display to "omitted" (empty thinking text in stream).
|
|
1914
1934
|
// Force summarized so thinking_delta events arrive. See anthropics/claude-agent-sdk-python#830.
|
|
1915
1935
|
if (effort) extraArgs["thinking-display"] = "summarized";
|
|
1936
|
+
const fallbackModel = fallbackModelForPrimaryModel(model.id);
|
|
1916
1937
|
|
|
1917
1938
|
// Suppress claude.ai cloud MCP servers (Figma/Canva/etc. auto-discovered via OAuth
|
|
1918
1939
|
// when the user is logged into Anthropic). These are a separate code path from
|
|
@@ -1927,10 +1948,12 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1927
1948
|
const childEnv = { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: "0", DISABLE_AUTO_COMPACT: "1" };
|
|
1928
1949
|
const queryOptions: NonNullable<Parameters<typeof query>[0]["options"]> = {
|
|
1929
1950
|
cwd,
|
|
1951
|
+
model: model.id,
|
|
1930
1952
|
env: childEnv,
|
|
1931
1953
|
...CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
1932
1954
|
permissionMode: "bypassPermissions",
|
|
1933
1955
|
includePartialMessages: true,
|
|
1956
|
+
...(fallbackModel ? { fallbackModel } : {}),
|
|
1934
1957
|
...(providerSettings.fastMode ? { settings: { fastMode: true } } : {}),
|
|
1935
1958
|
systemPrompt: {
|
|
1936
1959
|
type: "preset", preset: "claude_code",
|
|
@@ -1949,6 +1972,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1949
1972
|
debug("provider: fresh query",
|
|
1950
1973
|
`model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
1951
1974
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1975
|
+
`fallback=${fallbackModel ?? "none"}`,
|
|
1952
1976
|
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled} fastMode=${providerSettings.fastMode === true}`,
|
|
1953
1977
|
`claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
|
|
1954
1978
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`);
|
|
@@ -2248,6 +2272,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2248
2272
|
}
|
|
2249
2273
|
};
|
|
2250
2274
|
pi.on("session_start", (event, ctx) => {
|
|
2275
|
+
recordProjectTrust(ctx);
|
|
2251
2276
|
piUI = ctx.ui;
|
|
2252
2277
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
2253
2278
|
clearSession(`session_start:${event.reason}`);
|
package/src/models.ts
CHANGED
|
@@ -2,13 +2,59 @@
|
|
|
2
2
|
// `resolveModelId` 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
|
|
5
|
+
export const FABLE_MODEL_ID = "claude-fable-5";
|
|
6
|
+
export const FABLE_FALLBACK_MODEL_ID = "claude-opus-4-8";
|
|
7
|
+
|
|
8
|
+
export function fallbackModelForPrimaryModel(modelId: string): string | undefined {
|
|
9
|
+
return modelId === FABLE_MODEL_ID ? FABLE_FALLBACK_MODEL_ID : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MODEL_IDS_IN_ORDER = [
|
|
13
|
+
FABLE_MODEL_ID,
|
|
14
|
+
FABLE_FALLBACK_MODEL_ID,
|
|
15
|
+
"claude-opus-4-7",
|
|
16
|
+
"claude-opus-4-6",
|
|
17
|
+
"claude-sonnet-4-6",
|
|
18
|
+
"claude-haiku-4-5",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
type BridgeModelMetadata = {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
reasoning: boolean;
|
|
25
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
26
|
+
input: ("text" | "image")[];
|
|
27
|
+
contextWindow: number;
|
|
28
|
+
maxTokens: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const FALLBACK_MODELS: Record<string, BridgeModelMetadata> = {
|
|
32
|
+
[FABLE_MODEL_ID]: {
|
|
33
|
+
id: FABLE_MODEL_ID,
|
|
34
|
+
name: "Claude Fable 5",
|
|
35
|
+
reasoning: true,
|
|
36
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
37
|
+
input: ["text", "image"],
|
|
38
|
+
contextWindow: 1000000,
|
|
39
|
+
maxTokens: 128000,
|
|
40
|
+
},
|
|
41
|
+
[FABLE_FALLBACK_MODEL_ID]: {
|
|
42
|
+
id: FABLE_FALLBACK_MODEL_ID,
|
|
43
|
+
name: "Claude Opus 4.8",
|
|
44
|
+
reasoning: true,
|
|
45
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
46
|
+
input: ["text", "image"],
|
|
47
|
+
contextWindow: 1000000,
|
|
48
|
+
maxTokens: 128000,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
6
51
|
|
|
7
52
|
// Project pi-ai's model entries down to the fields pi's registerProvider expects,
|
|
8
|
-
//
|
|
53
|
+
// keep MODEL_IDS_IN_ORDER ordering, and fill bridge-owned future IDs when pi-ai
|
|
54
|
+
// has not shipped metadata for them yet. Unknown missing IDs are still dropped.
|
|
9
55
|
export function buildModels<T extends { id: string; [key: string]: any }>(piAiModels: T[]) {
|
|
10
56
|
return MODEL_IDS_IN_ORDER
|
|
11
|
-
.map((id) => piAiModels.find((m) => m.id === id))
|
|
57
|
+
.map((id) => piAiModels.find((m) => m.id === id) ?? FALLBACK_MODELS[id])
|
|
12
58
|
.filter((m) => m != null)
|
|
13
59
|
// Forward thinkingLevelMap so per-model overrides (e.g. opus-4-7 mapping
|
|
14
60
|
// xhigh→xhigh instead of xhigh→max) are visible to the effort lookup.
|