Files
CrossRelay/internal/responses/types.go
T

178 lines
5.4 KiB
Go

// Package responses describes the OpenAI Responses API that Codex
// speaks and converts it to and from Chat Completions.
package responses
import (
"bytes"
"encoding/json"
"strings"
)
// Request is the Responses API request body. Only the fields CrossRelay
// understands are modelled; everything else is ignored.
type Request struct {
Model string `json:"model"`
Instructions string `json:"instructions,omitempty"`
Input json.RawMessage `json:"input"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// Tool is one tool definition. Type is "function" for regular tools;
// "custom" and "local_shell" are freeform forms Codex may send.
type Tool struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters json.RawMessage `json:"parameters,omitempty"`
}
// Item is one entry of the input array. Only the fields belonging to
// Type are populated.
type Item struct {
Type string `json:"type"` // message | function_call | function_call_output | reasoning
Role string `json:"role,omitempty"`
// message
Content json.RawMessage `json:"content,omitempty"` // string or []ContentPart
// function_call
CallID string `json:"call_id,omitempty"`
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
// function_call_output
Output json.RawMessage `json:"output,omitempty"`
// reasoning
Summary json.RawMessage `json:"summary,omitempty"`
}
// ContentPart is one part of a message item's content.
type ContentPart struct {
Type string `json:"type"` // input_text | output_text | input_image
Text string `json:"text,omitempty"`
// input_image
ImageURL string `json:"image_url,omitempty"`
}
// Response is the Responses API response body.
type Response struct {
ID string `json:"id"`
Object string `json:"object"`
CreatedAt int64 `json:"created_at"`
Status string `json:"status"`
Model string `json:"model"`
Output []OutItem `json:"output"`
OutputText string `json:"output_text,omitempty"`
Usage *Usage `json:"usage,omitempty"`
IncompleteDetails *Incomplete `json:"incomplete_details,omitempty"`
Error *RespError `json:"error,omitempty"`
}
// OutItem is one output entry: an assistant message or a function call.
type OutItem struct {
ID string `json:"id,omitempty"`
Type string `json:"type"` // message | function_call
Role string `json:"role,omitempty"`
Status string `json:"status,omitempty"`
Content []ContentPart `json:"content,omitempty"`
CallID string `json:"call_id,omitempty"`
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
}
// Usage reports token counts.
type Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalTokens int `json:"total_tokens"`
}
// IncompleteDetails explains a non-completed response.
type Incomplete struct {
Reason string `json:"reason"`
}
// RespError is the error payload of a failed response.
type RespError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// ErrorResponse is the Responses API error envelope.
type ErrorResponse struct {
Error Detail `json:"error"`
}
// Detail is the error payload.
type Detail struct {
Type string `json:"type"`
Message string `json:"message"`
}
// ParseInput decodes the input field, which is either a plain string
// or an array of items.
func ParseInput(raw json.RawMessage) ([]Item, error) {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || string(trimmed) == "null" {
return nil, nil
}
if trimmed[0] == '"' {
var s string
if err := json.Unmarshal(trimmed, &s); err != nil {
return nil, err
}
if s == "" {
return nil, nil
}
return []Item{{Type: "message", Role: "user", Content: trimmed}}, nil
}
var items []Item
if err := json.Unmarshal(trimmed, &items); err != nil {
return nil, err
}
return items, nil
}
// ParseContent decodes a message item's content field into parts. A
// plain string becomes a single text part.
func ParseContent(raw json.RawMessage) ([]ContentPart, error) {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || string(trimmed) == "null" {
return nil, nil
}
if trimmed[0] == '"' {
var s string
if err := json.Unmarshal(trimmed, &s); err != nil {
return nil, err
}
if s == "" {
return nil, nil
}
return []ContentPart{{Type: "input_text", Text: s}}, nil
}
var parts []ContentPart
if err := json.Unmarshal(trimmed, &parts); err != nil {
return nil, err
}
return parts, nil
}
// OutputText flattens a function_call_output output field, which is
// either a plain string or an array of text parts.
func OutputText(raw json.RawMessage) string {
parts, err := ParseContent(raw)
if err != nil {
return ""
}
var sb strings.Builder
for _, p := range parts {
switch p.Type {
case "output_text", "input_text", "text":
sb.WriteString(p.Text)
}
}
return sb.String()
}