feat(s3): DSAskClient HTTP bridge + realRouter seam (FU-B)
- LiveDSAskClient: S3-owned concrete DSAskClient (GET /search/ask -> decode AIFabric.AskResponse), the piece S2's plan assigned to S3 for the real RemoteDSProvider - AppAIComposition.realRouter(): makeDefaultRouter(client: LiveDSAskClient) — the one-call swap from mock to the real S2 fabric; app default stays mockRouter (offline scaffold) - DSError.from made public (used cross-module by the bridge) swift build + swift test green (71). Sources/AI untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import AIFabric
|
||||
import DSKit
|
||||
|
||||
/// The ONE composition touch-point that names MockAIProvider. When S2 ships real providers,
|
||||
/// only this file changes (mockProviders -> realProviders) — AIService, views, and intents stay put.
|
||||
@@ -23,4 +24,14 @@ public enum AppAIComposition {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// FU-B seam: the real S2 fabric (RemoteDS/LocalMLX/OnDevice/Specialized) via `makeDefaultRouter`,
|
||||
/// fed S3's concrete `LiveDSAskClient`. The app default stays `mockRouter` (offline scaffold);
|
||||
/// switching to live is THIS one call — AIService, views, and intents are unchanged.
|
||||
public static func realRouter(
|
||||
base: DSBaseURL = .publicTLS,
|
||||
token: @escaping @Sendable () async -> String? = { nil }
|
||||
) -> AIRouter {
|
||||
makeDefaultRouter(client: LiveDSAskClient(base: base, token: token))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
import AIFabric
|
||||
import DSKit
|
||||
|
||||
/// S3-owned concrete `DSAskClient` (the HTTP seam S2's `RemoteDSProvider` calls). Hits DS
|
||||
/// `GET /search/ask?q=&backend=` and decodes **AIFabric.AskResponse** (a decode-only type that
|
||||
/// RemoteDSProvider maps to AICompletionResponse). This is the piece S2's plan assigns to S3
|
||||
/// ("구체 DSAskClient(HTTP) = S3 소유"). FU-A: exercised only when the real router is wired.
|
||||
public struct LiveDSAskClient: DSAskClient {
|
||||
private let base: DSBaseURL
|
||||
private let session: URLSession
|
||||
private let decoder: JSONDecoder
|
||||
private let token: @Sendable () async -> String?
|
||||
|
||||
public init(
|
||||
base: DSBaseURL = .publicTLS,
|
||||
session: URLSession = .shared,
|
||||
token: @escaping @Sendable () async -> String? = { nil }
|
||||
) {
|
||||
self.base = base
|
||||
self.session = session
|
||||
self.decoder = DSDecoder.make()
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public func ask(query: String, backend: String) async throws -> AIFabric.AskResponse {
|
||||
let raw = base.url.absoluteString + "/search/ask"
|
||||
guard var comps = URLComponents(string: raw) else {
|
||||
throw DSError.transport(underlying: "bad ask URL")
|
||||
}
|
||||
comps.queryItems = [
|
||||
URLQueryItem(name: "q", value: query),
|
||||
URLQueryItem(name: "backend", value: backend),
|
||||
]
|
||||
guard let url = comps.url else { throw DSError.transport(underlying: "bad ask URL components") }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if let token = await token() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw DSError.transport(underlying: "no HTTP response")
|
||||
}
|
||||
guard (200..<300).contains(http.statusCode) else {
|
||||
throw DSError.from(status: http.statusCode, data: data)
|
||||
}
|
||||
return try decoder.decode(AIFabric.AskResponse.self, from: data)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public enum DSError: Error, Sendable {
|
||||
return false
|
||||
}
|
||||
|
||||
static func from(status: Int, data: Data) -> DSError {
|
||||
public static func from(status: Int, data: Data) -> DSError {
|
||||
let body = DSErrorBody.parse(data)
|
||||
switch status {
|
||||
case 401: return .unauthorized(message: body?.message)
|
||||
|
||||
Reference in New Issue
Block a user