341 lines
12 KiB
Go
341 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// ============================================================
|
|
// Request Types (CreateResponseBody)
|
|
// ============================================================
|
|
|
|
// ResponseRequest models the OpenResponses CreateResponseBody.
|
|
type ResponseRequest struct {
|
|
Model string `json:"model"`
|
|
Input InputUnion `json:"input"`
|
|
Instructions *string `json:"instructions,omitempty"`
|
|
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
Stream bool `json:"stream,omitempty"`
|
|
PreviousResponseID *string `json:"previous_response_id,omitempty"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
|
|
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
|
|
TopLogprobs *int `json:"top_logprobs,omitempty"`
|
|
Truncation *string `json:"truncation,omitempty"`
|
|
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
|
Tools json.RawMessage `json:"tools,omitempty"`
|
|
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
|
Store *bool `json:"store,omitempty"`
|
|
Text json.RawMessage `json:"text,omitempty"`
|
|
Reasoning json.RawMessage `json:"reasoning,omitempty"`
|
|
Include []string `json:"include,omitempty"`
|
|
ServiceTier *string `json:"service_tier,omitempty"`
|
|
Background *bool `json:"background,omitempty"`
|
|
StreamOptions json.RawMessage `json:"stream_options,omitempty"`
|
|
MaxToolCalls *int `json:"max_tool_calls,omitempty"`
|
|
|
|
// Non-spec extension: allows client to select a specific provider.
|
|
Provider string `json:"provider,omitempty"`
|
|
}
|
|
|
|
// InputUnion handles the polymorphic "input" field: string or []InputItem.
|
|
type InputUnion struct {
|
|
String *string
|
|
Items []InputItem
|
|
}
|
|
|
|
func (u *InputUnion) UnmarshalJSON(data []byte) error {
|
|
if string(data) == "null" {
|
|
return nil
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err == nil {
|
|
u.String = &s
|
|
return nil
|
|
}
|
|
var items []InputItem
|
|
if err := json.Unmarshal(data, &items); err == nil {
|
|
u.Items = items
|
|
return nil
|
|
}
|
|
return fmt.Errorf("input must be a string or array of items")
|
|
}
|
|
|
|
func (u InputUnion) MarshalJSON() ([]byte, error) {
|
|
if u.String != nil {
|
|
return json.Marshal(*u.String)
|
|
}
|
|
if u.Items != nil {
|
|
return json.Marshal(u.Items)
|
|
}
|
|
return []byte("null"), nil
|
|
}
|
|
|
|
// InputItem is a discriminated union on "type".
|
|
// Valid types: message, item_reference, function_call, function_call_output, reasoning.
|
|
type InputItem struct {
|
|
Type string `json:"type"`
|
|
Role string `json:"role,omitempty"`
|
|
Content json.RawMessage `json:"content,omitempty"`
|
|
ID string `json:"id,omitempty"`
|
|
CallID string `json:"call_id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Arguments string `json:"arguments,omitempty"`
|
|
Output string `json:"output,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Internal Types (providers + conversation store)
|
|
// ============================================================
|
|
|
|
// Message is the normalized internal message representation.
|
|
type Message struct {
|
|
Role string `json:"role"`
|
|
Content []ContentBlock `json:"content"`
|
|
CallID string `json:"call_id,omitempty"` // for tool messages
|
|
}
|
|
|
|
// ContentBlock is a typed content element.
|
|
type ContentBlock struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
// NormalizeInput converts the request Input into messages for providers.
|
|
// Does NOT include instructions (the server prepends those separately).
|
|
func (r *ResponseRequest) NormalizeInput() []Message {
|
|
if r.Input.String != nil {
|
|
return []Message{{
|
|
Role: "user",
|
|
Content: []ContentBlock{{Type: "input_text", Text: *r.Input.String}},
|
|
}}
|
|
}
|
|
|
|
var msgs []Message
|
|
for _, item := range r.Input.Items {
|
|
switch item.Type {
|
|
case "message", "":
|
|
msg := Message{Role: item.Role}
|
|
if item.Content != nil {
|
|
var s string
|
|
if err := json.Unmarshal(item.Content, &s); err == nil {
|
|
contentType := "input_text"
|
|
if item.Role == "assistant" {
|
|
contentType = "output_text"
|
|
}
|
|
msg.Content = []ContentBlock{{Type: contentType, Text: s}}
|
|
} else {
|
|
var blocks []ContentBlock
|
|
_ = json.Unmarshal(item.Content, &blocks)
|
|
msg.Content = blocks
|
|
}
|
|
}
|
|
msgs = append(msgs, msg)
|
|
case "function_call_output":
|
|
msgs = append(msgs, Message{
|
|
Role: "tool",
|
|
Content: []ContentBlock{{Type: "input_text", Text: item.Output}},
|
|
CallID: item.CallID,
|
|
})
|
|
}
|
|
}
|
|
return msgs
|
|
}
|
|
|
|
// ============================================================
|
|
// Response Types (ResponseResource)
|
|
// ============================================================
|
|
|
|
// Response is the spec-compliant ResponseResource.
|
|
type Response struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
CompletedAt *int64 `json:"completed_at"`
|
|
Status string `json:"status"`
|
|
IncompleteDetails *IncompleteDetails `json:"incomplete_details"`
|
|
Model string `json:"model"`
|
|
PreviousResponseID *string `json:"previous_response_id"`
|
|
Instructions *string `json:"instructions"`
|
|
Output []OutputItem `json:"output"`
|
|
Error *ResponseError `json:"error"`
|
|
Tools json.RawMessage `json:"tools"`
|
|
ToolChoice json.RawMessage `json:"tool_choice"`
|
|
Truncation string `json:"truncation"`
|
|
ParallelToolCalls bool `json:"parallel_tool_calls"`
|
|
Text json.RawMessage `json:"text"`
|
|
TopP float64 `json:"top_p"`
|
|
PresencePenalty float64 `json:"presence_penalty"`
|
|
FrequencyPenalty float64 `json:"frequency_penalty"`
|
|
TopLogprobs int `json:"top_logprobs"`
|
|
Temperature float64 `json:"temperature"`
|
|
Reasoning json.RawMessage `json:"reasoning"`
|
|
Usage *Usage `json:"usage"`
|
|
MaxOutputTokens *int `json:"max_output_tokens"`
|
|
MaxToolCalls *int `json:"max_tool_calls"`
|
|
Store bool `json:"store"`
|
|
Background bool `json:"background"`
|
|
ServiceTier string `json:"service_tier"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
SafetyIdentifier *string `json:"safety_identifier"`
|
|
PromptCacheKey *string `json:"prompt_cache_key"`
|
|
|
|
// Non-spec extension
|
|
Provider string `json:"provider,omitempty"`
|
|
}
|
|
|
|
// OutputItem represents a typed item in the response output.
|
|
type OutputItem struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Status string `json:"status"`
|
|
Role string `json:"role,omitempty"`
|
|
Content []ContentPart `json:"content,omitempty"`
|
|
CallID string `json:"call_id,omitempty"` // for function_call
|
|
Name string `json:"name,omitempty"` // for function_call
|
|
Arguments string `json:"arguments,omitempty"` // for function_call
|
|
}
|
|
|
|
// ContentPart is a content block within an output item.
|
|
type ContentPart struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
Annotations []Annotation `json:"annotations"`
|
|
}
|
|
|
|
// Annotation on output text content.
|
|
type Annotation struct {
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// IncompleteDetails explains why a response is incomplete.
|
|
type IncompleteDetails struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// ResponseError describes an error in the response.
|
|
type ResponseError struct {
|
|
Type string `json:"type"`
|
|
Message string `json:"message"`
|
|
Code *string `json:"code"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Usage Types
|
|
// ============================================================
|
|
|
|
// Usage captures token accounting with sub-details.
|
|
type Usage struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
InputTokensDetails InputTokensDetails `json:"input_tokens_details"`
|
|
OutputTokensDetails OutputTokensDetails `json:"output_tokens_details"`
|
|
}
|
|
|
|
// InputTokensDetails breaks down input token usage.
|
|
type InputTokensDetails struct {
|
|
CachedTokens int `json:"cached_tokens"`
|
|
}
|
|
|
|
// OutputTokensDetails breaks down output token usage.
|
|
type OutputTokensDetails struct {
|
|
ReasoningTokens int `json:"reasoning_tokens"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Streaming Types
|
|
// ============================================================
|
|
|
|
// StreamEvent represents a single SSE event in the streaming response.
|
|
// Fields are selectively populated based on the event Type.
|
|
type StreamEvent struct {
|
|
Type string `json:"type"`
|
|
SequenceNumber int `json:"sequence_number"`
|
|
Response *Response `json:"response,omitempty"`
|
|
OutputIndex *int `json:"output_index,omitempty"`
|
|
Item *OutputItem `json:"item,omitempty"`
|
|
ItemID string `json:"item_id,omitempty"`
|
|
ContentIndex *int `json:"content_index,omitempty"`
|
|
Part *ContentPart `json:"part,omitempty"`
|
|
Delta string `json:"delta,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
Arguments string `json:"arguments,omitempty"` // for function_call_arguments.done
|
|
}
|
|
|
|
// ============================================================
|
|
// Provider Result Types (internal, not exposed via HTTP)
|
|
// ============================================================
|
|
|
|
// ProviderResult is returned by Provider.Generate.
|
|
type ProviderResult struct {
|
|
ID string
|
|
Model string
|
|
Text string
|
|
Usage Usage
|
|
ToolCalls []ToolCall
|
|
}
|
|
|
|
// ProviderStreamDelta is sent through the stream channel.
|
|
type ProviderStreamDelta struct {
|
|
ID string
|
|
Model string
|
|
Text string
|
|
Done bool
|
|
Usage *Usage
|
|
ToolCallDelta *ToolCallDelta
|
|
}
|
|
|
|
// ToolCall represents a function call from the model.
|
|
type ToolCall struct {
|
|
ID string
|
|
Name string
|
|
Arguments string // JSON string
|
|
}
|
|
|
|
// ToolCallDelta represents a streaming chunk of a tool call.
|
|
type ToolCallDelta struct {
|
|
Index int
|
|
ID string
|
|
Name string
|
|
Arguments string
|
|
}
|
|
|
|
// ============================================================
|
|
// Models Endpoint Types
|
|
// ============================================================
|
|
|
|
// ModelInfo describes a single model available through the gateway.
|
|
type ModelInfo struct {
|
|
ID string `json:"id"`
|
|
Provider string `json:"provider"`
|
|
}
|
|
|
|
// ModelsResponse is returned by GET /v1/models.
|
|
type ModelsResponse struct {
|
|
Object string `json:"object"`
|
|
Data []ModelInfo `json:"data"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Validation
|
|
// ============================================================
|
|
|
|
// Validate performs basic structural validation.
|
|
func (r *ResponseRequest) Validate() error {
|
|
if r == nil {
|
|
return errors.New("request is nil")
|
|
}
|
|
if r.Model == "" {
|
|
return errors.New("model is required")
|
|
}
|
|
if r.Input.String == nil && len(r.Input.Items) == 0 {
|
|
return errors.New("input is required")
|
|
}
|
|
return nil
|
|
}
|