From 871d8e35a111b5d71f42221b47078c675b0a1382 Mon Sep 17 00:00:00 2001 From: runbgp <99215336+runbgp@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:52:49 +0000 Subject: [PATCH] [fix]: preserve GLM-5.3-Flash max reasoning effort Affected packages: core/providers/openai and tests/e2e/api provider harness. --- core/changelog.md | 1 + core/providers/openai/glmeffort_test.go | 56 ++++++ .../openai/responses_marshal_test.go | 2 + core/providers/openai/utils.go | 3 +- .../e2e/api/collections/provider-harness.json | 166 ++++++++++++++++++ 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 core/providers/openai/glmeffort_test.go diff --git a/core/changelog.md b/core/changelog.md index 86cac25c641..1c3593de2f5 100644 --- a/core/changelog.md +++ b/core/changelog.md @@ -1,3 +1,4 @@ +[fix]: preserve GLM-5.3-Flash max reasoning effort on OpenAI-compatible routes [@runbgp](https://github.com/runbgp) - fix: give a Bedrock message a placeholder text block instead of a null `content` field when it has no text and no tool calls - `BedrockMessage.Content` has no `omitempty`, so a message with empty text and no tool calls (or an empty `tool_calls` array) serialized as `content:null`, which Converse rejects with "Member must not be null" (#2765) - [fix]: marshal required nullable response fields as null [@PSR94](https://github.com/PSR94) - fix: accept top-level arrays from OpenAI-compatible model APIs [@dani29](https://github.com/dani29) diff --git a/core/providers/openai/glmeffort_test.go b/core/providers/openai/glmeffort_test.go new file mode 100644 index 00000000000..19f8f93523c --- /dev/null +++ b/core/providers/openai/glmeffort_test.go @@ -0,0 +1,56 @@ +package openai + +import ( + "encoding/json" + "testing" + + "github.com/bytedance/sonic" + "github.com/maximhq/bifrost/core/schemas" + "github.com/stretchr/testify/require" +) + +// Exercise the inbound OpenAI dialect and the outbound shared converter used +// by vLLM, including Hugging Face namespaces and both streaming request shapes. +func TestGLM53FlashReasoningEffortRoundTrip(t *testing.T) { + for _, model := range []string{"glm-5.3-flash", "zai-org/GLM-5.3-Flash"} { + for _, stream := range []bool{false, true} { + for _, effort := range []string{"", "low", "high", "max"} { + t.Run(model+"/"+effort+"/stream="+map[bool]string{false: "false", true: "true"}[stream], func(t *testing.T) { + payload := map[string]interface{}{ + "model": "vllm/" + model, + "messages": []map[string]string{{"role": "user", "content": "What is 7 + 5?"}}, + "stream": stream, + "max_tokens": 100, + "temperature": 0.0, + } + if effort != "" { + payload["reasoning_effort"] = effort + } + raw, err := json.Marshal(payload) + require.NoError(t, err) + var inbound OpenAIChatRequest + require.NoError(t, sonic.Unmarshal(raw, &inbound)) + ctx := schemas.NewBifrostContext(nil, schemas.NoDeadline) + request := inbound.ToBifrostChatRequest(ctx) + require.Equal(t, schemas.VLLM, request.Provider) + require.Equal(t, model, request.Model) + outbound := ToOpenAIChatRequest(ctx, request) + require.NotNil(t, outbound) + outbound.Stream = inbound.Stream + wire, err := json.Marshal(outbound) + require.NoError(t, err) + var body map[string]interface{} + require.NoError(t, json.Unmarshal(wire, &body)) + if effort == "" { + require.NotContains(t, body, "reasoning_effort") + } else { + require.Equal(t, effort, body["reasoning_effort"]) + require.Equal(t, effort, *request.Params.Reasoning.Effort, "conversion must not mutate input") + } + require.Equal(t, stream, body["stream"]) + require.Equal(t, 0.0, body["temperature"]) + }) + } + } + } +} diff --git a/core/providers/openai/responses_marshal_test.go b/core/providers/openai/responses_marshal_test.go index 04292dfe3b4..6e6d1d685e0 100644 --- a/core/providers/openai/responses_marshal_test.go +++ b/core/providers/openai/responses_marshal_test.go @@ -179,6 +179,8 @@ func TestNormalizeOpenAIReasoningEffort(t *testing.T) { {"provider-prefixed gpt-5.6 keeps max", "openai/gpt-5.6", "max", "max"}, {"deepseek-v4 keeps max", "deepseek-v4", "max", "max"}, {"glm-5.2 keeps max", "glm-5.2", "max", "max"}, + {"glm-5.3-flash keeps max", "glm-5.3-flash", "max", "max"}, + {"namespaced glm-5.3-flash keeps max", "vllm/zai-org/GLM-5.3-Flash", "max", "max"}, {"gpt-5.5 downgrades max to xhigh", "gpt-5.5", "max", "xhigh"}, {"gpt-5.2 downgrades max to xhigh", "gpt-5.2", "max", "xhigh"}, {"gpt-5.5 keeps xhigh", "gpt-5.5", "xhigh", "xhigh"}, diff --git a/core/providers/openai/utils.go b/core/providers/openai/utils.go index 5a129a2c9b0..ed66604ee96 100644 --- a/core/providers/openai/utils.go +++ b/core/providers/openai/utils.go @@ -114,7 +114,8 @@ func acceptsMaxEffort(model string) bool { modelLower := bareModelLower(model) return strings.Contains(modelLower, "gpt-5.6") || strings.Contains(modelLower, "deepseek-v4") || - strings.Contains(modelLower, "glm-5.2") + strings.Contains(modelLower, "glm-5.2") || + strings.Contains(modelLower, "glm-5.3-flash") } // bareModelLower strips any provider prefix and lowercases, so the effort diff --git a/tests/e2e/api/collections/provider-harness.json b/tests/e2e/api/collections/provider-harness.json index 71e612b85a3..445d9fbbde1 100644 --- a/tests/e2e/api/collections/provider-harness.json +++ b/tests/e2e/api/collections/provider-harness.json @@ -140504,6 +140504,172 @@ ] } ] + }, + { + "name": "69. GLM-5.3-Flash max effort survives vLLM conversion", + "description": "Regression for GLM-5.3-Flash max being downgraded to high in the shared OpenAI converter. Requires a vllm key serving zai-org/GLM-5.3-Flash and client.allow_per_request_raw_override=true in the isolated harness gateway. Assert outbound raw_request, never infer effort from generated text or token counts. HF model card: https://huggingface.co/zai-org/GLM-5.3-Flash#note. Related: #6162 and #6054.", + "item": [ + { + "name": "vllm/zai-org/GLM-5.3-Flash max effort chat completion", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-bf-send-back-raw-request", + "value": "true" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"model\": \"vllm/zai-org/GLM-5.3-Flash\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"What is 7 + 5? Answer with the number only.\"\n }\n ],\n \"reasoning_effort\": \"max\",\n \"max_tokens\": 100,\n \"temperature\": 0,\n \"stream\": false\n}" + }, + "url": { + "raw": "{{baseUrl}}/openai/v1/chat/completions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "openai", + "v1", + "chat", + "completions" + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", + "var txt = pm.response.text();", + "pm.test('request succeeds', function () { pm.expect(pm.response.code, txt).to.be.below(400); });", + "var chunks = [];", + "chunks.push(pm.response.json());", + "var rr;", + "chunks.forEach(function (chunk) { var raw = (chunk.extra_fields || {}).raw_request; if (raw) { rr = typeof raw === 'string' ? JSON.parse(raw) : raw; } });", + "pm.test('outbound reasoning effort is preserved', function () {", + " pm.expect(rr, 'raw_request missing; enable allow_per_request_raw_override in the harness gateway').to.be.an('object');", + " pm.expect(rr.reasoning_effort).to.eql('max');", + "});" + ] + } + } + ] + }, + { + "name": "vllm/zai-org/GLM-5.3-Flash max effort streaming", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-bf-send-back-raw-request", + "value": "true" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"model\": \"vllm/zai-org/GLM-5.3-Flash\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"What is 7 + 5? Answer with the number only.\"\n }\n ],\n \"reasoning_effort\": \"max\",\n \"max_tokens\": 100,\n \"temperature\": 0,\n \"stream\": true\n}" + }, + "url": { + "raw": "{{baseUrl}}/openai/v1/chat/completions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "openai", + "v1", + "chat", + "completions" + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", + "var txt = pm.response.text();", + "pm.test('request succeeds', function () { pm.expect(pm.response.code, txt).to.be.below(400); });", + "var chunks = [];", + "txt.split('\\n').forEach(function (line) { if (line.indexOf('data:') !== 0) { return; } var data = line.slice(5).trim(); if (data && data !== '[DONE]') { chunks.push(JSON.parse(data)); } });", + "var rr;", + "chunks.forEach(function (chunk) { var raw = (chunk.extra_fields || {}).raw_request; if (raw) { rr = typeof raw === 'string' ? JSON.parse(raw) : raw; } });", + "pm.test('outbound reasoning effort is preserved', function () {", + " pm.expect(rr, 'raw_request missing; enable allow_per_request_raw_override in the harness gateway').to.be.an('object');", + " pm.expect(rr.reasoning_effort).to.eql('max');", + "});", + "pm.test('stream terminates', function () { pm.expect(txt).to.include('data: [DONE]'); });" + ] + } + } + ] + }, + { + "name": "vllm/zai-org/GLM-5.3-Flash low effort chat completion", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-bf-send-back-raw-request", + "value": "true" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"model\": \"vllm/zai-org/GLM-5.3-Flash\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"What is 7 + 5? Answer with the number only.\"\n }\n ],\n \"reasoning_effort\": \"low\",\n \"max_tokens\": 100,\n \"temperature\": 0,\n \"stream\": false\n}" + }, + "url": { + "raw": "{{baseUrl}}/openai/v1/chat/completions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "openai", + "v1", + "chat", + "completions" + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", + "var txt = pm.response.text();", + "pm.test('request succeeds', function () { pm.expect(pm.response.code, txt).to.be.below(400); });", + "var chunks = [];", + "chunks.push(pm.response.json());", + "var rr;", + "chunks.forEach(function (chunk) { var raw = (chunk.extra_fields || {}).raw_request; if (raw) { rr = typeof raw === 'string' ? JSON.parse(raw) : raw; } });", + "pm.test('outbound reasoning effort is preserved', function () {", + " pm.expect(rr, 'raw_request missing; enable allow_per_request_raw_override in the harness gateway').to.be.an('object');", + " pm.expect(rr.reasoning_effort).to.eql('low');", + "});" + ] + } + } + ] + } + ] } ] } \ No newline at end of file