1// Package openai provides an implementation of the fantasy AI SDK for OpenAI's language models.
2package openai
3
4import (
5 "cmp"
6 "context"
7 "maps"
8
9 "charm.land/fantasy"
10 "charm.land/fantasy/providers/internal/httpheaders"
11 "github.com/openai/openai-go/v2"
12 "github.com/openai/openai-go/v2/option"
13)
14
15const (
16 // Name is the name of the OpenAI provider.
17 Name = "openai"
18 // DefaultURL is the default URL for the OpenAI API.
19 DefaultURL = "https://api.openai.com/v1"
20)
21
22type provider struct {
23 options options
24}
25
26type options struct {
27 baseURL string
28 apiKey string
29 organization string
30 project string
31 name string
32 useResponsesAPI bool
33 headers map[string]string
34 userAgent string
35 noDefaultUserAgent bool
36 client option.HTTPClient
37 sdkOptions []option.RequestOption
38 objectMode fantasy.ObjectMode
39 languageModelOptions []LanguageModelOption
40}
41
42// Option defines a function that configures OpenAI provider options.
43type Option = func(*options)
44
45// New creates a new OpenAI provider with the given options.
46func New(opts ...Option) (fantasy.Provider, error) {
47 providerOptions := options{
48 headers: map[string]string{},
49 languageModelOptions: make([]LanguageModelOption, 0),
50 }
51 for _, o := range opts {
52 o(&providerOptions)
53 }
54
55 providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
56 providerOptions.name = cmp.Or(providerOptions.name, Name)
57
58 if providerOptions.organization != "" {
59 providerOptions.headers["OpenAi-Organization"] = providerOptions.organization
60 }
61 if providerOptions.project != "" {
62 providerOptions.headers["OpenAi-Project"] = providerOptions.project
63 }
64
65 return &provider{options: providerOptions}, nil
66}
67
68// WithBaseURL sets the base URL for the OpenAI provider.
69func WithBaseURL(baseURL string) Option {
70 return func(o *options) {
71 o.baseURL = baseURL
72 }
73}
74
75// WithAPIKey sets the API key for the OpenAI provider.
76func WithAPIKey(apiKey string) Option {
77 return func(o *options) {
78 o.apiKey = apiKey
79 }
80}
81
82// WithOrganization sets the organization for the OpenAI provider.
83func WithOrganization(organization string) Option {
84 return func(o *options) {
85 o.organization = organization
86 }
87}
88
89// WithProject sets the project for the OpenAI provider.
90func WithProject(project string) Option {
91 return func(o *options) {
92 o.project = project
93 }
94}
95
96// WithName sets the name for the OpenAI provider.
97func WithName(name string) Option {
98 return func(o *options) {
99 o.name = name
100 }
101}
102
103// WithHeaders sets the headers for the OpenAI provider.
104func WithHeaders(headers map[string]string) Option {
105 return func(o *options) {
106 maps.Copy(o.headers, headers)
107 }
108}
109
110// WithHTTPClient sets the HTTP client for the OpenAI provider.
111func WithHTTPClient(client option.HTTPClient) Option {
112 return func(o *options) {
113 o.client = client
114 }
115}
116
117// WithSDKOptions sets the SDK options for the OpenAI provider.
118func WithSDKOptions(opts ...option.RequestOption) Option {
119 return func(o *options) {
120 o.sdkOptions = append(o.sdkOptions, opts...)
121 }
122}
123
124// WithLanguageModelOptions sets the language model options for the OpenAI provider.
125func WithLanguageModelOptions(opts ...LanguageModelOption) Option {
126 return func(o *options) {
127 o.languageModelOptions = append(o.languageModelOptions, opts...)
128 }
129}
130
131// WithUseResponsesAPI configures the provider to use the responses API for models that support it.
132func WithUseResponsesAPI() Option {
133 return func(o *options) {
134 o.useResponsesAPI = true
135 }
136}
137
138// WithUserAgent sets an explicit User-Agent header, overriding the default and any
139// value set via WithHeaders.
140func WithUserAgent(ua string) Option {
141 return func(o *options) {
142 o.userAgent = ua
143 }
144}
145
146// WithSkipUserAgent prevents the provider from setting a default
147// User-Agent header, preserving the underlying SDK's own User-Agent.
148// This is needed for providers like OpenRouter whose API behaviour depends
149// on the User-Agent matching the SDK that is making the request.
150//
151// This function is provisional and may be removed in a future release.
152func WithSkipUserAgent() Option {
153 return func(o *options) {
154 o.noDefaultUserAgent = true
155 }
156}
157
158// WithObjectMode sets the object generation mode.
159func WithObjectMode(om fantasy.ObjectMode) Option {
160 return func(o *options) {
161 // not supported
162 if om == fantasy.ObjectModeJSON {
163 om = fantasy.ObjectModeAuto
164 }
165 o.objectMode = om
166 }
167}
168
169// LanguageModel implements fantasy.Provider.
170func (o *provider) LanguageModel(_ context.Context, modelID string) (fantasy.LanguageModel, error) {
171 openaiClientOptions := make([]option.RequestOption, 0, 5+len(o.options.headers)+len(o.options.sdkOptions))
172 openaiClientOptions = append(openaiClientOptions, option.WithMaxRetries(0))
173
174 if o.options.apiKey != "" {
175 openaiClientOptions = append(openaiClientOptions, option.WithAPIKey(o.options.apiKey))
176 }
177 if o.options.baseURL != "" {
178 openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(o.options.baseURL))
179 }
180
181 if o.options.noDefaultUserAgent {
182 for key, value := range o.options.headers {
183 openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
184 }
185 if o.options.userAgent != "" {
186 openaiClientOptions = append(openaiClientOptions, option.WithHeader("User-Agent", o.options.userAgent))
187 }
188 } else {
189 defaultUA := httpheaders.DefaultUserAgent(fantasy.Version)
190 resolved := httpheaders.ResolveHeaders(o.options.headers, o.options.userAgent, defaultUA)
191 for key, value := range resolved {
192 openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
193 }
194 }
195
196 if o.options.client != nil {
197 openaiClientOptions = append(openaiClientOptions, option.WithHTTPClient(o.options.client))
198 }
199
200 openaiClientOptions = append(openaiClientOptions, o.options.sdkOptions...)
201
202 client := openai.NewClient(openaiClientOptions...)
203
204 if o.options.useResponsesAPI && IsResponsesModel(modelID) {
205 // Not supported for responses API
206 objectMode := o.options.objectMode
207 if objectMode == fantasy.ObjectModeJSON {
208 objectMode = fantasy.ObjectModeAuto
209 }
210 return newResponsesLanguageModel(modelID, o.options.name, client, objectMode, o.options.noDefaultUserAgent), nil
211 }
212
213 languageModelOptions := append([]LanguageModelOption{}, o.options.languageModelOptions...)
214 languageModelOptions = append(languageModelOptions, WithLanguageModelObjectMode(o.options.objectMode))
215 if o.options.noDefaultUserAgent {
216 languageModelOptions = append(languageModelOptions, WithLanguageModelSkipUserAgent())
217 }
218
219 return newLanguageModel(
220 modelID,
221 o.options.name,
222 client,
223 languageModelOptions...,
224 ), nil
225}
226
227func (o *provider) Name() string {
228 return Name
229}