style: 格式化 packages/@ant/ 下所有文件以通过 biome ci

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-05-01 21:55:51 +08:00
parent c32f26cf21
commit 9ea9859dce
92 changed files with 5903 additions and 5188 deletions

View File

@@ -29,7 +29,10 @@ export { resolveGeminiModel } from './providers/gemini/modelMapping.js'
// Gemini provider utilities
export { anthropicMessagesToGemini } from './providers/gemini/convertMessages.js'
export { anthropicToolsToGemini, anthropicToolChoiceToGemini } from './providers/gemini/convertTools.js'
export {
anthropicToolsToGemini,
anthropicToolChoiceToGemini,
} from './providers/gemini/convertTools.js'
export { adaptGeminiStreamToAnthropic } from './providers/gemini/streamAdapter.js'
export {
GEMINI_THOUGHT_SIGNATURE_FIELD,
@@ -59,5 +62,8 @@ export {
// Shared OpenAI conversion utilities
export { anthropicMessagesToOpenAI } from './shared/openaiConvertMessages.js'
export type { ConvertMessagesOptions } from './shared/openaiConvertMessages.js'
export { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './shared/openaiConvertTools.js'
export {
anthropicToolsToOpenAI,
anthropicToolChoiceToOpenAI,
} from './shared/openaiConvertTools.js'
export { adaptOpenAIStreamToAnthropic } from './shared/openaiStreamAdapter.js'

View File

@@ -1,8 +1,5 @@
import { describe, expect, test } from 'bun:test'
import type {
AssistantMessage,
UserMessage,
} from '../../../types/message.js'
import type { AssistantMessage, UserMessage } from '../../../types/message.js'
import { anthropicMessagesToGemini } from '../convertMessages.js'
function makeUserMsg(content: string | any[]): UserMessage {
@@ -23,10 +20,9 @@ function makeAssistantMsg(content: string | any[]): AssistantMessage {
describe('anthropicMessagesToGemini', () => {
test('converts system prompt to systemInstruction', () => {
const result = anthropicMessagesToGemini(
[makeUserMsg('hello')],
['You are helpful.'] as any,
)
const result = anthropicMessagesToGemini([makeUserMsg('hello')], [
'You are helpful.',
] as any)
expect(result.systemInstruction).toEqual({
parts: [{ text: 'You are helpful.' }],
@@ -202,17 +198,19 @@ describe('anthropicMessagesToGemini', () => {
test('converts base64 image to inlineData', () => {
const result = anthropicMessagesToGemini(
[makeUserMsg([
{ type: 'text', text: 'describe this' },
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: 'iVBORw0KGgo=',
[
makeUserMsg([
{ type: 'text', text: 'describe this' },
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: 'iVBORw0KGgo=',
},
},
},
])],
]),
],
[] as any,
)
expect(result.contents).toEqual([
@@ -228,15 +226,17 @@ describe('anthropicMessagesToGemini', () => {
test('converts url image to text fallback', () => {
const result = anthropicMessagesToGemini(
[makeUserMsg([
{
type: 'image',
source: {
type: 'url',
url: 'https://example.com/img.png',
[
makeUserMsg([
{
type: 'image',
source: {
type: 'url',
url: 'https://example.com/img.png',
},
},
},
])],
]),
],
[] as any,
)
expect(result.contents).toEqual([
@@ -249,15 +249,17 @@ describe('anthropicMessagesToGemini', () => {
test('defaults to image/png when media_type is missing', () => {
const result = anthropicMessagesToGemini(
[makeUserMsg([
{
type: 'image',
source: {
type: 'base64',
data: 'ABC123',
[
makeUserMsg([
{
type: 'image',
source: {
type: 'base64',
data: 'ABC123',
},
},
},
])],
]),
],
[] as any,
)
expect(result.contents[0].parts[0]).toEqual({

View File

@@ -120,11 +120,11 @@ describe('anthropicToolChoiceToGemini', () => {
})
test('maps explicit tool choice', () => {
expect(
anthropicToolChoiceToGemini({ type: 'tool', name: 'bash' }),
).toEqual({
mode: 'ANY',
allowedFunctionNames: ['bash'],
})
expect(anthropicToolChoiceToGemini({ type: 'tool', name: 'bash' })).toEqual(
{
mode: 'ANY',
allowedFunctionNames: ['bash'],
},
)
})
})

View File

@@ -57,7 +57,8 @@ describe('adaptGeminiStreamToAnthropic', () => {
const textDeltas = events.filter(
event =>
event.type === 'content_block_delta' && event.delta.type === 'text_delta',
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta',
)
expect(events[0].type).toBe('message_start')
@@ -92,7 +93,9 @@ describe('adaptGeminiStreamToAnthropic', () => {
},
])
const blockStart = events.find(event => event.type === 'content_block_start')
const blockStart = events.find(
event => event.type === 'content_block_start',
)
expect(blockStart.content_block.type).toBe('thinking')
const signatureDelta = events.find(
@@ -125,7 +128,9 @@ describe('adaptGeminiStreamToAnthropic', () => {
},
])
const blockStart = events.find(event => event.type === 'content_block_start')
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')

View File

@@ -93,7 +93,10 @@ function convertInternalUserMessage(
return {
role: 'user',
parts: content.flatMap(block =>
convertUserContentBlockToGeminiParts(block as unknown as string | Record<string, unknown>, toolNamesById),
convertUserContentBlockToGeminiParts(
block as unknown as string | Record<string, unknown>,
toolNamesById,
),
),
}
}
@@ -115,7 +118,8 @@ function convertUserContentBlockToGeminiParts(
return [
{
functionResponse: {
name: toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
name:
toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
response: toolResultToResponseObject(toolResult),
},
},
@@ -170,7 +174,9 @@ function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent {
parts.push(
...createTextGeminiParts(
block.text,
getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
getGeminiThoughtSignature(
block as unknown as Record<string, unknown>,
),
),
)
continue
@@ -194,8 +200,12 @@ function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent {
name: toolUse.name,
args: normalizeToolUseInput(toolUse.input),
},
...(getGeminiThoughtSignature(block as unknown as Record<string, unknown>) && {
thoughtSignature: getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
...(getGeminiThoughtSignature(
block as unknown as Record<string, unknown>,
) && {
thoughtSignature: getGeminiThoughtSignature(
block as unknown as Record<string, unknown>,
),
}),
})
}
@@ -255,12 +265,10 @@ function toolResultToResponseObject(
block: BetaToolResultBlockParam,
): Record<string, unknown> {
const result = normalizeToolResultContent(block.content)
if (
result &&
typeof result === 'object' &&
!Array.isArray(result)
) {
return block.is_error ? { ...(result as Record<string, unknown>), is_error: true } : result as Record<string, unknown>
if (result && typeof result === 'object' && !Array.isArray(result)) {
return block.is_error
? { ...(result as Record<string, unknown>), is_error: true }
: (result as Record<string, unknown>)
}
return {
@@ -299,7 +307,9 @@ function normalizeToolResultContent(content: unknown): unknown {
return content ?? ''
}
function getGeminiThoughtSignature(block: Record<string, unknown>): string | undefined {
function getGeminiThoughtSignature(
block: Record<string, unknown>,
): string | undefined {
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
return typeof signature === 'string' && signature.length > 0
? signature

View File

@@ -1,8 +1,5 @@
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type {
GeminiFunctionCallingConfig,
GeminiTool,
} from './types.js'
import type { GeminiFunctionCallingConfig, GeminiTool } from './types.js'
const GEMINI_JSON_SCHEMA_TYPES = new Set([
'string',
@@ -34,7 +31,9 @@ function normalizeGeminiJsonSchemaType(
return undefined
}
function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined {
function inferGeminiJsonSchemaTypeFromValue(
value: unknown,
): string | undefined {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
if (typeof value === 'string') return 'string'
@@ -97,9 +96,7 @@ function sanitizeGeminiJsonSchemaArray(
return sanitized.length > 0 ? sanitized : undefined
}
function sanitizeGeminiJsonSchema(
schema: unknown,
): Record<string, unknown> {
function sanitizeGeminiJsonSchema(schema: unknown): Record<string, unknown> {
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
return {}
}
@@ -236,17 +233,20 @@ export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] {
const functionDeclarations = tools
.filter(tool => {
const toolType = (tool as unknown as { type?: string }).type
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
return (
tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
)
})
.map(tool => {
const anyTool = tool as unknown as Record<string, unknown>
const name = (anyTool.name as string) || ''
const description = (anyTool.description as string) || ''
const inputSchema =
(anyTool.input_schema as Record<string, unknown> | undefined) ?? {
type: 'object',
properties: {},
}
const inputSchema = (anyTool.input_schema as
| Record<string, unknown>
| undefined) ?? {
type: 'object',
properties: {},
}
return {
name,
@@ -255,9 +255,7 @@ export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] {
}
})
return functionDeclarations.length > 0
? [{ functionDeclarations }]
: []
return functionDeclarations.length > 0 ? [{ functionDeclarations }] : []
}
export function anthropicToolChoiceToGemini(

View File

@@ -10,9 +10,8 @@ export async function* adaptGeminiStreamToAnthropic(
let started = false
let stopped = false
let nextContentIndex = 0
let openTextLikeBlock:
| { index: number; type: 'text' | 'thinking' }
| null = null
let openTextLikeBlock: { index: number; type: 'text' | 'thinking' } | null =
null
let sawToolUse = false
let finishReason: string | undefined
let inputTokens = 0
@@ -85,7 +84,10 @@ export async function* adaptGeminiStreamToAnthropic(
} as BetaRawMessageStreamEvent
}
if (part.functionCall.args && Object.keys(part.functionCall.args).length > 0) {
if (
part.functionCall.args &&
Object.keys(part.functionCall.args).length > 0
) {
yield {
type: 'content_block_delta',
index: toolIndex,
@@ -213,9 +215,7 @@ export async function* adaptGeminiStreamToAnthropic(
}
}
function getTextLikeBlockType(
part: GeminiPart,
): 'text' | 'thinking' | null {
function getTextLikeBlockType(part: GeminiPart): 'text' | 'thinking' | null {
if (typeof part.text !== 'string') {
return null
}

View File

@@ -33,11 +33,14 @@ describe('resolveGrokModel', () => {
})
test('maps haiku models to grok-3-mini-fast', () => {
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe('grok-3-mini-fast')
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe(
'grok-3-mini-fast',
)
})
test('GROK_MODEL_MAP overrides family mapping', () => {
process.env.GROK_MODEL_MAP = '{"opus":"grok-4","sonnet":"grok-3","haiku":"grok-mini"}'
process.env.GROK_MODEL_MAP =
'{"opus":"grok-4","sonnet":"grok-3","haiku":"grok-mini"}'
expect(resolveGrokModel('claude-opus-4-6')).toBe('grok-4')
expect(resolveGrokModel('claude-sonnet-4-6')).toBe('grok-3')
expect(resolveGrokModel('claude-haiku-4-5-20251001')).toBe('grok-mini')
@@ -62,6 +65,8 @@ describe('resolveGrokModel', () => {
})
test('falls back to family default for unlisted model', () => {
expect(resolveGrokModel('claude-opus-99-20300101')).toBe('grok-4.20-reasoning')
expect(resolveGrokModel('claude-opus-99-20300101')).toBe(
'grok-4.20-reasoning',
)
})
})

View File

@@ -131,7 +131,13 @@ describe('anthropicMessagesToOpenAI', () => {
],
[] as any,
)
expect(result).toEqual([{ role: 'assistant', content: 'visible response', reasoning_content: 'internal thoughts...' }] as any)
expect(result).toEqual([
{
role: 'assistant',
content: 'visible response',
reasoning_content: 'internal thoughts...',
},
] as any)
})
test('handles full conversation with tools', () => {
@@ -487,10 +493,19 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
[
makeUserMsg('run ls'),
makeAssistantMsg([
{ type: 'tool_use' as const, id: 'toolu_1', name: 'bash', input: { command: 'ls' } },
{
type: 'tool_use' as const,
id: 'toolu_1',
name: 'bash',
input: { command: 'ls' },
},
]),
makeUserMsg([
{ type: 'tool_result' as const, tool_use_id: 'toolu_1', content: 'file.txt' },
{
type: 'tool_result' as const,
tool_use_id: 'toolu_1',
content: 'file.txt',
},
{ type: 'text' as const, text: 'looks good' },
]),
],
@@ -499,7 +514,10 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
// Find the tool message and the user text message
const toolIdx = result.findIndex(m => m.role === 'tool')
const userTextIdx = result.findIndex(
m => m.role === 'user' && typeof m.content === 'string' && m.content.includes('looks good'),
m =>
m.role === 'user' &&
typeof m.content === 'string' &&
m.content.includes('looks good'),
)
expect(toolIdx).toBeGreaterThanOrEqual(0)
expect(userTextIdx).toBeGreaterThanOrEqual(0)
@@ -512,15 +530,26 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
[
makeUserMsg('do something'),
makeAssistantMsg([
{ type: 'tool_use' as const, id: 'toolu_2', name: 'bash', input: { command: 'pwd' } },
{
type: 'tool_use' as const,
id: 'toolu_2',
name: 'bash',
input: { command: 'pwd' },
},
]),
makeUserMsg([
{ type: 'tool_result' as const, tool_use_id: 'toolu_2', content: '/home/user' },
{
type: 'tool_result' as const,
tool_use_id: 'toolu_2',
content: '/home/user',
},
]),
],
[] as any,
)
const assistantIdx = result.findIndex(m => m.role === 'assistant' && (m as any).tool_calls)
const assistantIdx = result.findIndex(
m => m.role === 'assistant' && (m as any).tool_calls,
)
const toolIdx = result.findIndex(m => m.role === 'tool')
expect(assistantIdx).toBeGreaterThanOrEqual(0)
expect(toolIdx).toBe(assistantIdx + 1)

View File

@@ -1,5 +1,8 @@
import { describe, expect, test } from 'bun:test'
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openaiConvertTools.js'
import {
anthropicToolsToOpenAI,
anthropicToolChoiceToOpenAI,
} from '../openaiConvertTools.js'
describe('anthropicToolsToOpenAI', () => {
test('converts basic tool', () => {

View File

@@ -3,7 +3,9 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions/comp
import { adaptOpenAIStreamToAnthropic } from '../openaiStreamAdapter.js'
/** Helper to create a mock async iterable from chunk array */
function mockStream(chunks: ChatCompletionChunk[]): AsyncIterable<ChatCompletionChunk> {
function mockStream(
chunks: ChatCompletionChunk[],
): AsyncIterable<ChatCompletionChunk> {
return {
[Symbol.asyncIterator]() {
let i = 0
@@ -18,7 +20,9 @@ function mockStream(chunks: ChatCompletionChunk[]): AsyncIterable<ChatCompletion
}
/** Create a minimal ChatCompletionChunk */
function makeChunk(overrides: Partial<ChatCompletionChunk> & any = {}): ChatCompletionChunk {
function makeChunk(
overrides: Partial<ChatCompletionChunk> & any = {},
): ChatCompletionChunk {
return {
id: 'chatcmpl-test',
object: 'chat.completion.chunk',
@@ -32,7 +36,10 @@ function makeChunk(overrides: Partial<ChatCompletionChunk> & any = {}): ChatComp
/** Collect all emitted Anthropic events from the stream adapter for assertion */
async function collectEvents(chunks: ChatCompletionChunk[]) {
const events: any[] = []
for await (const event of adaptOpenAIStreamToAnthropic(mockStream(chunks), 'gpt-4o')) {
for await (const event of adaptOpenAIStreamToAnthropic(
mockStream(chunks),
'gpt-4o',
)) {
events.push(event)
}
return events
@@ -42,25 +49,31 @@ describe('adaptOpenAIStreamToAnthropic', () => {
test('emits message_start on first chunk', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: { role: 'assistant', content: '' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { role: 'assistant', content: '' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{
index: 0,
delta: { content: 'hello' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { content: 'hello' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{
index: 0,
delta: {},
finish_reason: 'stop',
}],
choices: [
{
index: 0,
delta: {},
finish_reason: 'stop',
},
],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}),
])
@@ -73,10 +86,14 @@ describe('adaptOpenAIStreamToAnthropic', () => {
test('converts text content stream', async () => {
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'Hello' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'Hello' }, finish_reason: null },
],
}),
makeChunk({
choices: [{ index: 0, delta: { content: ' world' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: ' world' }, finish_reason: null },
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
@@ -91,7 +108,9 @@ describe('adaptOpenAIStreamToAnthropic', () => {
expect(types).toContain('message_delta')
expect(types).toContain('message_stop')
const textDeltas = events.filter(e => e.type === 'content_block_delta') as any[]
const textDeltas = events.filter(
e => e.type === 'content_block_delta',
) as any[]
expect(textDeltas[0].delta.text).toBe('Hello')
expect(textDeltas[1].delta.text).toBe(' world')
})
@@ -99,42 +118,54 @@ describe('adaptOpenAIStreamToAnthropic', () => {
test('converts tool_calls stream', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{
index: 0,
id: 'call_abc',
type: 'function',
function: { name: 'bash', arguments: '' },
}],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_abc',
type: 'function',
function: { name: 'bash', arguments: '' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{
index: 0,
function: { arguments: '{"comm' },
}],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
function: { arguments: '{"comm' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{
index: 0,
function: { arguments: 'and":"ls"}' },
}],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
function: { arguments: 'and":"ls"}' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
@@ -146,7 +177,8 @@ describe('adaptOpenAIStreamToAnthropic', () => {
expect(blockStart.content_block.name).toBe('bash')
const jsonDeltas = events.filter(
e => e.type === 'content_block_delta' && e.delta.type === 'input_json_delta',
e =>
e.type === 'content_block_delta' && e.delta.type === 'input_json_delta',
) as any[]
const fullArgs = jsonDeltas.map(d => d.delta.partial_json).join('')
expect(fullArgs).toBe('{"command":"ls"}')
@@ -171,13 +203,21 @@ describe('adaptOpenAIStreamToAnthropic', () => {
// return finish_reason "stop" when they actually made tool calls.
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"cmd":"ls"}' } }],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_1',
function: { name: 'bash', arguments: '{"cmd":"ls"}' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
@@ -191,13 +231,21 @@ describe('adaptOpenAIStreamToAnthropic', () => {
test('maps finish_reason tool_calls to tool_use', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{}' } }],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_1',
function: { name: 'bash', arguments: '{}' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
@@ -211,7 +259,9 @@ describe('adaptOpenAIStreamToAnthropic', () => {
test('maps finish_reason length to max_tokens', async () => {
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'truncated' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'truncated' }, finish_reason: null },
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'length' }],
@@ -225,23 +275,35 @@ describe('adaptOpenAIStreamToAnthropic', () => {
test('handles mixed text and tool_calls', async () => {
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'Thinking...' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'Thinking...' }, finish_reason: null },
],
}),
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'grep', arguments: '{"p":"test"}' } }],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_1',
function: { name: 'grep', arguments: '{"p":"test"}' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
}),
])
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
const blockStarts = events.filter(
e => e.type === 'content_block_start',
) as any[]
expect(blockStarts.length).toBe(2)
expect(blockStarts[0].content_block.type).toBe('text')
expect(blockStarts[1].content_block.type).toBe('tool_use')
@@ -252,18 +314,22 @@ describe('thinking support (reasoning_content)', () => {
test('converts reasoning_content to thinking block', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: { reasoning_content: 'Let me analyze this...' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { reasoning_content: 'Let me analyze this...' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{
index: 0,
delta: { reasoning_content: ' step by step.' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { reasoning_content: ' step by step.' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
@@ -277,7 +343,8 @@ describe('thinking support (reasoning_content)', () => {
// Should have thinking_delta events
const thinkingDeltas = events.filter(
e => e.type === 'content_block_delta' && e.delta.type === 'thinking_delta',
e =>
e.type === 'content_block_delta' && e.delta.type === 'thinking_delta',
) as any[]
expect(thinkingDeltas.length).toBe(2)
expect(thinkingDeltas[0].delta.thinking).toBe('Let me analyze this...')
@@ -287,18 +354,22 @@ describe('thinking support (reasoning_content)', () => {
test('converts reasoning then content (DeepSeek-style)', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: { reasoning_content: 'Thinking about the answer...' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { reasoning_content: 'Thinking about the answer...' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{
index: 0,
delta: { content: 'Here is my answer.' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { content: 'Here is my answer.' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
@@ -306,13 +377,17 @@ describe('thinking support (reasoning_content)', () => {
])
// Should have two content blocks: thinking + text
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
const blockStarts = events.filter(
e => e.type === 'content_block_start',
) as any[]
expect(blockStarts.length).toBe(2)
expect(blockStarts[0].content_block.type).toBe('thinking')
expect(blockStarts[1].content_block.type).toBe('text')
// Thinking block should be closed before text block starts
const blockStops = events.filter(e => e.type === 'content_block_stop') as any[]
const blockStops = events.filter(
e => e.type === 'content_block_stop',
) as any[]
expect(blockStops[0].index).toBe(0) // thinking block closed at index 0
expect(blockStarts[1].index).toBe(1) // text block starts at index 1
@@ -326,27 +401,39 @@ describe('thinking support (reasoning_content)', () => {
test('handles reasoning then tool_calls', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: { reasoning_content: 'I need to run a command.' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { reasoning_content: 'I need to run a command.' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"c":"ls"}' } }],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_1',
function: { name: 'bash', arguments: '{"c":"ls"}' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
}),
])
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
const blockStarts = events.filter(
e => e.type === 'content_block_start',
) as any[]
expect(blockStarts.length).toBe(2)
expect(blockStarts[0].content_block.type).toBe('thinking')
expect(blockStarts[1].content_block.type).toBe('tool_use')
@@ -355,25 +442,31 @@ describe('thinking support (reasoning_content)', () => {
test('thinking block index is 0, text block index is 1', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: { reasoning_content: 'reason' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { reasoning_content: 'reason' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{
index: 0,
delta: { content: 'answer' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { content: 'answer' },
finish_reason: null,
},
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
}),
])
const blockStarts = events.filter(e => e.type === 'content_block_start') as any[]
const blockStarts = events.filter(
e => e.type === 'content_block_start',
) as any[]
expect(blockStarts[0].index).toBe(0)
expect(blockStarts[1].index).toBe(1)
})
@@ -383,11 +476,13 @@ describe('prompt caching support', () => {
test('maps cached_tokens to cache_read_input_tokens', async () => {
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: { content: 'hi' },
finish_reason: null,
}],
choices: [
{
index: 0,
delta: { content: 'hi' },
finish_reason: null,
},
],
usage: {
prompt_tokens: 1000,
completion_tokens: 0,
@@ -463,7 +558,9 @@ describe('prompt caching support', () => {
// emitted before the trailing chunk and always has input_tokens=0.
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'hello' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'hello' }, finish_reason: null },
],
}),
// finish_reason chunk — usage not yet available
makeChunk({
@@ -493,14 +590,20 @@ describe('prompt caching support', () => {
// the autocompact threshold (~33k), so compaction never fires.
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'answer' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'answer' }, finish_reason: null },
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
}),
makeChunk({
choices: [],
usage: { prompt_tokens: 800, completion_tokens: 200, total_tokens: 1000 },
usage: {
prompt_tokens: 800,
completion_tokens: 200,
total_tokens: 1000,
},
}),
])
@@ -514,13 +617,21 @@ describe('prompt caching support', () => {
// when the model made tool calls and usage arrives in a trailing chunk.
const events = await collectEvents([
makeChunk({
choices: [{
index: 0,
delta: {
tool_calls: [{ index: 0, id: 'call_x', function: { name: 'bash', arguments: '{"cmd":"ls"}' } }],
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_x',
function: { name: 'bash', arguments: '{"cmd":"ls"}' },
},
],
},
finish_reason: null,
},
finish_reason: null,
}],
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
@@ -540,9 +651,14 @@ describe('prompt caching support', () => {
test('message_delta always comes before message_stop', async () => {
// Verifies event ordering is preserved after deferring to post-loop emission.
const events = await collectEvents([
makeChunk({ choices: [{ index: 0, delta: { content: 'x' }, finish_reason: null }] }),
makeChunk({
choices: [{ index: 0, delta: { content: 'x' }, finish_reason: null }],
}),
makeChunk({ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] }),
makeChunk({ choices: [], usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } }),
makeChunk({
choices: [],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}),
])
const types = events.map(e => e.type)
@@ -561,7 +677,9 @@ describe('prompt caching support', () => {
// queryModelOpenAI's spread — even though cachedTokens was captured internally.
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'answer' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'answer' }, finish_reason: null },
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
@@ -638,7 +756,9 @@ describe('prompt caching support', () => {
// Some endpoints send usage in the finish_reason chunk instead of a trailing chunk.
const events = await collectEvents([
makeChunk({
choices: [{ index: 0, delta: { content: 'result' }, finish_reason: null }],
choices: [
{ index: 0, delta: { content: 'result' }, finish_reason: null },
],
}),
makeChunk({
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],

View File

@@ -13,7 +13,14 @@ import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messag
* Individual message subtypes (UserMessage, AssistantMessage, etc.) extend
* this with narrower `type` literals and additional fields.
*/
export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress' | 'grouped_tool_use' | 'collapsed_read_search'
export type MessageType =
| 'user'
| 'assistant'
| 'system'
| 'attachment'
| 'progress'
| 'grouped_tool_use'
| 'collapsed_read_search'
/** A single content element inside message.content arrays. */
export type ContentItem = ContentBlockParam | ContentBlock
@@ -34,7 +41,14 @@ export type Message = {
isCompactSummary?: boolean
toolUseResult?: unknown
isVisibleInTranscriptOnly?: boolean
attachment?: { type: string; toolUseID?: string; [key: string]: unknown; addedNames: string[]; addedLines: string[]; removedNames: string[] }
attachment?: {
type: string
toolUseID?: string
[key: string]: unknown
addedNames: string[]
addedLines: string[]
removedNames: string[]
}
message?: {
role?: string
id?: string
@@ -49,8 +63,12 @@ export type AssistantMessage = Message & {
type: 'assistant'
message: NonNullable<Message['message']>
}
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> = Message & { type: 'attachment'; attachment: T }
export type ProgressMessage<T = unknown> = Message & { type: 'progress'; data: T }
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> =
Message & { type: 'attachment'; attachment: T }
export type ProgressMessage<T = unknown> = Message & {
type: 'progress'
data: T
}
export type SystemLocalCommandMessage = Message & { type: 'system' }
export type SystemMessage = Message & { type: 'system' }
export type UserMessage = Message & {