332 lines
8.8 KiB
Swift
332 lines
8.8 KiB
Swift
//
|
|
// OpenRouterModels.swift
|
|
// oAI
|
|
//
|
|
// OpenRouter API request and response models
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Copyright (C) 2026 Rune Olsen
|
|
//
|
|
// This file is part of oAI.
|
|
//
|
|
// oAI is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// oAI is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
|
|
// Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public
|
|
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
import Foundation
|
|
|
|
// MARK: - API Request
|
|
|
|
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]?
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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]?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case role
|
|
case content
|
|
case toolCalls = "tool_calls"
|
|
case images
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case promptTokens = "prompt_tokens"
|
|
case completionTokens = "completion_tokens"
|
|
case totalTokens = "total_tokens"
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 images: [OpenRouterChatResponse.ImageOutput]?
|
|
}
|
|
|
|
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: - Error Response
|
|
|
|
struct OpenRouterErrorResponse: Codable {
|
|
let error: ErrorDetail
|
|
|
|
struct ErrorDetail: Codable {
|
|
let message: String
|
|
let type: String?
|
|
let code: String?
|
|
}
|
|
}
|