MockModel.swift 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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. struct MockModel: LanguageModel {
  15. typealias Executor = MockModelExecutor
  16. /// An output the mock model produces on a single generation turn. Listed in
  17. /// the order the model should emit them across turns: a `.toolCall` is
  18. /// followed by a continuation turn once its output returns, so a sequence
  19. /// that calls a tool should end with a `.text` response.
  20. enum Event: Hashable {
  21. case toolCall(name: String, arguments: String)
  22. case text(String)
  23. }
  24. let events: [Event]
  25. let tokenCount: Int
  26. var capabilities: LanguageModelCapabilities {
  27. LanguageModelCapabilities(capabilities: [.toolCalling])
  28. }
  29. var executorConfiguration: MockModelExecutor.Configuration {
  30. MockModelExecutor.Configuration(events: events, tokenCount: tokenCount)
  31. }
  32. /// A model that responds with a single text response.
  33. init(textResponse: String, tokenCount: Int) {
  34. self.events = [.text(textResponse)]
  35. self.tokenCount = tokenCount
  36. }
  37. /// A model that emits `events` in order, one per generation turn. The event
  38. /// for each turn is chosen by counting how many turns have already been
  39. /// taken for the current prompt, so the sequence restarts on every prompt.
  40. init(events: [Event], tokenCount: Int) {
  41. self.events = events
  42. self.tokenCount = tokenCount
  43. }
  44. }
  45. struct MockModelExecutor: LanguageModelExecutor {
  46. struct Configuration: Hashable {
  47. var events: [MockModel.Event]
  48. var tokenCount: Int
  49. }
  50. let events: [MockModel.Event]
  51. let tokenCount: Int
  52. init(configuration: Configuration) throws {
  53. self.events = configuration.events
  54. self.tokenCount = configuration.tokenCount
  55. }
  56. nonisolated func respond(
  57. to request: LanguageModelExecutorGenerationRequest,
  58. model: MockModel,
  59. streamingInto channel: LanguageModelExecutorGenerationChannel
  60. ) async throws {
  61. switch event(for: request.transcript) {
  62. case .toolCall(let name, let arguments):
  63. await channel.send(
  64. .toolCalls(
  65. entryID: UUID().uuidString,
  66. action: .toolCall(
  67. id: UUID().uuidString,
  68. name: name,
  69. action: .appendArguments(arguments, tokenCount: tokenCount)
  70. )
  71. )
  72. )
  73. case .text(let text):
  74. let entryID = UUID().uuidString
  75. await channel.send(
  76. .response(
  77. entryID: entryID,
  78. action: .appendText(text, tokenCount: tokenCount)
  79. )
  80. )
  81. await channel.send(
  82. .response(
  83. entryID: entryID,
  84. action: .updateUsage(
  85. input: .init(totalTokenCount: tokenCount, cachedTokenCount: 0),
  86. output: .init(totalTokenCount: tokenCount, reasoningTokenCount: 0)
  87. )
  88. )
  89. )
  90. }
  91. }
  92. /// The event to emit for this turn: the number of model-generated entries
  93. /// (tool calls and responses) since the last prompt indexes into `events`,
  94. /// clamped to the final event so a sequence ending in `.text` always
  95. /// terminates.
  96. private func event(for transcript: Transcript) -> MockModel.Event {
  97. var index = 0
  98. for entry in transcript {
  99. switch entry {
  100. case .prompt:
  101. index = 0
  102. case .toolCalls, .response, .reasoning:
  103. index += 1
  104. default:
  105. break
  106. }
  107. }
  108. return events[min(index, events.count - 1)]
  109. }
  110. }