diff --git a/Sources/OpenAI/Public/Models/ChatResult.swift b/Sources/OpenAI/Public/Models/ChatResult.swift index 4cb563a9..9e20319f 100644 --- a/Sources/OpenAI/Public/Models/ChatResult.swift +++ b/Sources/OpenAI/Public/Models/ChatResult.swift @@ -268,6 +268,19 @@ public struct ChatResult: Codable, Equatable, Sendable { case audioTokens = "audio_tokens" case cachedTokens = "cached_tokens" } + + public init(audioTokens: Int, cachedTokens: Int) { + self.audioTokens = audioTokens + self.cachedTokens = cachedTokens + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let parsingOptions = decoder.userInfo[.parsingOptions] as? ParsingOptions ?? [] + // OpenAI-compatible providers (e.g. DeepSeek, Gemini) often send only one of these fields. + self.audioTokens = try container.decodeInt(forKey: .audioTokens, parsingOptions: parsingOptions) + self.cachedTokens = try container.decodeInt(forKey: .cachedTokens, parsingOptions: parsingOptions) + } } public struct CompletionTokensDetails: Codable, Equatable, Sendable { diff --git a/Tests/OpenAITests/ChatResultTests.swift b/Tests/OpenAITests/ChatResultTests.swift index 11f91393..e28a1301 100644 --- a/Tests/OpenAITests/ChatResultTests.swift +++ b/Tests/OpenAITests/ChatResultTests.swift @@ -216,6 +216,42 @@ final class ChatResultTests: XCTestCase { XCTAssertEqual(usage.completionTokensDetails?.audioTokens, 35) } + func testPromptTokensDetailsMemberwiseInit() { + let details = ChatResult.CompletionUsage.PromptTokensDetails(audioTokens: 3, cachedTokens: 7) + let usage = ChatResult.CompletionUsage( + completionTokens: 1, promptTokens: 2, totalTokens: 3, promptTokensDetails: details + ) + XCTAssertEqual(usage.promptTokensDetails?.audioTokens, 3) + XCTAssertEqual(usage.promptTokensDetails?.cachedTokens, 7) + } + + func testUsageDecodesWhenPromptTokensDetailsOmitsAudioTokens() throws { + // Some OpenAI-compatible providers (e.g. DeepSeek, Gemini) only send `cached_tokens`. + let jsonString = """ + { + "id": "chatcmpl-1", + "object": "chat.completion", + "created": 1677652288, + "model": "deepseek-chat", + "choices": [], + "usage": { + "prompt_tokens": 60, + "completion_tokens": 40, + "total_tokens": 100, + "prompt_tokens_details": { + "cached_tokens": 12 + } + } + } + """ + + decoder.userInfo = [.parsingOptions: ParsingOptions.relaxed] + let usage = try XCTUnwrap(decode(jsonString).usage) + XCTAssertEqual(usage.totalTokens, 100) + XCTAssertEqual(usage.promptTokensDetails?.cachedTokens, 12) + XCTAssertEqual(usage.promptTokensDetails?.audioTokens, 0) + } + private func decode(_ jsonString: String) throws -> ChatResult { let jsonData = jsonString.data(using: .utf8)! return try decoder.decode(ChatResult.self, from: jsonData)