164 lines
8.5 KiB
Go
164 lines
8.5 KiB
Go
package claude
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestConvertClaudeRequestToInteractionsMapsMessagesToolsAndStream(t *testing.T) {
|
|
raw := []byte(`{"model":"gemini-3.1-flash-lite","stream":true,"max_tokens":1024,"tools":[{"name":"get_weather","description":"Weather","input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"messages":[{"role":"user","content":[{"type":"text","text":"今天北京的天气怎么样?"}]}]}`)
|
|
out := ConvertClaudeRequestToInteractions("gemini-3.1-flash-lite", raw, true)
|
|
if got := gjson.GetBytes(out, "model").String(); got != "gemini-3.1-flash-lite" {
|
|
t.Fatalf("model = %q, want gemini-3.1-flash-lite. Output: %s", got, string(out))
|
|
}
|
|
if !gjson.GetBytes(out, "stream").Bool() {
|
|
t.Fatalf("stream should be true. Output: %s", string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "generation_config.max_output_tokens").Int(); got != 1024 {
|
|
t.Fatalf("max_output_tokens = %d, want 1024. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "input.0.type").String(); got != "user_input" {
|
|
t.Fatalf("input.0.type = %q, want user_input. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "input.0.content.0.text").String(); got != "今天北京的天气怎么样?" {
|
|
t.Fatalf("input text = %q. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "tools.0.parameters.properties.location.type").String(); got != "string" {
|
|
t.Fatalf("tool schema was not mapped. Output: %s", string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "function" {
|
|
t.Fatalf("tools.0.type = %q, want function. Output: %s", got, string(out))
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToInteractionsMapsToolUseAndResult(t *testing.T) {
|
|
raw := []byte(`{"model":"gemini-3.1-flash-lite","messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"get_weather","input":{"location":"北京"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"晴"}]}]}`)
|
|
out := ConvertClaudeRequestToInteractions("gemini-3.1-flash-lite", raw, false)
|
|
if got := gjson.GetBytes(out, "input.0.type").String(); got != "function_call" {
|
|
t.Fatalf("input.0.type = %q, want function_call. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "input.0.call_id").String(); got != "toolu_1" {
|
|
t.Fatalf("call_id = %q, want toolu_1. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "input.1.type").String(); got != "function_result" {
|
|
t.Fatalf("input.1.type = %q, want function_result. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "input.1.result").String(); got != "晴" {
|
|
t.Fatalf("result = %q, want 晴. Output: %s", got, string(out))
|
|
}
|
|
}
|
|
|
|
func TestConvertInteractionsResponseToClaudeStream(t *testing.T) {
|
|
var param any
|
|
var out [][]byte
|
|
chunks := [][]byte{
|
|
[]byte(`event: interaction.created
|
|
data: {"interaction":{"id":"interaction_1","model":"gemini-3.1-flash-lite"},"event_type":"interaction.created"}`),
|
|
[]byte(`event: step.start
|
|
data: {"index":0,"step":{"type":"model_output"},"event_type":"step.start"}`),
|
|
[]byte(`event: step.delta
|
|
data: {"index":0,"delta":{"type":"text","text":"北京今天晴"},"event_type":"step.delta"}`),
|
|
[]byte(`event: step.stop
|
|
data: {"index":0,"event_type":"step.stop"}`),
|
|
[]byte(`event: interaction.completed
|
|
data: {"interaction":{"id":"interaction_1","model":"gemini-3.1-flash-lite","usage":{"total_input_tokens":3,"total_output_tokens":4}},"event_type":"interaction.completed"}`),
|
|
[]byte(`event: done
|
|
data: [DONE]`),
|
|
}
|
|
for _, chunk := range chunks {
|
|
out = append(out, ConvertInteractionsResponseToClaude(context.Background(), "gemini-3.1-flash-lite", nil, nil, chunk, ¶m)...)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "message_start"); gjson.GetBytes(payload, "message.model").String() != "gemini-3.1-flash-lite" {
|
|
t.Fatalf("message_start payload = %s", payload)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "content_block_delta"); gjson.GetBytes(payload, "delta.text").String() != "北京今天晴" {
|
|
t.Fatalf("content_block_delta payload = %s", payload)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "message_delta"); gjson.GetBytes(payload, "usage.output_tokens").Int() != 4 {
|
|
t.Fatalf("message_delta payload = %s", payload)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "message_stop"); gjson.GetBytes(payload, "type").String() != "message_stop" {
|
|
t.Fatalf("message_stop payload = %s", payload)
|
|
}
|
|
}
|
|
|
|
func TestConvertInteractionsResponseToClaudeStreamToolCall(t *testing.T) {
|
|
var param any
|
|
var out [][]byte
|
|
chunks := [][]byte{
|
|
[]byte(`data: {"interaction":{"id":"interaction_1","model":"gemini-3.1-flash-lite"},"event_type":"interaction.created"}`),
|
|
[]byte(`data: {"index":0,"step":{"type":"function_call","id":"toolu_1","signature":"sig_1","name":"get_weather","arguments":{}},"event_type":"step.start"}`),
|
|
[]byte(`data: {"index":0,"delta":{"type":"arguments_delta","arguments":"{\"location\":\"北京\"}"},"event_type":"step.delta"}`),
|
|
[]byte(`data: {"index":0,"event_type":"step.stop"}`),
|
|
[]byte(`data: {"interaction":{"usage":{"total_input_tokens":1,"total_output_tokens":2}},"event_type":"interaction.completed"}`),
|
|
}
|
|
for _, chunk := range chunks {
|
|
out = append(out, ConvertInteractionsResponseToClaude(context.Background(), "gemini-3.1-flash-lite", nil, nil, chunk, ¶m)...)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "content_block_start"); gjson.GetBytes(payload, "content_block.type").String() != "tool_use" {
|
|
t.Fatalf("content_block_start payload = %s", payload)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "content_block_start"); gjson.GetBytes(payload, "content_block.signature").String() != "sig_1" {
|
|
t.Fatalf("content_block_start signature payload = %s", payload)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "content_block_delta"); gjson.GetBytes(payload, "delta.partial_json").String() != `{"location":"北京"}` {
|
|
t.Fatalf("content_block_delta payload = %s", payload)
|
|
}
|
|
if payload := findClaudeEventPayload(out, "message_delta"); gjson.GetBytes(payload, "delta.stop_reason").String() != "tool_use" {
|
|
t.Fatalf("message_delta payload = %s", payload)
|
|
}
|
|
}
|
|
|
|
func TestConvertInteractionsResponseToClaudeStreamFinishMetadataUsage(t *testing.T) {
|
|
var param any
|
|
out := ConvertInteractionsResponseToClaude(context.Background(), "claude-test", nil, nil, []byte(`data: {"event_type":"finish","metadata":{"total_usage":{"total_input_tokens":2,"total_output_tokens":6,"total_tokens":8}}}`), ¶m)
|
|
payload := findClaudeEventPayload(out, "message_delta")
|
|
if len(payload) == 0 {
|
|
t.Fatalf("message_delta payload not found")
|
|
}
|
|
if got := gjson.GetBytes(payload, "usage.input_tokens").Int(); got != 2 {
|
|
t.Fatalf("input_tokens = %d, want 2. Payload: %s", got, string(payload))
|
|
}
|
|
if got := gjson.GetBytes(payload, "usage.output_tokens").Int(); got != 6 {
|
|
t.Fatalf("output_tokens = %d, want 6. Payload: %s", got, string(payload))
|
|
}
|
|
}
|
|
|
|
func TestConvertInteractionsResponseToClaudeNonStream(t *testing.T) {
|
|
raw := []byte(`{"id":"interaction_1","model":"gemini-3.1-flash-lite","steps":[{"type":"model_output","content":[{"type":"text","text":"ok"}]},{"type":"function_call","call_id":"toolu_1","signature":"sig_1","name":"lookup","arguments":{"q":"x"}}],"usage":{"total_input_tokens":3,"total_output_tokens":4}}`)
|
|
out := ConvertInteractionsResponseToClaudeNonStream(context.Background(), "gemini-3.1-flash-lite", nil, nil, raw, nil)
|
|
if got := gjson.GetBytes(out, "content.0.text").String(); got != "ok" {
|
|
t.Fatalf("text = %q, want ok. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "content.1.type").String(); got != "tool_use" {
|
|
t.Fatalf("tool block type = %q, want tool_use. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "content.1.signature").String(); got != "sig_1" {
|
|
t.Fatalf("tool signature = %q, want sig_1. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "stop_reason").String(); got != "tool_use" {
|
|
t.Fatalf("stop_reason = %q, want tool_use. Output: %s", got, string(out))
|
|
}
|
|
if got := gjson.GetBytes(out, "usage.input_tokens").Int(); got != 3 {
|
|
t.Fatalf("input_tokens = %d, want 3. Output: %s", got, string(out))
|
|
}
|
|
}
|
|
|
|
func findClaudeEventPayload(events [][]byte, eventName string) []byte {
|
|
prefix := []byte("data:")
|
|
for _, event := range events {
|
|
if !bytes.Contains(event, []byte("event: "+eventName)) {
|
|
continue
|
|
}
|
|
for _, line := range bytes.Split(event, []byte("\n")) {
|
|
line = bytes.TrimSpace(line)
|
|
if bytes.HasPrefix(line, prefix) {
|
|
return bytes.TrimSpace(line[len(prefix):])
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|