Translate Responses API requests (input→messages, instructions→system,
max_output_tokens→max_tokens, text.format→response_format, flat tools→nested
{function:{…}}) to upstream chat/completions, then convert responses back to
Responses format (streaming SSE event lifecycle + non-streaming JSON).
Verified against OpenAI migration guide and Python SDK Response model:
- Echo back required fields parallel_tool_calls/tool_choice/tools
- Include content:[] in reasoning items, logprobs:[] in output_text parts
- Support function_call/function_call_output multi-turn input items
- Map usage fields prompt_tokens→input_tokens, completion_tokens→output_tokens
306 lines
9.0 KiB
Go
306 lines
9.0 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseResponsesRequest_StringInput(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":"hello world","stream":false}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if req.Model != "GLM-4.7" {
|
|
t.Fatalf("model = %q", req.Model)
|
|
}
|
|
if len(req.Messages) != 1 {
|
|
t.Fatalf("messages = %d items", len(req.Messages))
|
|
}
|
|
if req.Messages[0]["role"] != "user" {
|
|
t.Fatalf("role = %v", req.Messages[0]["role"])
|
|
}
|
|
if req.Messages[0]["content"] != "hello world" {
|
|
t.Fatalf("content = %v", req.Messages[0]["content"])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_Instructions(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","instructions":"be helpful","input":"hi"}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Messages) != 2 {
|
|
t.Fatalf("messages = %d items", len(req.Messages))
|
|
}
|
|
if req.Messages[0]["role"] != "system" {
|
|
t.Fatalf("first role = %v", req.Messages[0]["role"])
|
|
}
|
|
if req.Messages[0]["content"] != "be helpful" {
|
|
t.Fatalf("first content = %v", req.Messages[0]["content"])
|
|
}
|
|
if req.Messages[1]["role"] != "user" {
|
|
t.Fatalf("second role = %v", req.Messages[1]["role"])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_ArrayInput(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":[
|
|
{"role":"user","content":"hello"},
|
|
{"role":"assistant","content":"hi there"},
|
|
{"role":"user","content":"how are you?"}
|
|
]}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Messages) != 3 {
|
|
t.Fatalf("messages = %d items", len(req.Messages))
|
|
}
|
|
if req.Messages[0]["role"] != "user" || req.Messages[0]["content"] != "hello" {
|
|
t.Fatalf("msg[0] = %v", req.Messages[0])
|
|
}
|
|
if req.Messages[1]["role"] != "assistant" || req.Messages[1]["content"] != "hi there" {
|
|
t.Fatalf("msg[1] = %v", req.Messages[1])
|
|
}
|
|
if req.Messages[2]["role"] != "user" || req.Messages[2]["content"] != "how are you?" {
|
|
t.Fatalf("msg[2] = %v", req.Messages[2])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_ContentParts(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":[
|
|
{"role":"user","content":[
|
|
{"type":"input_text","text":"describe this"},
|
|
{"type":"input_image","image_url":"data:image/png;base64,abc"}
|
|
]}
|
|
]}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Messages) != 1 {
|
|
t.Fatalf("messages = %d items", len(req.Messages))
|
|
}
|
|
content, ok := req.Messages[0]["content"].([]map[string]any)
|
|
if !ok {
|
|
t.Fatalf("content type = %T", req.Messages[0]["content"])
|
|
}
|
|
if len(content) != 2 {
|
|
t.Fatalf("content parts = %d", len(content))
|
|
}
|
|
if content[0]["type"] != "text" || content[0]["text"] != "describe this" {
|
|
t.Fatalf("content[0] = %v", content[0])
|
|
}
|
|
if content[1]["type"] != "image_url" {
|
|
t.Fatalf("content[1] type = %v", content[1])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_MaxOutputTokens(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":"hi","max_output_tokens":500}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, ok := req.Extra["max_tokens"]; !ok {
|
|
t.Fatal("max_tokens not in Extra")
|
|
} else {
|
|
var n int
|
|
_ = json.Unmarshal(v, &n)
|
|
if n != 500 {
|
|
t.Fatalf("max_tokens = %d", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_Tools(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":"hi","tools":[
|
|
{"type":"function","name":"get_weather","description":"get weather","parameters":{"type":"object"}}
|
|
]}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, ok := req.Extra["tools"]
|
|
if !ok {
|
|
t.Fatal("tools not in Extra")
|
|
}
|
|
var tools []map[string]any
|
|
if err := json.Unmarshal(raw, &tools); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tools) != 1 {
|
|
t.Fatalf("tools = %d", len(tools))
|
|
}
|
|
if tools[0]["type"] != "function" {
|
|
t.Fatalf("tool type = %v", tools[0]["type"])
|
|
}
|
|
fn, ok := tools[0]["function"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("function type = %T", tools[0]["function"])
|
|
}
|
|
if fn["name"] != "get_weather" {
|
|
t.Fatalf("function name = %v", fn["name"])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_FunctionCallInput(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":[
|
|
{"role":"user","content":"what's the weather?"},
|
|
{"type":"function_call","call_id":"call_123","name":"get_weather","arguments":"{\"city\":\"NYC\"}"},
|
|
{"type":"function_call_output","call_id":"call_123","output":"sunny 72F"}
|
|
]}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Messages) != 3 {
|
|
t.Fatalf("messages = %d items", len(req.Messages))
|
|
}
|
|
// First message is user text
|
|
if req.Messages[0]["role"] != "user" {
|
|
t.Fatalf("msg[0] role = %v", req.Messages[0]["role"])
|
|
}
|
|
// Second message is assistant with tool_calls
|
|
if req.Messages[1]["role"] != "assistant" {
|
|
t.Fatalf("msg[1] role = %v", req.Messages[1]["role"])
|
|
}
|
|
tcs, ok := req.Messages[1]["tool_calls"].([]any)
|
|
if !ok || len(tcs) != 1 {
|
|
t.Fatalf("msg[1] tool_calls = %v", req.Messages[1]["tool_calls"])
|
|
}
|
|
tc, _ := tcs[0].(map[string]any)
|
|
if tc["id"] != "call_123" {
|
|
t.Fatalf("tool call id = %v", tc["id"])
|
|
}
|
|
fn, _ := tc["function"].(map[string]any)
|
|
if fn["name"] != "get_weather" {
|
|
t.Fatalf("function name = %v", fn["name"])
|
|
}
|
|
// Third message is tool result
|
|
if req.Messages[2]["role"] != "tool" {
|
|
t.Fatalf("msg[2] role = %v", req.Messages[2]["role"])
|
|
}
|
|
if req.Messages[2]["tool_call_id"] != "call_123" {
|
|
t.Fatalf("msg[2] tool_call_id = %v", req.Messages[2]["tool_call_id"])
|
|
}
|
|
if req.Messages[2]["content"] != "sunny 72F" {
|
|
t.Fatalf("msg[2] content = %v", req.Messages[2]["content"])
|
|
}
|
|
}
|
|
|
|
func TestUsageToResponses(t *testing.T) {
|
|
usage := map[string]any{
|
|
"prompt_tokens": 10,
|
|
"completion_tokens": 20,
|
|
"total_tokens": 30,
|
|
"prompt_tokens_details": map[string]any{"cached_tokens": 4},
|
|
"completion_tokens_details": map[string]any{"reasoning_tokens": 5},
|
|
}
|
|
result := UsageToResponses(usage)
|
|
m, ok := result.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("result type = %T", result)
|
|
}
|
|
if m["input_tokens"] != float64(10) {
|
|
t.Fatalf("input_tokens = %v", m["input_tokens"])
|
|
}
|
|
if m["output_tokens"] != float64(20) {
|
|
t.Fatalf("output_tokens = %v", m["output_tokens"])
|
|
}
|
|
if m["total_tokens"] != float64(30) {
|
|
t.Fatalf("total_tokens = %v", m["total_tokens"])
|
|
}
|
|
if d, ok := m["input_tokens_details"].(map[string]any); !ok || d["cached_tokens"] != float64(4) {
|
|
t.Fatalf("input_tokens_details = %v", m["input_tokens_details"])
|
|
}
|
|
if d, ok := m["output_tokens_details"].(map[string]any); !ok || d["reasoning_tokens"] != float64(5) {
|
|
t.Fatalf("output_tokens_details = %v", m["output_tokens_details"])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_TextFormat(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":"Jane, 54","text":{"format":{"type":"json_schema","name":"person","strict":true,"schema":{"type":"object"}}}}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rf, ok := req.Extra["response_format"]
|
|
if !ok {
|
|
t.Fatal("response_format not in Extra")
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(rf, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m["type"] != "json_schema" {
|
|
t.Fatalf("type = %v", m["type"])
|
|
}
|
|
js, ok := m["json_schema"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("json_schema = %v", m["json_schema"])
|
|
}
|
|
if js["name"] != "person" {
|
|
t.Fatalf("name = %v", js["name"])
|
|
}
|
|
if js["strict"] != true {
|
|
t.Fatalf("strict = %v", js["strict"])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesRequest_TextFormatJsonObject(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":"hi","text":{"format":{"type":"json_object"}}}`
|
|
req, err := ParseResponsesRequest([]byte(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rf, ok := req.Extra["response_format"]
|
|
if !ok {
|
|
t.Fatal("response_format not in Extra")
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(rf, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m["type"] != "json_object" {
|
|
t.Fatalf("type = %v", m["type"])
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesMeta_Defaults(t *testing.T) {
|
|
meta := ParseResponsesMeta([]byte(`{"model":"GLM-4.7","input":"hi"}`))
|
|
if meta.ParallelToolCalls != false {
|
|
t.Fatalf("parallel_tool_calls = %v", meta.ParallelToolCalls)
|
|
}
|
|
if meta.ToolChoice != "auto" {
|
|
t.Fatalf("tool_choice = %v", meta.ToolChoice)
|
|
}
|
|
tools, ok := meta.Tools.([]any)
|
|
if !ok || len(tools) != 0 {
|
|
t.Fatalf("tools = %v", meta.Tools)
|
|
}
|
|
}
|
|
|
|
func TestParseResponsesMeta_EchoBack(t *testing.T) {
|
|
body := `{"model":"GLM-4.7","input":"hi","parallel_tool_calls":true,"tool_choice":"required","tools":[{"type":"function","name":"get_weather"}],"temperature":0.7,"max_output_tokens":500}`
|
|
meta := ParseResponsesMeta([]byte(body))
|
|
if meta.ParallelToolCalls != true {
|
|
t.Fatalf("parallel_tool_calls = %v", meta.ParallelToolCalls)
|
|
}
|
|
if meta.ToolChoice != "required" {
|
|
t.Fatalf("tool_choice = %v", meta.ToolChoice)
|
|
}
|
|
tools, ok := meta.Tools.([]any)
|
|
if !ok || len(tools) != 1 {
|
|
t.Fatalf("tools = %v", meta.Tools)
|
|
}
|
|
if meta.Temperature != 0.7 {
|
|
t.Fatalf("temperature = %v", meta.Temperature)
|
|
}
|
|
if meta.MaxOutputTokens != float64(500) {
|
|
t.Fatalf("max_output_tokens = %v", meta.MaxOutputTokens)
|
|
}
|
|
}
|