New version v2.3.6
This commit is contained in:
@@ -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? {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user