New version v2.3.6

This commit is contained in:
2026-03-04 10:19:16 +01:00
parent 65a35cd508
commit 49f842f119
52 changed files with 14034 additions and 358 deletions

View File

@@ -61,6 +61,11 @@ struct ProviderCapabilities: Codable {
// MARK: - Chat Request
struct ReasoningConfig: Sendable {
let effort: String // "high", "medium", "low", "minimal"
let exclude: Bool // true = use reasoning internally, hide from response
}
struct ChatRequest {
let messages: [Message]
let model: String
@@ -72,6 +77,7 @@ struct ChatRequest {
let tools: [Tool]?
let onlineMode: Bool
let imageGeneration: Bool
let reasoning: ReasoningConfig?
init(
messages: [Message],
@@ -83,7 +89,8 @@ struct ChatRequest {
systemPrompt: String? = nil,
tools: [Tool]? = nil,
onlineMode: Bool = false,
imageGeneration: Bool = false
imageGeneration: Bool = false,
reasoning: ReasoningConfig? = nil
) {
self.messages = messages
self.model = model
@@ -95,6 +102,7 @@ struct ChatRequest {
self.tools = tools
self.onlineMode = onlineMode
self.imageGeneration = imageGeneration
self.reasoning = reasoning
}
}
@@ -174,6 +182,14 @@ struct StreamChunk {
let content: String?
let role: String?
let images: [Data]?
let thinking: String? // reasoning/thinking tokens (nil if excluded or not supported)
init(content: String?, role: String?, images: [Data]? = nil, thinking: String? = nil) {
self.content = content
self.role = role
self.images = images
self.thinking = thinking
}
}
var deltaContent: String? {

View File

@@ -27,6 +27,16 @@ 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]
@@ -37,6 +47,7 @@ struct OpenRouterChatRequest: Codable {
let tools: [Tool]?
let toolChoice: String?
let modalities: [String]?
let reasoning: ReasoningAPIConfig?
struct APIMessage: Codable {
let role: String
@@ -126,6 +137,7 @@ struct OpenRouterChatRequest: Codable {
case tools
case toolChoice = "tool_choice"
case modalities
case reasoning
}
}
@@ -205,7 +217,46 @@ struct OpenRouterStreamChunk: Codable {
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 {

View File

@@ -110,7 +110,8 @@ class OpenRouterProvider: AIProvider {
return output.contains("image")
}
return false
}()
}(),
thinking: modelData.supportedParameters?.contains("reasoning") ?? false
),
architecture: modelData.architecture.map { arch in
ModelInfo.Architecture(
@@ -368,6 +369,10 @@ class OpenRouterProvider: AIProvider {
effectiveModel = request.model
}
let reasoningConfig: ReasoningAPIConfig? = request.reasoning.map {
ReasoningAPIConfig(effort: $0.effort, exclude: $0.exclude ? true : nil)
}
return OpenRouterChatRequest(
model: effectiveModel,
messages: apiMessages,
@@ -377,7 +382,8 @@ class OpenRouterProvider: AIProvider {
topP: request.topP,
tools: request.tools,
toolChoice: request.tools != nil ? "auto" : nil,
modalities: request.imageGeneration ? ["text", "image"] : nil
modalities: request.imageGeneration ? ["text", "image"] : nil,
reasoning: reasoningConfig
)
}
@@ -416,7 +422,11 @@ class OpenRouterProvider: AIProvider {
throw ProviderError.invalidResponse
}
let images = choice.delta.images.flatMap { decodeImageOutputs($0) }
// Merge images from both sources: top-level `images` field and content-block images
let topLevelImages = choice.delta.images.flatMap { decodeImageOutputs($0) } ?? []
let blockImages = decodeImageOutputs(choice.delta.contentBlockImages) ?? []
let allImages = topLevelImages + blockImages
let images: [Data]? = allImages.isEmpty ? nil : allImages
return StreamChunk(
id: apiChunk.id,
@@ -424,7 +434,8 @@ class OpenRouterProvider: AIProvider {
delta: StreamChunk.Delta(
content: choice.delta.content,
role: choice.delta.role,
images: images
images: images,
thinking: choice.delta.reasoning
),
finishReason: choice.finishReason,
usage: apiChunk.usage.map { usage in