First public release v2.3.1

This commit is contained in:
2026-02-19 16:39:23 +01:00
parent 52e3d0c07e
commit f3d673ab27
15 changed files with 1032 additions and 60 deletions

View File

@@ -70,8 +70,9 @@ class AnthropicProvider: AIProvider {
return false
}
// MARK: - Models (hardcoded Anthropic has no public models list endpoint)
// MARK: - Models
/// Local metadata used to enrich API results (pricing, context length) and as offline fallback.
private static let knownModels: [ModelInfo] = [
ModelInfo(
id: "claude-opus-4-6",
@@ -123,12 +124,64 @@ class AnthropicProvider: AIProvider {
),
]
/// Fetch live model list from GET /v1/models, enriched with local pricing/context metadata.
/// Falls back to knownModels if the request fails (no key, offline, etc.).
func listModels() async throws -> [ModelInfo] {
return Self.knownModels
guard let url = URL(string: "\(baseURL)/models") else { return Self.knownModels }
var request = URLRequest(url: url)
request.httpMethod = "GET"
do {
try await applyAuth(to: &request)
} catch {
Log.api.warning("Anthropic listModels: auth failed, using fallback — \(error.localizedDescription)")
return Self.knownModels
}
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
Log.api.warning("Anthropic listModels: HTTP \(code), using fallback")
return Self.knownModels
}
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let items = json["data"] as? [[String: Any]] else {
Log.api.warning("Anthropic listModels: unexpected JSON shape, using fallback")
return Self.knownModels
}
let enrichment = Dictionary(uniqueKeysWithValues: Self.knownModels.map { ($0.id, $0) })
let models: [ModelInfo] = items.compactMap { item in
guard let id = item["id"] as? String,
id.hasPrefix("claude-") else { return nil }
let displayName = item["display_name"] as? String ?? id
if let known = enrichment[id] { return known }
// Unknown new model use display name and sensible defaults
return ModelInfo(
id: id,
name: displayName,
description: item["description"] as? String ?? "",
contextLength: 200_000,
pricing: .init(prompt: 0, completion: 0),
capabilities: .init(vision: true, tools: true, online: false)
)
}
Log.api.info("Anthropic listModels: fetched \(models.count) model(s) from API")
return models.isEmpty ? Self.knownModels : models
} catch {
Log.api.warning("Anthropic listModels: network error (\(error.localizedDescription)), using fallback")
return Self.knownModels
}
}
func getModel(_ id: String) async throws -> ModelInfo? {
return Self.knownModels.first { $0.id == id }
let models = try await listModels()
return models.first { $0.id == id }
}
// MARK: - Chat Completion
@@ -212,7 +265,7 @@ class AnthropicProvider: AIProvider {
var body: [String: Any] = [
"model": model,
"messages": conversationMessages,
"max_tokens": maxTokens ?? 4096,
"max_tokens": maxTokens ?? 16000,
"stream": false
]
if let systemText = systemText {
@@ -282,6 +335,7 @@ class AnthropicProvider: AIProvider {
var currentId = ""
var currentModel = request.model
var inputTokens = 0
for try await line in bytes.lines {
// Anthropic SSE: "event: ..." and "data: {...}"
@@ -299,6 +353,9 @@ class AnthropicProvider: AIProvider {
if let message = event["message"] as? [String: Any] {
currentId = message["id"] as? String ?? ""
currentModel = message["model"] as? String ?? request.model
if let usageDict = message["usage"] as? [String: Any] {
inputTokens = usageDict["input_tokens"] as? Int ?? 0
}
}
case "content_block_delta":
@@ -321,7 +378,7 @@ class AnthropicProvider: AIProvider {
var usage: ChatResponse.Usage? = nil
if let usageDict = event["usage"] as? [String: Any] {
let outputTokens = usageDict["output_tokens"] as? Int ?? 0
usage = ChatResponse.Usage(promptTokens: 0, completionTokens: outputTokens, totalTokens: outputTokens)
usage = ChatResponse.Usage(promptTokens: inputTokens, completionTokens: outputTokens, totalTokens: inputTokens + outputTokens)
}
continuation.yield(StreamChunk(
id: currentId,
@@ -431,7 +488,7 @@ class AnthropicProvider: AIProvider {
var body: [String: Any] = [
"model": request.model,
"messages": apiMessages,
"max_tokens": request.maxTokens ?? 4096,
"max_tokens": request.maxTokens ?? 16000,
"stream": stream
]