13699864d8
- Jarvis integration: manage oAI-Web agents and usage from inside the app (/jarvis command, Settings tab 11) - Model category filter: keyword-based categorisation with popover picker in model selector - Categories shown in ModelInfoView with coloured chips; dot indicators on model rows Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
5.1 KiB
Swift
157 lines
5.1 KiB
Swift
//
|
|
// JarvisService.swift
|
|
// oAI
|
|
//
|
|
// HTTP client for the Jarvis (oAI-Web) REST API.
|
|
// Auth: Authorization: Bearer <api-key>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Copyright (C) 2026 Rune Olsen
|
|
|
|
import Foundation
|
|
import os
|
|
|
|
final class JarvisService: Sendable {
|
|
static let shared = JarvisService()
|
|
private init() {}
|
|
|
|
private let log = Logger(subsystem: "com.oai.oAI", category: "jarvis")
|
|
|
|
private var baseURL: String { SettingsService.shared.jarvisURL }
|
|
private var apiKey: String? { SettingsService.shared.jarvisAPIKey }
|
|
|
|
// MARK: - Agents
|
|
|
|
func listAgents() async throws -> [JarvisAgent] {
|
|
try await get("/api/agents")
|
|
}
|
|
|
|
func createAgent(_ input: JarvisAgentInput) async throws -> JarvisAgent {
|
|
try await post("/api/agents", body: input)
|
|
}
|
|
|
|
func updateAgent(id: String, _ input: JarvisAgentInput) async throws -> JarvisAgent {
|
|
try await put("/api/agents/\(id)", body: input)
|
|
}
|
|
|
|
func deleteAgent(id: String) async throws {
|
|
try await voidRequest("DELETE", path: "/api/agents/\(id)")
|
|
}
|
|
|
|
func toggleAgent(id: String) async throws -> JarvisAgent {
|
|
try await post("/api/agents/\(id)/toggle", body: Empty())
|
|
}
|
|
|
|
func runAgent(id: String) async throws {
|
|
try await voidRequest("POST", path: "/api/agents/\(id)/run")
|
|
}
|
|
|
|
func stopAgent(id: String) async throws {
|
|
try await voidRequest("POST", path: "/api/agents/\(id)/stop")
|
|
}
|
|
|
|
func agentRuns(id: String) async throws -> [JarvisAgentRun] {
|
|
try await get("/api/agents/\(id)/runs")
|
|
}
|
|
|
|
// MARK: - Usage
|
|
|
|
func usage() async throws -> [JarvisUsageStat] {
|
|
let data = try await rawData("GET", path: "/api/usage")
|
|
// API returns {summary:{...}, by_agent:[...], chat:{...}}
|
|
if let w = try? JSONDecoder().decode(JarvisUsageResponse.self, from: data) {
|
|
return w.byAgent ?? []
|
|
}
|
|
// Fallback: plain array
|
|
if let arr = try? JSONDecoder().decode([JarvisUsageStat].self, from: data) { return arr }
|
|
return []
|
|
}
|
|
|
|
func credits() async throws -> JarvisCreditsResponse {
|
|
try await get("/api/usage/openrouter-credits")
|
|
}
|
|
|
|
// MARK: - Control
|
|
|
|
func queueStatus() async throws -> JarvisQueueStatus {
|
|
try await get("/api/status")
|
|
}
|
|
|
|
func pauseAll() async throws {
|
|
try await voidRequest("POST", path: "/api/pause")
|
|
}
|
|
|
|
func resumeAll() async throws {
|
|
try await voidRequest("POST", path: "/api/resume")
|
|
}
|
|
|
|
// MARK: - Connection test
|
|
|
|
func testConnection() async -> Bool {
|
|
guard !baseURL.isEmpty, let key = apiKey, !key.isEmpty else { return false }
|
|
do {
|
|
let _: [JarvisAgent] = try await get("/api/agents")
|
|
return true
|
|
} catch {
|
|
log.warning("Jarvis connection test failed: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - HTTP core
|
|
|
|
private func get<T: Decodable>(_ path: String) async throws -> T {
|
|
let data = try await rawData("GET", path: path)
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
private func post<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
|
let data = try await rawData("POST", path: path, body: body)
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
private func put<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
|
|
let data = try await rawData("PUT", path: path, body: body)
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
private func voidRequest(_ method: String, path: String) async throws {
|
|
_ = try await rawData(method, path: path)
|
|
}
|
|
|
|
private func rawData<B: Encodable>(_ method: String, path: String, body: B? = nil as Empty?) async throws -> Data {
|
|
guard let url = URL(string: baseURL.trimmingCharacters(in: .whitespaces) + path) else {
|
|
throw JarvisError.invalidURL
|
|
}
|
|
guard let key = apiKey, !key.isEmpty else {
|
|
throw JarvisError.noAPIKey
|
|
}
|
|
|
|
var req = URLRequest(url: url)
|
|
req.httpMethod = method
|
|
req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
|
|
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
req.timeoutInterval = 30
|
|
|
|
if let body {
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
req.httpBody = try JSONEncoder().encode(body)
|
|
}
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: req)
|
|
|
|
guard let http = response as? HTTPURLResponse else {
|
|
throw JarvisError.invalidResponse
|
|
}
|
|
guard (200..<300).contains(http.statusCode) else {
|
|
let msg = (try? JSONDecoder().decode(JarvisErrBody.self, from: data))?.detail ?? "HTTP \(http.statusCode)"
|
|
log.error("Jarvis \(method) \(path) → \(http.statusCode): \(msg)")
|
|
throw JarvisError.serverError(http.statusCode, msg)
|
|
}
|
|
return data
|
|
}
|
|
}
|
|
|
|
private struct Empty: Codable {}
|
|
private struct JarvisErrBody: Decodable { let detail: String? }
|