mirror of
https://github.com/ollama/ollama.git
synced 2026-03-11 17:34:04 -05:00
openai: tweak v1/responses to conform better (#13736)
* openai: tweak v1/responses to conform better * openai: provide better error for image URLs * lint
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@@ -441,6 +442,7 @@ type ResponsesWriter struct {
|
|||||||
stream bool
|
stream bool
|
||||||
responseID string
|
responseID string
|
||||||
itemID string
|
itemID string
|
||||||
|
request openai.ResponsesRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *ResponsesWriter) writeEvent(eventType string, data any) error {
|
func (w *ResponsesWriter) writeEvent(eventType string, data any) error {
|
||||||
@@ -478,7 +480,9 @@ func (w *ResponsesWriter) writeResponse(data []byte) (int, error) {
|
|||||||
|
|
||||||
// Non-streaming response
|
// Non-streaming response
|
||||||
w.ResponseWriter.Header().Set("Content-Type", "application/json")
|
w.ResponseWriter.Header().Set("Content-Type", "application/json")
|
||||||
response := openai.ToResponse(w.model, w.responseID, w.itemID, chatResponse)
|
response := openai.ToResponse(w.model, w.responseID, w.itemID, chatResponse, w.request)
|
||||||
|
completedAt := time.Now().Unix()
|
||||||
|
response.CompletedAt = &completedAt
|
||||||
return len(data), json.NewEncoder(w.ResponseWriter).Encode(response)
|
return len(data), json.NewEncoder(w.ResponseWriter).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,11 +527,12 @@ func ResponsesMiddleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
w := &ResponsesWriter{
|
w := &ResponsesWriter{
|
||||||
BaseWriter: BaseWriter{ResponseWriter: c.Writer},
|
BaseWriter: BaseWriter{ResponseWriter: c.Writer},
|
||||||
converter: openai.NewResponsesStreamConverter(responseID, itemID, req.Model),
|
converter: openai.NewResponsesStreamConverter(responseID, itemID, req.Model, req),
|
||||||
model: req.Model,
|
model: req.Model,
|
||||||
stream: streamRequested,
|
stream: streamRequested,
|
||||||
responseID: responseID,
|
responseID: responseID,
|
||||||
itemID: itemID,
|
itemID: itemID,
|
||||||
|
request: req,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set headers based on streaming mode
|
// Set headers based on streaming mode
|
||||||
|
|||||||
@@ -630,6 +630,10 @@ func nameFromToolCallID(messages []Message, toolCallID string) string {
|
|||||||
|
|
||||||
// decodeImageURL decodes a base64 data URI into raw image bytes.
|
// decodeImageURL decodes a base64 data URI into raw image bytes.
|
||||||
func decodeImageURL(url string) (api.ImageData, error) {
|
func decodeImageURL(url string) (api.ImageData, error) {
|
||||||
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
|
return nil, errors.New("image URLs are not currently supported, please use base64 encoded data instead")
|
||||||
|
}
|
||||||
|
|
||||||
types := []string{"jpeg", "jpg", "png", "webp"}
|
types := []string{"jpeg", "jpg", "png", "webp"}
|
||||||
|
|
||||||
// Support blank mime type to match /api/chat's behavior of taking just unadorned base64
|
// Support blank mime type to match /api/chat's behavior of taking just unadorned base64
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
@@ -265,9 +266,9 @@ type ResponsesText struct {
|
|||||||
type ResponsesTool struct {
|
type ResponsesTool struct {
|
||||||
Type string `json:"type"` // "function"
|
Type string `json:"type"` // "function"
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description,omitempty"`
|
Description *string `json:"description"` // nullable but required
|
||||||
Strict bool `json:"strict,omitempty"`
|
Strict *bool `json:"strict"` // nullable but required
|
||||||
Parameters map[string]any `json:"parameters,omitempty"`
|
Parameters map[string]any `json:"parameters"` // nullable but required
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponsesRequest struct {
|
type ResponsesRequest struct {
|
||||||
@@ -475,11 +476,16 @@ func convertTool(t ResponsesTool) (api.Tool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var description string
|
||||||
|
if t.Description != nil {
|
||||||
|
description = *t.Description
|
||||||
|
}
|
||||||
|
|
||||||
return api.Tool{
|
return api.Tool{
|
||||||
Type: t.Type,
|
Type: t.Type,
|
||||||
Function: api.ToolFunction{
|
Function: api.ToolFunction{
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Description: t.Description,
|
Description: description,
|
||||||
Parameters: params,
|
Parameters: params,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -516,17 +522,60 @@ func convertInputMessage(m ResponsesInputMessage) (api.Message, error) {
|
|||||||
|
|
||||||
// Response types for the Responses API
|
// Response types for the Responses API
|
||||||
|
|
||||||
|
// ResponsesTextField represents the text output configuration in the response.
|
||||||
|
type ResponsesTextField struct {
|
||||||
|
Format ResponsesTextFormat `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponsesReasoningOutput represents reasoning configuration in the response.
|
||||||
|
type ResponsesReasoningOutput struct {
|
||||||
|
Effort *string `json:"effort,omitempty"`
|
||||||
|
Summary *string `json:"summary,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponsesError represents an error in the response.
|
||||||
|
type ResponsesError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponsesIncompleteDetails represents details about why a response was incomplete.
|
||||||
|
type ResponsesIncompleteDetails struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
type ResponsesResponse struct {
|
type ResponsesResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
Status string `json:"status"`
|
CompletedAt *int64 `json:"completed_at"`
|
||||||
Model string `json:"model"`
|
Status string `json:"status"`
|
||||||
Output []ResponsesOutputItem `json:"output"`
|
IncompleteDetails *ResponsesIncompleteDetails `json:"incomplete_details"`
|
||||||
Usage *ResponsesUsage `json:"usage,omitempty"`
|
Model string `json:"model"`
|
||||||
// TODO(drifkin): add `temperature` and `top_p` to the response, but this
|
PreviousResponseID *string `json:"previous_response_id"`
|
||||||
// requires additional plumbing to find the effective values since the
|
Instructions *string `json:"instructions"`
|
||||||
// defaults can come from the model or the request
|
Output []ResponsesOutputItem `json:"output"`
|
||||||
|
Error *ResponsesError `json:"error"`
|
||||||
|
Tools []ResponsesTool `json:"tools"`
|
||||||
|
ToolChoice any `json:"tool_choice"`
|
||||||
|
Truncation string `json:"truncation"`
|
||||||
|
ParallelToolCalls bool `json:"parallel_tool_calls"`
|
||||||
|
Text ResponsesTextField `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 *ResponsesReasoningOutput `json:"reasoning"`
|
||||||
|
Usage *ResponsesUsage `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]any `json:"metadata"`
|
||||||
|
SafetyIdentifier *string `json:"safety_identifier"`
|
||||||
|
PromptCacheKey *string `json:"prompt_cache_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponsesOutputItem struct {
|
type ResponsesOutputItem struct {
|
||||||
@@ -550,18 +599,39 @@ type ResponsesReasoningSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResponsesOutputContent struct {
|
type ResponsesOutputContent struct {
|
||||||
Type string `json:"type"` // "output_text"
|
Type string `json:"type"` // "output_text"
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
Annotations []any `json:"annotations"`
|
||||||
|
Logprobs []any `json:"logprobs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponsesInputTokensDetails struct {
|
||||||
|
CachedTokens int `json:"cached_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponsesOutputTokensDetails struct {
|
||||||
|
ReasoningTokens int `json:"reasoning_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponsesUsage struct {
|
type ResponsesUsage struct {
|
||||||
InputTokens int `json:"input_tokens"`
|
InputTokens int `json:"input_tokens"`
|
||||||
OutputTokens int `json:"output_tokens"`
|
OutputTokens int `json:"output_tokens"`
|
||||||
TotalTokens int `json:"total_tokens"`
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
InputTokensDetails ResponsesInputTokensDetails `json:"input_tokens_details"`
|
||||||
|
OutputTokensDetails ResponsesOutputTokensDetails `json:"output_tokens_details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToResponse converts an api.ChatResponse to a Responses API response
|
// derefFloat64 returns the value of a float64 pointer, or a default if nil.
|
||||||
func ToResponse(model, responseID, itemID string, chatResponse api.ChatResponse) ResponsesResponse {
|
func derefFloat64(p *float64, def float64) float64 {
|
||||||
|
if p != nil {
|
||||||
|
return *p
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToResponse converts an api.ChatResponse to a Responses API response.
|
||||||
|
// The request is used to echo back request parameters in the response.
|
||||||
|
func ToResponse(model, responseID, itemID string, chatResponse api.ChatResponse, request ResponsesRequest) ResponsesResponse {
|
||||||
var output []ResponsesOutputItem
|
var output []ResponsesOutputItem
|
||||||
|
|
||||||
// Add reasoning item if thinking is present
|
// Add reasoning item if thinking is present
|
||||||
@@ -585,6 +655,7 @@ func ToResponse(model, responseID, itemID string, chatResponse api.ChatResponse)
|
|||||||
output = append(output, ResponsesOutputItem{
|
output = append(output, ResponsesOutputItem{
|
||||||
ID: fmt.Sprintf("fc_%s_%d", responseID, i),
|
ID: fmt.Sprintf("fc_%s_%d", responseID, i),
|
||||||
Type: "function_call",
|
Type: "function_call",
|
||||||
|
Status: "completed",
|
||||||
CallID: tc.ID,
|
CallID: tc.ID,
|
||||||
Name: tc.Function.Name,
|
Name: tc.Function.Name,
|
||||||
Arguments: tc.Function.Arguments,
|
Arguments: tc.Function.Arguments,
|
||||||
@@ -598,25 +669,90 @@ func ToResponse(model, responseID, itemID string, chatResponse api.ChatResponse)
|
|||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: []ResponsesOutputContent{
|
Content: []ResponsesOutputContent{
|
||||||
{
|
{
|
||||||
Type: "output_text",
|
Type: "output_text",
|
||||||
Text: chatResponse.Message.Content,
|
Text: chatResponse.Message.Content,
|
||||||
|
Annotations: []any{},
|
||||||
|
Logprobs: []any{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var instructions *string
|
||||||
|
if request.Instructions != "" {
|
||||||
|
instructions = &request.Instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build truncation with default
|
||||||
|
truncation := "disabled"
|
||||||
|
if request.Truncation != nil {
|
||||||
|
truncation = *request.Truncation
|
||||||
|
}
|
||||||
|
|
||||||
|
tools := request.Tools
|
||||||
|
if tools == nil {
|
||||||
|
tools = []ResponsesTool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
text := ResponsesTextField{
|
||||||
|
Format: ResponsesTextFormat{Type: "text"},
|
||||||
|
}
|
||||||
|
if request.Text != nil && request.Text.Format != nil {
|
||||||
|
text.Format = *request.Text.Format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build reasoning output from request
|
||||||
|
var reasoning *ResponsesReasoningOutput
|
||||||
|
if request.Reasoning.Effort != "" || request.Reasoning.Summary != "" {
|
||||||
|
reasoning = &ResponsesReasoningOutput{}
|
||||||
|
if request.Reasoning.Effort != "" {
|
||||||
|
reasoning.Effort = &request.Reasoning.Effort
|
||||||
|
}
|
||||||
|
if request.Reasoning.Summary != "" {
|
||||||
|
reasoning.Summary = &request.Reasoning.Summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ResponsesResponse{
|
return ResponsesResponse{
|
||||||
ID: responseID,
|
ID: responseID,
|
||||||
Object: "response",
|
Object: "response",
|
||||||
CreatedAt: chatResponse.CreatedAt.Unix(),
|
CreatedAt: chatResponse.CreatedAt.Unix(),
|
||||||
Status: "completed",
|
CompletedAt: nil, // Set by middleware when writing final response
|
||||||
Model: model,
|
Status: "completed",
|
||||||
Output: output,
|
IncompleteDetails: nil, // Only populated if response incomplete
|
||||||
|
Model: model,
|
||||||
|
PreviousResponseID: nil, // Not supported
|
||||||
|
Instructions: instructions,
|
||||||
|
Output: output,
|
||||||
|
Error: nil, // Only populated on failure
|
||||||
|
Tools: tools,
|
||||||
|
ToolChoice: "auto", // Default value
|
||||||
|
Truncation: truncation,
|
||||||
|
ParallelToolCalls: true, // Default value
|
||||||
|
Text: text,
|
||||||
|
TopP: derefFloat64(request.TopP, 1.0),
|
||||||
|
PresencePenalty: 0, // Default value
|
||||||
|
FrequencyPenalty: 0, // Default value
|
||||||
|
TopLogprobs: 0, // Default value
|
||||||
|
Temperature: derefFloat64(request.Temperature, 1.0),
|
||||||
|
Reasoning: reasoning,
|
||||||
Usage: &ResponsesUsage{
|
Usage: &ResponsesUsage{
|
||||||
InputTokens: chatResponse.PromptEvalCount,
|
InputTokens: chatResponse.PromptEvalCount,
|
||||||
OutputTokens: chatResponse.EvalCount,
|
OutputTokens: chatResponse.EvalCount,
|
||||||
TotalTokens: chatResponse.PromptEvalCount + chatResponse.EvalCount,
|
TotalTokens: chatResponse.PromptEvalCount + chatResponse.EvalCount,
|
||||||
|
// TODO(drifkin): wire through the actual values
|
||||||
|
InputTokensDetails: ResponsesInputTokensDetails{CachedTokens: 0},
|
||||||
|
// TODO(drifkin): wire through the actual values
|
||||||
|
OutputTokensDetails: ResponsesOutputTokensDetails{ReasoningTokens: 0},
|
||||||
},
|
},
|
||||||
|
MaxOutputTokens: request.MaxOutputTokens,
|
||||||
|
MaxToolCalls: nil, // Not supported
|
||||||
|
Store: false, // We don't store responses
|
||||||
|
Background: request.Background,
|
||||||
|
ServiceTier: "default", // Default value
|
||||||
|
Metadata: map[string]any{},
|
||||||
|
SafetyIdentifier: nil, // Not supported
|
||||||
|
PromptCacheKey: nil, // Not supported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,6 +772,7 @@ type ResponsesStreamConverter struct {
|
|||||||
responseID string
|
responseID string
|
||||||
itemID string
|
itemID string
|
||||||
model string
|
model string
|
||||||
|
request ResponsesRequest
|
||||||
|
|
||||||
// State tracking (mutated across Process calls)
|
// State tracking (mutated across Process calls)
|
||||||
firstWrite bool
|
firstWrite bool
|
||||||
@@ -668,11 +805,12 @@ func (c *ResponsesStreamConverter) newEvent(eventType string, data map[string]an
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewResponsesStreamConverter creates a new converter with the given configuration.
|
// NewResponsesStreamConverter creates a new converter with the given configuration.
|
||||||
func NewResponsesStreamConverter(responseID, itemID, model string) *ResponsesStreamConverter {
|
func NewResponsesStreamConverter(responseID, itemID, model string, request ResponsesRequest) *ResponsesStreamConverter {
|
||||||
return &ResponsesStreamConverter{
|
return &ResponsesStreamConverter{
|
||||||
responseID: responseID,
|
responseID: responseID,
|
||||||
itemID: itemID,
|
itemID: itemID,
|
||||||
model: model,
|
model: model,
|
||||||
|
request: request,
|
||||||
firstWrite: true,
|
firstWrite: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,25 +855,120 @@ func (c *ResponsesStreamConverter) Process(r api.ChatResponse) []ResponsesStream
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildResponseObject creates a full response object with all required fields for streaming events.
|
||||||
|
func (c *ResponsesStreamConverter) buildResponseObject(status string, output []any, usage map[string]any) map[string]any {
|
||||||
|
var instructions any = nil
|
||||||
|
if c.request.Instructions != "" {
|
||||||
|
instructions = c.request.Instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
truncation := "disabled"
|
||||||
|
if c.request.Truncation != nil {
|
||||||
|
truncation = *c.request.Truncation
|
||||||
|
}
|
||||||
|
|
||||||
|
var tools []any
|
||||||
|
if c.request.Tools != nil {
|
||||||
|
for _, t := range c.request.Tools {
|
||||||
|
tools = append(tools, map[string]any{
|
||||||
|
"type": t.Type,
|
||||||
|
"name": t.Name,
|
||||||
|
"description": t.Description,
|
||||||
|
"strict": t.Strict,
|
||||||
|
"parameters": t.Parameters,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tools == nil {
|
||||||
|
tools = []any{}
|
||||||
|
}
|
||||||
|
|
||||||
|
textFormat := map[string]any{"type": "text"}
|
||||||
|
if c.request.Text != nil && c.request.Text.Format != nil {
|
||||||
|
textFormat = map[string]any{
|
||||||
|
"type": c.request.Text.Format.Type,
|
||||||
|
}
|
||||||
|
if c.request.Text.Format.Name != "" {
|
||||||
|
textFormat["name"] = c.request.Text.Format.Name
|
||||||
|
}
|
||||||
|
if c.request.Text.Format.Schema != nil {
|
||||||
|
textFormat["schema"] = c.request.Text.Format.Schema
|
||||||
|
}
|
||||||
|
if c.request.Text.Format.Strict != nil {
|
||||||
|
textFormat["strict"] = *c.request.Text.Format.Strict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reasoning any = nil
|
||||||
|
if c.request.Reasoning.Effort != "" || c.request.Reasoning.Summary != "" {
|
||||||
|
r := map[string]any{}
|
||||||
|
if c.request.Reasoning.Effort != "" {
|
||||||
|
r["effort"] = c.request.Reasoning.Effort
|
||||||
|
} else {
|
||||||
|
r["effort"] = nil
|
||||||
|
}
|
||||||
|
if c.request.Reasoning.Summary != "" {
|
||||||
|
r["summary"] = c.request.Reasoning.Summary
|
||||||
|
} else {
|
||||||
|
r["summary"] = nil
|
||||||
|
}
|
||||||
|
reasoning = r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build top_p and temperature with defaults
|
||||||
|
topP := 1.0
|
||||||
|
if c.request.TopP != nil {
|
||||||
|
topP = *c.request.TopP
|
||||||
|
}
|
||||||
|
temperature := 1.0
|
||||||
|
if c.request.Temperature != nil {
|
||||||
|
temperature = *c.request.Temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"id": c.responseID,
|
||||||
|
"object": "response",
|
||||||
|
"created_at": time.Now().Unix(),
|
||||||
|
"completed_at": nil,
|
||||||
|
"status": status,
|
||||||
|
"incomplete_details": nil,
|
||||||
|
"model": c.model,
|
||||||
|
"previous_response_id": nil,
|
||||||
|
"instructions": instructions,
|
||||||
|
"output": output,
|
||||||
|
"error": nil,
|
||||||
|
"tools": tools,
|
||||||
|
"tool_choice": "auto",
|
||||||
|
"truncation": truncation,
|
||||||
|
"parallel_tool_calls": true,
|
||||||
|
"text": map[string]any{"format": textFormat},
|
||||||
|
"top_p": topP,
|
||||||
|
"presence_penalty": 0,
|
||||||
|
"frequency_penalty": 0,
|
||||||
|
"top_logprobs": 0,
|
||||||
|
"temperature": temperature,
|
||||||
|
"reasoning": reasoning,
|
||||||
|
"usage": usage,
|
||||||
|
"max_output_tokens": c.request.MaxOutputTokens,
|
||||||
|
"max_tool_calls": nil,
|
||||||
|
"store": false,
|
||||||
|
"background": c.request.Background,
|
||||||
|
"service_tier": "default",
|
||||||
|
"metadata": map[string]any{},
|
||||||
|
"safety_identifier": nil,
|
||||||
|
"prompt_cache_key": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ResponsesStreamConverter) createResponseCreatedEvent() ResponsesStreamEvent {
|
func (c *ResponsesStreamConverter) createResponseCreatedEvent() ResponsesStreamEvent {
|
||||||
return c.newEvent("response.created", map[string]any{
|
return c.newEvent("response.created", map[string]any{
|
||||||
"response": map[string]any{
|
"response": c.buildResponseObject("in_progress", []any{}, nil),
|
||||||
"id": c.responseID,
|
|
||||||
"object": "response",
|
|
||||||
"status": "in_progress",
|
|
||||||
"output": []any{},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ResponsesStreamConverter) createResponseInProgressEvent() ResponsesStreamEvent {
|
func (c *ResponsesStreamConverter) createResponseInProgressEvent() ResponsesStreamEvent {
|
||||||
return c.newEvent("response.in_progress", map[string]any{
|
return c.newEvent("response.in_progress", map[string]any{
|
||||||
"response": map[string]any{
|
"response": c.buildResponseObject("in_progress", []any{}, nil),
|
||||||
"id": c.responseID,
|
|
||||||
"object": "response",
|
|
||||||
"status": "in_progress",
|
|
||||||
"output": []any{},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,9 +995,10 @@ func (c *ResponsesStreamConverter) processThinking(thinking string) []ResponsesS
|
|||||||
|
|
||||||
// Emit delta
|
// Emit delta
|
||||||
events = append(events, c.newEvent("response.reasoning_summary_text.delta", map[string]any{
|
events = append(events, c.newEvent("response.reasoning_summary_text.delta", map[string]any{
|
||||||
"item_id": c.reasoningItemID,
|
"item_id": c.reasoningItemID,
|
||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
"delta": thinking,
|
"summary_index": 0,
|
||||||
|
"delta": thinking,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// TODO(drifkin): consider adding
|
// TODO(drifkin): consider adding
|
||||||
@@ -783,9 +1017,10 @@ func (c *ResponsesStreamConverter) finishReasoning() []ResponsesStreamEvent {
|
|||||||
|
|
||||||
events := []ResponsesStreamEvent{
|
events := []ResponsesStreamEvent{
|
||||||
c.newEvent("response.reasoning_summary_text.done", map[string]any{
|
c.newEvent("response.reasoning_summary_text.done", map[string]any{
|
||||||
"item_id": c.reasoningItemID,
|
"item_id": c.reasoningItemID,
|
||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
"text": c.accumulatedThinking,
|
"summary_index": 0,
|
||||||
|
"text": c.accumulatedThinking,
|
||||||
}),
|
}),
|
||||||
c.newEvent("response.output_item.done", map[string]any{
|
c.newEvent("response.output_item.done", map[string]any{
|
||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
@@ -898,8 +1133,10 @@ func (c *ResponsesStreamConverter) processTextContent(content string) []Response
|
|||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
"content_index": c.contentIndex,
|
"content_index": c.contentIndex,
|
||||||
"part": map[string]any{
|
"part": map[string]any{
|
||||||
"type": "output_text",
|
"type": "output_text",
|
||||||
"text": "",
|
"text": "",
|
||||||
|
"annotations": []any{},
|
||||||
|
"logprobs": []any{},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -913,6 +1150,7 @@ func (c *ResponsesStreamConverter) processTextContent(content string) []Response
|
|||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
"content_index": 0,
|
"content_index": 0,
|
||||||
"delta": content,
|
"delta": content,
|
||||||
|
"logprobs": []any{},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return events
|
return events
|
||||||
@@ -944,8 +1182,10 @@ func (c *ResponsesStreamConverter) buildFinalOutput() []any {
|
|||||||
"status": "completed",
|
"status": "completed",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": []map[string]any{{
|
"content": []map[string]any{{
|
||||||
"type": "output_text",
|
"type": "output_text",
|
||||||
"text": c.accumulatedText,
|
"text": c.accumulatedText,
|
||||||
|
"annotations": []any{},
|
||||||
|
"logprobs": []any{},
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -967,6 +1207,7 @@ func (c *ResponsesStreamConverter) processCompletion(r api.ChatResponse) []Respo
|
|||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
"content_index": 0,
|
"content_index": 0,
|
||||||
"text": c.accumulatedText,
|
"text": c.accumulatedText,
|
||||||
|
"logprobs": []any{},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// response.content_part.done
|
// response.content_part.done
|
||||||
@@ -975,8 +1216,10 @@ func (c *ResponsesStreamConverter) processCompletion(r api.ChatResponse) []Respo
|
|||||||
"output_index": c.outputIndex,
|
"output_index": c.outputIndex,
|
||||||
"content_index": 0,
|
"content_index": 0,
|
||||||
"part": map[string]any{
|
"part": map[string]any{
|
||||||
"type": "output_text",
|
"type": "output_text",
|
||||||
"text": c.accumulatedText,
|
"text": c.accumulatedText,
|
||||||
|
"annotations": []any{},
|
||||||
|
"logprobs": []any{},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -989,26 +1232,31 @@ func (c *ResponsesStreamConverter) processCompletion(r api.ChatResponse) []Respo
|
|||||||
"status": "completed",
|
"status": "completed",
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": []map[string]any{{
|
"content": []map[string]any{{
|
||||||
"type": "output_text",
|
"type": "output_text",
|
||||||
"text": c.accumulatedText,
|
"text": c.accumulatedText,
|
||||||
|
"annotations": []any{},
|
||||||
|
"logprobs": []any{},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// response.completed
|
// response.completed
|
||||||
events = append(events, c.newEvent("response.completed", map[string]any{
|
usage := map[string]any{
|
||||||
"response": map[string]any{
|
"input_tokens": r.PromptEvalCount,
|
||||||
"id": c.responseID,
|
"output_tokens": r.EvalCount,
|
||||||
"object": "response",
|
"total_tokens": r.PromptEvalCount + r.EvalCount,
|
||||||
"status": "completed",
|
"input_tokens_details": map[string]any{
|
||||||
"output": c.buildFinalOutput(),
|
"cached_tokens": 0,
|
||||||
"usage": map[string]any{
|
|
||||||
"input_tokens": r.PromptEvalCount,
|
|
||||||
"output_tokens": r.EvalCount,
|
|
||||||
"total_tokens": r.PromptEvalCount + r.EvalCount,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
"output_tokens_details": map[string]any{
|
||||||
|
"reasoning_tokens": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response := c.buildResponseObject("completed", c.buildFinalOutput(), usage)
|
||||||
|
response["completed_at"] = time.Now().Unix()
|
||||||
|
events = append(events, c.newEvent("response.completed", map[string]any{
|
||||||
|
"response": response,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|||||||
@@ -850,7 +850,7 @@ func TestFromResponsesRequest_Images(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestResponsesStreamConverter_TextOnly(t *testing.T) {
|
func TestResponsesStreamConverter_TextOnly(t *testing.T) {
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
// First chunk with content
|
// First chunk with content
|
||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
@@ -916,7 +916,7 @@ func TestResponsesStreamConverter_TextOnly(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestResponsesStreamConverter_ToolCalls(t *testing.T) {
|
func TestResponsesStreamConverter_ToolCalls(t *testing.T) {
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
Message: api.Message{
|
Message: api.Message{
|
||||||
@@ -952,7 +952,7 @@ func TestResponsesStreamConverter_ToolCalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestResponsesStreamConverter_Reasoning(t *testing.T) {
|
func TestResponsesStreamConverter_Reasoning(t *testing.T) {
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
// First chunk with thinking
|
// First chunk with thinking
|
||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
@@ -1267,7 +1267,7 @@ func TestToResponse_WithReasoning(t *testing.T) {
|
|||||||
Content: "The answer is 42",
|
Content: "The answer is 42",
|
||||||
},
|
},
|
||||||
Done: true,
|
Done: true,
|
||||||
})
|
}, ResponsesRequest{})
|
||||||
|
|
||||||
// Should have 2 output items: reasoning + message
|
// Should have 2 output items: reasoning + message
|
||||||
if len(response.Output) != 2 {
|
if len(response.Output) != 2 {
|
||||||
@@ -1638,7 +1638,7 @@ func TestFromResponsesRequest_ShorthandFormats(t *testing.T) {
|
|||||||
|
|
||||||
func TestResponsesStreamConverter_OutputIncludesContent(t *testing.T) {
|
func TestResponsesStreamConverter_OutputIncludesContent(t *testing.T) {
|
||||||
// Verify that response.output_item.done includes content field for messages
|
// Verify that response.output_item.done includes content field for messages
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
// First chunk
|
// First chunk
|
||||||
converter.Process(api.ChatResponse{
|
converter.Process(api.ChatResponse{
|
||||||
@@ -1686,7 +1686,7 @@ func TestResponsesStreamConverter_OutputIncludesContent(t *testing.T) {
|
|||||||
|
|
||||||
func TestResponsesStreamConverter_ResponseCompletedIncludesOutput(t *testing.T) {
|
func TestResponsesStreamConverter_ResponseCompletedIncludesOutput(t *testing.T) {
|
||||||
// Verify that response.completed includes the output array
|
// Verify that response.completed includes the output array
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
// Process some content
|
// Process some content
|
||||||
converter.Process(api.ChatResponse{
|
converter.Process(api.ChatResponse{
|
||||||
@@ -1730,7 +1730,7 @@ func TestResponsesStreamConverter_ResponseCompletedIncludesOutput(t *testing.T)
|
|||||||
|
|
||||||
func TestResponsesStreamConverter_ResponseCreatedIncludesOutput(t *testing.T) {
|
func TestResponsesStreamConverter_ResponseCreatedIncludesOutput(t *testing.T) {
|
||||||
// Verify that response.created includes an empty output array
|
// Verify that response.created includes an empty output array
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
Message: api.Message{Content: "Hi"},
|
Message: api.Message{Content: "Hi"},
|
||||||
@@ -1757,7 +1757,7 @@ func TestResponsesStreamConverter_ResponseCreatedIncludesOutput(t *testing.T) {
|
|||||||
|
|
||||||
func TestResponsesStreamConverter_SequenceNumbers(t *testing.T) {
|
func TestResponsesStreamConverter_SequenceNumbers(t *testing.T) {
|
||||||
// Verify that events include incrementing sequence numbers
|
// Verify that events include incrementing sequence numbers
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
Message: api.Message{Content: "Hello"},
|
Message: api.Message{Content: "Hello"},
|
||||||
@@ -1791,7 +1791,7 @@ func TestResponsesStreamConverter_SequenceNumbers(t *testing.T) {
|
|||||||
|
|
||||||
func TestResponsesStreamConverter_FunctionCallStatus(t *testing.T) {
|
func TestResponsesStreamConverter_FunctionCallStatus(t *testing.T) {
|
||||||
// Verify that function call items include status field
|
// Verify that function call items include status field
|
||||||
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b")
|
converter := NewResponsesStreamConverter("resp_123", "msg_456", "gpt-oss:20b", ResponsesRequest{})
|
||||||
|
|
||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
Message: api.Message{
|
Message: api.Message{
|
||||||
|
|||||||
Reference in New Issue
Block a user