450 lines
12 KiB
Swift
450 lines
12 KiB
Swift
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 var metadata: JSONValue? = nil
|
|
|
|
public var toolCallMetadata: ToolCallMetadata? {
|
|
guard role == .tool,
|
|
let object = metadata?.objectValue,
|
|
object["kind"]?.stringValue == "tool_call"
|
|
else { return nil }
|
|
|
|
return ToolCallMetadata(
|
|
toolCallId: object["toolCallId"]?.stringValue,
|
|
toolName: object["toolName"]?.stringValue ?? name,
|
|
status: object["status"]?.stringValue,
|
|
summary: object["summary"]?.stringValue,
|
|
durationMs: object["durationMs"]?.numberValue.map(Int.init),
|
|
error: object["error"]?.stringValue,
|
|
resultPreview: object["resultPreview"]?.stringValue
|
|
)
|
|
}
|
|
|
|
public var isToolCallLog: Bool {
|
|
toolCallMetadata != nil
|
|
}
|
|
}
|
|
|
|
public struct ToolCallMetadata: Hashable, Sendable {
|
|
public var toolCallId: String?
|
|
public var toolName: String?
|
|
public var status: String?
|
|
public var summary: String?
|
|
public var durationMs: Int?
|
|
public var error: String?
|
|
public var resultPreview: String?
|
|
}
|
|
|
|
public enum JSONValue: Codable, Hashable, Sendable {
|
|
case string(String)
|
|
case number(Double)
|
|
case bool(Bool)
|
|
case object([String: JSONValue])
|
|
case array([JSONValue])
|
|
case null
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if container.decodeNil() {
|
|
self = .null
|
|
return
|
|
}
|
|
if let value = try? container.decode(Bool.self) {
|
|
self = .bool(value)
|
|
return
|
|
}
|
|
if let value = try? container.decode(Double.self) {
|
|
self = .number(value)
|
|
return
|
|
}
|
|
if let value = try? container.decode(String.self) {
|
|
self = .string(value)
|
|
return
|
|
}
|
|
if let value = try? container.decode([String: JSONValue].self) {
|
|
self = .object(value)
|
|
return
|
|
}
|
|
if let value = try? container.decode([JSONValue].self) {
|
|
self = .array(value)
|
|
return
|
|
}
|
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case let .string(value):
|
|
try container.encode(value)
|
|
case let .number(value):
|
|
try container.encode(value)
|
|
case let .bool(value):
|
|
try container.encode(value)
|
|
case let .object(value):
|
|
try container.encode(value)
|
|
case let .array(value):
|
|
try container.encode(value)
|
|
case .null:
|
|
try container.encodeNil()
|
|
}
|
|
}
|
|
|
|
public var stringValue: String? {
|
|
if case let .string(value) = self {
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public var numberValue: Double? {
|
|
if case let .number(value) = self {
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public var objectValue: [String: JSONValue]? {
|
|
if case let .object(value) = self {
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
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 CompletionStreamToolCall: Codable, Sendable {
|
|
public var toolCallId: String
|
|
public var name: String
|
|
public var status: String
|
|
public var summary: String
|
|
public var args: [String: JSONValue]
|
|
public var startedAt: String
|
|
public var completedAt: String
|
|
public var durationMs: Int
|
|
public var error: String?
|
|
public var resultPreview: String?
|
|
}
|
|
|
|
public struct StreamErrorPayload: Codable, Sendable {
|
|
public var message: String
|
|
}
|
|
|
|
public enum CompletionStreamEvent: Sendable {
|
|
case meta(CompletionStreamMeta)
|
|
case toolCall(CompletionStreamToolCall)
|
|
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
|
|
}()
|
|
|
|
static let sybilShortTime: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = .autoupdatingCurrent
|
|
formatter.dateStyle = .none
|
|
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())
|
|
}
|
|
|
|
var sybilShortTimeLabel: String {
|
|
DateFormatter.sybilShortTime.string(from: self)
|
|
}
|
|
}
|