Skip to main content
Glama
firebase
by firebase
beta.ts15.1 kB
/** * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { BetaMessageStream } from '@anthropic-ai/sdk/lib/BetaMessageStream.js'; import type { BetaContentBlock, BetaImageBlockParam, BetaMessage, MessageCreateParams as BetaMessageCreateParams, MessageCreateParamsNonStreaming as BetaMessageCreateParamsNonStreaming, MessageCreateParamsStreaming as BetaMessageCreateParamsStreaming, BetaMessageParam, BetaRawMessageStreamEvent, BetaRedactedThinkingBlockParam, BetaRequestDocumentBlock, BetaStopReason, BetaTextBlockParam, BetaThinkingBlockParam, BetaTool, BetaToolResultBlockParam, BetaToolUseBlockParam, } from '@anthropic-ai/sdk/resources/beta/messages'; import type { GenerateRequest, GenerateResponseData, ModelResponseData, Part, } from 'genkit'; import { logger } from 'genkit/logging'; import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js'; import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js'; import { BaseRunner } from './base.js'; import { RunnerTypes } from './types.js'; /** * Server-managed tool blocks emitted by the beta API that Genkit cannot yet * interpret. We fail fast on these so callers do not accidentally treat them as * locally executable tool invocations. */ /** * Server tool types that exist in beta but are not yet supported. * Note: server_tool_use and web_search_tool_result ARE supported (same as stable API). */ const BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES = new Set<string>([ 'web_fetch_tool_result', 'code_execution_tool_result', 'bash_code_execution_tool_result', 'text_editor_code_execution_tool_result', 'mcp_tool_result', 'mcp_tool_use', 'container_upload', ]); const unsupportedServerToolError = (blockType: string): string => `Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`; interface BetaRunnerTypes extends RunnerTypes { Message: BetaMessage; Stream: BetaMessageStream; StreamEvent: BetaRawMessageStreamEvent; RequestBody: BetaMessageCreateParamsNonStreaming; StreamingRequestBody: BetaMessageCreateParamsStreaming; Tool: BetaTool; MessageParam: BetaMessageParam; ToolResponseContent: BetaTextBlockParam | BetaImageBlockParam; ContentBlockParam: | BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlock | BetaToolUseBlockParam | BetaToolResultBlockParam | BetaThinkingBlockParam | BetaRedactedThinkingBlockParam; } /** * Runner for the Anthropic Beta API. */ export class BetaRunner extends BaseRunner<BetaRunnerTypes> { constructor(params: ClaudeRunnerParams) { super(params); } /** * Map a Genkit Part -> Anthropic beta content block param. * Supports: text, images (base64 data URLs), PDFs (document source), * tool_use (client tool request), tool_result (client tool response). */ protected toAnthropicMessageContent( part: Part ): | BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlock | BetaToolUseBlockParam | BetaToolResultBlockParam | BetaThinkingBlockParam | BetaRedactedThinkingBlockParam { if (part.reasoning) { const signature = this.getThinkingSignature(part); if (!signature) { throw new Error( 'Anthropic thinking parts require a signature when sending back to the API. Preserve the `custom.anthropicThinking.signature` value from the original response.' ); } return { type: 'thinking', thinking: part.reasoning, signature, }; } const redactedThinking = this.getRedactedThinkingData(part); if (redactedThinking !== undefined) { return { type: 'redacted_thinking', data: redactedThinking, }; } // Text if (part.text) { return { type: 'text', text: part.text }; } // Media if (part.media) { if (part.media.contentType === 'application/pdf') { return { type: 'document', source: this.toPdfDocumentSource(part.media), }; } const source = this.toImageSource(part.media); if (source.kind === 'base64') { return { type: 'image', source: { type: 'base64', data: source.data, media_type: source.mediaType, }, }; } return { type: 'image', source: { type: 'url', url: source.url, }, }; } // Tool request (client tool use) if (part.toolRequest) { if (!part.toolRequest.ref) { throw new Error( `Tool request ref is required for Anthropic API. Part: ${JSON.stringify( part.toolRequest )}` ); } return { type: 'tool_use', id: part.toolRequest.ref, name: part.toolRequest.name, input: part.toolRequest.input, }; } // Tool response (client tool result) if (part.toolResponse) { if (!part.toolResponse.ref) { throw new Error( `Tool response ref is required for Anthropic API. Part: ${JSON.stringify( part.toolResponse )}` ); } const betaResult: BetaToolResultBlockParam = { type: 'tool_result', tool_use_id: part.toolResponse.ref, content: [this.toAnthropicToolResponseContent(part)], }; return betaResult; } throw new Error( `Unsupported genkit part fields encountered for current message role: ${JSON.stringify( part )}.` ); } protected createMessage( body: BetaMessageCreateParamsNonStreaming, abortSignal: AbortSignal ): Promise<BetaMessage> { return this.client.beta.messages.create(body, { signal: abortSignal }); } protected streamMessages( body: BetaMessageCreateParamsStreaming, abortSignal: AbortSignal ): BetaMessageStream { return this.client.beta.messages.stream(body, { signal: abortSignal }); } /** * Build non-streaming request body. */ protected toAnthropicRequestBody( modelName: string, request: GenerateRequest<typeof AnthropicConfigSchema>, cacheSystemPrompt?: boolean ): BetaMessageCreateParamsNonStreaming { const model = KNOWN_CLAUDE_MODELS[modelName]; const { system, messages } = this.toAnthropicMessages(request.messages); const mappedModelName = request.config?.version ?? extractVersion(model, modelName); let betaSystem: BetaMessageCreateParamsNonStreaming['system']; if (system !== undefined) { betaSystem = cacheSystemPrompt ? [ { type: 'text' as const, text: system, cache_control: { type: 'ephemeral' as const }, }, ] : system; } const body: BetaMessageCreateParamsNonStreaming = { model: mappedModelName, max_tokens: request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS, messages, }; if (betaSystem !== undefined) body.system = betaSystem; if (request.config?.stopSequences !== undefined) body.stop_sequences = request.config.stopSequences; if (request.config?.temperature !== undefined) body.temperature = request.config.temperature; if (request.config?.topK !== undefined) body.top_k = request.config.topK; if (request.config?.topP !== undefined) body.top_p = request.config.topP; if (request.config?.tool_choice !== undefined) { body.tool_choice = request.config .tool_choice as BetaMessageCreateParams['tool_choice']; } if (request.config?.metadata !== undefined) { body.metadata = request.config .metadata as BetaMessageCreateParams['metadata']; } if (request.tools) { body.tools = request.tools.map((tool) => this.toAnthropicTool(tool)); } const thinkingConfig = this.toAnthropicThinkingConfig( request.config?.thinking ); if (thinkingConfig) { body.thinking = thinkingConfig as BetaMessageCreateParams['thinking']; } if (request.output?.format && request.output.format !== 'text') { throw new Error( `Only text output format is supported for Claude models currently` ); } return body; } /** * Build streaming request body. */ protected toAnthropicStreamingRequestBody( modelName: string, request: GenerateRequest<typeof AnthropicConfigSchema>, cacheSystemPrompt?: boolean ): BetaMessageCreateParamsStreaming { const model = KNOWN_CLAUDE_MODELS[modelName]; const { system, messages } = this.toAnthropicMessages(request.messages); const mappedModelName = request.config?.version ?? extractVersion(model, modelName); const betaSystem = system === undefined ? undefined : cacheSystemPrompt ? [ { type: 'text' as const, text: system, cache_control: { type: 'ephemeral' as const }, }, ] : system; const body: BetaMessageCreateParamsStreaming = { model: mappedModelName, max_tokens: request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS, messages, stream: true, }; if (betaSystem !== undefined) body.system = betaSystem; if (request.config?.stopSequences !== undefined) body.stop_sequences = request.config.stopSequences; if (request.config?.temperature !== undefined) body.temperature = request.config.temperature; if (request.config?.topK !== undefined) body.top_k = request.config.topK; if (request.config?.topP !== undefined) body.top_p = request.config.topP; if (request.config?.tool_choice !== undefined) { body.tool_choice = request.config .tool_choice as BetaMessageCreateParams['tool_choice']; } if (request.config?.metadata !== undefined) { body.metadata = request.config .metadata as BetaMessageCreateParams['metadata']; } if (request.tools) { body.tools = request.tools.map((tool) => this.toAnthropicTool(tool)); } const thinkingConfig = this.toAnthropicThinkingConfig( request.config?.thinking ); if (thinkingConfig) { body.thinking = thinkingConfig as BetaMessageCreateParams['thinking']; } if (request.output?.format && request.output.format !== 'text') { throw new Error( `Only text output format is supported for Claude models currently` ); } return body; } protected toGenkitResponse(message: BetaMessage): GenerateResponseData { return { candidates: [ { index: 0, finishReason: this.fromBetaStopReason(message.stop_reason), message: { role: 'model', content: message.content.map((block) => this.fromBetaContentBlock(block) ), }, }, ], usage: { inputTokens: message.usage.input_tokens, outputTokens: message.usage.output_tokens, }, custom: message, }; } protected toGenkitPart(event: BetaRawMessageStreamEvent): Part | undefined { if (event.type === 'content_block_start') { const blockType = (event.content_block as { type?: string }).type; if ( blockType && BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(blockType) ) { throw new Error(unsupportedServerToolError(blockType)); } return this.fromBetaContentBlock(event.content_block); } if (event.type === 'content_block_delta') { if (event.delta.type === 'text_delta') { return { text: event.delta.text }; } if (event.delta.type === 'thinking_delta') { return { reasoning: event.delta.thinking }; } // server/client tool input_json_delta not supported yet return undefined; } return undefined; } private fromBetaContentBlock(contentBlock: BetaContentBlock): Part { switch (contentBlock.type) { case 'tool_use': { return { toolRequest: { ref: contentBlock.id, name: contentBlock.name ?? 'unknown_tool', input: contentBlock.input, }, }; } case 'mcp_tool_use': throw new Error(unsupportedServerToolError(contentBlock.type)); case 'server_tool_use': { const baseName = contentBlock.name ?? 'unknown_tool'; const serverToolName = 'server_name' in contentBlock && contentBlock.server_name ? `${contentBlock.server_name}/${baseName}` : baseName; return { text: `[Anthropic server tool ${serverToolName}] input: ${JSON.stringify(contentBlock.input)}`, custom: { anthropicServerToolUse: { id: contentBlock.id, name: serverToolName, input: contentBlock.input, }, }, }; } case 'web_search_tool_result': return this.toWebSearchToolResultPart({ type: contentBlock.type, toolUseId: contentBlock.tool_use_id, content: contentBlock.content, }); case 'text': return { text: contentBlock.text }; case 'thinking': return this.createThinkingPart( contentBlock.thinking, contentBlock.signature ); case 'redacted_thinking': return { custom: { redactedThinking: contentBlock.data } }; default: { if (BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES.has(contentBlock.type)) { throw new Error(unsupportedServerToolError(contentBlock.type)); } const unknownType = (contentBlock as { type: string }).type; logger.warn( `Unexpected Anthropic beta content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify( contentBlock )}` ); return { text: '' }; } } } private fromBetaStopReason( reason: BetaStopReason | null ): ModelResponseData['finishReason'] { switch (reason) { case 'max_tokens': case 'model_context_window_exceeded': return 'length'; case 'end_turn': case 'stop_sequence': case 'tool_use': case 'pause_turn': return 'stop'; case null: return 'unknown'; case 'refusal': return 'other'; default: return 'other'; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/firebase/genkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server