608 lines
16 KiB
Swift
608 lines
16 KiB
Swift
import Foundation
|
|
|
|
public enum Provider: String, Codable, CaseIterable, Hashable, Sendable {
|
|
case openai
|
|
case anthropic
|
|
case xai
|
|
case hermesAgent = "hermes-agent"
|
|
|
|
public var displayName: String {
|
|
switch self {
|
|
case .openai: return "OpenAI"
|
|
case .anthropic: return "Anthropic"
|
|
case .xai: return "xAI"
|
|
case .hermesAgent: return "Hermes Agent"
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum MessageRole: String, Codable, Hashable, Sendable {
|
|
case system
|
|
case user
|
|
case assistant
|
|
case tool
|
|
}
|
|
|
|
public struct ChatAttachment: Codable, Hashable, Identifiable, Sendable {
|
|
public enum Kind: String, Codable, Hashable, Sendable {
|
|
case image
|
|
case text
|
|
}
|
|
|
|
public var id: String
|
|
public var kind: Kind
|
|
public var filename: String
|
|
public var mimeType: String
|
|
public var sizeBytes: Int
|
|
public var dataUrl: String?
|
|
public var text: String?
|
|
public var truncated: Bool?
|
|
|
|
public init(
|
|
id: String,
|
|
kind: Kind,
|
|
filename: String,
|
|
mimeType: String,
|
|
sizeBytes: Int,
|
|
dataUrl: String? = nil,
|
|
text: String? = nil,
|
|
truncated: Bool? = nil
|
|
) {
|
|
self.id = id
|
|
self.kind = kind
|
|
self.filename = filename
|
|
self.mimeType = mimeType
|
|
self.sizeBytes = sizeBytes
|
|
self.dataUrl = dataUrl
|
|
self.text = text
|
|
self.truncated = truncated
|
|
}
|
|
|
|
public static func image(
|
|
id: String = UUID().uuidString,
|
|
filename: String,
|
|
mimeType: String,
|
|
sizeBytes: Int,
|
|
dataUrl: String
|
|
) -> ChatAttachment {
|
|
ChatAttachment(
|
|
id: id,
|
|
kind: .image,
|
|
filename: filename,
|
|
mimeType: mimeType,
|
|
sizeBytes: sizeBytes,
|
|
dataUrl: dataUrl
|
|
)
|
|
}
|
|
|
|
public static func text(
|
|
id: String = UUID().uuidString,
|
|
filename: String,
|
|
mimeType: String,
|
|
sizeBytes: Int,
|
|
text: String,
|
|
truncated: Bool
|
|
) -> ChatAttachment {
|
|
ChatAttachment(
|
|
id: id,
|
|
kind: .text,
|
|
filename: filename,
|
|
mimeType: mimeType,
|
|
sizeBytes: sizeBytes,
|
|
text: text,
|
|
truncated: truncated
|
|
)
|
|
}
|
|
|
|
var jsonValue: JSONValue {
|
|
var object: [String: JSONValue] = [
|
|
"kind": .string(kind.rawValue),
|
|
"id": .string(id),
|
|
"filename": .string(filename),
|
|
"mimeType": .string(mimeType),
|
|
"sizeBytes": .number(Double(sizeBytes))
|
|
]
|
|
|
|
if let dataUrl {
|
|
object["dataUrl"] = .string(dataUrl)
|
|
}
|
|
if let text {
|
|
object["text"] = .string(text)
|
|
}
|
|
if let truncated {
|
|
object["truncated"] = .bool(truncated)
|
|
}
|
|
|
|
return .object(object)
|
|
}
|
|
|
|
static func attachments(from metadata: JSONValue?) -> [ChatAttachment] {
|
|
guard let metadataObject = metadata?.objectValue,
|
|
let values = metadataObject["attachments"]?.arrayValue
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return values.compactMap { value in
|
|
guard let object = value.objectValue,
|
|
let kindRaw = object["kind"]?.stringValue,
|
|
let kind = Kind(rawValue: kindRaw),
|
|
let id = object["id"]?.stringValue,
|
|
let filename = object["filename"]?.stringValue,
|
|
let mimeType = object["mimeType"]?.stringValue,
|
|
let sizeNumber = object["sizeBytes"]?.numberValue
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return ChatAttachment(
|
|
id: id,
|
|
kind: kind,
|
|
filename: filename,
|
|
mimeType: mimeType,
|
|
sizeBytes: Int(sizeNumber),
|
|
dataUrl: object["dataUrl"]?.stringValue,
|
|
text: object["text"]?.stringValue,
|
|
truncated: object["truncated"]?.boolValue
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 attachments: [ChatAttachment] {
|
|
ChatAttachment.attachments(from: metadata)
|
|
}
|
|
|
|
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 var arrayValue: [JSONValue]? {
|
|
if case let .array(value) = self {
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public var boolValue: Bool? {
|
|
if case let .bool(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 ActiveRunsResponse: Codable, Hashable, Sendable {
|
|
public var chats: [String]
|
|
public var searches: [String]
|
|
|
|
public init(chats: [String] = [], searches: [String] = []) {
|
|
self.chats = chats
|
|
self.searches = searches
|
|
}
|
|
}
|
|
|
|
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 var attachments: [ChatAttachment]?
|
|
|
|
public init(role: MessageRole, content: String, name: String? = nil, attachments: [ChatAttachment]? = nil) {
|
|
self.role = role
|
|
self.content = content
|
|
self.name = name
|
|
self.attachments = attachments
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|