Repository navigation
fix(protocols): omit absent optional fields in chat completion responses - #202
Merged
luckyq merged 2 commits intoSep 2, 2026
Merged
Conversation
Align chat response serialization with the OpenAI spec (openai/openai-openapi v2.3.0): optional fields are omitted when absent instead of serialized as explicit null, while required-and-nullable fields stay always-present. Omit when None (previously serialized as null): - CreateChatCompletionResponse / CreateChatCompletionStreamResponse: service_tier, system_fingerprint, usage - ChatCompletionMessageToolCallChunk: id, type, function (continuation chunks now carry only index + function.arguments, matching OpenAI) - FunctionCallStream: name, arguments - ChatCompletionStreamResponseDeltaFunctionCall: name, arguments - ChatCompletionResponseMessage.reasoning_content (our extension, not in the spec at all) Always serialize again (was omitted; spec marks it required + nullable): - ChatCompletionResponseMessage.refusal — OpenAI emits "refusal": null on every non-refusal response Deliberately unchanged (required per spec; omitting would deviate): - choices[].finish_reason, non-stream choices[].logprobs Own CompletionUsage / PromptTokensDetails / CompletionTokensDetails and the legacy CreateCompletionResponse instead of re-exporting them: upstream async-openai 0.41.1 serializes absent usage-detail fields as null (e.g. "audio_tokens": null) although the spec marks them optional and non-nullable, and serializes the legacy response's optional system_fingerprint/usage as null. Field sets mirror upstream, so constructors are unaffected. Verified end-to-end on a live vLLM deployment (Qwen3-0.6B): 47 shape assertions across non-stream, stream, tool-calling, include_usage, logprobs, n>1, and reasoning-parser paths. Closes ai-dynamo#157 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: doxu <doxu@nvidia.com>
…alization Address review: replacing the re-exported async-openai CompletionUsage / PromptTokensDetails / CompletionTokensDetails (and the legacy CreateCompletionResponse) with same-named local structs was a source-breaking API change — callers holding upstream values could no longer assign them to the public usage fields. Restore the upstream re-exports and fix the wire shape instead with a serialize_with shadow on the chat response usage fields, so absent usage-details fields are omitted without changing any public type identity. The legacy completions surface is no longer touched by this PR. Also per review: drop pre-existing-behavior delta assertions from the stream shape test and remove narrating test comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: doxu <doxu@nvidia.com>
Contributor
Author
indrajit96
self-requested a review
August 31, 2026 18:32
Contributor
[P3] Update the usage-details description after restoring upstream typesThe “Also fixed: usage-details nulls” section still says At current head Suggested fixUpdate this section to describe the |
indrajit96
approved these changes
Sep 1, 2026
indrajit96
left a comment
Contributor
There was a problem hiding this comment.
LGTM!
Thanks for adding this! @luckyq great work!
Contributor
|
/ok to test 7035dfb |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Aligns chat completion response serialization with the official OpenAI spec (openai/openai-openapi v2.3.0): optional fields are omitted when absent instead of serialized as explicit
null, while required-and-nullable fields stay always-present.The two axes are orthogonal —
requiredgoverns whether the key must be present;nullablegoverns whether the value may benull:null(e.g.refusal, non-streamlogprobs)finish_reason)nullare both spec-legalnullis invalid (e.g.tool_calls)Field by field:
usage/service_tier/system_fingerprint(non-stream + stream)nullid/type/function,function.namenullon continuation chunksmessage.reasoning_content(our extension, not in spec)nullChatCompletionStreamResponseDeltaFunctionCall.name/argumentsnullmessage.refusal"refusal": nullon every responsechoices[].finish_reason, non-streamchoices[].logprobsmessage.tool_calls/function_callOne nuance on streaming
usage: withstream_options.include_usage, OpenAI emits"usage": nullon chunks before the final one. Omitting the key is still spec-legal, so we omit whenNone; callers needing exact-null mirroring can inject it at the HTTP boundary (documented on the struct).Also fixed: usage-details nulls (same bug class)
Upstream async-openai 0.41.1 serializes absent usage-detail fields as
null("audio_tokens": null) although the spec marks them optional and non-nullable — OpenAI emits"audio_tokens": 0, nevernull.Fixed at the serialization boundary rather than by owning types: the two chat response
usagefields carry aserialize_withthat routes through a private shadow struct which skips absent fields.CompletionUsage,PromptTokensDetails, andCompletionTokensDetailsremain upstream re-exports, so public type identity is unchanged and callers holding upstream values still compile; deserialization stays derived. The legacy completions surface (CreateCompletionResponse) is not touched by this PR.(An earlier revision of this PR replaced those types with same-named local structs — a source-breaking change, reverted in 7035dfb after review. The upstream serialization bug is worth reporting to async-openai separately.)
Validation
null), including a deserialization round-trip through theserialize_withpathcargo test --workspace,cargo clippy --all-targets --all-features -- -D warnings,cargo fmt --checkall clean[patch.crates-io]): 47 shape assertions across non-stream, stream, tool-calling,include_usage,logprobs/top_logprobs,n>1, and reasoning-parser paths — including a recursive sweep asserting no unexpectednullanywhere in any response. Liveusageat current head:{"prompt_tokens":10,"completion_tokens":25,"total_tokens":35,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":24}}— both shadow branches exercised, nonullfields.Closes #157
🤖 Generated with Claude Code