fix: restore chat completion multi-choice support#672
Conversation
Greptile SummaryThis PR restores multi-choice support for chat completions by reintroducing the
|
| Filename | Overview |
|---|---|
| packages/data-designer-engine/src/data_designer/engine/models/clients/types.py | Adds ChatCompletionChoice dataclass and extends ChatCompletionResponse with a choices list and a messages property; backward compat preserved via __post_init__ guard. |
| packages/data-designer-engine/src/data_designer/engine/models/clients/parsing.py | Refactors response parsing to iterate over all choices instead of only the first; adds normalize_choice_list, parse_chat_completion_choice, and async counterparts. |
| packages/data-designer-engine/src/data_designer/engine/models/facade.py | Adds n to _COMPLETION_REQUEST_FIELDS so it forwards through completion(); strips n inside generate()/agenerate() loops because those methods return a single parsed result. |
| packages/data-designer-engine/src/data_designer/engine/models/clients/adapters/anthropic.py | Adds n to the Anthropic adapter's excluded-fields list, correctly preventing it from being forwarded to the Anthropic API. |
| packages/data-designer-engine/src/data_designer/engine/models/clients/init.py | Exports the new ChatCompletionChoice type alongside the existing public surface. |
Sequence Diagram
sequenceDiagram
participant Caller
participant ModelFacade
participant ModelClient
participant Parser
Caller->>ModelFacade: "completion(messages, n=4)"
ModelFacade->>ModelFacade: _build_chat_completion_request (n included)
ModelFacade->>ModelClient: "completion(ChatCompletionRequest[n=4])"
ModelClient->>Parser: parse_chat_completion_response(raw)
Parser->>Parser: normalize_choice_list(raw["choices"])
loop for each choice [0..n-1]
Parser->>Parser: parse_chat_completion_choice(choice)
end
Parser-->>ModelClient: "ChatCompletionResponse(message=choices[0].message, choices=[...])"
ModelClient-->>ModelFacade: ChatCompletionResponse
ModelFacade-->>Caller: ChatCompletionResponse
Caller->>ModelFacade: "generate(prompt, n=4)"
ModelFacade->>ModelFacade: completion_kwargs.pop("n", None)
ModelFacade->>ModelFacade: completion(messages) [no n]
ModelFacade-->>Caller: (parsed_result, messages)
Note over ModelFacade: Anthropic adapter also strips n from the outgoing HTTP body
Reviews (4): Last reviewed commit: "Merge branch 'main' into nmulepati/fix-6..." | Re-trigger Greptile
Review: PR #672 — fix: restore chat completion multi-choice supportSummaryThe PR restores chat-completion multi-choice support (issue #620) by:
The change is well-scoped and backward-compatible. Existing single-choice callers continue to work via FindingsCorrectness
API design
Tests
Style / conventions
Performance / security
Structural ImpactNo pre-computed structural impact analysis was available ( VerdictApprove with minor follow-ups. The core change is clean, backward-compatible, and well-tested for the sync OpenAI-compatible path. The two items I'd most like to see addressed before merge:
Nice-to-have: async multi-choice parsing test, soft validation of |
|
Addressed the feedback in
Validation:
|
Restore the chat completion n request field and preserve all returned choices in the canonical response while keeping response.message as the first choice. Add coverage for request forwarding, compatibility access, multi-choice parsing, and generate forwarding. Fixes #620 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Prevent generate and agenerate from forwarding multi-choice requests that they cannot expose, while keeping completion() multi-choice support intact. Add coverage for async parsing and Anthropic n exclusion. Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
b2e22f7 to
a0e0fc0
Compare
johnnygreco
left a comment
There was a problem hiding this comment.
I found one remaining issue after the direct n stripping update: generate()/agenerate() can still forward n when it comes from model/provider configuration or nested extra_body, so the single-result APIs may still request multiple choices and discard all but the first.
Validation I ran locally on the reviewed diff:
uv run pytest packages/data-designer-engine/tests/engine/models/clients/test_parsing.py packages/data-designer-engine/tests/engine/models/clients/test_openai_compatible.py packages/data-designer-engine/tests/engine/models/clients/test_anthropic.py packages/data-designer-engine/tests/engine/models/test_facade.py->188 passedgit diff --check 71997624b31045c8b0268055f4a14eb02acce905-> passed
|
|
||
| while True: | ||
| completion_kwargs = dict(kwargs) | ||
| completion_kwargs.pop("n", None) |
There was a problem hiding this comment.
[P2] This only removes a top-level kwarg before calling completion(). completion() immediately re-applies model/provider defaults via consolidate_kwargs(), and TransportKwargs flattens extra_body, so n from inference_parameters.extra_body, provider extra_body, or caller extra_body={"n": 4} is still sent on generate()/agenerate() and extra choices get discarded. Please clear n after consolidation or otherwise strip it from nested/configured extra_body for both generate paths.
📋 Summary
Restores chat completion multi-choice compatibility by reintroducing the
nrequest field and preserving all returned choices in the canonical response. Existing single-choice callers can continue usingresponse.message, while callers that request multiple completions can useresponse.choices.🔗 Related Issue
Fixes #620
🔄 Changes
ChatCompletionRequest.nand export a newChatCompletionChoiceresponse type.ChatCompletionResponse.choices, while keepingresponse.messageas the first choice for existing callers.nthroughModelFacade.completion()and OpenAI-compatible transport bodies, while excluding it from Anthropic request forwarding.nfromgenerate()andagenerate()before delegating to completion because those APIs return one parsed result.nexclusion, andgenerate(..., n=...)stripping.🔍 Attention Areas
types.py— updates the canonical chat completion response contract while preservingresponse.message.facade.py—completion(..., n=...)exposes multiple choices, whilegenerate(..., n=...)stripsnbecause it returns a single parsed result.🧪 Testing
make testpasses (not run; focused model suite was run instead)PYTHONPATH=packages/data-designer-config/src:packages/data-designer-engine/src:packages/data-designer/src uv run --group dev pytest packages/data-designer-engine/tests/engine/models -qpasses (532 passed)uv run --group dev ruff check ...passes for touched files✅ Checklist