threadwire 0.1.6 → 0.1.9

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.
@@ -0,0 +1,209 @@
1
+ // @ts-check
2
+
3
+ import {randomBytes} from "node:crypto"
4
+
5
+ const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u
6
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u
7
+ const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u
8
+ const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u
9
+ const TASK_PATTERN = /^[0-9a-f]{64}$/u
10
+ const SESSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u
11
+
12
+ /** @typedef {{provider: "kimi", runId: string, networkId: string, taskId: string, sessionId: string, modelAlias: string, model: string, expiresAt: number, active: boolean}} KimiGrant */
13
+ /** @typedef {{alias: string, model: string, protocol: "kimi"}} ApprovedKimiModel */
14
+ const ALLOWED_HEADERS = new Set([
15
+ "authorization", "content-length", "content-type", "accept", "accept-encoding",
16
+ "accept-language", "sec-fetch-mode", "user-agent",
17
+ "x-stainless-arch", "x-stainless-lang", "x-stainless-os",
18
+ "x-stainless-package-version", "x-stainless-runtime", "x-stainless-runtime-version",
19
+ "x-stainless-retry-count", "x-stainless-timeout", "x-stainless-async"
20
+ ])
21
+ const BODY_KEYS = new Set([
22
+ "model", "messages", "stream", "stream_options", "tools", "thinking",
23
+ "max_completion_tokens", "temperature", "top_p", "n", "presence_penalty",
24
+ "frequency_penalty", "stop", "prompt_cache_key", "response_format"
25
+ ])
26
+ const NUMERIC_KEYS = new Set([
27
+ "max_completion_tokens", "temperature", "top_p", "n", "presence_penalty", "frequency_penalty"
28
+ ])
29
+
30
+ export const KIMI_CHAT_COMPLETIONS_PATH = "/coding/v1/chat/completions"
31
+
32
+ /** @param {{now?: () => number, random?: (bytes: number) => Buffer}} [options] */
33
+ export function createKimiGrantStore(options = {}) {
34
+ const now = options.now ?? Date.now
35
+ const random = options.random ?? randomBytes
36
+ /** @type {Map<string, KimiGrant>} */
37
+ const grants = new Map()
38
+ return {
39
+ /** @param {{provider: "kimi", runId: string, networkId: string, taskId: string, sessionId: string, modelAlias: string, model: string, ttlMs: number}} grant */
40
+ issue(grant) {
41
+ if (grant.provider !== "kimi" || !ID_PATTERN.test(grant.runId) || !ID_PATTERN.test(grant.networkId)
42
+ || !TASK_PATTERN.test(grant.taskId) || !SESSION_PATTERN.test(grant.sessionId)
43
+ || !ALIAS_PATTERN.test(grant.modelAlias) || !MODEL_PATTERN.test(grant.model)
44
+ || !Number.isSafeInteger(grant.ttlMs) || grant.ttlMs < 1 || grant.ttlMs > 3_600_000) {
45
+ throw new Error("Invalid Kimi broker grant")
46
+ }
47
+ const token = random(32).toString("base64url")
48
+ if (!TOKEN_PATTERN.test(token) || grants.has(token)) throw new Error("Invalid Kimi broker grant")
49
+ grants.set(token, {...grant, expiresAt: now() + grant.ttlMs, active: false})
50
+ return token
51
+ },
52
+ /** @param {string} token */
53
+ activate(token) {
54
+ const grant = currentGrant(grants, token, now)
55
+ if (grant.active) throw new Error("Kimi broker grant already active")
56
+ grant.active = true
57
+ return {...grant}
58
+ },
59
+ /** @param {string} token @param {{provider: string, runId: string, networkId: string, taskId: string, sessionId: string, modelAlias: string}} context */
60
+ authorize(token, context) {
61
+ const grant = currentGrant(grants, token, now)
62
+ if (grant.provider !== context.provider || grant.runId !== context.runId
63
+ || grant.networkId !== context.networkId || grant.taskId !== context.taskId
64
+ || grant.sessionId !== context.sessionId || grant.modelAlias !== context.modelAlias) {
65
+ throw new Error("Kimi broker grant lineage denied")
66
+ }
67
+ if (!grant.active) throw new Error("Kimi broker grant pending")
68
+ return {...grant}
69
+ },
70
+ /** @param {string} token */
71
+ revoke(token) { grants.delete(token) },
72
+ clear() { grants.clear() }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @param {Map<string, KimiGrant>} grants
78
+ * @param {string} token
79
+ * @param {() => number} now
80
+ */
81
+ function currentGrant(grants, token, now) {
82
+ const grant = grants.get(token)
83
+ if (!grant) throw new Error("Kimi broker grant unknown token")
84
+ if (grant.expiresAt < now()) {
85
+ grants.delete(token)
86
+ throw new Error("Kimi broker grant expired")
87
+ }
88
+ return grant
89
+ }
90
+
91
+ /** @param {{method?: string, path?: string, headers?: Record<string, string | string[] | undefined>}} request */
92
+ export function validateKimiBrokerRequest(request) {
93
+ if (request.method !== "POST" || request.path !== "/v1/chat/completions") {
94
+ throw new Error("Kimi broker request denied")
95
+ }
96
+ for (const header of Object.keys(request.headers ?? {})) {
97
+ if (!ALLOWED_HEADERS.has(header.toLowerCase())) throw new Error(`Kimi broker request denied header: ${header.toLowerCase()}`)
98
+ }
99
+ return {method: "POST", path: "/v1/chat/completions", upstreamPath: KIMI_CHAT_COMPLETIONS_PATH}
100
+ }
101
+
102
+ /** @param {Buffer} body @param {string} allowedModel */
103
+ export function validateKimiChatBody(body, allowedModel) {
104
+ const text = body.toString("utf8")
105
+ if (topLevelKeyCount(text, "model") !== 1) throw denied()
106
+ let value
107
+ try { value = JSON.parse(text) } catch { throw denied() }
108
+ if (!record(value) || Object.keys(value).some((key) => !BODY_KEYS.has(key))) throw denied()
109
+ if (value.model !== allowedModel || !Array.isArray(value.messages) || value.stream !== true) throw denied()
110
+ if (value.messages.length === 0 || value.messages.length > 4096 || !value.messages.every(validMessage)) throw denied()
111
+ if (value.tools !== undefined && (!Array.isArray(value.tools) || value.tools.length > 512 || !value.tools.every(record))) throw denied()
112
+ if (value.stream_options !== undefined && (!record(value.stream_options)
113
+ || !exactKeys(value.stream_options, ["include_usage"]) || value.stream_options.include_usage !== true)) throw denied()
114
+ if (value.thinking !== undefined && !validThinking(value.thinking)) throw denied()
115
+ if (value.response_format !== undefined && !record(value.response_format)) throw denied()
116
+ if (value.prompt_cache_key !== undefined && (typeof value.prompt_cache_key !== "string" || value.prompt_cache_key.length > 512)) throw denied()
117
+ if (value.stop !== undefined && !validStop(value.stop)) throw denied()
118
+ for (const key of NUMERIC_KEYS) {
119
+ const item = value[key]
120
+ if (item !== undefined && (typeof item !== "number" || !Number.isFinite(item))) throw denied()
121
+ }
122
+ if (value.max_completion_tokens !== undefined && (typeof value.max_completion_tokens !== "number"
123
+ || !Number.isSafeInteger(value.max_completion_tokens)
124
+ || value.max_completion_tokens < 1 || value.max_completion_tokens > 1_000_000)) throw denied()
125
+ if (value.n !== undefined && value.n !== 1) throw denied()
126
+ return value
127
+ }
128
+
129
+ /** @param {unknown} value */
130
+ function validMessage(value) {
131
+ if (!record(value) || typeof value.role !== "string" || !["system", "user", "assistant", "tool"].includes(value.role)) return false
132
+ const allowed = new Set(["role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content", "reasoning_details", "reasoning", "tools"])
133
+ return Object.keys(value).every((key) => allowed.has(key))
134
+ }
135
+
136
+ /** @param {unknown} value */
137
+ function validThinking(value) {
138
+ if (!record(value) || Object.keys(value).some((key) => !["type", "effort", "keep"].includes(key))) return false
139
+ if (value.type !== undefined && value.type !== "enabled" && value.type !== "disabled") return false
140
+ if (value.effort !== undefined && (typeof value.effort !== "string" || !/^[A-Za-z0-9_-]{1,32}$/u.test(value.effort))) return false
141
+ return value.keep === undefined || value.keep === "all"
142
+ }
143
+
144
+ /** @param {unknown} value */
145
+ function validStop(value) {
146
+ return typeof value === "string" ? value.length <= 512
147
+ : Array.isArray(value) && value.length <= 16 && value.every((item) => typeof item === "string" && item.length <= 512)
148
+ }
149
+
150
+ /** @param {string | undefined} authorization */
151
+ export function kimiBearerToken(authorization) {
152
+ const match = /^Bearer ([A-Za-z0-9_-]{32,128})$/u.exec(authorization ?? "")
153
+ if (!match || !TOKEN_PATTERN.test(match[1] ?? "")) throw new Error("Kimi broker grant format denied")
154
+ return /** @type {string} */ (match[1])
155
+ }
156
+
157
+ /** @param {string | undefined} value */
158
+ export function parseApprovedKimiModels(value) {
159
+ let parsed
160
+ try { parsed = JSON.parse(value ?? "") } catch { throw new Error("THREADWIRE_ALLOWED_KIMI_MODELS must contain allowed Kimi models") }
161
+ if (!record(parsed) || Object.keys(parsed).length < 1 || Object.keys(parsed).length > 32) {
162
+ throw new Error("THREADWIRE_ALLOWED_KIMI_MODELS must contain allowed Kimi models")
163
+ }
164
+ /** @type {Map<string, ApprovedKimiModel>} */
165
+ const models = new Map()
166
+ for (const [alias, item] of Object.entries(parsed)) {
167
+ if (!ALIAS_PATTERN.test(alias) || !record(item) || !exactKeys(item, ["model", "protocol"])
168
+ || typeof item.model !== "string" || !MODEL_PATTERN.test(item.model) || item.protocol !== "kimi") {
169
+ throw new Error("THREADWIRE_ALLOWED_KIMI_MODELS must contain allowed Kimi models")
170
+ }
171
+ models.set(alias, {alias, model: item.model, protocol: "kimi"})
172
+ }
173
+ return models
174
+ }
175
+
176
+ /** @returns {Error} */
177
+ function denied() { return new Error("Kimi broker request denied") }
178
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
179
+ function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
180
+ /** @param {Record<string, unknown>} value @param {readonly string[]} keys */
181
+ function exactKeys(value, keys) {
182
+ const actual = Object.keys(value).sort()
183
+ return actual.length === keys.length && actual.every((key, index) => key === [...keys].sort()[index])
184
+ }
185
+
186
+ /** @param {string} text @param {string} wanted @returns {number} */
187
+ function topLevelKeyCount(text, wanted) {
188
+ let depth = 0
189
+ let count = 0
190
+ let index = 0
191
+ while (index < text.length) {
192
+ if (text[index] === "\"") {
193
+ let string = ""
194
+ index++
195
+ while (index < text.length && text[index] !== "\"") {
196
+ if (text[index] === "\\") { index += 2; continue }
197
+ string += text[index++]
198
+ }
199
+ if (depth === 1) {
200
+ let next = index + 1
201
+ while (/\s/u.test(text[next] ?? "")) next++
202
+ if (text[next] === ":" && string === wanted) count++
203
+ }
204
+ } else if (text[index] === "{") depth++
205
+ else if (text[index] === "}") depth--
206
+ index++
207
+ }
208
+ return count
209
+ }
@@ -0,0 +1,325 @@
1
+ // @ts-check
2
+
3
+ import {createServer} from "node:http"
4
+ import {once} from "node:events"
5
+ import {abortable} from "./absolute-deadline.js"
6
+ import {
7
+ createKimiGrantStore,
8
+ kimiBearerToken,
9
+ parseApprovedKimiModels,
10
+ validateKimiBrokerRequest,
11
+ validateKimiChatBody
12
+ } from "./kimi-model-broker-policy.js"
13
+ import {openKimiOAuthStore} from "./kimi-oauth-store.js"
14
+
15
+ const MAX_REQUEST_BYTES = 2_097_152
16
+ const MAX_RESPONSE_BYTES = 8_388_608
17
+ const UPSTREAM_URL = "https://api.kimi.com/coding/v1/chat/completions"
18
+
19
+ /** @typedef {{provider: "kimi", runId: string, networkId: string, taskId: string, sessionId: string, modelAlias: string, model: string, ttlMs: number}} KimiGrant */
20
+ /** @typedef {{alias: string, model: string, protocol: "kimi"}} ApprovedKimiModel */
21
+ /** @typedef {ReturnType<typeof createKimiGrantStore>} KimiGrantStore */
22
+ /** @typedef {Awaited<ReturnType<typeof openKimiOAuthStore>>} KimiOAuthStore */
23
+
24
+ /** @param {{environment?: NodeJS.ProcessEnv, fetchImplementation?: typeof fetch, oauthFetchImplementation?: typeof fetch}} [options] */
25
+ export async function startKimiModelBroker(options = {}) {
26
+ const environment = options.environment ?? process.env
27
+ const host = safeHost(environment.THREADWIRE_KIMI_MODEL_BROKER_HOST ?? "0.0.0.0")
28
+ const port = portValue(environment.THREADWIRE_KIMI_MODEL_BROKER_PORT, 8791)
29
+ const adminToken = authoritySetting(environment.THREADWIRE_KIMI_MODEL_BROKER_ADMIN_TOKEN, "THREADWIRE_KIMI_MODEL_BROKER_ADMIN_TOKEN")
30
+ const approvedModels = parseApprovedKimiModels(environment.THREADWIRE_ALLOWED_KIMI_MODELS)
31
+ const oauth = await openKimiOAuthStore({
32
+ file: safeSetting(environment.THREADWIRE_KIMI_OAUTH_FILE, "THREADWIRE_KIMI_OAUTH_FILE"),
33
+ fetchImplementation: options.oauthFetchImplementation ?? fetch
34
+ })
35
+ const fetchImplementation = options.fetchImplementation ?? fetch
36
+ const grants = createKimiGrantStore()
37
+ /** @type {Map<string, import("node:http").Server>} */
38
+ const listeners = new Map()
39
+ /** @type {Map<string, {controller: AbortController, timer: NodeJS.Timeout, sockets: Set<import("node:net").Socket>}>} */
40
+ const runtimes = new Map()
41
+ /** @type {Set<import("node:net").Socket>} */
42
+ const controlSockets = new Set()
43
+ let stopping = false
44
+
45
+ /** @param {string} token */
46
+ const revokeGrant = async (token) => {
47
+ grants.revoke(token)
48
+ const runtime = runtimes.get(token)
49
+ runtimes.delete(token)
50
+ if (runtime !== undefined) {
51
+ clearTimeout(runtime.timer)
52
+ runtime.controller.abort(new Error("Kimi broker grant revoked"))
53
+ for (const socket of runtime.sockets) socket.destroy()
54
+ runtime.sockets.clear()
55
+ }
56
+ await closeListener(listeners, token)
57
+ }
58
+
59
+ const server = createServer(async (request, response) => {
60
+ try {
61
+ if (stopping) throw new Error("Kimi model broker is shutting down")
62
+ if (request.method === "GET" && request.url === "/healthz") {
63
+ const health = await oauth.health()
64
+ sendJson(response, health.ready ? 200 : 503, health.ready
65
+ ? {ok: true, models: approvedModels.size}
66
+ : {ok: false, needsLogin: true})
67
+ return
68
+ }
69
+ requireAdmin(request.headers.authorization, adminToken)
70
+ if (request.method === "POST" && request.url === "/admin/grants") {
71
+ const health = await oauth.health()
72
+ if (!health.ready) throw new Error("Kimi OAuth login required")
73
+ const body = await readJson(request)
74
+ const selection = parseGrant(body, approvedModels)
75
+ const token = grants.issue(selection.grant)
76
+ const controller = new AbortController()
77
+ /** @type {Set<import("node:net").Socket>} */
78
+ const sockets = new Set()
79
+ const listener = createServer((workerRequest, workerResponse) => {
80
+ proxyWorkerRequest(workerRequest, workerResponse, {
81
+ token, grant: selection.grant, grants, oauth, fetchImplementation, signal: controller.signal
82
+ }).catch((error) => {
83
+ if (workerResponse.destroyed) return
84
+ const status = clientError(error) ? 403 : 502
85
+ sendJson(workerResponse, status, {error: status === 403 ? "request denied" : "Kimi model broker unavailable"})
86
+ })
87
+ })
88
+ listener.on("connection", (socket) => {
89
+ if (controller.signal.aborted) return socket.destroy()
90
+ sockets.add(socket)
91
+ socket.once("close", () => sockets.delete(socket))
92
+ })
93
+ try { await listen(listener, 0, selection.brokerAddress) } catch (error) {
94
+ grants.revoke(token)
95
+ throw error
96
+ }
97
+ listeners.set(token, listener)
98
+ const timer = setTimeout(() => { void revokeGrant(token) }, selection.grant.ttlMs)
99
+ timer.unref()
100
+ runtimes.set(token, {controller, timer, sockets})
101
+ const address = listener.address()
102
+ if (address === null || typeof address === "string") throw new Error("Kimi broker listener unavailable")
103
+ sendJson(response, 201, {token, port: address.port})
104
+ return
105
+ }
106
+ if (request.method === "POST" && request.url?.startsWith("/admin/grants/") && request.url.endsWith("/activate")) {
107
+ const token = decodeURIComponent(request.url.slice("/admin/grants/".length, -"/activate".length))
108
+ grants.activate(token)
109
+ sendJson(response, 200, {ok: true})
110
+ return
111
+ }
112
+ if (request.method === "DELETE" && request.url?.startsWith("/admin/grants/")) {
113
+ const token = decodeURIComponent(request.url.slice("/admin/grants/".length))
114
+ await revokeGrant(token)
115
+ response.writeHead(204)
116
+ response.end()
117
+ return
118
+ }
119
+ sendJson(response, 403, {error: "request denied"})
120
+ } catch (error) {
121
+ const status = clientError(error) ? 403 : 503
122
+ sendJson(response, status, {error: status === 403 ? "request denied" : "Kimi model broker unavailable"})
123
+ }
124
+ })
125
+ server.on("connection", (socket) => {
126
+ controlSockets.add(socket)
127
+ socket.once("close", () => controlSockets.delete(socket))
128
+ })
129
+ await listen(server, port, host)
130
+ return {
131
+ server,
132
+ activeListeners: () => listeners.size,
133
+ close: async () => {
134
+ if (stopping) return
135
+ stopping = true
136
+ for (const socket of controlSockets) socket.destroy()
137
+ await Promise.all([...listeners.keys()].map(revokeGrant))
138
+ await boundedClose(server)
139
+ },
140
+ config: {host, port, models: [...approvedModels.keys()]}
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @param {import("node:http").IncomingMessage} request
146
+ * @param {import("node:http").ServerResponse} response
147
+ * @param {{token: string, grant: KimiGrant, grants: KimiGrantStore, oauth: KimiOAuthStore, fetchImplementation: typeof fetch, signal: AbortSignal}} options
148
+ */
149
+ async function proxyWorkerRequest(request, response, options) {
150
+ throwIfAborted(options.signal)
151
+ const policyHeaders = {...request.headers}
152
+ delete policyHeaders.host
153
+ delete policyHeaders.connection
154
+ delete policyHeaders["transfer-encoding"]
155
+ validateKimiBrokerRequest({method: request.method ?? "", path: request.url ?? "", headers: policyHeaders})
156
+ const token = kimiBearerToken(request.headers.authorization)
157
+ if (token !== options.token) throw new Error("Kimi broker request denied")
158
+ const context = {
159
+ provider: "kimi", runId: options.grant.runId,
160
+ networkId: options.grant.networkId, taskId: options.grant.taskId,
161
+ sessionId: options.grant.sessionId, modelAlias: options.grant.modelAlias
162
+ }
163
+ const grant = options.grants.authorize(token, context)
164
+ const body = await readBody(request, options.signal)
165
+ options.grants.authorize(token, context)
166
+ validateKimiChatBody(body, grant.model)
167
+ const accessToken = await options.oauth.getAccessToken({signal: options.signal})
168
+ throwIfAborted(options.signal)
169
+ const upstream = await abortable(options.fetchImplementation(UPSTREAM_URL, {
170
+ method: "POST",
171
+ redirect: "error",
172
+ headers: {
173
+ authorization: `Bearer ${accessToken}`,
174
+ "content-type": "application/json",
175
+ accept: "text/event-stream",
176
+ "user-agent": "threadwire-kimi-model-broker/1"
177
+ },
178
+ body,
179
+ signal: options.signal
180
+ }), options.signal)
181
+ throwIfAborted(options.signal)
182
+ const contentType = safeContentType(upstream.headers.get("content-type"))
183
+ response.writeHead(upstream.status, {"content-type": contentType, "cache-control": "no-store"})
184
+ await streamResponse(upstream, response, options.signal)
185
+ }
186
+
187
+ /** @param {Response} upstream @param {import("node:http").ServerResponse} response @param {AbortSignal} signal */
188
+ async function streamResponse(upstream, response, signal) {
189
+ if (upstream.body === null) { response.end(); return }
190
+ const reader = upstream.body.getReader()
191
+ let bytes = 0
192
+ try {
193
+ while (true) {
194
+ throwIfAborted(signal)
195
+ const {done, value} = await reader.read()
196
+ if (done) break
197
+ bytes += value.byteLength
198
+ if (bytes > MAX_RESPONSE_BYTES) throw new Error("Kimi broker response exceeded capacity")
199
+ if (!response.write(Buffer.from(value))) await abortable(once(response, "drain"), signal)
200
+ }
201
+ response.end()
202
+ } catch (error) {
203
+ await reader.cancel().catch(() => {})
204
+ response.destroy(error instanceof Error ? error : undefined)
205
+ throw error
206
+ } finally { reader.releaseLock() }
207
+ }
208
+
209
+ /** @param {unknown} value @param {Map<string, ApprovedKimiModel>} approvedModels @returns {{brokerAddress: string, grant: KimiGrant}} */
210
+ function parseGrant(value, approvedModels) {
211
+ if (!record(value) || !exactKeys(value, ["brokerAddress", "modelAlias", "networkId", "provider", "runId", "sessionId", "taskId", "ttlMs"])
212
+ || value.provider !== "kimi" || typeof value.runId !== "string" || typeof value.networkId !== "string"
213
+ || typeof value.taskId !== "string" || typeof value.sessionId !== "string"
214
+ || typeof value.modelAlias !== "string" || typeof value.brokerAddress !== "string" || typeof value.ttlMs !== "number") {
215
+ throw new Error("Invalid Kimi broker grant")
216
+ }
217
+ if (!/^(?:\d{1,3}\.){3}\d{1,3}$/u.test(value.brokerAddress)) throw new Error("Invalid Kimi broker grant")
218
+ const model = approvedModels.get(value.modelAlias)
219
+ if (model === undefined) throw new Error("Kimi model is not allowed")
220
+ return {
221
+ brokerAddress: value.brokerAddress,
222
+ grant: {
223
+ provider: "kimi", runId: value.runId, networkId: value.networkId,
224
+ taskId: value.taskId, sessionId: value.sessionId,
225
+ modelAlias: model.alias, model: model.model, ttlMs: value.ttlMs
226
+ }
227
+ }
228
+ }
229
+
230
+ /** @param {import("node:http").IncomingMessage} request @returns {Promise<unknown>} */
231
+ async function readJson(request) {
232
+ const body = await readBody(request)
233
+ try { return JSON.parse(body.toString("utf8")) } catch { throw new Error("Invalid Kimi broker request") }
234
+ }
235
+
236
+ /** @param {import("node:http").IncomingMessage} request @param {AbortSignal | undefined} [signal] @returns {Promise<Buffer>} */
237
+ async function readBody(request, signal) {
238
+ /** @type {Buffer[]} */
239
+ const chunks = []
240
+ let size = 0
241
+ for await (const chunk of request) {
242
+ throwIfAborted(signal)
243
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
244
+ size += buffer.length
245
+ if (size > MAX_REQUEST_BYTES) throw new Error("Kimi broker request exceeded capacity")
246
+ chunks.push(buffer)
247
+ }
248
+ throwIfAborted(signal)
249
+ return Buffer.concat(chunks)
250
+ }
251
+
252
+ /** @param {AbortSignal | undefined} signal */
253
+ function throwIfAborted(signal) {
254
+ if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("Kimi broker request aborted")
255
+ }
256
+ /** @param {string | undefined} value @param {string} expected */
257
+ function requireAdmin(value, expected) {
258
+ if (value !== `Bearer ${expected}`) throw new Error("Kimi broker request denied")
259
+ }
260
+ /** @param {string | undefined} value @param {string} name @returns {string} */
261
+ function safeSetting(value, name) {
262
+ if (typeof value !== "string" || value.length === 0 || /[\0\r\n]/u.test(value)) throw new Error(`${name} is required`)
263
+ return value
264
+ }
265
+ /** @param {string | undefined} value @param {string} name @returns {string} */
266
+ function authoritySetting(value, name) {
267
+ const authority = safeSetting(value, name)
268
+ if (authority.length < 32 || authority.length > 512) throw new Error(`${name} must be at least 32 characters`)
269
+ return authority
270
+ }
271
+ /** @param {string} value @returns {string} */
272
+ function safeHost(value) {
273
+ if (value !== "0.0.0.0" && value !== "127.0.0.1" && value !== "::") throw new Error("Invalid Kimi model broker host")
274
+ return value
275
+ }
276
+ /** @param {string | undefined} value @param {number} fallback @returns {number} */
277
+ function portValue(value, fallback) {
278
+ if (value === undefined) return fallback
279
+ const number = Number(value)
280
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(number) || number < 1 || number > 65535) throw new Error("Invalid Kimi model broker port")
281
+ return number
282
+ }
283
+ /** @param {string | null} value @returns {string} */
284
+ function safeContentType(value) {
285
+ return value && /^[\w.+-]+\/[\w.+-]+(?:;\s*charset=[\w-]+)?$/u.test(value) ? value : "application/octet-stream"
286
+ }
287
+ /** @param {import("node:http").ServerResponse} response @param {number} status @param {unknown} value */
288
+ function sendJson(response, status, value) {
289
+ if (response.headersSent || response.destroyed) return
290
+ response.writeHead(status, {"content-type": "application/json", "cache-control": "no-store"})
291
+ response.end(JSON.stringify(value))
292
+ }
293
+ /** @param {unknown} error @returns {boolean} */
294
+ function clientError(error) {
295
+ return error instanceof Error && /(denied|invalid|unknown token|expired|pending|already active|not allowed)/iu.test(error.message)
296
+ }
297
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
298
+ function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
299
+ /** @param {Record<string, unknown>} value @param {readonly string[]} expected @returns {boolean} */
300
+ function exactKeys(value, expected) {
301
+ const actual = Object.keys(value).sort()
302
+ const wanted = [...expected].sort()
303
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index])
304
+ }
305
+ /** @param {import("node:http").Server} server @param {number} port @param {string} host @returns {Promise<void>} */
306
+ function listen(server, port, host) {
307
+ return new Promise((resolve, reject) => {
308
+ server.once("error", reject)
309
+ server.listen(port, host, () => { server.off("error", reject); resolve(undefined) })
310
+ })
311
+ }
312
+ /** @param {import("node:http").Server} server @returns {Promise<void>} */
313
+ function boundedClose(server) {
314
+ return Promise.race([
315
+ new Promise((resolve) => server.close(() => resolve(undefined))),
316
+ new Promise((resolve) => { const timer = setTimeout(resolve, 1000); timer.unref() })
317
+ ])
318
+ }
319
+ /** @param {Map<string, import("node:http").Server>} listeners @param {string} token */
320
+ async function closeListener(listeners, token) {
321
+ const listener = listeners.get(token)
322
+ listeners.delete(token)
323
+ if (listener === undefined) return
324
+ await boundedClose(listener)
325
+ }