Files
oai-swift/oAI/Providers/OpenRouterModels.swift
T
runeandClaude Sonnet 5 cf3f4ebfe4 Relicense oAI from AGPL-3.0-or-later to PolyForm Noncommercial 1.0.0
Switches the project from AGPL to a source-available license that
restricts commercial use — selling oAI or any part of it, standalone
or bundled into another product/service, now requires a separate
commercial license from the copyright holder. Noncommercial use,
study, modification, and sharing remain fully permitted.

Updates: LICENSE (canonical PolyForm Noncommercial 1.0.0 text +
commercial licensing contact note), SPDX headers and file-header
boilerplate across all Swift source files, the in-app About dialog's
license link (+ its localization catalog entry), README.md and
DEVELOPMENT.md license sections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:16:09 +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://mac.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?
}
}