Skip to main content
Glama
firebase
by firebase
integration_test.ts16.1 kB
/** * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as assert from 'assert'; import { genkit, z } from 'genkit'; import { describe, it } from 'node:test'; import { anthropic } from '../src/index.js'; import { __testClient } from '../src/types.js'; import { createMockAnthropicClient, createMockAnthropicMessage, mockContentBlockStart, mockMessageWithContent, mockMessageWithToolUse, mockTextChunk, } from './mocks/anthropic-client.js'; import { PluginOptions } from '../src/types.js'; describe('Anthropic Integration', () => { it('should successfully generate a response', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Hello', }); assert.strictEqual(result.text, 'Hello! How can I help you today?'); }); it('should handle tool calling workflow (call tool, receive result, generate final response)', async () => { const mockClient = createMockAnthropicClient({ sequentialResponses: [ // First response: tool use request mockMessageWithToolUse('get_weather', { city: 'NYC' }), // Second response: final text after tool result createMockAnthropicMessage({ text: 'The weather in NYC is sunny, 72°F', }), ], }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); // Define the tool ai.defineTool( { name: 'get_weather', description: 'Get the weather for a city', inputSchema: z.object({ city: z.string(), }), }, async (input: { city: string }) => { return `The weather in ${input.city} is sunny, 72°F`; } ); const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'What is the weather in NYC?', tools: ['get_weather'], }); assert.ok( result.text.includes('NYC') || result.text.includes('sunny') || result.text.includes('72') ); }); it('should handle multi-turn conversations', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); // First turn const response1 = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'My name is Alice', }); // Second turn with conversation history const response2 = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: "What's my name?", messages: response1.messages, }); // Verify conversation history is maintained assert.ok( response2.messages.length >= 2, 'Should have conversation history' ); assert.strictEqual(response2.messages[0].role, 'user'); assert.ok( response2.messages[0].content[0].text?.includes('Alice') || response2.messages[0].content[0].text?.includes('name') ); }); it('should stream responses with streaming callback', async () => { const mockClient = createMockAnthropicClient({ streamChunks: [ mockContentBlockStart('Hello'), mockTextChunk(' world'), mockTextChunk('!'), ], messageResponse: { content: [{ type: 'text', text: 'Hello world!', citations: null }], usage: { input_tokens: 5, output_tokens: 15, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, cache_creation: null, server_tool_use: null, service_tier: null, }, }, }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const chunks: any[] = []; const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Say hello world', streamingCallback: (chunk) => { chunks.push(chunk); }, }); assert.ok(chunks.length > 0, 'Should have received streaming chunks'); assert.ok(result.text, 'Should have final response text'); }); it('should handle media/image inputs', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', messages: [ { role: 'user', content: [ { text: 'Describe this image:' }, { media: { url: 'data:image/png;base64,R0lGODlhAQABAAAAACw=', contentType: 'image/png', }, }, ], }, ], }); assert.ok(result.text, 'Should generate response for image input'); }); it('should handle WEBP image inputs', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', messages: [ { role: 'user', content: [ { text: 'Describe this image:' }, { media: { url: 'data:image/webp;base64,AAA', contentType: 'image/webp', }, }, ], }, ], }); assert.ok(result.text, 'Should generate response for WEBP image input'); // Verify the request was made with correct media_type const createStub = mockClient.messages.create as any; assert.strictEqual(createStub.mock.calls.length, 1); const requestBody = createStub.mock.calls[0].arguments[0]; const imageContent = requestBody.messages[0].content.find( (c: any) => c.type === 'image' ); assert.ok(imageContent, 'Should have image content in request'); assert.strictEqual( imageContent.source.media_type, 'image/webp', 'Should use WEBP media type from data URL' ); }); it('should handle WEBP image with mismatched contentType (prefers data URL)', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', messages: [ { role: 'user', content: [ { media: { // Data URL says WEBP, but contentType says PNG - should use WEBP url: 'data:image/webp;base64,AAA', contentType: 'image/png', }, }, ], }, ], }); assert.ok(result.text, 'Should generate response for WEBP image input'); // Verify the request was made with WEBP (from data URL), not PNG (from contentType) const createStub = mockClient.messages.create as any; assert.strictEqual(createStub.mock.calls.length, 1); const requestBody = createStub.mock.calls[0].arguments[0]; const imageContent = requestBody.messages[0].content.find( (c: any) => c.type === 'image' ); assert.ok(imageContent, 'Should have image content in request'); assert.strictEqual( imageContent.source.media_type, 'image/webp', 'Should prefer data URL content type (webp) over contentType (png)' ); }); it('should throw helpful error for text/plain media', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); await assert.rejects( async () => { await ai.generate({ model: 'anthropic/claude-3-5-haiku', messages: [ { role: 'user', content: [ { media: { url: 'data:text/plain;base64,AAA', contentType: 'text/plain', }, }, ], }, ], }); }, (error: Error) => { return ( error.message.includes('Text files should be sent as text content') && error.message.includes('text:') ); }, 'Should throw helpful error for text/plain media' ); }); it('should forward thinking config and surface reasoning in responses', async () => { const thinkingContent = [ { type: 'thinking' as const, thinking: 'Let me analyze the problem carefully.', signature: 'sig_reasoning_123', }, { type: 'text' as const, text: 'The answer is 42.', citations: null, }, ]; const mockClient = createMockAnthropicClient({ messageResponse: mockMessageWithContent(thinkingContent), }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const thinkingConfig = { enabled: true, budgetTokens: 2048 }; const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'What is the meaning of life?', config: { thinking: thinkingConfig }, }); const createStub = mockClient.messages.create as any; assert.strictEqual(createStub.mock.calls.length, 1); const requestBody = createStub.mock.calls[0].arguments[0]; assert.deepStrictEqual(requestBody.thinking, { type: 'enabled', budget_tokens: 2048, }); assert.strictEqual( result.reasoning, 'Let me analyze the problem carefully.' ); const assistantMessage = result.messages[result.messages.length - 1]; const reasoningPart = assistantMessage.content.find( (part) => part.reasoning ); assert.ok(reasoningPart, 'Expected reasoning part in assistant message'); assert.strictEqual( reasoningPart?.custom?.anthropicThinking?.signature, 'sig_reasoning_123' ); }); it('should propagate API errors correctly', async () => { const apiError = new Error('API Error: 401 Unauthorized'); const mockClient = createMockAnthropicClient({ shouldError: apiError, }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); await assert.rejects( async () => { await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Hello', }); }, (error: Error) => { assert.strictEqual(error.message, 'API Error: 401 Unauthorized'); return true; } ); }); it('should respect abort signals for cancellation', async () => { // Note: Detailed abort signal handling is tested in converters_test.ts // This test verifies that errors (including abort errors) are properly propagated at the integration layer const mockClient = createMockAnthropicClient({ shouldError: new Error('AbortError'), }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); await assert.rejects( async () => { await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Hello', }); }, (error: Error) => { // Should propagate the error assert.ok( error.message.includes('AbortError'), 'Should propagate errors' ); return true; } ); }); it('should track token usage in responses', async () => { const mockClient = createMockAnthropicClient({ messageResponse: { usage: { input_tokens: 25, output_tokens: 50, cache_creation_input_tokens: 5, cache_read_input_tokens: 10, cache_creation: null, server_tool_use: null, service_tier: null, }, }, }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Hello', }); assert.ok(result.usage, 'Should have usage information'); assert.strictEqual(result.usage.inputTokens, 25); assert.strictEqual(result.usage.outputTokens, 50); }); it('should route requests through beta surface when plugin default is beta', async () => { const mockClient = createMockAnthropicClient(); const ai = genkit({ plugins: [ anthropic({ apiVersion: 'beta', [__testClient]: mockClient, } as PluginOptions), ], }); await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Hello', }); const betaCreateStub = mockClient.beta.messages.create as any; assert.strictEqual( betaCreateStub.mock.calls.length, 1, 'Beta API should be used' ); const regularCreateStub = mockClient.messages.create as any; assert.strictEqual( regularCreateStub.mock.calls.length, 0, 'Stable API should not be used' ); }); it('should stream thinking deltas as reasoning chunks', async () => { const thinkingConfig = { enabled: true, budgetTokens: 3072 }; const streamChunks = [ { type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '', signature: 'sig_stream_123', }, } as any, { type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'Analyzing intermediate steps.', }, } as any, { type: 'content_block_start', index: 1, content_block: { type: 'text', text: '', }, } as any, mockTextChunk('Final streamed response.'), ]; const finalMessage = mockMessageWithContent([ { type: 'thinking', thinking: 'Analyzing intermediate steps.', signature: 'sig_stream_123', }, { type: 'text', text: 'Final streamed response.', citations: null, }, ]); const mockClient = createMockAnthropicClient({ streamChunks, messageResponse: finalMessage, }); const ai = genkit({ plugins: [anthropic({ [__testClient]: mockClient } as PluginOptions)], }); const chunks: any[] = []; const result = await ai.generate({ model: 'anthropic/claude-3-5-haiku', prompt: 'Explain how you reason.', streamingCallback: (chunk) => chunks.push(chunk), config: { thinking: thinkingConfig }, }); const streamStub = mockClient.messages.stream as any; assert.strictEqual(streamStub.mock.calls.length, 1); const streamRequest = streamStub.mock.calls[0].arguments[0]; assert.deepStrictEqual(streamRequest.thinking, { type: 'enabled', budget_tokens: 3072, }); const hasReasoningChunk = chunks.some((chunk) => (chunk.content || []).some( (part: any) => part.reasoning === 'Analyzing intermediate steps.' ) ); assert.ok( hasReasoningChunk, 'Expected reasoning chunk in streaming callback' ); assert.strictEqual(result.reasoning, 'Analyzing intermediate steps.'); }); });

Latest Blog Posts

MCP directory API

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

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

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