From 5f4ffa0129d1e86a3ce8392414033abf33b7ef15 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 28 Oct 2025 09:50:08 +0000 Subject: [PATCH 1/3] feat: add support for max_completion_tokens parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for the max_completion_tokens parameter required by newer OpenAI models (gpt-5, gpt-4o). This parameter is used instead of max_tokens for these models. Changes: - Added MaxCompletionTokens field to ChatCompletionNewParamsWrapper - Updated MarshalJSON to include max_completion_tokens in extras - Updated UnmarshalJSON to extract and set max_completion_tokens - Added comprehensive test coverage for the new parameter Fixes #40 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- openai.go | 17 ++++++++++++-- openai_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/openai.go b/openai.go index dc3abc8..9b5d3c3 100644 --- a/openai.go +++ b/openai.go @@ -15,13 +15,18 @@ import ( type ChatCompletionNewParamsWrapper struct { openai.ChatCompletionNewParams `json:""` Stream bool `json:"stream,omitempty"` + MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` } func (c ChatCompletionNewParamsWrapper) MarshalJSON() ([]byte, error) { type shadow ChatCompletionNewParamsWrapper - return param.MarshalWithExtras(c, (*shadow)(&c), map[string]any{ + extras := map[string]any{ "stream": c.Stream, - }) + } + if c.MaxCompletionTokens != nil { + extras["max_completion_tokens"] = *c.MaxCompletionTokens + } + return param.MarshalWithExtras(c, (*shadow)(&c), extras) } func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { @@ -43,6 +48,14 @@ func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} } + // Extract max_completion_tokens if present + if maxCompletionTokens := utils.ExtractJSONField[float64](raw, "max_completion_tokens"); maxCompletionTokens > 0 { + tokens := int(maxCompletionTokens) + c.MaxCompletionTokens = &tokens + // Set it in the underlying params as well + c.ChatCompletionNewParams.MaxCompletionTokens = openai.Int(int64(tokens)) + } + return nil } diff --git a/openai_test.go b/openai_test.go index 35e52f2..c7b4f6c 100644 --- a/openai_test.go +++ b/openai_test.go @@ -1,6 +1,7 @@ package aibridge_test import ( + "encoding/json" "testing" "github.com/coder/aibridge" @@ -131,3 +132,62 @@ func TestOpenAILastUserPrompt(t *testing.T) { }) } } + +func TestMaxCompletionTokens(t *testing.T) { + t.Parallel() + + t.Run("unmarshal max_completion_tokens from JSON", func(t *testing.T) { + jsonStr := `{ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello"}], + "max_completion_tokens": 1024 + }` + + var wrapper aibridge.ChatCompletionNewParamsWrapper + err := json.Unmarshal([]byte(jsonStr), &wrapper) + require.NoError(t, err) + require.NotNil(t, wrapper.MaxCompletionTokens) + require.Equal(t, 1024, *wrapper.MaxCompletionTokens) + }) + + t.Run("marshal max_completion_tokens to JSON", func(t *testing.T) { + maxTokens := 2048 + wrapper := aibridge.ChatCompletionNewParamsWrapper{ + ChatCompletionNewParams: openai.ChatCompletionNewParams{ + Model: openai.ChatModelGPT4o, + Messages: []openai.ChatCompletionMessageParamUnion{ + openai.UserMessage("Hello"), + }, + }, + MaxCompletionTokens: &maxTokens, + } + + jsonBytes, err := json.Marshal(wrapper) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(jsonBytes, &result) + require.NoError(t, err) + require.Equal(t, float64(2048), result["max_completion_tokens"]) + }) + + t.Run("max_completion_tokens not set when nil", func(t *testing.T) { + wrapper := aibridge.ChatCompletionNewParamsWrapper{ + ChatCompletionNewParams: openai.ChatCompletionNewParams{ + Model: openai.ChatModelGPT4o, + Messages: []openai.ChatCompletionMessageParamUnion{ + openai.UserMessage("Hello"), + }, + }, + } + + jsonBytes, err := json.Marshal(wrapper) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(jsonBytes, &result) + require.NoError(t, err) + _, exists := result["max_completion_tokens"] + require.False(t, exists, "max_completion_tokens should not be present when nil") + }) +} From 90b5baf8a746e8e9ddeb6ccec3650ae2eb2843e3 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 28 Oct 2025 14:29:10 +0000 Subject: [PATCH 2/3] fix: handle explicit zero value for max_completion_tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from GitHub Copilot. The previous implementation would skip setting max_completion_tokens when the value was 0, which is incorrect as 0 is a valid value that should be explicitly passed to the OpenAI API. Changes: - Check for field existence in JSON rather than value > 0 - Handle explicit 0 values correctly - Add test cases for zero value marshaling/unmarshaling - Support multiple numeric types (float64, int, int64) for robustness 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- openai.go | 26 +++++++++++++++++++++----- openai_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/openai.go b/openai.go index 9b5d3c3..f076548 100644 --- a/openai.go +++ b/openai.go @@ -49,11 +49,27 @@ func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { } // Extract max_completion_tokens if present - if maxCompletionTokens := utils.ExtractJSONField[float64](raw, "max_completion_tokens"); maxCompletionTokens > 0 { - tokens := int(maxCompletionTokens) - c.MaxCompletionTokens = &tokens - // Set it in the underlying params as well - c.ChatCompletionNewParams.MaxCompletionTokens = openai.Int(int64(tokens)) + // We need to check if the field exists in the JSON to properly handle explicit 0 values + var data map[string]any + if err := json.Unmarshal(raw, &data); err == nil { + if val, exists := data["max_completion_tokens"]; exists { + // Field is explicitly set, convert to int + var tokens int + switch v := val.(type) { + case float64: + tokens = int(v) + case int: + tokens = v + case int64: + tokens = int(v) + default: + // Invalid type, skip + return nil + } + c.MaxCompletionTokens = &tokens + // Set it in the underlying params as well + c.ChatCompletionNewParams.MaxCompletionTokens = openai.Int(int64(tokens)) + } } return nil diff --git a/openai_test.go b/openai_test.go index c7b4f6c..228dedd 100644 --- a/openai_test.go +++ b/openai_test.go @@ -150,6 +150,20 @@ func TestMaxCompletionTokens(t *testing.T) { require.Equal(t, 1024, *wrapper.MaxCompletionTokens) }) + t.Run("unmarshal max_completion_tokens with zero value", func(t *testing.T) { + jsonStr := `{ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello"}], + "max_completion_tokens": 0 + }` + + var wrapper aibridge.ChatCompletionNewParamsWrapper + err := json.Unmarshal([]byte(jsonStr), &wrapper) + require.NoError(t, err) + require.NotNil(t, wrapper.MaxCompletionTokens, "max_completion_tokens should be set even when 0") + require.Equal(t, 0, *wrapper.MaxCompletionTokens) + }) + t.Run("marshal max_completion_tokens to JSON", func(t *testing.T) { maxTokens := 2048 wrapper := aibridge.ChatCompletionNewParamsWrapper{ @@ -171,6 +185,27 @@ func TestMaxCompletionTokens(t *testing.T) { require.Equal(t, float64(2048), result["max_completion_tokens"]) }) + t.Run("marshal max_completion_tokens with zero value", func(t *testing.T) { + maxTokens := 0 + wrapper := aibridge.ChatCompletionNewParamsWrapper{ + ChatCompletionNewParams: openai.ChatCompletionNewParams{ + Model: openai.ChatModelGPT4o, + Messages: []openai.ChatCompletionMessageParamUnion{ + openai.UserMessage("Hello"), + }, + }, + MaxCompletionTokens: &maxTokens, + } + + jsonBytes, err := json.Marshal(wrapper) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(jsonBytes, &result) + require.NoError(t, err) + require.Equal(t, float64(0), result["max_completion_tokens"], "max_completion_tokens should be present even when 0") + }) + t.Run("max_completion_tokens not set when nil", func(t *testing.T) { wrapper := aibridge.ChatCompletionNewParamsWrapper{ ChatCompletionNewParams: openai.ChatCompletionNewParams{ From 5439bca0291d1bb130fadb694817bf0c2bd23fc0 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 28 Oct 2025 15:03:22 +0000 Subject: [PATCH 3/3] fix: validate max_completion_tokens as positive integers only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the previous change that accepted zero values. Research shows that the OpenAI API requires positive integers for token limits, and there is no documentation supporting zero as a valid value. Changes: - Only accept max_completion_tokens > 0 - Add validation to reject 0 and negative values - Update tests to verify invalid values are ignored - Add test for negative value handling - Keep field existence check to improve over original implementation The GitHub Copilot review suggested accepting 0, but without any documentation proving it's valid, we should validate input to match API requirements. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- openai.go | 13 ++++++++----- openai_test.go | 37 ++++++++++++++----------------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/openai.go b/openai.go index f076548..d1cd543 100644 --- a/openai.go +++ b/openai.go @@ -48,8 +48,8 @@ func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{} } - // Extract max_completion_tokens if present - // We need to check if the field exists in the JSON to properly handle explicit 0 values + // Extract max_completion_tokens if present and positive + // OpenAI API requires positive integers for token limits var data map[string]any if err := json.Unmarshal(raw, &data); err == nil { if val, exists := data["max_completion_tokens"]; exists { @@ -66,9 +66,12 @@ func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error { // Invalid type, skip return nil } - c.MaxCompletionTokens = &tokens - // Set it in the underlying params as well - c.ChatCompletionNewParams.MaxCompletionTokens = openai.Int(int64(tokens)) + // Only set if positive (0 and negative values are invalid) + if tokens > 0 { + c.MaxCompletionTokens = &tokens + // Set it in the underlying params as well + c.ChatCompletionNewParams.MaxCompletionTokens = openai.Int(int64(tokens)) + } } } diff --git a/openai_test.go b/openai_test.go index 228dedd..b4a954a 100644 --- a/openai_test.go +++ b/openai_test.go @@ -150,7 +150,7 @@ func TestMaxCompletionTokens(t *testing.T) { require.Equal(t, 1024, *wrapper.MaxCompletionTokens) }) - t.Run("unmarshal max_completion_tokens with zero value", func(t *testing.T) { + t.Run("unmarshal max_completion_tokens with zero value ignored", func(t *testing.T) { jsonStr := `{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}], @@ -160,33 +160,24 @@ func TestMaxCompletionTokens(t *testing.T) { var wrapper aibridge.ChatCompletionNewParamsWrapper err := json.Unmarshal([]byte(jsonStr), &wrapper) require.NoError(t, err) - require.NotNil(t, wrapper.MaxCompletionTokens, "max_completion_tokens should be set even when 0") - require.Equal(t, 0, *wrapper.MaxCompletionTokens) + require.Nil(t, wrapper.MaxCompletionTokens, "max_completion_tokens should not be set when 0 (invalid value)") }) - t.Run("marshal max_completion_tokens to JSON", func(t *testing.T) { - maxTokens := 2048 - wrapper := aibridge.ChatCompletionNewParamsWrapper{ - ChatCompletionNewParams: openai.ChatCompletionNewParams{ - Model: openai.ChatModelGPT4o, - Messages: []openai.ChatCompletionMessageParamUnion{ - openai.UserMessage("Hello"), - }, - }, - MaxCompletionTokens: &maxTokens, - } - - jsonBytes, err := json.Marshal(wrapper) - require.NoError(t, err) + t.Run("unmarshal max_completion_tokens with negative value ignored", func(t *testing.T) { + jsonStr := `{ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello"}], + "max_completion_tokens": -100 + }` - var result map[string]interface{} - err = json.Unmarshal(jsonBytes, &result) + var wrapper aibridge.ChatCompletionNewParamsWrapper + err := json.Unmarshal([]byte(jsonStr), &wrapper) require.NoError(t, err) - require.Equal(t, float64(2048), result["max_completion_tokens"]) + require.Nil(t, wrapper.MaxCompletionTokens, "max_completion_tokens should not be set when negative (invalid value)") }) - t.Run("marshal max_completion_tokens with zero value", func(t *testing.T) { - maxTokens := 0 + t.Run("marshal max_completion_tokens to JSON", func(t *testing.T) { + maxTokens := 2048 wrapper := aibridge.ChatCompletionNewParamsWrapper{ ChatCompletionNewParams: openai.ChatCompletionNewParams{ Model: openai.ChatModelGPT4o, @@ -203,7 +194,7 @@ func TestMaxCompletionTokens(t *testing.T) { var result map[string]interface{} err = json.Unmarshal(jsonBytes, &result) require.NoError(t, err) - require.Equal(t, float64(0), result["max_completion_tokens"], "max_completion_tokens should be present even when 0") + require.Equal(t, float64(2048), result["max_completion_tokens"]) }) t.Run("max_completion_tokens not set when nil", func(t *testing.T) {