aisubs 0.1.0 → 0.3.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/dist/http.js CHANGED
@@ -1,39 +1,80 @@
1
- import { createServer } from "node:http";
2
- import { pipeline } from "node:stream/promises";
1
+ import cors from "@fastify/cors";
2
+ import websocket from "@fastify/websocket";
3
+ import Fastify from "fastify";
4
+ import { timingSafeEqual } from "node:crypto";
3
5
  import { Readable } from "node:stream";
4
- import { errorMessage, isRecord, stringValue, urlHost } from "./utils.js";
5
- const MAX_BODY_BYTES = 1024 * 1024;
6
+ import { proxyCompatible } from "./compatibility.js";
7
+ import { registerRealtimeProxy } from "./realtime.js";
8
+ import { errorMessage, isRecord, numberValue, stringValue, urlHost } from "./utils.js";
6
9
  const MAX_PROXY_BODY_BYTES = 10 * 1024 * 1024;
7
- function proxyBodyLimit(value = MAX_PROXY_BODY_BYTES) {
10
+ function bodyLimit(value = MAX_PROXY_BODY_BYTES) {
8
11
  if (!Number.isSafeInteger(value) || value <= 0) {
9
12
  throw new Error("maxProxyBodyBytes must be a positive safe integer");
10
13
  }
11
14
  return value;
12
15
  }
13
- async function readBody(request, maxBytes) {
14
- const contentLength = Number(request.headers["content-length"]);
15
- if (Number.isFinite(contentLength) && contentLength > maxBytes) {
16
- throw new Error("Request body is too large");
17
- }
18
- const chunks = [];
19
- let size = 0;
20
- for await (const chunk of request) {
21
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
22
- size += buffer.length;
23
- if (size > maxBytes)
24
- throw new Error("Request body is too large");
25
- chunks.push(buffer);
26
- }
27
- return Buffer.concat(chunks);
16
+ function sameSecret(actual, expected) {
17
+ if (!actual)
18
+ return false;
19
+ const left = Buffer.from(actual);
20
+ const right = Buffer.from(expected);
21
+ return left.length === right.length && timingSafeEqual(left, right);
22
+ }
23
+ function requestApiKeys(request) {
24
+ const authorization = request.headers.authorization;
25
+ const bearer = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
26
+ const header = (name) => {
27
+ const value = request.headers[name];
28
+ return Array.isArray(value) ? value[0] : value;
29
+ };
30
+ const queryKey = new URL(request.url, "http://aisubs.local").searchParams.get("key") ?? undefined;
31
+ return [bearer, header("x-api-key"), header("x-goog-api-key"), queryKey].filter((value) => value != null);
32
+ }
33
+ export function routeSegments(pathname) {
34
+ return pathname.split("/").filter(Boolean).map(decodeURIComponent);
35
+ }
36
+ function bodyBuffer(request) {
37
+ if (request.body == null)
38
+ return Buffer.alloc(0);
39
+ if (Buffer.isBuffer(request.body))
40
+ return request.body;
41
+ if (typeof request.body === "string")
42
+ return Buffer.from(request.body);
43
+ return Buffer.from(JSON.stringify(request.body));
28
44
  }
29
- export async function readJsonBody(request) {
30
- const body = await readBody(request, MAX_BODY_BYTES);
31
- if (body.length === 0)
45
+ function jsonBody(request) {
46
+ const body = bodyBuffer(request);
47
+ if (!body.length)
32
48
  return {};
33
49
  return JSON.parse(body.toString("utf8"));
34
50
  }
35
- function upstreamHeaders(upstream) {
36
- const headers = { "cache-control": "no-store" };
51
+ function requestHeaders(request) {
52
+ const headers = new Headers();
53
+ const privateHeaders = new Set([
54
+ "authorization",
55
+ "connection",
56
+ "content-length",
57
+ "cookie",
58
+ "host",
59
+ "origin",
60
+ "proxy-authenticate",
61
+ "proxy-authorization",
62
+ "referer",
63
+ "te",
64
+ "trailer",
65
+ "upgrade",
66
+ "x-api-key",
67
+ "x-goog-api-key",
68
+ ]);
69
+ for (const [name, value] of Object.entries(request.headers)) {
70
+ if (value != null && !privateHeaders.has(name)) {
71
+ headers.set(name, Array.isArray(value) ? value.join(", ") : String(value));
72
+ }
73
+ }
74
+ return headers;
75
+ }
76
+ function responseHeaders(upstream) {
77
+ const headers = new Headers({ "cache-control": "no-store" });
37
78
  upstream.headers.forEach((value, name) => {
38
79
  if (![
39
80
  "cache-control",
@@ -44,127 +85,143 @@ function upstreamHeaders(upstream) {
44
85
  "set-cookie",
45
86
  "transfer-encoding",
46
87
  ].includes(name)) {
47
- headers[name] = value;
88
+ headers.set(name, value);
48
89
  }
49
90
  });
50
91
  return headers;
51
92
  }
52
- async function sendUpstream(response, upstream) {
53
- response.writeHead(upstream.status, upstreamHeaders(upstream));
54
- if (upstream.body)
55
- await pipeline(Readable.fromWeb(upstream.body), response);
56
- else
57
- response.end();
58
- }
59
- export async function readProxyBody(request, maxBytes = MAX_PROXY_BODY_BYTES) {
60
- return readBody(request, proxyBodyLimit(maxBytes));
93
+ export async function sendWebResponse(reply, upstream) {
94
+ reply.code(upstream.status);
95
+ responseHeaders(upstream).forEach((value, name) => reply.header(name, value));
96
+ if (!upstream.body) {
97
+ await reply.send();
98
+ return;
99
+ }
100
+ await reply.send(Readable.fromWeb(upstream.body));
61
101
  }
62
- export function sendJson(response, status, body, headers = {}) {
63
- response.writeHead(status, {
64
- "content-type": "application/json; charset=utf-8",
65
- "cache-control": "no-store",
66
- ...headers,
102
+ function jsonResponse(body, status = 200, headers = {}) {
103
+ return Response.json(body, {
104
+ status,
105
+ headers: { "cache-control": "no-store", ...Object.fromEntries(new Headers(headers)) },
67
106
  });
68
- response.end(JSON.stringify(body));
69
107
  }
70
- export function routeSegments(pathname) {
71
- return pathname.split("/").filter(Boolean).map(decodeURIComponent);
108
+ function accountPath(parts) {
109
+ if (parts[0] !== "aisubs" || !parts[1] || !parts[2])
110
+ return null;
111
+ const upstream = parts.slice(3);
112
+ const versioned = upstream[0] === "v1";
113
+ if (versioned)
114
+ upstream.shift();
115
+ return {
116
+ provider: parts[1],
117
+ account: parts[2],
118
+ path: upstream.map(encodeURIComponent).join("/"),
119
+ versioned,
120
+ };
72
121
  }
73
- export async function handleSubscriptionAuthApi(auth, request, response, url, maxProxyBodyBytes = MAX_PROXY_BODY_BYTES) {
122
+ function openAiModel(provider, model) {
123
+ return {
124
+ id: model.id,
125
+ object: "model",
126
+ owned_by: provider,
127
+ capabilities: {
128
+ endpoints: model.endpoints ?? [],
129
+ input_modalities: model.inputModalities ?? ["text"],
130
+ reasoning_efforts: model.reasoningEfforts ?? [],
131
+ tools: model.supportsToolCall ?? false,
132
+ },
133
+ };
134
+ }
135
+ export async function handleSubscriptionAuthApi(auth, request, signal) {
136
+ const url = new URL(request.url, "http://aisubs.local");
74
137
  const parts = routeSegments(url.pathname);
75
- if (parts[0] === "aisubs" && parts[1] && parts[2] && request.method !== "OPTIONS") {
76
- const headers = new Headers();
77
- const privateHeaders = new Set([
78
- "authorization",
79
- "connection",
80
- "content-length",
81
- "cookie",
82
- "host",
83
- "origin",
84
- "proxy-authenticate",
85
- "proxy-authorization",
86
- "referer",
87
- "te",
88
- "trailer",
89
- "upgrade",
90
- "x-api-key",
91
- ]);
92
- for (const [name, value] of Object.entries(request.headers)) {
93
- if (value != null && !privateHeaders.has(name)) {
94
- headers.set(name, Array.isArray(value) ? value.join(", ") : value);
95
- }
138
+ const account = accountPath(parts);
139
+ if (account && request.method !== "OPTIONS") {
140
+ if (account.versioned && request.method === "GET" && account.path === "models") {
141
+ const catalog = await auth.getModels(account.provider, account.account);
142
+ return jsonResponse({
143
+ object: "list",
144
+ data: (catalog?.models ?? []).map((model) => openAiModel(account.provider, model)),
145
+ });
146
+ }
147
+ if (account.versioned && request.method === "GET" && account.path.startsWith("models/")) {
148
+ const id = decodeURIComponent(account.path.slice("models/".length));
149
+ const catalog = await auth.getModels(account.provider, account.account);
150
+ const model = catalog?.models.find((candidate) => candidate.id === id);
151
+ return model
152
+ ? jsonResponse(openAiModel(account.provider, model))
153
+ : jsonResponse({
154
+ error: {
155
+ message: `Model not found: ${id}`,
156
+ type: "invalid_request_error",
157
+ code: "model_not_found",
158
+ },
159
+ }, 404);
96
160
  }
97
- const body = await readProxyBody(request, maxProxyBodyBytes);
98
- const path = parts.slice(3).map(encodeURIComponent).join("/") + url.search;
99
- const upstream = await auth.proxy(parts[1], parts[2], path, {
161
+ const headers = requestHeaders(request);
162
+ const body = bodyBuffer(request);
163
+ url.searchParams.delete("key");
164
+ const path = `${account.path}${url.search}`;
165
+ if (account.versioned && request.method === "POST") {
166
+ const compatible = await proxyCompatible(auth, account.provider, account.account, path, body, headers, signal);
167
+ if (compatible)
168
+ return compatible;
169
+ }
170
+ return auth.proxy(account.provider, account.account, path, {
100
171
  method: request.method,
101
172
  headers,
102
173
  body: body.length ? body : undefined,
174
+ signal,
103
175
  });
104
- await sendUpstream(response, upstream);
105
- return true;
106
176
  }
107
177
  if (request.method === "GET" && parts.join("/") === "v1/providers") {
108
- sendJson(response, 200, { providers: auth.listProviders() });
109
- return true;
178
+ return jsonResponse({ providers: auth.listProviders() });
110
179
  }
111
180
  if (request.method === "GET" && parts.join("/") === "v1/auth") {
112
- sendJson(response, 200, { sessions: await auth.statuses() });
113
- return true;
181
+ return jsonResponse({ sessions: await auth.statuses() });
114
182
  }
115
183
  if (parts[0] === "v1" && parts[1] === "auth" && parts[2]) {
116
184
  const provider = parts[2];
117
185
  if (request.method === "GET" && parts.length === 3) {
118
- sendJson(response, 200, await auth.status(provider, {
186
+ return jsonResponse(await auth.status(provider, {
119
187
  account: url.searchParams.get("account") ?? undefined,
120
188
  validate: url.searchParams.get("validate") === "true",
121
189
  }));
122
- return true;
123
190
  }
124
191
  if (request.method === "GET" && parts[3] === "accounts") {
125
- sendJson(response, 200, { accounts: await auth.listAccounts(provider) });
126
- return true;
192
+ return jsonResponse({ accounts: await auth.listAccounts(provider) });
127
193
  }
128
194
  if (request.method === "GET" && parts[3] === "details") {
129
- sendJson(response, 200, await auth.credentialSummary(provider, url.searchParams.get("account") ?? undefined));
130
- return true;
195
+ return jsonResponse(await auth.credentialSummary(provider, url.searchParams.get("account") ?? undefined));
131
196
  }
132
197
  if (request.method === "POST" && parts[3] === "login") {
133
- const body = await readJsonBody(request);
198
+ const body = jsonBody(request);
134
199
  const attempt = await auth.signIn(provider, isRecord(body) ? body : undefined);
135
- sendJson(response, 202, {
200
+ return jsonResponse({
136
201
  id: attempt.id,
137
202
  provider: attempt.provider,
138
203
  accountKey: attempt.accountKey,
139
204
  state: attempt.state,
140
205
  prompt: attempt.prompt,
141
- });
142
- return true;
206
+ }, 202);
143
207
  }
144
208
  if (request.method === "DELETE" && parts.length === 3) {
145
209
  const accountKey = url.searchParams.get("account") ?? "default";
146
210
  await auth.signOut(provider, accountKey);
147
- sendJson(response, 200, { provider, accountKey, authenticated: false });
148
- return true;
211
+ return jsonResponse({ provider, accountKey, authenticated: false });
149
212
  }
150
213
  }
151
214
  if (request.method === "GET" && parts[0] === "v1" && parts[1] === "logins" && parts[2]) {
152
215
  const attempt = auth.getLoginAttempt(parts[2]);
153
- if (attempt)
154
- sendJson(response, 200, attempt);
155
- else
156
- sendJson(response, 404, { error: "Login not found" });
157
- return true;
216
+ return attempt ? jsonResponse(attempt) : jsonResponse({ error: "Login not found" }, 404);
158
217
  }
159
218
  if (request.method === "DELETE" && parts[0] === "v1" && parts[1] === "logins" && parts[2]) {
160
- if (auth.cancelLoginAttempt(parts[2]))
161
- sendJson(response, 200, { cancelled: true });
162
- else
163
- sendJson(response, 404, { error: "Pending login not found" });
164
- return true;
219
+ return auth.cancelLoginAttempt(parts[2])
220
+ ? jsonResponse({ cancelled: true })
221
+ : jsonResponse({ error: "Pending login not found" }, 404);
165
222
  }
166
223
  if (request.method === "POST" && parts[0] === "v1" && parts[1] === "fetch" && parts[2]) {
167
- const body = await readJsonBody(request);
224
+ const body = jsonBody(request);
168
225
  if (!isRecord(body))
169
226
  throw new Error("Fetch body requires an absolute url");
170
227
  const targetUrl = stringValue(body.url);
@@ -185,79 +242,130 @@ export async function handleSubscriptionAuthApi(auth, request, response, url, ma
185
242
  if (!headers.has("content-type"))
186
243
  headers.set("content-type", "application/json");
187
244
  }
188
- const upstream = await auth.fetch(parts[2], targetUrl, {
245
+ return auth.fetch(parts[2], targetUrl, {
189
246
  method: stringValue(body.method) ?? (requestBody ? "POST" : "GET"),
190
247
  headers,
191
248
  body: requestBody,
249
+ signal,
192
250
  }, stringValue(body.account));
193
- await sendUpstream(response, upstream);
194
- return true;
195
251
  }
196
252
  if (request.method === "GET" && parts[0] === "v1" && parts[1] === "usage" && parts[2]) {
197
- const usage = await auth.getUsage(parts[2], url.searchParams.get("account") ?? undefined);
198
- if (usage)
199
- sendJson(response, 200, usage);
200
- else
201
- sendJson(response, 404, { error: "Usage is not supported by this provider" });
202
- return true;
253
+ const usage = await auth.getUsage(parts[2], url.searchParams.get("account") ?? undefined, signal);
254
+ return usage
255
+ ? jsonResponse(usage)
256
+ : jsonResponse({ error: "Usage is not supported by this provider" }, 404);
203
257
  }
204
258
  if (request.method === "GET" && parts[0] === "v1" && parts[1] === "models" && parts[2]) {
205
- const models = await auth.getModels(parts[2], url.searchParams.get("account") ?? undefined);
206
- if (models)
207
- sendJson(response, 200, models);
208
- else
209
- sendJson(response, 404, { error: "Models are not supported by this provider" });
210
- return true;
259
+ const models = await auth.getModels(parts[2], url.searchParams.get("account") ?? undefined, signal);
260
+ return models
261
+ ? jsonResponse(models)
262
+ : jsonResponse({ error: "Models are not supported by this provider" }, 404);
211
263
  }
212
- return false;
264
+ return null;
213
265
  }
214
- export async function createSubscriptionAuthServer(options) {
266
+ /** Abort upstream work only when the client disconnects before the response finishes. */
267
+ export function clientAbortSignal(request, reply) {
268
+ const controller = new AbortController();
269
+ const abort = () => {
270
+ if (!reply.raw.writableFinished && !controller.signal.aborted)
271
+ controller.abort();
272
+ };
273
+ const cleanup = () => {
274
+ request.raw.off("aborted", abort);
275
+ reply.raw.off("close", abort);
276
+ };
277
+ request.raw.once("aborted", abort);
278
+ reply.raw.once("close", abort);
279
+ reply.raw.once("finish", cleanup);
280
+ if (request.raw.aborted || (reply.raw.destroyed && !reply.raw.writableFinished))
281
+ abort();
282
+ return controller.signal;
283
+ }
284
+ export function createApiApp(options) {
215
285
  if (!options.apiKey)
216
286
  throw new Error("A non-empty API key is required");
217
- const maxProxyBodyBytes = proxyBodyLimit(options.maxProxyBodyBytes);
218
- const host = options.host ?? "127.0.0.1";
219
- if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
220
- throw new Error("The built-in auth server may only bind to localhost");
221
- }
222
- const origin = `http://${urlHost(host)}`;
223
- const server = createServer(async (request, response) => {
224
- try {
225
- const url = new URL(request.url ?? "/", origin);
226
- if (url.pathname === "/health")
227
- return sendJson(response, 200, { ok: true });
228
- const headerKey = Array.isArray(request.headers["x-api-key"])
229
- ? request.headers["x-api-key"][0]
230
- : request.headers["x-api-key"];
231
- if (request.headers.authorization !== `Bearer ${options.apiKey}` &&
232
- headerKey !== options.apiKey) {
233
- return sendJson(response, 401, { error: "Unauthorized" });
234
- }
235
- if (!(await handleSubscriptionAuthApi(options.auth, request, response, url, maxProxyBodyBytes))) {
236
- sendJson(response, 404, { error: "Not found" });
237
- }
287
+ const app = Fastify({
288
+ bodyLimit: bodyLimit(options.maxProxyBodyBytes),
289
+ forceCloseConnections: true,
290
+ });
291
+ app.removeAllContentTypeParsers();
292
+ app.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => done(null, body));
293
+ void app.register(cors, {
294
+ origin: true,
295
+ credentials: false,
296
+ methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
297
+ allowedHeaders: [
298
+ "authorization",
299
+ "content-type",
300
+ "x-api-key",
301
+ "x-goog-api-key",
302
+ "anthropic-version",
303
+ "anthropic-beta",
304
+ "openai-beta",
305
+ ],
306
+ exposedHeaders: ["x-request-id", "retry-after"],
307
+ });
308
+ void app.register(websocket);
309
+ app.get("/health", async () => ({ ok: true }));
310
+ app.addHook("onRequest", async (request, reply) => {
311
+ if (request.url === "/health")
312
+ return;
313
+ if (!requestApiKeys(request).some((value) => sameSecret(value, options.apiKey))) {
314
+ await reply.code(401).send({
315
+ error: { message: "Unauthorized", type: "authentication_error", code: "invalid_api_key" },
316
+ });
238
317
  }
239
- catch (error) {
240
- if (response.headersSent)
241
- response.destroy();
318
+ });
319
+ void app.register(async (scope) => {
320
+ registerRealtimeProxy(scope, options.auth, (request) => requestApiKeys(request).some((value) => sameSecret(value, options.apiKey)));
321
+ });
322
+ app.route({
323
+ method: ["GET", "POST", "PUT", "PATCH", "DELETE"],
324
+ url: "/*",
325
+ async handler(request, reply) {
326
+ const response = await handleSubscriptionAuthApi(options.auth, request, clientAbortSignal(request, reply));
327
+ if (response)
328
+ await sendWebResponse(reply, response);
242
329
  else
243
- sendJson(response, 400, { error: errorMessage(error) });
244
- }
330
+ await reply.code(404).send({
331
+ error: { message: "Not found", type: "invalid_request_error", code: "not_found" },
332
+ });
333
+ },
245
334
  });
246
- await new Promise((resolve, reject) => {
247
- server.once("error", reject);
248
- server.listen(options.port ?? 0, host, () => {
249
- server.off("error", reject);
250
- resolve();
251
- });
335
+ app.setErrorHandler(async (error, request, reply) => {
336
+ const statusCode = isRecord(error) ? numberValue(error.statusCode) : undefined;
337
+ const status = statusCode && statusCode >= 400 ? statusCode : 400;
338
+ const code = isRecord(error) ? stringValue(error.code) : undefined;
339
+ const openAi = request.url.startsWith("/aisubs/");
340
+ await reply.code(status).send(openAi
341
+ ? {
342
+ error: {
343
+ message: errorMessage(error),
344
+ type: "invalid_request_error",
345
+ code: code ?? "invalid_request_error",
346
+ },
347
+ }
348
+ : { error: errorMessage(error) });
252
349
  });
253
- const address = server.address();
350
+ return app;
351
+ }
352
+ export async function createSubscriptionAuthServer(options) {
353
+ const host = options.host ?? "127.0.0.1";
354
+ if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
355
+ throw new Error("The built-in auth server may only bind to localhost");
356
+ }
357
+ const app = createApiApp(options);
358
+ await app.listen({ port: options.port ?? 0, host });
359
+ const address = app.server.address();
254
360
  if (!address || typeof address === "string") {
361
+ await app.close();
255
362
  throw new Error("Unable to determine auth server port");
256
363
  }
257
364
  return {
258
- server,
365
+ server: app.server,
366
+ app,
259
367
  apiKey: options.apiKey,
260
- url: `${origin}:${address.port}`,
261
- close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))),
368
+ url: `http://${urlHost(host)}:${address.port}`,
369
+ close: () => app.close(),
262
370
  };
263
371
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, type SubscriptionAccount, type SubscriptionAuthOptions, } from "./auth.js";
2
- export { defaultAiSubsDataDir, FileCredentialStore, MemoryCredentialStore } from "./store.js";
2
+ export { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore, MemoryCredentialStore, } from "./store.js";
3
3
  export { chatGptProvider, type ChatGptProviderOptions } from "./providers/chatgpt.js";
4
4
  export { claudeProvider, type ClaudeProviderOptions } from "./providers/claude.js";
5
5
  export { copilotProvider, type CopilotProviderOptions } from "./providers/copilot.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, } from "./auth.js";
2
- export { defaultAiSubsDataDir, FileCredentialStore, MemoryCredentialStore } from "./store.js";
2
+ export { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore, MemoryCredentialStore, } from "./store.js";
3
3
  export { chatGptProvider } from "./providers/chatgpt.js";
4
4
  export { claudeProvider } from "./providers/claude.js";
5
5
  export { copilotProvider } from "./providers/copilot.js";
@@ -92,6 +92,8 @@ function normalizeModel(value) {
92
92
  maxOutputTokens: numberValue(value.max_output_tokens),
93
93
  reasoningEfforts: levels.length ? levels : stringArray(value.supported_reasoning_efforts),
94
94
  inputModalities: stringArray(value.input_modalities),
95
+ endpoints: ["responses"],
96
+ supportsToolCall: value.supports_tool_calls === false || value.supports_tools === false ? false : true,
95
97
  available: visibility !== "hide" && value.supported_in_api !== false,
96
98
  priority: numberValue(value.priority) ?? Number.MAX_SAFE_INTEGER,
97
99
  };
@@ -273,7 +273,9 @@ export function claudeProvider(options = {}) {
273
273
  description: stringValue(value.description),
274
274
  contextWindow: numberValue(value.context_window),
275
275
  maxOutputTokens: numberValue(value.max_output_tokens),
276
+ inputModalities: ["text", "image", "document"],
276
277
  endpoints: ["messages"],
278
+ supportsToolCall: true,
277
279
  available: true,
278
280
  selectable: true,
279
281
  },
@@ -122,8 +122,6 @@ function normalizeModel(value) {
122
122
  const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
123
123
  const limits = isRecord(capabilities.limits) ? capabilities.limits : {};
124
124
  const supports = isRecord(capabilities.supports) ? capabilities.supports : {};
125
- if (supports.tool_calls === false)
126
- return null;
127
125
  const modalities = ["text"];
128
126
  if (supports.vision === true || isRecord(limits.vision))
129
127
  modalities.push("image");
@@ -0,0 +1,6 @@
1
+ import type { FastifyInstance, FastifyRequest } from "fastify";
2
+ import type { SubscriptionAuth } from "./auth.js";
3
+ type RealtimeAuth = (request: FastifyRequest) => boolean | Promise<boolean>;
4
+ /** Register a native Realtime WebSocket tunnel for providers that expose one. */
5
+ export declare function registerRealtimeProxy(app: FastifyInstance, auth: SubscriptionAuth, authenticate: RealtimeAuth): void;
6
+ export {};