ChatCompletionsLanguageModel.swift 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  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. public import Foundation
  13. #if canImport(FoundationNetworking)
  14. public import FoundationNetworking
  15. #endif
  16. public import FoundationModels
  17. #if canImport(CoreImage)
  18. private import CoreImage
  19. private import UniformTypeIdentifiers
  20. #endif
  21. /// A `LanguageModel` that talks to any OpenAI-compatible
  22. /// `/chat/completions` endpoint, streaming results back through the
  23. /// Foundation Models framework.
  24. ///
  25. /// Use this model to drive a `LanguageModelSession` against any remote
  26. /// service that implements the OpenAI Chat Completions API.
  27. ///
  28. /// ```swift
  29. /// let model = ChatCompletionsLanguageModel(
  30. /// name: "your-model-name",
  31. /// url: URL(string: "https://api.example.com")!,
  32. /// additionalHeaders: ["Authorization": "Bearer \(apiKey)"]
  33. /// )
  34. ///
  35. /// let session = LanguageModelSession(model: model)
  36. /// let response = try await session.respond(to: "Hello!")
  37. /// ```
  38. public struct ChatCompletionsLanguageModel: Sendable, LanguageModel {
  39. /// The name of the underlying model, sent in the `model` field of each
  40. /// chat completion request.
  41. public var name: String
  42. /// The base URL of the chat completions endpoint. The path
  43. /// `/v1/chat/completions` is appended automatically when the supplied
  44. /// URL does not already include a `v1` segment.
  45. public var url: URL
  46. /// Headers added to every outgoing request, merged on top of the
  47. /// defaults. Use this to provide authorization tokens or other
  48. /// vendor-specific headers.
  49. public var additionalHeaders: [String: String]
  50. public var supportsGuidedGeneration: Bool
  51. // Overridden in tests to inject a URLSession with mock protocol handlers.
  52. var urlSession: URLSession?
  53. /// Creates a chat completions language model.
  54. ///
  55. /// - Parameters:
  56. /// - name: The model identifier sent in the `model` field of each
  57. /// request.
  58. /// - url: The base URL of the chat completions endpoint.
  59. /// - additionalHeaders: Headers to merge on top of the defaults
  60. /// (for example, an `Authorization` header).
  61. /// - supportsGuidedGeneration: Whether the endpoint supports the
  62. /// `response_format` field for structured output. Defaults to `true`.
  63. /// - urlSessionConfiguration: An optional `URLSessionConfiguration` used
  64. /// to build the `URLSession` that drives requests. Use this to tune
  65. /// timeouts, proxies, or other transport settings. When `nil`, an
  66. /// ephemeral configuration is used.
  67. public init(
  68. name: String,
  69. url: URL,
  70. additionalHeaders: [String: String] = [:],
  71. supportsGuidedGeneration: Bool = true,
  72. urlSessionConfiguration: URLSessionConfiguration? = nil
  73. ) {
  74. self.name = name
  75. self.url = url
  76. self.additionalHeaders = additionalHeaders
  77. self.supportsGuidedGeneration = supportsGuidedGeneration
  78. self.urlSession = urlSessionConfiguration.map { URLSession(configuration: $0) }
  79. }
  80. // Implementation of LanguageModel Protocol
  81. public var capabilities: LanguageModelCapabilities {
  82. if supportsGuidedGeneration {
  83. LanguageModelCapabilities([.vision, .toolCalling, .reasoning, .guidedGeneration])
  84. } else {
  85. LanguageModelCapabilities([.vision, .toolCalling, .reasoning])
  86. }
  87. }
  88. public var executorConfiguration: Executor.Configuration {
  89. Executor.Configuration(
  90. modelName: name,
  91. url: url,
  92. additionalHeaders: additionalHeaders,
  93. urlSession: urlSession
  94. )
  95. }
  96. /// An error returned by the chat completions endpoint in the body of a
  97. /// failed request or a streaming error event.
  98. ///
  99. /// Servers may populate any subset of the optional fields.
  100. public struct APIError: LocalizedError {
  101. /// A human-readable explanation of the error returned by the server.
  102. public var message: String
  103. /// The error category reported by the server (for example,
  104. /// `"invalid_request_error"`).
  105. public var type: String?
  106. /// The name of the request parameter associated with the error,
  107. /// when applicable.
  108. public var param: String?
  109. /// A short machine-readable error code provided by the server.
  110. public var code: String?
  111. /// Creates a new API error.
  112. ///
  113. /// - Parameters:
  114. /// - message: A human-readable explanation of the error.
  115. /// - type: The error category reported by the server.
  116. /// - param: The request parameter associated with the error.
  117. /// - code: A short machine-readable error code.
  118. public init(
  119. message: String,
  120. type: String? = nil,
  121. param: String? = nil,
  122. code: String? = nil
  123. ) {
  124. self.message = message
  125. self.type = type
  126. self.param = param
  127. self.code = code
  128. }
  129. }
  130. /// An error raised by ``ChatCompletionsLanguageModel`` when a request
  131. /// cannot be issued or its response cannot be parsed.
  132. public enum RequestError: LocalizedError {
  133. /// The request could not be constructed because of invalid input.
  134. /// The associated value contains a human-readable description.
  135. case invalidRequest(_ description: String)
  136. /// A streaming chunk could not be decoded as a chat completion event.
  137. case invalidStreamData
  138. /// The endpoint returned a non-200 HTTP status code. The associated
  139. /// values contain the status code and the raw response body.
  140. case httpError(statusCode: Int, data: Data)
  141. public var errorDescription: String? {
  142. switch self {
  143. case .invalidRequest(let description):
  144. "Invalid request: \(description)"
  145. case .invalidStreamData:
  146. "Invalid streaming data received"
  147. case .httpError(let statusCode, let data):
  148. """
  149. HTTP error with status code \(statusCode):
  150. \(String(data: data, encoding: .utf8) ?? data.description)
  151. """
  152. }
  153. }
  154. }
  155. /// The wire format of an error envelope returned by the chat completions
  156. /// endpoint, used internally to decode error responses before raising
  157. /// them as ``APIError``.
  158. struct ErrorResponse: Codable, Sendable {
  159. var error: APIError
  160. struct APIError: Codable, Sendable {
  161. var message: String
  162. var type: String?
  163. var param: String?
  164. var code: String?
  165. }
  166. }
  167. public struct Executor: LanguageModelExecutor {
  168. public typealias Model = ChatCompletionsLanguageModel
  169. private let configuration: Configuration
  170. public init(configuration: Configuration) {
  171. self.configuration = configuration
  172. }
  173. public struct Configuration: Hashable, Sendable {
  174. fileprivate let modelName: String
  175. fileprivate let url: URL
  176. fileprivate let additionalHeaders: [String: String]
  177. fileprivate let urlSession: URLSession?
  178. public static func == (lhs: Configuration, rhs: Configuration) -> Bool {
  179. lhs.modelName == rhs.modelName
  180. && lhs.url == rhs.url
  181. && lhs.additionalHeaders == rhs.additionalHeaders
  182. }
  183. public func hash(into hasher: inout Hasher) {
  184. hasher.combine(modelName)
  185. hasher.combine(url)
  186. hasher.combine(additionalHeaders)
  187. }
  188. }
  189. public func respond(
  190. to request: LanguageModelExecutorGenerationRequest,
  191. model: ChatCompletionsLanguageModel,
  192. streamingInto channel: LanguageModelExecutorGenerationChannel
  193. ) async throws {
  194. // Caller-supplied headers override the defaults on conflict.
  195. let headers = [
  196. "Content-Type": "application/json",
  197. "Accept": "text/event-stream",
  198. "User-Agent": Bundle.main.bundleIdentifier ?? "com.apple.FoundationModels"
  199. ].merging(
  200. configuration.additionalHeaders,
  201. uniquingKeysWith: { _, custom in custom }
  202. )
  203. // Tests inject a URLSession; production uses a fresh ephemeral one.
  204. let client = ChatCompletionsClient(
  205. baseURL: configuration.url,
  206. headers: headers,
  207. session: configuration.urlSession ?? URLSession(configuration: .ephemeral)
  208. )
  209. // Translate the framework's request into the OpenAI-compatible wire format.
  210. let chatRequest = ChatCompletionsClient.ChatCompletionRequest(
  211. model: configuration.modelName,
  212. messages: try convertedTranscript(request.transcript),
  213. temperature: request.generationOptions.temperature,
  214. topP: try request.generationOptions.samplingMode.map(topP),
  215. maxCompletionTokens: request.generationOptions.maximumResponseTokens,
  216. tools: request.enabledToolDefinitions.map { tool in
  217. ChatCompletionsClient.Tool(
  218. function: ChatCompletionsClient.Tool.Function(
  219. name: tool.name,
  220. description: tool.description,
  221. parameters: tool.parameters
  222. )
  223. )
  224. },
  225. toolChoice: ChatCompletionsClient.ChatCompletionRequest.ToolChoice(
  226. mode: {
  227. switch request.generationOptions.toolCallingMode?.kind {
  228. case .allowed, .none: .auto
  229. case .required: .required
  230. case .disallowed: .none
  231. @unknown default: .auto
  232. }
  233. }()
  234. ),
  235. responseFormat: request.schema.map { schema in
  236. ChatCompletionsClient.ResponseFormat(
  237. jsonSchema: ChatCompletionsClient.ResponseFormat.JSONSchemaWrapper(
  238. name: schema.name,
  239. schema: schema
  240. )
  241. )
  242. }
  243. )
  244. // Stream the response back into the framework via `channel`.
  245. try await Self.processChunks(
  246. client.streamChatCompletions(request: chatRequest),
  247. into: channel
  248. )
  249. }
  250. private static func processChunks<ChunkSequence: AsyncSequence>(
  251. _ chunks: ChunkSequence,
  252. into channel: LanguageModelExecutorGenerationChannel
  253. ) async throws where ChunkSequence.Element == ChatCompletionsClient.ChatCompletionChunk {
  254. // Per-index `id`/`name` for tool calls. The first delta for a given
  255. // index supplies them; later deltas at the same index typically carry
  256. // only argument fragments and are routed using these latched values.
  257. // Argument accumulation is the framework's job — we just forward each
  258. // delta via `.appendArguments`.
  259. var toolCallRouting: [Int: (id: String, name: String)] = [:]
  260. // Stable entryIDs per event type for the duration of this stream.
  261. // Without these, interleaved reasoning/response/toolCalls chunks would
  262. // split into multiple transcript entries — the framework only coalesces
  263. // consecutive events of the same type into the trailing entry.
  264. let responseEntryID = UUID().uuidString
  265. let reasoningEntryID = UUID().uuidString
  266. let toolCallsEntryID = UUID().uuidString
  267. for try await chunk in chunks {
  268. if let delta = chunk.choices.first?.delta {
  269. if let reasoning = delta.reasoningContent {
  270. await channel.send(
  271. .reasoning(
  272. entryID: reasoningEntryID,
  273. action: .appendText(reasoning, tokenCount: 1)
  274. )
  275. )
  276. }
  277. if let toolCallDeltas = delta.toolCalls {
  278. for toolCallDelta in toolCallDeltas {
  279. let existing = toolCallRouting[toolCallDelta.index] ?? (id: "", name: "")
  280. let routing = (
  281. id: existing.id + (toolCallDelta.id ?? ""),
  282. name: existing.name + (toolCallDelta.function?.name ?? "")
  283. )
  284. toolCallRouting[toolCallDelta.index] = routing
  285. guard !routing.id.isEmpty, !routing.name.isEmpty else { continue }
  286. await channel.send(
  287. .toolCalls(
  288. entryID: toolCallsEntryID,
  289. action: .toolCall(
  290. id: routing.id,
  291. name: routing.name,
  292. action: .appendArguments(
  293. toolCallDelta.function?.arguments ?? "",
  294. tokenCount: 1
  295. )
  296. )
  297. )
  298. )
  299. }
  300. } else if let text = delta.content {
  301. await channel.send(
  302. .response(
  303. entryID: responseEntryID,
  304. action: .appendText(text, tokenCount: 1)
  305. )
  306. )
  307. }
  308. }
  309. // Send usage AFTER content so the authoritative cumulative total
  310. // overwrites any tokens credited by `appendText` for this chunk.
  311. if let usage = chunk.usage {
  312. await channel.send(
  313. .response(
  314. entryID: responseEntryID,
  315. action: .updateUsage(
  316. input: .init(
  317. totalTokenCount: usage.promptTokens,
  318. cachedTokenCount: usage.promptTokensDetails?.cachedTokens ?? 0
  319. ),
  320. output: .init(
  321. totalTokenCount: usage.completionTokens,
  322. reasoningTokenCount: usage.completionTokensDetails?.reasoningTokens ?? 0
  323. )
  324. )
  325. )
  326. )
  327. }
  328. }
  329. }
  330. private func topP(_ sampling: GenerationOptions.SamplingMode) throws -> Double {
  331. switch sampling.kind {
  332. case .greedy:
  333. return 0
  334. case .randomTopK:
  335. throw ChatCompletionsLanguageModel.RequestError.invalidRequest(
  336. "Top K sampling is not supported"
  337. )
  338. case .randomProbabilityThreshold(let threshold, let seed):
  339. guard seed == nil else {
  340. throw ChatCompletionsLanguageModel.RequestError.invalidRequest(
  341. "Setting a random seed is not supported"
  342. )
  343. }
  344. return threshold
  345. @unknown default:
  346. throw ChatCompletionsLanguageModel.RequestError.invalidRequest(
  347. "Unknown sampling mode \(sampling.kind) is not supported"
  348. )
  349. }
  350. }
  351. private func convertedTranscript(
  352. _ entries: some Collection<Transcript.Entry>
  353. ) throws -> [ChatCompletionsClient.ChatMessage] {
  354. // Converts a single transcript segment into chat-completion message content.
  355. func convertedSegment(
  356. _ segment: Transcript.Segment,
  357. in entry: Transcript.Entry
  358. ) throws -> [ChatCompletionsClient.MessageContent] {
  359. switch segment {
  360. case .text(let text):
  361. return [
  362. ChatCompletionsClient.MessageContent(
  363. text: text.content
  364. )
  365. ]
  366. // Structured content is serialized to JSON text on the wire.
  367. case .structure(let structure):
  368. return [
  369. ChatCompletionsClient.MessageContent(
  370. text: structure.content.jsonString
  371. )
  372. ]
  373. case .attachment(let attachment):
  374. switch attachment.content {
  375. case .image(let image):
  376. #if canImport(CoreImage)
  377. // Images are inlined as base64 data URLs (JPEG).
  378. let base64String = image.cgImage.jpegData().base64EncodedString()
  379. let dataURL = URL(string: "data:image/jpeg;base64,\(base64String)")!
  380. let imageURL = ChatCompletionsClient.MessageContent.ImageURL(url: dataURL)
  381. return [ChatCompletionsClient.MessageContent(imageURL: imageURL)]
  382. #else
  383. guard let url = image.url else {
  384. throw LanguageModelError.unsupportedTranscriptContent(
  385. LanguageModelError.UnsupportedTranscriptContent(
  386. unsupportedContent: [entry],
  387. debugDescription: "Image attachment without a URL is not supported by \(Self.self) on this platform."
  388. )
  389. )
  390. }
  391. let dataURL: URL
  392. if url.scheme == "data" {
  393. dataURL = url
  394. } else {
  395. let data = try Data(contentsOf: url)
  396. let base64String = data.base64EncodedString()
  397. dataURL = URL(string: "data:image/jpeg;base64,\(base64String)")!
  398. }
  399. let imageURL = ChatCompletionsClient.MessageContent.ImageURL(url: dataURL)
  400. return [ChatCompletionsClient.MessageContent(imageURL: imageURL)]
  401. #endif
  402. @unknown default:
  403. throw LanguageModelError.unsupportedTranscriptContent(
  404. LanguageModelError.UnsupportedTranscriptContent(
  405. unsupportedContent: [entry],
  406. debugDescription: "Attachment type not supported by \(Self.self)."
  407. )
  408. )
  409. }
  410. case .custom:
  411. throw LanguageModelError.unsupportedTranscriptContent(
  412. LanguageModelError.UnsupportedTranscriptContent(
  413. unsupportedContent: [entry],
  414. debugDescription: "Custom segments are not supported by \(Self.self)"
  415. )
  416. )
  417. @unknown default:
  418. throw LanguageModelError.unsupportedTranscriptContent(
  419. LanguageModelError.UnsupportedTranscriptContent(
  420. unsupportedContent: [entry],
  421. debugDescription: "Unknown segment type not supported by \(Self.self)"
  422. )
  423. )
  424. }
  425. }
  426. var messages: [ChatCompletionsClient.ChatMessage] = []
  427. // Reasoning entries are buffered and attached to the next assistant
  428. // message (response or toolCalls) via `reasoning_content`. If a turn
  429. // has only reasoning with no following assistant entry, it's emitted
  430. // as a standalone assistant message.
  431. var pendingReasoning: String? = nil
  432. func consumePendingReasoning() -> String? {
  433. defer { pendingReasoning = nil }
  434. return pendingReasoning
  435. }
  436. // Translate each transcript entry into one chat-completion message.
  437. for entry in entries {
  438. switch entry {
  439. case .instructions(let instructions):
  440. // Instructions become system-role messages.
  441. messages.append(
  442. ChatCompletionsClient.ChatMessage(
  443. role: .system,
  444. content: try instructions.segments.flatMap { try convertedSegment($0, in: entry) }
  445. )
  446. )
  447. case .prompt(let prompt):
  448. // User prompts; flush any orphaned reasoning as a message first.
  449. if let reasoning = consumePendingReasoning() {
  450. messages.append(
  451. ChatCompletionsClient.ChatMessage(
  452. role: .assistant,
  453. reasoningContent: reasoning
  454. )
  455. )
  456. }
  457. messages.append(
  458. ChatCompletionsClient.ChatMessage(
  459. role: .user,
  460. content: try prompt.segments.flatMap { try convertedSegment($0, in: entry) }
  461. )
  462. )
  463. case .toolCalls(let toolCalls):
  464. // Tool calls ride along on an assistant message, with any buffered reasoning attached.
  465. messages.append(
  466. ChatCompletionsClient.ChatMessage(
  467. role: .assistant,
  468. toolCalls: toolCalls.map { call in
  469. ChatCompletionsClient.ToolCall(
  470. id: call.id,
  471. function: ChatCompletionsClient.ToolCall.FunctionCall(
  472. name: call.toolName,
  473. arguments: call.arguments.jsonString
  474. )
  475. )
  476. },
  477. reasoningContent: consumePendingReasoning()
  478. )
  479. )
  480. case .toolOutput(let toolOutput):
  481. // Tool outputs become tool-role messages keyed by the originating call ID.
  482. messages.append(
  483. ChatCompletionsClient.ChatMessage(
  484. role: .tool,
  485. content: try toolOutput.segments.flatMap { try convertedSegment($0, in: entry) },
  486. toolCallID: toolOutput.id
  487. )
  488. )
  489. case .response(let response):
  490. // Assistant responses; attach any buffered reasoning to this message.
  491. messages.append(
  492. ChatCompletionsClient.ChatMessage(
  493. role: .assistant,
  494. content: try response.segments.flatMap { try convertedSegment($0, in: entry) },
  495. reasoningContent: consumePendingReasoning()
  496. )
  497. )
  498. case .reasoning(let reasoning):
  499. // Buffer reasoning text; it will attach to the next assistant entry.
  500. let text = reasoning.segments.compactMap { segment -> String? in
  501. if case .text(let textSegment) = segment { return textSegment.content }
  502. return nil
  503. }.joined()
  504. pendingReasoning = (pendingReasoning ?? "") + text
  505. @unknown default:
  506. continue
  507. }
  508. }
  509. // Trailing reasoning with no following assistant entry — emit it solo.
  510. if let reasoning = consumePendingReasoning() {
  511. messages.append(
  512. ChatCompletionsClient.ChatMessage(
  513. role: .assistant,
  514. reasoningContent: reasoning
  515. )
  516. )
  517. }
  518. return messages
  519. }
  520. }
  521. }
  522. private struct ChatCompletionsClient {
  523. let baseURL: URL
  524. let headers: [String: String]
  525. let session: URLSession
  526. func streamChatCompletions(
  527. request: ChatCompletionRequest
  528. ) -> AsyncThrowingStream<ChatCompletionChunk, Swift.Error> {
  529. AsyncThrowingStream { continuation in
  530. let task = Task {
  531. do {
  532. let urlRequest = try buildURLRequest(for: request)
  533. #if canImport(Darwin)
  534. let (stream, response) = try await session.bytes(for: urlRequest)
  535. let httpResponse = response as! HTTPURLResponse
  536. guard httpResponse.statusCode == 200 else {
  537. throw ChatCompletionsLanguageModel.RequestError.httpError(
  538. statusCode: httpResponse.statusCode,
  539. data: try await stream.reduce(Data(), { $0 + [$1] })
  540. )
  541. }
  542. for try await line in stream.lines {
  543. if let chunk = try parseStreamLine(line) {
  544. continuation.yield(chunk)
  545. }
  546. }
  547. continuation.finish()
  548. #else
  549. let (data, response) = try await session.data(for: urlRequest)
  550. let httpResponse = response as! HTTPURLResponse
  551. guard httpResponse.statusCode == 200 else {
  552. throw ChatCompletionsLanguageModel.RequestError.httpError(
  553. statusCode: httpResponse.statusCode,
  554. data: data
  555. )
  556. }
  557. let body = String(data: data, encoding: .utf8) ?? ""
  558. for line in body.split(separator: "\n", omittingEmptySubsequences: false) {
  559. if let chunk = try parseStreamLine(String(line)) {
  560. continuation.yield(chunk)
  561. }
  562. }
  563. continuation.finish()
  564. #endif
  565. } catch {
  566. continuation.finish(throwing: error)
  567. }
  568. }
  569. continuation.onTermination = { _ in task.cancel() }
  570. }
  571. }
  572. private func buildURLRequest(for request: ChatCompletionRequest) throws -> URLRequest {
  573. let isVersioned = baseURL.pathComponents.contains("v1")
  574. let endpoint = isVersioned ? "/chat/completions" : "/v1/chat/completions"
  575. let url = baseURL.appendingPathComponent(endpoint)
  576. var urlRequest = URLRequest(url: url)
  577. urlRequest.httpMethod = "POST"
  578. for (header, value) in headers {
  579. urlRequest.setValue(value, forHTTPHeaderField: header)
  580. }
  581. let encoder = JSONEncoder()
  582. urlRequest.httpBody = try encoder.encode(request)
  583. return urlRequest
  584. }
  585. func parseStreamLine(_ line: String) throws -> ChatCompletionChunk? {
  586. let trimmedLine = line.trimmingCharacters(in: .whitespaces)
  587. // Skip empty lines and comments
  588. guard !trimmedLine.isEmpty, !trimmedLine.hasPrefix(":") else {
  589. return nil
  590. }
  591. if trimmedLine.hasPrefix("data: ") {
  592. let jsonString = String(trimmedLine.dropFirst(6)) // Remove "data: "
  593. if jsonString.trimmingCharacters(in: .whitespaces) == "[DONE]" {
  594. return nil
  595. }
  596. guard let jsonData = jsonString.data(using: .utf8) else {
  597. throw ChatCompletionsLanguageModel.RequestError.invalidStreamData
  598. }
  599. let decoder = JSONDecoder()
  600. do {
  601. return try decoder.decode(ChatCompletionChunk.self, from: jsonData)
  602. } catch {
  603. if let response = try? decoder.decode(
  604. ChatCompletionsLanguageModel.ErrorResponse.self,
  605. from: jsonData
  606. ) {
  607. throw ChatCompletionsLanguageModel.APIError(
  608. message: response.error.message,
  609. type: response.error.type,
  610. param: response.error.param,
  611. code: response.error.code
  612. )
  613. }
  614. throw error
  615. }
  616. }
  617. return nil
  618. }
  619. struct ChatCompletionRequest: Encodable {
  620. enum ToolChoiceMode: String, Encodable {
  621. case auto
  622. case required
  623. case none
  624. }
  625. struct ToolChoice: Encodable {
  626. let mode: ToolChoiceMode
  627. func encode(to encoder: any Encoder) throws {
  628. var container = encoder.singleValueContainer()
  629. try container.encode(mode)
  630. }
  631. }
  632. var model: String
  633. var messages: [ChatMessage]
  634. var temperature: Double?
  635. var topP: Double?
  636. var maxCompletionTokens: Int?
  637. var tools: [Tool]?
  638. var toolChoice: ChatCompletionRequest.ToolChoice?
  639. var responseFormat: ResponseFormat?
  640. var stream = true
  641. var streamOptions = StreamOptions(includeUsage: true)
  642. struct StreamOptions: Encodable {
  643. var includeUsage: Bool
  644. private enum CodingKeys: String, CodingKey {
  645. case includeUsage = "include_usage"
  646. }
  647. }
  648. private enum CodingKeys: String, CodingKey {
  649. case model
  650. case messages
  651. case temperature
  652. case topP = "top_p"
  653. case maxCompletionTokens = "max_completion_tokens"
  654. case tools
  655. case responseFormat = "response_format"
  656. case stream
  657. case streamOptions = "stream_options"
  658. case toolChoice = "tool_choice"
  659. }
  660. }
  661. struct ChatMessage: Encodable {
  662. var role: Role
  663. var content: [MessageContent]
  664. var toolCalls: [ToolCall]?
  665. var toolCallID: String?
  666. var reasoningContent: String?
  667. private enum CodingKeys: String, CodingKey {
  668. case role
  669. case content
  670. case toolCalls = "tool_calls"
  671. case toolCallID = "tool_call_id"
  672. case reasoningContent = "reasoning_content"
  673. }
  674. enum Role: String, Encodable {
  675. case system
  676. case user
  677. case assistant
  678. case tool
  679. }
  680. init(
  681. role: Role,
  682. content: [MessageContent] = [],
  683. toolCalls: [ToolCall]? = nil,
  684. toolCallID: String? = nil,
  685. reasoningContent: String? = nil
  686. ) {
  687. self.role = role
  688. self.content = content
  689. self.toolCalls = toolCalls
  690. self.toolCallID = toolCallID
  691. self.reasoningContent = reasoningContent
  692. }
  693. func encode(to encoder: Encoder) throws {
  694. var container = encoder.container(keyedBy: CodingKeys.self)
  695. try container.encode(role, forKey: .role)
  696. let hasToolCalls = toolCalls?.isEmpty == false
  697. let compactText = content.count == 1 ? content.first?.text : nil
  698. if let compactText {
  699. try container.encode(compactText, forKey: .content)
  700. } else if !hasToolCalls && !content.isEmpty {
  701. try container.encode(content, forKey: .content)
  702. }
  703. try container.encodeIfPresent(toolCalls, forKey: .toolCalls)
  704. try container.encodeIfPresent(toolCallID, forKey: .toolCallID)
  705. try container.encodeIfPresent(reasoningContent, forKey: .reasoningContent)
  706. }
  707. }
  708. struct Tool: Encodable {
  709. var type: String = "function"
  710. var function: Function
  711. struct Function: Encodable {
  712. let name: String
  713. let description: String
  714. let parameters: GenerationSchema
  715. }
  716. }
  717. struct ToolCall: Codable {
  718. var id: String
  719. var type = "function"
  720. var function: FunctionCall
  721. struct FunctionCall: Codable {
  722. var name: String
  723. var arguments: String
  724. }
  725. }
  726. struct ResponseFormat: Encodable {
  727. var type = "json_schema"
  728. var jsonSchema: JSONSchemaWrapper
  729. private enum CodingKeys: String, CodingKey {
  730. case type
  731. case jsonSchema = "json_schema"
  732. }
  733. struct JSONSchemaWrapper: Encodable {
  734. var name: String
  735. var description: String?
  736. var schema: GenerationSchema
  737. var strict = true
  738. }
  739. }
  740. struct ChatCompletionChunk: Decodable {
  741. let id: String
  742. let model: String
  743. let choices: [Choice]
  744. let usage: Usage?
  745. struct Choice: Decodable {
  746. let delta: Delta
  747. struct Delta: Decodable {
  748. var role: String?
  749. var content: String?
  750. var reasoningContent: String?
  751. var toolCalls: [ToolCallDelta]?
  752. enum CodingKeys: String, CodingKey {
  753. case role
  754. case content
  755. case reasoningContent = "reasoning_content"
  756. case toolCalls = "tool_calls"
  757. }
  758. }
  759. }
  760. struct ToolCallDelta: Decodable {
  761. let index: Int
  762. let id: String?
  763. let type: String?
  764. let function: FunctionCallDelta?
  765. struct FunctionCallDelta: Decodable {
  766. let name: String?
  767. let arguments: String?
  768. }
  769. }
  770. fileprivate struct Usage: Decodable {
  771. let promptTokens: Int
  772. let completionTokens: Int
  773. let promptTokensDetails: PromptTokensDetails?
  774. let completionTokensDetails: CompletionTokensDetails?
  775. fileprivate struct PromptTokensDetails: Decodable {
  776. let cachedTokens: Int?
  777. private enum CodingKeys: String, CodingKey {
  778. case cachedTokens = "cached_tokens"
  779. }
  780. }
  781. fileprivate struct CompletionTokensDetails: Decodable {
  782. let reasoningTokens: Int?
  783. private enum CodingKeys: String, CodingKey {
  784. case reasoningTokens = "reasoning_tokens"
  785. }
  786. }
  787. private enum CodingKeys: String, CodingKey {
  788. case promptTokens = "prompt_tokens"
  789. case completionTokens = "completion_tokens"
  790. case promptTokensDetails = "prompt_tokens_details"
  791. case completionTokensDetails = "completion_tokens_details"
  792. }
  793. }
  794. }
  795. struct MessageContent: Codable {
  796. var type: ContentType
  797. var text: String?
  798. var imageURL: ImageURL?
  799. enum CodingKeys: String, CodingKey {
  800. case type
  801. case text
  802. case imageURL = "image_url"
  803. }
  804. enum ContentType: String, Codable {
  805. case text
  806. case imageURL = "image_url"
  807. }
  808. struct ImageURL: Codable {
  809. var url: URL
  810. var detail: String? = "auto"
  811. }
  812. init(text: String) {
  813. self.type = .text
  814. self.text = text
  815. self.imageURL = nil
  816. }
  817. init(imageURL: ImageURL) {
  818. self.type = .imageURL
  819. self.text = nil
  820. self.imageURL = imageURL
  821. }
  822. }
  823. }
  824. #if canImport(CoreImage)
  825. private extension CGImage {
  826. func jpegData() -> Data {
  827. let imageData = NSMutableData()
  828. let destination = CGImageDestinationCreateWithData(
  829. /* data */ imageData,
  830. /* format */ UTType.jpeg.identifier as CFString,
  831. /* count */ 1,
  832. /* options */ nil
  833. )!
  834. CGImageDestinationAddImage(destination, self, nil)
  835. CGImageDestinationFinalize(destination)
  836. return Data(referencing: imageData)
  837. }
  838. }
  839. #endif