@dvai-bridge/ios-shared-core 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ # Deep Voice Ai Limited - Software License Agreement
2
+
3
+ **Version 1.0.0**
4
+
5
+ This License Agreement governs the use of the DVAI-Bridge software (the "Software"). By downloading, installing, or using the Software, you agree to be bound by the terms of this License.
6
+
7
+ ---
8
+
9
+ ## 1. LICENSE GRANTS
10
+
11
+ ### 1.1 Development and Personal Use (Free Tier)
12
+ Deep Voice Ai Limited ("Licensor") grants you a non-exclusive, non-transferable, royalty-free license to use the Software solely for:
13
+ - Internal development and testing purposes.
14
+ - Non-commercial personal projects.
15
+ - Academic and non-profit research.
16
+
17
+ ### 1.2 Commercial Use (Paid Tier)
18
+ Any use of the Software for **Commercial Purposes** requires a separate, paid Commercial License from Licensor. "Commercial Purposes" include:
19
+ - Use in production environments.
20
+ - Integration into revenue-generating products or services.
21
+ - Distribution to third-party customers for a fee.
22
+ - Use by an entity with more than $100,000 USD in annual revenue.
23
+
24
+ To obtain a Commercial License, contact `info@deepvoiceai.co` or visit `https://deepvoiceai.co/licensing`.
25
+
26
+ ---
27
+
28
+ ## 2. RESTRICTIONS
29
+ Except as expressly permitted, you may not:
30
+ - Sublicense, rent, lease, or resell the Software without express permission.
31
+ - Remove any proprietary notices or branding from the Software.
32
+ - Use the Software for any illegal or malicious purposes.
33
+
34
+ ---
35
+
36
+ ## 3. INTELLECTUAL PROPERTY
37
+ The Software is owned by **Deep Voice Ai Limited** and is protected by copyright and intellectual property laws. This agreement does not transfer ownership of the Software.
38
+
39
+ ---
40
+
41
+ ## 4. NO WARRANTY
42
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
43
+
44
+ ---
45
+
46
+ ## 5. GOVERNING LAW
47
+ This License shall be governed by and construed in accordance with the laws of the jurisdiction where Deep Voice Ai Limited is registered.
48
+
49
+ ---
50
+
51
+ © 2026 Deep Voice Ai Limited. All rights reserved.
package/Package.swift ADDED
@@ -0,0 +1,62 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ // Shared HTTP-server + handler-dispatch types used by ALL backend cores
5
+ // (llama / foundation / coreml / mlx). This package was extracted from
6
+ // DVAILlamaCore so non-llama backends don't transitively pull in
7
+ // llama.xcframework + mtmd.xcframework — that coupling was the only
8
+ // thing preventing per-backend Mac Catalyst support, since llama.cpp's
9
+ // `build-xcframework.sh` doesn't produce a Catalyst slice.
10
+ //
11
+ // Public types here:
12
+ // - HandlerContext, HandlerResponse, DVAIHandlers (HandlerContext.swift)
13
+ // - DVAIRequest, DVAIResponse, DVAIHttpMethod (HandlerContext.swift)
14
+ // - CORSConfig, dispatchRoute, formatResponse (HandlerDispatch.swift)
15
+ // - HttpServer (HttpServer.swift)
16
+ //
17
+ // v3.2.0 — migrated off Telegraph onto Hummingbird. Telegraph 0.40
18
+ // buffered SSE bodies server-side AND its private HTTPParserC clang
19
+ // module collided with swift-nio's CNIOLLHTTP whenever a downstream
20
+ // target imported both. Hummingbird (built on swift-nio) gives us
21
+ // proper streaming SSE plus a single, consistent C-module footprint
22
+ // across DVAISharedCore, DVAIBridge, and the OffloadProxy. Public
23
+ // API surface is unchanged: HttpServer.installRoutes / tryBind / stop
24
+ // signatures match the Telegraph era 1:1 (the only call-site change
25
+ // is install-then-bind ordering, since Hummingbird requires the
26
+ // router at Application construction time).
27
+ //
28
+ // Platform floor: iOS 17 / macOS 14 — Hummingbird 2.x's own minimum.
29
+ // Earlier (Telegraph era) we shipped iOS 14 / macOS 12; the SSE
30
+ // streaming + cross-platform clang-module fix in v3.2.0 required
31
+ // migrating to swift-nio's HTTP stack, which carries the iOS 17 floor
32
+ // transitively. Backend cores bump their floors to match (DVAILlamaCore
33
+ // → 17/14; DVAIFoundationCore + DVAIMLXCore + DVAIBridge already at
34
+ // iOS 17+/macOS 14+).
35
+ let package = Package(
36
+ name: "DVAISharedCore",
37
+ platforms: [.iOS(.v17), .macOS(.v14)],
38
+ products: [
39
+ .library(name: "DVAISharedCore", targets: ["DVAISharedCore"]),
40
+ ],
41
+ dependencies: [
42
+ // Hummingbird 2.x — our HTTP server backbone. Built on swift-nio
43
+ // so we get streaming SSE response bodies for free. Pinned to
44
+ // 2.0.0 minor so swift-nio dep ranges line up with mlx-swift's
45
+ // pins; can be relaxed once Hummingbird 3 stabilises.
46
+ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
47
+ ],
48
+ targets: [
49
+ .target(
50
+ name: "DVAISharedCore",
51
+ dependencies: [
52
+ .product(name: "Hummingbird", package: "hummingbird"),
53
+ ],
54
+ path: "ios/Sources/DVAISharedCore"
55
+ ),
56
+ .testTarget(
57
+ name: "DVAISharedCoreTests",
58
+ dependencies: ["DVAISharedCore"],
59
+ path: "ios/Tests/DVAISharedCoreTests"
60
+ ),
61
+ ]
62
+ )
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ ![DVAI-Bridge](/assets/banner.png)
2
+
3
+ # DVAI-Bridge
4
+
5
+ <!-- [![Smoke — real models](https://github.com/Westenets/dvai-bridge/actions/workflows/smoke-real-models.yml/badge.svg?branch=main)](https://github.com/Westenets/dvai-bridge/actions/workflows/smoke-real-models.yml) -->
6
+
7
+ [![License](https://img.shields.io/badge/License-Commercial-blue.svg)](LICENSE) ![Node.js](https://img.shields.io/badge/Node.js-22+-green?logo=node.js) ![TypeScript](https://img.shields.io/badge/TypeScript-5.6+-blue?logo=typescript) ![Swift](https://img.shields.io/badge/Swift-5.9+-F05138?logo=swift) ![Kotlin](https://img.shields.io/badge/Kotlin-2.0+-7F52FF?logo=kotlin) ![Flutter](https://img.shields.io/badge/Flutter-3.39+-02569B?logo=flutter) ![.NET](https://img.shields.io/badge/.NET-10.0_LTS-512BD4?logo=dotnet)
8
+
9
+ > **The local OpenAI server you embed inside your app.**
10
+ > One library. One HTTP wire. Every platform. Zero install for your users.
11
+
12
+ **Docs:** [dvai-bridge.deepvoiceai.co](https://dvai-bridge.deepvoiceai.co)
13
+
14
+ ```ts
15
+ import { DVAI } from "@dvai-bridge/core";
16
+ import OpenAI from "openai";
17
+
18
+ const dvai = new DVAI({ backend: "transformers" });
19
+ await dvai.initialize();
20
+
21
+ const openai = new OpenAI({ baseURL: dvai.baseUrl, apiKey: "ignored" });
22
+ await openai.chat.completions.create({
23
+ model: dvai.transformersModelId,
24
+ messages: [{ role: "user", content: "Hello!" }],
25
+ });
26
+ ```
27
+
28
+ That's it. A real OpenAI-compatible server is now running inside your app's
29
+ own process. Point any OpenAI client — LangChain, the OpenAI SDK, the Vercel
30
+ AI SDK, anything — at `dvai.baseUrl` and your agent code keeps working.
31
+
32
+ Built by **[Deep Voice AI](https://deepvoiceai.co)**.
33
+
34
+ ---
35
+
36
+ ## Why it exists
37
+
38
+ Local AI works beautifully on a laptop with **Ollama + LangChain**. Then you
39
+ try to ship the app and your users don't have Ollama. Mobile can't run it.
40
+ Corporate IT won't add another daemon. So you reinvent the same plumbing —
41
+ spawn an inference engine, bind a port, translate to OpenAI HTTP, handle
42
+ CORS, manage lifecycle, wrap the accelerator of the day per platform — and
43
+ do it all over again for every target OS.
44
+
45
+ DVAI-Bridge is that plumbing, packaged as a library, for every client
46
+ platform.
47
+
48
+ ---
49
+
50
+ ## What you get
51
+
52
+ - **One OpenAI HTTP surface.** Bound on `127.0.0.1` (or `0.0.0.0` for
53
+ device-to-device). Streaming, embeddings, models, recovery — all built in.
54
+ - **Six SDKs.** `@dvai-bridge/core` + `react` + `vanilla` + `capacitor`,
55
+ `DVAIBridge` (Swift / iOS), `co.deepvoiceai:dvai-bridge` (Kotlin / Android),
56
+ `@dvai-bridge/react-native`, `dvai_bridge` (Flutter), `co.deepvoiceai.dvai-bridge` (.NET).
57
+ - **Nine backends.** WebLLM, Transformers.js, llama.cpp, Apple Foundation
58
+ Models, MLX, CoreML / ANE, MediaPipe LLM, LiteRT, ONNX Runtime GenAI —
59
+ selected per-platform, invisible to your agent code.
60
+ - **Native acceleration** wherever it runs: WebGPU in browsers, CUDA / Metal
61
+ / Vulkan / DirectML on desktop, ANE / Metal / MLX on iOS, NNAPI / QNN
62
+ Hexagon / GPU delegate on Android.
63
+ - **Multimodal.** Text, image, audio, video — declarative loader for
64
+ cutting-edge models (Gemma 4, LLaVA, Idefics) without waiting for library
65
+ updates.
66
+ - **Distributed inference (v3.0+).** Phone too slow? Offload to your laptop
67
+ on the same Wi-Fi via mDNS pairing — same OpenAI wire, transparent to
68
+ your code. Internet path via a self-hostable rendezvous server.
69
+ - **DVAI Hub (v3.1+).** A first-party desktop utility that turns any device
70
+ into a strong-peer for the rest of your fleet. Brand-neutral install via
71
+ Homebrew / winget / GitHub Releases, OR fork it for your own branded
72
+ companion. Routes through Ollama / LM Studio / vLLM / llama-server /
73
+ llamafile if you've already got those running.
74
+ - **Zero user install.** It's a library, not a daemon. `npm install`,
75
+ `cocoapods`, gradle — your CI already has the muscle for it.
76
+
77
+ ---
78
+
79
+ ## Supported platforms
80
+
81
+ | Stack | Package | Backends |
82
+ | --- | --- | --- |
83
+ | Browser (React, Vue, Svelte, vanilla JS) | `@dvai-bridge/core` + `react` / `vanilla` | WebLLM (WebGPU), Transformers.js (WebGPU / WASM SIMD) |
84
+ | Node / Bun / Electron | `@dvai-bridge/core` | Transformers.js, native llama.cpp |
85
+ | Capacitor hybrid mobile | `@dvai-bridge/capacitor` + backend slice | Native llama.cpp (Metal iOS, Vulkan / CPU Android) |
86
+ | iOS native (Swift) | `DVAIBridge` (SPM / CocoaPods) | llama.cpp (Metal), CoreML / ANE, Apple Foundation Models, MLX |
87
+ | Android native (Kotlin / Java) | `co.deepvoiceai:dvai-bridge` (AAR) | llama.cpp, MediaPipe LLM, LiteRT, NNAPI / QNN |
88
+ | React Native (≥0.77, TurboModule) | `@dvai-bridge/react-native` | All iOS + Android backends (delegates) |
89
+ | Flutter (≥3.39) | `dvai_bridge` (pub.dev) | All iOS + Android backends (Pigeon channels) |
90
+ | .NET 10 LTS (MAUI / Avalonia / WinUI / Catalyst / desktop) | `co.deepvoiceai.dvai-bridge*` (NuGet) | iOS / Android delegate to native; desktop = llama.cpp + ONNX Runtime GenAI + ML.NET |
91
+
92
+ Full quickstart per platform: [dvai-bridge.deepvoiceai.co/guide/getting-started](https://dvai-bridge.deepvoiceai.co/guide/getting-started)
93
+
94
+ ---
95
+
96
+ ## Examples
97
+
98
+ ```ts
99
+ // React
100
+ import { DVAIProvider, useDVAI } from "@dvai-bridge/react";
101
+ <DVAIProvider config={{ backend: "transformers" }}>
102
+ <Chat />
103
+ </DVAIProvider>;
104
+ function Chat() {
105
+ const { isReady, baseUrl } = useDVAI();
106
+ return isReady ? <div>Local AI live at {baseUrl}</div> : <Loading />;
107
+ }
108
+ ```
109
+
110
+ ```swift
111
+ // iOS
112
+ let server = try await DVAIBridge.shared.start()
113
+ // server.baseUrl = "http://127.0.0.1:38883/v1"
114
+ ```
115
+
116
+ ```kotlin
117
+ // Android
118
+ val server = DVAIBridge.start(context)
119
+ // server.baseUrl = "http://127.0.0.1:38883/v1"
120
+ ```
121
+
122
+ ```dart
123
+ // Flutter
124
+ final state = await DVAIBridge.instance.start(
125
+ backend: BackendKind.auto,
126
+ modelPath: '/path/to/model.gguf',
127
+ );
128
+ // state.baseUrl = "http://127.0.0.1:38883/v1"
129
+ ```
130
+
131
+ ```csharp
132
+ // .NET
133
+ var server = await DVAIBridge.Shared.StartAsync(new StartOptions {
134
+ Backend = BackendKind.Auto,
135
+ ModelPath = "/path/to/model.gguf",
136
+ });
137
+ // server.BaseUrl = "http://127.0.0.1:38883/v1"
138
+ ```
139
+
140
+ Multimodal, streaming, embeddings, distributed offload, the Hub —
141
+ everything's at the [docs site](https://dvai-bridge.deepvoiceai.co).
142
+
143
+ ---
144
+
145
+ ## What's new in v3.1
146
+
147
+ - **DVAI Hub** — Tauri desktop utility that's the strong-peer side of v3
148
+ distributed inference. `brew install deepvoiceai/dvai-hub/dvai-hub` (or
149
+ `winget install DeepVoiceAI.DVAIHub`) → mobile apps on the same Wi-Fi
150
+ pair with it and offload heavy inference. [Guide →](https://dvai-bridge.deepvoiceai.co/guide/dvai-hub)
151
+ - **External-engine bridge.** Hub surfaces Ollama / LM Studio / vLLM /
152
+ llama-server / llamafile as additional backend pools so paired apps
153
+ serve from whatever's already cached. Opt-in per engine.
154
+ - **Strict substitution policy.** Models with mismatched family / version /
155
+ size / type are refused by default; quant-only mismatches gated behind a
156
+ per-pairing `preferBetterQuant` flag. No silent mis-routing.
157
+ - **HMAC-signed identity** on `/v1/chat/completions`. Per-app audit logs
158
+ surface who served what, with structured `(appId, peerDeviceId,
159
+ engine, requestedModel, servedModel, outcome)` rows.
160
+ - **Library finalization.** `httpBindHost` (LAN bind), `chatCompletionInterceptor`
161
+ (extension point), HMAC primitives re-exported, `/v1/dvai/*` routes
162
+ actually dispatched, TransformersBackend Node-mode device fix.
163
+ [Migration v3.0 → v3.1 →](https://dvai-bridge.deepvoiceai.co/migration/v3.0-to-v3.1)
164
+
165
+ ---
166
+
167
+ ## Robustness
168
+
169
+ Streaming-correct (SSE passthrough + blank-chunk detection), generation
170
+ timeout, automatic engine-state recovery on fatal errors, port fallback,
171
+ worker offloading, Private Network Access ready, CORS configured. The
172
+ boring substrate so your agent code never has to think about it.
173
+
174
+ ---
175
+
176
+ ## Licensing
177
+
178
+ Dual: **free for development & personal use** on `localhost` (verified at
179
+ runtime). **Commercial use** requires a license key — `info@deepvoiceai.co`.
180
+
181
+ ---
182
+
183
+ ## Contributing
184
+
185
+ PRs welcome.
186
+
187
+ ```bash
188
+ pnpm install
189
+ pnpm build
190
+ bash scripts/build-all.sh # full matrix (auto-skips per-host)
191
+ ```
192
+
193
+ [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the PR flow. Per-platform
194
+ contributor docs (iOS / Android / RN / Flutter / .NET) under
195
+ [`docs/development/`](./docs/development/).
196
+
197
+ ---
198
+
199
+ © Deep Voice AI Limited. All rights reserved.
@@ -0,0 +1,121 @@
1
+ import Foundation
2
+
3
+ public struct HandlerContext: Sendable {
4
+ public let modelId: String
5
+ public let backendName: String
6
+
7
+ public init(modelId: String, backendName: String) {
8
+ self.modelId = modelId
9
+ self.backendName = backendName
10
+ }
11
+ }
12
+
13
+ public enum HandlerResponse {
14
+ case json(Int, Any)
15
+ case sse(AsyncStream<String>)
16
+ case error(Int, String)
17
+ }
18
+
19
+ public protocol DVAIHandlers: Sendable {
20
+ func handleChatCompletion(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse
21
+ func handleCompletion(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse
22
+ func handleEmbeddings(body: [String: Any], ctx: HandlerContext) async throws -> HandlerResponse
23
+ func handleModels(ctx: HandlerContext) async throws -> HandlerResponse
24
+ }
25
+
26
+ /// HTTP method enum used by the framework-neutral request abstraction
27
+ /// below. We could use Foundation's `URLRequest.HTTPMethod` strings, but
28
+ /// a closed enum makes the dispatch switch in `dispatchRoute` exhaustive.
29
+ public enum DVAIHttpMethod: String, Sendable {
30
+ case get = "GET"
31
+ case post = "POST"
32
+ case options = "OPTIONS"
33
+ case put = "PUT"
34
+ case delete = "DELETE"
35
+ case patch = "PATCH"
36
+ case head = "HEAD"
37
+
38
+ /// Decode any verb string into a method. Defaults to `.get` for
39
+ /// unknown verbs (the dispatcher then falls through to its
40
+ /// 404-with-CORS path).
41
+ public static func from(_ raw: String) -> DVAIHttpMethod {
42
+ DVAIHttpMethod(rawValue: raw.uppercased()) ?? .get
43
+ }
44
+ }
45
+
46
+ /// Framework-neutral request shape consumed by `dispatchRoute`. The
47
+ /// `HttpServer` actor (Hummingbird-backed) translates incoming
48
+ /// transport requests into this shape, so `dispatchRoute` and the
49
+ /// `DVAIHandlers` protocol stay independent of any specific HTTP
50
+ /// server framework. Tests construct these directly without spinning
51
+ /// up a server.
52
+ public struct DVAIRequest: Sendable {
53
+ public let method: DVAIHttpMethod
54
+ public let path: String
55
+ public let headers: [String: String]
56
+ public let body: Data
57
+
58
+ public init(
59
+ method: DVAIHttpMethod,
60
+ path: String,
61
+ headers: [String: String] = [:],
62
+ body: Data = Data()
63
+ ) {
64
+ self.method = method
65
+ self.path = path
66
+ self.headers = headers
67
+ self.body = body
68
+ }
69
+
70
+ /// Case-insensitive header lookup. The headers map is preserved
71
+ /// as the original casing the transport delivered, so direct
72
+ /// `headers["Origin"]` lookups still work for callers that know
73
+ /// the canonical case.
74
+ public func header(_ name: String) -> String? {
75
+ if let v = headers[name] { return v }
76
+ let lower = name.lowercased()
77
+ for (k, v) in headers where k.lowercased() == lower { return v }
78
+ return nil
79
+ }
80
+ }
81
+
82
+ /// Framework-neutral response shape returned by `dispatchRoute`. The
83
+ /// `HttpServer` translates this back into the transport's response
84
+ /// type. Two flavors:
85
+ ///
86
+ /// - `.buffered(...)` — body is fully materialised in memory; used
87
+ /// for JSON, errors, and CORS preflights.
88
+ /// - `.streaming(...)` — body is an `AsyncStream<String>` that the
89
+ /// transport flushes incrementally. Used for SSE chat-completion
90
+ /// streams. The transport is expected to write each chunk to the
91
+ /// consumer as it arrives.
92
+ public enum DVAIResponse: @unchecked Sendable {
93
+ case buffered(status: Int, headers: [String: String], body: Data)
94
+ case streaming(status: Int, headers: [String: String], stream: AsyncStream<String>)
95
+
96
+ /// Convenience for tests + buffered consumers.
97
+ public var status: Int {
98
+ switch self {
99
+ case .buffered(let s, _, _): return s
100
+ case .streaming(let s, _, _): return s
101
+ }
102
+ }
103
+
104
+ /// Convenience for tests + buffered consumers.
105
+ public var headers: [String: String] {
106
+ switch self {
107
+ case .buffered(_, let h, _): return h
108
+ case .streaming(_, let h, _): return h
109
+ }
110
+ }
111
+
112
+ /// Buffered body if the response is buffered; empty `Data` otherwise.
113
+ /// (Test ergonomics — streaming responses don't expose a synchronous
114
+ /// body, callers must consume the stream.)
115
+ public var body: Data {
116
+ switch self {
117
+ case .buffered(_, _, let b): return b
118
+ case .streaming: return Data()
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,132 @@
1
+ // Internal/HandlerDispatch.swift
2
+ import Foundation
3
+
4
+ /// CORS configuration for the dispatch layer.
5
+ public enum CORSConfig {
6
+ case wildcard
7
+ case exact(String)
8
+ case allowlist([String])
9
+
10
+ func headerValue(forRequestOrigin reqOrigin: String?) -> String? {
11
+ switch self {
12
+ case .wildcard:
13
+ return "*"
14
+ case .exact(let s):
15
+ return s
16
+ case .allowlist(let list):
17
+ guard let o = reqOrigin else { return nil }
18
+ return list.contains(o) ? o : nil
19
+ }
20
+ }
21
+ }
22
+
23
+ /// Builds the standard CORS + Private Network Access header set for a
24
+ /// response.
25
+ public func corsHeaders(reqOrigin: String?, config: CORSConfig) -> [String: String] {
26
+ var headers: [String: String] = [
27
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
28
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
29
+ "Access-Control-Allow-Private-Network": "true",
30
+ ]
31
+ if let allow = config.headerValue(forRequestOrigin: reqOrigin) {
32
+ headers["Access-Control-Allow-Origin"] = allow
33
+ }
34
+ return headers
35
+ }
36
+
37
+ /// Dispatches a framework-neutral `DVAIRequest` to the appropriate
38
+ /// `DVAIHandlers` method.
39
+ ///
40
+ /// - Catches handler errors and converts them to 500 responses.
41
+ /// - Handles `OPTIONS` preflight (204) and unknown routes (404).
42
+ /// - Adds CORS + PNA headers to every response.
43
+ /// - Returns a `.streaming` `DVAIResponse` for SSE handlers so the
44
+ /// transport can flush chunks incrementally.
45
+ public func dispatchRoute(
46
+ request: DVAIRequest,
47
+ handlers: DVAIHandlers,
48
+ ctx: HandlerContext,
49
+ corsConfig: CORSConfig
50
+ ) async -> DVAIResponse {
51
+ let reqOrigin = request.header("Origin")
52
+ let cors = corsHeaders(reqOrigin: reqOrigin, config: corsConfig)
53
+
54
+ // OPTIONS preflight — respond 204 with CORS headers, no body.
55
+ if request.method == .options {
56
+ return .buffered(status: 204, headers: cors, body: Data())
57
+ }
58
+
59
+ let path = request.path
60
+ let method = request.method
61
+
62
+ do {
63
+ let handlerResponse: HandlerResponse
64
+ switch (method, path) {
65
+ case (.post, "/v1/chat/completions"):
66
+ let body = try parseJSONBody(request.body)
67
+ handlerResponse = try await handlers.handleChatCompletion(body: body, ctx: ctx)
68
+ case (.post, "/v1/completions"):
69
+ let body = try parseJSONBody(request.body)
70
+ handlerResponse = try await handlers.handleCompletion(body: body, ctx: ctx)
71
+ case (.post, "/v1/embeddings"):
72
+ let body = try parseJSONBody(request.body)
73
+ handlerResponse = try await handlers.handleEmbeddings(body: body, ctx: ctx)
74
+ case (.get, "/v1/models"):
75
+ handlerResponse = try await handlers.handleModels(ctx: ctx)
76
+ default:
77
+ return makeErrorResponse(404, "not found", cors: cors)
78
+ }
79
+
80
+ return formatResponse(handlerResponse, cors: cors)
81
+ } catch {
82
+ return makeErrorResponse(500, error.localizedDescription, cors: cors)
83
+ }
84
+ }
85
+
86
+ /// Parse JSON body (empty data → empty dict).
87
+ public func parseJSONBody(_ data: Data) throws -> [String: Any] {
88
+ guard !data.isEmpty else { return [:] }
89
+ let obj = try JSONSerialization.jsonObject(with: data, options: [])
90
+ guard let dict = obj as? [String: Any] else {
91
+ throw NSError(
92
+ domain: "DVAIBridgeLlama",
93
+ code: 400,
94
+ userInfo: [NSLocalizedDescriptionKey: "Body must be a JSON object"]
95
+ )
96
+ }
97
+ return dict
98
+ }
99
+
100
+ /// Convert a `HandlerResponse` into a framework-neutral `DVAIResponse`.
101
+ /// SSE responses become `.streaming` so the transport flushes chunks
102
+ /// to the consumer as they arrive (Hummingbird's `ResponseBody` writer
103
+ /// pattern); JSON / error responses become `.buffered`.
104
+ public func formatResponse(_ response: HandlerResponse, cors: [String: String]) -> DVAIResponse {
105
+ switch response {
106
+ case .json(let status, let body):
107
+ let data = (try? JSONSerialization.data(withJSONObject: body, options: [])) ?? Data()
108
+ var headers = cors
109
+ headers["Content-Type"] = "application/json"
110
+ return .buffered(status: status, headers: headers, body: data)
111
+
112
+ case .sse(let stream):
113
+ var headers = cors
114
+ headers["Content-Type"] = "text/event-stream"
115
+ headers["Cache-Control"] = "no-cache"
116
+ headers["Connection"] = "keep-alive"
117
+ // Tell intermediate proxies + browsers not to buffer.
118
+ headers["X-Accel-Buffering"] = "no"
119
+ return .streaming(status: 200, headers: headers, stream: stream)
120
+
121
+ case .error(let status, let message):
122
+ return makeErrorResponse(status, message, cors: cors)
123
+ }
124
+ }
125
+
126
+ /// Build a JSON `{"error": "..."}` response with the given status.
127
+ public func makeErrorResponse(_ status: Int, _ message: String, cors: [String: String]) -> DVAIResponse {
128
+ let body = (try? JSONSerialization.data(withJSONObject: ["error": message])) ?? Data()
129
+ var headers = cors
130
+ headers["Content-Type"] = "application/json"
131
+ return .buffered(status: status, headers: headers, body: body)
132
+ }