|
// 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
|
|
}
|
|
}
|