Skip to content

Conversation

@roh26it
Copy link
Contributor

@roh26it roh26it commented Nov 25, 2025

Problem

The extra_body parameter was being passed incorrectly to the OpenAI SDK in chat.completions.create() and completions.create(). This caused the entire kwargs dict (including the extra_body key itself) to be merged into the request body.

Before this fix:

client.chat.completions.create(
    model="gpt-4",
    messages=[...],
    extra_body={'custom_field': 'value'}
)
# Request body contained: {"extra_body": {"custom_field": "value"}}  ❌ Wrong

After this fix:

client.chat.completions.create(
    model="gpt-4",
    messages=[...],
    extra_body={'custom_field': 'value'}
)
# Request body contains: {"custom_field": "value"}  ✅ Correct

Root Cause

In the internal stream_create and normal_create methods, the code was passing extra_body=kwargs directly to the OpenAI SDK. When a user passed extra_body={'a': 'b'}, it became part of kwargs as {'extra_body': {'a': 'b'}}, and this entire dict was passed to OpenAI's extra_body parameter.

Solution

The fix extracts extra_body from kwargs, merges its contents with remaining kwargs (excluding special params like extra_headers, extra_query, timeout), and passes the merged result to the OpenAI SDK's extra_body parameter. This matches the expected OpenAI SDK behavior.

⚠️ Breaking Change Notice

This fix changes the behavior of the extra_body parameter to match the OpenAI SDK convention:

Usage Pattern Old Behavior New Behavior
extra_body={'a': 'b'} Request body: {"extra_body": {"a": "b"}} Request body: {"a": "b"}
custom_field='value' (direct kwarg) Request body: {"custom_field": "value"} Request body: {"custom_field": "value"} (unchanged)

Who might be affected:

  • Users who were passing extra_body and unknowingly relying on the buggy behavior where "extra_body" appeared as a literal key in the request body
  • This is unlikely since it contradicts OpenAI SDK documentation and the Portkey backend wouldn't expect extra_body as a field name

Who benefits:

  • Users who expected extra_body to work like the OpenAI SDK (merging contents at the top level)
  • Users passing provider-specific parameters like thinking for Claude

Workaround users are unaffected:

  • Users who worked around the bug by passing custom fields directly as kwargs (e.g., custom_field='value') will see no change

Files Changed

  • portkey_ai/api_resources/apis/chat_complete.py: Fixed stream_create and normal_create (sync + async)
  • portkey_ai/api_resources/apis/complete.py: Fixed stream_create and normal_create (sync + async)

Note

This same bug pattern (extra_body=kwargs) exists in ~120 other places across the codebase (assistants, threads, images, audio, etc.). This PR fixes the core completions endpoints. Additional PRs can address the other endpoints if needed.

The extra_body parameter was being passed incorrectly to the OpenAI SDK,
causing the entire kwargs dict (including the 'extra_body' key itself)
to be merged into the request body.

Before this fix:
- User calls: create(..., extra_body={'a': 'b'})
- Request body contained: {'extra_body': {'a': 'b'}} (wrong)

After this fix:
- User calls: create(..., extra_body={'a': 'b'})
- Request body contains: {'a': 'b'} (correct)

The fix extracts extra_body from kwargs, merges its contents with
remaining kwargs, and passes the merged result to the OpenAI SDK's
extra_body parameter. This matches the expected OpenAI SDK behavior.

Fixed in:
- chat_complete.py: stream_create, normal_create (sync + async)
- complete.py: stream_create, normal_create (sync + async)
@roh26it roh26it requested review from VisargD and Copilot November 25, 2025 02:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where the extra_body parameter was incorrectly passed to the OpenAI SDK, causing the entire kwargs dict (including the extra_body key itself) to be nested in the request body instead of merging its contents at the top level.

Key Changes:

  • Extract and merge extra_body contents with remaining kwargs before passing to OpenAI SDK
  • Properly handle special parameters (extra_headers, extra_query, timeout) by extracting them separately
  • Apply fix to both sync and async methods in completions and chat completions APIs

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
portkey_ai/api_resources/apis/complete.py Fixed extra_body parameter handling in stream_create and normal_create methods (sync + async)
portkey_ai/api_resources/apis/chat_complete.py Fixed extra_body parameter handling in stream_create and normal_create methods (sync + async)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

