178 lines
5.4 KiB
Go
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()
|
|
}
|