@openai-oauth/core 2.0.0-beta.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/LICENSE +661 -0
- package/README.md +58 -0
- package/dist/auth.d.ts +33 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +242 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/runtime.d.ts +125 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +423 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +144 -0
- package/dist/state.d.ts +33 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +154 -0
- package/package.json +56 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { withoutTrailingSlash } from "@ai-sdk/provider-utils";
|
|
2
|
+
import { CodexResponsesState } from "./state.js";
|
|
3
|
+
export { collectCompletedResponseFromSse, iterateServerSentEvents, } from "./sse.js";
|
|
4
|
+
export { CodexResponsesState, } from "./state.js";
|
|
5
|
+
import { collectCompletedResponseFromSse } from "./sse.js";
|
|
6
|
+
export const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
7
|
+
export const DEFAULT_OPENAI_COMPATIBLE_BASE_URL = "https://openai-oauth.local/v1";
|
|
8
|
+
export const DEFAULT_OPENAI_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
9
|
+
export const DEFAULT_OPENAI_OAUTH_ISSUER = "https://auth.openai.com";
|
|
10
|
+
export const DEFAULT_OPENAI_OAUTH_SCOPE = "openid profile email offline_access";
|
|
11
|
+
const DEFAULT_CODEX_INSTRUCTIONS = "";
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
const bytesToBase64Url = (bytes) => {
|
|
14
|
+
let binary = "";
|
|
15
|
+
for (const byte of bytes) {
|
|
16
|
+
binary += String.fromCharCode(byte);
|
|
17
|
+
}
|
|
18
|
+
return btoa(binary)
|
|
19
|
+
.replaceAll("+", "-")
|
|
20
|
+
.replaceAll("/", "_")
|
|
21
|
+
.replace(/=+$/, "");
|
|
22
|
+
};
|
|
23
|
+
const randomURLSafeString = (byteLength) => {
|
|
24
|
+
const bytes = new Uint8Array(byteLength);
|
|
25
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
26
|
+
return bytesToBase64Url(bytes);
|
|
27
|
+
};
|
|
28
|
+
const createCodeChallenge = async (codeVerifier) => {
|
|
29
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", textEncoder.encode(codeVerifier));
|
|
30
|
+
return bytesToBase64Url(new Uint8Array(digest));
|
|
31
|
+
};
|
|
32
|
+
const trimTrailingSlash = (value) => value.replace(/\/$/, "");
|
|
33
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
export const usesServerReplayState = (value) => {
|
|
35
|
+
if (typeof value.previous_response_id === "string") {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(value.input)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return value.input.some((item) => isRecord(item) &&
|
|
42
|
+
item.type === "item_reference" &&
|
|
43
|
+
typeof item.id === "string");
|
|
44
|
+
};
|
|
45
|
+
const decodeBase64Url = (value) => {
|
|
46
|
+
try {
|
|
47
|
+
const padded = value + "=".repeat(((-value.length % 4) + 4) % 4);
|
|
48
|
+
const binary = atob(padded.replaceAll("-", "+").replaceAll("_", "/"));
|
|
49
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
50
|
+
return new TextDecoder().decode(bytes);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export const parseJwtClaims = (token) => {
|
|
57
|
+
if (typeof token !== "string" || !token.includes(".")) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const parts = token.split(".");
|
|
61
|
+
if (parts.length !== 3 || parts[1] === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const payload = decodeBase64Url(parts[1]);
|
|
65
|
+
if (typeof payload !== "string") {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(payload);
|
|
70
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
export const deriveAccountId = (idToken) => {
|
|
77
|
+
const claims = parseJwtClaims(idToken);
|
|
78
|
+
if (!claims) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const authClaim = claims["https://api.openai.com/auth"];
|
|
82
|
+
if (isRecord(authClaim)) {
|
|
83
|
+
const accountId = authClaim.chatgpt_account_id;
|
|
84
|
+
if (typeof accountId === "string" && accountId.length > 0) {
|
|
85
|
+
return accountId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const topLevelAccountId = claims.chatgpt_account_id;
|
|
89
|
+
if (typeof topLevelAccountId === "string" && topLevelAccountId.length > 0) {
|
|
90
|
+
return topLevelAccountId;
|
|
91
|
+
}
|
|
92
|
+
const organizations = claims.organizations;
|
|
93
|
+
if (Array.isArray(organizations)) {
|
|
94
|
+
const first = organizations[0];
|
|
95
|
+
if (isRecord(first) &&
|
|
96
|
+
typeof first.id === "string" &&
|
|
97
|
+
first.id.length > 0) {
|
|
98
|
+
return first.id;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
};
|
|
103
|
+
const resolveTokenUrl = (issuer, tokenUrl) => tokenUrl ?? `${trimTrailingSlash(issuer)}/oauth/token`;
|
|
104
|
+
const toTokenResponse = (payload) => {
|
|
105
|
+
if (!isRecord(payload)) {
|
|
106
|
+
throw new Error("OpenAI OAuth token response must be a JSON object.");
|
|
107
|
+
}
|
|
108
|
+
const accessToken = typeof payload.access_token === "string" ? payload.access_token : undefined;
|
|
109
|
+
if (!accessToken) {
|
|
110
|
+
throw new Error("OpenAI OAuth token response did not include access_token.");
|
|
111
|
+
}
|
|
112
|
+
const refreshToken = typeof payload.refresh_token === "string"
|
|
113
|
+
? payload.refresh_token
|
|
114
|
+
: undefined;
|
|
115
|
+
const idToken = typeof payload.id_token === "string" ? payload.id_token : undefined;
|
|
116
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : undefined;
|
|
117
|
+
return {
|
|
118
|
+
accessToken,
|
|
119
|
+
refreshToken,
|
|
120
|
+
idToken,
|
|
121
|
+
expiresIn,
|
|
122
|
+
accountId: deriveAccountId(idToken) ?? deriveAccountId(accessToken),
|
|
123
|
+
raw: payload,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const requestOpenAIOAuthTokens = async (options) => {
|
|
127
|
+
const issuer = options.issuer ?? DEFAULT_OPENAI_OAUTH_ISSUER;
|
|
128
|
+
const response = await pickFetch(options.fetch)(resolveTokenUrl(issuer, options.tokenUrl), {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify(options.body),
|
|
134
|
+
signal: options.signal,
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(`OpenAI OAuth token request failed with HTTP ${response.status}.`);
|
|
138
|
+
}
|
|
139
|
+
return toTokenResponse(await response.json());
|
|
140
|
+
};
|
|
141
|
+
export const createOpenAIOAuthRequest = async (options) => {
|
|
142
|
+
const state = options.state ?? randomURLSafeString(24);
|
|
143
|
+
const codeVerifier = options.codeVerifier ?? randomURLSafeString(48);
|
|
144
|
+
const codeChallenge = await createCodeChallenge(codeVerifier);
|
|
145
|
+
const issuer = trimTrailingSlash(options.issuer ?? DEFAULT_OPENAI_OAUTH_ISSUER);
|
|
146
|
+
const authorizationUrl = new URL(`${issuer}/oauth/authorize`);
|
|
147
|
+
authorizationUrl.searchParams.set("response_type", "code");
|
|
148
|
+
authorizationUrl.searchParams.set("client_id", options.clientId ?? DEFAULT_OPENAI_OAUTH_CLIENT_ID);
|
|
149
|
+
authorizationUrl.searchParams.set("redirect_uri", options.redirectUri);
|
|
150
|
+
authorizationUrl.searchParams.set("scope", options.scope ?? DEFAULT_OPENAI_OAUTH_SCOPE);
|
|
151
|
+
authorizationUrl.searchParams.set("state", state);
|
|
152
|
+
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
153
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
154
|
+
if (options.idTokenAddOrganizations ?? true) {
|
|
155
|
+
authorizationUrl.searchParams.set("id_token_add_organizations", "true");
|
|
156
|
+
}
|
|
157
|
+
if (options.simplifiedFlow ?? true) {
|
|
158
|
+
authorizationUrl.searchParams.set("codex_cli_simplified_flow", "true");
|
|
159
|
+
}
|
|
160
|
+
for (const [key, value] of Object.entries(options.extraParams ?? {})) {
|
|
161
|
+
if (value !== undefined && key !== "originator") {
|
|
162
|
+
authorizationUrl.searchParams.set(key, String(value));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
authorizationUrl: authorizationUrl.toString(),
|
|
167
|
+
state,
|
|
168
|
+
codeVerifier,
|
|
169
|
+
codeChallenge,
|
|
170
|
+
redirectUri: options.redirectUri,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
export const exchangeOpenAIOAuthCode = (options) => requestOpenAIOAuthTokens({
|
|
174
|
+
issuer: options.issuer,
|
|
175
|
+
tokenUrl: options.tokenUrl,
|
|
176
|
+
fetch: options.fetch,
|
|
177
|
+
signal: options.signal,
|
|
178
|
+
body: {
|
|
179
|
+
grant_type: "authorization_code",
|
|
180
|
+
code: options.code,
|
|
181
|
+
redirect_uri: options.redirectUri,
|
|
182
|
+
client_id: options.clientId ?? DEFAULT_OPENAI_OAUTH_CLIENT_ID,
|
|
183
|
+
code_verifier: options.codeVerifier,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
export const refreshOpenAIOAuthTokens = (options) => requestOpenAIOAuthTokens({
|
|
187
|
+
issuer: options.issuer,
|
|
188
|
+
tokenUrl: options.tokenUrl,
|
|
189
|
+
fetch: options.fetch,
|
|
190
|
+
signal: options.signal,
|
|
191
|
+
body: {
|
|
192
|
+
grant_type: "refresh_token",
|
|
193
|
+
refresh_token: options.refreshToken,
|
|
194
|
+
client_id: options.clientId ?? DEFAULT_OPENAI_OAUTH_CLIENT_ID,
|
|
195
|
+
scope: DEFAULT_OPENAI_OAUTH_SCOPE,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const pickFetch = (customFetch) => {
|
|
199
|
+
if (typeof customFetch === "function") {
|
|
200
|
+
return customFetch;
|
|
201
|
+
}
|
|
202
|
+
if (typeof globalThis.fetch === "function") {
|
|
203
|
+
return globalThis.fetch.bind(globalThis);
|
|
204
|
+
}
|
|
205
|
+
throw new Error("A fetch implementation is required for OpenAI OAuth.");
|
|
206
|
+
};
|
|
207
|
+
const resolveBaseURL = (baseURL) => withoutTrailingSlash(baseURL) ?? DEFAULT_CODEX_BASE_URL;
|
|
208
|
+
const resolveOpenAIBaseURL = (baseURL) => withoutTrailingSlash(baseURL) ?? DEFAULT_OPENAI_COMPATIBLE_BASE_URL;
|
|
209
|
+
const resolveTargetUrl = (input, baseURL) => {
|
|
210
|
+
const base = new URL(baseURL);
|
|
211
|
+
const parsed = /^https?:\/\//.test(input)
|
|
212
|
+
? new URL(input)
|
|
213
|
+
: new URL(input, "https://codex.invalid");
|
|
214
|
+
let pathname = parsed.pathname;
|
|
215
|
+
const basePath = withoutTrailingSlash(base.pathname) ?? "";
|
|
216
|
+
if (pathname === basePath) {
|
|
217
|
+
pathname = "/";
|
|
218
|
+
}
|
|
219
|
+
else if (basePath.length > 0 && pathname.startsWith(`${basePath}/`)) {
|
|
220
|
+
pathname = pathname.slice(basePath.length);
|
|
221
|
+
}
|
|
222
|
+
if (pathname === "/v1") {
|
|
223
|
+
pathname = "/";
|
|
224
|
+
}
|
|
225
|
+
else if (pathname.startsWith("/v1/")) {
|
|
226
|
+
pathname = pathname.slice(3);
|
|
227
|
+
}
|
|
228
|
+
return `${base.origin}${basePath}${pathname}${parsed.search}`;
|
|
229
|
+
};
|
|
230
|
+
const readRequestParts = async (input, init) => {
|
|
231
|
+
if (input instanceof Request) {
|
|
232
|
+
const headers = new Headers(input.headers);
|
|
233
|
+
if (init?.headers) {
|
|
234
|
+
new Headers(init.headers).forEach((value, key) => {
|
|
235
|
+
headers.set(key, value);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
url: input.url,
|
|
240
|
+
method: init?.method ?? input.method,
|
|
241
|
+
headers,
|
|
242
|
+
body: init?.body ??
|
|
243
|
+
(input.body == null ? undefined : await input.clone().text()),
|
|
244
|
+
signal: init?.signal ?? input.signal,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
url: String(input),
|
|
249
|
+
method: init?.method,
|
|
250
|
+
headers: new Headers(init?.headers),
|
|
251
|
+
body: init?.body,
|
|
252
|
+
signal: init?.signal,
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
const decodeBody = async (body) => {
|
|
256
|
+
if (body == null) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (typeof body === "string") {
|
|
260
|
+
return body;
|
|
261
|
+
}
|
|
262
|
+
if (body instanceof URLSearchParams || body instanceof FormData) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
if (body instanceof ReadableStream) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (body instanceof Blob) {
|
|
269
|
+
return body.text();
|
|
270
|
+
}
|
|
271
|
+
if (body instanceof ArrayBuffer) {
|
|
272
|
+
return new TextDecoder().decode(body);
|
|
273
|
+
}
|
|
274
|
+
if (ArrayBuffer.isView(body)) {
|
|
275
|
+
return new TextDecoder().decode(body);
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
};
|
|
279
|
+
export const getDefaultCodexInstructions = () => DEFAULT_CODEX_INSTRUCTIONS;
|
|
280
|
+
export const normalizeCodexResponsesBody = (body, options = {}) => {
|
|
281
|
+
const normalized = { ...body };
|
|
282
|
+
const instructions = typeof normalized.instructions === "string"
|
|
283
|
+
? normalized.instructions
|
|
284
|
+
: (options.instructions ?? getDefaultCodexInstructions());
|
|
285
|
+
normalized.instructions = instructions;
|
|
286
|
+
if (normalized.store === undefined) {
|
|
287
|
+
normalized.store = options.storeResponses ?? false;
|
|
288
|
+
}
|
|
289
|
+
if (options.forceStream) {
|
|
290
|
+
normalized.stream = true;
|
|
291
|
+
}
|
|
292
|
+
delete normalized.max_output_tokens;
|
|
293
|
+
return normalized;
|
|
294
|
+
};
|
|
295
|
+
const prepareResponsesRequestBody = async (pathname, headers, body, settings, state) => {
|
|
296
|
+
if (!pathname.endsWith("/responses")) {
|
|
297
|
+
return { body };
|
|
298
|
+
}
|
|
299
|
+
const contentType = headers.get("content-type");
|
|
300
|
+
if (contentType && !contentType.includes("application/json")) {
|
|
301
|
+
return { body };
|
|
302
|
+
}
|
|
303
|
+
const bodyText = await decodeBody(body);
|
|
304
|
+
if (typeof bodyText !== "string") {
|
|
305
|
+
return { body };
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(bodyText);
|
|
309
|
+
if (typeof parsed !== "object" ||
|
|
310
|
+
parsed === null ||
|
|
311
|
+
Array.isArray(parsed)) {
|
|
312
|
+
return { body };
|
|
313
|
+
}
|
|
314
|
+
const normalized = normalizeCodexResponsesBody(parsed, {
|
|
315
|
+
instructions: settings.instructions,
|
|
316
|
+
storeResponses: settings.storeResponses,
|
|
317
|
+
});
|
|
318
|
+
if (state?.requiresCachedState(normalized)) {
|
|
319
|
+
await state.waitForPendingCaptures();
|
|
320
|
+
}
|
|
321
|
+
const expanded = state?.expandRequestBody(normalized) ?? normalized;
|
|
322
|
+
return {
|
|
323
|
+
body: JSON.stringify(expanded),
|
|
324
|
+
requestBody: expanded,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return { body };
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const captureResponsesState = (response, requestBody, state) => {
|
|
332
|
+
if (state == null ||
|
|
333
|
+
requestBody == null ||
|
|
334
|
+
!response.ok ||
|
|
335
|
+
response.body == null) {
|
|
336
|
+
return response;
|
|
337
|
+
}
|
|
338
|
+
const [returnedBody, cachedBody] = response.body.tee();
|
|
339
|
+
const capturePromise = collectCompletedResponseFromSse(cachedBody)
|
|
340
|
+
.then((completedResponse) => {
|
|
341
|
+
state.rememberResponse(completedResponse, requestBody);
|
|
342
|
+
})
|
|
343
|
+
.catch(() => undefined);
|
|
344
|
+
state.trackPendingCapture(capturePromise);
|
|
345
|
+
return new Response(returnedBody, {
|
|
346
|
+
status: response.status,
|
|
347
|
+
statusText: response.statusText,
|
|
348
|
+
headers: new Headers(response.headers),
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
const resolveAuth = async (source) => {
|
|
352
|
+
const auth = typeof source === "function" ? await source() : source;
|
|
353
|
+
if (!auth) {
|
|
354
|
+
throw new Error("OpenAI OAuth session not found.");
|
|
355
|
+
}
|
|
356
|
+
return auth;
|
|
357
|
+
};
|
|
358
|
+
export const createCodexOAuthFetch = (settings) => {
|
|
359
|
+
const fetch = pickFetch(settings.fetch);
|
|
360
|
+
const baseURL = resolveBaseURL(settings.baseURL);
|
|
361
|
+
const responsesState = settings.responsesState === false
|
|
362
|
+
? undefined
|
|
363
|
+
: (settings.responsesState ?? new CodexResponsesState());
|
|
364
|
+
return async (input, init) => {
|
|
365
|
+
const request = await readRequestParts(input, init);
|
|
366
|
+
const targetUrl = resolveTargetUrl(request.url, baseURL);
|
|
367
|
+
const target = new URL(targetUrl);
|
|
368
|
+
const auth = await resolveAuth(settings.auth);
|
|
369
|
+
const headers = new Headers(settings.headers);
|
|
370
|
+
request.headers.forEach((value, key) => {
|
|
371
|
+
headers.set(key, value);
|
|
372
|
+
});
|
|
373
|
+
headers.delete("authorization");
|
|
374
|
+
headers.delete("chatgpt-account-id");
|
|
375
|
+
headers.delete("openai-beta");
|
|
376
|
+
headers.set("Authorization", `Bearer ${auth.accessToken}`);
|
|
377
|
+
headers.set("chatgpt-account-id", auth.accountId);
|
|
378
|
+
headers.set("OpenAI-Beta", "responses=experimental");
|
|
379
|
+
const preparedBody = await prepareResponsesRequestBody(target.pathname, headers, request.body, settings, responsesState);
|
|
380
|
+
const response = await fetch(target.toString(), {
|
|
381
|
+
method: request.method ?? init?.method,
|
|
382
|
+
headers,
|
|
383
|
+
body: preparedBody.body,
|
|
384
|
+
signal: request.signal ?? undefined,
|
|
385
|
+
});
|
|
386
|
+
return captureResponsesState(response, preparedBody.requestBody, responsesState);
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
const resolveOpenAICompatibleUrl = (path, baseURL) => {
|
|
390
|
+
if (/^https?:\/\//.test(path)) {
|
|
391
|
+
return path;
|
|
392
|
+
}
|
|
393
|
+
const normalizedPath = path.startsWith("/v1/")
|
|
394
|
+
? path.slice("/v1/".length)
|
|
395
|
+
: path.replace(/^\//, "");
|
|
396
|
+
return new URL(normalizedPath, `${baseURL}/`).toString();
|
|
397
|
+
};
|
|
398
|
+
export const createCodexOAuthClient = (settings) => {
|
|
399
|
+
const baseURL = resolveBaseURL(settings.baseURL);
|
|
400
|
+
const fetch = createCodexOAuthFetch(settings);
|
|
401
|
+
return {
|
|
402
|
+
baseURL,
|
|
403
|
+
fetch,
|
|
404
|
+
request: (path, init) => fetch(new URL(path, `${baseURL}/`).toString(), init),
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
export const createOpenAIOAuthTransport = (settings) => {
|
|
408
|
+
const baseURL = resolveOpenAIBaseURL(settings.openAIBaseURL);
|
|
409
|
+
const fetch = createCodexOAuthFetch(settings);
|
|
410
|
+
return {
|
|
411
|
+
kind: "openai-compatible",
|
|
412
|
+
provider: "chatgpt-codex",
|
|
413
|
+
baseURL,
|
|
414
|
+
fetch,
|
|
415
|
+
request: (path, init) => fetch(resolveOpenAICompatibleUrl(path, baseURL), init),
|
|
416
|
+
capabilities: {
|
|
417
|
+
responses: true,
|
|
418
|
+
chatCompletions: true,
|
|
419
|
+
models: true,
|
|
420
|
+
streaming: true,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
};
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ServerSentEvent = {
|
|
2
|
+
event?: string;
|
|
3
|
+
data?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function iterateServerSentEvents(stream: ReadableStream<Uint8Array>): AsyncGenerator<ServerSentEvent>;
|
|
6
|
+
export declare const collectCompletedResponseFromSse: (stream: ReadableStream<Uint8Array>) => Promise<Record<string, unknown>>;
|
|
7
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAwBD,wBAAuB,uBAAuB,CAC7C,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAChC,cAAc,CAAC,eAAe,CAAC,CAkCjC;AAuDD,eAAO,MAAM,+BAA+B,GAC3C,QAAQ,cAAc,CAAC,UAAU,CAAC,KAChC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmEjC,CAAA"}
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const SSE_SEPARATOR = /\r?\n\r?\n/;
|
|
2
|
+
const parseEventBlock = (block) => {
|
|
3
|
+
const event = {};
|
|
4
|
+
const dataLines = [];
|
|
5
|
+
for (const line of block.split(/\r?\n/)) {
|
|
6
|
+
if (line.startsWith("event:")) {
|
|
7
|
+
event.event = line.slice(6).trim();
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (line.startsWith("data:")) {
|
|
11
|
+
dataLines.push(line.slice(5).trimStart());
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (dataLines.length > 0) {
|
|
15
|
+
event.data = dataLines.join("\n");
|
|
16
|
+
}
|
|
17
|
+
return event;
|
|
18
|
+
};
|
|
19
|
+
export async function* iterateServerSentEvents(stream) {
|
|
20
|
+
const reader = stream.getReader();
|
|
21
|
+
const decoder = new TextDecoder();
|
|
22
|
+
let buffer = "";
|
|
23
|
+
let reachedEnd = false;
|
|
24
|
+
try {
|
|
25
|
+
while (true) {
|
|
26
|
+
const { value, done } = await reader.read();
|
|
27
|
+
if (done) {
|
|
28
|
+
reachedEnd = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
buffer += decoder.decode(value, { stream: true });
|
|
32
|
+
const blocks = buffer.split(SSE_SEPARATOR);
|
|
33
|
+
buffer = blocks.pop() ?? "";
|
|
34
|
+
for (const block of blocks) {
|
|
35
|
+
if (block.trim().length > 0) {
|
|
36
|
+
yield parseEventBlock(block);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (buffer.trim().length > 0) {
|
|
41
|
+
yield parseEventBlock(buffer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (!reachedEnd) {
|
|
46
|
+
void reader.cancel().catch(() => undefined);
|
|
47
|
+
}
|
|
48
|
+
reader.releaseLock();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
const terminalServerSentEvents = new Set([
|
|
53
|
+
"error",
|
|
54
|
+
"response.completed",
|
|
55
|
+
"response.failed",
|
|
56
|
+
"response.cancelled",
|
|
57
|
+
"response.canceled",
|
|
58
|
+
"response.incomplete",
|
|
59
|
+
]);
|
|
60
|
+
const terminalResponseStatuses = new Set([
|
|
61
|
+
"completed",
|
|
62
|
+
"failed",
|
|
63
|
+
"cancelled",
|
|
64
|
+
"canceled",
|
|
65
|
+
"incomplete",
|
|
66
|
+
]);
|
|
67
|
+
const isTerminalPayload = (data) => {
|
|
68
|
+
if (data === "[DONE]") {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(data);
|
|
73
|
+
if (!isRecord(parsed)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const type = parsed.type;
|
|
77
|
+
if (typeof type === "string" && terminalServerSentEvents.has(type)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const response = parsed.response;
|
|
81
|
+
if (!isRecord(response)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const responseType = response.type;
|
|
85
|
+
const status = response.status;
|
|
86
|
+
return ((typeof responseType === "string" &&
|
|
87
|
+
terminalServerSentEvents.has(responseType)) ||
|
|
88
|
+
(typeof status === "string" && terminalResponseStatuses.has(status)));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
export const collectCompletedResponseFromSse = async (stream) => {
|
|
95
|
+
let latestResponse;
|
|
96
|
+
let latestError;
|
|
97
|
+
const outputItems = new Map();
|
|
98
|
+
const withCollectedOutput = (response) => {
|
|
99
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
100
|
+
if (output.length > 0 || outputItems.size === 0) {
|
|
101
|
+
return response;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
...response,
|
|
105
|
+
output: [...outputItems.values()],
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
for await (const event of iterateServerSentEvents(stream)) {
|
|
109
|
+
if (typeof event.data !== "string" || event.data.length === 0) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const terminal = Boolean((event.event && terminalServerSentEvents.has(event.event)) ||
|
|
113
|
+
isTerminalPayload(event.data));
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(event.data);
|
|
116
|
+
if (!isRecord(parsed)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (event.event === "error") {
|
|
120
|
+
latestError = parsed;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const item = parsed.item;
|
|
124
|
+
if (isRecord(item) && typeof item.id === "string") {
|
|
125
|
+
outputItems.set(item.id, item);
|
|
126
|
+
}
|
|
127
|
+
const response = parsed.response;
|
|
128
|
+
if (isRecord(response)) {
|
|
129
|
+
latestResponse = response;
|
|
130
|
+
}
|
|
131
|
+
if (terminal && latestResponse) {
|
|
132
|
+
return withCollectedOutput(latestResponse);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
if (terminal && latestResponse) {
|
|
137
|
+
return withCollectedOutput(latestResponse);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (latestResponse) {
|
|
141
|
+
return withCollectedOutput(latestResponse);
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`No completed response found in SSE stream.${latestError ? ` Last error: ${JSON.stringify(latestError)}` : ""}`);
|
|
144
|
+
};
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type JsonRecord = Record<string, unknown>;
|
|
2
|
+
export type CodexResponsesStateSnapshot = {
|
|
3
|
+
items: Array<{
|
|
4
|
+
id: string;
|
|
5
|
+
item: JsonRecord;
|
|
6
|
+
}>;
|
|
7
|
+
responses: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
input: unknown[];
|
|
10
|
+
output: JsonRecord[];
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export type CodexResponsesStateOptions = {
|
|
14
|
+
snapshot?: CodexResponsesStateSnapshot;
|
|
15
|
+
onChange?: (snapshot: CodexResponsesStateSnapshot) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare class CodexResponsesState {
|
|
18
|
+
private readonly items;
|
|
19
|
+
private readonly responses;
|
|
20
|
+
private readonly pendingCaptures;
|
|
21
|
+
private readonly onChange?;
|
|
22
|
+
constructor(options?: CodexResponsesStateOptions);
|
|
23
|
+
waitForPendingCaptures(): Promise<void>;
|
|
24
|
+
trackPendingCapture(promise: Promise<void>): void;
|
|
25
|
+
requiresCachedState(body: JsonRecord): boolean;
|
|
26
|
+
expandRequestBody(body: JsonRecord): JsonRecord;
|
|
27
|
+
rememberResponse(response: unknown, requestBody?: JsonRecord): void;
|
|
28
|
+
snapshot(): CodexResponsesStateSnapshot;
|
|
29
|
+
private expandInput;
|
|
30
|
+
private emitChange;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAOzC,MAAM,MAAM,2BAA2B,GAAG;IACzC,KAAK,EAAE,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,UAAU,CAAA;KAChB,CAAC,CAAA;IACF,SAAS,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,EAAE,OAAO,EAAE,CAAA;QAChB,MAAM,EAAE,UAAU,EAAE,CAAA;KACpB,CAAC,CAAA;CACF,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACxC,QAAQ,CAAC,EAAE,2BAA2B,CAAA;IACtC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,CAAA;CAC1D,CAAA;AAwBD,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA2B;IAC3D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAiD;gBAE/D,OAAO,GAAE,0BAA+B;IA0B9C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ7C,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAOjD,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAiB9C,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU;IA+B/C,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,UAAU,GAAG,IAAI;IAiDnE,QAAQ,IAAI,2BAA2B;IAcvC,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,UAAU;CAGlB"}
|