ios: initial commit

This commit is contained in:
2026-02-20 00:09:02 -08:00
parent b91b03b74f
commit c47646a48c
24 changed files with 3406 additions and 19 deletions

View File

@@ -0,0 +1,314 @@
import Foundation
public enum Provider: String, Codable, CaseIterable, Hashable, Sendable {
case openai
case anthropic
case xai
public var displayName: String {
switch self {
case .openai: return "OpenAI"
case .anthropic: return "Anthropic"
case .xai: return "xAI"
}
}
}
public enum MessageRole: String, Codable, Hashable, Sendable {
case system
case user
case assistant
case tool
}
public struct ChatSummary: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var createdAt: Date
public var updatedAt: Date
public var initiatedProvider: Provider?
public var initiatedModel: String?
public var lastUsedProvider: Provider?
public var lastUsedModel: String?
}
public struct SearchSummary: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var query: String?
public var createdAt: Date
public var updatedAt: Date
}
public struct Message: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var createdAt: Date
public var role: MessageRole
public var content: String
public var name: String?
}
public struct ChatDetail: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var createdAt: Date
public var updatedAt: Date
public var initiatedProvider: Provider?
public var initiatedModel: String?
public var lastUsedProvider: Provider?
public var lastUsedModel: String?
public var messages: [Message]
}
public struct SearchResultItem: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var createdAt: Date
public var rank: Int
public var title: String?
public var url: String
public var publishedDate: String?
public var author: String?
public var text: String?
public var highlights: [String]?
public var highlightScores: [Double]?
public var score: Double?
public var favicon: String?
public var image: String?
}
public struct SearchCitation: Codable, Hashable, Sendable {
public var id: String?
public var url: String?
public var title: String?
public var publishedDate: String?
public var author: String?
public var text: String?
}
public struct SearchDetail: Codable, Identifiable, Hashable, Sendable {
public var id: String
public var title: String?
public var query: String?
public var createdAt: Date
public var updatedAt: Date
public var requestId: String?
public var latencyMs: Int?
public var error: String?
public var answerText: String?
public var answerRequestId: String?
public var answerCitations: [SearchCitation]?
public var answerError: String?
public var results: [SearchResultItem]
}
public struct SearchRunRequest: Codable, Sendable {
public var query: String?
public var title: String?
public var type: String?
public var numResults: Int?
public var includeDomains: [String]?
public var excludeDomains: [String]?
public init(
query: String? = nil,
title: String? = nil,
type: String? = nil,
numResults: Int? = nil,
includeDomains: [String]? = nil,
excludeDomains: [String]? = nil
) {
self.query = query
self.title = title
self.type = type
self.numResults = numResults
self.includeDomains = includeDomains
self.excludeDomains = excludeDomains
}
}
public struct CompletionRequestMessage: Codable, Sendable {
public var role: MessageRole
public var content: String
public var name: String?
public init(role: MessageRole, content: String, name: String? = nil) {
self.role = role
self.content = content
self.name = name
}
}
public struct CompletionStreamMeta: Codable, Sendable {
public var chatId: String
public var callId: String
public var provider: Provider
public var model: String
}
public struct CompletionStreamDelta: Codable, Sendable {
public var text: String
}
public struct CompletionStreamDone: Codable, Sendable {
public var text: String
}
public struct StreamErrorPayload: Codable, Sendable {
public var message: String
}
public enum CompletionStreamEvent: Sendable {
case meta(CompletionStreamMeta)
case delta(CompletionStreamDelta)
case done(CompletionStreamDone)
case error(StreamErrorPayload)
case ignored
}
public struct SearchResultsPayload: Codable, Sendable {
public var requestId: String?
public var results: [SearchResultItem]
}
public struct SearchErrorPayload: Codable, Sendable {
public var error: String
}
public struct SearchAnswerPayload: Codable, Sendable {
public var answerText: String?
public var answerRequestId: String?
public var answerCitations: [SearchCitation]?
}
public struct SearchDonePayload: Codable, Sendable {
public var search: SearchDetail
}
public enum SearchStreamEvent: Sendable {
case searchResults(SearchResultsPayload)
case searchError(SearchErrorPayload)
case answer(SearchAnswerPayload)
case answerError(SearchErrorPayload)
case done(SearchDonePayload)
case error(StreamErrorPayload)
case ignored
}
public struct ProviderModelInfo: Codable, Hashable, Sendable {
public var models: [String]
public var loadedAt: Date?
public var error: String?
}
public struct ModelCatalogResponse: Codable, Hashable, Sendable {
public var providers: [Provider: ProviderModelInfo]
enum CodingKeys: String, CodingKey {
case providers
}
public init(providers: [Provider: ProviderModelInfo]) {
self.providers = providers
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawProviders = try container.decode([String: ProviderModelInfo].self, forKey: .providers)
var mapped: [Provider: ProviderModelInfo] = [:]
for (key, value) in rawProviders {
if let provider = Provider(rawValue: key) {
mapped[provider] = value
}
}
self.providers = mapped
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let raw = Dictionary(uniqueKeysWithValues: providers.map { ($0.key.rawValue, $0.value) })
try container.encode(raw, forKey: .providers)
}
}
struct AuthSession: Codable {
var authenticated: Bool
var mode: String
}
struct ChatListResponse: Codable {
var chats: [ChatSummary]
}
struct SearchListResponse: Codable {
var searches: [SearchSummary]
}
struct ChatDetailResponse: Codable {
var chat: ChatDetail
}
struct SearchDetailResponse: Codable {
var search: SearchDetail
}
struct ChatCreateResponse: Codable {
var chat: ChatSummary
}
struct SearchCreateResponse: Codable {
var search: SearchSummary
}
struct DeleteResponse: Codable {
var deleted: Bool
}
struct SuggestTitleBody: Codable {
var chatId: String
var content: String
}
enum APIError: LocalizedError {
case invalidBaseURL
case httpError(statusCode: Int, message: String)
case networkError(message: String)
case decodingError(message: String)
case invalidResponse
case noResponseStream
var errorDescription: String? {
switch self {
case .invalidBaseURL:
return "Invalid API URL"
case let .httpError(_, message):
return message
case let .networkError(message):
return message
case let .decodingError(message):
return message
case .invalidResponse:
return "Unexpected server response"
case .noResponseStream:
return "No response stream"
}
}
}
extension DateFormatter {
static let sybilDisplayDate: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .autoupdatingCurrent
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}()
}
extension Date {
var sybilRelativeLabel: String {
let formatter = RelativeDateTimeFormatter()
formatter.locale = .autoupdatingCurrent
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: self, relativeTo: Date())
}
}