From 3b66512e6b941528285b59d642f7dd9814ff0c0d Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Tue, 25 Nov 2025 18:33:21 +0000 Subject: [PATCH] feat(js/plugins/anthropic): add anthropic plugin core implementation --- js/plugins/anthropic/.gitignore | 3 + js/plugins/anthropic/.npmignore | 17 + js/plugins/anthropic/NOTICE | 8 + js/plugins/anthropic/README.md | 195 ++++++++ js/plugins/anthropic/package.json | 70 +++ js/plugins/anthropic/src/index.ts | 155 ++++++ js/plugins/anthropic/src/list.ts | 192 ++++++++ js/plugins/anthropic/src/models.ts | 371 +++++++++++++++ js/plugins/anthropic/src/runner/base.ts | 550 ++++++++++++++++++++++ js/plugins/anthropic/src/runner/beta.ts | 494 +++++++++++++++++++ js/plugins/anthropic/src/runner/index.ts | 19 + js/plugins/anthropic/src/runner/stable.ts | 514 ++++++++++++++++++++ js/plugins/anthropic/src/runner/types.ts | 78 +++ js/plugins/anthropic/src/types.ts | 166 +++++++ js/plugins/anthropic/tsconfig.json | 4 + js/plugins/anthropic/tsup.config.ts | 22 + js/pnpm-lock.yaml | 206 ++++++-- 17 files changed, 3026 insertions(+), 38 deletions(-) create mode 100644 js/plugins/anthropic/.gitignore create mode 100644 js/plugins/anthropic/.npmignore create mode 100644 js/plugins/anthropic/NOTICE create mode 100644 js/plugins/anthropic/README.md create mode 100644 js/plugins/anthropic/package.json create mode 100644 js/plugins/anthropic/src/index.ts create mode 100644 js/plugins/anthropic/src/list.ts create mode 100644 js/plugins/anthropic/src/models.ts create mode 100644 js/plugins/anthropic/src/runner/base.ts create mode 100644 js/plugins/anthropic/src/runner/beta.ts create mode 100644 js/plugins/anthropic/src/runner/index.ts create mode 100644 js/plugins/anthropic/src/runner/stable.ts create mode 100644 js/plugins/anthropic/src/runner/types.ts create mode 100644 js/plugins/anthropic/src/types.ts create mode 100644 js/plugins/anthropic/tsconfig.json create mode 100644 js/plugins/anthropic/tsup.config.ts diff --git a/js/plugins/anthropic/.gitignore b/js/plugins/anthropic/.gitignore new file mode 100644 index 0000000000..d83aca04ae --- /dev/null +++ b/js/plugins/anthropic/.gitignore @@ -0,0 +1,3 @@ +lib/ +node_modules/ +coverage/ diff --git a/js/plugins/anthropic/.npmignore b/js/plugins/anthropic/.npmignore new file mode 100644 index 0000000000..d265d4ab73 --- /dev/null +++ b/js/plugins/anthropic/.npmignore @@ -0,0 +1,17 @@ +# typescript source files +src/ +tests/ +tsconfig.json +tsup.common.ts +tsup.config.ts + +# GitHub files +.github/ +.gitignore +.npmignore +CODE_OF_CONDUCT.md +CONTRIBUTING.md + +# Developer related files +.devcontainer/ +.vscode/ diff --git a/js/plugins/anthropic/NOTICE b/js/plugins/anthropic/NOTICE new file mode 100644 index 0000000000..dc335bb090 --- /dev/null +++ b/js/plugins/anthropic/NOTICE @@ -0,0 +1,8 @@ +This project includes code derived from the Firebase Genkit Anthropic community plugin +(https://github.com/BloomLabsInc/genkit-plugins/tree/main/plugins/anthropic). + +Copyright 2024 Bloom Labs Inc. +Copyright 2025 Google LLC. + +Licensed under the Apache License, Version 2.0. +See the LICENSE file distributed with this project for the full license text. diff --git a/js/plugins/anthropic/README.md b/js/plugins/anthropic/README.md new file mode 100644 index 0000000000..ec9c9115c0 --- /dev/null +++ b/js/plugins/anthropic/README.md @@ -0,0 +1,195 @@ +# Firebase Genkit + Anthropic AI + +

Firebase Genkit <> Anthropic AI Plugin

+ +

Anthropic AI plugin for Google Firebase Genkit

