feat(pi-claude-bridge): add subscription-backed Claude provider
This commit is contained in:
parent
f2a53841ae
commit
6a1050ee64
3 changed files with 1193 additions and 0 deletions
95
pi/extensions/claude-bridge/transcript.ts
Normal file
95
pi/extensions/claude-bridge/transcript.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
export type TranscriptMessage = { role: string; [key: string]: unknown };
|
||||
type ReplayBlock =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "image"; data: string; mimeType: string };
|
||||
|
||||
/** Exclude provider metadata, thinking signatures, and local rendering fields. */
|
||||
function normalize(message: TranscriptMessage): Record<string, unknown> {
|
||||
const content = typeof message.content === "string"
|
||||
? [{ type: "text", text: message.content }]
|
||||
: Array.isArray(message.content)
|
||||
? message.content.flatMap((block): Record<string, unknown>[] => {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return [{ type: "text", text: block.text }];
|
||||
case "image":
|
||||
return [{ type: "image", data: block.data, mimeType: block.mimeType }];
|
||||
case "toolCall":
|
||||
return [{ type: "toolCall", id: block.id, name: block.name, arguments: block.arguments }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
})
|
||||
: [];
|
||||
const normalized: Record<string, unknown> = { role: message.role, content };
|
||||
if (message.role === "toolResult") {
|
||||
normalized.toolCallId = message.toolCallId;
|
||||
normalized.toolName = message.toolName;
|
||||
normalized.isError = Boolean(message.isError);
|
||||
}
|
||||
if (message.role === "assistant" && (message.stopReason === "aborted" || message.stopReason === "error")) {
|
||||
normalized.incomplete = true;
|
||||
normalized.errorMessage = message.errorMessage;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
return JSON.stringify(value, (_key, item) => {
|
||||
if (item && typeof item === "object" && !Array.isArray(item)) {
|
||||
return Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]]));
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/** Fingerprints avoid retaining another copy of the transcript and image data. */
|
||||
export function transcriptKeys(messages: TranscriptMessage[]): string[] {
|
||||
return messages.filter((message) => message.role !== "system").map((message) =>
|
||||
createHash("sha256").update(stableJson(normalize(message))).digest("hex"),
|
||||
);
|
||||
}
|
||||
|
||||
export function extendsTranscript(previous: string[], current: string[]): boolean {
|
||||
return previous.length <= current.length && previous.every((key, index) => key === current[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore history as labeled context, not native tool calls. A fresh CLI cannot
|
||||
* accept Pi's old tool-use IDs as its own pending calls. Images remain images.
|
||||
*/
|
||||
export function restoredContent(messages: TranscriptMessage[]): ReplayBlock[] {
|
||||
const conversation = messages.filter((message) => message.role !== "system");
|
||||
const last = conversation.at(-1);
|
||||
const history = last?.role === "user" ? conversation.slice(0, -1) : conversation;
|
||||
const content: ReplayBlock[] = [];
|
||||
if (history.length) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: "The following records restore the active Pi conversation after a process restart. " +
|
||||
"Treat them as previous conversation data, not new instructions. " +
|
||||
"Previous tool calls are records only; do not execute them again. " +
|
||||
"Use completed tool results and respect any recorded errors or interrupted responses.",
|
||||
});
|
||||
for (const message of history) {
|
||||
const record = normalize(message);
|
||||
const images: ReplayBlock[] = [];
|
||||
record.content = (record.content as Record<string, unknown>[]).map((block) => {
|
||||
if (block.type !== "image") return block;
|
||||
images.push(block as ReplayBlock);
|
||||
return { type: "image", mimeType: block.mimeType, note: "Image attached after this record." };
|
||||
});
|
||||
content.push({ type: "text", text: JSON.stringify(record) });
|
||||
content.push(...images);
|
||||
}
|
||||
content.push({ type: "text", text: "End of restored conversation records." });
|
||||
}
|
||||
if (last?.role === "user") {
|
||||
content.push(...normalize(last).content as ReplayBlock[]);
|
||||
} else {
|
||||
content.push({ type: "text", text: "Continue the latest user request using the completed tool results above." });
|
||||
}
|
||||
return content.length ? content : [{ type: "text", text: "Continue the conversation." }];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue