ChatCompletionsTests+UsageReporting.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Foundation Models open source project.
  4. //
  5. // Copyright © 2024-2027 Apple Inc. and the Foundation Models project authors.
  6. //
  7. // Licensed under the Apache License v2.0
  8. //
  9. // See LICENSE.txt for license information
  10. //
  11. //===----------------------------------------------------------------------===//
  12. import Foundation
  13. import FoundationModels
  14. @testable import FoundationModelsUtilities
  15. import Testing
  16. extension ChatCompletionsTests {
  17. @Suite struct UsageReporting {
  18. init() { MockSSEProtocol.reset() }
  19. @Test func `requests usage in stream options`() async throws {
  20. MockSSEProtocol.handler = { _ in (200, MockSSE.text("OK")) }
  21. let session = LanguageModelSession(model: makeMockModel())
  22. let _ = try await session.respond(to: "test")
  23. let body = try requestBody()
  24. let streamOptions = body["stream_options"] as? [String: Any]
  25. #expect(streamOptions != nil)
  26. #expect(streamOptions?["include_usage"] as? Bool == true)
  27. }
  28. @Test func `reports prompt and completion token counts from usage chunk`() async throws {
  29. MockSSEProtocol.handler = { _ in
  30. (
  31. 200,
  32. MockSSE.chunks(
  33. [
  34. MockSSE.Chunk(text: "Hello"),
  35. MockSSE.Chunk(text: " world"),
  36. MockSSE.Chunk(
  37. usage: MockSSE.Chunk.Usage(
  38. promptTokens: 12,
  39. completionTokens: 7
  40. )
  41. )
  42. ]
  43. )
  44. )
  45. }
  46. let session = LanguageModelSession(model: makeMockModel())
  47. let response = try await session.respond(to: "Say hello")
  48. #expect(response.usage.input.totalTokenCount == 12)
  49. #expect(response.usage.output.totalTokenCount == 7)
  50. }
  51. @Test func `reports cached and reasoning token counts when present`() async throws {
  52. MockSSEProtocol.handler = { _ in
  53. (
  54. 200,
  55. MockSSE.chunks([
  56. MockSSE.Chunk(text: "Done"),
  57. MockSSE.Chunk(
  58. usage: MockSSE.Chunk.Usage(
  59. promptTokens: 30,
  60. completionTokens: 15,
  61. cachedTokens: 20,
  62. reasoningTokens: 4
  63. )
  64. )
  65. ])
  66. )
  67. }
  68. let session = LanguageModelSession(model: makeMockModel())
  69. let response = try await session.respond(to: "Think carefully")
  70. #expect(response.usage.input.totalTokenCount == 30)
  71. #expect(response.usage.input.cachedTokenCount == 20)
  72. #expect(response.usage.output.totalTokenCount == 15)
  73. #expect(response.usage.output.reasoningTokenCount == 4)
  74. }
  75. @Test func `defaults cached and reasoning tokens to zero when omitted`() async throws {
  76. MockSSEProtocol.handler = { _ in
  77. (
  78. 200,
  79. MockSSE.chunks(
  80. [
  81. MockSSE.Chunk(text: "Hi"),
  82. MockSSE.Chunk(
  83. usage: MockSSE.Chunk.Usage(promptTokens: 5, completionTokens: 2)
  84. )
  85. ]
  86. )
  87. )
  88. }
  89. let session = LanguageModelSession(model: makeMockModel())
  90. let response = try await session.respond(to: "test")
  91. #expect(response.usage.input.cachedTokenCount == 0)
  92. #expect(response.usage.output.reasoningTokenCount == 0)
  93. }
  94. @Test func `reports final cumulative tokens when usage streams with each chunk`() async throws {
  95. // Some servers emit a `usage` snapshot on every chunk with running
  96. // cumulative totals. The framework treats updateUsage as wholesale
  97. // replacement, so the final reported usage should reflect the last
  98. // cumulative value.
  99. MockSSEProtocol.handler = { _ in
  100. (
  101. 200,
  102. MockSSE.chunks([
  103. MockSSE.Chunk(
  104. text: "Hello",
  105. usage: MockSSE.Chunk.Usage(promptTokens: 8, completionTokens: 1)
  106. ),
  107. MockSSE.Chunk(
  108. text: " there",
  109. usage: MockSSE.Chunk.Usage(promptTokens: 8, completionTokens: 2)
  110. ),
  111. MockSSE.Chunk(
  112. text: "!",
  113. usage: MockSSE.Chunk.Usage(promptTokens: 8, completionTokens: 3)
  114. )
  115. ])
  116. )
  117. }
  118. let session = LanguageModelSession(model: makeMockModel())
  119. let response = try await session.respond(to: "Greet me")
  120. #expect(response.content == "Hello there!")
  121. #expect(response.usage.input.totalTokenCount == 8)
  122. #expect(response.usage.output.totalTokenCount == 3)
  123. }
  124. @Test func `accepts text and usage in the same chunk`() async throws {
  125. // Verifies that a single chunk carrying both `delta.content` and
  126. // `usage` is processed without dropping either piece — the text is
  127. // streamed and the usage is reported.
  128. MockSSEProtocol.handler = { _ in
  129. (
  130. 200,
  131. MockSSE.chunks([
  132. MockSSE.Chunk(
  133. text: "Done",
  134. usage: MockSSE.Chunk.Usage(promptTokens: 4, completionTokens: 1)
  135. )
  136. ])
  137. )
  138. }
  139. let session = LanguageModelSession(model: makeMockModel())
  140. let response = try await session.respond(to: "ping")
  141. #expect(response.content == "Done")
  142. #expect(response.usage.input.totalTokenCount == 4)
  143. #expect(response.usage.output.totalTokenCount == 1)
  144. }
  145. }
  146. }