Files
GopherGate/internal/providers/openai_responses.go
T
hobokenchicken e5ef39f327
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
feat: add OpenAI Responses API support (POST /v1/responses)
Add full Responses API endpoint alongside existing Chat Completions,
with identical logging/tracking/cost pipeline.

New:
- internal/models/responses.go — request/response/stream types + ToUsage() bridge
- internal/providers/openai_responses.go — OpenAI Responses/ResponsesStream

Modified:
- provider.go — Responses()+ResponsesStream() added to Provider interface
- helpers.go — BuildOpenAIResponsesBody, parsers, SSE stream reader
- circuit_breaker.go — CB wraps Responses, passthrough for stream
- server.go — POST /v1/responses route + handleResponses handler
- all non-OpenAI providers — stub methods with clear error messages

Logging: ResponsesUsage.ToUsage() bridges to models.Usage, feeding same
logRequest() -> DB insert -> dashboard WS -> client stats -> cost calc
pipeline. No schema or logger changes needed.
2026-05-02 16:38:17 -04:00

71 lines
1.9 KiB
Go

package providers
import (
"context"
"encoding/json"
"fmt"
"gophergate/internal/models"
)
// Responses sends a non-streaming request to OpenAI's /v1/responses endpoint.
func (p *OpenAIProvider) Responses(ctx context.Context, req *models.ResponsesRequest) (*models.ResponsesResponse, error) {
// Determine if streaming was requested
stream := req.Stream != nil && *req.Stream
body := BuildOpenAIResponsesBody(req, stream)
resp, err := p.client.R().
SetContext(ctx).
SetHeader("Authorization", "Bearer "+p.apiKey).
SetBody(body).
Post(fmt.Sprintf("%s/responses", p.config.BaseURL))
if err != nil {
return nil, fmt.Errorf("responses request failed: %w", err)
}
if !resp.IsSuccess() {
return nil, fmt.Errorf("OpenAI Responses API error (%d): %s", resp.StatusCode(), resp.String())
}
var respJSON map[string]interface{}
if err := json.Unmarshal(resp.Body(), &respJSON); err != nil {
return nil, fmt.Errorf("failed to parse responses response: %w", err)
}
return ParseOpenAIResponsesResponse(respJSON, req.Model)
}
// ResponsesStream sends a streaming request to OpenAI's /v1/responses endpoint.
func (p *OpenAIProvider) ResponsesStream(ctx context.Context, req *models.ResponsesRequest) (<-chan *models.ResponsesStreamChunk, error) {
body := BuildOpenAIResponsesBody(req, true)
resp, err := p.client.R().
SetContext(ctx).
SetHeader("Authorization", "Bearer "+p.apiKey).
SetBody(body).
SetDoNotParseResponse(true).
Post(fmt.Sprintf("%s/responses", p.config.BaseURL))
if err != nil {
return nil, fmt.Errorf("responses stream request failed: %w", err)
}
if !resp.IsSuccess() {
return nil, fmt.Errorf("OpenAI Responses API error (%d): %s", resp.StatusCode(), resp.String())
}
ch := make(chan *models.ResponsesStreamChunk)
go func() {
defer close(ch)
err := StreamOpenAIResponses(resp.RawBody(), ch)
if err != nil {
fmt.Printf("Responses stream error: %v\n", err)
}
}()
return ch, nil
}