0da5ec09e8
* feat: 添加gemini协议适配 * Remove unrelated local files from Gemini PR
176 lines
4.5 KiB
TypeScript
176 lines
4.5 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { adaptGeminiStreamToAnthropic } from '../streamAdapter.js'
|
|
import type { GeminiStreamChunk } from '../types.js'
|
|
|
|
function mockStream(
|
|
chunks: GeminiStreamChunk[],
|
|
): AsyncIterable<GeminiStreamChunk> {
|
|
return {
|
|
[Symbol.asyncIterator]() {
|
|
let index = 0
|
|
return {
|
|
async next() {
|
|
if (index >= chunks.length) {
|
|
return { done: true, value: undefined }
|
|
}
|
|
return { done: false, value: chunks[index++] }
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
async function collectEvents(chunks: GeminiStreamChunk[]) {
|
|
const events: any[] = []
|
|
for await (const event of adaptGeminiStreamToAnthropic(
|
|
mockStream(chunks),
|
|
'gemini-2.5-flash',
|
|
)) {
|
|
events.push(event)
|
|
}
|
|
return events
|
|
}
|
|
|
|
describe('adaptGeminiStreamToAnthropic', () => {
|
|
test('converts text chunks', async () => {
|
|
const events = await collectEvents([
|
|
{
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [{ text: 'Hello' }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [{ text: ' world' }],
|
|
},
|
|
finishReason: 'STOP',
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
const textDeltas = events.filter(
|
|
event =>
|
|
event.type === 'content_block_delta' && event.delta.type === 'text_delta',
|
|
)
|
|
|
|
expect(events[0].type).toBe('message_start')
|
|
expect(textDeltas).toHaveLength(2)
|
|
expect(textDeltas[0].delta.text).toBe('Hello')
|
|
expect(textDeltas[1].delta.text).toBe(' world')
|
|
|
|
const messageDelta = events.find(event => event.type === 'message_delta')
|
|
expect(messageDelta.delta.stop_reason).toBe('end_turn')
|
|
})
|
|
|
|
test('converts thinking chunks and signatures', async () => {
|
|
const events = await collectEvents([
|
|
{
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [{ text: 'Think', thought: true }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [{ thought: true, thoughtSignature: 'sig-123' }],
|
|
},
|
|
finishReason: 'STOP',
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
const blockStart = events.find(event => event.type === 'content_block_start')
|
|
expect(blockStart.content_block.type).toBe('thinking')
|
|
|
|
const signatureDelta = events.find(
|
|
event =>
|
|
event.type === 'content_block_delta' &&
|
|
event.delta.type === 'signature_delta',
|
|
)
|
|
expect(signatureDelta.delta.signature).toBe('sig-123')
|
|
})
|
|
|
|
test('converts function calls to tool_use blocks', async () => {
|
|
const events = await collectEvents([
|
|
{
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [
|
|
{
|
|
functionCall: {
|
|
name: 'bash',
|
|
args: { command: 'ls' },
|
|
},
|
|
thoughtSignature: 'sig-tool',
|
|
},
|
|
],
|
|
},
|
|
finishReason: 'STOP',
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
const blockStart = events.find(event => event.type === 'content_block_start')
|
|
expect(blockStart.content_block.type).toBe('tool_use')
|
|
expect(blockStart.content_block.name).toBe('bash')
|
|
|
|
const signatureDelta = events.find(
|
|
event =>
|
|
event.type === 'content_block_delta' &&
|
|
event.delta.type === 'signature_delta',
|
|
)
|
|
expect(signatureDelta.delta.signature).toBe('sig-tool')
|
|
|
|
const inputDelta = events.find(
|
|
event =>
|
|
event.type === 'content_block_delta' &&
|
|
event.delta.type === 'input_json_delta',
|
|
)
|
|
expect(inputDelta.delta.partial_json).toBe('{"command":"ls"}')
|
|
|
|
const messageDelta = events.find(event => event.type === 'message_delta')
|
|
expect(messageDelta.delta.stop_reason).toBe('tool_use')
|
|
})
|
|
|
|
test('maps usage metadata into output tokens', async () => {
|
|
const events = await collectEvents([
|
|
{
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [{ text: 'Hello' }],
|
|
},
|
|
finishReason: 'STOP',
|
|
},
|
|
],
|
|
usageMetadata: {
|
|
promptTokenCount: 10,
|
|
candidatesTokenCount: 5,
|
|
thoughtsTokenCount: 2,
|
|
},
|
|
},
|
|
])
|
|
|
|
const messageStart = events.find(event => event.type === 'message_start')
|
|
expect(messageStart.message.usage.input_tokens).toBe(10)
|
|
|
|
const messageDelta = events.find(event => event.type === 'message_delta')
|
|
expect(messageDelta.usage.output_tokens).toBe(7)
|
|
})
|
|
})
|