Files
hyungi_document_server/Sources/AI/Composition.swift
T
hyungi 5383a93f98 feat(ai-fabric): S2 LLM 패브릭 4 provider 결선 + 컴포지션 루트
risk-first 채움(RemoteDS→LocalMLX→OnDevice→Specialized) + makeDefaultRouter 컴포지션 루트.
동결 인터페이스(AIProvider/AIRouter/MockAIProvider) 무변경. SPM AIFabric 단독 빌드·테스트(46 PASS).

- RemoteDS: DSAskClient seam + AskResponse(ask.json) 매핑 + backend exhaustive switch(qwen/cloud TODO)
- LocalMLX: GET /v1/models probe + OpenAI /v1/chat/completions system/user call-shape + non-200 backendError
- OnDevice: FoundationModels 라이브(M5 Max) availability + respond() + GenerationError 9-case 매핑 + stateless/prewarm
- Specialized: scaffold-only(명시 unavailable, vision 폴백 가시화), cloud='claude-cloud' 503
- config 단일소스(env override) + 타임아웃/취소(URLSession 자동 honor, OnDevice 협조적)

실측 동결(S2-3a, M5 Max): availability=available · 취소=COOPERATIVE(~33ms) · 오버플로=exceededContextWindowSize
  · GenerationError 9-case(refusal·concurrentRequests 추가 발견, plan 정정).
한계: LocalMLX fixture=PROVISIONAL_SYNTHETIC(맥미니 offline → 라이브 재캡처 S2-Ff 대기).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:20:10 +09:00

83 lines
4.0 KiB
Swift

// Composition.swift S2 S3 ( b) + config.
//
// INTEGRATION ( b): (S3) `AIFabric` product ** SwiftPM ** ,
// Sources/AI ( / ). S3 makeDefaultRouter(...)
// MockAIProvider . DSAskClient(HTTP) = S3 .
//
// (S2-Fa): raw URL swap
// (2026-05-17 Hermes incident ). env override . ([[feedback_hermes_config_single_source_envvar]])
import Foundation
import os
public struct AIProviderConfiguration: Sendable {
/// llm-router base (trailing slash base; provider append).
public var localMLXBaseURL: URL
/// llm-router ( provisional 'gemma-macmini').
public var localMLXModel: String
/// DS API base S3 DSAskClient . https://document.hyungi.net/api · http://100.110.63.63:8000/api.
/// : DS `/search/ask` **trailing slash **( S3 client ).
public var dsBaseURL: URL
public var requestTimeout: TimeInterval
public var probeTimeout: TimeInterval
public init(
localMLXBaseURL: URL,
localMLXModel: String = "gemma-macmini",
dsBaseURL: URL,
requestTimeout: TimeInterval = 60,
probeTimeout: TimeInterval = 2
) {
self.localMLXBaseURL = localMLXBaseURL
self.localMLXModel = localMLXModel
self.dsBaseURL = dsBaseURL
self.requestTimeout = requestTimeout
self.probeTimeout = probeTimeout
}
/// override ( source). .
public static func resolved(
environment: [String: String] = ProcessInfo.processInfo.environment
) -> AIProviderConfiguration {
let localMLX = environment["AIFABRIC_LOCALMLX_URL"].flatMap(URL.init(string:))
?? URL(string: "http://100.76.254.116:8890")!
let model = environment["AIFABRIC_LOCALMLX_MODEL"] ?? "gemma-macmini"
let ds = environment["AIFABRIC_DS_URL"].flatMap(URL.init(string:))
?? URL(string: "https://document.hyungi.net/api")!
return AIProviderConfiguration(localMLXBaseURL: localMLX, localMLXModel: model, dsBaseURL: ds)
}
}
/// OSLog / (silent ). S3 (public).
public enum AIFabricLog {
static let router = Logger(subsystem: "ds-app.AIFabric", category: "AIRouter")
public static let routerHook: @Sendable (String) -> Void = { msg in
router.info("\(msg, privacy: .public)")
}
}
/// S3 S2 . 4 provider (vision ) + + log .
/// - client: S3 DS ask client(HTTP).
/// - config: ( = env override ).
/// - session: LocalMLX URLSession( .shared; mock ).
public func makeDefaultRouter(
client: DSAskClient,
config: AIProviderConfiguration = .resolved(),
session: URLSession = .shared,
policy: AIRoutingPolicy = .default,
log: @escaping @Sendable (String) -> Void = AIFabricLog.routerHook
) -> AIRouter {
let providers: [AIProviderID: any AIProvider] = [
.remoteDS: RemoteDSProvider(client: client),
.localMLX: LocalMLXProvider(
baseURL: config.localMLXBaseURL,
model: config.localMLXModel,
session: session,
requestTimeout: config.requestTimeout,
probeTimeout: config.probeTimeout
),
.onDevice: OnDeviceProvider(),
.specialized: SpecializedProvider(), // scaffold() vision
]
return AIRouter(providers: providers, policy: policy, log: log)
}