Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Agent/AgentViewModel/Core/AgentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,19 @@ final class AgentViewModel {
didSet { UserDefaults.standard.set(openRouterProtocol.rawValue, forKey: "openRouterProtocol") }
}

// MARK: - Requesty

var requestyAPIKey: String = KeychainService.shared.get(.requesty) ?? "" {
didSet { KeychainService.shared.set(.requesty, requestyAPIKey) }
}

var requestyModel: String = UserDefaults.standard.string(forKey: "requestyModel") ?? "" {
didSet { UserDefaults.standard.set(requestyModel, forKey: "requestyModel") }
}

var requestyModels: [OpenAIModelInfo] = []
var isFetchingRequestyModels = false

// MARK: - Google Gemini

var geminiAPIKey: String = KeychainService.shared.get(.gemini) ?? "" {
Expand Down
1 change: 1 addition & 0 deletions Agent/AgentViewModel/Core/Colors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ extension AgentViewModel {
case .bigModel: return zAITemperature
case .miniMax: return miniMaxTemperature
case .openRouter: return openAITemperature
case .requesty: return openAITemperature
case .qwen: return openAITemperature
case .gemini: return geminiTemperature
case .grok: return grokTemperature
Expand Down
49 changes: 49 additions & 0 deletions Agent/AgentViewModel/Features/ModelFetching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,54 @@ extension AgentViewModel {
return filtered.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}

func fetchRequestyModels() {
isFetchingRequestyModels = true
Task {
defer { isFetchingRequestyModels = false }
do {
let models = try await Self.fetchRequestyCatalog(apiKey: requestyAPIKey)
requestyModels = models
let ids = models.map(\.id)
if requestyModel.isEmpty || (!ids.isEmpty && !ids.contains(requestyModel)) {
requestyModel = ids.first ?? ""
}
} catch {
appendLog("Failed to fetch Requesty models: \(error.localizedDescription)")
requestyModels = []
}
}
}

/// Fetch Requesty's /models catalog and keep only entries Agent! can actually drive:
/// nonzero context_window AND supports_tool_calling. Requesty ids are already
/// `provider/model` (e.g. openai/gpt-4o-mini), so the id doubles as the display name.
private nonisolated static func fetchRequestyCatalog(apiKey: String) async throws -> [OpenAIModelInfo] {
guard let url = URL(string: "https://router.requesty.ai/v1/models") else { throw AgentError.invalidURL }
var request = URLRequest(url: url)
request.httpMethod = "GET"
if !apiKey.isEmpty {
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
}
request.timeoutInterval = llmAPITimeout

let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw AgentError.apiError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0, message: "Requesty /models error")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let entries = json["data"] as? [[String: Any]] else { return [] }

let filtered = entries.compactMap { entry -> OpenAIModelInfo? in
guard let id = entry["id"] as? String, !id.isEmpty else { return nil }
let ctx = entry["context_window"] as? Int ?? 0
guard ctx > 0 else { return nil }
// Agent!'s loop is tool-driven, so skip models that cannot call tools.
guard entry["supports_tool_calling"] as? Bool == true else { return nil }
return OpenAIModelInfo(id: id, name: id)
}
return filtered.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}

func fetchHuggingFaceModels() {
guard !huggingFaceAPIKey.isEmpty else {
huggingFaceModels = Self.defaultHuggingFaceModels
Expand Down Expand Up @@ -917,6 +965,7 @@ extension AgentViewModel {
case .vibe: if force || vibeModels.isEmpty { fetchVibeModels() }
case .miniMax: if force || miniMaxModels.isEmpty { fetchMiniMaxModels() }
case .openRouter: if force || openRouterModels.isEmpty { fetchOpenRouterModels() }
case .requesty: if force || requestyModels.isEmpty { fetchRequestyModels() }
case .bigModel: break
default: break
}
Expand Down
4 changes: 4 additions & 0 deletions Agent/AgentViewModel/Features/ScriptTabs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ extension AgentViewModel {
case .bigModel: return bigModelModel.replacingOccurrences(of: ":v", with: "")
case .miniMax: return miniMaxModel
case .openRouter: return openRouterModel
case .requesty: return requestyModel
case .qwen: return qwenModel
case .gemini: return geminiModel
case .grok: return grokModel
Expand All @@ -100,6 +101,7 @@ extension AgentViewModel {
case .bigModel: return bigModelAPIKey
case .miniMax: return miniMaxAPIKey
case .openRouter: return openRouterAPIKey
case .requesty: return requestyAPIKey
case .qwen: return qwenAPIKey
case .gemini: return geminiAPIKey
case .grok: return grokAPIKey
Expand Down Expand Up @@ -152,6 +154,8 @@ extension AgentViewModel {
?? Self.defaultMiniMaxModels.first(where: { $0.id == modelId })?.name ?? modelId
case .openRouter:
return openRouterModels.first(where: { $0.id == modelId })?.name ?? modelId
case .requesty:
return requestyModels.first(where: { $0.id == modelId })?.name ?? modelId
case .qwen:
return modelId
case .gemini:
Expand Down
1 change: 1 addition & 0 deletions Agent/AgentViewModel/Messages/Compression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ extension AgentViewModel {
case .bigModel: return 128_000
case .miniMax: return 1_000_000
case .openRouter: return 200_000
case .requesty: return 200_000
case .qwen: return 131_072
case .mistral: return 256_000
case .vibe: return 128_000
Expand Down
3 changes: 3 additions & 0 deletions Agent/AgentViewModel/TaskExecution/Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ extension AgentViewModel {
case .openRouter:
modelName = openRouterModel
isVision = Self.isVisionModel(openRouterModel)
case .requesty:
modelName = requestyModel
isVision = Self.isVisionModel(requestyModel)
case .qwen:
modelName = qwenModel
isVision = Self.isVisionModel(qwenModel)
Expand Down
1 change: 1 addition & 0 deletions Agent/Services/KeychainService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final class KeychainService: Sendable {
case qwen = "com.agent.qwen-api-key"
case miniMax = "com.agent.minimax-api-key"
case openRouter = "com.agent.openrouter-api-key"
case requesty = "com.agent.requesty-api-key"
case exa = "com.agent.exa-api-key"
case lmStudio = "com.agent.lmstudio-api-key"
}
Expand Down
12 changes: 11 additions & 1 deletion Agent/Services/LLMProviderSetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum LLMProviderSetup {
static func registerAllProviders() {
LLMRegistry.shared.registerAll([
claude, codex, openAI, gemini, grok, mistral, vibe, deepSeek, huggingFace, miniMax, zAI, bigModel, qwen, openRouter,
ollama, localOllama, vLLM, lmStudio, appleIntelligence
requesty, ollama, localOllama, vLLM, lmStudio, appleIntelligence
])
}

Expand Down Expand Up @@ -83,6 +83,16 @@ enum LLMProviderSetup {
capabilities: [.streaming, .tools, .vision, .systemPrompt]
)

static let requesty = LLMProviderConfig(
id: "requesty", displayName: "Requesty",
kind: .cloudAPI, apiProtocol: .openAI,
endpoint: LLMEndpoint(
chatURL: "https://router.requesty.ai/v1/chat/completions",
modelsURL: "https://router.requesty.ai/v1/models"
),
capabilities: [.streaming, .tools, .vision, .systemPrompt]
)

static let miniMax = LLMProviderConfig(
id: "miniMax", displayName: "MiniMax",
kind: .cloudAPI, apiProtocol: .openAI,
Expand Down
2 changes: 2 additions & 0 deletions Agent/Views/Settings/FallbackChainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ struct FallbackChainView: View {
case .grok: return oai(viewModel.grokModels)
case .mistral: return oai(viewModel.mistralModels)
case .openRouter: return oai(viewModel.openRouterModels)
case .requesty: return oai(viewModel.requestyModels)
default: return []
}
}
Expand Down Expand Up @@ -253,6 +254,7 @@ struct FallbackChainView: View {
case .bigModel: if !viewModel.bigModelModel.isEmpty { return viewModel.bigModelModel }
case .miniMax: if !viewModel.miniMaxModel.isEmpty { return viewModel.miniMaxModel }
case .openRouter: if !viewModel.openRouterModel.isEmpty { return viewModel.openRouterModel }
case .requesty: if !viewModel.requestyModel.isEmpty { return viewModel.requestyModel }
case .foundationModel: return "Apple Intelligence"
}
// Fall back to the first dynamically-fetched model for this provider
Expand Down
43 changes: 43 additions & 0 deletions Agent/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct SettingsView: View {
case .bigModel: return $viewModel.zAITemperature
case .miniMax: return $viewModel.miniMaxTemperature
case .openRouter: return $viewModel.openAITemperature
case .requesty: return $viewModel.openAITemperature
case .qwen: return $viewModel.openAITemperature
case .gemini: return $viewModel.geminiTemperature
case .grok: return $viewModel.grokTemperature
Expand Down Expand Up @@ -401,6 +402,48 @@ struct SettingsView: View {
}
}
}
} else if viewModel.selectedProvider == .requesty {
VStack(alignment: .leading, spacing: 10) {
Text("Requesty")
.font(.headline)

VStack(alignment: .leading, spacing: 4) {
Text("API Key").font(.caption).foregroundStyle(.secondary)
LockedSecureField(text: $viewModel.requestyAPIKey, placeholder: "Requesty API key", lockKey: "lock.requestyAPIKey")
}

VStack(alignment: .leading, spacing: 4) {
Text("Model").font(.caption).foregroundStyle(.secondary)
HStack {
if viewModel.requestyModels.isEmpty {
TextField("e.g. anthropic/claude-sonnet-4-5", text: $viewModel.requestyModel)
.textFieldStyle(.roundedBorder)
} else {
Picker("Model", selection: $viewModel.requestyModel) {
ForEach(viewModel.requestyModels) { model in
Text(model.name).tag(model.id)
}
}
.labelsHidden()
}

Button {
viewModel.fetchRequestyModels()
} label: {
if viewModel.isFetchingRequestyModels {
ProgressView()
.controlSize(.small)
} else {
Image(systemName: "arrow.clockwise")
}
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(viewModel.isFetchingRequestyModels)
.help("Fetch available models")
}
}
}
} else if viewModel.selectedProvider == .qwen {
VStack(alignment: .leading, spacing: 10) {
Text("Qwen (DashScope)")
Expand Down
9 changes: 9 additions & 0 deletions Agent/Views/Tabs/NewMainTabSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ struct NewMainTabSheet: View {
fetch: { viewModel.fetchModelsIfNeeded(for: .openRouter, force: true) }
)

case .requesty:
modelPickerWithFetch(
models: viewModel.requestyModels,
fallbackBinding: $selectedModelId,
isFetching: viewModel.isFetchingRequestyModels,
fetch: { viewModel.fetchModelsIfNeeded(for: .requesty, force: true) }
)

case .qwen:
TextField("Model (e.g. qwen-plus)", text: $selectedModelId)
.textFieldStyle(.roundedBorder)
Expand Down Expand Up @@ -309,6 +317,7 @@ struct NewMainTabSheet: View {
case .bigModel: return "glm-4.7"
case .miniMax: return viewModel.miniMaxModel.isEmpty ? "MiniMax-M3" : viewModel.miniMaxModel
case .openRouter: return viewModel.openRouterModel
case .requesty: return viewModel.requestyModel
case .qwen: return "qwen-plus"
case .gemini: return viewModel.geminiModel
case .grok: return viewModel.grokModel
Expand Down
2 changes: 2 additions & 0 deletions Agent/Views/Tools/ToolsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ struct ToolsView: View {
case .vibe: return $viewModel.vibeModel
case .miniMax: return $viewModel.miniMaxModel
case .openRouter: return $viewModel.openRouterModel
case .requesty: return $viewModel.requestyModel
case .foundationModel: return .constant("Apple Intelligence")
}
}
Expand Down Expand Up @@ -189,6 +190,7 @@ struct ToolsView: View {
case .lmStudio: return viewModel.lmStudioModels.map { ($0.id, $0.name) }
case .miniMax: return oai(viewModel.miniMaxModels, AgentViewModel.defaultMiniMaxModels)
case .openRouter: return viewModel.openRouterModels.map { ($0.id, $0.name) }
case .requesty: return viewModel.requestyModels.map { ($0.id, $0.name) }
case .bigModel: return []
case .foundationModel:
return [("Apple Intelligence", "Apple Intelligence")]
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

**One app. Any AI. Total command over your Mac.**

Agent! is a 100% native Swift 6.2 / SwiftUI app that wires **18 LLM providers** — Claude, Codex, OpenAI, Gemini, Grok, Mistral, Mistral Vibe, DeepSeek, Hugging Face, Z.ai, BigModel, Qwen, MiniMax, OpenRouter, Ollama (cloud and local), vLLM, and LM Studio — plus on-device **Apple Intelligence** — into an autonomous task loop that actually *does things*: reads your codebase, fixes the bug, builds the Xcode project, commits the diff, drives any Mac app through the Accessibility API, runs shell commands as you or as root, texts you results over iMessage, and answers to a spoken *"Agent!"*.
Agent! is a 100% native Swift 6.2 / SwiftUI app that wires **19 LLM providers** — Claude, Codex, OpenAI, Gemini, Grok, Mistral, Mistral Vibe, DeepSeek, Hugging Face, Z.ai, BigModel, Qwen, MiniMax, OpenRouter, Requesty, Ollama (cloud and local), vLLM, and LM Studio — plus on-device **Apple Intelligence** — into an autonomous task loop that actually *does things*: reads your codebase, fixes the bug, builds the Xcode project, commits the diff, drives any Mac app through the Accessibility API, runs shell commands as you or as root, texts you results over iMessage, and answers to a spoken *"Agent!"*.

No NPM, no Electron, no subscription, no telemetry. Bring your own API key, run fully local, or run free on Apple Intelligence. Every Swift package it depends on was written by the same author. See [Backstory](#backstory) below.

Expand Down Expand Up @@ -98,7 +98,7 @@ Just type what you want. Agent! figures out how and makes it happen.
- **🗂 Tabs, history, memory, plans, skills** — each tab has its own project folder and log; persistent user memory; multi-plan checklists surfaced in every prompt.
- **🔄 Fallback chain** — auto-switch to the next configured provider on 429/timeout/network failure.

## 🤖 18 AI Providers
## 🤖 19 AI Providers

| Provider | Cost | Best for |
|---|---|---|
Expand All @@ -114,6 +114,7 @@ Just type what you want. Agent! figures out how and makes it happen.
| **Qwen** (Alibaba) | Cheap | Qwen 3.8 via Dashscope |
| **MiniMax** | Cheap | 1M-token context |
| **OpenRouter** | Paid | 200+ models, one key; Claude routed via Anthropic protocol |
| **Requesty** | Paid | 300+ models via one OpenAI-compatible key; per-model pricing and capability metadata |
| **Ollama** (cloud) | Free tier | Hosted open models |
| **Local Ollama** / **vLLM** / **LM Studio** | Free + hardware | Fully offline; real per-model context window detected |
| **Apple Intelligence** | Free, on-device | Triage, summaries, token compression (brain icon, not the provider picker) |
Expand Down
5 changes: 3 additions & 2 deletions README_de.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

**Eine App. Jede KI. Volle Kontrolle über deinen Mac.**

Agent! ist eine zu 100 % native Swift-6.2-/SwiftUI-App, die **18 LLM-Anbieter** — Claude, Codex, OpenAI, Gemini, Grok, Mistral, Mistral Vibe, DeepSeek, Hugging Face, Z.ai, BigModel, Qwen, MiniMax, OpenRouter, Ollama (Cloud und lokal), vLLM und LM Studio — plus die geräteinterne **Apple Intelligence** — mit einer autonomen Aufgabenschleife verbindet, die wirklich *etwas tut*: Sie liest deinen Code, behebt den Fehler, baut das Xcode-Projekt, committet den Diff, steuert jede Mac-App über die Accessibility-API, führt Shell-Befehle als du oder als root aus, schickt dir Ergebnisse per iMessage und reagiert auf ein gesprochenes *„Agent!"*.
Agent! ist eine zu 100 % native Swift-6.2-/SwiftUI-App, die **19 LLM-Anbieter** — Claude, Codex, OpenAI, Gemini, Grok, Mistral, Mistral Vibe, DeepSeek, Hugging Face, Z.ai, BigModel, Qwen, MiniMax, OpenRouter, Requesty, Ollama (Cloud und lokal), vLLM und LM Studio — plus die geräteinterne **Apple Intelligence** — mit einer autonomen Aufgabenschleife verbindet, die wirklich *etwas tut*: Sie liest deinen Code, behebt den Fehler, baut das Xcode-Projekt, committet den Diff, steuert jede Mac-App über die Accessibility-API, führt Shell-Befehle als du oder als root aus, schickt dir Ergebnisse per iMessage und reagiert auf ein gesprochenes *„Agent!"*.

Kein NPM, kein Electron, kein Abo, keine Telemetrie. Bring deinen eigenen API-Schlüssel mit, lauf komplett lokal oder kostenlos mit Apple Intelligence. Jedes Swift-Paket, von dem die App abhängt, wurde vom selben Autor geschrieben. Siehe [Entstehungsgeschichte](#entstehungsgeschichte) unten.

Expand Down Expand Up @@ -98,7 +98,7 @@ Tipp einfach, was du willst. Agent! findet heraus wie und setzt es um.
- **🗂 Tabs, Verlauf, Gedächtnis, Pläne, Skills** — jeder Tab hat eigenen Projektordner und eigenes Log; persistentes Nutzergedächtnis; Multi-Plan-Checklisten in jedem Prompt.
- **🔄 Fallback-Kette** — automatischer Wechsel zum nächsten konfigurierten Anbieter bei 429/Timeout/Netzwerkfehler.

## 🤖 18 KI-Anbieter
## 🤖 19 KI-Anbieter

| Anbieter | Kosten | Am besten für |
|---|---|---|
Expand All @@ -111,6 +111,7 @@ Tipp einfach, was du willst. Agent! findet heraus wie und setzt es um.
| **DeepSeek** | Günstig | Budget-Coding, Cache-Hit-Reporting |
| **Hugging Face** | Variabel | Offene Modelle, serverless oder dedizierte Endpunkte |
| **OpenRouter** | Kostenpflichtig | 200+ Modelle, ein Schlüssel; Claude über Anthropic-Protokoll |
| **Requesty** | Kostenpflichtig | 300+ Modelle mit einem OpenAI-kompatiblen Schlüssel; Preise und Fähigkeiten pro Modell |
| **Z.ai** / **BigModel** | Günstig | GLM-5.3 — empfohlener Einstieg |
| **Qwen** (Alibaba) | Günstig | Qwen 3.8 über Dashscope |
| **MiniMax** | Günstig | 1M-Token-Kontext |
Expand Down
Loading
Loading