Section 8/172 menit
8. Cloud LLM (OpenAI, Anthropic, Gemini) dari iOS
8. Cloud LLM (OpenAI, Anthropic, Gemini) dari iOS
Kapan Cloud Diperlukan
- Capability tinggi: GPT-4, Claude 3.5/4, Gemini 1.5 jauh lebih cerdas dari Foundation Models.
- Knowledge terkini: model cloud lebih sering di-update.
- Multi-modal kuat: vision-language model (image + text reasoning).
- Konteks panjang: 200K-2M token context, vs Foundation Models yang lebih pendek.
Pattern: Direct API Call (untuk Prototyping)
swift
// cloud-llm: Direct API (HANYA untuk dev/prototype, JANGAN production)
import Foundation
struct OpenAIClient: Sendable {
let apiKey: String // ❌ NEVER hardcode atau ship dengan app
func chat(_ message: String) async throws -> String {
var req = URLRequest(url: URL(string: "https://api.openai.com/v1/chat/completions")!)
req.httpMethod = "POST"
req.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: [
"model": "gpt-4o-mini",
"messages": [["role": "user", "content": message]]
])
let (data, _) = try await URLSession.shared.data(for: req)
// parse response
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
let choices = json["choices"] as! [[String: Any]]
let message = choices[0]["message"] as! [String: Any]
return message["content"] as! String
}
}
Pattern: Proxy via Backend (Production)
Aturan: JANGAN PERNAH ship API key di app. Selalu lewat backend kamu sendiri.
swift
// cloud-llm: Production — call backend kamu, backend yang call OpenAI
actor AIService {
private let baseURL: URL
init(baseURL: URL) { self.baseURL = baseURL }
func chat(_ message: String, userToken: String) async throws -> AsyncStream<String> {
var req = URLRequest(url: baseURL.appendingPathComponent("ai/chat"))
req.httpMethod = "POST"
req.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
req.httpBody = try JSONEncoder().encode(["message": message])
let (bytes, _) = try await URLSession.shared.bytes(for: req)
return AsyncStream { continuation in
Task {
for try await line in bytes.lines {
// Server-Sent Events streaming
if line.hasPrefix("data: ") {
let token = String(line.dropFirst("data: ".count))
continuation.yield(token)
}
}
continuation.finish()
}
}
}
}
Alasan harus proxy:
- API key tidak boleh di app (akan ter-extract dengan IPA reverse-engineering).
- Rate limiting & quota per user.
- Audit & logging untuk compliance.
- Prompt template di server (cepat update tanpa release app).
- Cost monitoring & abuse detection.
Vendor Comparison (per 2026)
| Vendor | Best For | Pricing Model |
|---|---|---|
| OpenAI (GPT-4o, o1) | General + vision + structured output | Per token, tiers |
| Anthropic (Claude 4.7/4.6/4.5) | Long context (1M), coding, agentic | Per token + prompt caching |
| Google (Gemini 1.5/2.0) | Multi-modal, very long context (2M) | Per token, generous free tier |
| Mistral, Llama (via Groq/Together) | Open-weight, custom hosting | Per token, lebih murah |
Streaming UX
User tidak nyaman menunggu 3 detik untuk response. Selalu streaming kalau memungkinkan.
swift
// cloud-llm: Streaming response ke UI
@MainActor
@Observable
final class ChatViewModel {
var messages: [Message] = []
var streamingText: String = ""
private let ai: AIService
init(ai: AIService) { self.ai = ai }
func send(_ text: String, token: String) async {
messages.append(Message(role: .user, content: text))
streamingText = ""
do {
for await chunk in try await ai.chat(text, userToken: token) {
streamingText += chunk // UI update per chunk
}
messages.append(Message(role: .assistant, content: streamingText))
streamingText = ""
} catch {
messages.append(Message(role: .system, content: "Error: \(error)"))
}
}
}