Skip to content

Commit 3456009

Browse files
kunwarshivamnacx
andauthored
translator: guard nil response body in OpenAI ResponseBody (#2240)
**Description** A JSON `null` response body decodes into a nil `*ChatCompletionResponse` without an error, because the decode target is a `**ChatCompletionResponse` (`Decode(&resp)`). Some OpenAI-compatible backends return a `null` body with a 200 status (e.g. an upstream that aborts the response after committing headers under load). In the non-streaming path `ResponseBody` then dereferenced the nil response at `resp.Usage.PromptTokens`, panicking the ext-proc with a SIGSEGV (addr 0x80). `RedactBody` already guards `resp == nil`, so the nil case is known; the main path just missed it. This treats a nil response as an empty response: report zero token usage and fall back to the request model, mirroring the streaming path's behavior when no usage is present, rather than crashing. A regression test feeds a literal `null` body and asserts no panic and zero usage. Per the project's generative-AI policy: this change was prepared with the assistance of an AI coding tool (Claude Code). The submitter understands and takes full ownership of the change. **Related Issues/PRs (if applicable)** Fixes #2273 **Special notes for reviewers (if applicable)** N/A Signed-off-by: Kunwar Srivastav <kshivamsrivastav@gmail.com> Co-authored-by: Ignasi Barrera <ignasi@tetrate.io>
1 parent 5a8584a commit 3456009

2 files changed

Lines changed: 18 additions & 0 deletions

File tree

internal/translator/openai_openai.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ func (o *openAIToOpenAITranslatorV1ChatCompletion) ResponseBody(_ map[string]str
147147
if err := json.NewDecoder(body).Decode(&resp); err != nil {
148148
return nil, nil, tokenUsage, responseModel, fmt.Errorf("failed to unmarshal body: %w", err)
149149
}
150+
// A JSON `null` body decodes into a nil *resp without an error (the decode
151+
// target is a **ChatCompletionResponse), which some upstreams return with a
152+
// 200 status. Treat it as an empty response so we report zero usage and fall
153+
// back to the request model, mirroring the streaming path, instead of
154+
// dereferencing nil below.
155+
if resp == nil {
156+
resp = &openai.ChatCompletionResponse{}
157+
}
150158

151159
// Redact and log response when enabled
152160
if o.debugLogEnabled && o.enableRedaction && o.logger != nil {

internal/translator/openai_openai_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,16 @@ data: [DONE]
350350
_, _, _, _, err := o.ResponseBody(nil, bytes.NewBuffer([]byte("invalid")), false, nil)
351351
require.Error(t, err)
352352
})
353+
t.Run("null body", func(t *testing.T) {
354+
// A literal JSON `null` decodes to a nil response without an error;
355+
// it must not panic and should report zero usage.
356+
s := &testotel.MockSpan{}
357+
o := &openAIToOpenAITranslatorV1ChatCompletion{requestModel: "gpt-4o-mini"}
358+
_, _, usedToken, responseModel, err := o.ResponseBody(nil, bytes.NewBuffer([]byte("null")), false, s)
359+
require.NoError(t, err)
360+
require.Equal(t, tokenUsageFrom(0, -1, -1, 0, 0, -1), usedToken)
361+
require.Equal(t, "gpt-4o-mini", responseModel)
362+
})
353363
t.Run("valid body", func(t *testing.T) {
354364
s := &testotel.MockSpan{}
355365
var resp openai.ChatCompletionResponse

0 commit comments

Comments
 (0)