Skip to main content
Glama
firebase
by firebase
streaming_test.ts10.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 type { ModelAction } from 'genkit/model'; import { describe, mock, test } from 'node:test'; import { anthropic } from '../src/index.js'; import { PluginOptions, __testClient } from '../src/types.js'; import { createMockAnthropicClient, createMockAnthropicMessage, mockContentBlockStart, mockTextChunk, mockToolUseChunk, } from './mocks/anthropic-client.js'; describe('Streaming Integration Tests', () => { test('should use streaming API when onChunk is provided', async () => { const mockClient = createMockAnthropicClient({ streamChunks: [ mockContentBlockStart('Hello'), mockTextChunk(' world'), mockTextChunk('!'), ], messageResponse: createMockAnthropicMessage({ text: 'Hello world!', }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; const response = await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: new AbortController().signal, } ); // Verify final response assert.ok(response, 'Response should be returned'); assert.ok( response.candidates?.[0]?.message.content[0].text, 'Response should have text content' ); // Since we can't control whether the runner chooses streaming or not from // the plugin level, just verify we got a response // The runner-level tests verify streaming behavior in detail }); test('should handle streaming with multiple content blocks', async () => { const mockClient = createMockAnthropicClient({ streamChunks: [ mockContentBlockStart('First block'), mockTextChunk(' continues'), { type: 'content_block_start', index: 1, content_block: { type: 'text', text: 'Second block', }, } as any, { type: 'content_block_delta', index: 1, delta: { type: 'text_delta', text: ' here', }, } as any, ], messageResponse: createMockAnthropicMessage({ text: 'First block continues', }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; const response = await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: new AbortController().signal, } ); // Verify response is returned even with multiple content blocks assert.ok(response, 'Response should be returned'); }); test('should handle streaming with tool use', async () => { const mockClient = createMockAnthropicClient({ streamChunks: [ mockToolUseChunk('toolu_123', 'get_weather', { city: 'NYC' }), ], messageResponse: createMockAnthropicMessage({ toolUse: { id: 'toolu_123', name: 'get_weather', input: { city: 'NYC' }, }, }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; const response = await modelAction( { messages: [{ role: 'user', content: [{ text: 'Get NYC weather' }] }], tools: [ { name: 'get_weather', description: 'Get weather for a city', inputSchema: { type: 'object', properties: { city: { type: 'string' }, }, required: ['city'], }, }, ], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: new AbortController().signal, } ); // Verify tool use in response assert.ok(response.candidates?.[0]?.message.content[0].toolRequest); assert.strictEqual( response.candidates[0].message.content[0].toolRequest?.name, 'get_weather' ); }); test('should handle abort signal', async () => { const abortController = new AbortController(); const mockClient = createMockAnthropicClient({ messageResponse: createMockAnthropicMessage({ text: 'Hello world', }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; // Abort before starting abortController.abort(); // Test that abort signal is passed through // The actual abort behavior is tested in runner tests try { await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: abortController.signal, } ); // If we get here, the mock doesn't fully simulate abort behavior, // which is fine since runner tests cover this } catch (error: any) { // Expected abort error assert.ok( error.message.includes('Abort') || error.name === 'AbortError', 'Should throw abort error' ); } }); test('should handle errors during streaming', async () => { const mockClient = createMockAnthropicClient({ shouldError: new Error('API error'), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; try { await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: new AbortController().signal, } ); assert.fail('Should have thrown an error'); } catch (error: any) { assert.strictEqual(error.message, 'API error'); } }); test('should handle empty response', async () => { const mockClient = createMockAnthropicClient({ streamChunks: [], messageResponse: createMockAnthropicMessage({ text: '', }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; const response = await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: new AbortController().signal, } ); assert.ok(response, 'Should return response even with empty content'); }); test('should include usage metadata in streaming response', async () => { const mockClient = createMockAnthropicClient({ streamChunks: [mockContentBlockStart('Response'), mockTextChunk(' text')], messageResponse: createMockAnthropicMessage({ text: 'Response text', usage: { input_tokens: 50, output_tokens: 25, }, }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; const response = await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], output: { format: 'text' }, }, { onChunk: mock.fn() as any, abortSignal: new AbortController().signal, } ); assert.ok(response.usage, 'Should include usage metadata'); assert.strictEqual(response.usage?.inputTokens, 50); assert.strictEqual(response.usage?.outputTokens, 25); }); test('should not stream when onChunk is not provided', async () => { const mockClient = createMockAnthropicClient({ messageResponse: createMockAnthropicMessage({ text: 'Non-streaming response', }), }); const plugin = anthropic({ apiKey: 'test-key', [__testClient]: mockClient, } as PluginOptions); const modelAction = plugin.resolve!( 'model', 'claude-3-5-haiku-20241022' ) as ModelAction; await modelAction( { messages: [{ role: 'user', content: [{ text: 'Hello' }] }], }, { abortSignal: new AbortController().signal, } ); // Verify non-streaming API was called const createStub = mockClient.messages.create as any; assert.strictEqual(createStub.mock.calls.length, 1); // Verify stream API was NOT called const streamStub = mockClient.messages.stream as any; assert.strictEqual(streamStub.mock.calls.length, 0); }); });

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