Skip to content

Commit 4f6df74

Browse files
feat: adapt max_tokens → max_completion_tokens for OpenAI o-series models (#89)
OpenAI o-series models (o1, o3, o4) reject max_tokens and require max_completion_tokens instead. They also don't support temperature. The OpenAI provider now auto-detects these models and translates parameters before forwarding, so clients can always use max_tokens uniformly (Postel's Law). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 317e4bf commit 4f6df74

5 files changed

Lines changed: 269 additions & 20 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Guidance for AI models (like Claude) working with this codebase.
88

99
- **Module:** `gomodel` | **Go:** 1.25.0 | **Repo:** https://github.com/ENTERPILOT/GOModel
1010
- **Stage:** Development—backward compatibility is not a concern
11+
- **Design philosophy:** [Postel's Law](https://en.wikipedia.org/wiki/Robustness_principle) (the Robustness Principle) — *"Be conservative in what you send, be liberal in what you accept."* The gateway accepts client requests generously (e.g. `max_tokens` for any model) and adapts them to each provider's specific requirements before forwarding (e.g. translating `max_tokens``max_completion_tokens` for OpenAI reasoning models).
1112

1213
## Commands
1314

go.mod

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ require (
3636
github.com/containerd/log v0.1.0 // indirect
3737
github.com/containerd/platforms v0.2.1 // indirect
3838
github.com/cpuguy83/dockercfg v0.3.2 // indirect
39-
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
4039
github.com/davecgh/go-spew v1.1.1 // indirect
4140
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
4241
github.com/distribution/reference v0.6.0 // indirect
@@ -86,14 +85,11 @@ require (
8685
github.com/prometheus/common v0.67.4 // indirect
8786
github.com/prometheus/procfs v0.19.2 // indirect
8887
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
89-
github.com/russross/blackfriday/v2 v2.0.1 // indirect
9088
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
91-
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
9289
github.com/sirupsen/logrus v1.9.3 // indirect
9390
github.com/swaggo/files/v2 v2.0.0 // indirect
9491
github.com/tklauser/go-sysconf v0.3.12 // indirect
9592
github.com/tklauser/numcpus v0.6.1 // indirect
96-
github.com/urfave/cli/v2 v2.3.0 // indirect
9793
github.com/valyala/bytebufferpool v1.0.0 // indirect
9894
github.com/valyala/fasttemplate v1.2.2 // indirect
9995
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -123,5 +119,4 @@ require (
123119
modernc.org/libc v1.67.6 // indirect
124120
modernc.org/mathutil v1.7.1 // indirect
125121
modernc.org/memory v1.11.0 // indirect
126-
sigs.k8s.io/yaml v1.3.0 // indirect
127122
)

go.sum

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
44
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
55
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
66
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
7-
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
87
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
98
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
109
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -31,8 +30,6 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS
3130
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
3231
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
3332
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
34-
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
35-
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
3633
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
3734
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
3835
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -184,13 +181,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
184181
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
185182
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
186183
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
187-
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
188-
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
189-
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
190184
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
191185
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
192-
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
193-
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
194186
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
195187
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
196188
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -222,8 +214,6 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
222214
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
223215
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
224216
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
225-
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
226-
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
227217
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
228218
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
229219
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -335,7 +325,6 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
335325
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
336326
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
337327
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
338-
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
339328
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
340329
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
341330
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -372,5 +361,3 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
372361
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
373362
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
374363
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
375-
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
376-
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

internal/providers/openai/openai.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"io"
77
"net/http"
8+
"strings"
89

910
"gomodel/internal/core"
1011
"gomodel/internal/llmclient"
@@ -84,13 +85,56 @@ func isValidClientRequestID(id string) bool {
8485
return true
8586
}
8687

88+
// isOSeriesModel reports whether the model is an OpenAI o-series model
89+
// (o1, o3, o4) that requires max_completion_tokens instead of max_tokens
90+
// and does not support the temperature parameter.
91+
func isOSeriesModel(model string) bool {
92+
m := strings.ToLower(model)
93+
// Match o1, o3, o4 families (e.g. o3-mini, o4-mini, o3, o1-preview).
94+
// Non-reasoning models like gpt-4o start with "gpt-", not "o".
95+
return len(m) >= 2 && m[0] == 'o' && m[1] >= '0' && m[1] <= '9'
96+
}
97+
98+
// oSeriesChatRequest is the JSON body sent to OpenAI for o-series models.
99+
// It uses max_completion_tokens (required) instead of max_tokens (rejected).
100+
type oSeriesChatRequest struct {
101+
Model string `json:"model"`
102+
Messages []core.Message `json:"messages"`
103+
Stream bool `json:"stream,omitempty"`
104+
StreamOptions *core.StreamOptions `json:"stream_options,omitempty"`
105+
Reasoning *core.Reasoning `json:"reasoning,omitempty"`
106+
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
107+
}
108+
109+
// adaptForOSeries converts a ChatRequest into an oSeriesChatRequest,
110+
// mapping max_tokens → max_completion_tokens and dropping temperature.
111+
func adaptForOSeries(req *core.ChatRequest) *oSeriesChatRequest {
112+
return &oSeriesChatRequest{
113+
Model: req.Model,
114+
Messages: req.Messages,
115+
Stream: req.Stream,
116+
StreamOptions: req.StreamOptions,
117+
Reasoning: req.Reasoning,
118+
MaxCompletionTokens: req.MaxTokens,
119+
}
120+
}
121+
122+
// chatRequestBody returns the appropriate request body for the model.
123+
// Reasoning models get parameter adaptation; others pass through as-is.
124+
func chatRequestBody(req *core.ChatRequest) any {
125+
if isOSeriesModel(req.Model) {
126+
return adaptForOSeries(req)
127+
}
128+
return req
129+
}
130+
87131
// ChatCompletion sends a chat completion request to OpenAI
88132
func (p *Provider) ChatCompletion(ctx context.Context, req *core.ChatRequest) (*core.ChatResponse, error) {
89133
var resp core.ChatResponse
90134
err := p.client.Do(ctx, llmclient.Request{
91135
Method: http.MethodPost,
92136
Endpoint: "/chat/completions",
93-
Body: req,
137+
Body: chatRequestBody(req),
94138
}, &resp)
95139
if err != nil {
96140
return nil, err
@@ -104,10 +148,11 @@ func (p *Provider) ChatCompletion(ctx context.Context, req *core.ChatRequest) (*
104148

105149
// StreamChatCompletion returns a raw response body for streaming (caller must close)
106150
func (p *Provider) StreamChatCompletion(ctx context.Context, req *core.ChatRequest) (io.ReadCloser, error) {
151+
streamReq := req.WithStreaming()
107152
return p.client.DoStream(ctx, llmclient.Request{
108153
Method: http.MethodPost,
109154
Endpoint: "/chat/completions",
110-
Body: req.WithStreaming(),
155+
Body: chatRequestBody(streamReq),
111156
})
112157
}
113158

internal/providers/openai/openai_test.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,227 @@ func TestResponsesWithContext(t *testing.T) {
751751
}
752752
}
753753

754+
func TestIsOSeriesModel(t *testing.T) {
755+
tests := []struct {
756+
model string
757+
expected bool
758+
}{
759+
{"o3-mini", true},
760+
{"o4-mini", true},
761+
{"o3", true},
762+
{"o4", true},
763+
{"o1-preview", true},
764+
{"o1-mini", true},
765+
{"o3-mini-2025-01-31", true},
766+
{"gpt-4o", false},
767+
{"gpt-4o-mini", false},
768+
{"gpt-4", false},
769+
{"gpt-3.5-turbo", false},
770+
{"claude-3-opus", false},
771+
{"", false},
772+
{"o", false},
773+
{"openai", false},
774+
}
775+
for _, tt := range tests {
776+
t.Run(tt.model, func(t *testing.T) {
777+
if got := isOSeriesModel(tt.model); got != tt.expected {
778+
t.Errorf("isOSeriesModel(%q) = %v, want %v", tt.model, got, tt.expected)
779+
}
780+
})
781+
}
782+
}
783+
784+
func TestChatCompletion_ReasoningModel_AdaptsParameters(t *testing.T) {
785+
maxTokens := 1000
786+
787+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
788+
body, err := io.ReadAll(r.Body)
789+
if err != nil {
790+
t.Fatalf("failed to read request body: %v", err)
791+
}
792+
793+
var raw map[string]interface{}
794+
if err := json.Unmarshal(body, &raw); err != nil {
795+
t.Fatalf("failed to unmarshal request: %v", err)
796+
}
797+
798+
// max_tokens must NOT be present
799+
if _, ok := raw["max_tokens"]; ok {
800+
t.Error("reasoning model request should not contain max_tokens")
801+
}
802+
803+
// max_completion_tokens must be present with the right value
804+
mct, ok := raw["max_completion_tokens"]
805+
if !ok {
806+
t.Fatal("reasoning model request should contain max_completion_tokens")
807+
}
808+
if int(mct.(float64)) != maxTokens {
809+
t.Errorf("max_completion_tokens = %v, want %d", mct, maxTokens)
810+
}
811+
812+
// temperature must NOT be present
813+
if _, ok := raw["temperature"]; ok {
814+
t.Error("reasoning model request should not contain temperature")
815+
}
816+
817+
w.WriteHeader(http.StatusOK)
818+
_, _ = w.Write([]byte(`{
819+
"id": "chatcmpl-123",
820+
"object": "chat.completion",
821+
"model": "o3-mini",
822+
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hi"}, "finish_reason": "stop"}],
823+
"usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15}
824+
}`))
825+
}))
826+
defer server.Close()
827+
828+
provider := NewWithHTTPClient("test-api-key", nil, llmclient.Hooks{})
829+
provider.SetBaseURL(server.URL)
830+
831+
temp := 0.7
832+
req := &core.ChatRequest{
833+
Model: "o3-mini",
834+
Messages: []core.Message{{Role: "user", Content: "Hello"}},
835+
MaxTokens: &maxTokens,
836+
Temperature: &temp,
837+
}
838+
839+
resp, err := provider.ChatCompletion(context.Background(), req)
840+
if err != nil {
841+
t.Fatalf("unexpected error: %v", err)
842+
}
843+
if resp.Model != "o3-mini" {
844+
t.Errorf("Model = %q, want %q", resp.Model, "o3-mini")
845+
}
846+
}
847+
848+
func TestChatCompletion_NonReasoningModel_PassesMaxTokens(t *testing.T) {
849+
maxTokens := 1000
850+
851+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
852+
body, err := io.ReadAll(r.Body)
853+
if err != nil {
854+
t.Fatalf("failed to read request body: %v", err)
855+
}
856+
857+
var raw map[string]interface{}
858+
if err := json.Unmarshal(body, &raw); err != nil {
859+
t.Fatalf("failed to unmarshal request: %v", err)
860+
}
861+
862+
// max_tokens must be present
863+
mt, ok := raw["max_tokens"]
864+
if !ok {
865+
t.Fatal("non-reasoning model request should contain max_tokens")
866+
}
867+
if int(mt.(float64)) != maxTokens {
868+
t.Errorf("max_tokens = %v, want %d", mt, maxTokens)
869+
}
870+
871+
// max_completion_tokens must NOT be present
872+
if _, ok := raw["max_completion_tokens"]; ok {
873+
t.Error("non-reasoning model request should not contain max_completion_tokens")
874+
}
875+
876+
// temperature must be present
877+
if _, ok := raw["temperature"]; !ok {
878+
t.Error("non-reasoning model request should contain temperature")
879+
}
880+
881+
w.WriteHeader(http.StatusOK)
882+
_, _ = w.Write([]byte(`{
883+
"id": "chatcmpl-456",
884+
"object": "chat.completion",
885+
"model": "gpt-4o",
886+
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hi"}, "finish_reason": "stop"}],
887+
"usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15}
888+
}`))
889+
}))
890+
defer server.Close()
891+
892+
provider := NewWithHTTPClient("test-api-key", nil, llmclient.Hooks{})
893+
provider.SetBaseURL(server.URL)
894+
895+
temp := 0.7
896+
req := &core.ChatRequest{
897+
Model: "gpt-4o",
898+
Messages: []core.Message{{Role: "user", Content: "Hello"}},
899+
MaxTokens: &maxTokens,
900+
Temperature: &temp,
901+
}
902+
903+
resp, err := provider.ChatCompletion(context.Background(), req)
904+
if err != nil {
905+
t.Fatalf("unexpected error: %v", err)
906+
}
907+
if resp.Model != "gpt-4o" {
908+
t.Errorf("Model = %q, want %q", resp.Model, "gpt-4o")
909+
}
910+
}
911+
912+
func TestStreamChatCompletion_ReasoningModel_AdaptsParameters(t *testing.T) {
913+
maxTokens := 2000
914+
915+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
916+
body, err := io.ReadAll(r.Body)
917+
if err != nil {
918+
t.Fatalf("failed to read request body: %v", err)
919+
}
920+
921+
var raw map[string]interface{}
922+
if err := json.Unmarshal(body, &raw); err != nil {
923+
t.Fatalf("failed to unmarshal request: %v", err)
924+
}
925+
926+
// Must use max_completion_tokens, not max_tokens
927+
if _, ok := raw["max_tokens"]; ok {
928+
t.Error("streaming reasoning model request should not contain max_tokens")
929+
}
930+
mct, ok := raw["max_completion_tokens"]
931+
if !ok {
932+
t.Fatal("streaming reasoning model request should contain max_completion_tokens")
933+
}
934+
if int(mct.(float64)) != maxTokens {
935+
t.Errorf("max_completion_tokens = %v, want %d", mct, maxTokens)
936+
}
937+
938+
// stream must be true
939+
if stream, ok := raw["stream"].(bool); !ok || !stream {
940+
t.Error("stream should be true")
941+
}
942+
943+
w.WriteHeader(http.StatusOK)
944+
_, _ = w.Write([]byte(`data: {"id":"chatcmpl-123","object":"chat.completion.chunk","model":"o4-mini","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}
945+
946+
data: [DONE]
947+
`))
948+
}))
949+
defer server.Close()
950+
951+
provider := NewWithHTTPClient("test-api-key", nil, llmclient.Hooks{})
952+
provider.SetBaseURL(server.URL)
953+
954+
req := &core.ChatRequest{
955+
Model: "o4-mini",
956+
Messages: []core.Message{{Role: "user", Content: "Hello"}},
957+
MaxTokens: &maxTokens,
958+
}
959+
960+
body, err := provider.StreamChatCompletion(context.Background(), req)
961+
if err != nil {
962+
t.Fatalf("unexpected error: %v", err)
963+
}
964+
defer func() { _ = body.Close() }()
965+
966+
respBody, err := io.ReadAll(body)
967+
if err != nil {
968+
t.Fatalf("failed to read response body: %v", err)
969+
}
970+
if !strings.Contains(string(respBody), "o4-mini") {
971+
t.Error("response should contain o4-mini model")
972+
}
973+
}
974+
754975
func TestIsValidClientRequestID(t *testing.T) {
755976
tests := []struct {
756977
name string

0 commit comments

Comments
 (0)