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/CHANGELOG.md +17 -0
- package/README.md +478 -427
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +23 -2
- package/dist/cli.js +10 -7
- package/dist/compatibility.d.ts +14 -0
- package/dist/compatibility.js +1521 -0
- package/dist/dashboard/assets/index-CEDww1hA.css +2 -0
- package/dist/dashboard/assets/index-DrnM3oWy.js +84 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.d.ts +5 -2
- package/dist/dashboard.js +200 -75
- package/dist/http.d.ts +9 -6
- package/dist/http.js +258 -150
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/chatgpt.js +2 -0
- package/dist/providers/claude.js +2 -0
- package/dist/providers/copilot.js +0 -2
- package/dist/realtime.d.ts +6 -0
- package/dist/realtime.js +163 -0
- package/dist/store.d.ts +6 -0
- package/dist/store.js +51 -2
- package/examples/direct.mjs +31 -8
- package/examples/server.mjs +17 -5
- package/package.json +9 -1
- package/public/aisubs-dashboard.png +0 -0
- package/dist/dashboard/assets/index-BMoILzPw.js +0 -64
- package/dist/dashboard/assets/index-DHqDdNVe.css +0 -2
package/dist/http.js
CHANGED
|
@@ -1,39 +1,80 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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 {
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
const body =
|
|
31
|
-
if (body.length
|
|
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
|
|
36
|
-
const headers =
|
|
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
|
|
88
|
+
headers.set(name, value);
|
|
48
89
|
}
|
|
49
90
|
});
|
|
50
91
|
return headers;
|
|
51
92
|
}
|
|
52
|
-
async function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
-
|
|
109
|
-
return true;
|
|
178
|
+
return jsonResponse({ providers: auth.listProviders() });
|
|
110
179
|
}
|
|
111
180
|
if (request.method === "GET" && parts.join("/") === "v1/auth") {
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
return true;
|
|
192
|
+
return jsonResponse({ accounts: await auth.listAccounts(provider) });
|
|
127
193
|
}
|
|
128
194
|
if (request.method === "GET" && parts[3] === "details") {
|
|
129
|
-
|
|
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 =
|
|
198
|
+
const body = jsonBody(request);
|
|
134
199
|
const attempt = await auth.signIn(provider, isRecord(body) ? body : undefined);
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
264
|
+
return null;
|
|
213
265
|
}
|
|
214
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
330
|
+
await reply.code(404).send({
|
|
331
|
+
error: { message: "Not found", type: "invalid_request_error", code: "not_found" },
|
|
332
|
+
});
|
|
333
|
+
},
|
|
245
334
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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:
|
|
261
|
-
close: () =>
|
|
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
|
};
|
package/dist/providers/claude.js
CHANGED
|
@@ -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 {};
|