首次转发成功
This commit is contained in:
742
src/direct-conversion.test.ts
Normal file
742
src/direct-conversion.test.ts
Normal file
@@ -0,0 +1,742 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
buildUpstreamMessagesFromAnthropic,
|
||||
buildUpstreamMessagesFromChat,
|
||||
buildUpstreamMessagesFromResponses,
|
||||
buildUpstreamOptionsFromAnthropic,
|
||||
buildUpstreamOptionsFromChat,
|
||||
buildUpstreamOptionsFromResponses,
|
||||
} from './direct-request-builders';
|
||||
|
||||
import {
|
||||
applyCanonicalEvent,
|
||||
createCanonicalState,
|
||||
parseUpstreamChunk,
|
||||
type CanonicalEvent,
|
||||
} from './direct-canonical';
|
||||
import {
|
||||
buildChatCompletionResponse,
|
||||
collectChatCompletionStreamFrames,
|
||||
} from './direct-adapter-chat';
|
||||
import {
|
||||
buildResponsesResponse,
|
||||
collectResponsesStreamEvents,
|
||||
} from './direct-adapter-responses';
|
||||
import {
|
||||
buildAnthropicMessageResponse,
|
||||
collectAnthropicStreamEvents,
|
||||
} from './direct-adapter-messages';
|
||||
import { loadModels } from './direct-config';
|
||||
import type { DirectTextBlock } from './direct-types';
|
||||
|
||||
const cliContextBlocks: DirectTextBlock[] = [
|
||||
{ type: 'text', text: '<system-reminder>ctx</system-reminder>' },
|
||||
{ type: 'text', text: '<cwd>/workspace</cwd>' },
|
||||
];
|
||||
|
||||
function builderContext(systemPromptMode: 'original' | 'passthrough' | 'hybrid' = 'original') {
|
||||
return {
|
||||
systemPrompt: 'captured system prompt',
|
||||
systemPromptMode,
|
||||
cliUserContextBlocks: cliContextBlocks,
|
||||
};
|
||||
}
|
||||
|
||||
function accumulate(events: CanonicalEvent[]) {
|
||||
const state = createCanonicalState({
|
||||
id: 'resp_1',
|
||||
model: 'minimax-m2.7',
|
||||
created: 123,
|
||||
});
|
||||
|
||||
for (const event of events) {
|
||||
applyCanonicalEvent(state, event);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
test('buildUpstreamMessagesFromChat uses original system prompt by default and wraps final user query', () => {
|
||||
const messages = buildUpstreamMessagesFromChat(
|
||||
{
|
||||
messages: [
|
||||
{ role: 'system', content: 'request system' },
|
||||
{ role: 'assistant', content: 'older answer' },
|
||||
{ role: 'user', content: 'hello world' },
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'captured system prompt');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
|
||||
assert.equal(messages[1]?.role, 'assistant');
|
||||
assert.equal(messages[2]?.role, 'user');
|
||||
assert.equal(messages[2]?.agent, 'cli');
|
||||
assert.ok(Array.isArray(messages[2]?.content));
|
||||
assert.deepEqual(messages[2]?.content, [
|
||||
...cliContextBlocks,
|
||||
{ type: 'text', text: '<user_query>hello world</user_query>' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromChat can passthrough request system prompt instead of original', () => {
|
||||
const messages = buildUpstreamMessagesFromChat(
|
||||
{
|
||||
messages: [
|
||||
{ role: 'system', content: 'request system' },
|
||||
{ role: 'user', content: 'hello world' },
|
||||
],
|
||||
},
|
||||
builderContext('passthrough'),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'request system');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
|
||||
assert.equal(messages[1]?.role, 'user');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromChat can send original then passthrough system prompts in hybrid mode', () => {
|
||||
const messages = buildUpstreamMessagesFromChat(
|
||||
{
|
||||
messages: [
|
||||
{ role: 'system', content: 'request system' },
|
||||
{ role: 'user', content: 'hello world' },
|
||||
],
|
||||
},
|
||||
builderContext('hybrid'),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'captured system prompt');
|
||||
assert.equal(messages[1]?.role, 'system');
|
||||
assert.equal(messages[1]?.content, 'request system');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 2);
|
||||
assert.equal(messages[2]?.role, 'user');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromChat preserves assistant tool calls and tool results', () => {
|
||||
const messages = buildUpstreamMessagesFromChat(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'lookup', arguments: '{"q":"x"}' },
|
||||
}],
|
||||
},
|
||||
{ role: 'tool', tool_call_id: 'call_1', content: 'result' },
|
||||
{ role: 'user', content: 'continue' },
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[1]?.role, 'assistant');
|
||||
assert.deepEqual(messages[1]?.tool_calls, [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'lookup', arguments: '{"q":"x"}' },
|
||||
}]);
|
||||
assert.equal(messages[2]?.role, 'tool');
|
||||
assert.equal(messages[2]?.tool_call_id, 'call_1');
|
||||
assert.equal(messages[2]?.content, 'result');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromResponses uses original system prompt by default', () => {
|
||||
const messages = buildUpstreamMessagesFromResponses(
|
||||
{
|
||||
instructions: 'be concise',
|
||||
input: [
|
||||
{
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [{ type: 'input_text', text: 'say hi' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'captured system prompt');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
|
||||
assert.equal(messages[1]?.role, 'user');
|
||||
assert.equal(messages[1]?.agent, 'cli');
|
||||
assert.deepEqual(messages[1]?.content, [
|
||||
...cliContextBlocks,
|
||||
{ type: 'text', text: '<user_query>say hi</user_query>' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromResponses can passthrough request instructions instead of original', () => {
|
||||
const messages = buildUpstreamMessagesFromResponses(
|
||||
{
|
||||
instructions: 'be concise',
|
||||
input: 'say hi',
|
||||
},
|
||||
builderContext('passthrough'),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'be concise');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
|
||||
assert.equal(messages[1]?.role, 'user');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromResponses can send original then instructions in hybrid mode', () => {
|
||||
const messages = buildUpstreamMessagesFromResponses(
|
||||
{
|
||||
instructions: 'be concise',
|
||||
input: 'say hi',
|
||||
},
|
||||
builderContext('hybrid'),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'captured system prompt');
|
||||
assert.equal(messages[1]?.role, 'system');
|
||||
assert.equal(messages[1]?.content, 'be concise');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 2);
|
||||
assert.equal(messages[2]?.role, 'user');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromResponses accepts string message content', () => {
|
||||
const messages = buildUpstreamMessagesFromResponses(
|
||||
{
|
||||
input: [
|
||||
{
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: 'say hi',
|
||||
},
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[1]?.role, 'user');
|
||||
assert.deepEqual(messages[1]?.content, [
|
||||
...cliContextBlocks,
|
||||
{ type: 'text', text: '<user_query>say hi</user_query>' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromResponses preserves function call continuation items', () => {
|
||||
const messages = buildUpstreamMessagesFromResponses(
|
||||
{
|
||||
input: [
|
||||
{ type: 'function_call', call_id: 'call_1', name: 'lookup', arguments: '{"q":"x"}' },
|
||||
{ type: 'function_call_output', call_id: 'call_1', output: 'result' },
|
||||
{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'continue' }] },
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[1]?.role, 'assistant');
|
||||
assert.deepEqual(messages[1]?.tool_calls, [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'lookup', arguments: '{"q":"x"}' },
|
||||
}]);
|
||||
assert.equal(messages[2]?.role, 'tool');
|
||||
assert.equal(messages[2]?.tool_call_id, 'call_1');
|
||||
assert.equal(messages[2]?.content, 'result');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromAnthropic uses original system prompt by default', () => {
|
||||
const messages = buildUpstreamMessagesFromAnthropic(
|
||||
{
|
||||
system: 'anthropic system',
|
||||
messages: [
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'older answer' }] },
|
||||
{ role: 'user', content: [{ type: 'text', text: 'new question' }] },
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'captured system prompt');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
|
||||
assert.equal(messages[1]?.role, 'assistant');
|
||||
assert.equal(messages[1]?.content, 'older answer');
|
||||
assert.equal(messages[2]?.role, 'user');
|
||||
assert.equal(messages[2]?.agent, 'cli');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromAnthropic can passthrough request system instead of original', () => {
|
||||
const messages = buildUpstreamMessagesFromAnthropic(
|
||||
{
|
||||
system: 'anthropic system',
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: 'new question' }] }],
|
||||
},
|
||||
builderContext('passthrough'),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'anthropic system');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
|
||||
assert.equal(messages[1]?.role, 'user');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromAnthropic can send original then request system in hybrid mode', () => {
|
||||
const messages = buildUpstreamMessagesFromAnthropic(
|
||||
{
|
||||
system: 'anthropic system',
|
||||
messages: [{ role: 'user', content: [{ type: 'text', text: 'new question' }] }],
|
||||
},
|
||||
builderContext('hybrid'),
|
||||
);
|
||||
|
||||
assert.equal(messages[0]?.role, 'system');
|
||||
assert.equal(messages[0]?.content, 'captured system prompt');
|
||||
assert.equal(messages[1]?.role, 'system');
|
||||
assert.equal(messages[1]?.content, 'anthropic system');
|
||||
assert.equal(messages.filter((message) => message.role === 'system').length, 2);
|
||||
assert.equal(messages[2]?.role, 'user');
|
||||
});
|
||||
|
||||
test('buildUpstreamMessagesFromAnthropic preserves tool_use and tool_result blocks', () => {
|
||||
const messages = buildUpstreamMessagesFromAnthropic(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'tool_use', id: 'call_1', name: 'lookup', input: { q: 'x' } }],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'result' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
builderContext(),
|
||||
);
|
||||
|
||||
assert.equal(messages[1]?.role, 'assistant');
|
||||
assert.deepEqual(messages[1]?.tool_calls, [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: { name: 'lookup', arguments: '{"q":"x"}' },
|
||||
}]);
|
||||
assert.equal(messages[2]?.role, 'tool');
|
||||
assert.equal(messages[2]?.tool_call_id, 'call_1');
|
||||
assert.equal(messages[2]?.content, 'result');
|
||||
});
|
||||
|
||||
test('buildUpstreamOptionsFromChat passes through model tools and reasoning settings', () => {
|
||||
const options = buildUpstreamOptionsFromChat({
|
||||
model: 'kimi-k2.6',
|
||||
tools: [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }],
|
||||
temperature: 0.3,
|
||||
max_tokens: 4096,
|
||||
reasoning_effort: 'high',
|
||||
verbosity: 'low',
|
||||
reasoning_summary: 'detailed',
|
||||
});
|
||||
|
||||
assert.equal(options.model, 'kimi-k2.6');
|
||||
assert.equal(options.temperature, 0.3);
|
||||
assert.equal(options.max_tokens, 4096);
|
||||
assert.equal(options.reasoning_effort, 'high');
|
||||
assert.equal(options.verbosity, 'low');
|
||||
assert.equal(options.reasoning_summary, 'detailed');
|
||||
assert.deepEqual(options.tools, [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }]);
|
||||
});
|
||||
|
||||
test('buildUpstreamOptionsFromResponses passes through model tools and reasoning settings', () => {
|
||||
const options = buildUpstreamOptionsFromResponses({
|
||||
model: 'glm-5.1',
|
||||
tools: [{ type: 'function', name: 'lookup', description: 'd', parameters: { type: 'object' } }],
|
||||
temperature: 0.2,
|
||||
max_output_tokens: 2048,
|
||||
reasoning: { effort: 'medium', summary: 'auto' },
|
||||
text: { verbosity: 'high' },
|
||||
});
|
||||
|
||||
assert.equal(options.model, 'glm-5.1');
|
||||
assert.equal(options.temperature, 0.2);
|
||||
assert.equal(options.max_tokens, 2048);
|
||||
assert.equal(options.reasoning_effort, 'medium');
|
||||
assert.equal(options.reasoning_summary, 'auto');
|
||||
assert.equal(options.verbosity, 'high');
|
||||
assert.deepEqual(options.tools, [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }]);
|
||||
});
|
||||
|
||||
test('buildUpstreamOptionsFromAnthropic passes through model tools and thinking budget', () => {
|
||||
const options = buildUpstreamOptionsFromAnthropic({
|
||||
model: 'hunyuan-2.0-thinking',
|
||||
tools: [{ name: 'lookup', description: 'd', input_schema: { type: 'object' } }],
|
||||
max_tokens: 1024,
|
||||
thinking: { type: 'enabled', budget_tokens: 2048 },
|
||||
});
|
||||
|
||||
assert.equal(options.model, 'hunyuan-2.0-thinking');
|
||||
assert.equal(options.max_tokens, 1024);
|
||||
assert.equal(options.reasoning_effort, 'high');
|
||||
assert.deepEqual(options.tools, [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }]);
|
||||
});
|
||||
|
||||
test('loadModels reads config-backed model registry', () => {
|
||||
const models = loadModels();
|
||||
assert.ok(models.some((model) => model.id === 'kimi-k2.6' && model.name === 'Kimi-K2.6' && model.credits_multiplier === 0.59));
|
||||
assert.ok(models.some((model) => model.id === 'hunyuan-2.0-thinking' && model.credits_multiplier === 0.04));
|
||||
});
|
||||
test('parseUpstreamChunk extracts content, reasoning, finish reason, and usage', () => {
|
||||
const events = parseUpstreamChunk({
|
||||
id: 'up_1',
|
||||
model: 'ep-model',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 456,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: 'hello',
|
||||
reasoning_content: 'thinking',
|
||||
function_call: { name: 'lookup', arguments: '{"q":"x"}' },
|
||||
},
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 20,
|
||||
total_tokens: 30,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(events, [
|
||||
{ type: 'text_delta', text: 'hello' },
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'function_call_name', name: 'lookup' },
|
||||
{ type: 'function_call_arguments_delta', text: '{"q":"x"}' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{
|
||||
type: 'usage',
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('parseUpstreamChunk extracts tool call deltas', () => {
|
||||
const events = parseUpstreamChunk({
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
}],
|
||||
},
|
||||
finish_reason: 'tool_calls',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(events, [
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('parseUpstreamChunk ignores empty upstream finish reason placeholders', () => {
|
||||
const events = parseUpstreamChunk({
|
||||
choices: [
|
||||
{
|
||||
delta: { reasoning_content: 'thinking' },
|
||||
finish_reason: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(events, [{ type: 'reasoning_delta', text: 'thinking' }]);
|
||||
});
|
||||
|
||||
test('buildChatCompletionResponse maps canonical state to non-stream OpenAI response', () => {
|
||||
const state = accumulate([
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'text_delta', text: 'hello world' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{ type: 'usage', usage: { prompt_tokens: 2, completion_tokens: 3, total_tokens: 5 } },
|
||||
]);
|
||||
|
||||
const response = buildChatCompletionResponse(state);
|
||||
assert.equal(response.object, 'chat.completion');
|
||||
assert.equal(response.choices[0]?.message?.content, 'hello world');
|
||||
assert.equal(response.choices[0]?.message?.reasoning_content, 'thinking');
|
||||
assert.equal(response.usage?.total_tokens, 5);
|
||||
});
|
||||
|
||||
test('buildChatCompletionResponse maps tool calls to OpenAI message tool_calls', () => {
|
||||
const state = accumulate([
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai","unit":"celsius"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
]);
|
||||
|
||||
const response = buildChatCompletionResponse(state);
|
||||
assert.equal(response.choices[0]?.finish_reason, 'tool_calls');
|
||||
assert.deepEqual(response.choices[0]?.message?.tool_calls, [{
|
||||
id: 'call_1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai","unit":"celsius"}',
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
test('collectChatCompletionStreamFrames emits OpenAI SSE chunks and DONE', () => {
|
||||
const frames = collectChatCompletionStreamFrames(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'text_delta', text: 'hello' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{ type: 'usage', usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } },
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(frames.at(-1), 'data: [DONE]');
|
||||
assert.ok(frames.some((frame) => frame.includes('reasoning_content')));
|
||||
assert.ok(frames.some((frame) => frame.includes('"content":"hello"')));
|
||||
});
|
||||
|
||||
test('collectChatCompletionStreamFrames emits tool call deltas', () => {
|
||||
const frames = collectChatCompletionStreamFrames(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
],
|
||||
);
|
||||
|
||||
assert.ok(frames.some((frame) => frame.includes('"tool_calls"')));
|
||||
assert.ok(frames.some((frame) => frame.includes('"name":"fake_lookup_weather"')));
|
||||
assert.ok(frames.some((frame) => frame.includes('"finish_reason":"tool_calls"')));
|
||||
assert.equal(frames.at(-1), 'data: [DONE]');
|
||||
});
|
||||
|
||||
test('buildResponsesResponse maps canonical state to OpenAI responses payload', () => {
|
||||
const state = accumulate([
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'text_delta', text: 'hello world' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{ type: 'usage', usage: { prompt_tokens: 4, completion_tokens: 5, total_tokens: 9 } },
|
||||
]);
|
||||
|
||||
const response = buildResponsesResponse(state);
|
||||
const messageItem = response.output[1] as { type?: string; content?: Array<{ text?: string }> };
|
||||
assert.equal(response.object, 'response');
|
||||
assert.equal(response.status, 'completed');
|
||||
assert.equal(response.output[0]?.type, 'reasoning');
|
||||
assert.equal(messageItem.type, 'message');
|
||||
assert.equal(messageItem.content?.[0]?.text, 'hello world');
|
||||
});
|
||||
|
||||
test('buildResponsesResponse maps tool calls to function_call output items', () => {
|
||||
const state = accumulate([
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
]);
|
||||
|
||||
const response = buildResponsesResponse(state);
|
||||
assert.deepEqual(response.output.at(-1), {
|
||||
type: 'function_call',
|
||||
id: (response.output.at(-1) as { id: string }).id,
|
||||
call_id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
status: 'completed',
|
||||
});
|
||||
});
|
||||
|
||||
test('collectResponsesStreamEvents emits sub2api-style response events', () => {
|
||||
const events = collectResponsesStreamEvents(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'text_delta', text: 'hello' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{ type: 'usage', usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } },
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(events[0]?.type, 'response.created');
|
||||
assert.ok(events.some((event) => event.type === 'response.reasoning_summary_text.delta'));
|
||||
assert.ok(events.some((event) => event.type === 'response.output_text.delta'));
|
||||
assert.equal(events.at(-1)?.type, 'response.completed');
|
||||
});
|
||||
|
||||
test('collectResponsesStreamEvents starts text-only output at index zero', () => {
|
||||
const events = collectResponsesStreamEvents(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{ type: 'text_delta', text: 'hello' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
],
|
||||
);
|
||||
|
||||
const added = events.find((event) => event.type === 'response.output_item.added') as { output_index?: number } | undefined;
|
||||
const delta = events.find((event) => event.type === 'response.output_text.delta') as { output_index?: number } | undefined;
|
||||
const done = events.find((event) => event.type === 'response.output_text.done') as { output_index?: number } | undefined;
|
||||
assert.equal(added?.output_index, 0);
|
||||
assert.equal(delta?.output_index, 0);
|
||||
assert.equal(done?.output_index, 0);
|
||||
});
|
||||
|
||||
test('collectResponsesStreamEvents emits function call stream events', () => {
|
||||
const events = collectResponsesStreamEvents(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
],
|
||||
);
|
||||
|
||||
assert.ok(events.some((event) => event.type === 'response.output_item.added' && JSON.stringify(event).includes('fake_lookup_weather')));
|
||||
assert.ok(events.some((event) => event.type === 'response.function_call_arguments.delta' && JSON.stringify(event).includes('Shanghai')));
|
||||
assert.ok(events.some((event) => event.type === 'response.output_item.done' && JSON.stringify(event).includes('function_call')));
|
||||
});
|
||||
|
||||
test('collectResponsesStreamEvents starts tool-only output at index zero', () => {
|
||||
const events = collectResponsesStreamEvents(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
],
|
||||
);
|
||||
|
||||
const added = events.find((event) => event.type === 'response.output_item.added') as { output_index?: number } | undefined;
|
||||
const delta = events.find((event) => event.type === 'response.function_call_arguments.delta') as { output_index?: number } | undefined;
|
||||
const done = events.find((event) => event.type === 'response.function_call_arguments.done') as { output_index?: number } | undefined;
|
||||
assert.equal(added?.output_index, 0);
|
||||
assert.equal(delta?.output_index, 0);
|
||||
assert.equal(done?.output_index, 0);
|
||||
});
|
||||
|
||||
test('buildAnthropicMessageResponse includes thinking and text blocks', () => {
|
||||
const state = accumulate([
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'text_delta', text: 'hello world' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{ type: 'usage', usage: { prompt_tokens: 7, completion_tokens: 8, total_tokens: 15 } },
|
||||
]);
|
||||
|
||||
const response = buildAnthropicMessageResponse(state);
|
||||
assert.equal(response.type, 'message');
|
||||
assert.equal(response.content[0]?.type, 'thinking');
|
||||
assert.equal(response.content[1]?.type, 'text');
|
||||
assert.equal(response.content[1]?.text, 'hello world');
|
||||
assert.equal(response.stop_reason, 'end_turn');
|
||||
});
|
||||
|
||||
test('buildAnthropicMessageResponse maps tool calls to tool_use blocks', () => {
|
||||
const state = accumulate([
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
]);
|
||||
|
||||
const response = buildAnthropicMessageResponse(state);
|
||||
assert.equal(response.stop_reason, 'tool_use');
|
||||
assert.deepEqual(response.content.at(-1), {
|
||||
type: 'tool_use',
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
input: { city: 'Shanghai' },
|
||||
});
|
||||
});
|
||||
|
||||
test('collectAnthropicStreamEvents emits thinking and text SSE events', () => {
|
||||
const events = collectAnthropicStreamEvents(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{ type: 'reasoning_delta', text: 'thinking' },
|
||||
{ type: 'text_delta', text: 'hello' },
|
||||
{ type: 'finish', finishReason: 'stop' },
|
||||
{ type: 'usage', usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } },
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(events[0]?.event, 'message_start');
|
||||
assert.ok(events.some((event) => event.event === 'content_block_delta' && JSON.stringify(event.data).includes('thinking_delta')));
|
||||
assert.ok(events.some((event) => event.event === 'content_block_delta' && JSON.stringify(event.data).includes('text_delta')));
|
||||
assert.equal(events.at(-1)?.event, 'message_stop');
|
||||
});
|
||||
|
||||
test('collectAnthropicStreamEvents emits tool_use stream events', () => {
|
||||
const events = collectAnthropicStreamEvents(
|
||||
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
|
||||
[
|
||||
{
|
||||
type: 'tool_call_delta',
|
||||
index: 0,
|
||||
id: 'call_1',
|
||||
name: 'fake_lookup_weather',
|
||||
arguments: '{"city":"Shanghai"}',
|
||||
},
|
||||
{ type: 'finish', finishReason: 'tool_calls' },
|
||||
],
|
||||
);
|
||||
|
||||
assert.ok(events.some((event) => event.event === 'content_block_start' && JSON.stringify(event.data).includes('tool_use')));
|
||||
assert.ok(events.some((event) => event.event === 'content_block_delta' && JSON.stringify(event.data).includes('input_json_delta')));
|
||||
assert.ok(events.some((event) => event.event === 'message_delta' && JSON.stringify(event.data).includes('tool_use')));
|
||||
});
|
||||
Reference in New Issue
Block a user