Repository navigation
fix(chat): tolerate missing fields in prompt_tokens_details - #483
pcbeingused333 wants to merge 3 commits into
Conversation
OpenAI-compatible providers (e.g. DeepSeek, Gemini) send only cached_tokens or only a subset of the fields. Decoding then failed and the whole usage was silently dropped by the try? in ChatResult/ChatStreamResult. Default the missing counters to 0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
nezhyborets
left a comment
There was a problem hiding this comment.
Thanks for the PR, please see the comment
| public init(from decoder: Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| // OpenAI-compatible providers (e.g. DeepSeek, Gemini) often send only one of these fields. | ||
| self.audioTokens = try container.decodeIfPresent(Int.self, forKey: .audioTokens) ?? 0 | ||
| self.cachedTokens = try container.decodeIfPresent(Int.self, forKey: .cachedTokens) ?? 0 | ||
| } |
There was a problem hiding this comment.
Hi, thanks! Please see other uses of decodeIfPresent, the usage of parsingOptions. Looks something like this
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let parsingOptions = decoder.userInfo[.parsingOptions] as? ParsingOptions ?? []
self.id = try container.decodeString(forKey: .id, parsingOptions: parsingOptions)
...
}
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Done, now via This comment was drafted with Claude Code. |
| case cachedTokens = "cached_tokens" | ||
| } | ||
|
|
||
| public init(from decoder: any Decoder) throws { |
There was a problem hiding this comment.
Declaring init(from:) in the struct body stops Swift from synthesizing the memberwise init(audioTokens:cachedTokens:), so PromptTokensDetails can now only be created by decoding, even from @testable tests. Could you either move init(from:) into an extension, or add a public init(audioTokens:cachedTokens:) like CompletionTokensDetails has? A public one would also make the promptTokensDetails: parameter of CompletionUsage.init usable for anyone mocking usage.
There was a problem hiding this comment.
🟡 Changes recommended
Missing detail fields still cause default-configured clients to discard the entire usage object, and the regression test’s relaxed parsing setting does not cover this default behavior.
1 open issue
What changed in this PR
This PR aims to preserve chat usage data when OpenAI-compatible providers omit prompt-token detail fields, without changing the public Int property types.
Changes:
- Adds custom decoding for prompt-token details using parsing options, shared by chat and streaming responses.
- Adds a regression test for a response containing only
cached_tokens, with relaxed parsing enabled.
| File | Description |
|---|---|
| Tests/OpenAITests/ChatResultTests.swift | Tests usage decoding when audio_tokens is omitted. |
| Sources/OpenAI/Public/Models/ChatResult.swift | Adds parsing-option-aware decoding for prompt-token details. |
Review effort: Balanced
|
Also just ran the checks to see if they find anything that needs fixing |
Declaring init(from:) in the struct body suppressed the synthesized memberwise initializer; restore it as a public init, matching CompletionTokensDetails.
|
Added This comment was drafted with Claude Code. |

What
ChatResult.CompletionUsage.PromptTokensDetailsnow decodes withdecodeIfPresent, defaulting a missingaudio_tokensorcached_tokensto0. A regression test decodes a response whoseprompt_tokens_detailscontains onlycached_tokens.Why
audioTokensandcachedTokenswere decoded as requiredInts. OpenAI-compatible providers (DeepSeek, Gemini's compat endpoint) often send onlycached_tokens. Decodingprompt_tokens_detailsthen threw, and thetry?that guardsusageinChatResultandChatStreamResultswallowed the error, so the entireusage(includingprompt_tokens,completion_tokens,total_tokens) came backnilinstead of just lacking the missing detail.Affected Areas
ChatResult.CompletionUsage.PromptTokensDetailsdecoding, which is used by bothChatResultandChatStreamResult. The property types stayInt, so there is no public API change. (Making themInt?, likeCompletionTokensDetails, would be an alternative but source-breaking.)Tests: full
swift testin aswift:6.2container (223 tests) passes; the new test fails without the change (XCTUnwrapgets anilusage). No linked issue.This change was prepared with the help of an AI assistant (Claude Code), and the author is responsible for the change.
🤖 Generated with Claude Code