@modelprofile.com/flexharness-providers 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts_media_openai/image.d.ts +46 -0
- package/dist_ts_media_openai/image.js +110 -0
- package/dist_ts_media_openai/index.d.ts +10 -0
- package/dist_ts_media_openai/index.js +16 -0
- package/dist_ts_media_openai/plugins.d.ts +3 -0
- package/dist_ts_media_openai/plugins.js +4 -0
- package/dist_ts_openai_account/index.d.ts +6 -0
- package/dist_ts_openai_account/index.js +7 -0
- package/dist_ts_openai_account/plugins.d.ts +7 -0
- package/dist_ts_openai_account/plugins.js +6 -0
- package/dist_ts_openai_account/smartai.providers.credentials.d.ts +7 -0
- package/dist_ts_openai_account/smartai.providers.credentials.js +126 -0
- package/dist_ts_openai_account/smartai.providers.error.d.ts +11 -0
- package/dist_ts_openai_account/smartai.providers.error.js +30 -0
- package/dist_ts_openai_account/smartai.providers.interfaces.d.ts +161 -0
- package/dist_ts_openai_account/smartai.providers.interfaces.js +2 -0
- package/dist_ts_openai_account/smartai.providers.openai.d.ts +29 -0
- package/dist_ts_openai_account/smartai.providers.openai.js +1075 -0
- package/dist_ts_openai_account/smartai.providers.registry.d.ts +11 -0
- package/dist_ts_openai_account/smartai.providers.registry.js +53 -0
- package/dist_ts_openai_auth/index.d.ts +6 -0
- package/dist_ts_openai_auth/index.js +10 -0
- package/dist_ts_openai_auth/interfaces.d.ts +71 -0
- package/dist_ts_openai_auth/interfaces.js +2 -0
- package/dist_ts_openai_auth/smartai.auth.openai.d.ts +34 -0
- package/dist_ts_openai_auth/smartai.auth.openai.js +603 -0
- package/dist_ts_openai_auth/smartai.middleware.openai.d.ts +6 -0
- package/dist_ts_openai_auth/smartai.middleware.openai.js +47 -0
- package/dist_ts_openai_auth_files/index.d.ts +43 -0
- package/dist_ts_openai_auth_files/index.js +377 -0
- package/dist_ts_openai_auth_files/plugins.d.ts +9 -0
- package/dist_ts_openai_auth_files/plugins.js +10 -0
- package/dist_ts_providers/anthropic/index.d.ts +8 -0
- package/dist_ts_providers/anthropic/index.js +13 -0
- package/dist_ts_providers/anthropic/plugins.d.ts +4 -0
- package/dist_ts_providers/anthropic/plugins.js +4 -0
- package/dist_ts_providers/anthropic/smartai.middleware.anthropic.d.ts +8 -0
- package/dist_ts_providers/anthropic/smartai.middleware.anthropic.js +10 -0
- package/dist_ts_providers/google/index.d.ts +5 -0
- package/dist_ts_providers/google/index.js +6 -0
- package/dist_ts_providers/google/plugins.d.ts +3 -0
- package/dist_ts_providers/google/plugins.js +3 -0
- package/dist_ts_providers/groq/index.d.ts +5 -0
- package/dist_ts_providers/groq/index.js +6 -0
- package/dist_ts_providers/groq/plugins.d.ts +3 -0
- package/dist_ts_providers/groq/plugins.js +3 -0
- package/dist_ts_providers/index.d.ts +8 -0
- package/dist_ts_providers/index.js +9 -0
- package/dist_ts_providers/mistral/index.d.ts +5 -0
- package/dist_ts_providers/mistral/index.js +6 -0
- package/dist_ts_providers/mistral/plugins.d.ts +3 -0
- package/dist_ts_providers/mistral/plugins.js +3 -0
- package/dist_ts_providers/ollama/index.d.ts +6 -0
- package/dist_ts_providers/ollama/index.js +7 -0
- package/dist_ts_providers/ollama/interfaces.d.ts +26 -0
- package/dist_ts_providers/ollama/interfaces.js +2 -0
- package/dist_ts_providers/ollama/smartai.provider.ollama.d.ts +8 -0
- package/dist_ts_providers/ollama/smartai.provider.ollama.js +378 -0
- package/dist_ts_providers/openai/index.d.ts +15 -0
- package/dist_ts_providers/openai/index.js +14 -0
- package/dist_ts_providers/openai/plugins.d.ts +6 -0
- package/dist_ts_providers/openai/plugins.js +4 -0
- package/dist_ts_providers/perplexity/index.d.ts +5 -0
- package/dist_ts_providers/perplexity/index.js +6 -0
- package/dist_ts_providers/perplexity/plugins.d.ts +3 -0
- package/dist_ts_providers/perplexity/plugins.js +3 -0
- package/dist_ts_providers/xai/index.d.ts +5 -0
- package/dist_ts_providers/xai/index.js +6 -0
- package/dist_ts_providers/xai/plugins.d.ts +3 -0
- package/dist_ts_providers/xai/plugins.js +3 -0
- package/dist_ts_research/index.d.ts +19 -0
- package/dist_ts_research/index.js +98 -0
- package/dist_ts_research/plugins.d.ts +2 -0
- package/dist_ts_research/plugins.js +3 -0
- package/license.md +21 -0
- package/openai-codex-license.txt +201 -0
- package/package.json +105 -0
- package/readme.md +62 -0
- package/third-party-notices.md +19 -0
- package/ts_media_openai/image.ts +147 -0
- package/ts_media_openai/index.ts +26 -0
- package/ts_media_openai/plugins.ts +3 -0
- package/ts_media_openai/readme.md +37 -0
- package/ts_openai_account/index.ts +11 -0
- package/ts_openai_account/plugins.ts +9 -0
- package/ts_openai_account/readme.md +37 -0
- package/ts_openai_account/smartai.providers.credentials.ts +163 -0
- package/ts_openai_account/smartai.providers.error.ts +49 -0
- package/ts_openai_account/smartai.providers.interfaces.ts +222 -0
- package/ts_openai_account/smartai.providers.openai.ts +1209 -0
- package/ts_openai_account/smartai.providers.registry.ts +57 -0
- package/ts_openai_auth/index.ts +12 -0
- package/ts_openai_auth/interfaces.ts +89 -0
- package/ts_openai_auth/readme.md +37 -0
- package/ts_openai_auth/smartai.auth.openai.ts +789 -0
- package/ts_openai_auth/smartai.middleware.openai.ts +55 -0
- package/ts_openai_auth_files/index.ts +499 -0
- package/ts_openai_auth_files/plugins.ts +13 -0
- package/ts_openai_auth_files/readme.md +37 -0
- package/ts_providers/anthropic/index.ts +20 -0
- package/ts_providers/anthropic/plugins.ts +4 -0
- package/ts_providers/anthropic/readme.md +37 -0
- package/ts_providers/anthropic/smartai.middleware.anthropic.ts +12 -0
- package/ts_providers/google/index.ts +8 -0
- package/ts_providers/google/plugins.ts +3 -0
- package/ts_providers/google/readme.md +37 -0
- package/ts_providers/groq/index.ts +8 -0
- package/ts_providers/groq/plugins.ts +3 -0
- package/ts_providers/groq/readme.md +37 -0
- package/ts_providers/index.ts +8 -0
- package/ts_providers/mistral/index.ts +8 -0
- package/ts_providers/mistral/plugins.ts +3 -0
- package/ts_providers/mistral/readme.md +37 -0
- package/ts_providers/ollama/index.ts +8 -0
- package/ts_providers/ollama/interfaces.ts +28 -0
- package/ts_providers/ollama/readme.md +37 -0
- package/ts_providers/ollama/smartai.provider.ollama.ts +426 -0
- package/ts_providers/openai/index.ts +29 -0
- package/ts_providers/openai/plugins.ts +6 -0
- package/ts_providers/openai/readme.md +37 -0
- package/ts_providers/perplexity/index.ts +8 -0
- package/ts_providers/perplexity/plugins.ts +3 -0
- package/ts_providers/perplexity/readme.md +37 -0
- package/ts_providers/readme.md +62 -0
- package/ts_providers/xai/index.ts +8 -0
- package/ts_providers/xai/plugins.ts +3 -0
- package/ts_providers/xai/readme.md +37 -0
- package/ts_research/index.ts +120 -0
- package/ts_research/plugins.ts +2 -0
- package/ts_research/readme.md +37 -0
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
// OpenAI protocol portions are adapted in TypeScript; see third-party-notices.md.
|
|
2
|
+
import * as plugins from './plugins.js';
|
|
3
|
+
import {
|
|
4
|
+
OPENAI_CHATGPT_CODEX_BASE_URL,
|
|
5
|
+
OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
|
|
6
|
+
completeOpenAiChatGptDeviceCodeLogin,
|
|
7
|
+
createOpenAiChatGptBrowserAuthorization,
|
|
8
|
+
exchangeOpenAiChatGptBrowserAuthorizationCode,
|
|
9
|
+
parseOpenAiChatGptBrowserCallback,
|
|
10
|
+
parseOpenAiChatGptTokenInfo,
|
|
11
|
+
refreshOpenAiChatGptTokenData,
|
|
12
|
+
requestOpenAiChatGptDeviceCode,
|
|
13
|
+
revokeOpenAiChatGptToken,
|
|
14
|
+
} from '@modelprofile.com/flexharness-providers/auth';
|
|
15
|
+
import { createSmartAiProviderError, SmartAiProviderError } from './smartai.providers.error.js';
|
|
16
|
+
import type {
|
|
17
|
+
IOpenAiChatGptOAuthCredential,
|
|
18
|
+
IOpenAiProviderAdapterOptions,
|
|
19
|
+
ISmartAiProviderAccountSummary,
|
|
20
|
+
ISmartAiProviderAccountRateLimits,
|
|
21
|
+
ISmartAiProviderAdapter,
|
|
22
|
+
ISmartAiProviderApiKeyCredential,
|
|
23
|
+
ISmartAiProviderDescriptor,
|
|
24
|
+
ISmartAiProviderLoginHandle,
|
|
25
|
+
ISmartAiProviderLoginOptions,
|
|
26
|
+
ISmartAiProviderLoginResult,
|
|
27
|
+
ISmartAiProviderModel,
|
|
28
|
+
ISmartAiProviderModelListOptions,
|
|
29
|
+
ISmartAiProviderOperationOptions,
|
|
30
|
+
ISmartAiProviderRateLimitDetails,
|
|
31
|
+
ISmartAiProviderRateLimitWindow,
|
|
32
|
+
TSmartAiProviderCancelStatus,
|
|
33
|
+
TSmartAiProviderAccountRateLimitsResult,
|
|
34
|
+
TSmartAiProviderCredential,
|
|
35
|
+
TSmartAiProviderModelListResult,
|
|
36
|
+
TSmartAiProviderRefreshResult,
|
|
37
|
+
} from './smartai.providers.interfaces.js';
|
|
38
|
+
import type { IOpenAiChatGptTokenData } from '@modelprofile.com/flexharness-providers/auth';
|
|
39
|
+
|
|
40
|
+
const defaultBrowserLoginTimeoutMs = 11 * 60 * 1000;
|
|
41
|
+
const defaultDeviceLoginTimeoutMs = 15 * 60 * 1000;
|
|
42
|
+
const defaultOperationTimeoutMs = 30_000;
|
|
43
|
+
const maximumOperationTimeoutMs = 20 * 60 * 1000;
|
|
44
|
+
const maximumCatalogBytes = 2 * 1024 * 1024;
|
|
45
|
+
const maximumAccountRateLimitsBytes = 256 * 1024;
|
|
46
|
+
const maximumCatalogModels = 256;
|
|
47
|
+
const maximumAdditionalRateLimits = 128;
|
|
48
|
+
const maximumCursorBytes = 4096;
|
|
49
|
+
const maximumModelPageSize = 50;
|
|
50
|
+
const defaultClientVersion = '0.146.1';
|
|
51
|
+
const maximumHeaderBytes = 16 * 1024;
|
|
52
|
+
const maximumCallbackUrlBytes = 4096;
|
|
53
|
+
const accountRateLimitsUrl = 'https://chatgpt.com/backend-api/wham/usage';
|
|
54
|
+
|
|
55
|
+
interface IOperation {
|
|
56
|
+
controller: AbortController;
|
|
57
|
+
signal: AbortSignal;
|
|
58
|
+
readonly timedOut: boolean;
|
|
59
|
+
cleanup(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ISharedRefresh {
|
|
63
|
+
controller: AbortController;
|
|
64
|
+
promise: Promise<TSmartAiProviderRefreshResult>;
|
|
65
|
+
waiters: number;
|
|
66
|
+
settled: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface IBrowserCallbackServer {
|
|
70
|
+
port: number;
|
|
71
|
+
waitForCallback(
|
|
72
|
+
authorization: Awaited<ReturnType<typeof createOpenAiChatGptBrowserAuthorization>>,
|
|
73
|
+
): Promise<string>;
|
|
74
|
+
close(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ICursorPayload {
|
|
78
|
+
version: 1;
|
|
79
|
+
projectionVersion: 1;
|
|
80
|
+
digest: string;
|
|
81
|
+
includeHidden: boolean;
|
|
82
|
+
offset: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class ModelHttpError extends Error {
|
|
86
|
+
constructor(public readonly status: number) {
|
|
87
|
+
super('Model catalog request failed.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class AccountRateLimitsHttpError extends Error {
|
|
92
|
+
constructor(public readonly status: number) {
|
|
93
|
+
super('Account rate-limits request failed.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
98
|
+
typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
|
99
|
+
|
|
100
|
+
const boundedString = (
|
|
101
|
+
valueArg: unknown,
|
|
102
|
+
maximumBytesArg: number,
|
|
103
|
+
allowEmptyArg = false,
|
|
104
|
+
): string => {
|
|
105
|
+
if (
|
|
106
|
+
typeof valueArg !== 'string'
|
|
107
|
+
|| (!allowEmptyArg && valueArg.length === 0)
|
|
108
|
+
|| plugins.Buffer.byteLength(valueArg, 'utf8') > maximumBytesArg
|
|
109
|
+
) {
|
|
110
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
111
|
+
}
|
|
112
|
+
return valueArg;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const createOperation = (
|
|
116
|
+
optionsArg: ISmartAiProviderOperationOptions,
|
|
117
|
+
defaultTimeoutMsArg: number,
|
|
118
|
+
): IOperation => {
|
|
119
|
+
const timeoutMs = optionsArg.timeoutMs ?? defaultTimeoutMsArg;
|
|
120
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > maximumOperationTimeoutMs) {
|
|
121
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
122
|
+
}
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
let timedOut = false;
|
|
125
|
+
const onExternalAbort = (): void => controller.abort();
|
|
126
|
+
optionsArg.signal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
127
|
+
if (optionsArg.signal?.aborted) controller.abort();
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
timedOut = true;
|
|
130
|
+
controller.abort();
|
|
131
|
+
}, timeoutMs);
|
|
132
|
+
return {
|
|
133
|
+
controller,
|
|
134
|
+
signal: controller.signal,
|
|
135
|
+
get timedOut() {
|
|
136
|
+
return timedOut;
|
|
137
|
+
},
|
|
138
|
+
cleanup: () => {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
optionsArg.signal?.removeEventListener('abort', onExternalAbort);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const classifyOperationError = <TFallback extends
|
|
146
|
+
| 'ACCOUNT_RATE_LIMITS_FAILED'
|
|
147
|
+
| 'LOGIN_FAILED'
|
|
148
|
+
| 'MODEL_LIST_FAILED'
|
|
149
|
+
| 'REFRESH_FAILED'>(
|
|
150
|
+
errorArg: unknown,
|
|
151
|
+
operationArg: IOperation,
|
|
152
|
+
fallbackArg: TFallback,
|
|
153
|
+
): 'ABORTED' | 'TIMEOUT' | TFallback => {
|
|
154
|
+
if (operationArg.timedOut) return 'TIMEOUT';
|
|
155
|
+
if (operationArg.signal.aborted) return 'ABORTED';
|
|
156
|
+
if (errorArg instanceof SmartAiProviderError && errorArg.code === 'ABORTED') return 'ABORTED';
|
|
157
|
+
if (errorArg instanceof SmartAiProviderError && errorArg.code === 'TIMEOUT') return 'TIMEOUT';
|
|
158
|
+
return fallbackArg;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const toTokenData = (credentialArg: IOpenAiChatGptOAuthCredential): IOpenAiChatGptTokenData => {
|
|
162
|
+
if (
|
|
163
|
+
credentialArg.kind !== 'chatgptOAuth'
|
|
164
|
+
|| credentialArg.providerId !== 'openai'
|
|
165
|
+
|| !credentialArg.accessToken
|
|
166
|
+
|| !credentialArg.refreshToken
|
|
167
|
+
|| !credentialArg.idToken
|
|
168
|
+
) {
|
|
169
|
+
throw createSmartAiProviderError('CREDENTIAL_INVALID');
|
|
170
|
+
}
|
|
171
|
+
let tokenInfo;
|
|
172
|
+
try {
|
|
173
|
+
tokenInfo = parseOpenAiChatGptTokenInfo(credentialArg.idToken);
|
|
174
|
+
} catch {
|
|
175
|
+
throw createSmartAiProviderError('CREDENTIAL_INVALID');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
accessToken: credentialArg.accessToken,
|
|
179
|
+
refreshToken: credentialArg.refreshToken,
|
|
180
|
+
idToken: credentialArg.idToken,
|
|
181
|
+
accountId: tokenInfo.chatgptAccountId,
|
|
182
|
+
tokenInfo,
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const toCredential = (
|
|
187
|
+
tokenDataArg: IOpenAiChatGptTokenData,
|
|
188
|
+
lastRefreshAtArg?: string,
|
|
189
|
+
): IOpenAiChatGptOAuthCredential => {
|
|
190
|
+
if (!tokenDataArg.idToken) throw createSmartAiProviderError('CREDENTIAL_INVALID');
|
|
191
|
+
return {
|
|
192
|
+
kind: 'chatgptOAuth',
|
|
193
|
+
providerId: 'openai',
|
|
194
|
+
accessToken: tokenDataArg.accessToken,
|
|
195
|
+
refreshToken: tokenDataArg.refreshToken,
|
|
196
|
+
idToken: tokenDataArg.idToken,
|
|
197
|
+
...(lastRefreshAtArg === undefined ? {} : { lastRefreshAt: lastRefreshAtArg }),
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const toSummary = (
|
|
202
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
203
|
+
): ISmartAiProviderAccountSummary => {
|
|
204
|
+
const tokenData = toTokenData(credentialArg);
|
|
205
|
+
return {
|
|
206
|
+
providerId: 'openai',
|
|
207
|
+
authKind: 'chatgptOAuth',
|
|
208
|
+
...(tokenData.tokenInfo.chatgptAccountId
|
|
209
|
+
? { accountId: tokenData.tokenInfo.chatgptAccountId }
|
|
210
|
+
: {}),
|
|
211
|
+
...(tokenData.tokenInfo.email ? { email: tokenData.tokenInfo.email } : {}),
|
|
212
|
+
...(tokenData.tokenInfo.chatgptPlanType ? { plan: tokenData.tokenInfo.chatgptPlanType } : {}),
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const closeServer = async (
|
|
217
|
+
serverArg: plugins.Server,
|
|
218
|
+
socketsArg: Set<plugins.Socket>,
|
|
219
|
+
): Promise<void> => {
|
|
220
|
+
await new Promise<void>((resolve, reject) => {
|
|
221
|
+
if (!serverArg.listening) {
|
|
222
|
+
resolve();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
serverArg.close((errorArg) => {
|
|
226
|
+
if ((errorArg as NodeJS.ErrnoException | undefined)?.code === 'ERR_SERVER_NOT_RUNNING') {
|
|
227
|
+
resolve();
|
|
228
|
+
} else if (errorArg) {
|
|
229
|
+
reject(errorArg);
|
|
230
|
+
} else {
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
for (const socket of socketsArg) socket.destroy();
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const bindBrowserCallbackServer = async (
|
|
239
|
+
portsArg: readonly number[],
|
|
240
|
+
signalArg: AbortSignal,
|
|
241
|
+
): Promise<IBrowserCallbackServer> => {
|
|
242
|
+
if (
|
|
243
|
+
portsArg.length === 0
|
|
244
|
+
|| portsArg.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535)
|
|
245
|
+
) {
|
|
246
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
247
|
+
}
|
|
248
|
+
let lastError: unknown;
|
|
249
|
+
for (let index = 0; index < portsArg.length; index++) {
|
|
250
|
+
const port = portsArg[index];
|
|
251
|
+
const sockets = new Set<plugins.Socket>();
|
|
252
|
+
let authorization: Awaited<ReturnType<typeof createOpenAiChatGptBrowserAuthorization>> | undefined;
|
|
253
|
+
let settled = false;
|
|
254
|
+
let resolveCallback!: (url: string) => void;
|
|
255
|
+
let rejectCallback!: (error: SmartAiProviderError) => void;
|
|
256
|
+
const callback = new Promise<string>((resolve, reject) => {
|
|
257
|
+
resolveCallback = resolve;
|
|
258
|
+
rejectCallback = reject;
|
|
259
|
+
});
|
|
260
|
+
void callback.catch(() => undefined);
|
|
261
|
+
const server = plugins.http.createServer({ maxHeaderSize: maximumHeaderBytes }, (request, response) => {
|
|
262
|
+
const rejectRequest = (status: number): void => {
|
|
263
|
+
response.writeHead(status, {
|
|
264
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
265
|
+
'Cache-Control': 'no-store',
|
|
266
|
+
'X-Content-Type-Options': 'nosniff',
|
|
267
|
+
});
|
|
268
|
+
response.end('OpenAI login callback rejected.');
|
|
269
|
+
};
|
|
270
|
+
const host = request.headers.host;
|
|
271
|
+
const requestUrl = request.url;
|
|
272
|
+
const headerBytes = request.rawHeaders.reduce(
|
|
273
|
+
(total, entry) => total + plugins.Buffer.byteLength(entry, 'utf8'),
|
|
274
|
+
0,
|
|
275
|
+
);
|
|
276
|
+
if (
|
|
277
|
+
settled
|
|
278
|
+
|| !authorization
|
|
279
|
+
|| request.method !== 'GET'
|
|
280
|
+
|| host !== `localhost:${port}`
|
|
281
|
+
|| typeof requestUrl !== 'string'
|
|
282
|
+
|| plugins.Buffer.byteLength(requestUrl, 'utf8') > maximumCallbackUrlBytes
|
|
283
|
+
|| headerBytes > maximumHeaderBytes
|
|
284
|
+
) {
|
|
285
|
+
rejectRequest(settled ? 410 : 400);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const callbackUrl = `http://${host}${requestUrl}`;
|
|
289
|
+
let providerDenied = false;
|
|
290
|
+
try {
|
|
291
|
+
const callback = parseOpenAiChatGptBrowserCallback(callbackUrl, {
|
|
292
|
+
expectedState: authorization.state,
|
|
293
|
+
expectedRedirectUri: authorization.redirectUri,
|
|
294
|
+
});
|
|
295
|
+
providerDenied = 'providerError' in callback;
|
|
296
|
+
} catch {
|
|
297
|
+
rejectRequest(400);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
settled = true;
|
|
301
|
+
response.writeHead(200, {
|
|
302
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
303
|
+
'Cache-Control': 'no-store',
|
|
304
|
+
'X-Content-Type-Options': 'nosniff',
|
|
305
|
+
});
|
|
306
|
+
response.end(providerDenied
|
|
307
|
+
? 'OpenAI login was not authorized. You may close this window.'
|
|
308
|
+
: 'OpenAI login completed. You may close this window.');
|
|
309
|
+
resolveCallback(callbackUrl);
|
|
310
|
+
});
|
|
311
|
+
server.on('connection', (socket) => {
|
|
312
|
+
sockets.add(socket);
|
|
313
|
+
socket.once('close', () => sockets.delete(socket));
|
|
314
|
+
});
|
|
315
|
+
let serverClosePromise: Promise<void> | undefined;
|
|
316
|
+
const ensureServerClosed = (): Promise<void> => {
|
|
317
|
+
if (!serverClosePromise) {
|
|
318
|
+
serverClosePromise = closeServer(server, sockets).finally(() => {
|
|
319
|
+
serverClosePromise = undefined;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return serverClosePromise;
|
|
323
|
+
};
|
|
324
|
+
const onAbort = (): void => {
|
|
325
|
+
if (!settled) {
|
|
326
|
+
settled = true;
|
|
327
|
+
rejectCallback(createSmartAiProviderError('ABORTED'));
|
|
328
|
+
}
|
|
329
|
+
void ensureServerClosed().catch(() => undefined);
|
|
330
|
+
};
|
|
331
|
+
signalArg.addEventListener('abort', onAbort, { once: true });
|
|
332
|
+
try {
|
|
333
|
+
await new Promise<void>((resolve, reject) => {
|
|
334
|
+
const onError = (errorArg: Error): void => {
|
|
335
|
+
server.removeListener('listening', onListening);
|
|
336
|
+
reject(errorArg);
|
|
337
|
+
};
|
|
338
|
+
const onListening = (): void => {
|
|
339
|
+
server.removeListener('error', onError);
|
|
340
|
+
resolve();
|
|
341
|
+
};
|
|
342
|
+
server.once('error', onError);
|
|
343
|
+
server.once('listening', onListening);
|
|
344
|
+
server.listen(port, '127.0.0.1');
|
|
345
|
+
});
|
|
346
|
+
if (signalArg.aborted) onAbort();
|
|
347
|
+
return {
|
|
348
|
+
port,
|
|
349
|
+
waitForCallback: async (authorizationArg) => {
|
|
350
|
+
authorization = authorizationArg;
|
|
351
|
+
return callback;
|
|
352
|
+
},
|
|
353
|
+
close: async () => {
|
|
354
|
+
signalArg.removeEventListener('abort', onAbort);
|
|
355
|
+
if (!settled) {
|
|
356
|
+
settled = true;
|
|
357
|
+
rejectCallback(createSmartAiProviderError('LOGIN_CANCELED'));
|
|
358
|
+
}
|
|
359
|
+
await ensureServerClosed();
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
} catch (errorArg) {
|
|
363
|
+
signalArg.removeEventListener('abort', onAbort);
|
|
364
|
+
await ensureServerClosed().catch(() => undefined);
|
|
365
|
+
lastError = errorArg;
|
|
366
|
+
const code = (errorArg as NodeJS.ErrnoException).code;
|
|
367
|
+
if (code !== 'EADDRINUSE' || index === portsArg.length - 1) break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
throw createSmartAiProviderError(
|
|
371
|
+
(lastError as NodeJS.ErrnoException | undefined)?.code === 'EADDRINUSE'
|
|
372
|
+
? 'LOGIN_BUSY'
|
|
373
|
+
: 'LOGIN_FAILED',
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
class OpenAiProviderLoginHandle implements ISmartAiProviderLoginHandle {
|
|
378
|
+
public readonly completion: Promise<ISmartAiProviderLoginResult>;
|
|
379
|
+
private closePromise?: Promise<void>;
|
|
380
|
+
private done = false;
|
|
381
|
+
|
|
382
|
+
constructor(
|
|
383
|
+
public readonly prompt: ISmartAiProviderLoginHandle['prompt'],
|
|
384
|
+
completionArg: Promise<ISmartAiProviderLoginResult>,
|
|
385
|
+
private readonly abortController: AbortController,
|
|
386
|
+
private readonly cleanup: () => Promise<void>,
|
|
387
|
+
private readonly onDone: () => void,
|
|
388
|
+
) {
|
|
389
|
+
this.completion = completionArg.finally(async () => {
|
|
390
|
+
this.done = true;
|
|
391
|
+
try {
|
|
392
|
+
await this.cleanup();
|
|
393
|
+
} finally {
|
|
394
|
+
this.onDone();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
void this.completion.catch(() => undefined);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
public async cancel(): Promise<TSmartAiProviderCancelStatus> {
|
|
401
|
+
if (this.done || this.abortController.signal.aborted) return 'notFound';
|
|
402
|
+
this.abortController.abort();
|
|
403
|
+
return 'canceled';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
public async close(): Promise<void> {
|
|
407
|
+
if (this.closePromise) return this.closePromise;
|
|
408
|
+
const closeAttempt = (async () => {
|
|
409
|
+
await this.cancel();
|
|
410
|
+
await this.completion.catch(() => undefined);
|
|
411
|
+
await this.cleanup();
|
|
412
|
+
})();
|
|
413
|
+
this.closePromise = closeAttempt;
|
|
414
|
+
try {
|
|
415
|
+
await closeAttempt;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
this.closePromise = undefined;
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const normalizeModelListOptions = (
|
|
424
|
+
optionsArg: ISmartAiProviderModelListOptions,
|
|
425
|
+
): Required<Pick<ISmartAiProviderModelListOptions, 'limit' | 'includeHidden'>>
|
|
426
|
+
& ISmartAiProviderModelListOptions => {
|
|
427
|
+
const limit = optionsArg.limit ?? maximumModelPageSize;
|
|
428
|
+
if (
|
|
429
|
+
!Number.isSafeInteger(limit)
|
|
430
|
+
|| limit < 1
|
|
431
|
+
|| limit > maximumModelPageSize
|
|
432
|
+
|| (optionsArg.cursor !== undefined
|
|
433
|
+
&& (optionsArg.cursor.length === 0
|
|
434
|
+
|| plugins.Buffer.byteLength(optionsArg.cursor, 'utf8') > maximumCursorBytes))
|
|
435
|
+
|| (optionsArg.includeHidden !== undefined && typeof optionsArg.includeHidden !== 'boolean')
|
|
436
|
+
) {
|
|
437
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
438
|
+
}
|
|
439
|
+
return { ...optionsArg, limit, includeHidden: optionsArg.includeHidden ?? false };
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const readBoundedJson = async (
|
|
443
|
+
responseArg: Response,
|
|
444
|
+
signalArg: AbortSignal,
|
|
445
|
+
maximumBytesArg: number,
|
|
446
|
+
): Promise<unknown> => {
|
|
447
|
+
const declared = responseArg.headers.get('content-length');
|
|
448
|
+
if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > maximumBytesArg)) {
|
|
449
|
+
await responseArg.body?.cancel().catch(() => undefined);
|
|
450
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
451
|
+
}
|
|
452
|
+
if (!responseArg.body) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
453
|
+
const reader = responseArg.body.getReader();
|
|
454
|
+
const chunks: Uint8Array[] = [];
|
|
455
|
+
let size = 0;
|
|
456
|
+
let completed = false;
|
|
457
|
+
const onAbort = (): void => {
|
|
458
|
+
void reader.cancel().catch(() => undefined);
|
|
459
|
+
};
|
|
460
|
+
signalArg.addEventListener('abort', onAbort, { once: true });
|
|
461
|
+
if (signalArg.aborted) onAbort();
|
|
462
|
+
try {
|
|
463
|
+
while (true) {
|
|
464
|
+
if (signalArg.aborted) throw createSmartAiProviderError('ABORTED');
|
|
465
|
+
const { done, value } = await reader.read();
|
|
466
|
+
if (signalArg.aborted) throw createSmartAiProviderError('ABORTED');
|
|
467
|
+
if (done) break;
|
|
468
|
+
size += value.byteLength;
|
|
469
|
+
if (size > maximumBytesArg) {
|
|
470
|
+
await reader.cancel().catch(() => undefined);
|
|
471
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
472
|
+
}
|
|
473
|
+
chunks.push(value);
|
|
474
|
+
}
|
|
475
|
+
completed = true;
|
|
476
|
+
} finally {
|
|
477
|
+
signalArg.removeEventListener('abort', onAbort);
|
|
478
|
+
if (!completed) void reader.cancel().catch(() => undefined);
|
|
479
|
+
reader.releaseLock();
|
|
480
|
+
}
|
|
481
|
+
const body = plugins.Buffer.allocUnsafe(size);
|
|
482
|
+
let offset = 0;
|
|
483
|
+
for (const chunk of chunks) {
|
|
484
|
+
body.set(chunk, offset);
|
|
485
|
+
offset += chunk.byteLength;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
return JSON.parse(body.toString('utf8')) as unknown;
|
|
489
|
+
} catch {
|
|
490
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const boundedInteger = (
|
|
495
|
+
valueArg: unknown,
|
|
496
|
+
minimumArg: number,
|
|
497
|
+
maximumArg = Number.MAX_SAFE_INTEGER,
|
|
498
|
+
): number => {
|
|
499
|
+
if (
|
|
500
|
+
!Number.isSafeInteger(valueArg)
|
|
501
|
+
|| (valueArg as number) < minimumArg
|
|
502
|
+
|| (valueArg as number) > maximumArg
|
|
503
|
+
) {
|
|
504
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
505
|
+
}
|
|
506
|
+
return valueArg as number;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const projectRateLimitWindow = (
|
|
510
|
+
valueArg: unknown,
|
|
511
|
+
): ISmartAiProviderRateLimitWindow => {
|
|
512
|
+
if (!isRecord(valueArg)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
513
|
+
return {
|
|
514
|
+
usedPercent: boundedInteger(valueArg.used_percent, 0, 100),
|
|
515
|
+
limitWindowSeconds: boundedInteger(valueArg.limit_window_seconds, 1),
|
|
516
|
+
resetAfterSeconds: boundedInteger(valueArg.reset_after_seconds, 0),
|
|
517
|
+
resetsAt: boundedInteger(valueArg.reset_at, 0),
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const projectRateLimitDetails = (valueArg: unknown): ISmartAiProviderRateLimitDetails => {
|
|
522
|
+
if (
|
|
523
|
+
!isRecord(valueArg)
|
|
524
|
+
|| typeof valueArg.allowed !== 'boolean'
|
|
525
|
+
|| typeof valueArg.limit_reached !== 'boolean'
|
|
526
|
+
) {
|
|
527
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
allowed: valueArg.allowed,
|
|
531
|
+
limitReached: valueArg.limit_reached,
|
|
532
|
+
...(valueArg.primary_window === null || valueArg.primary_window === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { primaryWindow: projectRateLimitWindow(valueArg.primary_window) }),
|
|
535
|
+
...(valueArg.secondary_window === null || valueArg.secondary_window === undefined
|
|
536
|
+
? {}
|
|
537
|
+
: { secondaryWindow: projectRateLimitWindow(valueArg.secondary_window) }),
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const projectAccountRateLimits = (
|
|
542
|
+
valueArg: unknown,
|
|
543
|
+
observedAtArg: string,
|
|
544
|
+
): ISmartAiProviderAccountRateLimits => {
|
|
545
|
+
if (!isRecord(valueArg)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
546
|
+
const rawAdditionalRateLimits = valueArg.additional_rate_limits ?? [];
|
|
547
|
+
if (
|
|
548
|
+
!Array.isArray(rawAdditionalRateLimits)
|
|
549
|
+
|| rawAdditionalRateLimits.length > maximumAdditionalRateLimits
|
|
550
|
+
) {
|
|
551
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
552
|
+
}
|
|
553
|
+
const additionalRateLimits = rawAdditionalRateLimits.map((rawLimit) => {
|
|
554
|
+
if (!isRecord(rawLimit)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
555
|
+
return {
|
|
556
|
+
limitName: boundedString(rawLimit.limit_name, 256),
|
|
557
|
+
meteredFeature: boundedString(rawLimit.metered_feature, 256),
|
|
558
|
+
...(rawLimit.rate_limit === null || rawLimit.rate_limit === undefined
|
|
559
|
+
? {}
|
|
560
|
+
: { rateLimit: projectRateLimitDetails(rawLimit.rate_limit) }),
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
let rateLimitReachedType: string | undefined;
|
|
564
|
+
if (valueArg.rate_limit_reached_type !== null && valueArg.rate_limit_reached_type !== undefined) {
|
|
565
|
+
if (!isRecord(valueArg.rate_limit_reached_type)) {
|
|
566
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
567
|
+
}
|
|
568
|
+
rateLimitReachedType = boundedString(valueArg.rate_limit_reached_type.type, 256);
|
|
569
|
+
}
|
|
570
|
+
let rateLimitResetCreditsAvailableCount: number | undefined;
|
|
571
|
+
if (valueArg.rate_limit_reset_credits !== null && valueArg.rate_limit_reset_credits !== undefined) {
|
|
572
|
+
if (!isRecord(valueArg.rate_limit_reset_credits)) {
|
|
573
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
574
|
+
}
|
|
575
|
+
rateLimitResetCreditsAvailableCount = boundedInteger(
|
|
576
|
+
valueArg.rate_limit_reset_credits.available_count,
|
|
577
|
+
0,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
providerId: 'openai',
|
|
582
|
+
plan: boundedString(valueArg.plan_type, 256),
|
|
583
|
+
observedAt: observedAtArg,
|
|
584
|
+
...(valueArg.rate_limit === null || valueArg.rate_limit === undefined
|
|
585
|
+
? {}
|
|
586
|
+
: { rateLimit: projectRateLimitDetails(valueArg.rate_limit) }),
|
|
587
|
+
additionalRateLimits,
|
|
588
|
+
...(rateLimitReachedType === undefined ? {} : { rateLimitReachedType }),
|
|
589
|
+
...(rateLimitResetCreditsAvailableCount === undefined
|
|
590
|
+
? {}
|
|
591
|
+
: { rateLimitResetCreditsAvailableCount }),
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const projectCatalog = (valueArg: unknown): ISmartAiProviderModel[] => {
|
|
596
|
+
if (!isRecord(valueArg) || !Array.isArray(valueArg.models) || valueArg.models.length > maximumCatalogModels) {
|
|
597
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
598
|
+
}
|
|
599
|
+
const modelIds = new Set<string>();
|
|
600
|
+
const entries = valueArg.models.map((rawModel, sourceIndex) => {
|
|
601
|
+
if (!isRecord(rawModel)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
602
|
+
const modelId = boundedString(rawModel.slug, 256);
|
|
603
|
+
if (modelIds.has(modelId)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
604
|
+
modelIds.add(modelId);
|
|
605
|
+
const visibility = rawModel.visibility;
|
|
606
|
+
if (visibility !== 'list' && visibility !== 'hide' && visibility !== 'none') {
|
|
607
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
608
|
+
}
|
|
609
|
+
if (!Number.isSafeInteger(rawModel.priority)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
610
|
+
if (!Array.isArray(rawModel.supported_reasoning_levels) || rawModel.supported_reasoning_levels.length > 16) {
|
|
611
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
612
|
+
}
|
|
613
|
+
const reasoningEfforts = rawModel.supported_reasoning_levels.map((rawEffort) => {
|
|
614
|
+
if (!isRecord(rawEffort)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
615
|
+
return {
|
|
616
|
+
effort: boundedString(rawEffort.effort, 128),
|
|
617
|
+
description: boundedString(rawEffort.description, 1024, true),
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
const modalities = rawModel.input_modalities ?? ['text', 'image'];
|
|
621
|
+
if (
|
|
622
|
+
!Array.isArray(modalities)
|
|
623
|
+
|| modalities.length > 3
|
|
624
|
+
|| modalities.some((entry) => entry !== 'text' && entry !== 'image' && entry !== 'audio')
|
|
625
|
+
|| new Set(modalities).size !== modalities.length
|
|
626
|
+
) {
|
|
627
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
628
|
+
}
|
|
629
|
+
const rawServiceTiers = rawModel.service_tiers ?? [];
|
|
630
|
+
if (!Array.isArray(rawServiceTiers) || rawServiceTiers.length > 16) {
|
|
631
|
+
throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
632
|
+
}
|
|
633
|
+
const serviceTiers = rawServiceTiers.map((rawTier) => {
|
|
634
|
+
if (!isRecord(rawTier)) throw createSmartAiProviderError('PROTOCOL_ERROR');
|
|
635
|
+
return {
|
|
636
|
+
id: boundedString(rawTier.id, 128),
|
|
637
|
+
name: boundedString(rawTier.name, 256),
|
|
638
|
+
description: boundedString(rawTier.description, 1024, true),
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
const modelMessages = isRecord(rawModel.model_messages) ? rawModel.model_messages : undefined;
|
|
642
|
+
const variables = isRecord(modelMessages?.instructions_variables)
|
|
643
|
+
? modelMessages.instructions_variables
|
|
644
|
+
: undefined;
|
|
645
|
+
const supportsPersonality = typeof modelMessages?.instructions_template === 'string'
|
|
646
|
+
&& modelMessages.instructions_template.includes('{{ personality }}')
|
|
647
|
+
&& typeof variables?.personality_default === 'string'
|
|
648
|
+
&& typeof variables.personality_friendly === 'string'
|
|
649
|
+
&& typeof variables.personality_pragmatic === 'string';
|
|
650
|
+
return {
|
|
651
|
+
priority: rawModel.priority as number,
|
|
652
|
+
sourceIndex,
|
|
653
|
+
model: {
|
|
654
|
+
providerId: 'openai' as const,
|
|
655
|
+
modelId,
|
|
656
|
+
wireModel: modelId,
|
|
657
|
+
displayName: boundedString(rawModel.display_name, 512),
|
|
658
|
+
description: rawModel.description === null || rawModel.description === undefined
|
|
659
|
+
? ''
|
|
660
|
+
: boundedString(rawModel.description, 4096, true),
|
|
661
|
+
hidden: visibility !== 'list',
|
|
662
|
+
reasoningEfforts,
|
|
663
|
+
defaultReasoningEffort: rawModel.default_reasoning_level === null
|
|
664
|
+
|| rawModel.default_reasoning_level === undefined
|
|
665
|
+
? 'none'
|
|
666
|
+
: boundedString(rawModel.default_reasoning_level, 128),
|
|
667
|
+
inputModalities: modalities as ISmartAiProviderModel['inputModalities'],
|
|
668
|
+
supportsPersonality,
|
|
669
|
+
serviceTiers,
|
|
670
|
+
...(rawModel.default_service_tier === null || rawModel.default_service_tier === undefined
|
|
671
|
+
? {}
|
|
672
|
+
: { defaultServiceTier: boundedString(rawModel.default_service_tier, 128) }),
|
|
673
|
+
isDefault: false,
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
entries.sort((left, right) => left.priority - right.priority || left.sourceIndex - right.sourceIndex);
|
|
678
|
+
const models = entries.map((entry) => entry.model);
|
|
679
|
+
const defaultModel = models.find((model) => !model.hidden) ?? models[0];
|
|
680
|
+
if (defaultModel) defaultModel.isDefault = true;
|
|
681
|
+
return models;
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
685
|
+
public readonly descriptor: ISmartAiProviderDescriptor = {
|
|
686
|
+
providerId: 'openai',
|
|
687
|
+
displayName: 'OpenAI',
|
|
688
|
+
loginFlows: ['browser', 'device'],
|
|
689
|
+
supportsApiKey: true,
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
private readonly fetchImplementation: typeof fetch;
|
|
693
|
+
private readonly originator: string;
|
|
694
|
+
private readonly now: () => Date;
|
|
695
|
+
private readonly cursorKey = plugins.crypto.randomBytes(32);
|
|
696
|
+
private readonly handles = new Set<OpenAiProviderLoginHandle>();
|
|
697
|
+
private readonly operations = new Set<AbortController>();
|
|
698
|
+
private readonly refreshes = new Map<string, ISharedRefresh>();
|
|
699
|
+
private closed = false;
|
|
700
|
+
private disposePromise?: Promise<void>;
|
|
701
|
+
|
|
702
|
+
constructor(optionsArg: IOpenAiProviderAdapterOptions = {}) {
|
|
703
|
+
this.fetchImplementation = optionsArg.fetch ?? globalThis.fetch;
|
|
704
|
+
if (!this.fetchImplementation) throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
705
|
+
this.originator = optionsArg.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR;
|
|
706
|
+
this.now = optionsArg.now ?? (() => new Date());
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private requireOpen(): void {
|
|
710
|
+
if (this.closed) throw createSmartAiProviderError('CLOSED');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private authOptions(signalArg: AbortSignal) {
|
|
714
|
+
return {
|
|
715
|
+
fetch: this.fetchImplementation,
|
|
716
|
+
signal: signalArg,
|
|
717
|
+
safeErrors: true,
|
|
718
|
+
} as const;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
public createApiKeyCredential(apiKeyArg: string): ISmartAiProviderApiKeyCredential {
|
|
722
|
+
this.requireOpen();
|
|
723
|
+
if (typeof apiKeyArg !== 'string' || apiKeyArg.length === 0) {
|
|
724
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
725
|
+
}
|
|
726
|
+
return { kind: 'apiKey', providerId: 'openai', apiKey: apiKeyArg };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
public inspectCredential(
|
|
730
|
+
credentialArg: TSmartAiProviderCredential,
|
|
731
|
+
): ISmartAiProviderAccountSummary {
|
|
732
|
+
this.requireOpen();
|
|
733
|
+
if (credentialArg.kind === 'apiKey') {
|
|
734
|
+
if (credentialArg.providerId !== 'openai' || !credentialArg.apiKey) {
|
|
735
|
+
throw createSmartAiProviderError('CREDENTIAL_INVALID');
|
|
736
|
+
}
|
|
737
|
+
return { providerId: 'openai', authKind: 'apiKey' };
|
|
738
|
+
}
|
|
739
|
+
return toSummary(credentialArg);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
public async beginLogin(
|
|
743
|
+
optionsArg: ISmartAiProviderLoginOptions,
|
|
744
|
+
): Promise<ISmartAiProviderLoginHandle> {
|
|
745
|
+
this.requireOpen();
|
|
746
|
+
if (optionsArg.flow !== 'browser' && optionsArg.flow !== 'device') {
|
|
747
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
748
|
+
}
|
|
749
|
+
const defaultTimeoutMs = optionsArg.flow === 'browser'
|
|
750
|
+
? defaultBrowserLoginTimeoutMs
|
|
751
|
+
: defaultDeviceLoginTimeoutMs;
|
|
752
|
+
if (
|
|
753
|
+
optionsArg.flow === 'device'
|
|
754
|
+
&& (optionsArg.timeoutMs ?? defaultTimeoutMs) > defaultDeviceLoginTimeoutMs
|
|
755
|
+
) {
|
|
756
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
757
|
+
}
|
|
758
|
+
const operation = createOperation(optionsArg, defaultTimeoutMs);
|
|
759
|
+
const abortController = operation.controller;
|
|
760
|
+
this.operations.add(abortController);
|
|
761
|
+
const timeoutMs = optionsArg.timeoutMs ?? defaultTimeoutMs;
|
|
762
|
+
let callbackServer: IBrowserCallbackServer | undefined;
|
|
763
|
+
const cleanup = async (): Promise<void> => {
|
|
764
|
+
this.operations.delete(abortController);
|
|
765
|
+
operation.cleanup();
|
|
766
|
+
await callbackServer?.close();
|
|
767
|
+
};
|
|
768
|
+
try {
|
|
769
|
+
let prompt: ISmartAiProviderLoginHandle['prompt'];
|
|
770
|
+
let completion: Promise<ISmartAiProviderLoginResult>;
|
|
771
|
+
if (optionsArg.flow === 'device') {
|
|
772
|
+
const deviceCode = await requestOpenAiChatGptDeviceCode(this.authOptions(abortController.signal));
|
|
773
|
+
prompt = {
|
|
774
|
+
flow: 'device',
|
|
775
|
+
verificationUrl: deviceCode.verificationUrl,
|
|
776
|
+
userCode: deviceCode.userCode,
|
|
777
|
+
};
|
|
778
|
+
completion = completeOpenAiChatGptDeviceCodeLogin(deviceCode, {
|
|
779
|
+
...this.authOptions(abortController.signal),
|
|
780
|
+
timeoutMs,
|
|
781
|
+
}).then((tokenData) => {
|
|
782
|
+
const credential = toCredential(tokenData);
|
|
783
|
+
return { credential, account: toSummary(credential) };
|
|
784
|
+
});
|
|
785
|
+
} else {
|
|
786
|
+
callbackServer = await bindBrowserCallbackServer([1455, 1457], abortController.signal);
|
|
787
|
+
const authorization = await createOpenAiChatGptBrowserAuthorization({
|
|
788
|
+
...this.authOptions(abortController.signal),
|
|
789
|
+
redirectUri: `http://localhost:${callbackServer.port}/auth/callback`,
|
|
790
|
+
originator: this.originator,
|
|
791
|
+
});
|
|
792
|
+
prompt = { flow: 'browser', authUrl: authorization.authUrl };
|
|
793
|
+
completion = callbackServer.waitForCallback(authorization).then(async (callbackUrl) => {
|
|
794
|
+
const callback = parseOpenAiChatGptBrowserCallback(callbackUrl, {
|
|
795
|
+
expectedState: authorization.state,
|
|
796
|
+
expectedRedirectUri: authorization.redirectUri,
|
|
797
|
+
});
|
|
798
|
+
if ('providerError' in callback) {
|
|
799
|
+
throw createSmartAiProviderError('LOGIN_CANCELED');
|
|
800
|
+
}
|
|
801
|
+
const tokenData = await exchangeOpenAiChatGptBrowserAuthorizationCode(
|
|
802
|
+
callback.authorizationCode,
|
|
803
|
+
{
|
|
804
|
+
...this.authOptions(abortController.signal),
|
|
805
|
+
redirectUri: authorization.redirectUri,
|
|
806
|
+
codeVerifier: authorization.codeVerifier,
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
const credential = toCredential(tokenData);
|
|
810
|
+
return { credential, account: toSummary(credential) };
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
this.requireOpen();
|
|
814
|
+
if (abortController.signal.aborted) throw createSmartAiProviderError('ABORTED');
|
|
815
|
+
const handle = new OpenAiProviderLoginHandle(
|
|
816
|
+
prompt,
|
|
817
|
+
completion.catch((errorArg) => {
|
|
818
|
+
if (operation.timedOut) throw createSmartAiProviderError('TIMEOUT');
|
|
819
|
+
if (optionsArg.signal?.aborted) throw createSmartAiProviderError('ABORTED');
|
|
820
|
+
if (abortController.signal.aborted) throw createSmartAiProviderError('LOGIN_CANCELED');
|
|
821
|
+
if (errorArg instanceof SmartAiProviderError) throw errorArg;
|
|
822
|
+
throw createSmartAiProviderError('LOGIN_FAILED');
|
|
823
|
+
}),
|
|
824
|
+
abortController,
|
|
825
|
+
cleanup,
|
|
826
|
+
() => this.handles.delete(handle),
|
|
827
|
+
);
|
|
828
|
+
this.handles.add(handle);
|
|
829
|
+
return handle;
|
|
830
|
+
} catch (errorArg) {
|
|
831
|
+
abortController.abort();
|
|
832
|
+
await cleanup().catch(() => undefined);
|
|
833
|
+
if (operation.timedOut) throw createSmartAiProviderError('TIMEOUT');
|
|
834
|
+
if (optionsArg.signal?.aborted) throw createSmartAiProviderError('ABORTED');
|
|
835
|
+
if (errorArg instanceof SmartAiProviderError) throw errorArg;
|
|
836
|
+
throw createSmartAiProviderError('LOGIN_FAILED');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
public async refreshCredential(
|
|
841
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
842
|
+
optionsArg: ISmartAiProviderOperationOptions = {},
|
|
843
|
+
): Promise<TSmartAiProviderRefreshResult> {
|
|
844
|
+
this.requireOpen();
|
|
845
|
+
toTokenData(credentialArg);
|
|
846
|
+
const waitOperation = createOperation(optionsArg, defaultOperationTimeoutMs);
|
|
847
|
+
if (waitOperation.signal.aborted) {
|
|
848
|
+
const failure: TSmartAiProviderRefreshResult = {
|
|
849
|
+
success: false,
|
|
850
|
+
credential: credentialArg,
|
|
851
|
+
errorCode: waitOperation.timedOut ? 'TIMEOUT' : 'ABORTED',
|
|
852
|
+
};
|
|
853
|
+
waitOperation.cleanup();
|
|
854
|
+
return failure;
|
|
855
|
+
}
|
|
856
|
+
let sharedRefresh: ISharedRefresh | undefined;
|
|
857
|
+
let joinedRefresh = false;
|
|
858
|
+
let onAbort: (() => void) | undefined;
|
|
859
|
+
try {
|
|
860
|
+
const refreshKey = plugins.crypto.createHash('sha256')
|
|
861
|
+
.update(credentialArg.refreshToken, 'utf8')
|
|
862
|
+
.digest('base64url');
|
|
863
|
+
sharedRefresh = this.refreshes.get(refreshKey);
|
|
864
|
+
if (!sharedRefresh) {
|
|
865
|
+
const controller = new AbortController();
|
|
866
|
+
let createdRefresh!: ISharedRefresh;
|
|
867
|
+
const promise = this.performRefreshCredential(credentialArg, {
|
|
868
|
+
signal: controller.signal,
|
|
869
|
+
}).finally(() => {
|
|
870
|
+
createdRefresh.settled = true;
|
|
871
|
+
if (this.refreshes.get(refreshKey) === createdRefresh) {
|
|
872
|
+
this.refreshes.delete(refreshKey);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
createdRefresh = {
|
|
876
|
+
controller,
|
|
877
|
+
promise,
|
|
878
|
+
waiters: 0,
|
|
879
|
+
settled: false,
|
|
880
|
+
};
|
|
881
|
+
this.refreshes.set(refreshKey, createdRefresh);
|
|
882
|
+
sharedRefresh = createdRefresh;
|
|
883
|
+
}
|
|
884
|
+
sharedRefresh.waiters++;
|
|
885
|
+
joinedRefresh = true;
|
|
886
|
+
const aborted = new Promise<TSmartAiProviderRefreshResult>((resolve) => {
|
|
887
|
+
onAbort = () => resolve({
|
|
888
|
+
success: false,
|
|
889
|
+
credential: credentialArg,
|
|
890
|
+
errorCode: waitOperation.timedOut ? 'TIMEOUT' : 'ABORTED',
|
|
891
|
+
});
|
|
892
|
+
waitOperation.signal.addEventListener('abort', onAbort, { once: true });
|
|
893
|
+
});
|
|
894
|
+
return await Promise.race([sharedRefresh.promise, aborted]);
|
|
895
|
+
} finally {
|
|
896
|
+
if (onAbort) waitOperation.signal.removeEventListener('abort', onAbort);
|
|
897
|
+
waitOperation.cleanup();
|
|
898
|
+
if (sharedRefresh && joinedRefresh) sharedRefresh.waiters--;
|
|
899
|
+
if (sharedRefresh && joinedRefresh && sharedRefresh.waiters === 0 && !sharedRefresh.settled) {
|
|
900
|
+
sharedRefresh.controller.abort();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private async performRefreshCredential(
|
|
906
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
907
|
+
optionsArg: ISmartAiProviderOperationOptions,
|
|
908
|
+
): Promise<TSmartAiProviderRefreshResult> {
|
|
909
|
+
const input = toTokenData(credentialArg);
|
|
910
|
+
const operation = createOperation(optionsArg, defaultOperationTimeoutMs);
|
|
911
|
+
this.operations.add(operation.controller);
|
|
912
|
+
try {
|
|
913
|
+
const refreshed = await refreshOpenAiChatGptTokenData(input, this.authOptions(operation.signal));
|
|
914
|
+
const tokenChanged = refreshed.accessToken !== input.accessToken
|
|
915
|
+
|| refreshed.refreshToken !== input.refreshToken;
|
|
916
|
+
if (!tokenChanged || !refreshed.idToken) {
|
|
917
|
+
return { success: false, credential: credentialArg, errorCode: 'REFRESH_FAILED' };
|
|
918
|
+
}
|
|
919
|
+
const previous = credentialArg.lastRefreshAt === undefined
|
|
920
|
+
? 0
|
|
921
|
+
: Date.parse(credentialArg.lastRefreshAt);
|
|
922
|
+
const now = this.now().getTime();
|
|
923
|
+
const lastRefreshAt = new Date(Math.max(now, Number.isFinite(previous) ? previous + 1 : now))
|
|
924
|
+
.toISOString();
|
|
925
|
+
const credential = toCredential(refreshed, lastRefreshAt);
|
|
926
|
+
return { success: true, credential, account: toSummary(credential) };
|
|
927
|
+
} catch (errorArg) {
|
|
928
|
+
return {
|
|
929
|
+
success: false,
|
|
930
|
+
credential: credentialArg,
|
|
931
|
+
errorCode: classifyOperationError(errorArg, operation, 'REFRESH_FAILED'),
|
|
932
|
+
};
|
|
933
|
+
} finally {
|
|
934
|
+
this.operations.delete(operation.controller);
|
|
935
|
+
operation.cleanup();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private async fetchCatalog(
|
|
940
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
941
|
+
signalArg: AbortSignal,
|
|
942
|
+
): Promise<ISmartAiProviderModel[]> {
|
|
943
|
+
const tokenData = toTokenData(credentialArg);
|
|
944
|
+
const accountId = tokenData.tokenInfo.chatgptAccountId;
|
|
945
|
+
const url = `${OPENAI_CHATGPT_CODEX_BASE_URL}/models?client_version=${encodeURIComponent(defaultClientVersion)}`;
|
|
946
|
+
let response: Response;
|
|
947
|
+
let onAbort!: () => void;
|
|
948
|
+
try {
|
|
949
|
+
const fetchPromise = this.fetchImplementation(url, {
|
|
950
|
+
method: 'GET',
|
|
951
|
+
redirect: 'error',
|
|
952
|
+
signal: signalArg,
|
|
953
|
+
headers: {
|
|
954
|
+
Accept: 'application/json',
|
|
955
|
+
Authorization: `Bearer ${credentialArg.accessToken}`,
|
|
956
|
+
...(accountId ? { 'ChatGPT-Account-ID': accountId } : {}),
|
|
957
|
+
...(tokenData.tokenInfo.chatgptAccountIsFedramp ? { 'X-OpenAI-Fedramp': 'true' } : {}),
|
|
958
|
+
originator: this.originator,
|
|
959
|
+
version: defaultClientVersion,
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
void fetchPromise.then((lateResponse) => {
|
|
963
|
+
if (signalArg.aborted) void lateResponse.body?.cancel().catch(() => undefined);
|
|
964
|
+
}).catch(() => undefined);
|
|
965
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
966
|
+
onAbort = () => reject(createSmartAiProviderError('ABORTED'));
|
|
967
|
+
signalArg.addEventListener('abort', onAbort, { once: true });
|
|
968
|
+
if (signalArg.aborted) onAbort();
|
|
969
|
+
});
|
|
970
|
+
response = await Promise.race([fetchPromise, aborted]);
|
|
971
|
+
} catch {
|
|
972
|
+
if (signalArg.aborted) throw createSmartAiProviderError('ABORTED');
|
|
973
|
+
throw createSmartAiProviderError('NETWORK_FAILED');
|
|
974
|
+
} finally {
|
|
975
|
+
if (onAbort) signalArg.removeEventListener('abort', onAbort);
|
|
976
|
+
}
|
|
977
|
+
if (!response.ok) {
|
|
978
|
+
await response.body?.cancel().catch(() => undefined);
|
|
979
|
+
throw new ModelHttpError(response.status);
|
|
980
|
+
}
|
|
981
|
+
return projectCatalog(await readBoundedJson(response, signalArg, maximumCatalogBytes));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private encodeCursor(payloadArg: ICursorPayload): string {
|
|
985
|
+
const body = plugins.Buffer.from(JSON.stringify(payloadArg), 'utf8').toString('base64url');
|
|
986
|
+
const mac = plugins.crypto.createHmac('sha256', this.cursorKey).update(body, 'ascii').digest('base64url');
|
|
987
|
+
return `v1.${body}.${mac}`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private decodeCursor(cursorArg: string): ICursorPayload {
|
|
991
|
+
const parts = cursorArg.split('.');
|
|
992
|
+
if (parts.length !== 3 || parts[0] !== 'v1') {
|
|
993
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
994
|
+
}
|
|
995
|
+
const expectedMac = plugins.crypto.createHmac('sha256', this.cursorKey)
|
|
996
|
+
.update(parts[1], 'ascii')
|
|
997
|
+
.digest();
|
|
998
|
+
let actualMac: plugins.Buffer;
|
|
999
|
+
let payload: unknown;
|
|
1000
|
+
try {
|
|
1001
|
+
actualMac = plugins.Buffer.from(parts[2], 'base64url');
|
|
1002
|
+
payload = JSON.parse(plugins.Buffer.from(parts[1], 'base64url').toString('utf8')) as unknown;
|
|
1003
|
+
} catch {
|
|
1004
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
1005
|
+
}
|
|
1006
|
+
if (actualMac.byteLength !== expectedMac.byteLength || !plugins.crypto.timingSafeEqual(actualMac, expectedMac)) {
|
|
1007
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
1008
|
+
}
|
|
1009
|
+
if (
|
|
1010
|
+
!isRecord(payload)
|
|
1011
|
+
|| Object.keys(payload).length !== 5
|
|
1012
|
+
|| payload.version !== 1
|
|
1013
|
+
|| payload.projectionVersion !== 1
|
|
1014
|
+
|| typeof payload.digest !== 'string'
|
|
1015
|
+
|| !/^[A-Za-z0-9_-]{43}$/.test(payload.digest)
|
|
1016
|
+
|| typeof payload.includeHidden !== 'boolean'
|
|
1017
|
+
|| !Number.isSafeInteger(payload.offset)
|
|
1018
|
+
|| (payload.offset as number) < 0
|
|
1019
|
+
) {
|
|
1020
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
1021
|
+
}
|
|
1022
|
+
return payload as unknown as ICursorPayload;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
public async listModels(
|
|
1026
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
1027
|
+
optionsArg: ISmartAiProviderModelListOptions = {},
|
|
1028
|
+
): Promise<TSmartAiProviderModelListResult> {
|
|
1029
|
+
this.requireOpen();
|
|
1030
|
+
toTokenData(credentialArg);
|
|
1031
|
+
const options = normalizeModelListOptions(optionsArg);
|
|
1032
|
+
const operation = createOperation(options, defaultOperationTimeoutMs);
|
|
1033
|
+
this.operations.add(operation.controller);
|
|
1034
|
+
try {
|
|
1035
|
+
let catalog: ISmartAiProviderModel[];
|
|
1036
|
+
try {
|
|
1037
|
+
catalog = await this.fetchCatalog(credentialArg, operation.signal);
|
|
1038
|
+
} catch (errorArg) {
|
|
1039
|
+
if (!(errorArg instanceof ModelHttpError) || errorArg.status !== 401) throw errorArg;
|
|
1040
|
+
return { success: false, errorCode: 'CREDENTIAL_REFRESH_REQUIRED' };
|
|
1041
|
+
}
|
|
1042
|
+
const filtered = options.includeHidden ? catalog : catalog.filter((model) => !model.hidden);
|
|
1043
|
+
const digest = plugins.crypto.createHash('sha256').update(JSON.stringify(filtered)).digest('base64url');
|
|
1044
|
+
let offset = 0;
|
|
1045
|
+
if (options.cursor !== undefined) {
|
|
1046
|
+
const cursor = this.decodeCursor(options.cursor);
|
|
1047
|
+
if (
|
|
1048
|
+
cursor.digest !== digest
|
|
1049
|
+
|| cursor.includeHidden !== options.includeHidden
|
|
1050
|
+
|| cursor.offset > filtered.length
|
|
1051
|
+
) {
|
|
1052
|
+
throw createSmartAiProviderError('INVALID_ARGUMENT');
|
|
1053
|
+
}
|
|
1054
|
+
offset = cursor.offset;
|
|
1055
|
+
}
|
|
1056
|
+
const models = filtered.slice(offset, offset + options.limit);
|
|
1057
|
+
const nextOffset = offset + models.length;
|
|
1058
|
+
const nextCursor = nextOffset < filtered.length
|
|
1059
|
+
? this.encodeCursor({
|
|
1060
|
+
version: 1,
|
|
1061
|
+
projectionVersion: 1,
|
|
1062
|
+
digest,
|
|
1063
|
+
includeHidden: options.includeHidden,
|
|
1064
|
+
offset: nextOffset,
|
|
1065
|
+
})
|
|
1066
|
+
: undefined;
|
|
1067
|
+
return {
|
|
1068
|
+
success: true,
|
|
1069
|
+
models,
|
|
1070
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
1071
|
+
};
|
|
1072
|
+
} catch (errorArg) {
|
|
1073
|
+
if (errorArg instanceof SmartAiProviderError && errorArg.code === 'INVALID_ARGUMENT') {
|
|
1074
|
+
throw errorArg;
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
success: false,
|
|
1078
|
+
errorCode: classifyOperationError(errorArg, operation, 'MODEL_LIST_FAILED'),
|
|
1079
|
+
};
|
|
1080
|
+
} finally {
|
|
1081
|
+
this.operations.delete(operation.controller);
|
|
1082
|
+
operation.cleanup();
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private async fetchAccountRateLimits(
|
|
1087
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
1088
|
+
signalArg: AbortSignal,
|
|
1089
|
+
): Promise<ISmartAiProviderAccountRateLimits> {
|
|
1090
|
+
const tokenData = toTokenData(credentialArg);
|
|
1091
|
+
const accountId = tokenData.tokenInfo.chatgptAccountId;
|
|
1092
|
+
let response: Response;
|
|
1093
|
+
let onAbort!: () => void;
|
|
1094
|
+
try {
|
|
1095
|
+
const fetchPromise = this.fetchImplementation(accountRateLimitsUrl, {
|
|
1096
|
+
method: 'GET',
|
|
1097
|
+
redirect: 'error',
|
|
1098
|
+
signal: signalArg,
|
|
1099
|
+
headers: {
|
|
1100
|
+
Accept: 'application/json',
|
|
1101
|
+
Authorization: `Bearer ${credentialArg.accessToken}`,
|
|
1102
|
+
...(accountId ? { 'ChatGPT-Account-Id': accountId } : {}),
|
|
1103
|
+
...(tokenData.tokenInfo.chatgptAccountIsFedramp ? { 'X-OpenAI-Fedramp': 'true' } : {}),
|
|
1104
|
+
'User-Agent': this.originator,
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
void fetchPromise.then((lateResponse) => {
|
|
1108
|
+
if (signalArg.aborted) void lateResponse.body?.cancel().catch(() => undefined);
|
|
1109
|
+
}).catch(() => undefined);
|
|
1110
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
1111
|
+
onAbort = () => reject(createSmartAiProviderError('ABORTED'));
|
|
1112
|
+
signalArg.addEventListener('abort', onAbort, { once: true });
|
|
1113
|
+
if (signalArg.aborted) onAbort();
|
|
1114
|
+
});
|
|
1115
|
+
response = await Promise.race([fetchPromise, aborted]);
|
|
1116
|
+
} catch {
|
|
1117
|
+
if (signalArg.aborted) throw createSmartAiProviderError('ABORTED');
|
|
1118
|
+
throw createSmartAiProviderError('NETWORK_FAILED');
|
|
1119
|
+
} finally {
|
|
1120
|
+
if (onAbort) signalArg.removeEventListener('abort', onAbort);
|
|
1121
|
+
}
|
|
1122
|
+
if (!response.ok) {
|
|
1123
|
+
await response.body?.cancel().catch(() => undefined);
|
|
1124
|
+
throw new AccountRateLimitsHttpError(response.status);
|
|
1125
|
+
}
|
|
1126
|
+
return projectAccountRateLimits(
|
|
1127
|
+
await readBoundedJson(response, signalArg, maximumAccountRateLimitsBytes),
|
|
1128
|
+
this.now().toISOString(),
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
public async getAccountRateLimits(
|
|
1133
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
1134
|
+
optionsArg: ISmartAiProviderOperationOptions = {},
|
|
1135
|
+
): Promise<TSmartAiProviderAccountRateLimitsResult> {
|
|
1136
|
+
this.requireOpen();
|
|
1137
|
+
toTokenData(credentialArg);
|
|
1138
|
+
const operation = createOperation(optionsArg, defaultOperationTimeoutMs);
|
|
1139
|
+
this.operations.add(operation.controller);
|
|
1140
|
+
try {
|
|
1141
|
+
try {
|
|
1142
|
+
const rateLimits = await this.fetchAccountRateLimits(credentialArg, operation.signal);
|
|
1143
|
+
return { success: true, rateLimits };
|
|
1144
|
+
} catch (errorArg) {
|
|
1145
|
+
if (!(errorArg instanceof AccountRateLimitsHttpError) || errorArg.status !== 401) {
|
|
1146
|
+
throw errorArg;
|
|
1147
|
+
}
|
|
1148
|
+
return { success: false, errorCode: 'CREDENTIAL_REFRESH_REQUIRED' };
|
|
1149
|
+
}
|
|
1150
|
+
} catch (errorArg) {
|
|
1151
|
+
return {
|
|
1152
|
+
success: false,
|
|
1153
|
+
errorCode: classifyOperationError(errorArg, operation, 'ACCOUNT_RATE_LIMITS_FAILED'),
|
|
1154
|
+
};
|
|
1155
|
+
} finally {
|
|
1156
|
+
this.operations.delete(operation.controller);
|
|
1157
|
+
operation.cleanup();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
public async logoutCredential(
|
|
1162
|
+
credentialArg: IOpenAiChatGptOAuthCredential,
|
|
1163
|
+
optionsArg: ISmartAiProviderOperationOptions = {},
|
|
1164
|
+
): Promise<void> {
|
|
1165
|
+
this.requireOpen();
|
|
1166
|
+
toTokenData(credentialArg);
|
|
1167
|
+
const operation = createOperation(optionsArg, defaultOperationTimeoutMs);
|
|
1168
|
+
this.operations.add(operation.controller);
|
|
1169
|
+
try {
|
|
1170
|
+
await revokeOpenAiChatGptToken(credentialArg.refreshToken, {
|
|
1171
|
+
...this.authOptions(operation.signal),
|
|
1172
|
+
tokenTypeHint: 'refresh_token',
|
|
1173
|
+
});
|
|
1174
|
+
} catch {
|
|
1175
|
+
throw createSmartAiProviderError(
|
|
1176
|
+
operation.timedOut ? 'TIMEOUT' : operation.signal.aborted ? 'ABORTED' : 'NETWORK_FAILED',
|
|
1177
|
+
);
|
|
1178
|
+
} finally {
|
|
1179
|
+
this.operations.delete(operation.controller);
|
|
1180
|
+
operation.cleanup();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
public async dispose(): Promise<void> {
|
|
1185
|
+
if (this.disposePromise) return this.disposePromise;
|
|
1186
|
+
this.closed = true;
|
|
1187
|
+
const disposeAttempt = (async () => {
|
|
1188
|
+
for (const operation of this.operations) operation.abort();
|
|
1189
|
+
const results = await Promise.allSettled([...this.handles].map((handle) => handle.close()));
|
|
1190
|
+
const operationDeadline = Date.now() + 5_000;
|
|
1191
|
+
while (this.operations.size > 0 && Date.now() < operationDeadline) {
|
|
1192
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
1193
|
+
}
|
|
1194
|
+
if (results.some((result) => result.status === 'rejected')) {
|
|
1195
|
+
throw createSmartAiProviderError('CLOSED');
|
|
1196
|
+
}
|
|
1197
|
+
if (this.operations.size > 0) throw createSmartAiProviderError('CLOSED');
|
|
1198
|
+
this.handles.clear();
|
|
1199
|
+
this.cursorKey.fill(0);
|
|
1200
|
+
})();
|
|
1201
|
+
this.disposePromise = disposeAttempt;
|
|
1202
|
+
try {
|
|
1203
|
+
await disposeAttempt;
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
this.disposePromise = undefined;
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|