Add OpenAI Responses API (/v1/responses) endpoint
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
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// --- Responses API request parsing & conversion ---
|
||||
|
||||
// ParseResponsesRequest converts a raw OpenAI Responses API (POST /v1/responses)
|
||||
// request body into a ChatCompletionRequest suitable for the upstream gateway.
|
||||
// It translates input→messages, instructions→system message, max_output_tokens→
|
||||
// max_tokens, and reshapes tools from the Responses flat format to the Chat
|
||||
// Completions nested {function:{…}} format.
|
||||
func ParseResponsesRequest(body []byte) (ChatCompletionRequest, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return ChatCompletionRequest{}, err
|
||||
}
|
||||
|
||||
var req ChatCompletionRequest
|
||||
if v, ok := raw["model"]; ok {
|
||||
_ = json.Unmarshal(v, &req.Model)
|
||||
}
|
||||
if v, ok := raw["stream"]; ok {
|
||||
_ = json.Unmarshal(v, &req.Stream)
|
||||
}
|
||||
|
||||
// Build messages from instructions + input.
|
||||
messages := make([]map[string]any, 0, 4)
|
||||
if v, ok := raw["instructions"]; ok {
|
||||
var s string
|
||||
if json.Unmarshal(v, &s) == nil && s != "" {
|
||||
messages = append(messages, map[string]any{"role": "system", "content": s})
|
||||
}
|
||||
}
|
||||
if v, ok := raw["input"]; ok {
|
||||
msgs, err := responsesInputToMessages(v)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
messages = append(messages, msgs...)
|
||||
}
|
||||
req.Messages = messages
|
||||
|
||||
// Extra: renamed + pass-through fields sent to upstream as-is.
|
||||
extra := map[string]json.RawMessage{}
|
||||
if v, ok := raw["max_output_tokens"]; ok {
|
||||
extra["max_tokens"] = v
|
||||
}
|
||||
for _, key := range []string{
|
||||
"temperature", "top_p", "top_k", "frequency_penalty",
|
||||
"presence_penalty", "stop", "seed", "user",
|
||||
} {
|
||||
if v, ok := raw[key]; ok {
|
||||
extra[key] = v
|
||||
}
|
||||
}
|
||||
if v, ok := raw["tools"]; ok {
|
||||
if translated, err := translateResponsesTools(v); err == nil {
|
||||
extra["tools"] = translated
|
||||
} else {
|
||||
extra["tools"] = v // fall back to pass-through
|
||||
}
|
||||
}
|
||||
if v, ok := raw["tool_choice"]; ok {
|
||||
extra["tool_choice"] = v
|
||||
}
|
||||
// Structured Outputs: Responses API uses text.format instead of
|
||||
// response_format. Translate text.format → response_format for the
|
||||
// upstream Chat Completions endpoint.
|
||||
if v, ok := raw["text"]; ok {
|
||||
if rf, err := translateTextFormat(v); err == nil {
|
||||
extra["response_format"] = rf
|
||||
}
|
||||
}
|
||||
req.Extra = extra
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// translateTextFormat converts the Responses API "text" field (containing a
|
||||
// "format" sub-object) into a Chat Completions response_format value.
|
||||
// - {text:{format:{type:"json_object"}}} → {type:"json_object"}
|
||||
// - {text:{format:{type:"json_schema",name,schema,strict}}} →
|
||||
// {type:"json_schema",json_schema:{name,schema,strict}}
|
||||
func translateTextFormat(textRaw json.RawMessage) (json.RawMessage, error) {
|
||||
var text struct {
|
||||
Format map[string]any `json:"format"`
|
||||
}
|
||||
if err := json.Unmarshal(textRaw, &text); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(text.Format) == 0 {
|
||||
return nil, errors.New("empty text.format")
|
||||
}
|
||||
t, _ := text.Format["type"].(string)
|
||||
switch t {
|
||||
case "json_object":
|
||||
return json.Marshal(map[string]any{"type": "json_object"})
|
||||
case "json_schema":
|
||||
js := map[string]any{}
|
||||
for _, k := range []string{"name", "schema", "strict"} {
|
||||
if v, ok := text.Format[k]; ok {
|
||||
js[k] = v
|
||||
}
|
||||
}
|
||||
return json.Marshal(map[string]any{"type": "json_schema", "json_schema": js})
|
||||
default:
|
||||
return json.Marshal(text.Format) // pass through unknown types
|
||||
}
|
||||
}
|
||||
|
||||
// ResponsesMeta holds fields from the Responses API request that should be
|
||||
// echoed back in the response object (the OpenAI SDK requires them).
|
||||
type ResponsesMeta struct {
|
||||
ParallelToolCalls any
|
||||
ToolChoice any
|
||||
Tools any
|
||||
Temperature any
|
||||
TopP any
|
||||
MaxOutputTokens any
|
||||
}
|
||||
|
||||
// ParseResponsesMeta extracts echo-back fields from the raw request body.
|
||||
func ParseResponsesMeta(body []byte) ResponsesMeta {
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(body, &m)
|
||||
meta := ResponsesMeta{
|
||||
ParallelToolCalls: false,
|
||||
ToolChoice: "auto",
|
||||
Tools: []any{},
|
||||
}
|
||||
if v, ok := m["parallel_tool_calls"]; ok {
|
||||
meta.ParallelToolCalls = v
|
||||
}
|
||||
if v, ok := m["tool_choice"]; ok {
|
||||
meta.ToolChoice = v
|
||||
}
|
||||
// Tools are echoed back in the Responses API flat format (not the
|
||||
// translated Chat Completions format).
|
||||
if v, ok := m["tools"]; ok {
|
||||
meta.Tools = v
|
||||
} else {
|
||||
meta.Tools = []any{}
|
||||
}
|
||||
if v, ok := m["temperature"]; ok {
|
||||
meta.Temperature = v
|
||||
}
|
||||
if v, ok := m["top_p"]; ok {
|
||||
meta.TopP = v
|
||||
}
|
||||
if v, ok := m["max_output_tokens"]; ok {
|
||||
meta.MaxOutputTokens = v
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
// responsesInputToMessages converts the Responses API "input" field (which may
|
||||
// be a plain string or an array of input items) into Chat Completions messages.
|
||||
func responsesInputToMessages(input json.RawMessage) ([]map[string]any, error) {
|
||||
// Case 1: input is a plain string.
|
||||
var s string
|
||||
if json.Unmarshal(input, &s) == nil {
|
||||
return []map[string]any{{"role": "user", "content": s}}, nil
|
||||
}
|
||||
|
||||
// Case 2: input is an array of items.
|
||||
var items []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(input, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
// Determine the type — most items are message-like with a role.
|
||||
var role string
|
||||
if v, ok := item["role"]; ok {
|
||||
_ = json.Unmarshal(v, &role)
|
||||
}
|
||||
var itemType string
|
||||
if v, ok := item["type"]; ok {
|
||||
_ = json.Unmarshal(v, &itemType)
|
||||
}
|
||||
|
||||
switch {
|
||||
case itemType == "function_call":
|
||||
// An assistant tool call from a previous turn.
|
||||
msg := map[string]any{"role": "assistant"}
|
||||
tc := map[string]any{"type": "function"}
|
||||
inner := map[string]any{}
|
||||
if v, ok := item["name"]; ok {
|
||||
var name string
|
||||
_ = json.Unmarshal(v, &name)
|
||||
inner["name"] = name
|
||||
}
|
||||
if v, ok := item["arguments"]; ok {
|
||||
var args string
|
||||
_ = json.Unmarshal(v, &args)
|
||||
inner["arguments"] = args
|
||||
}
|
||||
if v, ok := item["call_id"]; ok {
|
||||
var id string
|
||||
_ = json.Unmarshal(v, &id)
|
||||
tc["id"] = id
|
||||
}
|
||||
tc["function"] = inner
|
||||
msg["tool_calls"] = []any{tc}
|
||||
messages = append(messages, msg)
|
||||
case itemType == "function_call_output":
|
||||
// A tool result from a previous turn.
|
||||
msg := map[string]any{"role": "tool"}
|
||||
if v, ok := item["call_id"]; ok {
|
||||
var id string
|
||||
_ = json.Unmarshal(v, &id)
|
||||
msg["tool_call_id"] = id
|
||||
}
|
||||
if v, ok := item["output"]; ok {
|
||||
var out string
|
||||
_ = json.Unmarshal(v, &out)
|
||||
msg["content"] = out
|
||||
}
|
||||
messages = append(messages, msg)
|
||||
default:
|
||||
// Standard message item with role + content.
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
// "developer" maps to "system" for broad upstream compatibility.
|
||||
if role == "developer" {
|
||||
role = "system"
|
||||
}
|
||||
msg := map[string]any{"role": role}
|
||||
if v, ok := item["content"]; ok {
|
||||
msg["content"] = convertContentParts(v)
|
||||
} else {
|
||||
msg["content"] = ""
|
||||
}
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// convertContentParts converts a Responses API content field (string or array
|
||||
// of content parts) into the Chat Completions content format.
|
||||
func convertContentParts(raw json.RawMessage) any {
|
||||
// Content is a plain string.
|
||||
var s string
|
||||
if json.Unmarshal(raw, &s) == nil {
|
||||
return s
|
||||
}
|
||||
// Content is an array of parts.
|
||||
var parts []map[string]any
|
||||
if json.Unmarshal(raw, &parts) != nil {
|
||||
return string(raw) // fallback
|
||||
}
|
||||
result := make([]map[string]any, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
pt, _ := p["type"].(string)
|
||||
switch pt {
|
||||
case "input_text", "output_text", "text":
|
||||
result = append(result, map[string]any{"type": "text", "text": p["text"]})
|
||||
case "input_image":
|
||||
img := map[string]any{"type": "image_url"}
|
||||
if url, ok := p["image_url"]; ok {
|
||||
img["image_url"] = map[string]any{"url": url}
|
||||
} else if d, ok := p["image"]; ok {
|
||||
img["image_url"] = map[string]any{"url": d}
|
||||
}
|
||||
result = append(result, img)
|
||||
default:
|
||||
// Pass through unknown part types as-is.
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// translateResponsesTools converts tools from the Responses API flat format
|
||||
// to the Chat Completions nested {type:"function",function:{…}} format.
|
||||
func translateResponsesTools(raw json.RawMessage) (json.RawMessage, error) {
|
||||
var tools []map[string]any
|
||||
if err := json.Unmarshal(raw, &tools); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]map[string]any, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
tt, _ := t["type"].(string)
|
||||
if tt != "function" && tt != "" {
|
||||
// Non-function tool types (web_search, file_search, etc.) —
|
||||
// pass through as-is; upstream may or may not support them.
|
||||
out = append(out, t)
|
||||
continue
|
||||
}
|
||||
fn := map[string]any{}
|
||||
for _, key := range []string{"name", "description", "parameters", "strict"} {
|
||||
if v, ok := t[key]; ok {
|
||||
fn[key] = v
|
||||
}
|
||||
}
|
||||
out = append(out, map[string]any{"type": "function", "function": fn})
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// --- Responses API response building ---
|
||||
|
||||
// UsageToResponses converts a Chat Completions usage value (as decoded by
|
||||
// json.Unmarshal into any) to the Responses API usage field names
|
||||
// (input_tokens / output_tokens instead of prompt_tokens / completion_tokens).
|
||||
func UsageToResponses(v any) any {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return v
|
||||
}
|
||||
var m map[string]any
|
||||
if json.Unmarshal(b, &m) != nil {
|
||||
return v
|
||||
}
|
||||
out := map[string]any{}
|
||||
if pt, ok := m["prompt_tokens"]; ok {
|
||||
out["input_tokens"] = pt
|
||||
}
|
||||
if ct, ok := m["completion_tokens"]; ok {
|
||||
out["output_tokens"] = ct
|
||||
}
|
||||
if tt, ok := m["total_tokens"]; ok {
|
||||
out["total_tokens"] = tt
|
||||
}
|
||||
if d, ok := m["prompt_tokens_details"]; ok {
|
||||
out["input_tokens_details"] = d
|
||||
} else if d, ok := m["input_tokens_details"]; ok {
|
||||
out["input_tokens_details"] = d
|
||||
}
|
||||
if d, ok := m["completion_tokens_details"]; ok {
|
||||
out["output_tokens_details"] = d
|
||||
} else if d, ok := m["output_tokens_details"]; ok {
|
||||
out["output_tokens_details"] = d
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user