5383a93f98
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>
83 lines
4.0 KiB
Swift
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)
|
|
}
|