// // OpenAIProviderTests.swift // oAITests // // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Copyright (C) 2026 Rune Olsen import Testing import Foundation @testable import oAI @Suite("OpenAIProvider request/response conversion") struct OpenAIProviderTests { private var provider: OpenAIProvider { OpenAIProvider(apiKey: "test-key") } // MARK: - buildURLRequest @Test("Sets the Authorization header and JSON content type") func setsAuthAndContentTypeHeaders() throws { let request = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "gpt-4o") let urlRequest = try provider.buildURLRequest(from: request, stream: false) #expect(urlRequest.value(forHTTPHeaderField: "Authorization") == "Bearer test-key") #expect(urlRequest.value(forHTTPHeaderField: "Content-Type") == "application/json") #expect(urlRequest.value(forHTTPHeaderField: "Accept") == nil) } @Test("Streaming requests add an SSE Accept header and stream_options") func streamingAddsAcceptHeaderAndUsageOption() throws { let request = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "gpt-4o") let urlRequest = try provider.buildURLRequest(from: request, stream: true) #expect(urlRequest.value(forHTTPHeaderField: "Accept") == "text/event-stream") let body = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] let streamOptions = body["stream_options"] as? [String: Any] #expect(streamOptions?["include_usage"] as? Bool == true) } @Test("o1/o3 reasoning models omit temperature even when one is requested") func reasoningModelsOmitTemperature() throws { let request = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "o1-preview", temperature: 0.7) let urlRequest = try provider.buildURLRequest(from: request, stream: false) let body = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] #expect(body["temperature"] == nil) } @Test("Non-reasoning models include the requested temperature") func nonReasoningModelsIncludeTemperature() throws { let request = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "gpt-4o", temperature: 0.7) let urlRequest = try provider.buildURLRequest(from: request, stream: false) let body = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] #expect(body["temperature"] as? Double == 0.7) } @Test("A message with an image attachment produces multi-part vision-format content") func imageAttachmentProducesMultipartContent() throws { let attachment = FileAttachment(path: "photo.png", type: .image, data: Data([0x01])) let message = Message(role: .user, content: "look at this", attachments: [attachment]) let request = ChatRequest(messages: [message], model: "gpt-4o") let urlRequest = try provider.buildURLRequest(from: request, stream: false) let body = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] let messages = body["messages"] as? [[String: Any]] let content = messages?.first?["content"] as? [[String: Any]] #expect(content?.count == 2) #expect(content?.first?["type"] as? String == "text") #expect(content?.last?["type"] as? String == "image_url") } // MARK: - convertToChatResponse @Test("Returns an empty-content response when there are no choices, rather than throwing") func convertToChatResponseEmptyChoicesFallback() { let apiResponse = OpenRouterChatResponse(id: "x", model: "m", choices: [], usage: nil, created: 0) let response = provider.convertToChatResponse(apiResponse) #expect(response.content == "") #expect(response.role == "assistant") } // MARK: - fallbackModels @Test("Fallback models are non-empty, all support tools, and are sorted by name") func fallbackModelsAreWellFormed() { let models = provider.fallbackModels() #expect(!models.isEmpty) #expect(models.allSatisfy { $0.capabilities.tools == true }) #expect(models.allSatisfy { $0.capabilities.online == false }) #expect(models.map(\.name) == models.map(\.name).sorted()) } }