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)"))
        }
    }
}