extra_headers = kwargs.pop("extra_headers", None)
extra_query = kwargs.pop("extra_query", None)
timeout = kwargs.pop("timeout", None)
user_extra_body = kwargs.pop("extra_body", None) or {}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using or {} after pop() will replace {} (falsy empty dict) with {}, but more critically, it will replace None with {} which masks the distinction. If the intent is to treat missing extra_body differently from explicit extra_body=None, this logic is incorrect. If extra_body={} is passed explicitly, it becomes falsy and gets replaced with a new empty dict. Consider using if user_extra_body is None: user_extra_body = {} or kwargs.pop('extra_body', {}) instead.

Suggested change
user_extra_body = kwargs.pop("extra_body", None) or {}
user_extra_body = kwargs.pop("extra_body", None)
if user_extra_body is None:
user_extra_body = {}

Copilot uses AI. Check for mistakes.
extra_body=kwargs,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=merged_extra_body if merged_extra_body else None,
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional if merged_extra_body else None evaluates to None when merged_extra_body is an empty dict {}, which is falsy in Python. This means if both user_extra_body and kwargs are empty, extra_body=None is passed instead of extra_body={}. Depending on the OpenAI SDK's handling, this could behave differently. Consider using extra_body=merged_extra_body or None or simply extra_body=merged_extra_body if empty dicts are acceptable.

Suggested change
extra_body=merged_extra_body if merged_extra_body else None,
extra_body=merged_extra_body,

Copilot uses AI. Check for mistakes.
extra_headers = kwargs.pop("extra_headers", None)
extra_query = kwargs.pop("extra_query", None)
timeout = kwargs.pop("timeout", None)
user_extra_body = kwargs.pop("extra_body", None) or {}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using or {} after pop() will replace {} (falsy empty dict) with {}, but more critically, it will replace None with {} which masks the distinction. If the intent is to treat missing extra_body differently from explicit extra_body=None, this logic is incorrect. If extra_body={} is passed explicitly, it becomes falsy and gets replaced with a new empty dict. Consider using if user_extra_body is None: user_extra_body = {} or kwargs.pop('extra_body', {}) instead.

Suggested change
user_extra_body = kwargs.pop("extra_body", None) or {}
user_extra_body = kwargs.pop("extra_body", {})

Copilot uses AI. Check for mistakes.
extra_headers=extra_headers,
extra_body=kwargs,
extra_query=extra_query,
extra_body=merged_extra_body if merged_extra_body else None,
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional if merged_extra_body else None evaluates to None when merged_extra_body is an empty dict {}, which is falsy in Python. This means if both user_extra_body and kwargs are empty, extra_body=None is passed instead of extra_body={}. Depending on the OpenAI SDK's handling, this could behave differently. Consider using extra_body=merged_extra_body or None or simply extra_body=merged_extra_body if empty dicts are acceptable.

Suggested change
extra_body=merged_extra_body if merged_extra_body else None,
extra_body=merged_extra_body,

Copilot uses AI. Check for mistakes.
Remove unnecessary conditional when passing extra_body to OpenAI SDK.
Empty dict and None are both handled correctly by the SDK.
@roh26it
Copy link
Contributor Author

roh26it commented Nov 25, 2025

Responses to Review Comments

On keeping or {} pattern (comments on lines 45, 80)

Thanks for the suggestion! However, keeping the or {} pattern is intentional for safety. If a user explicitly passes extra_body=None, kwargs.pop('extra_body', {}) would return None, and then {**None, **kwargs} would raise a TypeError.

The or {} ensures we always have a dict to merge:

  • extra_body not present: None or {}{}
  • extra_body=None: None or {}{}
  • extra_body={}: {} or {}{}
  • extra_body={'a': 'b'}: {'a': 'b'} or {}{'a': 'b'}

On simplifying extra_body passing (comments on lines 68, 98)

✅ Applied! Removed the unnecessary conditional. The OpenAI SDK handles both None and {} correctly, so passing the dict directly is cleaner.

Commit: 1d9c53e

@roh26it roh26it requested a review from Copilot November 25, 2025 03:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@VisargD VisargD merged commit 9dad1cd into main Nov 25, 2025
5 of 6 checks passed
@VisargD VisargD deleted the fix/extra-body-parameter-handling branch November 25, 2025 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants