@vanillagreen/pi-claude-bridge 1.4.1 → 1.6.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 +14 -8
- package/bundle/index.js +99 -5
- package/package.json +1 -1
- package/src/config.ts +39 -2
- package/src/index.ts +28 -3
- package/src/models.ts +59 -3
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
Run Claude Code as a Pi provider. Adds `claude-bridge/*` models to `/model`
|
|
6
|
+
Run Claude Code as a Pi provider. Adds `claude-bridge/*` models to `/model` while keeping Pi's tools and TUI.
|
|
7
7
|
|
|
8
|
-
Forked from [`elidickinson/pi-claude-bridge`](https://github.com/elidickinson/pi-claude-bridge).
|
|
8
|
+
Forked from [`elidickinson/pi-claude-bridge`](https://github.com/elidickinson/pi-claude-bridge). This fork removes the AskClaude tool and adds opt-in forwarding for Pi prompt context.
|
|
9
9
|
|
|
10
10
|
## Highlights
|
|
11
11
|
|
|
12
|
-
- `claude-bridge/claude-
|
|
12
|
+
- `claude-bridge/claude-fable-5`, Sonnet 5, Opus 4.8, Opus 4.7, Sonnet 4.6, 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,22 +92,26 @@ 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`, `claude-bridge/claude-sonnet-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`.
|
|
96
102
|
|
|
97
|
-
When Claude Code
|
|
103
|
+
When Claude Code reports a rate-limit reset time, the bridge shows one clear `[rate-limit]` warning with timezone context and avoids repeating the same error line. If `pi-qol` is installed, it can use the reset time to resume later.
|
|
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
|
|
107
|
+
If Claude Code accepts a turn but produces no visible output, the bridge returns a retryable assistant error with a backoff hint instead of leaving Pi stuck waiting. 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
|
|
|
105
111
|
Set `CLAUDE_BRIDGE_DEBUG=1` to write bridge logs to `~/.pi/agent/claude-bridge.log` and per-query Claude Code CLI logs under `~/.pi/agent/cc-cli-logs/`.
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
Tool-result integrity problems are surfaced even when debug logging is off. Pi shows an error notification and writes a diagnostic file to `~/.pi/agent/claude-bridge-diag.log` so lost or mismatched tool output is visible.
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
Startup failures include the resolved Claude executable and working directory, which makes missing binaries and wrong launch directories easier to fix.
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
Contributor-facing stream, tool-result, and startup diagnostics are documented in [`DEVELOPMENT.md`](./DEVELOPMENT.md).
|
package/bundle/index.js
CHANGED
|
@@ -22237,9 +22237,51 @@ 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
|
+
var SONNET_5_MODEL_ID = "claude-sonnet-5";
|
|
22243
|
+
function fallbackModelForPrimaryModel(modelId) {
|
|
22244
|
+
return modelId === FABLE_MODEL_ID ? FABLE_FALLBACK_MODEL_ID : void 0;
|
|
22245
|
+
}
|
|
22246
|
+
var MODEL_IDS_IN_ORDER = [
|
|
22247
|
+
FABLE_MODEL_ID,
|
|
22248
|
+
FABLE_FALLBACK_MODEL_ID,
|
|
22249
|
+
"claude-opus-4-7",
|
|
22250
|
+
"claude-opus-4-6",
|
|
22251
|
+
SONNET_5_MODEL_ID,
|
|
22252
|
+
"claude-sonnet-4-6",
|
|
22253
|
+
"claude-haiku-4-5"
|
|
22254
|
+
];
|
|
22255
|
+
var FALLBACK_MODELS = {
|
|
22256
|
+
[FABLE_MODEL_ID]: {
|
|
22257
|
+
id: FABLE_MODEL_ID,
|
|
22258
|
+
name: "Claude Fable 5",
|
|
22259
|
+
reasoning: true,
|
|
22260
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
22261
|
+
input: ["text", "image"],
|
|
22262
|
+
contextWindow: 1e6,
|
|
22263
|
+
maxTokens: 128e3
|
|
22264
|
+
},
|
|
22265
|
+
[FABLE_FALLBACK_MODEL_ID]: {
|
|
22266
|
+
id: FABLE_FALLBACK_MODEL_ID,
|
|
22267
|
+
name: "Claude Opus 4.8",
|
|
22268
|
+
reasoning: true,
|
|
22269
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
22270
|
+
input: ["text", "image"],
|
|
22271
|
+
contextWindow: 1e6,
|
|
22272
|
+
maxTokens: 128e3
|
|
22273
|
+
},
|
|
22274
|
+
[SONNET_5_MODEL_ID]: {
|
|
22275
|
+
id: SONNET_5_MODEL_ID,
|
|
22276
|
+
name: "Claude Sonnet 5",
|
|
22277
|
+
reasoning: true,
|
|
22278
|
+
input: ["text", "image"],
|
|
22279
|
+
contextWindow: 1e6,
|
|
22280
|
+
maxTokens: 128e3
|
|
22281
|
+
}
|
|
22282
|
+
};
|
|
22241
22283
|
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 }) => ({
|
|
22284
|
+
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
22285
|
id,
|
|
22244
22286
|
name,
|
|
22245
22287
|
reasoning,
|
|
@@ -22638,8 +22680,34 @@ function projectSettingsPath(cwd) {
|
|
|
22638
22680
|
current = parent;
|
|
22639
22681
|
}
|
|
22640
22682
|
}
|
|
22683
|
+
var PROJECT_TRUST_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.project-trust");
|
|
22684
|
+
function projectTrustRegistry() {
|
|
22685
|
+
const host = globalThis;
|
|
22686
|
+
const existing = host[PROJECT_TRUST_SYMBOL];
|
|
22687
|
+
if (existing) return existing;
|
|
22688
|
+
const created = {};
|
|
22689
|
+
host[PROJECT_TRUST_SYMBOL] = created;
|
|
22690
|
+
return created;
|
|
22691
|
+
}
|
|
22692
|
+
function recordProjectTrust(ctx2) {
|
|
22693
|
+
if (!ctx2.cwd) return;
|
|
22694
|
+
let trusted = true;
|
|
22695
|
+
try {
|
|
22696
|
+
trusted = ctx2.isProjectTrusted?.() === true;
|
|
22697
|
+
} catch {
|
|
22698
|
+
trusted = false;
|
|
22699
|
+
}
|
|
22700
|
+
const registry2 = projectTrustRegistry();
|
|
22701
|
+
if (!registry2.projectSettings) registry2.projectSettings = /* @__PURE__ */ new Map();
|
|
22702
|
+
registry2.projectSettings.set(projectSettingsPath(ctx2.cwd), trusted);
|
|
22703
|
+
}
|
|
22704
|
+
function projectSettingsTrusted(settingsPath) {
|
|
22705
|
+
return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
|
|
22706
|
+
}
|
|
22641
22707
|
function settingsPaths(cwd) {
|
|
22642
|
-
|
|
22708
|
+
const user = join2(piUserDir(), "settings.json");
|
|
22709
|
+
const project = projectSettingsPath(cwd);
|
|
22710
|
+
return projectSettingsTrusted(project) ? [user, project] : [user];
|
|
22643
22711
|
}
|
|
22644
22712
|
function tryParseJson(path) {
|
|
22645
22713
|
if (!existsSync3(path)) return {};
|
|
@@ -22747,7 +22815,9 @@ function managerToConfig(raw) {
|
|
|
22747
22815
|
}
|
|
22748
22816
|
function loadConfig(cwd) {
|
|
22749
22817
|
const global2 = tryParseJson(join2(piUserDir(), "claude-bridge.json"));
|
|
22750
|
-
const
|
|
22818
|
+
const projectSettings = projectSettingsPath(cwd);
|
|
22819
|
+
const trustedProject = projectSettingsTrusted(projectSettings);
|
|
22820
|
+
const project = trustedProject ? tryParseJson(join2(dirname2(projectSettings), "claude-bridge.json")) : {};
|
|
22751
22821
|
const manager = managerToConfig(readManagerConfig(cwd));
|
|
22752
22822
|
const provider = normalizeProviderConfig({ ...global2.provider, ...project.provider, ...manager.provider });
|
|
22753
22823
|
return {
|
|
@@ -38549,6 +38619,13 @@ function finalizeCurrentStream(stopReason) {
|
|
|
38549
38619
|
ctx().currentPiStream.end();
|
|
38550
38620
|
ctx().currentPiStream = null;
|
|
38551
38621
|
}
|
|
38622
|
+
function updateTurnOutputModel(modelId) {
|
|
38623
|
+
const c2 = ctx();
|
|
38624
|
+
if (typeof modelId !== "string" || !modelId || !c2.turnOutput) return;
|
|
38625
|
+
if (c2.turnOutput.model === modelId) return;
|
|
38626
|
+
debug(`provider: active Claude model changed ${c2.turnOutput.model} -> ${modelId}`);
|
|
38627
|
+
c2.turnOutput.model = modelId;
|
|
38628
|
+
}
|
|
38552
38629
|
function processStreamEvent(message, customToolNameToPi, model) {
|
|
38553
38630
|
const c2 = ctx();
|
|
38554
38631
|
if (!c2.currentPiStream || !c2.turnOutput) return;
|
|
@@ -38560,6 +38637,7 @@ function processStreamEvent(message, customToolNameToPi, model) {
|
|
|
38560
38637
|
}
|
|
38561
38638
|
if (event?.type === "message_start") {
|
|
38562
38639
|
c2.resetToolTracking();
|
|
38640
|
+
updateTurnOutputModel(event.message?.model);
|
|
38563
38641
|
if (event.message?.usage) updateUsage(c2.turnOutput, event.message.usage, model);
|
|
38564
38642
|
return;
|
|
38565
38643
|
}
|
|
@@ -38698,6 +38776,7 @@ function processAssistantMessage(message, model, customToolNameToPi) {
|
|
|
38698
38776
|
const c2 = ctx();
|
|
38699
38777
|
const assistantMsg = message.message;
|
|
38700
38778
|
if (!assistantMsg?.content) return;
|
|
38779
|
+
updateTurnOutputModel(assistantMsg.model);
|
|
38701
38780
|
if (c2.turnSawStreamEvent) {
|
|
38702
38781
|
if (appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi)) {
|
|
38703
38782
|
c2.turnSawToolCall = true;
|
|
@@ -38744,6 +38823,8 @@ function processAssistantMessage(message, model, customToolNameToPi) {
|
|
|
38744
38823
|
const toolBlock = c2.turnBlocks[idx];
|
|
38745
38824
|
c2.currentPiStream?.push({ type: "toolcall_start", contentIndex: idx, partial: c2.turnOutput });
|
|
38746
38825
|
c2.currentPiStream?.push({ type: "toolcall_end", contentIndex: idx, toolCall: toolBlock, partial: c2.turnOutput });
|
|
38826
|
+
} else if (block.type === "fallback") {
|
|
38827
|
+
updateTurnOutputModel(block.to?.model);
|
|
38747
38828
|
} else {
|
|
38748
38829
|
debug("processAssistantMessage: unhandled block type", block.type);
|
|
38749
38830
|
}
|
|
@@ -38795,6 +38876,14 @@ async function consumeQuery(sdkQuery, customToolNameToPi, model, cwd, bridgeConf
|
|
|
38795
38876
|
case "system":
|
|
38796
38877
|
if (message.subtype === "init" && message.session_id) {
|
|
38797
38878
|
capturedSessionId = message.session_id;
|
|
38879
|
+
} else if (message.subtype === "model_refusal_fallback") {
|
|
38880
|
+
const originalModel = message.original_model;
|
|
38881
|
+
const fallbackModel = message.fallback_model;
|
|
38882
|
+
updateTurnOutputModel(fallbackModel);
|
|
38883
|
+
debug("consumeQuery: model_refusal_fallback", JSON.stringify({ originalModel, fallbackModel }));
|
|
38884
|
+
if (originalModel === FABLE_MODEL_ID && fallbackModel === FABLE_FALLBACK_MODEL_ID) {
|
|
38885
|
+
safeNotify("Claude bridge switched Fable 5 to Opus 4.8 after Claude Code safety fallback.", "info");
|
|
38886
|
+
}
|
|
38798
38887
|
}
|
|
38799
38888
|
break;
|
|
38800
38889
|
case "user":
|
|
@@ -38949,16 +39038,19 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
38949
39038
|
const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
38950
39039
|
const requestedEffort = options?.reasoning ? model.thinkingLevelMap?.[options.reasoning] ?? REASONING_TO_EFFORT[options.reasoning] : void 0;
|
|
38951
39040
|
const effort = resolveConfiguredEffort(model.id, requestedEffort, providerSettings);
|
|
38952
|
-
const extraArgs = {
|
|
39041
|
+
const extraArgs = {};
|
|
38953
39042
|
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
38954
39043
|
if (effort) extraArgs["thinking-display"] = "summarized";
|
|
39044
|
+
const fallbackModel = fallbackModelForPrimaryModel(model.id);
|
|
38955
39045
|
const childEnv = { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: "0", DISABLE_AUTO_COMPACT: "1" };
|
|
38956
39046
|
const queryOptions = {
|
|
38957
39047
|
cwd,
|
|
39048
|
+
model: model.id,
|
|
38958
39049
|
env: childEnv,
|
|
38959
39050
|
...CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
38960
39051
|
permissionMode: "bypassPermissions",
|
|
38961
39052
|
includePartialMessages: true,
|
|
39053
|
+
...fallbackModel ? { fallbackModel } : {},
|
|
38962
39054
|
...providerSettings.fastMode ? { settings: { fastMode: true } } : {},
|
|
38963
39055
|
systemPrompt: {
|
|
38964
39056
|
type: "preset",
|
|
@@ -38978,6 +39070,7 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
38978
39070
|
"provider: fresh query",
|
|
38979
39071
|
`model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
38980
39072
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
39073
|
+
`fallback=${fallbackModel ?? "none"}`,
|
|
38981
39074
|
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled} fastMode=${providerSettings.fastMode === true}`,
|
|
38982
39075
|
`claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
|
|
38983
39076
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`
|
|
@@ -39238,6 +39331,7 @@ function index_default(pi) {
|
|
|
39238
39331
|
}
|
|
39239
39332
|
};
|
|
39240
39333
|
pi.on("session_start", (event, ctx2) => {
|
|
39334
|
+
recordProjectTrust(ctx2);
|
|
39241
39335
|
piUI = ctx2.ui;
|
|
39242
39336
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
39243
39337
|
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,69 @@
|
|
|
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
|
+
export const SONNET_5_MODEL_ID = "claude-sonnet-5";
|
|
8
|
+
|
|
9
|
+
export function fallbackModelForPrimaryModel(modelId: string): string | undefined {
|
|
10
|
+
return modelId === FABLE_MODEL_ID ? FABLE_FALLBACK_MODEL_ID : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const MODEL_IDS_IN_ORDER = [
|
|
14
|
+
FABLE_MODEL_ID,
|
|
15
|
+
FABLE_FALLBACK_MODEL_ID,
|
|
16
|
+
"claude-opus-4-7",
|
|
17
|
+
"claude-opus-4-6",
|
|
18
|
+
SONNET_5_MODEL_ID,
|
|
19
|
+
"claude-sonnet-4-6",
|
|
20
|
+
"claude-haiku-4-5",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
type BridgeModelMetadata = {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
reasoning: boolean;
|
|
27
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
28
|
+
input: ("text" | "image")[];
|
|
29
|
+
contextWindow: number;
|
|
30
|
+
maxTokens: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const FALLBACK_MODELS: Record<string, BridgeModelMetadata> = {
|
|
34
|
+
[FABLE_MODEL_ID]: {
|
|
35
|
+
id: FABLE_MODEL_ID,
|
|
36
|
+
name: "Claude Fable 5",
|
|
37
|
+
reasoning: true,
|
|
38
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
39
|
+
input: ["text", "image"],
|
|
40
|
+
contextWindow: 1000000,
|
|
41
|
+
maxTokens: 128000,
|
|
42
|
+
},
|
|
43
|
+
[FABLE_FALLBACK_MODEL_ID]: {
|
|
44
|
+
id: FABLE_FALLBACK_MODEL_ID,
|
|
45
|
+
name: "Claude Opus 4.8",
|
|
46
|
+
reasoning: true,
|
|
47
|
+
thinkingLevelMap: { xhigh: "xhigh" },
|
|
48
|
+
input: ["text", "image"],
|
|
49
|
+
contextWindow: 1000000,
|
|
50
|
+
maxTokens: 128000,
|
|
51
|
+
},
|
|
52
|
+
[SONNET_5_MODEL_ID]: {
|
|
53
|
+
id: SONNET_5_MODEL_ID,
|
|
54
|
+
name: "Claude Sonnet 5",
|
|
55
|
+
reasoning: true,
|
|
56
|
+
input: ["text", "image"],
|
|
57
|
+
contextWindow: 1000000,
|
|
58
|
+
maxTokens: 128000,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
6
61
|
|
|
7
62
|
// Project pi-ai's model entries down to the fields pi's registerProvider expects,
|
|
8
|
-
//
|
|
63
|
+
// keep MODEL_IDS_IN_ORDER ordering, and fill bridge-owned future IDs when pi-ai
|
|
64
|
+
// has not shipped metadata for them yet. Unknown missing IDs are still dropped.
|
|
9
65
|
export function buildModels<T extends { id: string; [key: string]: any }>(piAiModels: T[]) {
|
|
10
66
|
return MODEL_IDS_IN_ORDER
|
|
11
|
-
.map((id) => piAiModels.find((m) => m.id === id))
|
|
67
|
+
.map((id) => piAiModels.find((m) => m.id === id) ?? FALLBACK_MODELS[id])
|
|
12
68
|
.filter((m) => m != null)
|
|
13
69
|
// Forward thinkingLevelMap so per-model overrides (e.g. opus-4-7 mapping
|
|
14
70
|
// xhigh→xhigh instead of xhigh→max) are visible to the effort lookup.
|