+ +`@genkit-ai/anthropic` is the official Anthropic plugin for [Firebase Genkit](https://github.com/firebase/genkit). It supersedes the earlier community package `genkitx-anthropic` and is now maintained by Google. + +## Supported models + +The plugin supports the most recent Anthropic models: **Claude Sonnet 4.5**, **Claude Opus 4.1**, **Claude Haiku 4.5**, **Claude Sonnet 4**, **Claude Opus 4**, **Claude 3.5 Haiku**, and **Claude 3 Haiku**. + +## Installation + +Install the plugin in your project with your favorite package manager: + +- `npm install @genkit-ai/anthropic` +- `yarn add @genkit-ai/anthropic` +- `pnpm add @genkit-ai/anthropic` + +## Usage + +### Initialize + +```typescript +import { genkit } from 'genkit'; +import { anthropic } from '@genkit-ai/anthropic'; + +const ai = genkit({ + plugins: [anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })], + // specify a default model for generate here if you wish: + model: anthropic.model('claude-sonnet-4-5'), +}); +``` + +### Basic examples + +The simplest way to generate text is by using the `generate` method: + +```typescript +const response = await ai.generate({ + model: anthropic.model('claude-3-haiku'), + prompt: 'Tell me a joke.', +}); + +console.log(response.text); +``` + +### Multi-modal prompt + +```typescript +// ...initialize Genkit instance (as shown above)... + +const response = await ai.generate({ + prompt: [ + { text: 'What animal is in the photo?' }, + { media: { url: imageUrl } }, + ], + config: { + // control of the level of visual detail when processing image embeddings + // Low detail level also decreases the token usage + visualDetailLevel: 'low', + }, +}); +console.log(response.text); +``` + +### Extended thinking + +Claude 4 models can expose their internal reasoning. Enable it per-request with the Anthropic thinking config and read the reasoning from the response: + +```typescript +const response = await ai.generate({ + prompt: 'Walk me through your reasoning for Fermat’s little theorem.', + config: { + thinking: { + enabled: true, + budgetTokens: 4096, // Must be >= 1024 and less than max_tokens + }, + }, +}); + +console.log(response.text); // Final assistant answer +console.log(response.reasoning); // Summarized thinking steps +``` + +When thinking is enabled, request bodies sent through the plugin include the `thinking` payload (`{ type: 'enabled', budget_tokens: … }`) that Anthropic's API expects, and streamed responses deliver `reasoning` parts as they arrive so you can render the chain-of-thought incrementally. + +### Beta API Limitations + +The beta API surface provides access to experimental features, but some server-managed tool blocks are not yet supported by this plugin. The following beta API features will cause an error if encountered: + +- `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` + +Note that `server_tool_use` and `web_search_tool_result` ARE supported and work with both stable and beta APIs. + +### Within a flow + +```typescript +import { z } from 'genkit'; + +// ...initialize Genkit instance (as shown above)... + +export const jokeFlow = ai.defineFlow( + { + name: 'jokeFlow', + inputSchema: z.string(), + outputSchema: z.string(), + }, + async (subject) => { + const llmResponse = await ai.generate({ + prompt: `tell me a joke about ${subject}`, + }); + return llmResponse.text; + } +); +``` + +### Direct model usage (without Genkit instance) + +The plugin supports Genkit Plugin API v2, which allows you to use models directly without initializing the full Genkit framework: + +```typescript +import { anthropic } from '@genkit-ai/anthropic'; + +// Create a model reference directly +const claude = anthropic.model('claude-sonnet-4-5'); + +// Use the model directly +const response = await claude({ + messages: [ + { + role: 'user', + content: [{ text: 'Tell me a joke.' }], + }, + ], +}); + +console.log(response); +``` + +You can also create model references using the plugin's `model()` method: + +```typescript +import { anthropic } from '@genkit-ai/anthropic'; + +// Create model references +const claudeSonnet45 = anthropic.model('claude-sonnet-4-5'); +const claudeOpus41 = anthropic.model('claude-opus-4-1'); +const claude35Haiku = anthropic.model('claude-3-5-haiku'); + +// Use the model reference directly +const response = await claudeSonnet45({ + messages: [ + { + role: 'user', + content: [{ text: 'Hello!' }], + }, + ], +}); +``` + +This approach is useful for: + +- Framework developers who need raw model access +- Testing models in isolation +- Using Genkit models in non-Genkit applications + +## Acknowledgements + +This plugin builds on the community work published as [`genkitx-anthropic`](https://github.com/BloomLabsInc/genkit-plugins/blob/main/plugins/anthropic/README.md) by Bloom Labs Inc. Their Apache 2.0–licensed implementation provided the foundation for this maintained package. + +## Contributing + +Want to contribute to the project? That's awesome! Head over to our [Contribution Guidelines](CONTRIBUTING.md). + +## Need support? + +> [!NOTE] +> This repository depends on Google's Firebase Genkit. For issues and questions related to Genkit, please refer to instructions available in [Genkit's repository](https://github.com/firebase/genkit). + + +## Credits + +This plugin is maintained by Google with acknowledgement to the community contributions from [Bloom Labs Inc](https://github.com/BloomLabsInc). + +## License + +This project is licensed under the [Apache 2.0 License](https://github.com/BloomLabsInc/genkit-plugins/blob/main/LICENSE). diff --git a/js/plugins/anthropic/package.json b/js/plugins/anthropic/package.json new file mode 100644 index 0000000000..e3c3e5f036 --- /dev/null +++ b/js/plugins/anthropic/package.json @@ -0,0 +1,70 @@ +{ + "name": "@genkit-ai/anthropic", + "description": "Genkit AI framework plugin for Anthropic APIs.", + "keywords": [ + "genkit", + "genkit-plugin", + "genkit-model", + "anthropic", + "anthropic-ai", + "claude-4", + "haiku-4", + "opus", + "haiku", + "sonnet", + "ai", + "genai", + "generative-ai" + ], + "version": "1.21.0", + "type": "commonjs", + "repository": { + "type": "git", + "url": "https://github.com/firebase/genkit.git", + "directory": "js/plugins/anthropic" + }, + "author": "genkit", + "license": "Apache-2.0", + "peerDependencies": { + "genkit": "workspace:^" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.68.0" + }, + "devDependencies": { + "@types/node": "^20.11.16", + "check-node-version": "^4.2.1", + "genkit": "workspace:*", + "npm-run-all": "^4.1.5", + "rimraf": "^6.0.1", + "tsup": "^8.3.5", + "tsx": "^4.19.2", + "typescript": "^4.9.0" + }, + "types": "./lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.mjs", + "default": "./lib/index.js" + } + }, + "files": [ + "lib" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "scripts": { + "check": "tsc", + "compile": "tsup-node", + "build:clean": "rimraf ./lib", + "build": "npm-run-all build:clean check compile", + "build:watch": "tsup-node --watch", + "test": "tsx --test tests/*_test.ts", + "test:file": "tsx --test", + "test:coverage": "check-node-version --node '>=22' && tsx --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' ./tests/**/*_test.ts" + } +} diff --git a/js/plugins/anthropic/src/index.ts b/js/plugins/anthropic/src/index.ts new file mode 100644 index 0000000000..d5a0fef9cb --- /dev/null +++ b/js/plugins/anthropic/src/index.ts @@ -0,0 +1,155 @@ +/** + * Copyright 2024 Bloom Labs Inc + * 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 Anthropic from '@anthropic-ai/sdk'; +import { genkitPluginV2, type GenkitPluginV2 } from 'genkit/plugin'; + +import { ActionMetadata, ModelReference, z } from 'genkit'; +import { ModelAction } from 'genkit/model'; +import { ActionType } from 'genkit/registry'; +import { listActions } from './list.js'; +import { + AnthropicConfigSchemaType, + ClaudeConfig, + ClaudeModelName, + KNOWN_CLAUDE_MODELS, + KnownClaudeModels, + claudeModel, + claudeModelReference, +} from './models.js'; +import { InternalPluginOptions, PluginOptions, __testClient } from './types.js'; + +const PROMPT_CACHING_BETA_HEADER_VALUE = 'prompt-caching-2024-07-31'; + +/** + * Gets or creates an Anthropic client instance. + * Supports test client injection for internal testing. + */ +function getAnthropicClient(options?: PluginOptions): Anthropic { + // Check for test client injection first (internal use only) + const internalOptions = options as InternalPluginOptions | undefined; + if (internalOptions?.[__testClient]) { + return internalOptions[__testClient]; + } + + // Production path: create real client + const apiKey = options?.apiKey || process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error( + 'Please pass in the API key or set the ANTHROPIC_API_KEY environment variable' + ); + } + const defaultHeaders: Record = {}; + if (options?.cacheSystemPrompt) { + defaultHeaders['anthropic-beta'] = PROMPT_CACHING_BETA_HEADER_VALUE; + } + return new Anthropic({ apiKey, defaultHeaders }); +} + +/** + * This module provides an interface to the Anthropic AI models through the Genkit plugin system. + * It allows users to interact with various Claude models by providing an API key and optional configuration. + * + * The main export is the `anthropic` plugin, which can be configured with an API key either directly or through + * environment variables. It initializes the Anthropic client and makes available the Claude models for use. + * + * Exports: + * - anthropic: The main plugin function to interact with the Anthropic AI. + * + * Usage: + * To use the Claude models, initialize the anthropic plugin inside `genkit()` and pass the configuration options. If no API key is provided in the options, the environment variable `ANTHROPIC_API_KEY` must be set. If you want to cache the system prompt, set `cacheSystemPrompt` to `true`. **Note:** Prompt caching is in beta and may change. To learn more, see https://docs.anthropic.com/en/docs/prompt-caching. + * + * Example: + * ``` + * import { anthropic } from '@genkit-ai/anthropic'; + * import { genkit } from 'genkit'; + * + * const ai = genkit({ + * plugins: [ + * anthropic({ apiKey: 'your-api-key', cacheSystemPrompt: false }) + * ... // other plugins + * ] + * }); + * + * // Access models via the plugin's model() method: + * const model = anthropic.model('claude-sonnet-4'); + * ``` + */ +function anthropicPlugin(options?: PluginOptions): GenkitPluginV2 { + const client = getAnthropicClient(options); + const defaultApiVersion = options?.apiVersion; + + let listActionsCache: ActionMetadata[] | null = null; + + return genkitPluginV2({ + name: 'anthropic', + init: async () => { + const actions: ModelAction[] = []; + for (const name of Object.keys(KNOWN_CLAUDE_MODELS)) { + const action = claudeModel({ + name, + client, + cacheSystemPrompt: options?.cacheSystemPrompt, + defaultApiVersion, + }); + actions.push(action); + } + return actions; + }, + resolve: (actionType: ActionType, name: string) => { + if (actionType === 'model') { + // Strip the 'anthropic/' namespace prefix if present + const modelName = name.startsWith('anthropic/') ? name.slice(10) : name; + return claudeModel({ + name: modelName, + client, + cacheSystemPrompt: options?.cacheSystemPrompt, + defaultApiVersion, + }); + } + return undefined; + }, + list: async () => { + if (listActionsCache) return listActionsCache; + listActionsCache = await listActions(client); + return listActionsCache; + }, + }); +} + +export type AnthropicPlugin = { + (pluginOptions?: PluginOptions): GenkitPluginV2; + model( + name: KnownClaudeModels | (ClaudeModelName & {}), + config?: ClaudeConfig + ): ModelReference; + model(name: string, config?: any): ModelReference; +}; + +/** + * Anthropic AI plugin for Genkit. + * Includes Claude models (3, 3.5, and 4 series). + */ +export const anthropic = anthropicPlugin as AnthropicPlugin; +(anthropic as any).model = ( + name: string, + config?: any +): ModelReference => { + return claudeModelReference(name, config); +}; + +export default anthropic; diff --git a/js/plugins/anthropic/src/list.ts b/js/plugins/anthropic/src/list.ts new file mode 100644 index 0000000000..f04b1cf082 --- /dev/null +++ b/js/plugins/anthropic/src/list.ts @@ -0,0 +1,192 @@ +/** + * Copyright 2024 Bloom Labs Inc + * 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 Anthropic from '@anthropic-ai/sdk'; +import { modelActionMetadata } from 'genkit/plugin'; + +import { ActionMetadata, ModelReference, z } from 'genkit'; +import { GENERIC_CLAUDE_MODEL_INFO, KNOWN_CLAUDE_MODELS } from './models.js'; +import { AnthropicConfigSchema } from './types.js'; + +function normalizeModelId(modelId: string): string { + // Strip date suffixes (e.g. "-20241001") or "-latest" so lookups hit canonical keys. + return modelId.replace(/-(?:\d{8}|latest)$/i, ''); +} + +type ModelMetadataParams = Parameters[0]; + +interface MergeKnownModelMetadataParams { + modelId: string; + ref: ModelReference; + metadataByName: Map; + orderedNames: string[]; +} +/** + * Integrates metadata for a known Claude model into the aggregated list. + * + * It merges version information collected from the Anthropic API onto the + * canonical model definition while preserving any additional metadata that + * may already exist in the accumulator. + */ +function mergeKnownModelMetadata({ + modelId, + ref, + metadataByName, + orderedNames, +}: MergeKnownModelMetadataParams): void { + // Merge onto any prior metadata we have for this named model. + const existing = metadataByName.get(ref.name); + const priorInfo = existing?.info ?? ref.info ?? {}; + const priorVersions = Array.isArray(priorInfo.versions) + ? priorInfo.versions + : (ref.info?.versions ?? []); + + // Track every concrete model ID surfaced by the API so they appear as selectable versions. + const versions = new Set(priorVersions); + versions.add(modelId); + + metadataByName.set(ref.name, { + name: ref.name, + info: { + ...priorInfo, + versions: Array.from(versions), + }, + configSchema: ref.configSchema, + }); + + if (!existing) { + // Preserve the discovery order for determinism. + orderedNames.push(ref.name); + } +} + +interface MergeFallbackModelMetadataParams { + modelId: string; + normalizedId: string; + displayName?: string; + metadataByName: Map; + orderedNames: string[]; +} + +/** + * Creates or updates metadata entries for Anthropic models that are not + * explicitly enumerated in `KNOWN_CLAUDE_MODELS`. + * + * The resulting metadata uses a generic Claude descriptor while capturing + * the specific model ID returned by the API so it can be surfaced in the + * Genkit UI. + */ +function mergeFallbackModelMetadata({ + modelId, + normalizedId, + displayName, + metadataByName, + orderedNames, +}: MergeFallbackModelMetadataParams): void { + const fallbackName = `anthropic/${modelId}`; + const existing = metadataByName.get(fallbackName); + const fallbackLabel = + displayName ?? + `Anthropic - ${normalizedId !== modelId ? normalizedId : modelId}`; + + if (existing) { + const priorVersions = existing.info?.versions ?? []; + const versions = new Set( + Array.isArray(priorVersions) ? priorVersions : [] + ); + versions.add(modelId); + + metadataByName.set(fallbackName, { + ...existing, + info: { + ...existing.info, + versions: Array.from(versions), + }, + }); + return; + } + + metadataByName.set(fallbackName, { + name: fallbackName, + info: { + ...GENERIC_CLAUDE_MODEL_INFO, + label: fallbackLabel, + versions: modelId ? [modelId] : [...GENERIC_CLAUDE_MODEL_INFO.versions], + supports: { + ...GENERIC_CLAUDE_MODEL_INFO.supports, + output: [...GENERIC_CLAUDE_MODEL_INFO.supports.output], + }, + }, + configSchema: AnthropicConfigSchema, + }); + orderedNames.push(fallbackName); +} + +/** + * Retrieves available Anthropic models from the API and converts them into Genkit action metadata. + * + * This function queries the Anthropic API for the list of available models, matches them against + * known Claude models, and generates metadata for both known and unknown models. The resulting + * metadata includes version information and configuration schemas suitable for use in Genkit. + * + * @param client - The Anthropic API client instance + * @returns A promise that resolves to an array of action metadata for all discovered models + */ +export async function listActions( + client: Anthropic +): Promise { + const clientModels = (await client.models.list()).data; + const metadataByName = new Map(); + const orderedNames: string[] = []; + + for (const modelInfo of clientModels) { + const modelId = modelInfo.id; + if (!modelId) { + continue; + } + + const normalizedId = normalizeModelId(modelId); + const ref = KNOWN_CLAUDE_MODELS[normalizedId]; + + if (ref) { + mergeKnownModelMetadata({ + modelId, + ref, + metadataByName, + orderedNames, + }); + continue; + } + + // For models we don't explicitly track, synthesize a generic entry that still surfaces the ID. + mergeFallbackModelMetadata({ + modelId, + normalizedId, + displayName: modelInfo.display_name ?? undefined, + metadataByName, + orderedNames, + }); + } + + return orderedNames.map((name) => { + const metadata = metadataByName.get(name); + if (!metadata) { + throw new Error(`Missing metadata for model: ${name}`); + } + return modelActionMetadata(metadata); + }); +} diff --git a/js/plugins/anthropic/src/models.ts b/js/plugins/anthropic/src/models.ts new file mode 100644 index 0000000000..ab9f1a198e --- /dev/null +++ b/js/plugins/anthropic/src/models.ts @@ -0,0 +1,371 @@ +/** + * Copyright 2024 Bloom Labs Inc + * 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 type Anthropic from '@anthropic-ai/sdk'; +import type { + GenerateRequest, + GenerateResponseData, + ModelReference, + StreamingCallback, +} from 'genkit'; +import { z } from 'genkit'; +import type { GenerateResponseChunkData, ModelAction } from 'genkit/model'; +import { modelRef } from 'genkit/model'; +import { model } from 'genkit/plugin'; + +import type { ModelInfo } from 'genkit/model'; +import { BetaRunner, Runner } from './runner/index.js'; +import { + AnthropicBaseConfigSchema, + AnthropicBaseConfigSchemaType, + AnthropicConfigSchema, + AnthropicThinkingConfigSchema, + resolveBetaEnabled, + type ClaudeModelParams, + type ClaudeRunnerParams, +} from './types.js'; + +// This contains all the Anthropic config schema types +type ConfigSchemaType = + | AnthropicBaseConfigSchemaType + | AnthropicThinkingConfigSchemaType; + +/** + * Creates a model reference for a Claude model. + * Computes the default version from info.versions array and sets it on the modelRef. + */ +function commonRef( + name: string, + info?: ModelInfo, + configSchema: ConfigSchemaType = AnthropicConfigSchema +): ModelReference { + // Compute default version from info.versions array + let defaultVersion: string | undefined; + if (info?.versions && info.versions.length > 0) { + // Prefer version with '-latest' suffix + const latestVersion = info.versions.find((v) => v.endsWith('-latest')); + if (latestVersion) { + defaultVersion = latestVersion; + } else if (info.versions.includes(name)) { + // If base name exists in versions array, use it directly + defaultVersion = name; + } else { + // Otherwise use first version + defaultVersion = info.versions[0]; + } + } + + return modelRef({ + name: `anthropic/${name}`, + configSchema, + version: defaultVersion, + info: info ?? { + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + }); +} + +export const KNOWN_CLAUDE_MODELS: Record< + string, + ModelReference< + AnthropicBaseConfigSchemaType | AnthropicThinkingConfigSchemaType + > +> = { + 'claude-3-haiku': commonRef( + 'claude-3-haiku', + { + versions: ['claude-3-haiku-20240307'], + label: 'Anthropic - Claude 3 Haiku', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicBaseConfigSchema + ), + 'claude-3-5-haiku': commonRef( + 'claude-3-5-haiku', + { + versions: ['claude-3-5-haiku-20241022', 'claude-3-5-haiku'], + label: 'Anthropic - Claude 3.5 Haiku', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicBaseConfigSchema + ), + 'claude-sonnet-4': commonRef( + 'claude-sonnet-4', + { + versions: ['claude-sonnet-4-20250514'], + label: 'Anthropic - Claude Sonnet 4', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicThinkingConfigSchema + ), + 'claude-opus-4': commonRef( + 'claude-opus-4', + { + versions: ['claude-opus-4-20250514'], + label: 'Anthropic - Claude Opus 4', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicThinkingConfigSchema + ), + 'claude-sonnet-4-5': commonRef( + 'claude-sonnet-4-5', + { + versions: ['claude-sonnet-4-5-20250929', 'claude-sonnet-4-5'], + label: 'Anthropic - Claude Sonnet 4.5', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicThinkingConfigSchema + ), + 'claude-haiku-4-5': commonRef( + 'claude-haiku-4-5', + { + versions: ['claude-haiku-4-5-20251001', 'claude-haiku-4-5'], + label: 'Anthropic - Claude Haiku 4.5', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicThinkingConfigSchema + ), + 'claude-opus-4-1': commonRef( + 'claude-opus-4-1', + { + versions: ['claude-opus-4-1-20250805', 'claude-opus-4-1'], + label: 'Anthropic - Claude Opus 4.1', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, + }, + AnthropicThinkingConfigSchema + ), +}; + +/** + * Gets the un-prefixed model name from a modelReference. + */ +export function extractVersion( + model: ModelReference | undefined, + modelName: string +): string { + if (model?.version) { + return model.version; + } + // Fallback: extract from model name (remove 'anthropic/' prefix if present) + return modelName.replace(/^anthropic\//, ''); +} + +/** + * Generic Claude model info for unknown/unsupported models. + * Used when a model name is not in KNOWN_CLAUDE_MODELS. + */ +export const GENERIC_CLAUDE_MODEL_INFO = { + versions: [], + label: 'Anthropic - Claude', + supports: { + multiturn: true, + tools: true, + media: true, + systemRole: true, + output: ['text'], + }, +}; + +export type KnownClaudeModels = keyof typeof KNOWN_CLAUDE_MODELS; +export type ClaudeModelName = string; +export type AnthropicConfigSchemaType = typeof AnthropicConfigSchema; +export type AnthropicThinkingConfigSchemaType = + typeof AnthropicThinkingConfigSchema; +export type ClaudeConfig = z.infer; + +/** + * Creates the runner used by Genkit to interact with the Claude model. + * @param params Configuration for the Claude runner. + * @param configSchema The config schema for this model (used for type inference). + * @returns The runner that Genkit will call when the model is invoked. + */ +export function claudeRunner( + params: ClaudeRunnerParams, + configSchema: TConfigSchema +) { + const { defaultApiVersion, ...runnerParams } = params; + + if (!runnerParams.client) { + throw new Error('Anthropic client is required to create a runner'); + } + + let stableRunner: Runner | null = null; + let betaRunner: BetaRunner | null = null; + + return async ( + request: GenerateRequest, + { + streamingRequested, + sendChunk, + abortSignal, + }: { + streamingRequested: boolean; + sendChunk: StreamingCallback; + abortSignal: AbortSignal; + } + ): Promise => { + // Cast to AnthropicConfigSchema for internal runner which expects the full schema + const normalizedRequest = request as unknown as GenerateRequest< + typeof AnthropicConfigSchema + >; + const isBeta = resolveBetaEnabled( + normalizedRequest.config, + defaultApiVersion + ); + const runner = isBeta + ? (betaRunner ??= new BetaRunner(runnerParams)) + : (stableRunner ??= new Runner(runnerParams)); + return runner.run(normalizedRequest, { + streamingRequested, + sendChunk, + abortSignal, + }); + }; +} + +/** + * Creates a model reference for a Claude model. + * This allows referencing models without initializing the plugin. + */ +export function claudeModelReference( + name: string, + config?: z.infer +): ModelReference { + const knownModel = KNOWN_CLAUDE_MODELS[name]; + if (knownModel) { + return modelRef({ + name: knownModel.name, + info: knownModel.info, + configSchema: knownModel.configSchema, + version: knownModel.version, + config, + }); + } + + // For unknown models, create a basic reference + return modelRef({ + name: `anthropic/${name}`, + configSchema: AnthropicConfigSchema, + config, + }); +} + +/** + * Defines a Claude model with the given name and Anthropic client. + * Accepts any model name and lets the API validate it. If the model is in KNOWN_CLAUDE_MODELS, uses that modelRef + * for better defaults; otherwise creates a generic model reference. + */ +export function claudeModel( + paramsOrName: ClaudeModelParams | string, + client?: Anthropic, + cacheSystemPrompt?: boolean, + defaultApiVersion?: 'stable' | 'beta' +): ModelAction { + const params = + typeof paramsOrName === 'string' + ? { + name: paramsOrName, + client: + client ?? + (() => { + throw new Error( + 'Anthropic client is required to create a model action' + ); + })(), + cacheSystemPrompt, + defaultApiVersion, + } + : paramsOrName; + + const { + name, + client: runnerClient, + cacheSystemPrompt: cachePrompt, + defaultApiVersion: apiVersion, + } = params; + // Use supported model ref if available, otherwise create generic model ref + const modelRef = KNOWN_CLAUDE_MODELS[name]; + const modelInfo = modelRef ? modelRef.info : GENERIC_CLAUDE_MODEL_INFO; + const configSchema = modelRef?.configSchema ?? AnthropicConfigSchema; + + return model< + AnthropicBaseConfigSchemaType | AnthropicThinkingConfigSchemaType + >( + { + name: `anthropic/${name}`, + ...modelInfo, + configSchema: configSchema, + }, + claudeRunner( + { + name, + client: runnerClient, + cacheSystemPrompt: cachePrompt, + defaultApiVersion: apiVersion, + }, + configSchema + ) + ); +} diff --git a/js/plugins/anthropic/src/runner/base.ts b/js/plugins/anthropic/src/runner/base.ts new file mode 100644 index 0000000000..e6b7132e28 --- /dev/null +++ b/js/plugins/anthropic/src/runner/base.ts @@ -0,0 +1,550 @@ +/** + * 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 { Anthropic } from '@anthropic-ai/sdk'; +import type { DocumentBlockParam } from '@anthropic-ai/sdk/resources/messages'; +import type { + GenerateRequest, + GenerateResponseChunkData, + GenerateResponseData, + MessageData, + Part, + Role, +} from 'genkit'; +import { Message as GenkitMessage } from 'genkit'; +import type { ToolDefinition } from 'genkit/model'; + +import { + AnthropicConfigSchema, + Media, + MediaSchema, + MediaType, + MediaTypeSchema, + type ClaudeRunnerParams, + type ThinkingConfig, +} from '../types.js'; + +import { + RunnerContentBlockParam, + RunnerMessage, + RunnerMessageParam, + RunnerRequestBody, + RunnerStream, + RunnerStreamEvent, + RunnerStreamingRequestBody, + RunnerTool, + RunnerToolResponseContent, + RunnerTypes, +} from './types.js'; + +const ANTHROPIC_THINKING_CUSTOM_KEY = 'anthropicThinking'; + +/** + * Shared runner logic for Anthropic SDK integrations. + * + * Concrete subclasses pass in their SDK-specific type bundle via `RunnerTypes`, + * letting this base class handle message/tool translation once for both the + * stable and beta APIs that share the same conceptual surface. + */ +export abstract class BaseRunner { + protected name: string; + protected client: Anthropic; + protected cacheSystemPrompt?: boolean; + + /** + * Default maximum output tokens for Claude models when not specified in the request. + */ + protected readonly DEFAULT_MAX_OUTPUT_TOKENS = 4096; + + constructor(params: ClaudeRunnerParams) { + this.name = params.name; + this.client = params.client; + this.cacheSystemPrompt = params.cacheSystemPrompt; + } + + /** + * Converts a Genkit role to the corresponding Anthropic role. + */ + protected toAnthropicRole( + role: Role, + toolMessageType?: 'tool_use' | 'tool_result' + ): 'user' | 'assistant' { + 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}`); + } + + protected isMediaType(value: string): value is MediaType { + return MediaTypeSchema.safeParse(value).success; + } + + protected isMediaObject(obj: unknown): obj is Media { + return MediaSchema.safeParse(obj).success; + } + + /** + * Checks if a URL is a data URL (starts with 'data:'). + */ + protected isDataUrl(url: string): boolean { + return url.startsWith('data:'); + } + + protected extractDataFromBase64Url( + url: string + ): { data: string; contentType: string } | null { + 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. + */ + protected toPdfDocumentSource(media: Media): DocumentBlockParam['source'] { + 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. + */ + protected toImageSource( + media: Media + ): + | { kind: 'base64'; data: string; mediaType: MediaType } + | { kind: 'url'; url: string } { + 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)) { + // Provide helpful error message for text files + 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'); + } + + // For non-data URLs, use the provided contentType or default to a generic type + // Note: Anthropic will validate the actual content when fetching from URL + if (media.contentType) { + if (!this.isMediaType(media.contentType)) { + // Provide helpful error message for text files + 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. + */ + protected toAnthropicToolResponseContent( + part: Part + ): RunnerToolResponseContent { + const output = part.toolResponse?.output ?? {}; + + // Handle Media objects (images returned by tools) + if (this.isMediaObject(output)) { + const { data, contentType } = + this.extractDataFromBase64Url(output.url) ?? {}; + if (data && contentType) { + if (!this.isMediaType(contentType)) { + // Provide helpful error message for text files + 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, + }, + }; + } + } + + // Handle string outputs - check if it's a data URL + if (typeof output === 'string') { + // Check if string is a data URL (e.g., "data:image/gif;base64,...") + if (this.isDataUrl(output)) { + const { data, contentType } = + this.extractDataFromBase64Url(output) ?? {}; + if (data && contentType) { + if (!this.isMediaType(contentType)) { + // Provide helpful error message for text files + 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, + }, + }; + } + } + // Regular string output + return { + type: 'text', + text: output, + }; + } + + // Handle other outputs by stringifying + return { + type: 'text', + text: JSON.stringify(output), + }; + } + + protected createThinkingPart(thinking: string, signature?: string): Part { + const custom = + signature !== undefined + ? { + [ANTHROPIC_THINKING_CUSTOM_KEY]: { signature }, + } + : undefined; + return custom + ? { + reasoning: thinking, + custom, + } + : { + reasoning: thinking, + }; + } + + protected getThinkingSignature(part: Part): string | undefined { + const custom = part.custom as Record | undefined; + const thinkingValue = custom?.[ANTHROPIC_THINKING_CUSTOM_KEY]; + if ( + typeof thinkingValue === 'object' && + thinkingValue !== null && + 'signature' in thinkingValue && + typeof (thinkingValue as { signature: unknown }).signature === 'string' + ) { + return (thinkingValue as { signature: string }).signature; + } + return undefined; + } + + protected getRedactedThinkingData(part: Part): string | undefined { + const custom = part.custom as Record | undefined; + const redacted = custom?.redactedThinking; + return typeof redacted === 'string' ? redacted : undefined; + } + + protected toAnthropicThinkingConfig( + config: ThinkingConfig | undefined + ): + | { type: 'enabled'; budget_tokens: number } + | { type: 'disabled' } + | undefined { + if (!config) return undefined; + + const { enabled, budgetTokens } = config; + + if (enabled === true) { + if (budgetTokens === undefined) { + return undefined; + } + return { type: 'enabled', budget_tokens: budgetTokens }; + } + + if (enabled === false) { + return { type: 'disabled' }; + } + + if (budgetTokens !== undefined) { + return { type: 'enabled', budget_tokens: budgetTokens }; + } + + return undefined; + } + + protected toWebSearchToolResultPart(params: { + toolUseId: string; + content: unknown; + type: string; + }): Part { + const { toolUseId, content, type } = params; + return { + text: `[Anthropic server tool result ${toolUseId}] ${JSON.stringify(content)}`, + custom: { + anthropicServerToolResult: { + type, + toolUseId, + content, + }, + }, + }; + } + + /** + * Converts a Genkit Part to the corresponding Anthropic content block. + * Each runner implements this to return its specific API type. + */ + protected abstract toAnthropicMessageContent( + part: Part + ): RunnerContentBlockParam; + + /** + * Converts Genkit messages to Anthropic format. + * Extracts system message and converts remaining messages using the runner's + * toAnthropicMessageContent implementation. + */ + protected toAnthropicMessages(messages: MessageData[]): { + system?: string; + messages: RunnerMessageParam[]; + } { + let system: string | undefined; + + if (messages[0]?.role === 'system') { + const systemMessage = messages[0]; + const textParts: string[] = []; + + for (const part of systemMessage.content ?? []) { + if (part.text) { + textParts.push(part.text); + } else 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.' + ); + } + } + + // Concatenate multiple text parts into a single string. + // Note: The Anthropic SDK supports system as string | Array, + // so we could alternatively preserve the multi-part structure as: + // system = textParts.map(text => ({ type: 'text', text })) + // However, concatenation is simpler and maintains semantic equivalence while + // keeping the cache control logic straightforward in the concrete runners. + system = textParts.length > 0 ? textParts.join('\n\n') : undefined; + } + + const messagesToIterate = + system !== undefined ? messages.slice(1) : messages; + const anthropicMsgs: RunnerMessageParam[] = []; + + for (const message of messagesToIterate) { + const msg = new GenkitMessage(message); + + // Detect tool message kind from Genkit Parts (no SDK typing needed) + const hadToolUse = msg.content.some((p) => !!p.toolRequest); + const hadToolResult = msg.content.some((p) => !!p.toolResponse); + + const toolMessageType = hadToolUse + ? ('tool_use' as const) + : hadToolResult + ? ('tool_result' as const) + : undefined; + + 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. + */ + protected toAnthropicTool(tool: ToolDefinition): RunnerTool { + return { + name: tool.name, + description: tool.description, + input_schema: tool.inputSchema, + } as RunnerTool; + } + + /** + * Converts an Anthropic request to a non-streaming Anthropic API request body. + * @param modelName The name of the Anthropic model to use. + * @param request The Genkit GenerateRequest to convert. + * @param cacheSystemPrompt Whether to cache the system prompt. + * @returns The converted Anthropic API non-streaming request body. + * @throws An error if an unsupported output format is requested. + */ + protected abstract toAnthropicRequestBody( + modelName: string, + request: GenerateRequest, + cacheSystemPrompt?: boolean + ): RunnerRequestBody; + + /** + * Converts an Anthropic request to a streaming Anthropic API request body. + * @param modelName The name of the Anthropic model to use. + * @param request The Genkit GenerateRequest to convert. + * @param cacheSystemPrompt Whether to cache the system prompt. + * @returns The converted Anthropic API streaming request body. + * @throws An error if an unsupported output format is requested. + */ + protected abstract toAnthropicStreamingRequestBody( + modelName: string, + request: GenerateRequest, + cacheSystemPrompt?: boolean + ): RunnerStreamingRequestBody; + + protected abstract createMessage( + body: RunnerRequestBody, + abortSignal: AbortSignal + ): Promise>; + + protected abstract streamMessages( + body: RunnerStreamingRequestBody, + abortSignal: AbortSignal + ): RunnerStream; + + protected abstract toGenkitResponse( + message: RunnerMessage + ): GenerateResponseData; + + protected abstract toGenkitPart( + event: RunnerStreamEvent + ): Part | undefined; + + public async run( + request: GenerateRequest, + options: { + streamingRequested: boolean; + sendChunk: (chunk: GenerateResponseChunkData) => void; + abortSignal: AbortSignal; + } + ): Promise { + const { streamingRequested, sendChunk, abortSignal } = options; + + if (streamingRequested) { + const body = this.toAnthropicStreamingRequestBody( + this.name, + request, + this.cacheSystemPrompt + ); + const stream = this.streamMessages(body, 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, + this.cacheSystemPrompt + ); + const response = await this.createMessage(body, abortSignal); + return this.toGenkitResponse(response); + } +} diff --git a/js/plugins/anthropic/src/runner/beta.ts b/js/plugins/anthropic/src/runner/beta.ts new file mode 100644 index 0000000000..6a71fa71d5 --- /dev/null +++ b/js/plugins/anthropic/src/runner/beta.ts @@ -0,0 +1,494 @@ +/** + * 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([ + '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 { + 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 { + 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, + 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, + 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'; + } + } +} diff --git a/js/plugins/anthropic/src/runner/index.ts b/js/plugins/anthropic/src/runner/index.ts new file mode 100644 index 0000000000..ce7e3c6fdd --- /dev/null +++ b/js/plugins/anthropic/src/runner/index.ts @@ -0,0 +1,19 @@ +/** + * 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. + */ + +export { BaseRunner } from './base.js'; +export { BetaRunner } from './beta.js'; +export { Runner } from './stable.js'; diff --git a/js/plugins/anthropic/src/runner/stable.ts b/js/plugins/anthropic/src/runner/stable.ts new file mode 100644 index 0000000000..0c8f7ffc4f --- /dev/null +++ b/js/plugins/anthropic/src/runner/stable.ts @@ -0,0 +1,514 @@ +/** + * 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 { MessageStream } from '@anthropic-ai/sdk/lib/MessageStream.js'; +import type { + ContentBlock, + DocumentBlockParam, + ImageBlockParam, + Message, + MessageCreateParams, + MessageCreateParamsNonStreaming, + MessageCreateParamsStreaming, + MessageParam, + MessageStreamEvent, + RedactedThinkingBlockParam, + TextBlockParam, + ThinkingBlockParam, + Tool, + ToolResultBlockParam, + ToolUseBlockParam, +} from '@anthropic-ai/sdk/resources/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 as BaseRunnerTypes } from './types.js'; +interface RunnerTypes extends BaseRunnerTypes { + Message: Message; + Stream: MessageStream; + StreamEvent: MessageStreamEvent; + RequestBody: MessageCreateParamsNonStreaming; + StreamingRequestBody: MessageCreateParamsStreaming; + Tool: Tool; + MessageParam: MessageParam; + ToolResponseContent: TextBlockParam | ImageBlockParam; + ContentBlockParam: + | TextBlockParam + | ImageBlockParam + | DocumentBlockParam + | ToolUseBlockParam + | ToolResultBlockParam + | ThinkingBlockParam + | RedactedThinkingBlockParam; +} + +export class Runner extends BaseRunner { + constructor(params: ClaudeRunnerParams) { + super(params); + } + + protected toAnthropicMessageContent( + part: Part + ): + | TextBlockParam + | ImageBlockParam + | DocumentBlockParam + | ToolUseBlockParam + | ToolResultBlockParam + | ThinkingBlockParam + | RedactedThinkingBlockParam { + 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, + }; + } + + if (part.text) { + return { + type: 'text', + text: part.text, + citations: null, + }; + } + + 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, + }, + }; + } + + 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, + }; + } + + if (part.toolResponse) { + if (!part.toolResponse.ref) { + throw new Error( + `Tool response ref is required for Anthropic API. Part: ${JSON.stringify( + part.toolResponse + )}` + ); + } + return { + type: 'tool_result', + tool_use_id: part.toolResponse.ref, + content: [this.toAnthropicToolResponseContent(part)], + }; + } + + throw new Error( + `Unsupported genkit part fields encountered for current message role: ${JSON.stringify( + part + )}.` + ); + } + + protected toAnthropicRequestBody( + modelName: string, + request: GenerateRequest, + cacheSystemPrompt?: boolean + ): MessageCreateParamsNonStreaming { + const model = KNOWN_CLAUDE_MODELS[modelName]; + const { system, messages } = this.toAnthropicMessages(request.messages); + const mappedModelName = + request.config?.version ?? extractVersion(model, modelName); + + const systemValue = + system === undefined + ? undefined + : cacheSystemPrompt + ? [ + { + type: 'text' as const, + text: system, + cache_control: { type: 'ephemeral' as const }, + }, + ] + : system; + + const body: MessageCreateParamsNonStreaming = { + model: mappedModelName, + max_tokens: + request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS, + messages, + }; + + if (systemValue !== undefined) { + body.system = systemValue; + } + + if (request.tools) { + body.tools = request.tools.map((tool) => this.toAnthropicTool(tool)); + } + 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?.temperature !== undefined) { + body.temperature = request.config.temperature; + } + if (request.config?.stopSequences !== undefined) { + body.stop_sequences = request.config.stopSequences; + } + if (request.config?.metadata !== undefined) { + body.metadata = request.config.metadata; + } + if (request.config?.tool_choice !== undefined) { + body.tool_choice = request.config.tool_choice; + } + const thinkingConfig = this.toAnthropicThinkingConfig( + request.config?.thinking + ); + if (thinkingConfig) { + body.thinking = thinkingConfig as MessageCreateParams['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 toAnthropicStreamingRequestBody( + modelName: string, + request: GenerateRequest, + cacheSystemPrompt?: boolean + ): MessageCreateParamsStreaming { + const model = KNOWN_CLAUDE_MODELS[modelName]; + const { system, messages } = this.toAnthropicMessages(request.messages); + const mappedModelName = + request.config?.version ?? extractVersion(model, modelName); + + const systemValue = + system === undefined + ? undefined + : cacheSystemPrompt + ? [ + { + type: 'text' as const, + text: system, + cache_control: { type: 'ephemeral' as const }, + }, + ] + : system; + + const body: MessageCreateParamsStreaming = { + model: mappedModelName, + max_tokens: + request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS, + messages, + stream: true, + }; + + if (systemValue !== undefined) { + body.system = systemValue; + } + + if (request.tools) { + body.tools = request.tools.map((tool) => this.toAnthropicTool(tool)); + } + 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?.temperature !== undefined) { + body.temperature = request.config.temperature; + } + if (request.config?.stopSequences !== undefined) { + body.stop_sequences = request.config.stopSequences; + } + if (request.config?.metadata !== undefined) { + body.metadata = request.config.metadata; + } + if (request.config?.tool_choice !== undefined) { + body.tool_choice = request.config.tool_choice; + } + const thinkingConfig = this.toAnthropicThinkingConfig( + request.config?.thinking + ); + if (thinkingConfig) { + body.thinking = + thinkingConfig as MessageCreateParamsStreaming['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 async createMessage( + body: MessageCreateParamsNonStreaming, + abortSignal: AbortSignal + ): Promise { + return await this.client.messages.create(body, { signal: abortSignal }); + } + + protected streamMessages( + body: MessageCreateParamsStreaming, + abortSignal: AbortSignal + ): MessageStream { + return this.client.messages.stream(body, { signal: abortSignal }); + } + + protected toGenkitResponse(message: Message): GenerateResponseData { + return this.fromAnthropicResponse(message); + } + + protected toGenkitPart(event: MessageStreamEvent): Part | undefined { + return this.fromAnthropicContentBlockChunk(event); + } + + protected fromAnthropicContentBlockChunk( + event: MessageStreamEvent + ): Part | undefined { + // Handle content_block_delta events + if (event.type === 'content_block_delta') { + const delta = event.delta; + + if (delta.type === 'input_json_delta') { + throw new Error( + 'Anthropic streaming tool input (input_json_delta) is not yet supported. Please disable streaming or upgrade this plugin.' + ); + } + + if (delta.type === 'text_delta') { + return { text: delta.text }; + } + + if (delta.type === 'thinking_delta') { + return { reasoning: delta.thinking }; + } + + // signature_delta - ignore + return undefined; + } + + // Handle content_block_start events + if (event.type === 'content_block_start') { + const block = event.content_block; + + switch (block.type) { + case 'server_tool_use': + return { + text: `[Anthropic server tool ${block.name}] input: ${JSON.stringify(block.input)}`, + custom: { + anthropicServerToolUse: { + id: block.id, + name: block.name, + input: block.input, + }, + }, + }; + + case 'web_search_tool_result': + return this.toWebSearchToolResultPart({ + type: block.type, + toolUseId: block.tool_use_id, + content: block.content, + }); + + case 'text': + return { text: block.text }; + + case 'thinking': + return this.createThinkingPart(block.thinking, block.signature); + + case 'redacted_thinking': + return { custom: { redactedThinking: block.data } }; + + case 'tool_use': + return { + toolRequest: { + ref: block.id, + name: block.name, + input: block.input, + }, + }; + + default: { + const unknownType = (block as { type: string }).type; + logger.warn( + `Unexpected Anthropic content block type in stream: ${unknownType}. Returning undefined. Content block: ${JSON.stringify(block)}` + ); + return undefined; + } + } + } + + // Other event types (message_start, message_delta, etc.) - ignore + return undefined; + } + + protected fromAnthropicContentBlock(contentBlock: ContentBlock): Part { + switch (contentBlock.type) { + case 'server_tool_use': + return { + text: `[Anthropic server tool ${contentBlock.name}] input: ${JSON.stringify(contentBlock.input)}`, + custom: { + anthropicServerToolUse: { + id: contentBlock.id, + name: contentBlock.name, + input: contentBlock.input, + }, + }, + }; + + case 'web_search_tool_result': + return this.toWebSearchToolResultPart({ + type: contentBlock.type, + toolUseId: contentBlock.tool_use_id, + content: contentBlock.content, + }); + + case 'tool_use': + return { + toolRequest: { + ref: contentBlock.id, + name: contentBlock.name, + input: contentBlock.input, + }, + }; + + case 'text': + return { text: contentBlock.text }; + + case 'thinking': + return this.createThinkingPart( + contentBlock.thinking, + contentBlock.signature + ); + + case 'redacted_thinking': + return { custom: { redactedThinking: contentBlock.data } }; + + default: { + const unknownType = (contentBlock as { type: string }).type; + logger.warn( + `Unexpected Anthropic content block type: ${unknownType}. Returning empty text. Content block: ${JSON.stringify(contentBlock)}` + ); + return { text: '' }; + } + } + } + + protected fromAnthropicStopReason( + reason: Message['stop_reason'] + ): ModelResponseData['finishReason'] { + switch (reason) { + case 'max_tokens': + return 'length'; + case 'end_turn': + // fall through + case 'stop_sequence': + // fall through + case 'tool_use': + return 'stop'; + case null: + return 'unknown'; + default: + return 'other'; + } + } + + protected fromAnthropicResponse(response: Message): GenerateResponseData { + return { + candidates: [ + { + index: 0, + finishReason: this.fromAnthropicStopReason(response.stop_reason), + message: { + role: 'model', + content: response.content.map((block) => + this.fromAnthropicContentBlock(block) + ), + }, + }, + ], + usage: { + inputTokens: response.usage.input_tokens, + outputTokens: response.usage.output_tokens, + }, + custom: response, + }; + } +} diff --git a/js/plugins/anthropic/src/runner/types.ts b/js/plugins/anthropic/src/runner/types.ts new file mode 100644 index 0000000000..5fd04c6911 --- /dev/null +++ b/js/plugins/anthropic/src/runner/types.ts @@ -0,0 +1,78 @@ +/** + * 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. + */ + +/** + * Type contract that each Anthropic runner passes into the generic `BaseRunner`. + * + * The concrete runners (stable vs. beta SDKs) bind these slots to their SDK’s + * concrete interfaces so the shared logic in `BaseRunner` can stay strongly typed + * without knowing which SDK variant it is talking to. + * + * Properties are `unknown` by default, so every subclass must plug in the + * correct Anthropic types to keep the generic plumbing sound. + */ +type RunnerTypes = { + Message: unknown; + Stream: AsyncIterable & { finalMessage(): Promise }; + StreamEvent: unknown; + RequestBody: unknown; + StreamingRequestBody: unknown; + Tool: unknown; + MessageParam: unknown; + ContentBlockParam: unknown; + ToolResponseContent: unknown; +}; + +type RunnerMessage = ApiTypes['Message']; + +/** Streaming handle that yields Anthropic events and exposes the final message. */ +type RunnerStream = ApiTypes['Stream']; + +/** Discrete event emitted by the Anthropic stream (delta, block start, etc.). */ +type RunnerStreamEvent = ApiTypes['StreamEvent']; + +/** Non-streaming request payload shape for create-message calls. */ +type RunnerRequestBody = ApiTypes['RequestBody']; +type RunnerStreamingRequestBody = + ApiTypes['StreamingRequestBody']; + +/** Tool definition compatible with the target Anthropic SDK. */ +type RunnerTool = ApiTypes['Tool']; + +/** Anthropic message param shape used when sending history to the API. */ +type RunnerMessageParam = + ApiTypes['MessageParam']; + +/** Content block that the runner sends to Anthropic for a single part. */ +type RunnerContentBlockParam = + ApiTypes['ContentBlockParam']; + +/** Tool response block that Anthropic expects when returning tool output. */ +type RunnerToolResponseContent = + ApiTypes['ToolResponseContent']; + +export { + RunnerContentBlockParam, + RunnerMessage, + RunnerMessageParam, + RunnerRequestBody, + RunnerStream, + RunnerStreamEvent, + RunnerStreamingRequestBody, + RunnerTool, + RunnerToolResponseContent, + RunnerTypes, +}; diff --git a/js/plugins/anthropic/src/types.ts b/js/plugins/anthropic/src/types.ts new file mode 100644 index 0000000000..9796d71c36 --- /dev/null +++ b/js/plugins/anthropic/src/types.ts @@ -0,0 +1,166 @@ +/** + * Copyright 2024 Bloom Labs Inc + * 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 type Anthropic from '@anthropic-ai/sdk'; +import { z } from 'genkit'; +import { GenerationCommonConfigSchema } from 'genkit/model'; + +/** + * Internal symbol for dependency injection in tests. + * Not part of the public API. + * @internal + */ +export const __testClient = Symbol('testClient'); + +/** + * Plugin configuration options for the Anthropic plugin. + */ +export interface PluginOptions { + apiKey?: string; + cacheSystemPrompt?: boolean; + /** Default API surface for all requests unless overridden per-request. */ + apiVersion?: 'stable' | 'beta'; +} + +/** + * Internal plugin options that include test client injection. + * @internal + */ +export interface InternalPluginOptions extends PluginOptions { + [__testClient]?: Anthropic; +} + +/** + * Shared parameters required to construct Claude helpers. + */ +interface ClaudeHelperParamsBase { + name: string; + client: Anthropic; + cacheSystemPrompt?: boolean; + defaultApiVersion?: 'stable' | 'beta'; +} + +/** + * Parameters for creating a Claude model action. + */ +export interface ClaudeModelParams extends ClaudeHelperParamsBase {} + +/** + * Parameters for creating a Claude runner. + */ +export interface ClaudeRunnerParams extends ClaudeHelperParamsBase {} + +export const AnthropicBaseConfigSchema = GenerationCommonConfigSchema.extend({ + tool_choice: z + .union([ + z.object({ + type: z.literal('auto'), + }), + z.object({ + type: z.literal('any'), + }), + z.object({ + type: z.literal('tool'), + name: z.string(), + }), + ]) + .optional(), + metadata: z + .object({ + user_id: z.string().optional(), + }) + .optional(), + /** Optional shorthand to pick API surface for this request. */ + apiVersion: z.enum(['stable', 'beta']).optional(), +}); + +export type AnthropicBaseConfigSchemaType = typeof AnthropicBaseConfigSchema; + +export const ThinkingConfigSchema = z + .object({ + enabled: z.boolean().optional(), + budgetTokens: z.number().int().min(1_024).optional(), + }) + .superRefine((value, ctx) => { + if (value.enabled && value.budgetTokens === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['budgetTokens'], + message: 'budgetTokens is required when thinking is enabled', + }); + } + }); + +export const AnthropicThinkingConfigSchema = AnthropicBaseConfigSchema.extend({ + thinking: ThinkingConfigSchema.optional(), +}); + +export const AnthropicConfigSchema = AnthropicThinkingConfigSchema; + +export type ThinkingConfig = z.infer; +export type AnthropicBaseConfig = z.infer; +export type AnthropicThinkingConfig = z.infer< + typeof AnthropicThinkingConfigSchema +>; +export type ClaudeConfig = AnthropicThinkingConfig | AnthropicBaseConfig; + +/** + * Media object representation with URL and optional content type. + */ +export interface Media { + url: string; + contentType?: string; +} + +export const MediaSchema = z.object({ + url: z.string(), + contentType: z.string().optional(), +}); + +export const MediaTypeSchema = z.enum([ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', +]); + +export type MediaType = z.infer; + +export const MEDIA_TYPES = { + JPEG: 'image/jpeg', + PNG: 'image/png', + GIF: 'image/gif', + WEBP: 'image/webp', +} as const satisfies Record; + +/** + * Resolve whether beta API should be used for this call. + * Priority: + * 1. request.config.apiVersion (per-request override - explicit stable or beta) + * 2. pluginDefaultApiVersion (plugin-wide default) + * 3. otherwise stable + */ +export function resolveBetaEnabled( + cfg: AnthropicThinkingConfig | AnthropicBaseConfig | undefined, + pluginDefaultApiVersion?: 'stable' | 'beta' +): boolean { + if (cfg?.apiVersion !== undefined) { + return cfg.apiVersion === 'beta'; + } + if (pluginDefaultApiVersion === 'beta') return true; + return false; +} diff --git a/js/plugins/anthropic/tsconfig.json b/js/plugins/anthropic/tsconfig.json new file mode 100644 index 0000000000..596e2cf729 --- /dev/null +++ b/js/plugins/anthropic/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/js/plugins/anthropic/tsup.config.ts b/js/plugins/anthropic/tsup.config.ts new file mode 100644 index 0000000000..d55507161f --- /dev/null +++ b/js/plugins/anthropic/tsup.config.ts @@ -0,0 +1,22 @@ +/** + * 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 { defineConfig, type Options } from 'tsup'; +import { defaultOptions } from '../../tsup.common'; + +export default defineConfig({ + ...(defaultOptions as Options), +}); diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 00f3027cfd..328d92253e 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -181,7 +181,7 @@ importers: optionalDependencies: '@genkit-ai/firebase': specifier: ^1.16.1 - version: 1.16.1(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) + version: 1.16.1(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) doc-snippets: dependencies: @@ -257,6 +257,37 @@ importers: specifier: ^4.9.0 version: 4.9.5 + plugins/anthropic: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.68.0 + version: 0.68.0(zod@3.25.67) + devDependencies: + '@types/node': + specifier: ^20.11.16 + version: 20.19.1 + check-node-version: + specifier: ^4.2.1 + version: 4.2.1 + genkit: + specifier: workspace:* + version: link:../../genkit + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + tsup: + specifier: ^8.3.5 + version: 8.5.0(postcss@8.4.47)(tsx@4.20.3)(typescript@4.9.5)(yaml@2.8.0) + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^4.9.0 + version: 4.9.5 + plugins/checks: dependencies: '@genkit-ai/ai': @@ -988,6 +1019,25 @@ importers: specifier: '>=12.2' version: 13.4.0(encoding@0.1.13) + testapps/anthropic: + dependencies: + '@genkit-ai/anthropic': + specifier: workspace:* + version: link:../../plugins/anthropic + genkit: + specifier: workspace:* + version: link:../../genkit + devDependencies: + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^5.6.2 + version: 5.8.3 + testapps/basic-gemini: dependencies: '@genkit-ai/firebase': @@ -1001,7 +1051,7 @@ importers: version: link:../../plugins/google-genai '@google/genai': specifier: ^1.29.0 - version: 1.29.0 + version: 1.29.1 express: specifier: ^4.20.0 version: 4.21.2 @@ -1029,7 +1079,7 @@ importers: version: link:../../plugins/compat-oai '@genkit-ai/express': specifier: ^1.1.0 - version: 1.12.0(@genkit-ai/core@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(express@5.1.0)(genkit@genkit) + version: 1.12.0(@genkit-ai/core@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(express@5.1.0)(genkit@genkit) genkit: specifier: workspace:* version: link:../../genkit @@ -1596,7 +1646,7 @@ importers: version: link:../../plugins/ollama genkitx-openai: specifier: ^0.10.1 - version: 0.10.1(@genkit-ai/ai@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(@genkit-ai/core@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(encoding@0.1.13)(ws@8.18.3) + version: 0.10.1(@genkit-ai/ai@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(@genkit-ai/core@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(encoding@0.1.13)(ws@8.18.3) devDependencies: rimraf: specifier: ^6.0.1 @@ -2036,6 +2086,15 @@ packages: '@anthropic-ai/sdk@0.24.3': resolution: {integrity: sha512-916wJXO6T6k8R6BAAcLhLPv/pnLGy7YSEBZXZ1XTFbLcTZE8oTy3oDW9WJf9KKZwMvVcePIfoTSvzXHRcGxkQQ==} + '@anthropic-ai/sdk@0.68.0': + resolution: {integrity: sha512-SMYAmbbiprG8k1EjEPMTwaTqssDT7Ae+jxcR5kWXiqTlbwMR2AthXtscEVWOHkRfyAV5+y3PFYTJRNa3OJWIEw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@anthropic-ai/sdk@0.9.1': resolution: {integrity: sha512-wa1meQ2WSfoY8Uor3EdrJq0jTiZJoKoSii2ZVWRY1oN4Tlr5s59pADg9T79FTbPe1/se5c3pBeZgJL63wmuoBA==} @@ -2196,6 +2255,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.7': resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} engines: {node: '>=6.9.0'} @@ -2625,11 +2688,11 @@ packages: '@firebase/webchannel-wrapper@1.0.3': resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} - '@genkit-ai/ai@1.22.0': - resolution: {integrity: sha512-TDKO+zWyM5YI8zE4a0IlqlpgHuLB4B4islzgWDvzdQlbjtyJp0ayODAMFhS2ruQ6+a/UdXDySRrOX/RcqF4yjA==} + '@genkit-ai/ai@1.23.0': + resolution: {integrity: sha512-9CuZJYZnnmAtqVcOEDU/bSCJYPELu2oveEoOSECVA6St3bmebID9mL0F51AKGi6YyB1Xz+GLny35PTbqxrhAWw==} - '@genkit-ai/core@1.22.0': - resolution: {integrity: sha512-etVlpwJkPoy91xR6H5+S/AWZPJMeovb7N35+B90md1+6xWcodQF7WZ3chKcH31Xamlz+jTIvd3riiZGY9RFumg==} + '@genkit-ai/core@1.23.0': + resolution: {integrity: sha512-JG5QPwM49HZmH69ky2DZUevJLEkeIkAo1EdIbYC0E32cH4VZp0Mn1ooA73oTLZm0/D/LrJ2zc5487Eu3gIhs9Q==} '@genkit-ai/express@1.12.0': resolution: {integrity: sha512-QAxSS07dX5ovSfsUB4s90KaDnv4zg1wnoxCZCa+jBsYUyv9NvCCTsOk25xAQgGxc7xi3+MD+3AsPier5oZILIg==} @@ -2743,8 +2806,8 @@ packages: resolution: {integrity: sha512-HqYqoivNtkq59po8m7KI0n+lWKdz4kabENncYQXZCX/hBWJfXtKAfR/2nUQsP+TwSfHKoA7zDL2RrJYIv/j3VQ==} engines: {node: '>=18.0.0'} - '@google/genai@1.29.0': - resolution: {integrity: sha512-cQP7Ssa06W+MSAyVtL/812FBtZDoDehnFObIpK1xo5Uv4XvqBcVZ8OhXgihOIXWn7xvPQGvLclR8+yt3Ysnd9g==} + '@google/genai@1.29.1': + resolution: {integrity: sha512-Buywpq0A6xf9cOdhiWCi5KUiDBbZkjCH5xbl+xxNQRItoYQgd31p0OKyn5cUnT0YNzC/pAmszqXoOc7kncqfFQ==} engines: {node: '>=20.0.0'} peerDependencies: '@modelcontextprotocol/sdk': ^1.20.1 @@ -4579,6 +4642,10 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -4590,6 +4657,11 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-node-version@4.2.1: + resolution: {integrity: sha512-YYmFYHV/X7kSJhuN/QYHUu998n/TRuDe8UenM3+m5NrkiH670lb9ILqHIvBencvJc4SDh+XcbXMR4b+TtubJiw==} + engines: {node: '>=8.3.0'} + hasBin: true + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -5275,8 +5347,8 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} - genkit@1.22.0: - resolution: {integrity: sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==} + genkit@1.23.0: + resolution: {integrity: sha512-8BOpwt40Yqadc6shvfcKk4fTzrtlIV8ntv9Mqx3Yul9BbM4kRXW+h6nYMaPMKT4ZdErzZgnfxU7e6t1lXFFP4A==} genkitx-openai@0.10.1: resolution: {integrity: sha512-E9/DzyQcBUSTy81xT2pvEmdnn9Q/cKoojEt6lD/EdOeinhqE9oa59d/kuXTokCMekTrj3Rk7LtNBQIDjnyjNOA==} @@ -5369,8 +5441,8 @@ packages: resolution: {integrity: sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==} engines: {node: '>=14'} - google-logging-utils@1.1.2: - resolution: {integrity: sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==} + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} google-p12-pem@4.0.1: @@ -5899,6 +5971,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -6295,6 +6371,9 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + map-values@1.0.1: + resolution: {integrity: sha512-BbShUnr5OartXJe1GeccAWtfro11hhgNJg6G9/UtWKjVGvV5U4C09cg5nk8JUevhXODaXY+hQ3xxMUKSs62ONQ==} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -6528,6 +6607,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-filter@1.0.2: + resolution: {integrity: sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==} + object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -6893,6 +6975,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -7006,6 +7091,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -7378,6 +7466,9 @@ packages: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -7815,6 +7906,12 @@ snapshots: transitivePeerDependencies: - encoding + '@anthropic-ai/sdk@0.68.0(zod@3.25.67)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 3.25.67 + '@anthropic-ai/sdk@0.9.1(encoding@0.1.13)': dependencies: '@types/node': 18.19.112 @@ -8012,6 +8109,8 @@ snapshots: '@babel/core': 7.25.7 '@babel/helper-plugin-utils': 7.25.7 + '@babel/runtime@7.28.4': {} + '@babel/template@7.25.7': dependencies: '@babel/code-frame': 7.25.7 @@ -8508,9 +8607,9 @@ snapshots: '@firebase/webchannel-wrapper@1.0.3': {} - '@genkit-ai/ai@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': + '@genkit-ai/ai@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': dependencies: - '@genkit-ai/core': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) + '@genkit-ai/core': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) '@opentelemetry/api': 1.9.0 '@types/node': 20.19.1 colorette: 2.0.20 @@ -8529,9 +8628,9 @@ snapshots: - supports-color optional: true - '@genkit-ai/ai@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit)': + '@genkit-ai/ai@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit)': dependencies: - '@genkit-ai/core': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) + '@genkit-ai/core': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) '@opentelemetry/api': 1.9.0 '@types/node': 20.19.1 colorette: 2.0.20 @@ -8549,7 +8648,7 @@ snapshots: - genkit - supports-color - '@genkit-ai/core@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': + '@genkit-ai/core@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0) @@ -8571,7 +8670,7 @@ snapshots: zod: 3.25.67 zod-to-json-schema: 3.24.5(zod@3.25.67) optionalDependencies: - '@genkit-ai/firebase': 1.16.1(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) + '@genkit-ai/firebase': 1.16.1(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) transitivePeerDependencies: - '@google-cloud/firestore' - encoding @@ -8581,7 +8680,7 @@ snapshots: - supports-color optional: true - '@genkit-ai/core@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit)': + '@genkit-ai/core@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0) @@ -8612,9 +8711,9 @@ snapshots: - genkit - supports-color - '@genkit-ai/express@1.12.0(@genkit-ai/core@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(express@5.1.0)(genkit@genkit)': + '@genkit-ai/express@1.12.0(@genkit-ai/core@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(express@5.1.0)(genkit@genkit)': dependencies: - '@genkit-ai/core': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) + '@genkit-ai/core': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) body-parser: 1.20.3 cors: 2.8.5 express: 5.1.0 @@ -8622,12 +8721,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@genkit-ai/firebase@1.16.1(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': + '@genkit-ai/firebase@1.16.1(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': dependencies: - '@genkit-ai/google-cloud': 1.16.1(encoding@0.1.13)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) + '@genkit-ai/google-cloud': 1.16.1(encoding@0.1.13)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) '@google-cloud/firestore': 7.11.1(encoding@0.1.13) firebase-admin: 13.4.0(encoding@0.1.13) - genkit: 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1) + genkit: 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1) optionalDependencies: firebase: 11.9.1 transitivePeerDependencies: @@ -8648,7 +8747,7 @@ snapshots: - supports-color optional: true - '@genkit-ai/google-cloud@1.16.1(encoding@0.1.13)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': + '@genkit-ai/google-cloud@1.16.1(encoding@0.1.13)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1))': dependencies: '@google-cloud/logging-winston': 6.0.1(encoding@0.1.13)(winston@3.17.0) '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.19.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) @@ -8664,7 +8763,7 @@ snapshots: '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) - genkit: 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1) + genkit: 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1) google-auth-library: 9.15.1(encoding@0.1.13) node-fetch: 3.3.2 winston: 3.17.0 @@ -8891,7 +8990,7 @@ snapshots: - encoding - supports-color - '@google/genai@1.29.0': + '@google/genai@1.29.1': dependencies: google-auth-library: 10.5.0 ws: 8.18.3 @@ -10821,6 +10920,11 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -10830,6 +10934,15 @@ snapshots: charenc@0.0.2: {} + check-node-version@4.2.1: + dependencies: + chalk: 3.0.0 + map-values: 1.0.1 + minimist: 1.2.8 + object-filter: 1.0.2 + run-parallel: 1.2.0 + semver: 6.3.1 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -11708,15 +11821,15 @@ snapshots: gcp-metadata@8.1.2: dependencies: gaxios: 7.1.3 - google-logging-utils: 1.1.2 + google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: - supports-color - genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1): + genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1): dependencies: - '@genkit-ai/ai': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) - '@genkit-ai/core': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) + '@genkit-ai/ai': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) + '@genkit-ai/core': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)) uuid: 10.0.0 transitivePeerDependencies: - '@google-cloud/firestore' @@ -11726,10 +11839,10 @@ snapshots: - supports-color optional: true - genkitx-openai@0.10.1(@genkit-ai/ai@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(@genkit-ai/core@1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(encoding@0.1.13)(ws@8.18.3): + genkitx-openai@0.10.1(@genkit-ai/ai@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(@genkit-ai/core@1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit))(encoding@0.1.13)(ws@8.18.3): dependencies: - '@genkit-ai/ai': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) - '@genkit-ai/core': 1.22.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) + '@genkit-ai/ai': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) + '@genkit-ai/core': 1.23.0(@google-cloud/firestore@7.11.1(encoding@0.1.13))(encoding@0.1.13)(firebase-admin@13.4.0(encoding@0.1.13))(firebase@11.9.1)(genkit@genkit) openai: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.67) zod: 3.25.67 transitivePeerDependencies: @@ -11829,7 +11942,7 @@ snapshots: ecdsa-sig-formatter: 1.0.11 gaxios: 7.1.3 gcp-metadata: 8.1.2 - google-logging-utils: 1.1.2 + google-logging-utils: 1.1.3 gtoken: 8.0.0 jws: 4.0.0 transitivePeerDependencies: @@ -11881,7 +11994,7 @@ snapshots: - encoding - supports-color - google-logging-utils@1.1.2: {} + google-logging-utils@1.1.3: {} google-p12-pem@4.0.1: dependencies: @@ -12698,6 +12811,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.4 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -13034,6 +13152,8 @@ snapshots: dependencies: tmpl: 1.0.5 + map-values@1.0.1: {} + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -13245,6 +13365,8 @@ snapshots: object-assign@4.1.1: {} + object-filter@1.0.2: {} + object-hash@3.0.0: {} object-inspect@1.13.1: {} @@ -13606,6 +13728,8 @@ snapshots: dependencies: side-channel: 1.1.0 + queue-microtask@1.2.3: {} + range-parser@1.2.1: {} raw-body@2.5.2: @@ -13769,6 +13893,10 @@ snapshots: transitivePeerDependencies: - supports-color + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -14213,6 +14341,8 @@ snapshots: triple-beam@1.4.1: {} + ts-algebra@2.0.0: {} + ts-interface-checker@0.1.13: {} ts-jest@29.4.0(@babel/core@7.25.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.19.1)(typescript@4.9.5)))(typescript@4.9.5):