UNPKG

@genkit-ai/anthropic

Version:

Genkit AI framework plugin for Anthropic APIs.

308 lines 10.2 kB
import { Message as GenkitMessage } from "genkit"; import { MediaSchema, MediaTypeSchema } from "../types.mjs"; class BaseRunner { name; client; /** * Default maximum output tokens for Claude models when not specified in the request. */ DEFAULT_MAX_OUTPUT_TOKENS = 4096; constructor(params) { this.name = params.name; this.client = params.client; } /** * Converts a Genkit role to the corresponding Anthropic role. */ toAnthropicRole(role, toolMessageType) { if (role === "user") { return "user"; } if (role === "model") { return "assistant"; } if (role === "tool") { return toolMessageType === "tool_use" ? "assistant" : "user"; } throw new Error(`Unsupported genkit role: ${role}`); } isMediaType(value) { return MediaTypeSchema.safeParse(value).success; } isMediaObject(obj) { return MediaSchema.safeParse(obj).success; } /** * Checks if a URL is a data URL (starts with 'data:'). */ isDataUrl(url) { return url.startsWith("data:"); } extractDataFromBase64Url(url) { const match = url.match(/^data:([^;]+);base64,(.+)$/); return match && { contentType: match[1], data: match[2] }; } /** * Both the stable and beta Anthropic SDKs accept the same JSON shape for PDF * document sources (either `type: 'base64'` with a base64 payload or `type: 'url'` * with a public URL). Even though the return type references the stable SDK * union, TypeScript’s structural typing lets the beta runner reuse this helper. */ toPdfDocumentSource(media) { if (media.contentType !== "application/pdf") { throw new Error( `PDF contentType mismatch: expected application/pdf, got ${media.contentType}` ); } const url = media.url; if (this.isDataUrl(url)) { const extracted = this.extractDataFromBase64Url(url); if (!extracted) { throw new Error( `Invalid PDF data URL format: ${url.substring(0, 50)}...` ); } const { data, contentType } = extracted; if (contentType !== "application/pdf") { throw new Error( `PDF contentType mismatch: expected application/pdf, got ${contentType}` ); } return { type: "base64", media_type: "application/pdf", data }; } return { type: "url", url }; } /** * Normalizes Genkit `Media` into either a base64 payload or a remote URL * accepted by the Anthropic SDK. Anthropic supports both `data:` URLs (which * we forward as base64) and remote `https` URLs without additional handling. */ toImageSource(media) { if (this.isDataUrl(media.url)) { const extracted = this.extractDataFromBase64Url(media.url); const { data, contentType } = extracted ?? {}; if (!data || !contentType) { throw new Error( `Invalid genkit part media provided to toAnthropicMessageContent: ${JSON.stringify( media )}.` ); } const resolvedMediaType = contentType; if (!resolvedMediaType) { throw new Error("Media type is required but was not provided"); } if (!this.isMediaType(resolvedMediaType)) { if (resolvedMediaType === "text/plain") { throw new Error( `Unsupported media type: ${resolvedMediaType}. Text files should be sent as text content in the message, not as media. For example, use { text: '...' } instead of { media: { url: '...', contentType: 'text/plain' } }` ); } throw new Error(`Unsupported media type: ${resolvedMediaType}`); } return { kind: "base64", data, mediaType: resolvedMediaType }; } if (!media.url) { throw new Error("Media url is required but was not provided"); } if (media.contentType) { if (!this.isMediaType(media.contentType)) { if (media.contentType === "text/plain") { throw new Error( `Unsupported media type: ${media.contentType}. Text files should be sent as text content in the message, not as media. For example, use { text: '...' } instead of { media: { url: '...', contentType: 'text/plain' } }` ); } throw new Error(`Unsupported media type: ${media.contentType}`); } } return { kind: "url", url: media.url }; } /** * Converts tool response output to the appropriate Anthropic content format. * Handles Media objects, data URLs, strings, and other outputs. */ toAnthropicToolResponseContent(part) { const output = part.toolResponse?.output ?? {}; if (this.isMediaObject(output)) { const { data, contentType } = this.extractDataFromBase64Url(output.url) ?? {}; if (data && contentType) { if (!this.isMediaType(contentType)) { if (contentType === "text/plain") { throw new Error( `Unsupported media type: ${contentType}. Text files should be sent as text content, not as media.` ); } throw new Error(`Unsupported media type: ${contentType}`); } return { type: "image", source: { type: "base64", data, media_type: contentType } }; } } if (typeof output === "string") { if (this.isDataUrl(output)) { const { data, contentType } = this.extractDataFromBase64Url(output) ?? {}; if (data && contentType) { if (!this.isMediaType(contentType)) { if (contentType === "text/plain") { throw new Error( `Unsupported media type: ${contentType}. Text files should be sent as text content, not as media.` ); } throw new Error(`Unsupported media type: ${contentType}`); } return { type: "image", source: { type: "base64", data, media_type: contentType } }; } } return { type: "text", text: output }; } return { type: "text", text: JSON.stringify(output) }; } getThinkingSignature(part) { const metadata = part.metadata; return typeof metadata?.thoughtSignature === "string" ? metadata.thoughtSignature : void 0; } getRedactedThinkingData(part) { const custom = part.custom; const redacted = custom?.redactedThinking; return typeof redacted === "string" ? redacted : void 0; } toAnthropicThinkingConfig(config) { if (!config) return void 0; const { enabled, budgetTokens, adaptive, display } = config; if (adaptive === true) { return { type: "adaptive", ...display !== void 0 && { display } }; } if (enabled === true) { if (budgetTokens === void 0) { throw new Error("budgetTokens is required when thinking is enabled"); } return { type: "enabled", budget_tokens: budgetTokens }; } if (enabled === false) { return { type: "disabled" }; } if (budgetTokens !== void 0) { return { type: "enabled", budget_tokens: budgetTokens }; } return void 0; } /** * Converts Genkit messages to Anthropic format. * Extracts system message and converts remaining messages using the runner's * toAnthropicMessageContent implementation. */ toAnthropicMessages(messages) { let system; if (messages[0]?.role === "system") { const systemMessage = messages[0]; messages = messages.slice(1); for (const part of systemMessage.content ?? []) { if (part.media || part.toolRequest || part.toolResponse) { throw new Error( "System messages can only contain text content. Media, tool requests, and tool responses are not supported in system messages." ); } } system = systemMessage.content.map( (part) => this.toAnthropicMessageContent(part) ); } const anthropicMsgs = []; for (const message of messages) { const msg = new GenkitMessage(message); const hadToolUse = msg.content.some((p) => !!p.toolRequest); const hadToolResult = msg.content.some((p) => !!p.toolResponse); const toolMessageType = hadToolUse ? "tool_use" : hadToolResult ? "tool_result" : void 0; const role = this.toAnthropicRole(message.role, toolMessageType); const content = msg.content.map( (part) => this.toAnthropicMessageContent(part) ); anthropicMsgs.push({ role, content }); } return { system, messages: anthropicMsgs }; } /** * Converts a Genkit ToolDefinition to an Anthropic Tool object. * * Anthropic requires `input_schema.type` to be present (usually `"object"`). * Genkit's `ToolDefinition` may have an empty schema (e.g. from `z.void()`) * which lacks the `type` field. We default to `{ type: "object" }` to * prevent 400 errors from the Anthropic API. */ toAnthropicTool(tool) { const schema = tool.inputSchema || {}; const inputSchema = "type" in schema ? schema : { ...schema, type: "object" }; return { name: tool.name, description: tool.description, input_schema: inputSchema }; } async run(request, options) { const { streamingRequested, sendChunk, abortSignal } = options; if (streamingRequested) { const body2 = this.toAnthropicStreamingRequestBody(this.name, request); const stream = this.streamMessages(body2, abortSignal); for await (const event of stream) { const part = this.toGenkitPart(event); if (part) { sendChunk({ index: 0, content: [part] }); } } const finalMessage = await stream.finalMessage(); return this.toGenkitResponse(finalMessage); } const body = this.toAnthropicRequestBody(this.name, request); const response = await this.createMessage(body, abortSignal); return this.toGenkitResponse(response); } } export { BaseRunner }; //# sourceMappingURL=base.mjs.map