import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif #if os(macOS) import SweetCookieKit #endif private let ollamaSessionCookieNames: Set = [ "session", "__Secure-session", "ollama_session", "__Host-ollama_session", "wos-session", "__Secure-next-auth.session-token", "next-auth.session-token", ] private func isRecognizedOllamaSessionCookieName(_ name: String) -> Bool { if ollamaSessionCookieNames.contains(name) { return true } // next-auth can split tokens into chunked cookies: `.0`, `.1`, ... return name.hasPrefix("__Secure-next-auth.session-token.") || name.hasPrefix("next-auth.session-token.") } private func hasRecognizedOllamaSessionCookie(in header: String) -> Bool { CookieHeaderNormalizer.pairs(from: header).contains { pair in isRecognizedOllamaSessionCookieName(pair.name) } } public enum OllamaUsageError: LocalizedError, Sendable { private static let signInURL = "https://ollama.com/signin" case missingAPIKey case notLoggedIn case invalidCredentials case apiUnauthorized case parseFailed(String) case networkError(String) case noSessionCookie public var errorDescription: String? { switch self { case .missingAPIKey: "Missing Ollama API key. Set apiKey in ~/.codexbar/config.json or OLLAMA_API_KEY." case .notLoggedIn: "Not signed in to Ollama. Please sign in at \(Self.signInURL)." case .invalidCredentials: "Ollama session cookie expired. Please sign in again at \(Self.signInURL)." case .apiUnauthorized: "Ollama API key is invalid or revoked." case let .parseFailed(message): "Could not parse Ollama usage: \(message)" case let .networkError(message): "Ollama request failed: \(message)" case .noSessionCookie: "No Ollama session cookie found. Please sign in at \(Self.signInURL) in your browser." } } } #if os(macOS) private let ollamaCookieImportOrder: BrowserCookieImportOrder = ProviderDefaults.metadata[.ollama]?.browserCookieOrder ?? Browser.defaultImportOrder public enum OllamaCookieImporter { private static let cookieClient = BrowserCookieClient() private static let cookieDomains = ["ollama.com", "www.ollama.com"] static let defaultPreferredBrowsers: [Browser] = [.chrome] static let defaultAllowFallbackBrowsers = true public struct SessionInfo: Sendable { public let cookies: [HTTPCookie] public let sourceLabel: String public init(cookies: [HTTPCookie], sourceLabel: String) { self.cookies = cookies self.sourceLabel = sourceLabel } public var cookieHeader: String { self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") } } public static func importSessions( browserDetection: BrowserDetection, preferredBrowsers: [Browser] = [.chrome], allowFallbackBrowsers: Bool = false, logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { let log: (String) -> Void = { msg in logger?("[ollama-cookie] \(msg)") } let preferredSources = preferredBrowsers.isEmpty ? ollamaCookieImportOrder.cookieImportCandidates(using: browserDetection) : preferredBrowsers.cookieImportCandidates(using: browserDetection) let preferredCandidates = self.collectSessionInfo(from: preferredSources, logger: log) return try self.selectSessionInfosWithFallback( preferredCandidates: preferredCandidates, allowFallbackBrowsers: allowFallbackBrowsers, loadFallbackCandidates: { guard !preferredBrowsers.isEmpty else { return [] } let fallbackSources = self.fallbackBrowserSources( browserDetection: browserDetection, excluding: preferredSources) guard !fallbackSources.isEmpty else { return [] } log("No recognized Ollama session in preferred browsers; trying fallback import order") return self.collectSessionInfo(from: fallbackSources, logger: log) }, logger: log) } public static func importSession( browserDetection: BrowserDetection, preferredBrowsers: [Browser] = [.chrome], allowFallbackBrowsers: Bool = false, logger: ((String) -> Void)? = nil) throws -> SessionInfo { let sessions = try self.importSessions( browserDetection: browserDetection, preferredBrowsers: preferredBrowsers, allowFallbackBrowsers: allowFallbackBrowsers, logger: logger) guard let first = sessions.first else { throw OllamaUsageError.noSessionCookie } return first } static func selectSessionInfos( from candidates: [SessionInfo], logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { var recognized: [SessionInfo] = [] for candidate in candidates { let names = candidate.cookies.map(\.name).joined(separator: ", ") logger?("\(candidate.sourceLabel) cookies: \(names)") if self.containsRecognizedSessionCookie(in: candidate.cookies) { logger?("Found Ollama session cookie in \(candidate.sourceLabel)") recognized.append(candidate) } else { logger?("\(candidate.sourceLabel) cookies found, but no recognized session cookie present") } } guard !recognized.isEmpty else { throw OllamaUsageError.noSessionCookie } return recognized } static func selectSessionInfo( from candidates: [SessionInfo], logger: ((String) -> Void)? = nil) throws -> SessionInfo { guard let first = try self.selectSessionInfos(from: candidates, logger: logger).first else { throw OllamaUsageError.noSessionCookie } return first } static func selectSessionInfosWithFallback( preferredCandidates: [SessionInfo], allowFallbackBrowsers: Bool, loadFallbackCandidates: () -> [SessionInfo], logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { guard allowFallbackBrowsers else { return try self.selectSessionInfos(from: preferredCandidates, logger: logger) } do { return try self.selectSessionInfos(from: preferredCandidates, logger: logger) } catch OllamaUsageError.noSessionCookie { let fallbackCandidates = loadFallbackCandidates() return try self.selectSessionInfos(from: fallbackCandidates, logger: logger) } } static func selectSessionInfoWithFallback( preferredCandidates: [SessionInfo], allowFallbackBrowsers: Bool, loadFallbackCandidates: () -> [SessionInfo], logger: ((String) -> Void)? = nil) throws -> SessionInfo { guard let first = try self.selectSessionInfosWithFallback( preferredCandidates: preferredCandidates, allowFallbackBrowsers: allowFallbackBrowsers, loadFallbackCandidates: loadFallbackCandidates, logger: logger).first else { throw OllamaUsageError.noSessionCookie } return first } private static func fallbackBrowserSources( browserDetection: BrowserDetection, excluding triedSources: [Browser]) -> [Browser] { let tried = Set(triedSources) return ollamaCookieImportOrder.cookieImportCandidates(using: browserDetection) .filter { !tried.contains($0) } } private static func collectSessionInfo( from browserSources: [Browser], logger: @escaping (String) -> Void) -> [SessionInfo] { var candidates: [SessionInfo] = [] for browserSource in browserSources { do { let query = BrowserCookieQuery(domains: self.cookieDomains) let sources = try Self.cookieClient.codexBarRecords( matching: query, in: browserSource, logger: logger) for source in sources where !source.records.isEmpty { let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin) guard !cookies.isEmpty else { continue } candidates.append(SessionInfo(cookies: cookies, sourceLabel: source.label)) } } catch { BrowserCookieAccessGate.recordIfNeeded(error) logger("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } } return candidates } private static func containsRecognizedSessionCookie(in cookies: [HTTPCookie]) -> Bool { cookies.contains { cookie in isRecognizedOllamaSessionCookieName(cookie.name) } } } #endif public struct OllamaUsageFetcher: Sendable { private static let settingsURL = URL(string: "https://ollama.com/settings")! @MainActor private static var recentDumps: [String] = [] private struct CookieCandidate { let cookieHeader: String let sourceLabel: String } enum RetryableParseFailure: Error { case missingUsageData } public let browserDetection: BrowserDetection private let makeURLSession: @Sendable (URLSessionTaskDelegate?) -> URLSession public init(browserDetection: BrowserDetection) { self.browserDetection = browserDetection self.makeURLSession = { delegate in URLSession(configuration: .ephemeral, delegate: delegate, delegateQueue: nil) } } init( browserDetection: BrowserDetection, makeURLSession: @escaping @Sendable (URLSessionTaskDelegate?) -> URLSession) { self.browserDetection = browserDetection self.makeURLSession = makeURLSession } public func fetch( cookieHeaderOverride: String? = nil, manualCookieMode: Bool = false, logger: ((String) -> Void)? = nil, now: Date = Date()) async throws -> OllamaUsageSnapshot { let cookieCandidates = try await self.resolveCookieCandidates( override: cookieHeaderOverride, manualCookieMode: manualCookieMode, logger: logger) return try await self.fetchUsingCookieCandidates( cookieCandidates, logger: logger, now: now) } static func shouldRetryWithNextCookieCandidate(after error: Error) -> Bool { switch error { case OllamaUsageError.invalidCredentials, OllamaUsageError.notLoggedIn: true case RetryableParseFailure.missingUsageData: true default: false } } private func fetchUsingCookieCandidates( _ candidates: [CookieCandidate], logger: ((String) -> Void)?, now: Date) async throws -> OllamaUsageSnapshot { do { return try await ProviderCandidateRetryRunner.run( candidates, shouldRetry: { error in Self.shouldRetryWithNextCookieCandidate(after: error) }, onRetry: { candidate, _ in logger?("[ollama] Auth failed for \(candidate.sourceLabel); trying next cookie candidate") }, attempt: { candidate in logger?("[ollama] Using cookies from \(candidate.sourceLabel)") let names = self.cookieNames(from: candidate.cookieHeader) if !names.isEmpty { logger?("[ollama] Cookie names: \(names.joined(separator: ", "))") } let diagnostics = RedirectDiagnostics(cookieHeader: candidate.cookieHeader, logger: logger) do { let (html, responseInfo) = try await self.fetchHTMLWithDiagnostics( cookieHeader: candidate.cookieHeader, diagnostics: diagnostics) if let logger { self.logDiagnostics(responseInfo: responseInfo, diagnostics: diagnostics, logger: logger) } do { return try Self.parseSnapshotForRetry(html: html, now: now) } catch { let surfacedError = Self.surfacedError(from: error) if let logger { logger("[ollama] Parse failed: \(surfacedError.localizedDescription)") self.logHTMLHints(html: html, logger: logger) } throw error } } catch { if let logger { self.logDiagnostics(responseInfo: nil, diagnostics: diagnostics, logger: logger) } throw error } }) } catch ProviderCandidateRetryRunnerError.noCandidates { throw OllamaUsageError.noSessionCookie } catch { throw Self.surfacedError(from: error) } } private static func parseSnapshotForRetry(html: String, now: Date) throws -> OllamaUsageSnapshot { switch OllamaUsageParser.parseClassified(html: html, now: now) { case let .success(snapshot): return snapshot case .failure(.notLoggedIn): throw OllamaUsageError.notLoggedIn case .failure(.missingUsageData): throw RetryableParseFailure.missingUsageData } } private static func surfacedError(from error: Error) -> Error { switch error { case RetryableParseFailure.missingUsageData: OllamaUsageError.parseFailed("Missing Ollama usage data.") default: error } } private func resolveCookieCandidates( override: String?, manualCookieMode: Bool, logger: ((String) -> Void)?) async throws -> [CookieCandidate] { if let manualHeader = try Self.resolveManualCookieHeader( override: override, manualCookieMode: manualCookieMode, logger: logger) { return [CookieCandidate(cookieHeader: manualHeader, sourceLabel: "manual cookie header")] } #if os(macOS) let sessions = try OllamaCookieImporter.importSessions( browserDetection: self.browserDetection, preferredBrowsers: OllamaCookieImporter.defaultPreferredBrowsers, allowFallbackBrowsers: OllamaCookieImporter.defaultAllowFallbackBrowsers, logger: logger) return sessions.map { session in CookieCandidate(cookieHeader: session.cookieHeader, sourceLabel: session.sourceLabel) } #else throw OllamaUsageError.noSessionCookie #endif } public func debugRawProbe( cookieHeaderOverride: String? = nil, manualCookieMode: Bool = false) async -> String { let stamp = ISO8601DateFormatter().string(from: Date()) var lines: [String] = [] lines.append("=== Ollama Debug Probe @ \(stamp) ===") lines.append("") do { let cookieHeader = try await self.resolveCookieHeader( override: cookieHeaderOverride, manualCookieMode: manualCookieMode, logger: { msg in lines.append("[cookie] \(msg)") }) let diagnostics = RedirectDiagnostics(cookieHeader: cookieHeader, logger: nil) let cookieNames = CookieHeaderNormalizer.pairs(from: cookieHeader).map(\.name) lines.append("Cookie names: \(cookieNames.joined(separator: ", "))") let (snapshot, responseInfo) = try await self.fetchWithDiagnostics( cookieHeader: cookieHeader, diagnostics: diagnostics) lines.append("") lines.append("Fetch Success") lines.append("Status: \(responseInfo.statusCode) \(responseInfo.url)") if !diagnostics.redirects.isEmpty { lines.append("") lines.append("Redirects:") for entry in diagnostics.redirects { lines.append(" \(entry)") } } lines.append("") lines.append("Plan: \(snapshot.planName ?? "unknown")") lines.append("Session: \(snapshot.sessionUsedPercent?.description ?? "nil")%") lines.append("Weekly: \(snapshot.weeklyUsedPercent?.description ?? "nil")%") lines.append("Session resetsAt: \(snapshot.sessionResetsAt?.description ?? "nil")") lines.append("Weekly resetsAt: \(snapshot.weeklyResetsAt?.description ?? "nil")") let output = lines.joined(separator: "\n") Task { @MainActor in Self.recordDump(output) } return output } catch { lines.append("") lines.append("Probe Failed: \(error.localizedDescription)") let output = lines.joined(separator: "\n") Task { @MainActor in Self.recordDump(output) } return output } } public static func latestDumps() async -> String { await MainActor.run { let result = Self.recentDumps.joined(separator: "\n\n---\n\n") return result.isEmpty ? "No Ollama probe dumps captured yet." : result } } private func resolveCookieHeader( override: String?, manualCookieMode: Bool, logger: ((String) -> Void)?) async throws -> String { if let manualHeader = try Self.resolveManualCookieHeader( override: override, manualCookieMode: manualCookieMode, logger: logger) { return manualHeader } #if os(macOS) let session = try OllamaCookieImporter.importSession( browserDetection: self.browserDetection, preferredBrowsers: OllamaCookieImporter.defaultPreferredBrowsers, allowFallbackBrowsers: OllamaCookieImporter.defaultAllowFallbackBrowsers, logger: logger) logger?("[ollama] Using cookies from \(session.sourceLabel)") return session.cookieHeader #else throw OllamaUsageError.noSessionCookie #endif } static func resolveManualCookieHeader( override: String?, manualCookieMode: Bool, logger: ((String) -> Void)? = nil) throws -> String? { if let override = CookieHeaderNormalizer.normalize(override) { guard hasRecognizedOllamaSessionCookie(in: override) else { logger?("[ollama] Manual cookie header missing recognized session cookie") throw OllamaUsageError.noSessionCookie } logger?("[ollama] Using manual cookie header") return override } if manualCookieMode { throw OllamaUsageError.noSessionCookie } return nil } private func fetchWithDiagnostics( cookieHeader: String, diagnostics: RedirectDiagnostics, now: Date = Date()) async throws -> (OllamaUsageSnapshot, ResponseInfo) { let (html, responseInfo) = try await self.fetchHTMLWithDiagnostics( cookieHeader: cookieHeader, diagnostics: diagnostics) let snapshot = try OllamaUsageParser.parse(html: html, now: now) return (snapshot, responseInfo) } private func fetchHTMLWithDiagnostics( cookieHeader: String, diagnostics: RedirectDiagnostics) async throws -> (String, ResponseInfo) { var request = URLRequest(url: Self.settingsURL) request.httpMethod = "GET" request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") request.setValue( "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", forHTTPHeaderField: "accept") request.setValue( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", forHTTPHeaderField: "user-agent") request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language") request.setValue("https://ollama.com", forHTTPHeaderField: "origin") request.setValue(Self.settingsURL.absoluteString, forHTTPHeaderField: "referer") let session = self.makeURLSession(diagnostics) let httpResponse = try await session.response(for: request) let responseInfo = ResponseInfo( statusCode: httpResponse.statusCode, url: httpResponse.response.url?.absoluteString ?? "unknown") if httpResponse.statusCode == 200, Self.isSignInRedirect(httpResponse.response.url) { throw OllamaUsageError.invalidCredentials } guard httpResponse.statusCode == 200 else { if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw OllamaUsageError.invalidCredentials } throw OllamaUsageError.networkError("HTTP \(httpResponse.statusCode)") } let html = String(data: httpResponse.data, encoding: .utf8) ?? "" return (html, responseInfo) } @MainActor private static func recordDump(_ text: String) { if self.recentDumps.count >= 5 { self.recentDumps.removeFirst() } self.recentDumps.append(text) } private final class RedirectDiagnostics: NSObject, URLSessionTaskDelegate, @unchecked Sendable { private let cookieHeader: String private let logger: ((String) -> Void)? var redirects: [String] = [] init(cookieHeader: String, logger: ((String) -> Void)?) { self.cookieHeader = cookieHeader self.logger = logger } func urlSession( _: URLSession, task _: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { let from = response.url?.absoluteString ?? "unknown" let to = request.url?.absoluteString ?? "unknown" self.redirects.append("\(response.statusCode) \(from) -> \(to)") var updated = request if OllamaUsageFetcher.shouldAttachCookie(to: request.url), !self.cookieHeader.isEmpty { updated.setValue(self.cookieHeader, forHTTPHeaderField: "Cookie") } else { updated.setValue(nil, forHTTPHeaderField: "Cookie") } if let referer = response.url?.absoluteString { updated.setValue(referer, forHTTPHeaderField: "referer") } if let logger { logger("[ollama] Redirect \(response.statusCode) \(from) -> \(to)") } completionHandler(updated) } } private struct ResponseInfo { let statusCode: Int let url: String } private func logDiagnostics( responseInfo: ResponseInfo?, diagnostics: RedirectDiagnostics, logger: (String) -> Void) { if let responseInfo { logger("[ollama] Response: \(responseInfo.statusCode) \(responseInfo.url)") } if !diagnostics.redirects.isEmpty { logger("[ollama] Redirects:") for entry in diagnostics.redirects { logger("[ollama] \(entry)") } } } private func logHTMLHints(html: String, logger: (String) -> Void) { logger("[ollama] HTML length: \(html.utf8.count) bytes") logger("[ollama] Contains Cloud Usage: \(html.contains("Cloud Usage"))") logger("[ollama] Contains Session usage: \(html.contains("Session usage"))") logger("[ollama] Contains Hourly usage: \(html.contains("Hourly usage"))") logger("[ollama] Contains Weekly usage: \(html.contains("Weekly usage"))") } private func cookieNames(from header: String) -> [String] { header.split(separator: ";", omittingEmptySubsequences: false).compactMap { part in let trimmed = part.trimmingCharacters(in: .whitespaces) guard let idx = trimmed.firstIndex(of: "=") else { return nil } let name = trimmed[.. Bool { guard url?.scheme?.lowercased() == "https" else { return false } guard let host = url?.host?.lowercased() else { return false } if host == "ollama.com" || host == "www.ollama.com" { return true } return host.hasSuffix(".ollama.com") } static func isSignInRedirect(_ url: URL?) -> Bool { guard url?.scheme?.lowercased() == "https" else { return false } guard let url, let host = url.host?.lowercased() else { return false } let path = url.path.lowercased() if host == "ollama.com" || host == "www.ollama.com" { return path == "/signin" } // WorkOS AuthKit ultimately bounces unauthenticated requests to a hosted // Ollama sign-in page on the `signin.ollama.com` subdomain; any landing // there means the session is expired and the user must sign in again. if host == "signin.ollama.com" { return true } // WorkOS AuthKit serves the hosted authorization flow from auth.workos.com // (and historically api.workos.com); match any WorkOS host carrying the // authorize path so the detection survives host changes or CNAMEs. return host.hasSuffix(".workos.com") && path.hasPrefix("/user_management/authorize") } } public struct OllamaAPISettingsReader: Sendable { public static let apiKeyEnvironmentKeys = [ "OLLAMA_API_KEY", "OLLAMA_KEY", ] public static func apiKey( environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { for key in self.apiKeyEnvironmentKeys { guard let value = self.cleaned(environment[key]), !value.isEmpty else { continue } return value } return nil } private static func cleaned(_ raw: String?) -> String? { guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil } if (value.hasPrefix("\"") && value.hasSuffix("\"")) || (value.hasPrefix("'") && value.hasSuffix("'")) { value = String(value.dropFirst().dropLast()) } return value.trimmingCharacters(in: .whitespacesAndNewlines) } } public struct OllamaAPIUsageSnapshot: Sendable { public let modelCount: Int public let updatedAt: Date public init(modelCount: Int, updatedAt: Date) { self.modelCount = modelCount self.updatedAt = updatedAt } public func toUsageSnapshot() -> UsageSnapshot { UsageSnapshot( primary: nil, secondary: nil, tertiary: nil, providerCost: nil, updatedAt: self.updatedAt, identity: ProviderIdentitySnapshot( providerID: .ollama, accountEmail: nil, accountOrganization: nil, loginMethod: "API key")) } } public enum OllamaAPIUsageFetcher { public static let tagsURL = URL(string: "https://ollama.com/api/tags")! public static let validationURL = URL(string: "https://ollama.com/api/web_search")! private static let timeoutSeconds: TimeInterval = 20 public static func fetchUsage( apiKey: String, tagsURL: URL = Self.tagsURL, validationURL: URL? = nil, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, now: Date = Date()) async throws -> OllamaAPIUsageSnapshot { let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { throw OllamaUsageError.missingAPIKey } let resolvedValidationURL = try self.resolveValidationURL(tagsURL: tagsURL, override: validationURL) try await self.validateAPIKey(trimmed, validationURL: resolvedValidationURL, transport: transport) var request = URLRequest(url: tagsURL) request.httpMethod = "GET" request.timeoutInterval = Self.timeoutSeconds request.setValue("Bearer \(trimmed)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("CodexBar/1.0", forHTTPHeaderField: "User-Agent") let response: ProviderHTTPResponse do { response = try await transport.response(for: request) } catch { if error is CancellationError || (error as? URLError)?.code == .cancelled || Task.isCancelled { throw CancellationError() } throw OllamaUsageError.networkError(error.localizedDescription) } switch response.statusCode { case 200: return try Self.parseTags(data: response.data, now: now) case 401, 403: throw OllamaUsageError.apiUnauthorized default: throw OllamaUsageError.networkError("HTTP \(response.statusCode)") } } private static func validateAPIKey( _ apiKey: String, validationURL: URL, transport: any ProviderHTTPTransport) async throws { var request = URLRequest(url: validationURL) request.httpMethod = "POST" request.timeoutInterval = Self.timeoutSeconds request.httpBody = Data(#"{"query":""}"#.utf8) request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("CodexBar/1.0", forHTTPHeaderField: "User-Agent") let response: ProviderHTTPResponse do { response = try await transport.response(for: request) } catch { if error is CancellationError || (error as? URLError)?.code == .cancelled || Task.isCancelled { throw CancellationError() } throw OllamaUsageError.networkError(error.localizedDescription) } switch response.statusCode { case 200, 400: return case 401, 403: throw OllamaUsageError.apiUnauthorized default: throw OllamaUsageError.networkError("HTTP \(response.statusCode)") } } private static func resolveValidationURL(tagsURL: URL, override: URL?) throws -> URL { let validationURL = override ?? (tagsURL == Self.tagsURL ? Self.validationURL : tagsURL.deletingLastPathComponent().appendingPathComponent("web_search")) let endpointValidator = ProviderEndpointOverrideValidator() guard endpointValidator.validatedURLAllowingLoopbackHTTP(tagsURL.absoluteString) != nil, endpointValidator.validatedURLAllowingLoopbackHTTP(validationURL.absoluteString) != nil else { throw OllamaUsageError.networkError( "Ollama API endpoints must use HTTPS or loopback HTTP.") } guard self.sameOrigin(tagsURL, validationURL) else { throw OllamaUsageError.networkError( "Ollama key validation and model catalog endpoints must share an origin.") } return validationURL } private static func sameOrigin(_ lhs: URL, _ rhs: URL) -> Bool { lhs.scheme?.lowercased() == rhs.scheme?.lowercased() && lhs.host?.lowercased() == rhs.host?.lowercased() && self.effectivePort(lhs) == self.effectivePort(rhs) } private static func effectivePort(_ url: URL) -> Int? { if let port = url.port { return port } switch url.scheme?.lowercased() { case "https": return 443 case "http": return 80 default: return nil } } static func _parseTagsForTesting(_ data: Data, now: Date = Date()) throws -> OllamaAPIUsageSnapshot { try self.parseTags(data: data, now: now) } private static func parseTags(data: Data, now: Date) throws -> OllamaAPIUsageSnapshot { do { let response = try JSONDecoder().decode(TagsResponse.self, from: data) return OllamaAPIUsageSnapshot(modelCount: response.models.count, updatedAt: now) } catch { throw OllamaUsageError.parseFailed(error.localizedDescription) } } private struct TagsResponse: Decodable { let models: [Model] } private struct Model: Decodable {} }