@vanillagreen/pi-claude-bridge 1.1.4 → 1.3.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 +27 -1
- package/bundle/index.js +15454 -11932
- package/package.json +47 -3
- package/src/config.ts +70 -1
- package/src/convert.ts +77 -20
- package/src/index.ts +554 -60
- package/src/models.ts +1 -1
- package/src/query-state.ts +187 -3
- package/src/tool-pairing-audit.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-claude-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Pi provider bridge that runs Claude Code through the Claude Agent SDK, with opt-in forwarding for Pi prompt context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -85,6 +85,50 @@
|
|
|
85
85
|
"category": "Claude Code",
|
|
86
86
|
"apply": "live"
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
"key": "allowExtraUsage",
|
|
90
|
+
"label": "Allow extra usage helper",
|
|
91
|
+
"description": "Allow claude-bridge to launch Claude Code's /extra-usage flow when rate limits require extra usage. Billing/admin approval still happens in Claude's browser page.",
|
|
92
|
+
"type": "boolean",
|
|
93
|
+
"default": false,
|
|
94
|
+
"category": "Claude Code",
|
|
95
|
+
"apply": "live"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"key": "fastMode",
|
|
99
|
+
"label": "Fast mode",
|
|
100
|
+
"description": "Enable Claude Code fast mode for bridge requests when the selected model supports it. Off by default.",
|
|
101
|
+
"type": "boolean",
|
|
102
|
+
"default": false,
|
|
103
|
+
"category": "Claude Code",
|
|
104
|
+
"apply": "live"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"key": "forceEffort",
|
|
108
|
+
"label": "Force Claude effort",
|
|
109
|
+
"description": "Override Pi's thinking-level mapping for all claude-bridge requests. Use max to reach Claude Code max effort even though Pi has no max thinking level. none keeps Pi's selected level.",
|
|
110
|
+
"type": "enum",
|
|
111
|
+
"enumValues": [
|
|
112
|
+
"none",
|
|
113
|
+
"low",
|
|
114
|
+
"medium",
|
|
115
|
+
"high",
|
|
116
|
+
"xhigh",
|
|
117
|
+
"max"
|
|
118
|
+
],
|
|
119
|
+
"default": "none",
|
|
120
|
+
"category": "Claude Code",
|
|
121
|
+
"apply": "live"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"key": "modelEffortOverrides",
|
|
125
|
+
"label": "Model effort overrides",
|
|
126
|
+
"description": "Optional JSON object mapping claude-bridge model ids to Claude Code effort levels, e.g. {\"claude-opus-4-8\":\"max\"}. Keys may also use claude-bridge/<id> or *.",
|
|
127
|
+
"type": "string",
|
|
128
|
+
"default": "{}",
|
|
129
|
+
"category": "Claude Code",
|
|
130
|
+
"apply": "live"
|
|
131
|
+
},
|
|
88
132
|
{
|
|
89
133
|
"key": "pathToClaudeCodeExecutable",
|
|
90
134
|
"label": "Claude executable path",
|
|
@@ -98,8 +142,8 @@
|
|
|
98
142
|
}
|
|
99
143
|
},
|
|
100
144
|
"dependencies": {
|
|
101
|
-
"@anthropic-ai/claude-agent-sdk": "0.
|
|
102
|
-
"@anthropic-ai/sdk": "
|
|
145
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.158",
|
|
146
|
+
"@anthropic-ai/sdk": "0.93.0",
|
|
103
147
|
"cc-session-io": "^0.3.1",
|
|
104
148
|
"change-case": "^5.4.4"
|
|
105
149
|
},
|
package/src/config.ts
CHANGED
|
@@ -9,11 +9,22 @@ import { dirname, join, resolve } from "path";
|
|
|
9
9
|
|
|
10
10
|
export const PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
|
|
11
11
|
|
|
12
|
+
export type BridgeEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
|
13
|
+
|
|
14
|
+
const VALID_EFFORT_LEVELS = new Set<BridgeEffortLevel>(["low", "medium", "high", "xhigh", "max"]);
|
|
15
|
+
|
|
12
16
|
export interface Config {
|
|
13
17
|
enabled?: boolean;
|
|
14
18
|
/** Low-level Claude Agent SDK plumbing. Most users won't need these. */
|
|
15
19
|
provider?: {
|
|
16
20
|
appendSystemPrompt?: boolean;
|
|
21
|
+
allowExtraUsage?: boolean;
|
|
22
|
+
/** Enable Claude Code fast mode for bridge requests. */
|
|
23
|
+
fastMode?: boolean;
|
|
24
|
+
/** Force this Claude Code effort level for every bridge request. */
|
|
25
|
+
forceEffort?: BridgeEffortLevel;
|
|
26
|
+
/** Per-model Claude Code effort overrides keyed by model id (e.g. claude-opus-4-8). */
|
|
27
|
+
modelEffortOverrides?: Record<string, BridgeEffortLevel>;
|
|
17
28
|
settingSources?: SettingSource[];
|
|
18
29
|
strictMcpConfig?: boolean;
|
|
19
30
|
pathToClaudeCodeExecutable?: string;
|
|
@@ -105,12 +116,69 @@ function stringFrom(raw: SettingsRecord, key: string): string | undefined {
|
|
|
105
116
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
106
117
|
}
|
|
107
118
|
|
|
119
|
+
function hasOwn(raw: SettingsRecord, key: string): boolean {
|
|
120
|
+
return Object.prototype.hasOwnProperty.call(raw, key);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeEffortLevel(value: unknown): BridgeEffortLevel | undefined {
|
|
124
|
+
if (typeof value !== "string") return undefined;
|
|
125
|
+
const normalized = value.trim().toLowerCase();
|
|
126
|
+
if (normalized === "" || normalized === "none" || normalized === "auto" || normalized === "default") return undefined;
|
|
127
|
+
return VALID_EFFORT_LEVELS.has(normalized as BridgeEffortLevel) ? normalized as BridgeEffortLevel : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeModelEffortOverrides(value: unknown): Record<string, BridgeEffortLevel> | undefined {
|
|
131
|
+
let source: unknown = value;
|
|
132
|
+
if (typeof source === "string") {
|
|
133
|
+
const trimmed = source.trim();
|
|
134
|
+
if (!trimmed || trimmed === "{}") return undefined;
|
|
135
|
+
try {
|
|
136
|
+
source = JSON.parse(trimmed);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const record = asRecord(source);
|
|
142
|
+
if (!record) return undefined;
|
|
143
|
+
|
|
144
|
+
const out: Record<string, BridgeEffortLevel> = {};
|
|
145
|
+
for (const [modelId, rawEffort] of Object.entries(record)) {
|
|
146
|
+
const key = modelId.trim();
|
|
147
|
+
const effort = normalizeEffortLevel(rawEffort);
|
|
148
|
+
if (key && effort) out[key] = effort;
|
|
149
|
+
}
|
|
150
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeProviderConfig(provider: Config["provider"] | undefined): Config["provider"] {
|
|
154
|
+
if (!provider) return {};
|
|
155
|
+
const raw = provider as SettingsRecord;
|
|
156
|
+
const out: Config["provider"] = { ...provider };
|
|
157
|
+
const forceEffort = normalizeEffortLevel(raw.forceEffort);
|
|
158
|
+
if (forceEffort) out.forceEffort = forceEffort;
|
|
159
|
+
else delete out.forceEffort;
|
|
160
|
+
const modelEffortOverrides = normalizeModelEffortOverrides(raw.modelEffortOverrides);
|
|
161
|
+
if (modelEffortOverrides) out.modelEffortOverrides = modelEffortOverrides;
|
|
162
|
+
else delete out.modelEffortOverrides;
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
108
166
|
function managerToConfig(raw: SettingsRecord): Partial<Config> {
|
|
109
167
|
const provider: Config["provider"] = {};
|
|
110
168
|
const promptContext: Config["promptContext"] = {};
|
|
111
169
|
|
|
112
170
|
const appendSystemPrompt = boolFrom(raw, "appendSystemPrompt");
|
|
113
171
|
if (appendSystemPrompt !== undefined) provider.appendSystemPrompt = appendSystemPrompt;
|
|
172
|
+
const allowExtraUsage = boolFrom(raw, "allowExtraUsage");
|
|
173
|
+
if (allowExtraUsage !== undefined) provider.allowExtraUsage = allowExtraUsage;
|
|
174
|
+
const fastMode = boolFrom(raw, "fastMode");
|
|
175
|
+
if (fastMode !== undefined) provider.fastMode = fastMode;
|
|
176
|
+
if (hasOwn(raw, "forceEffort")) {
|
|
177
|
+
provider.forceEffort = normalizeEffortLevel(raw.forceEffort);
|
|
178
|
+
}
|
|
179
|
+
if (hasOwn(raw, "modelEffortOverrides")) {
|
|
180
|
+
provider.modelEffortOverrides = normalizeModelEffortOverrides(raw.modelEffortOverrides);
|
|
181
|
+
}
|
|
114
182
|
const strictMcpConfig = boolFrom(raw, "strictMcpConfig");
|
|
115
183
|
if (strictMcpConfig !== undefined) provider.strictMcpConfig = strictMcpConfig;
|
|
116
184
|
const claudePath = stringFrom(raw, "pathToClaudeCodeExecutable");
|
|
@@ -136,9 +204,10 @@ export function loadConfig(cwd: string): Config {
|
|
|
136
204
|
const global = tryParseJson(join(piUserDir(), "claude-bridge.json"));
|
|
137
205
|
const project = tryParseJson(join(cwd, ".pi", "claude-bridge.json"));
|
|
138
206
|
const manager = managerToConfig(readManagerConfig(cwd));
|
|
207
|
+
const provider = normalizeProviderConfig({ ...global.provider, ...project.provider, ...manager.provider });
|
|
139
208
|
return {
|
|
140
209
|
enabled: manager.enabled ?? project.enabled ?? global.enabled ?? true,
|
|
141
|
-
provider
|
|
210
|
+
provider,
|
|
142
211
|
promptContext: { ...global.promptContext, ...project.promptContext, ...manager.promptContext },
|
|
143
212
|
};
|
|
144
213
|
}
|
package/src/convert.ts
CHANGED
|
@@ -80,6 +80,34 @@ function assistantProvenancePrefix(msg: PiMessage): string | undefined {
|
|
|
80
80
|
return `[Prior Pi assistant response from ${provider ?? api ?? "unknown-provider"}${model ? `/${model}` : ""}]\n`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function userMessageToAnthropic(msg: PiMessage): SessionMessage {
|
|
84
|
+
if (typeof msg.content === "string") return { role: "user", content: msg.content || "[empty]" };
|
|
85
|
+
if (Array.isArray(msg.content)) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (const block of msg.content) {
|
|
88
|
+
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
89
|
+
else if (block.type === "image" && block.data && block.mimeType) parts.push(imageBlockToAnthropic(block));
|
|
90
|
+
}
|
|
91
|
+
const kept = parts.filter(Boolean) as ContentBlock[];
|
|
92
|
+
return { role: "user", content: kept.length ? kept : "[image]" };
|
|
93
|
+
}
|
|
94
|
+
return { role: "user", content: "[empty]" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toolResultToAnthropicBlock(msg: PiMessage, sanitizedIds: Map<string, string>): ContentBlock {
|
|
98
|
+
const content = toolResultContentToAnthropic(msg.content as string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>);
|
|
99
|
+
return {
|
|
100
|
+
type: "tool_result",
|
|
101
|
+
tool_use_id: sanitizeToolId((msg as { toolCallId: string }).toolCallId, sanitizedIds),
|
|
102
|
+
content: content || "",
|
|
103
|
+
is_error: (msg as { isError?: boolean }).isError,
|
|
104
|
+
} as ContentBlock;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasToolUse(msg: PiMessage): boolean {
|
|
108
|
+
return msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some((block) => block.type === "toolCall");
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
/** Convert pi message array to Anthropic API format. */
|
|
84
112
|
export function convertPiMessages(
|
|
85
113
|
messages: PiMessage[],
|
|
@@ -88,22 +116,26 @@ export function convertPiMessages(
|
|
|
88
116
|
const anthropicMessages = [];
|
|
89
117
|
const sanitizedIds = new Map();
|
|
90
118
|
|
|
91
|
-
|
|
119
|
+
const pushToolResultGroup = (toolMessages: PiMessage[]): void => {
|
|
120
|
+
if (toolMessages.length === 0) return;
|
|
121
|
+
anthropicMessages.push({
|
|
122
|
+
role: "user",
|
|
123
|
+
content: toolMessages.map((toolMsg) => {
|
|
124
|
+
const content = toolResultContentToAnthropic(toolMsg.content as string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>);
|
|
125
|
+
return {
|
|
126
|
+
type: "tool_result",
|
|
127
|
+
tool_use_id: sanitizeToolId((toolMsg as { toolCallId: string }).toolCallId, sanitizedIds),
|
|
128
|
+
content: content || "",
|
|
129
|
+
is_error: (toolMsg as { isError?: boolean }).isError,
|
|
130
|
+
};
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < messages.length; i++) {
|
|
136
|
+
const msg = messages[i];
|
|
92
137
|
if (msg.role === "user") {
|
|
93
|
-
|
|
94
|
-
anthropicMessages.push({ role: "user", content: msg.content || "[empty]" });
|
|
95
|
-
} else if (Array.isArray(msg.content)) {
|
|
96
|
-
const parts = [];
|
|
97
|
-
for (const block of msg.content) {
|
|
98
|
-
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
99
|
-
else if (block.type === "image" && block.data && block.mimeType) {
|
|
100
|
-
parts.push(imageBlockToAnthropic(block));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
anthropicMessages.push({ role: "user", content: parts.filter(Boolean).length ? parts.filter(Boolean) as ContentBlock[] : "[image]" });
|
|
104
|
-
} else {
|
|
105
|
-
anthropicMessages.push({ role: "user", content: "[empty]" });
|
|
106
|
-
}
|
|
138
|
+
anthropicMessages.push(userMessageToAnthropic(msg));
|
|
107
139
|
} else if (msg.role === "assistant") {
|
|
108
140
|
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
109
141
|
const blocks = [];
|
|
@@ -125,12 +157,37 @@ export function convertPiMessages(
|
|
|
125
157
|
}
|
|
126
158
|
if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
|
|
127
159
|
anthropicMessages.push({ role: "assistant", content: blocks });
|
|
160
|
+
|
|
161
|
+
// Pi may inject steer/followUp user messages between parallel tool
|
|
162
|
+
// results, while runtime extraction treats every toolResult after the
|
|
163
|
+
// assistant (until the next assistant) as one turn. Claude history must
|
|
164
|
+
// put all tool_result blocks immediately after the tool_use assistant;
|
|
165
|
+
// replay interleaved user text only after that grouped result message.
|
|
166
|
+
if (hasToolUse(msg)) {
|
|
167
|
+
const toolMessages: PiMessage[] = [];
|
|
168
|
+
const interleavedUsers: PiMessage[] = [];
|
|
169
|
+
let j = i + 1;
|
|
170
|
+
for (; j < messages.length; j++) {
|
|
171
|
+
const next = messages[j];
|
|
172
|
+
if (next.role === "assistant") break;
|
|
173
|
+
if (next.role === "toolResult") toolMessages.push(next);
|
|
174
|
+
else if (next.role === "user") interleavedUsers.push(next);
|
|
175
|
+
else break;
|
|
176
|
+
}
|
|
177
|
+
if (toolMessages.length > 0) {
|
|
178
|
+
pushToolResultGroup(toolMessages);
|
|
179
|
+
for (const userMsg of interleavedUsers) anthropicMessages.push(userMessageToAnthropic(userMsg));
|
|
180
|
+
i = j - 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
128
183
|
} else if (msg.role === "toolResult") {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
const blocks: ContentBlock[] = [];
|
|
185
|
+
for (; i < messages.length; i++) {
|
|
186
|
+
const toolMsg = messages[i];
|
|
187
|
+
if (toolMsg.role !== "toolResult") { i--; break; }
|
|
188
|
+
blocks.push(toolResultToAnthropicBlock(toolMsg, sanitizedIds));
|
|
189
|
+
}
|
|
190
|
+
anthropicMessages.push({ role: "user", content: blocks });
|
|
134
191
|
}
|
|
135
192
|
}
|
|
136
193
|
|