ccpoad / internal /protocol /registry_stream_toolcalls_test.go
anyalerob's picture
Upload folder using huggingface_hub
2986042 verified
Raw
History Blame Contribute Delete
25 kB
package protocol_test
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"ccLoad/internal/protocol"
"ccLoad/internal/protocol/builtin"
)
// TestRegistry_Stream_OpenAIToCodex_ToolCalls 验证 OpenAI stream tool_calls 增量
// 经过多个 chunk 拼接 arguments 后,[DONE] 时输出 response.output_item.done(type=function_call)。
func TestRegistry_Stream_OpenAIToCodex_ToolCalls(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
// chunk 1: tool_call 开头,携带 id/name,arguments 为空字符串
`data: {"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}` + "\n\n",
// chunk 2: arguments 第一段
`data: {"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]}}]}` + "\n\n",
// chunk 3: arguments 第二段(完整 JSON 闭合)
`data: {"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"Beijing\"}"}}]}}]}` + "\n\n",
// chunk 4: finish_reason
`data: {"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}` + "\n\n",
// chunk 5: [DONE]
"data: [DONE]\n\n",
}
var state any
var allOutput bytes.Buffer
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Codex, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
for _, b := range out {
allOutput.Write(b)
}
}
result := allOutput.String()
if !strings.Contains(result, `event: response.output_item.done`) {
t.Fatalf("expected response.output_item.done event, got:\n%s", result)
}
if !strings.Contains(result, `"type":"function_call"`) {
t.Fatalf("expected type=function_call, got:\n%s", result)
}
if !strings.Contains(result, `"name":"get_weather"`) {
t.Fatalf("expected name=get_weather, got:\n%s", result)
}
// 拼接后的完整 arguments 字符串应完整出现(JSON 字符串内部以转义形式存在)
if !strings.Contains(result, `city`) {
t.Fatalf("expected city in arguments, got:\n%s", result)
}
if !strings.Contains(result, `Beijing`) {
t.Fatalf("expected Beijing in arguments, got:\n%s", result)
}
// call_id 应保留原始 id
if !strings.Contains(result, `"call_id":"call_abc"`) {
t.Fatalf("expected call_id=call_abc, got:\n%s", result)
}
// 必须有 response.completed
if !strings.Contains(result, `event: response.completed`) {
t.Fatalf("expected response.completed event, got:\n%s", result)
}
}
// TestRegistry_Stream_CodexToOpenAI_FunctionCall 验证 Codex stream function_call 转成 OpenAI tool_calls chunk。
func TestRegistry_Stream_CodexToOpenAI_FunctionCall(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
// Codex SSE 格式: event: <type>\ndata: <json>\n\n
codexChunk := `event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_xyz","name":"search","arguments":"{\"q\":\"hello\"}"}}
`
var state any
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", nil, nil, []byte(codexChunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
if len(out) == 0 {
t.Fatalf("expected output chunks, got none")
}
result := string(bytes.Join(out, nil))
if !strings.Contains(result, `"tool_calls"`) {
t.Fatalf("expected tool_calls in output, got:\n%s", result)
}
if !strings.Contains(result, `"id":"call_xyz"`) {
t.Fatalf("expected id=call_xyz, got:\n%s", result)
}
if !strings.Contains(result, `"name":"search"`) {
t.Fatalf("expected name=search, got:\n%s", result)
}
if !strings.Contains(result, `hello`) {
t.Fatalf("expected 'hello' in arguments, got:\n%s", result)
}
}
func TestRegistry_Stream_CodexToOpenAI_FunctionCallStringArgumentsStayRawJSON(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
codexChunk := `event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_skill_1","name":"Skill","arguments":"{\"args\":\"skill: \\\"superpowers:using-superpowers\\\"\",\"skill\":\"superpowers:using-superpowers\"}"}}
`
var state any
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", nil, nil, []byte(codexChunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
if len(out) != 1 {
t.Fatalf("expected one output chunk, got %#v", out)
}
result := string(out[0])
if !strings.Contains(result, `"arguments":"{\"args\":\"skill: \\\"superpowers:using-superpowers\\\"\",\"skill\":\"superpowers:using-superpowers\"}"`) {
t.Fatalf("expected raw JSON arguments string, got:\n%s", result)
}
}
func TestRegistry_Stream_CodexToOpenAI_FunctionCallIndices(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
`event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"one\"}"}}
`,
`event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_2","name":"search","arguments":"{\"q\":\"two\"}"}}
`,
}
var state any
var outputs [][]byte
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
outputs = append(outputs, out...)
}
if len(outputs) != 2 {
t.Fatalf("expected 2 output chunks, got %d", len(outputs))
}
if !strings.Contains(string(outputs[0]), `"index":0`) {
t.Fatalf("expected first tool call index 0, got:\n%s", outputs[0])
}
if !strings.Contains(string(outputs[1]), `"index":1`) {
t.Fatalf("expected second tool call index 1, got:\n%s", outputs[1])
}
}
func TestRegistry_Stream_CodexToOpenAI_FunctionCallCompletion(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
`event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"one\"}"}}
`,
`event: response.completed
data: {"type":"response.completed","response":{"id":"resp_1","model":"gpt-4o","usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}}
`,
}
var state any
var outputs [][]byte
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
outputs = append(outputs, out...)
}
if len(outputs) != 3 {
t.Fatalf("expected tool call chunk + finish chunk + [DONE], got %d", len(outputs))
}
if !strings.Contains(string(outputs[1]), `"finish_reason":"tool_calls"`) {
t.Fatalf("expected completion finish_reason=tool_calls, got:\n%s", outputs[1])
}
if string(outputs[2]) != "data: [DONE]\n\n" {
t.Fatalf("expected OpenAI DONE sentinel, got:\n%s", outputs[2])
}
}
// TestRegistry_Stream_CodexToAnthropic_FunctionCall 验证 Codex stream function_call
// 转成 Anthropic content_block_start(type=tool_use) + input_json_delta + content_block_stop。
func TestRegistry_Stream_CodexToAnthropic_FunctionCall(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
codexChunk := `event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","id":"toolu_01","call_id":"call_fc1","name":"calculator","arguments":"{\"expr\":\"1+2\"}"}}
`
var state any
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte(codexChunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
if len(out) == 0 {
t.Fatalf("expected output chunks, got none")
}
result := string(bytes.Join(out, nil))
if !strings.Contains(result, `event: content_block_start`) {
t.Fatalf("expected content_block_start, got:\n%s", result)
}
if !strings.Contains(result, `"type":"tool_use"`) {
t.Fatalf("expected type=tool_use, got:\n%s", result)
}
if !strings.Contains(result, `"name":"calculator"`) {
t.Fatalf("expected name=calculator, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_delta`) {
t.Fatalf("expected content_block_delta, got:\n%s", result)
}
if !strings.Contains(result, `"input_json_delta"`) {
t.Fatalf("expected input_json_delta type, got:\n%s", result)
}
if !strings.Contains(result, `expr`) {
t.Fatalf("expected expr in partial_json, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_stop`) {
t.Fatalf("expected content_block_stop, got:\n%s", result)
}
}
func TestRegistry_Stream_CodexToAnthropic_FunctionCallUsesCallID(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
codexChunk := `event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_fc1","name":"calculator","arguments":"{\"expr\":\"1+2\"}"}}
`
var state any
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte(codexChunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
if len(out) == 0 {
t.Fatalf("expected output chunks, got none")
}
result := string(bytes.Join(out, nil))
if !strings.Contains(result, `"id":"call_fc1"`) {
t.Fatalf("expected tool_use id to preserve call_id, got:\n%s", result)
}
}
func TestRegistry_Stream_CodexToAnthropic_FunctionCallCompletion(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
`event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_fc1","name":"calculator","arguments":"{\"expr\":\"1+2\"}"}}
`,
`event: response.completed
data: {"type":"response.completed","response":{"id":"resp_1","model":"claude-3-5-sonnet","usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}}
`,
}
var state any
var outputs [][]byte
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
outputs = append(outputs, out...)
}
result := string(bytes.Join(outputs, nil))
if strings.Count(result, `event: content_block_stop`) != 1 {
t.Fatalf("expected exactly one content_block_stop for tool-only response, got:\n%s", result)
}
if !strings.Contains(result, `"stop_reason":"tool_use"`) {
t.Fatalf("expected stop_reason=tool_use for tool-only response, got:\n%s", result)
}
}
func TestRegistry_Stream_CodexToAnthropic_TextThenFunctionCall(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
`event: response.output_text.delta
data: {"type":"response.output_text.delta","delta":"thinking..."}
`,
`event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_fc1","name":"calculator","arguments":"{\"expr\":\"1+2\"}"}}
`,
`event: response.completed
data: {"type":"response.completed","response":{"id":"resp_1","model":"claude-3-5-sonnet","usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}}
`,
}
var state any
var outputs [][]byte
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
outputs = append(outputs, out...)
}
result := string(bytes.Join(outputs, nil))
if strings.Count(result, `event: content_block_start`) != 2 {
t.Fatalf("expected separate text/tool content blocks, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_start`) || !strings.Contains(result, `"index":0`) || !strings.Contains(result, `"type":"text"`) {
t.Fatalf("expected text block at index 0, got:\n%s", result)
}
if !strings.Contains(result, `"index":1`) || !strings.Contains(result, `"type":"tool_use"`) || !strings.Contains(result, `"id":"call_fc1"`) || !strings.Contains(result, `"name":"calculator"`) {
t.Fatalf("expected tool block at index 1, got:\n%s", result)
}
if strings.Count(result, `event: content_block_stop`) != 2 {
t.Fatalf("expected exactly two content_block_stop events, got:\n%s", result)
}
if !strings.Contains(result, `"stop_reason":"tool_use"`) {
t.Fatalf("expected stop_reason=tool_use when final block is tool_use, got:\n%s", result)
}
}
// TestRegistry_Stream_CodexToAnthropic_Reasoning 验证 Codex stream reasoning(有 summary text)
// 转成 Anthropic thinking 块(content_block_start + thinking_delta + content_block_stop)。
func TestRegistry_Stream_CodexToAnthropic_Reasoning(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
// reasoning item 包含 summary 数组
codexChunk := `event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"reasoning","id":"rs_01","summary":[{"type":"summary_text","text":"step by step reasoning"}]}}
`
var state any
out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte(codexChunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
if len(out) == 0 {
t.Fatalf("expected output chunks, got none")
}
result := string(bytes.Join(out, nil))
if !strings.Contains(result, `event: content_block_start`) {
t.Fatalf("expected content_block_start, got:\n%s", result)
}
if !strings.Contains(result, `"type":"thinking"`) {
t.Fatalf("expected type=thinking, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_delta`) {
t.Fatalf("expected content_block_delta, got:\n%s", result)
}
if !strings.Contains(result, `"thinking_delta"`) {
t.Fatalf("expected thinking_delta type, got:\n%s", result)
}
if !strings.Contains(result, `"step by step reasoning"`) {
t.Fatalf("expected reasoning text, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_stop`) {
t.Fatalf("expected content_block_stop, got:\n%s", result)
}
}
// TestRegistry_Stream_OpenAIToAnthropic_ToolCalls 验证 OpenAI stream tool_calls 增量
// 在 finish_reason=tool_calls 时批量输出 Anthropic tool_use 块(start+delta+stop)。
func TestRegistry_Stream_OpenAIToAnthropic_ToolCalls(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
// chunk 1: tool_call 首 chunk,携带 id/name
`data: {"id":"chatcmpl-2","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_t1","type":"function","function":{"name":"translate","arguments":""}}]}}]}` + "\n\n",
// chunk 2: arguments 增量
`data: {"id":"chatcmpl-2","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"text\":\"hello\"}"}}]}}]}` + "\n\n",
// chunk 3: finish_reason,触发 flush
`data: {"id":"chatcmpl-2","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}` + "\n\n",
"data: [DONE]\n\n",
}
var state any
var allOutput bytes.Buffer
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
for _, b := range out {
allOutput.Write(b)
}
}
result := allOutput.String()
if !strings.Contains(result, `event: content_block_start`) {
t.Fatalf("expected content_block_start, got:\n%s", result)
}
if !strings.Contains(result, `"type":"tool_use"`) {
t.Fatalf("expected type=tool_use, got:\n%s", result)
}
if !strings.Contains(result, `"name":"translate"`) {
t.Fatalf("expected name=translate, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_delta`) {
t.Fatalf("expected content_block_delta, got:\n%s", result)
}
if !strings.Contains(result, `"input_json_delta"`) {
t.Fatalf("expected input_json_delta type, got:\n%s", result)
}
if !strings.Contains(result, `"partial_json":"{\"text\":\"hello\"}"`) {
t.Fatalf("expected tool arguments in partial_json, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_stop`) {
t.Fatalf("expected content_block_stop, got:\n%s", result)
}
if !strings.Contains(result, `"stop_reason":"tool_use"`) {
t.Fatalf("expected stop_reason=tool_use, got:\n%s", result)
}
}
func TestRegistry_Stream_OpenAIToAnthropic_TextThenFragmentedToolCalls(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
`data: {"id":"chatcmpl-4","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"content":"让我检查一下"}}]}` + "\n\n",
`data: {"id":"chatcmpl-4","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_lookup","type":"function","function":{"name":"Bash","arguments":""}}]}}]}` + "\n\n",
`data: {"id":"chatcmpl-4","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"command\":\"ls\""}}]}}]}` + "\n\n",
`data: {"id":"chatcmpl-4","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"}"}}]},"finish_reason":"tool_calls"}]}` + "\n\n",
"data: [DONE]\n\n",
}
var state any
var allOutput bytes.Buffer
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
for _, b := range out {
allOutput.Write(b)
}
}
result := allOutput.String()
events := parseSSEEvents(t, result)
expectedEvents := []string{
"message_start",
"content_block_start",
"content_block_delta",
"content_block_stop",
"content_block_start",
"content_block_delta",
"content_block_stop",
"message_delta",
"message_stop",
}
if len(events) != len(expectedEvents) {
t.Fatalf("expected %d SSE events, got %d:\n%s", len(expectedEvents), len(events), result)
}
for i, expected := range expectedEvents {
if events[i].Event != expected {
t.Fatalf("expected event[%d]=%s, got %s:\n%s", i, expected, events[i].Event, result)
}
}
textStart := mustMap(t, events[1].Data["content_block"])
if mustInt(t, events[1].Data["index"]) != 0 || mustString(t, textStart["type"]) != "text" {
t.Fatalf("expected text block to start at index 0, got:\n%s", result)
}
textDelta := mustMap(t, events[2].Data["delta"])
if mustInt(t, events[2].Data["index"]) != 0 || mustString(t, textDelta["type"]) != "text_delta" || mustString(t, textDelta["text"]) != "让我检查一下" {
t.Fatalf("expected text delta to stay on index 0, got:\n%s", result)
}
if mustInt(t, events[3].Data["index"]) != 0 {
t.Fatalf("expected text block to close on index 0 before tool call, got:\n%s", result)
}
toolStart := mustMap(t, events[4].Data["content_block"])
if mustInt(t, events[4].Data["index"]) != 1 ||
mustString(t, toolStart["type"]) != "tool_use" ||
mustString(t, toolStart["id"]) != "call_lookup" ||
mustString(t, toolStart["name"]) != "Bash" {
t.Fatalf("expected tool block to open on index 1, got:\n%s", result)
}
toolDelta := mustMap(t, events[5].Data["delta"])
if mustInt(t, events[5].Data["index"]) != 1 ||
mustString(t, toolDelta["type"]) != "input_json_delta" ||
mustString(t, toolDelta["partial_json"]) != `{"command":"ls"}` {
t.Fatalf("expected fragmented tool arguments to be reassembled, got:\n%s", result)
}
if mustInt(t, events[6].Data["index"]) != 1 {
t.Fatalf("expected tool block to close on index 1, got:\n%s", result)
}
messageDelta := mustMap(t, events[7].Data["delta"])
if mustString(t, messageDelta["stop_reason"]) != "tool_use" {
t.Fatalf("expected stop_reason=tool_use, got:\n%s", result)
}
}
func TestRegistry_Stream_OpenAIToAnthropic_ToolCalls_AllSplitPoints(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
arguments := `{"command":"ls"}`
for split := 0; split <= len(arguments); split++ {
t.Run(fmt.Sprintf("split-%d", split), func(t *testing.T) {
chunks := []string{
`data: {"id":"chatcmpl-2","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_lookup","type":"function","function":{"name":"Bash","arguments":""}}]}}]}` + "\n\n",
fmt.Sprintf(`data: {"id":"chatcmpl-2","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":%q}}]}}]}`+"\n\n", arguments[:split]),
fmt.Sprintf(`data: {"id":"chatcmpl-2","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":%q}}]},"finish_reason":"tool_calls"}]}`+"\n\n", arguments[split:]),
"data: [DONE]\n\n",
}
var state any
var allOutput bytes.Buffer
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("split %d stream error: %v", split, err)
}
for _, b := range out {
allOutput.Write(b)
}
}
result := allOutput.String()
if !strings.Contains(result, `"id":"call_lookup"`) || !strings.Contains(result, `"name":"Bash"`) {
t.Fatalf("split %d expected tool_use metadata, got:\n%s", split, result)
}
if !strings.Contains(result, `"partial_json":"{\"command\":\"ls\"}"`) || !strings.Contains(result, `"stop_reason":"tool_use"`) {
t.Fatalf("split %d expected reassembled anthropic tool payload, got:\n%s", split, result)
}
})
}
}
// TestRegistry_Stream_OpenAIToAnthropic_Reasoning 验证 OpenAI stream reasoning_content
// 转成 Anthropic thinking 块(start + thinking_delta + stop),finish_reason=stop 时关闭块。
func TestRegistry_Stream_OpenAIToAnthropic_Reasoning(t *testing.T) {
t.Parallel()
reg := protocol.NewRegistry()
builtin.Register(reg)
chunks := []string{
// chunk 1: reasoning_content 第一段
`data: {"id":"chatcmpl-3","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"reasoning_content":"Let me think"}}]}` + "\n\n",
// chunk 2: reasoning_content 第二段
`data: {"id":"chatcmpl-3","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"reasoning_content":" carefully"}}]}` + "\n\n",
// chunk 3: finish_reason=stop,关闭 thinking 块
`data: {"id":"chatcmpl-3","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}` + "\n\n",
"data: [DONE]\n\n",
}
var state any
var allOutput bytes.Buffer
for _, chunk := range chunks {
out, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", nil, nil, []byte(chunk), &state)
if err != nil {
t.Fatalf("stream error: %v", err)
}
for _, b := range out {
allOutput.Write(b)
}
}
result := allOutput.String()
if !strings.Contains(result, `event: content_block_start`) {
t.Fatalf("expected content_block_start, got:\n%s", result)
}
if !strings.Contains(result, `"type":"thinking"`) {
t.Fatalf("expected type=thinking, got:\n%s", result)
}
if !strings.Contains(result, `"thinking_delta"`) {
t.Fatalf("expected thinking_delta type, got:\n%s", result)
}
if !strings.Contains(result, `"Let me think"`) {
t.Fatalf("expected first reasoning chunk, got:\n%s", result)
}
if !strings.Contains(result, `" carefully"`) {
t.Fatalf("expected second reasoning chunk, got:\n%s", result)
}
if !strings.Contains(result, `event: content_block_stop`) {
t.Fatalf("expected content_block_stop, got:\n%s", result)
}
// finish_reason=stop → stop_reason=end_turn
if !strings.Contains(result, `"stop_reason":"end_turn"`) {
t.Fatalf("expected stop_reason=end_turn, got:\n%s", result)
}
}