openai_responses.go RawHistoryFolder
1package provider
2
3// OpenAI Responses API client (api.openai.com/v1/responses).
4//
5// Strategy: reuse the existing codexClient (which already speaks the
6// Responses wire format) via a header-rewriting RoundTripper. The codex
7// client always sends `chatgpt-account-id` and `openai-beta`; the public
8// Responses API on api.openai.com accepts but doesn't require those —
9// we strip the account-id header on its way out so the request looks
10// like a normal authenticated OpenAI call.
11//
12// This is a separate provider from `openai` (which is Chat Completions);
13// users opt in by passing `--provider openai-responses` or by picking a
14// model whose catalog entry tags it under that provider id.
15//
16// Auth: API key via Authorization: Bearer (set by codexClient itself).
17// Different from openai-codex, which uses OAuth subscription tokens.
18
19import (
20 "net/http"
21 "strings"
22)
23
24const openaiResponsesDefaultBaseURL = "https://api.openai.com/v1/responses"
25
26// openaiResponsesTransport rewrites headers so the codex client (which
27// is designed for chatgpt.com OAuth tokens) can target the public
28// OpenAI Responses API with a normal API key.
29type openaiResponsesTransport struct {
30 inner http.RoundTripper
31}
32
33func (t *openaiResponsesTransport) RoundTrip(req *http.Request) (*http.Response, error) {
34 clone := req.Clone(req.Context())
35 // The codex client always sets chatgpt-account-id; api.openai.com
36 // doesn't need it and may reject the OAuth identity headers, so
37 // strip them.
38 clone.Header.Del("chatgpt-account-id")
39 clone.Header.Del("openai-beta")
40 clone.Header.Del("originator")
41 // The shared Codex client sets a Bearer header even with an empty key.
42 // Keyless custom Responses endpoints need no Authorization header at all.
43 if strings.TrimSpace(clone.Header.Get("authorization")) == "Bearer" {
44 clone.Header.Del("authorization")
45 }
46 return t.inner.RoundTrip(clone)
47}
48
49// NewOpenAIResponses returns an OpenAI Responses-API client (API-key
50// flow). Uses the same wire format as the ChatGPT Codex backend but
51// with the public api.openai.com endpoint and standard Bearer auth.
52//
53// baseURL may be empty; defaults to https://api.openai.com/v1/responses.
54func NewOpenAIResponses(apiKey, baseURL string) Client {
55 return NewOpenAIResponsesNamed(apiKey, baseURL, "openai-responses")
56}
57
58// NewOpenAIResponsesNamed returns a public OpenAI Responses-API client
59// reporting the supplied provider name. This lets the `openai` provider
60// route Responses-only preview models without changing the visible provider.
61func NewOpenAIResponsesNamed(apiKey, baseURL, name string) Client {
62 baseURL = responsesURL(baseURL)
63 httpClient := &http.Client{
64 Transport: &openaiResponsesTransport{inner: http.DefaultTransport},
65 Timeout: 0,
66 }
67 inner := &codexClient{
68 token: apiKey,
69 accountID: "", // unused; transport strips the header
70 baseURL: strings.TrimRight(baseURL, "/"),
71 errorLabel: "openai",
72 providerName: name,
73 http: httpClient,
74 }
75 return &renamedClient{inner: inner, name: name}
76}
77
78// responsesURL accepts either an API root or a complete Responses endpoint.
79// Catalog model entries generally carry API roots, while explicit overrides
80// may already include /responses.
81func responsesURL(baseURL string) string {
82 baseURL = strings.TrimRight(baseURL, "/")
83 if baseURL == "" {
84 return openaiResponsesDefaultBaseURL
85 }
86 if strings.HasSuffix(baseURL, "/responses") {
87 return baseURL
88 }
89 if versionSegmentSuffix.MatchString(baseURL) {
90 return baseURL + "/responses"
91 }
92 return baseURL + "/v1/responses"
93}