@@ -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+
754975func TestIsValidClientRequestID (t * testing.T ) {
755976 tests := []struct {
756977 name string
0 commit comments