Add OpenRouter dedicated images API support

Fetches /api/v1/images/models in parallel with /models and merges results
into the model picker. Image-only models (e.g. Sourceful, Seedream, Flux
via this endpoint) were previously invisible since they don't appear in the
standard /models endpoint.

Models from the images API get usesImagesAPI=true and route through a new
generateImageAPIResponse() path in ChatViewModel that POSTs to /api/v1/images
with {model, prompt} instead of the chat completions endpoint. The response's
b64_json data is decoded and displayed via the existing GeneratedImagesView.

Cost is taken directly from the usage.cost field in the images API response
(USD per image) via a new rawCostUSD field on ChatResponse.Usage, bypassing
the token-based calculateCost() path used for chat models.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 14:34:56 +02:00
parent 40c5f25517
commit f2949cea3b
5 changed files with 243 additions and 23 deletions
+15 -1
View File
@@ -132,15 +132,19 @@ struct ChatResponse: Codable {
let totalTokens: Int
let cacheCreationInputTokens: Int?
let cacheReadInputTokens: Int?
/// Direct USD cost returned by the images API (bypasses token-based calculation).
let rawCostUSD: Double?
init(promptTokens: Int, completionTokens: Int, totalTokens: Int, cacheCreationInputTokens: Int? = nil, cacheReadInputTokens: Int? = nil) {
init(promptTokens: Int, completionTokens: Int, totalTokens: Int, cacheCreationInputTokens: Int? = nil, cacheReadInputTokens: Int? = nil, rawCostUSD: Double? = nil) {
self.promptTokens = promptTokens
self.completionTokens = completionTokens
self.totalTokens = totalTokens
self.cacheCreationInputTokens = cacheCreationInputTokens
self.cacheReadInputTokens = cacheReadInputTokens
self.rawCostUSD = rawCostUSD
}
// rawCostUSD is set programmatically, never decoded from API responses
enum CodingKeys: String, CodingKey {
case promptTokens = "prompt_tokens"
case completionTokens = "completion_tokens"
@@ -148,6 +152,16 @@ struct ChatResponse: Codable {
case cacheCreationInputTokens = "cache_creation_input_tokens"
case cacheReadInputTokens = "cache_read_input_tokens"
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
promptTokens = try c.decode(Int.self, forKey: .promptTokens)
completionTokens = try c.decode(Int.self, forKey: .completionTokens)
totalTokens = try c.decode(Int.self, forKey: .totalTokens)
cacheCreationInputTokens = try c.decodeIfPresent(Int.self, forKey: .cacheCreationInputTokens)
cacheReadInputTokens = try c.decodeIfPresent(Int.self, forKey: .cacheReadInputTokens)
rawCostUSD = nil
}
}
// Custom Codable since ToolCallInfo/generatedImages are not from API directly