Files
oai-swift/oAI/Providers/OpenRouterModels.swift
T
runeandClaude Sonnet 5 0af644456e Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:34:04 +02:00

493 lines
15 KiB
Swift

//
// OpenRouterModels.swift
// oAI
//
// OpenRouter API request and response models
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import Foundation
// MARK: - API Request
struct ReasoningAPIConfig: Codable {
let effort: String
let exclude: Bool?
enum CodingKeys: String, CodingKey {
case effort
case exclude
}
}
struct OpenRouterChatRequest: Codable {
let model: String
let messages: [APIMessage]
var stream: Bool
let maxTokens: Int?
let temperature: Double?
let topP: Double?
let tools: [Tool]?
let toolChoice: String?
let modalities: [String]?
let reasoning: ReasoningAPIConfig?
let cacheControl: CacheControl?
struct CacheControl: Codable {
let type: String
}
struct APIMessage: Codable {
let role: String
let content: MessageContent
enum MessageContent: Codable {
case string(String)
case array([ContentItem])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self) {
self = .string(str)
} else if let arr = try? container.decode([ContentItem].self) {
self = .array(arr)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid content")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let str):
try container.encode(str)
case .array(let arr):
try container.encode(arr)
}
}
}
enum ContentItem: Codable {
case text(String)
case image(ImageContent)
struct TextContent: Codable {
let type: String // "text"
let text: String
}
struct ImageContent: Codable {
let type: String // "image_url"
let imageUrl: ImageURL
struct ImageURL: Codable {
let url: String
}
enum CodingKeys: String, CodingKey {
case type
case imageUrl = "image_url"
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let textContent = try? container.decode(TextContent.self), textContent.type == "text" {
self = .text(textContent.text)
} else if let image = try? container.decode(ImageContent.self) {
self = .image(image)
} else if let str = try? container.decode(String.self) {
self = .text(str)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid content item")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .text(let text):
try container.encode(TextContent(type: "text", text: text))
case .image(let image):
try container.encode(image)
}
}
}
}
enum CodingKeys: String, CodingKey {
case model
case messages
case stream
case maxTokens = "max_tokens"
case temperature
case topP = "top_p"
case tools
case toolChoice = "tool_choice"
case modalities
case reasoning
case cacheControl = "cache_control"
}
}
// MARK: - API Response
struct OpenRouterChatResponse: Codable {
let id: String
let model: String
let choices: [Choice]
let usage: Usage?
let created: Int
struct Choice: Codable {
let index: Int
let message: MessageContent
let finishReason: String?
struct MessageContent: Codable {
let role: String
let content: String?
let toolCalls: [APIToolCall]?
let images: [ImageOutput]?
// Images extracted from content[] blocks (e.g. GPT-5 Image response format)
let contentBlockImages: [ImageOutput]
private struct ContentBlock: Codable {
let type: String
let text: String?
let imageUrl: ImageOutput.ImageURL?
enum CodingKeys: String, CodingKey {
case type, text
case imageUrl = "image_url"
}
}
enum CodingKeys: String, CodingKey {
case role
case content
case toolCalls = "tool_calls"
case images
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
role = try c.decode(String.self, forKey: .role)
toolCalls = try c.decodeIfPresent([APIToolCall].self, forKey: .toolCalls)
images = try c.decodeIfPresent([ImageOutput].self, forKey: .images)
// content can be a plain String OR an array of content blocks
if let text = try? c.decodeIfPresent(String.self, forKey: .content) {
content = text
contentBlockImages = []
} else if let blocks = try? c.decodeIfPresent([ContentBlock].self, forKey: .content) {
content = blocks.compactMap { $0.text }.joined().nonEmptyOrNil
contentBlockImages = blocks.compactMap { block in
guard block.type == "image_url", let url = block.imageUrl else { return nil }
return ImageOutput(imageUrl: url)
}
} else {
content = nil
contentBlockImages = []
}
}
}
enum CodingKeys: String, CodingKey {
case index
case message
case finishReason = "finish_reason"
}
}
struct ImageOutput: Codable {
let imageUrl: ImageURL
struct ImageURL: Codable {
let url: String
}
enum CodingKeys: String, CodingKey {
case imageUrl = "image_url"
}
}
struct Usage: Codable {
let promptTokens: Int
let completionTokens: Int
let totalTokens: Int
let promptTokensDetails: PromptTokensDetails?
struct PromptTokensDetails: Codable {
let cachedTokens: Int?
let cacheWriteTokens: Int?
enum CodingKeys: String, CodingKey {
case cachedTokens = "cached_tokens"
case cacheWriteTokens = "cache_write_tokens"
}
}
enum CodingKeys: String, CodingKey {
case promptTokens = "prompt_tokens"
case completionTokens = "completion_tokens"
case totalTokens = "total_tokens"
case promptTokensDetails = "prompt_tokens_details"
}
}
}
// MARK: - Streaming Response
struct OpenRouterStreamChunk: Codable {
let id: String
let model: String
let choices: [StreamChoice]
let usage: OpenRouterChatResponse.Usage?
struct StreamChoice: Codable {
let index: Int
let delta: Delta
let finishReason: String?
struct Delta: Codable {
let role: String?
let content: String?
let reasoning: String?
// images[] from top-level delta field (custom OpenRouter format)
let images: [OpenRouterChatResponse.ImageOutput]?
// images extracted from content[] array (standard OpenAI content-block format)
let contentBlockImages: [OpenRouterChatResponse.ImageOutput]
private struct ContentBlock: Codable {
let type: String
let text: String?
let imageUrl: OpenRouterChatResponse.ImageOutput.ImageURL?
enum CodingKeys: String, CodingKey {
case type, text
case imageUrl = "image_url"
}
}
enum CodingKeys: String, CodingKey {
case role, content, images, reasoning
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
role = try c.decodeIfPresent(String.self, forKey: .role)
images = try c.decodeIfPresent([OpenRouterChatResponse.ImageOutput].self, forKey: .images)
reasoning = try c.decodeIfPresent(String.self, forKey: .reasoning)
// content can be a plain String OR an array of content blocks
if let text = try? c.decodeIfPresent(String.self, forKey: .content) {
content = text
contentBlockImages = []
} else if let blocks = try? c.decodeIfPresent([ContentBlock].self, forKey: .content) {
content = blocks.compactMap { $0.text }.joined().nonEmptyOrNil
contentBlockImages = blocks.compactMap { block in
guard block.type == "image_url", let url = block.imageUrl else { return nil }
return OpenRouterChatResponse.ImageOutput(imageUrl: url)
}
} else {
content = nil
contentBlockImages = []
}
}
}
enum CodingKeys: String, CodingKey {
case index
case delta
case finishReason = "finish_reason"
}
}
}
// MARK: - Models List
struct OpenRouterModelsResponse: Codable {
let data: [ModelData]
struct ModelData: Codable {
let id: String
let name: String
let description: String?
let contextLength: Int
let pricing: PricingData
let architecture: Architecture?
let supportedParameters: [String]?
let outputModalities: [String]?
struct PricingData: Codable {
let prompt: String
let completion: String
}
struct Architecture: Codable {
let modality: String?
let tokenizer: String?
let instructType: String?
enum CodingKeys: String, CodingKey {
case modality
case tokenizer
case instructType = "instruct_type"
}
}
enum CodingKeys: String, CodingKey {
case id
case name
case description
case contextLength = "context_length"
case pricing
case architecture
case supportedParameters = "supported_parameters"
case outputModalities = "output_modalities"
}
}
}
// MARK: - Credits Response
struct OpenRouterCreditsResponse: Codable {
let data: CreditsData
struct CreditsData: Codable {
let totalCredits: Double?
let totalUsage: Double?
enum CodingKeys: String, CodingKey {
case totalCredits = "total_credits"
case totalUsage = "total_usage"
}
}
}
// MARK: - Tool Call Models
struct APIToolCall: Codable {
let id: String
let type: String
let function: FunctionCall
struct FunctionCall: Codable {
let name: String
let arguments: String
}
}
/// Message shape for encoding assistant messages that contain tool calls
struct AssistantToolCallMessage: Encodable {
let role: String
let content: String?
let toolCalls: [APIToolCall]
enum CodingKeys: String, CodingKey {
case role
case content
case toolCalls = "tool_calls"
}
}
/// Message shape for encoding tool result messages back to the API
struct ToolResultMessage: Encodable {
let role: String // "tool"
let toolCallId: String
let name: String
let content: String
enum CodingKeys: String, CodingKey {
case role
case toolCallId = "tool_call_id"
case name
case content
}
}
// MARK: - Images API — Model Discovery
struct OpenRouterImageModelsResponse: Codable {
let data: [ImageModelData]
struct ImageModelData: Codable {
let id: String
let name: String
let description: String?
let architecture: Architecture?
let supportsStreaming: Bool?
struct Architecture: Codable {
let inputModalities: [String]?
let outputModalities: [String]?
enum CodingKeys: String, CodingKey {
case inputModalities = "input_modalities"
case outputModalities = "output_modalities"
}
}
enum CodingKeys: String, CodingKey {
case id, name, description, architecture
case supportsStreaming = "supports_streaming"
}
}
}
// MARK: - Images API — Generation Response
struct OpenRouterImageGenerationResponse: Codable {
let created: Int?
let data: [ImageData]
let usage: Usage?
struct ImageData: Codable {
let b64Json: String
let mediaType: String?
enum CodingKeys: String, CodingKey {
case b64Json = "b64_json"
case mediaType = "media_type"
}
}
struct Usage: Codable {
let promptTokens: Int
let completionTokens: Int
let totalTokens: Int
let cost: Double?
enum CodingKeys: String, CodingKey {
case promptTokens = "prompt_tokens"
case completionTokens = "completion_tokens"
case totalTokens = "total_tokens"
case cost
}
}
}
// MARK: - Error Response
struct OpenRouterErrorResponse: Codable {
let error: ErrorDetail
struct ErrorDetail: Codable {
let message: String
let type: String?
let code: String?
}
}