// retoor <retoor@molodetz.nl>
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
actor OpenRouterClient {
private let apiKey: String
private let baseURL = "https://openrouter.ai/api/v1"
private let model: String
private let systemPrompt: String
private let maxTokens: Int
private let temperature: Double
init(
apiKey: String,
model: String = "x-ai/grok-code-fast-1",
systemPrompt: String? = nil,
maxTokens: Int = 500,
temperature: Double = 0.7
) {
self.apiKey = apiKey
self.model = model
self.maxTokens = maxTokens
self.temperature = temperature
self.systemPrompt = systemPrompt ?? """
You are Grokii, an AI assistant bot on devRant - a community platform for developers to rant about their work, share experiences, and help each other.
Your personality:
- Knowledgeable about programming, software development, DevOps, and tech industry
- Friendly but professional, with subtle developer humor
- Direct and concise - developers appreciate efficiency
- Empathetic to developer frustrations (bugs, legacy code, meetings, etc.)
Response guidelines:
- Keep responses under 800 characters (devRant has limits)
- Do NOT use markdown formatting (no **, ##, ```, etc.) - devRant renders plain text only
- Use plain text formatting: CAPS for emphasis, dashes for lists
- Include code snippets only when essential, keep them short
- Be helpful but avoid being preachy or over-explaining
You have context about the rant and recent discussion. Answer naturally as part of the conversation.
"""
}
func chat(message: String, context: String? = nil) async throws -> String {
var messages: [[String: String]] = [
["role": "system", "content": systemPrompt]
]
if let context = context {
messages.append(["role": "system", "content": "Context from the discussion:\n\(context)"])
}
messages.append(["role": "user", "content": message])
let requestBody: [String: Any] = [
"model": model,
"messages": messages,
"max_tokens": maxTokens,
"temperature": temperature
]
let jsonData = try JSONSerialization.data(withJSONObject: requestBody)
var request = URLRequest(url: URL(string: "\(baseURL)/chat/completions")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("https://github.com/user/grokii", forHTTPHeaderField: "HTTP-Referer")
request.setValue("Grokii", forHTTPHeaderField: "X-Title")
request.httpBody = jsonData
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw OpenRouterError.invalidResponse
}
guard httpResponse.statusCode == 200 else {
let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error"
throw OpenRouterError.apiError(status: httpResponse.statusCode, message: errorBody)
}
let responseObject = try JSONDecoder().decode(ChatCompletionResponse.self, from: data)
guard let content = responseObject.choices.first?.message.content else {
throw OpenRouterError.noContent
}
return content
}
}
enum OpenRouterError: Error, CustomStringConvertible {
case invalidResponse
case apiError(status: Int, message: String)
case noContent
case missingAPIKey
var description: String {
switch self {
case .invalidResponse:
return "Invalid response from OpenRouter"
case .apiError(let status, let message):
return "OpenRouter API error (\(status)): \(message)"
case .noContent:
return "No content in OpenRouter response"
case .missingAPIKey:
return "OPENROUTER_API_KEY environment variable not set"
}
}
}
struct ChatCompletionResponse: Decodable {
let choices: [Choice]
struct Choice: Decodable {
let message: Message
}
struct Message: Decodable {
let content: String
}
}