// // OpenRouterProviderTests.swift // oAITests // // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Copyright (C) 2026 Rune Olsen import Testing import Foundation @testable import oAI @Suite("OpenRouterProvider request/response conversion") struct OpenRouterProviderTests { private var provider: OpenRouterProvider { OpenRouterProvider(apiKey: "test-key") } // MARK: - buildAPIRequest @Test("Plain text message with no attachments uses simple string content") func buildRequestPlainText() throws { let request = ChatRequest(messages: [Message(role: .user, content: "hello")], model: "openai/gpt-4o") let apiRequest = try provider.buildAPIRequest(from: request) guard case .string(let text) = apiRequest.messages.first?.content else { Issue.record("Expected .string content for a message with no attachments") return } #expect(text == "hello") } @Test("Message with an image attachment uses array content with a base64 data URL") func buildRequestWithImageAttachment() throws { let attachment = FileAttachment(path: "photo.png", type: .image, data: Data([0x01, 0x02])) let message = Message(role: .user, content: "check this out", attachments: [attachment]) let request = ChatRequest(messages: [message], model: "openai/gpt-4o") let apiRequest = try provider.buildAPIRequest(from: request) guard case .array(let items) = apiRequest.messages.first?.content else { Issue.record("Expected .array content for a message with attachments") return } #expect(items.count == 2) // text + image } @Test("Online mode appends :online suffix, but not for image generation requests") func onlineModeSuffix() throws { let base = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o", onlineMode: true) let request = try provider.buildAPIRequest(from: base) #expect(request.model == "openai/gpt-4o:online") let imageGenRequest = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o", onlineMode: true, imageGeneration: true) let noSuffix = try provider.buildAPIRequest(from: imageGenRequest) #expect(noSuffix.model == "openai/gpt-4o") } @Test("Online mode does not double-append :online if the model already has it") func onlineModeNoDoubleSuffix() throws { let request = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o:online", onlineMode: true) let apiRequest = try provider.buildAPIRequest(from: request) #expect(apiRequest.model == "openai/gpt-4o:online") } @Test("Anthropic models get an explicit cache_control opt-in; others don't") func cacheControlOnlyForAnthropic() throws { let anthropicRequest = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "anthropic/claude-sonnet-4-5") let anthropicAPI = try provider.buildAPIRequest(from: anthropicRequest) #expect(anthropicAPI.cacheControl?.type == "ephemeral") let openAIRequest = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o") let openAIAPI = try provider.buildAPIRequest(from: openAIRequest) #expect(openAIAPI.cacheControl == nil) } // MARK: - convertToChatResponse @Test("Throws invalidResponse when there are no choices") func convertToChatResponseThrowsOnEmptyChoices() { let apiResponse = OpenRouterChatResponse(id: "x", model: "m", choices: [], usage: nil, created: 0) #expect(throws: (any Error).self) { try provider.convertToChatResponse(apiResponse) } } // MARK: - decodeImageOutputs @Test("Decodes a base64 data URL into raw Data") func decodeImageOutputsValidDataURL() { let payload = Data([0xDE, 0xAD, 0xBE, 0xEF]) let dataURL = "data:image/png;base64,\(payload.base64EncodedString())" let output = OpenRouterChatResponse.ImageOutput(imageUrl: .init(url: dataURL)) let decoded = provider.decodeImageOutputs([output]) #expect(decoded?.first == payload) } @Test("Returns nil for an empty outputs array") func decodeImageOutputsEmpty() { #expect(provider.decodeImageOutputs([]) == nil) } @Test("Skips a malformed URL with no comma separator") func decodeImageOutputsMalformedURL() { let output = OpenRouterChatResponse.ImageOutput(imageUrl: .init(url: "not-a-data-url")) #expect(provider.decodeImageOutputs([output]) == nil) } }