aisubs 0.3.6 → 0.3.8
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 +14 -0
- package/README.md +29 -29
- package/dist/auth.d.ts +4 -1
- package/dist/auth.js +143 -25
- package/dist/cli.js +5 -3
- package/dist/compatibility.js +157 -137
- package/dist/dashboard/assets/index-CePTUw7Z.css +2 -0
- package/dist/dashboard/assets/index-Vscu32Dz.js +84 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.js +15 -26
- package/dist/http.d.ts +3 -0
- package/dist/http.js +13 -31
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -6
- package/dist/memory-store.d.ts +9 -0
- package/dist/memory-store.js +34 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.js +2 -0
- package/dist/providers/chatgpt.d.ts +2 -1
- package/dist/providers/chatgpt.js +64 -2
- package/dist/providers/opencode.d.ts +6 -2
- package/dist/providers/opencode.js +65 -21
- package/dist/proxy-headers.d.ts +2 -0
- package/dist/proxy-headers.js +35 -0
- package/dist/realtime.js +6 -20
- package/dist/sqlite-store.d.ts +61 -0
- package/dist/sqlite-store.js +323 -0
- package/dist/store.d.ts +0 -23
- package/dist/store.js +1 -191
- package/dist/types.d.ts +40 -0
- package/dist/utils.js +15 -7
- package/examples/direct.mjs +8 -11
- package/examples/server.mjs +10 -15
- package/package.json +15 -6
- package/dist/dashboard/assets/index-BJDbjHnw.css +0 -2
- package/dist/dashboard/assets/index-BbN84qld.js +0 -84
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light dark" />
|
|
7
7
|
<meta name="theme-color" content="#181818" />
|
|
8
8
|
<title>AI Subs</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Vscu32Dz.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CePTUw7Z.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/dist/dashboard.js
CHANGED
|
@@ -2,14 +2,14 @@ import cors from "@fastify/cors";
|
|
|
2
2
|
import websocket from "@fastify/websocket";
|
|
3
3
|
import Fastify from "fastify";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { randomBytes
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { clientAbortSignal, handleSubscriptionAuthApi, routeSegments, sendWebResponse, } from "./http.js";
|
|
10
|
+
import { clientAbortSignal, handleSubscriptionAuthApi, routeSegments, sendWebResponse, bodyLimit, requestApiKeys, sameSecret, } from "./http.js";
|
|
11
11
|
import { registerRealtimeProxy } from "./realtime.js";
|
|
12
|
-
import { errorMessage, isRecord, stringValue, urlHost } from "./utils.js";
|
|
12
|
+
import { errorMessage, isRecord, numberValue, stringValue, urlHost } from "./utils.js";
|
|
13
13
|
const ASSET_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), "dashboard");
|
|
14
14
|
const CONTENT_TYPES = {
|
|
15
15
|
".css": "text/css; charset=utf-8",
|
|
@@ -18,23 +18,6 @@ const CONTENT_TYPES = {
|
|
|
18
18
|
".svg": "image/svg+xml",
|
|
19
19
|
".woff2": "font/woff2",
|
|
20
20
|
};
|
|
21
|
-
function sameSecret(actual, expected) {
|
|
22
|
-
if (!actual)
|
|
23
|
-
return false;
|
|
24
|
-
const left = Buffer.from(actual);
|
|
25
|
-
const right = Buffer.from(expected);
|
|
26
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
27
|
-
}
|
|
28
|
-
function requestApiKeys(request) {
|
|
29
|
-
const authorization = request.headers.authorization;
|
|
30
|
-
const bearer = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
|
|
31
|
-
const header = (name) => {
|
|
32
|
-
const value = request.headers[name];
|
|
33
|
-
return Array.isArray(value) ? value[0] : value;
|
|
34
|
-
};
|
|
35
|
-
const queryKey = new URL(request.url, "http://aisubs.local").searchParams.get("key") ?? undefined;
|
|
36
|
-
return [bearer, header("x-api-key"), header("x-goog-api-key"), queryKey].filter((value) => value != null);
|
|
37
|
-
}
|
|
38
21
|
function cookie(request, name) {
|
|
39
22
|
for (const part of request.headers.cookie?.split(";") ?? []) {
|
|
40
23
|
const [key, ...value] = part.trim().split("=");
|
|
@@ -123,7 +106,7 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
123
106
|
const logStreams = new Set();
|
|
124
107
|
let requestId = 0;
|
|
125
108
|
const app = Fastify({
|
|
126
|
-
bodyLimit: options.maxProxyBodyBytes
|
|
109
|
+
bodyLimit: bodyLimit(options.maxProxyBodyBytes),
|
|
127
110
|
forceCloseConnections: true,
|
|
128
111
|
});
|
|
129
112
|
app.removeAllContentTypeParsers();
|
|
@@ -147,7 +130,8 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
147
130
|
await app.register(async (scope) => {
|
|
148
131
|
registerRealtimeProxy(scope, options.auth, (request) => {
|
|
149
132
|
return (requestApiKeys(request).some((value) => sameSecret(value, apiKey)) ||
|
|
150
|
-
sameSecret(cookie(request, "aisubs_session"), sessionToken)
|
|
133
|
+
(sameSecret(cookie(request, "aisubs_session"), sessionToken) &&
|
|
134
|
+
request.headers.origin === `http://${request.headers.host}`));
|
|
151
135
|
});
|
|
152
136
|
});
|
|
153
137
|
app.addHook("onRequest", async (request, reply) => {
|
|
@@ -173,8 +157,10 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
173
157
|
if (requestLogs.length > 200)
|
|
174
158
|
requestLogs.shift();
|
|
175
159
|
const event = `data: ${JSON.stringify(entry)}\n\n`;
|
|
176
|
-
for (const stream of logStreams)
|
|
177
|
-
stream.write(event)
|
|
160
|
+
for (const stream of logStreams) {
|
|
161
|
+
if (!stream.write(event))
|
|
162
|
+
stream.destroy();
|
|
163
|
+
}
|
|
178
164
|
});
|
|
179
165
|
}
|
|
180
166
|
});
|
|
@@ -217,7 +203,7 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
217
203
|
for (const entry of requestLogs)
|
|
218
204
|
reply.raw.write(`data: ${JSON.stringify(entry)}\n\n`);
|
|
219
205
|
logStreams.add(reply.raw);
|
|
220
|
-
|
|
206
|
+
reply.raw.once("close", () => logStreams.delete(reply.raw));
|
|
221
207
|
return;
|
|
222
208
|
}
|
|
223
209
|
if (apiRoute && cookieAuthenticated && !bearerAuthenticated) {
|
|
@@ -293,7 +279,10 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
293
279
|
});
|
|
294
280
|
app.setErrorHandler(async (error, request, reply) => {
|
|
295
281
|
requestErrors.set(request, errorMessage(error));
|
|
296
|
-
|
|
282
|
+
const status = isRecord(error) ? numberValue(error.statusCode) : undefined;
|
|
283
|
+
await reply
|
|
284
|
+
.code(status && status >= 400 && status <= 599 ? status : 400)
|
|
285
|
+
.send({ error: errorMessage(error) });
|
|
297
286
|
});
|
|
298
287
|
await app.listen({ port: options.port ?? 0, host });
|
|
299
288
|
const address = app.server.address();
|
package/dist/http.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
|
|
2
2
|
import type { Server } from "node:http";
|
|
3
3
|
import type { SubscriptionAuth } from "./auth.js";
|
|
4
|
+
export declare function bodyLimit(value?: number): number;
|
|
5
|
+
export declare function sameSecret(actual: string | undefined, expected: string): boolean;
|
|
6
|
+
export declare function requestApiKeys(request: FastifyRequest): string[];
|
|
4
7
|
export declare function routeSegments(pathname: string): string[];
|
|
5
8
|
export declare function sendWebResponse(reply: FastifyReply, upstream: Response): Promise<void>;
|
|
6
9
|
export declare function handleSubscriptionAuthApi(auth: SubscriptionAuth, request: FastifyRequest, signal?: AbortSignal): Promise<Response | null>;
|
package/dist/http.js
CHANGED
|
@@ -6,21 +6,22 @@ import { Readable } from "node:stream";
|
|
|
6
6
|
import { proxyCompatible } from "./compatibility.js";
|
|
7
7
|
import { registerRealtimeProxy } from "./realtime.js";
|
|
8
8
|
import { errorMessage, isRecord, numberValue, stringValue, urlHost } from "./utils.js";
|
|
9
|
+
import { proxyRequestHeaders } from "./proxy-headers.js";
|
|
9
10
|
const MAX_PROXY_BODY_BYTES = 10 * 1024 * 1024;
|
|
10
|
-
function bodyLimit(value = MAX_PROXY_BODY_BYTES) {
|
|
11
|
+
export function bodyLimit(value = MAX_PROXY_BODY_BYTES) {
|
|
11
12
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
12
13
|
throw new Error("maxProxyBodyBytes must be a positive safe integer");
|
|
13
14
|
}
|
|
14
15
|
return value;
|
|
15
16
|
}
|
|
16
|
-
function sameSecret(actual, expected) {
|
|
17
|
+
export function sameSecret(actual, expected) {
|
|
17
18
|
if (!actual)
|
|
18
19
|
return false;
|
|
19
20
|
const left = Buffer.from(actual);
|
|
20
21
|
const right = Buffer.from(expected);
|
|
21
22
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
22
23
|
}
|
|
23
|
-
function requestApiKeys(request) {
|
|
24
|
+
export function requestApiKeys(request) {
|
|
24
25
|
const authorization = request.headers.authorization;
|
|
25
26
|
const bearer = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
|
|
26
27
|
const header = (name) => {
|
|
@@ -50,28 +51,12 @@ function jsonBody(request) {
|
|
|
50
51
|
}
|
|
51
52
|
function requestHeaders(request) {
|
|
52
53
|
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
54
|
for (const [name, value] of Object.entries(request.headers)) {
|
|
70
|
-
if (value != null
|
|
55
|
+
if (value != null) {
|
|
71
56
|
headers.set(name, Array.isArray(value) ? value.join(", ") : String(value));
|
|
72
57
|
}
|
|
73
58
|
}
|
|
74
|
-
return headers;
|
|
59
|
+
return proxyRequestHeaders(headers);
|
|
75
60
|
}
|
|
76
61
|
function responseHeaders(upstream) {
|
|
77
62
|
const headers = new Headers({ "cache-control": "no-store" });
|
|
@@ -127,6 +112,7 @@ function openAiModel(provider, model) {
|
|
|
127
112
|
capabilities: {
|
|
128
113
|
endpoints: model.endpoints ?? [],
|
|
129
114
|
input_modalities: model.inputModalities ?? ["text"],
|
|
115
|
+
output_modalities: model.outputModalities ?? ["text"],
|
|
130
116
|
reasoning_efforts: model.reasoningEfforts ?? [],
|
|
131
117
|
tools: model.supportsToolCall ?? false,
|
|
132
118
|
},
|
|
@@ -144,17 +130,11 @@ function codexResponsesBody(input, model) {
|
|
|
144
130
|
if (Array.isArray(body.input)) {
|
|
145
131
|
body.input = body.input
|
|
146
132
|
.filter((item) => isRecord(item) &&
|
|
147
|
-
|
|
133
|
+
(item.type === undefined ||
|
|
134
|
+
["message", "function_call", "function_call_output"].includes(String(item.type))))
|
|
148
135
|
.map((item) => {
|
|
149
|
-
if (item.type === "message") {
|
|
150
|
-
|
|
151
|
-
? item.content
|
|
152
|
-
.filter(isRecord)
|
|
153
|
-
.map((part) => stringValue(part.text))
|
|
154
|
-
.filter((value) => Boolean(value))
|
|
155
|
-
.join("")
|
|
156
|
-
: item.content;
|
|
157
|
-
return { type: "message", role: stringValue(item.role) ?? "user", content };
|
|
136
|
+
if (item.type === "message" || item.type === undefined) {
|
|
137
|
+
return { type: "message", role: stringValue(item.role) ?? "user", content: item.content };
|
|
158
138
|
}
|
|
159
139
|
return item;
|
|
160
140
|
});
|
|
@@ -394,10 +374,12 @@ export function clientAbortSignal(request, reply) {
|
|
|
394
374
|
const abort = () => {
|
|
395
375
|
if (!reply.raw.writableFinished && !controller.signal.aborted)
|
|
396
376
|
controller.abort();
|
|
377
|
+
cleanup();
|
|
397
378
|
};
|
|
398
379
|
const cleanup = () => {
|
|
399
380
|
request.raw.off("aborted", abort);
|
|
400
381
|
reply.raw.off("close", abort);
|
|
382
|
+
reply.raw.off("finish", cleanup);
|
|
401
383
|
};
|
|
402
384
|
request.raw.once("aborted", abort);
|
|
403
385
|
reply.raw.once("close", abort);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, type SubscriptionAccount, type SubscriptionAuthOptions, } from "./auth.js";
|
|
2
|
-
export {
|
|
3
|
-
export { chatGptProvider, type ChatGptProviderOptions } from "./providers/chatgpt.js";
|
|
4
|
-
export { claudeProvider, type ClaudeProviderOptions } from "./providers/claude.js";
|
|
5
|
-
export { copilotProvider, type CopilotProviderOptions } from "./providers/copilot.js";
|
|
6
|
-
export { grokProvider, type GrokProviderOptions } from "./providers/grok.js";
|
|
7
|
-
export { openCodeGoProvider, openCodeZenProvider } from "./providers/opencode.js";
|
|
2
|
+
export { MemoryCredentialStore } from "./memory-store.js";
|
|
8
3
|
export { parseChatGptUsage, parseCopilotUsage, parseGrokUsage } from "./usage.js";
|
|
9
|
-
export type { CredentialStore, CredentialSummary, BrowserLoginPrompt, DeviceLoginPrompt, ImmediateLoginPrompt, LoginAttempt, LoginMode, LoginPrompt, LoginState, OAuthCredential, ProviderAdapter, ProviderId, ProviderLogin, ProviderLoginField, ProviderModel, ProviderModels, ProviderSummary, ProviderUsage, ProviderUsageContext, ProviderUsageData, Session, SubscriptionAccountDetails, UsageFact, UsageMeter, UsageResetCredit, UsageResetCredits, } from "./types.js";
|
|
4
|
+
export type { CredentialStore, CoordinatedCredentialStore, CredentialSummary, BrowserLoginPrompt, DeviceLoginPrompt, ImmediateLoginPrompt, LoginAttempt, LoginMode, LoginPrompt, LoginState, OAuthCredential, VersionedCredential, ProviderAdapter, ProviderId, ProviderLogin, ProviderLoginField, ProviderModel, ProviderModels, ProviderSummary, ProviderUsage, ProviderUsageContext, ProviderUsageData, Session, SubscriptionAccountDetails, UsageFact, UsageMeter, UsageResetCredit, UsageResetCredits, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, } from "./auth.js";
|
|
2
|
-
export {
|
|
3
|
-
export { chatGptProvider } from "./providers/chatgpt.js";
|
|
4
|
-
export { claudeProvider } from "./providers/claude.js";
|
|
5
|
-
export { copilotProvider } from "./providers/copilot.js";
|
|
6
|
-
export { grokProvider } from "./providers/grok.js";
|
|
7
|
-
export { openCodeGoProvider, openCodeZenProvider } from "./providers/opencode.js";
|
|
2
|
+
export { MemoryCredentialStore } from "./memory-store.js";
|
|
8
3
|
export { parseChatGptUsage, parseCopilotUsage, parseGrokUsage } from "./usage.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CredentialStore, OAuthCredential, ProviderId } from "./types.js";
|
|
2
|
+
export declare class MemoryCredentialStore implements CredentialStore {
|
|
3
|
+
private readonly values;
|
|
4
|
+
private readonly queues;
|
|
5
|
+
read(provider: ProviderId): Promise<OAuthCredential | null>;
|
|
6
|
+
listKeys(): Promise<string[]>;
|
|
7
|
+
modify(provider: ProviderId, update: (current: OAuthCredential | null) => OAuthCredential | null | Promise<OAuthCredential | null>): Promise<OAuthCredential | null>;
|
|
8
|
+
delete(provider: ProviderId): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class MemoryCredentialStore {
|
|
2
|
+
values = new Map();
|
|
3
|
+
queues = new Map();
|
|
4
|
+
async read(provider) {
|
|
5
|
+
return this.values.get(provider) ?? null;
|
|
6
|
+
}
|
|
7
|
+
async listKeys() {
|
|
8
|
+
return [...this.values.keys()];
|
|
9
|
+
}
|
|
10
|
+
async modify(provider, update) {
|
|
11
|
+
const previous = this.queues.get(provider) ?? Promise.resolve();
|
|
12
|
+
let result = null;
|
|
13
|
+
const current = previous.then(async () => {
|
|
14
|
+
result = await update(this.values.get(provider) ?? null);
|
|
15
|
+
if (result)
|
|
16
|
+
this.values.set(provider, result);
|
|
17
|
+
else
|
|
18
|
+
this.values.delete(provider);
|
|
19
|
+
});
|
|
20
|
+
const settled = current.catch(() => { });
|
|
21
|
+
this.queues.set(provider, settled);
|
|
22
|
+
try {
|
|
23
|
+
await current;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (this.queues.get(provider) === settled)
|
|
27
|
+
this.queues.delete(provider);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
async delete(provider) {
|
|
32
|
+
await this.modify(provider, () => null);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/node.d.ts
ADDED
package/dist/node.js
ADDED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ProviderAdapter } from "../types.js";
|
|
1
|
+
import type { ProviderAdapter, ProviderModel } from "../types.js";
|
|
2
|
+
export declare function parseOpenAiImageModels(html: string): ProviderModel[];
|
|
2
3
|
export interface ChatGptProviderOptions {
|
|
3
4
|
clientId?: string;
|
|
4
5
|
compatibilityVersion?: string;
|
|
@@ -12,6 +12,7 @@ const VERIFICATION_URL = `${ISSUER}/codex/device`;
|
|
|
12
12
|
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
13
13
|
const RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
14
14
|
const MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
|
|
15
|
+
const OPENAI_MODELS_URL = "https://developers.openai.com/api/docs/models";
|
|
15
16
|
const EXPIRY_SKEW_MS = 5 * 60_000;
|
|
16
17
|
const BROWSER_LOGIN_TIMEOUT_MS = 10 * 60_000;
|
|
17
18
|
const BROWSER_CALLBACK_PORTS = [1455, 1457];
|
|
@@ -84,6 +85,7 @@ function normalizeModel(value) {
|
|
|
84
85
|
})
|
|
85
86
|
: [];
|
|
86
87
|
const visibility = stringValue(value.visibility);
|
|
88
|
+
const outputModalities = stringArray(value.output_modalities);
|
|
87
89
|
return {
|
|
88
90
|
id,
|
|
89
91
|
name: stringValue(value.display_name) ?? stringValue(value.name),
|
|
@@ -92,12 +94,65 @@ function normalizeModel(value) {
|
|
|
92
94
|
maxOutputTokens: numberValue(value.max_output_tokens),
|
|
93
95
|
reasoningEfforts: levels.length ? levels : stringArray(value.supported_reasoning_efforts),
|
|
94
96
|
inputModalities: stringArray(value.input_modalities),
|
|
95
|
-
|
|
97
|
+
outputModalities,
|
|
98
|
+
endpoints: outputModalities?.includes("image")
|
|
99
|
+
? ["images/generations", "images/edits"]
|
|
100
|
+
: ["responses"],
|
|
96
101
|
supportsToolCall: value.supports_tool_calls === false || value.supports_tools === false ? false : true,
|
|
97
102
|
available: visibility !== "hide" && value.supported_in_api !== false,
|
|
98
103
|
priority: numberValue(value.priority) ?? Number.MAX_SAFE_INTEGER,
|
|
99
104
|
};
|
|
100
105
|
}
|
|
106
|
+
function imageModelName(id) {
|
|
107
|
+
const [prefix, suffix] = id.startsWith("chatgpt-image-")
|
|
108
|
+
? ["ChatGPT Image", id.slice("chatgpt-image-".length)]
|
|
109
|
+
: id.startsWith("dall-e-")
|
|
110
|
+
? ["DALL-E", id.slice("dall-e-".length)]
|
|
111
|
+
: ["GPT Image", id.slice("gpt-image-".length)];
|
|
112
|
+
const label = suffix
|
|
113
|
+
.split("-")
|
|
114
|
+
.map((part) => (/^[a-z]/.test(part) ? part[0].toUpperCase() + part.slice(1) : part))
|
|
115
|
+
.join(" ");
|
|
116
|
+
return `${prefix} ${label}`.trim();
|
|
117
|
+
}
|
|
118
|
+
export function parseOpenAiImageModels(html) {
|
|
119
|
+
const ids = new Set();
|
|
120
|
+
const links = html.matchAll(/href=["'](?:https:\/\/developers\.openai\.com)?\/api\/docs\/models\/([^"'/?#]+)[^"']*["']/gi);
|
|
121
|
+
for (const link of links) {
|
|
122
|
+
const id = decodeURIComponent(link[1] ?? "").toLowerCase();
|
|
123
|
+
if (/^(?:gpt-image-|chatgpt-image-|dall-e-)/.test(id))
|
|
124
|
+
ids.add(id);
|
|
125
|
+
}
|
|
126
|
+
return [...ids].map(openAiImageModel);
|
|
127
|
+
}
|
|
128
|
+
function openAiImageModel(id) {
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
name: imageModelName(id),
|
|
132
|
+
inputModalities: ["text", "image"],
|
|
133
|
+
outputModalities: ["image"],
|
|
134
|
+
endpoints: ["images/generations", "images/edits"],
|
|
135
|
+
supportsToolCall: false,
|
|
136
|
+
available: true,
|
|
137
|
+
selectable: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function discoverOpenAiImageModels(fetcher, signal) {
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetcher(OPENAI_MODELS_URL, {
|
|
143
|
+
headers: { accept: "text/html" },
|
|
144
|
+
signal,
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok)
|
|
147
|
+
return [];
|
|
148
|
+
return parseOpenAiImageModels(await response.text());
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (signal.aborted)
|
|
152
|
+
throw error;
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
101
156
|
async function normalizeChatGptRequest(request) {
|
|
102
157
|
if (request.method !== "POST" || !new URL(request.url).pathname.endsWith("/responses")) {
|
|
103
158
|
return request;
|
|
@@ -115,6 +170,7 @@ async function normalizeChatGptRequest(request) {
|
|
|
115
170
|
headers.set("session-id", sessionId);
|
|
116
171
|
// Preserve instruction placement and message boundaries. The native Codex
|
|
117
172
|
// client sends top-level instructions too; moving them does not enable caching.
|
|
173
|
+
delete body.max_output_tokens;
|
|
118
174
|
delete body.prompt_cache_options;
|
|
119
175
|
delete body.prompt_cache_retention;
|
|
120
176
|
const stripBreakpoints = (value) => Array.isArray(value)
|
|
@@ -459,12 +515,18 @@ export function chatGptProvider(options = {}) {
|
|
|
459
515
|
const response = await fetch(url, { headers: { accept: "application/json" }, signal });
|
|
460
516
|
const raw = await responseJson(response, "ChatGPT models");
|
|
461
517
|
const models = Array.isArray(raw.models) ? raw.models : [];
|
|
462
|
-
|
|
518
|
+
const languageModels = models
|
|
463
519
|
.map(normalizeModel)
|
|
464
520
|
.filter((model) => Boolean(model))
|
|
465
521
|
.filter((model) => model.available !== false)
|
|
466
522
|
.sort((left, right) => left.priority - right.priority)
|
|
467
523
|
.map(({ priority: _priority, ...model }) => model);
|
|
524
|
+
const listed = new Map(languageModels.map((model) => [model.id, model]));
|
|
525
|
+
for (const model of await discoverOpenAiImageModels(fetcher, signal)) {
|
|
526
|
+
if (!listed.has(model.id))
|
|
527
|
+
listed.set(model.id, model);
|
|
528
|
+
}
|
|
529
|
+
return [...listed.values()];
|
|
468
530
|
},
|
|
469
531
|
isPermanentRefreshError(error) {
|
|
470
532
|
return (error instanceof ChatGptTokenError &&
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ProviderAdapter, ProviderUsageData } from "../types.js";
|
|
2
|
+
export interface OpenCodeProviderOptions {
|
|
3
|
+
compatibilityVersion?: string;
|
|
4
|
+
fetch?: typeof globalThis.fetch;
|
|
5
|
+
}
|
|
2
6
|
export declare function parseOpenCodeGoUsage(raw: unknown): ProviderUsageData | null;
|
|
3
|
-
export declare function openCodeGoProvider(): ProviderAdapter;
|
|
4
|
-
export declare function openCodeZenProvider(): ProviderAdapter;
|
|
7
|
+
export declare function openCodeGoProvider(options?: OpenCodeProviderOptions): ProviderAdapter;
|
|
8
|
+
export declare function openCodeZenProvider(options?: OpenCodeProviderOptions): ProviderAdapter;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { bearerRequest, isRecord, numberValue, requireAllowedHost, responseJson, stringArray, stringValue, } from "../utils.js";
|
|
2
3
|
const API_HOST = "opencode.ai";
|
|
3
4
|
const API_KEY_LIFETIME_MS = 365 * 24 * 60 * 60_000;
|
|
5
|
+
const DEFAULT_COMPATIBILITY_VERSION = "1.18.31";
|
|
6
|
+
const COMPATIBILITY_VERSION_TTL_MS = 60 * 60_000;
|
|
7
|
+
const ID_PATTERN = /^(ses|msg)_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
|
|
8
|
+
const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
4
9
|
function openCodeUsageMeter(id, label, raw) {
|
|
5
10
|
if (!isRecord(raw))
|
|
6
11
|
return null;
|
|
@@ -33,18 +38,53 @@ export function parseOpenCodeGoUsage(raw) {
|
|
|
33
38
|
note: "Provider-reported quota usage. API-equivalent costs shown in chats are estimates, not charges.",
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
41
|
+
function openCodeProvider(config, options = {}) {
|
|
42
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
43
|
+
let versionRequest;
|
|
44
|
+
let versionExpiresAt = 0;
|
|
45
|
+
const compatibilityVersion = () => {
|
|
46
|
+
if (options.compatibilityVersion)
|
|
47
|
+
return Promise.resolve(options.compatibilityVersion);
|
|
48
|
+
if (!versionRequest || Date.now() >= versionExpiresAt) {
|
|
49
|
+
versionExpiresAt = Date.now() + COMPATIBILITY_VERSION_TTL_MS;
|
|
50
|
+
versionRequest = fetcher("https://api.github.com/repos/anomalyco/opencode/releases/latest", {
|
|
51
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "aisubs" },
|
|
52
|
+
})
|
|
53
|
+
.then(async (response) => {
|
|
54
|
+
if (!response.ok)
|
|
55
|
+
throw new Error(`OpenCode version lookup failed: ${response.status}`);
|
|
56
|
+
const raw = await response.json();
|
|
57
|
+
return isRecord(raw) ? stringValue(raw.tag_name)?.replace(/^v/, "") : undefined;
|
|
58
|
+
})
|
|
59
|
+
.then((version) => version || DEFAULT_COMPATIBILITY_VERSION)
|
|
60
|
+
.catch(() => DEFAULT_COMPATIBILITY_VERSION);
|
|
61
|
+
}
|
|
62
|
+
return versionRequest;
|
|
63
|
+
};
|
|
64
|
+
const routingIds = new Map();
|
|
65
|
+
let lastTimestamp = 0;
|
|
66
|
+
let counter = 0;
|
|
67
|
+
const identifier = (prefix, source) => {
|
|
68
|
+
if (source && ID_PATTERN.test(source) && source.startsWith(`${prefix}_`))
|
|
69
|
+
return source;
|
|
70
|
+
const key = source ? `${prefix}:${source}` : undefined;
|
|
71
|
+
const cached = key ? routingIds.get(key) : undefined;
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached;
|
|
74
|
+
const timestamp = Date.now();
|
|
75
|
+
counter = timestamp === lastTimestamp ? counter + 1 : 1;
|
|
76
|
+
lastTimestamp = timestamp;
|
|
77
|
+
const encoded = BigInt(timestamp) * 0x1000n + BigInt(counter);
|
|
78
|
+
const time = Buffer.alloc(6);
|
|
79
|
+
for (let index = 0; index < time.length; index += 1) {
|
|
80
|
+
time[index] = Number((encoded >> BigInt(40 - 8 * index)) & 0xffn);
|
|
81
|
+
}
|
|
82
|
+
const random = [...randomBytes(14)].map((byte) => ID_CHARS[byte % ID_CHARS.length]).join("");
|
|
83
|
+
const value = `${prefix}_${time.toString("hex")}${random}`;
|
|
84
|
+
if (key)
|
|
85
|
+
routingIds.set(key, value);
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
48
88
|
const credential = (apiKey) => ({
|
|
49
89
|
accessToken: apiKey,
|
|
50
90
|
expiresAt: Date.now() + API_KEY_LIFETIME_MS,
|
|
@@ -79,9 +119,15 @@ function openCodeProvider(config) {
|
|
|
79
119
|
async refresh(current) {
|
|
80
120
|
return { ...current, expiresAt: Date.now() + API_KEY_LIFETIME_MS };
|
|
81
121
|
},
|
|
82
|
-
authorize(request, current) {
|
|
122
|
+
async authorize(request, current) {
|
|
83
123
|
requireAllowedHost(request, [API_HOST]);
|
|
84
|
-
|
|
124
|
+
const client = request.headers.get("x-opencode-client")?.trim() || "aisubs";
|
|
125
|
+
return bearerRequest(request, current, {
|
|
126
|
+
"user-agent": `opencode/latest/${await compatibilityVersion()}/${client}`,
|
|
127
|
+
"x-opencode-client": client,
|
|
128
|
+
"x-opencode-session": identifier("ses", request.headers.get("x-opencode-session")),
|
|
129
|
+
"x-opencode-request": identifier("msg", request.headers.get("x-opencode-request")),
|
|
130
|
+
});
|
|
85
131
|
},
|
|
86
132
|
async getModels({ fetch, signal }) {
|
|
87
133
|
const raw = await responseJson(await fetch(`${config.baseUrl}/models`, {
|
|
@@ -103,9 +149,7 @@ function openCodeProvider(config) {
|
|
|
103
149
|
id,
|
|
104
150
|
name: stringValue(value.name),
|
|
105
151
|
description: stringValue(value.description),
|
|
106
|
-
endpoints: stringArray(value.supported_endpoints) ??
|
|
107
|
-
stringArray(value.endpoints) ??
|
|
108
|
-
documentedEndpoints(config.id, id),
|
|
152
|
+
endpoints: stringArray(value.supported_endpoints) ?? stringArray(value.endpoints),
|
|
109
153
|
available: true,
|
|
110
154
|
selectable: true,
|
|
111
155
|
},
|
|
@@ -129,19 +173,19 @@ function openCodeProvider(config) {
|
|
|
129
173
|
},
|
|
130
174
|
};
|
|
131
175
|
}
|
|
132
|
-
export function openCodeGoProvider() {
|
|
176
|
+
export function openCodeGoProvider(options = {}) {
|
|
133
177
|
return openCodeProvider({
|
|
134
178
|
id: "opencode-go",
|
|
135
179
|
name: "OpenCode Go",
|
|
136
180
|
description: "OpenCode Go subscription access with an OpenCode API key.",
|
|
137
181
|
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
138
|
-
});
|
|
182
|
+
}, options);
|
|
139
183
|
}
|
|
140
|
-
export function openCodeZenProvider() {
|
|
184
|
+
export function openCodeZenProvider(options = {}) {
|
|
141
185
|
return openCodeProvider({
|
|
142
186
|
id: "opencode-zen",
|
|
143
187
|
name: "OpenCode Zen",
|
|
144
188
|
description: "OpenCode Zen pay-as-you-go access with an OpenCode API key.",
|
|
145
189
|
baseUrl: "https://opencode.ai/zen/v1",
|
|
146
|
-
});
|
|
190
|
+
}, options);
|
|
147
191
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Remove local credentials, routing metadata, and hop-by-hop headers before authorization. */
|
|
2
|
+
export function proxyRequestHeaders(input) {
|
|
3
|
+
const headers = new Headers(input);
|
|
4
|
+
const connectionHeaders = headers
|
|
5
|
+
.get("connection")
|
|
6
|
+
?.split(",")
|
|
7
|
+
.map((name) => name.trim()) ?? [];
|
|
8
|
+
for (const name of [
|
|
9
|
+
...connectionHeaders,
|
|
10
|
+
"authorization",
|
|
11
|
+
"connection",
|
|
12
|
+
"content-length",
|
|
13
|
+
"cookie",
|
|
14
|
+
"forwarded",
|
|
15
|
+
"host",
|
|
16
|
+
"keep-alive",
|
|
17
|
+
"origin",
|
|
18
|
+
"proxy-authenticate",
|
|
19
|
+
"proxy-authorization",
|
|
20
|
+
"referer",
|
|
21
|
+
"te",
|
|
22
|
+
"trailer",
|
|
23
|
+
"transfer-encoding",
|
|
24
|
+
"upgrade",
|
|
25
|
+
"x-api-key",
|
|
26
|
+
"x-goog-api-key",
|
|
27
|
+
]) {
|
|
28
|
+
if (name)
|
|
29
|
+
headers.delete(name);
|
|
30
|
+
}
|
|
31
|
+
const routingHeaders = [...headers.keys()].filter((name) => name.startsWith("x-forwarded-") || name.startsWith("sec-websocket-"));
|
|
32
|
+
for (const name of routingHeaders)
|
|
33
|
+
headers.delete(name);
|
|
34
|
+
return headers;
|
|
35
|
+
}
|
package/dist/realtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { errorMessage } from "./utils.js";
|
|
3
|
+
import { proxyRequestHeaders } from "./proxy-headers.js";
|
|
3
4
|
function rawDataBytes(data) {
|
|
4
5
|
if (Array.isArray(data))
|
|
5
6
|
return data.reduce((total, item) => total + item.byteLength, 0);
|
|
@@ -31,24 +32,8 @@ function upstreamHeaders(request) {
|
|
|
31
32
|
return headers;
|
|
32
33
|
}
|
|
33
34
|
function clientHeaders(request) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
"connection",
|
|
37
|
-
"cookie",
|
|
38
|
-
"host",
|
|
39
|
-
"origin",
|
|
40
|
-
"proxy-authorization",
|
|
41
|
-
"sec-websocket-extensions",
|
|
42
|
-
"sec-websocket-key",
|
|
43
|
-
"sec-websocket-protocol",
|
|
44
|
-
"sec-websocket-version",
|
|
45
|
-
"upgrade",
|
|
46
|
-
"x-api-key",
|
|
47
|
-
"x-goog-api-key",
|
|
48
|
-
]);
|
|
49
|
-
return Object.fromEntries(Object.entries(request.headers).flatMap(([name, value]) => value == null || local.has(name)
|
|
50
|
-
? []
|
|
51
|
-
: [[name, Array.isArray(value) ? value.join(", ") : String(value)]]));
|
|
35
|
+
const headers = Object.fromEntries(Object.entries(request.headers).flatMap(([name, value]) => value == null ? [] : [[name, Array.isArray(value) ? value.join(", ") : String(value)]]));
|
|
36
|
+
return Object.fromEntries(proxyRequestHeaders(headers));
|
|
52
37
|
}
|
|
53
38
|
/** Register a native Realtime WebSocket tunnel for providers that expose one. */
|
|
54
39
|
export function registerRealtimeProxy(app, auth, authenticate) {
|
|
@@ -116,9 +101,10 @@ export function registerRealtimeProxy(app, auth, authenticate) {
|
|
|
116
101
|
?.split(",")
|
|
117
102
|
.map((value) => value.trim())
|
|
118
103
|
.filter(Boolean);
|
|
104
|
+
const options = { headers: upstreamHeaders(authorized), handshakeTimeout: 30_000 };
|
|
119
105
|
upstream = protocols?.length
|
|
120
|
-
? new WebSocket(target, protocols,
|
|
121
|
-
: new WebSocket(target,
|
|
106
|
+
? new WebSocket(target, protocols, options)
|
|
107
|
+
: new WebSocket(target, options);
|
|
122
108
|
upstream.on("open", () => {
|
|
123
109
|
for (const item of pending)
|
|
124
110
|
upstream.send(item.data, { binary: item.binary });
|