@raybase/openai-subscription 0.0.2 → 0.1.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/ARCHITECTURE.md +80 -0
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/compatibility/fixtures/device-usercode.json +5 -0
- package/compatibility/fixtures/responses-function-call.sse +23 -0
- package/compatibility/fixtures/responses-image-input.sse +15 -0
- package/compatibility/fixtures/responses-image-request.json +32 -0
- package/compatibility/fixtures/responses-invalid-function-parameters.json +8 -0
- package/compatibility/fixtures/responses-invalid-json-schema.json +8 -0
- package/compatibility/fixtures/responses-mixed.sse +41 -0
- package/compatibility/fixtures/responses-multi-tool.sse +35 -0
- package/compatibility/fixtures/responses.sse +26 -0
- package/compatibility/manifest.json +50 -0
- package/dist/crypto.d.ts +12 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +86 -0
- package/dist/crypto.js.map +1 -0
- package/dist/health.d.ts +9 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +22 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +6 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +341 -0
- package/dist/module.js.map +1 -0
- package/dist/provider.d.ts +8 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +361 -0
- package/dist/provider.js.map +1 -0
- package/dist/store.d.ts +51 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +342 -0
- package/dist/store.js.map +1 -0
- package/dist/system-tables.d.ts +7 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +59 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport.d.ts +104 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +1115 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -8
- package/skills/raybase-openai-subscription/SKILL.md +57 -0
- package/skills/raybase-openai-subscription/references/compatibility-and-testing.md +129 -0
- package/skills/raybase-openai-subscription/references/module-contract.md +133 -0
- package/skills/raybase-openai-subscription/references/structured-outputs.md +159 -0
- package/templates/astro-emdash/.agents/skills/raybase-openai-subscription/SKILL.md +57 -0
- package/templates/astro-emdash/.agents/skills/raybase-openai-subscription/references/compatibility-and-testing.md +129 -0
- package/templates/astro-emdash/.agents/skills/raybase-openai-subscription/references/module-contract.md +133 -0
- package/templates/astro-emdash/.agents/skills/raybase-openai-subscription/references/structured-outputs.md +159 -0
- package/templates/astro-emdash/src/openai-subscription.ts +11 -0
- package/templates/react-router/.agents/skills/raybase-openai-subscription/SKILL.md +57 -0
- package/templates/react-router/.agents/skills/raybase-openai-subscription/references/compatibility-and-testing.md +129 -0
- package/templates/react-router/.agents/skills/raybase-openai-subscription/references/module-contract.md +133 -0
- package/templates/react-router/.agents/skills/raybase-openai-subscription/references/structured-outputs.md +159 -0
- package/templates/react-router/app/openai-subscription.ts +11 -0
- package/templates/tanstack-start/.agents/skills/raybase-openai-subscription/SKILL.md +57 -0
- package/templates/tanstack-start/.agents/skills/raybase-openai-subscription/references/compatibility-and-testing.md +129 -0
- package/templates/tanstack-start/.agents/skills/raybase-openai-subscription/references/module-contract.md +133 -0
- package/templates/tanstack-start/.agents/skills/raybase-openai-subscription/references/structured-outputs.md +159 -0
- package/templates/tanstack-start/src/openai-subscription.ts +11 -0
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
import { RaybaseInferenceProviderError } from "@raybase/core";
|
|
2
|
+
const authOrigin = "https://auth.openai.com";
|
|
3
|
+
const codexOrigin = "https://chatgpt.com";
|
|
4
|
+
const localRelaySecretHeader = "x-raybase-local-codex-relay-secret";
|
|
5
|
+
const clientId = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
|
+
/**
|
|
7
|
+
* The complete set of headers the module authors on an outbound request.
|
|
8
|
+
*
|
|
9
|
+
* chatgpt.com is itself behind Cloudflare, and its front door refuses any
|
|
10
|
+
* request carrying Cloudflare's own hop metadata — `cf-worker`, which workerd
|
|
11
|
+
* stamps on every outbound subrequest, and `cf-connecting-ip`/`cf-visitor`. It
|
|
12
|
+
* answers `403` with an HTML blocked page before the Codex API is reached, so
|
|
13
|
+
* no `x-oai-request-id` ever exists. `raybase dev`'s relay strips that class of
|
|
14
|
+
* header at its own boundary, but the deployed `cloudflare-vpc` lane hands the
|
|
15
|
+
* request straight to `binding.fetch()` with no boundary of its own. Pinning
|
|
16
|
+
* the header set here gives every egress strategy the same guarantee.
|
|
17
|
+
*
|
|
18
|
+
* This is not a fingerprint disguise: the module still identifies itself with
|
|
19
|
+
* `originator: raybase_openai_subscription`. It is the removal of intermediary
|
|
20
|
+
* metadata the module never authored, in the same spirit as dropping `host`.
|
|
21
|
+
*
|
|
22
|
+
* Residual risk this cannot cover: anything the runtime appends after `fetch()`
|
|
23
|
+
* leaves the module's hands. Only one real turn through `CODEX_EGRESS` on a
|
|
24
|
+
* deployed Worker can rule that out — see README "Live Worker verification".
|
|
25
|
+
*/
|
|
26
|
+
const outboundHeaderAllowlist = new Set([
|
|
27
|
+
"content-type",
|
|
28
|
+
"authorization",
|
|
29
|
+
"chatgpt-account-id",
|
|
30
|
+
"originator",
|
|
31
|
+
"accept"
|
|
32
|
+
]);
|
|
33
|
+
const maxJsonBytes = 256 * 1024;
|
|
34
|
+
const maxSseBytes = 4 * 1024 * 1024;
|
|
35
|
+
const maxSseEventBytes = 256 * 1024;
|
|
36
|
+
const codex400JsonShapeErrorCodes = new Set([
|
|
37
|
+
"protocol:invalid_json",
|
|
38
|
+
"protocol:json_size_limit",
|
|
39
|
+
"protocol:expected_object"
|
|
40
|
+
]);
|
|
41
|
+
export class OpenAISubscriptionTransportHttpError extends Error {
|
|
42
|
+
status;
|
|
43
|
+
code;
|
|
44
|
+
retryAt;
|
|
45
|
+
contentType;
|
|
46
|
+
cfRay;
|
|
47
|
+
oaiRequestId;
|
|
48
|
+
param;
|
|
49
|
+
constructor(status, code, retryAt, contentType, cfRay, oaiRequestId,
|
|
50
|
+
/**
|
|
51
|
+
* The upstream `error.param` when it names a structural part of the request
|
|
52
|
+
* the app itself controls — `tools[7].parameters.draft`. It is the app's own
|
|
53
|
+
* schema path, never user or account data, and it is the difference between
|
|
54
|
+
* "one of your tools is wrong" and knowing which one.
|
|
55
|
+
*/
|
|
56
|
+
param) {
|
|
57
|
+
super(`OpenAI subscription transport failed (${status})`);
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.retryAt = retryAt;
|
|
61
|
+
this.contentType = contentType;
|
|
62
|
+
this.cfRay = cfRay;
|
|
63
|
+
this.oaiRequestId = oaiRequestId;
|
|
64
|
+
this.param = param;
|
|
65
|
+
this.name = "OpenAISubscriptionTransportHttpError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class OpenAISubscriptionCodexRequestError extends RaybaseInferenceProviderError {
|
|
69
|
+
status = 400;
|
|
70
|
+
contentType;
|
|
71
|
+
oaiRequestId;
|
|
72
|
+
constructor(token, metadata = {}) {
|
|
73
|
+
super({
|
|
74
|
+
code: `protocol:codex_400_${token}`,
|
|
75
|
+
message: "The OpenAI subscription request was rejected."
|
|
76
|
+
});
|
|
77
|
+
this.name = "OpenAISubscriptionCodexRequestError";
|
|
78
|
+
this.contentType = metadata.contentType;
|
|
79
|
+
this.oaiRequestId = metadata.oaiRequestId;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class OpenAISubscriptionTransportProtocolError extends RaybaseInferenceProviderError {
|
|
83
|
+
status;
|
|
84
|
+
contentType;
|
|
85
|
+
cfRayPresent;
|
|
86
|
+
redirected;
|
|
87
|
+
oaiRequestId;
|
|
88
|
+
constructor(fingerprint, metadata = {}) {
|
|
89
|
+
super({
|
|
90
|
+
code: `protocol:${fingerprint}`,
|
|
91
|
+
health: "protocol_update_required",
|
|
92
|
+
actionHref: "/admin/openai-subscription",
|
|
93
|
+
message: "The OpenAI subscription protocol changed. Update the Raybase module."
|
|
94
|
+
});
|
|
95
|
+
this.name = "OpenAISubscriptionTransportProtocolError";
|
|
96
|
+
this.status = metadata.status;
|
|
97
|
+
this.contentType = metadata.contentType;
|
|
98
|
+
this.cfRayPresent = metadata.cfRayPresent;
|
|
99
|
+
this.redirected = metadata.redirected;
|
|
100
|
+
this.oaiRequestId = metadata.oaiRequestId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export class CodexSubscriptionTransport {
|
|
104
|
+
#authFetch;
|
|
105
|
+
#codexFetch;
|
|
106
|
+
#now;
|
|
107
|
+
#authOrigin;
|
|
108
|
+
#codexOrigin;
|
|
109
|
+
#requestTimeoutMs;
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
const sharedFetch = options.fetch ?? fetch;
|
|
112
|
+
this.#authFetch = options.authFetch ?? sharedFetch;
|
|
113
|
+
this.#codexFetch = options.codexFetch ?? sharedFetch;
|
|
114
|
+
this.#now = options.now ?? (() => new Date());
|
|
115
|
+
this.#authOrigin = normalizeOrigin(options.endpoints?.authOrigin ?? authOrigin, "auth origin");
|
|
116
|
+
this.#codexOrigin = normalizeOrigin(options.endpoints?.codexOrigin ?? codexOrigin, "Codex origin");
|
|
117
|
+
this.#requestTimeoutMs = options.requestTimeoutMs ?? 45_000;
|
|
118
|
+
if (!Number.isFinite(this.#requestTimeoutMs) || this.#requestTimeoutMs <= 0) {
|
|
119
|
+
throw new Error("OpenAI subscription request timeout must be positive");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async startDeviceAuth(signal) {
|
|
123
|
+
const payload = await this.#json(endpoint(this.#authOrigin, "/api/accounts/deviceauth/usercode"), { client_id: clientId }, {}, signal);
|
|
124
|
+
const deviceAuthId = requiredString(payload, "device_auth_id");
|
|
125
|
+
const userCode = requiredString(payload, "user_code");
|
|
126
|
+
const intervalSeconds = boundedInteger(payload.interval, 1, 60, "device interval");
|
|
127
|
+
return {
|
|
128
|
+
deviceAuthId,
|
|
129
|
+
userCode,
|
|
130
|
+
verificationUrl: endpoint(this.#authOrigin, "/codex/device"),
|
|
131
|
+
intervalSeconds,
|
|
132
|
+
expiresAt: new Date(this.#now().getTime() + 15 * 60_000).toISOString()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async pollDeviceAuth(deviceAuthId, userCode, intervalSeconds, signal) {
|
|
136
|
+
const request = await this.#request(endpoint(this.#authOrigin, "/api/accounts/deviceauth/token"), { device_auth_id: deviceAuthId, user_code: userCode }, {}, signal);
|
|
137
|
+
try {
|
|
138
|
+
if (request.response.status === 403 || request.response.status === 404) {
|
|
139
|
+
return { status: "pending", intervalSeconds };
|
|
140
|
+
}
|
|
141
|
+
if (request.response.status === 429) {
|
|
142
|
+
return { status: "pending", intervalSeconds: Math.min(intervalSeconds + 5, 60) };
|
|
143
|
+
}
|
|
144
|
+
if (!request.response.ok)
|
|
145
|
+
throw statusError(request.response);
|
|
146
|
+
const payload = await strictJsonResponse(request.response, request.signal);
|
|
147
|
+
return {
|
|
148
|
+
status: "authorized",
|
|
149
|
+
authorizationCode: requiredString(payload, "authorization_code"),
|
|
150
|
+
codeVerifier: requiredString(payload, "code_verifier"),
|
|
151
|
+
codeChallenge: requiredString(payload, "code_challenge")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
request.cleanup();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async exchangeDeviceCode(result, signal) {
|
|
159
|
+
return this.#tokenRequest({
|
|
160
|
+
grant_type: "authorization_code",
|
|
161
|
+
client_id: clientId,
|
|
162
|
+
code: result.authorizationCode,
|
|
163
|
+
code_verifier: result.codeVerifier,
|
|
164
|
+
redirect_uri: endpoint(this.#authOrigin, "/deviceauth/callback")
|
|
165
|
+
}, signal);
|
|
166
|
+
}
|
|
167
|
+
async refresh(refreshToken, signal) {
|
|
168
|
+
return this.#tokenRequest({
|
|
169
|
+
grant_type: "refresh_token",
|
|
170
|
+
client_id: clientId,
|
|
171
|
+
refresh_token: refreshToken
|
|
172
|
+
}, signal);
|
|
173
|
+
}
|
|
174
|
+
async chat(tokens, input, signal) {
|
|
175
|
+
let text = "";
|
|
176
|
+
const toolCalls = [];
|
|
177
|
+
let result = {};
|
|
178
|
+
for await (const event of await this.chatStream(tokens, input, signal)) {
|
|
179
|
+
if (event.type === "text-delta")
|
|
180
|
+
text += event.text;
|
|
181
|
+
if (event.type === "tool-call")
|
|
182
|
+
toolCalls.push(event.toolCall);
|
|
183
|
+
if (event.type === "done")
|
|
184
|
+
result = event.result ?? {};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
text: typeof result.text === "string" ? result.text : text,
|
|
188
|
+
toolCalls: result.toolCalls ?? toolCalls,
|
|
189
|
+
finishReason: result.finishReason ?? "stop",
|
|
190
|
+
usage: result.usage,
|
|
191
|
+
provider: result.provider
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async chatStream(tokens, input, signal) {
|
|
195
|
+
const request = await this.#streamRequest(endpoint(this.#codexOrigin, "/backend-api/codex/responses"), toResponsesRequest(input), {
|
|
196
|
+
authorization: `Bearer ${tokens.accessToken}`,
|
|
197
|
+
...(tokens.accountId ? { "chatgpt-account-id": tokens.accountId } : {}),
|
|
198
|
+
originator: "raybase_openai_subscription",
|
|
199
|
+
accept: "text/event-stream"
|
|
200
|
+
}, signal);
|
|
201
|
+
if (!request.response.ok) {
|
|
202
|
+
try {
|
|
203
|
+
throw await codexStatusError(request.response, request.signal);
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
void request.response.body?.cancel().catch(() => undefined);
|
|
207
|
+
request.cleanup();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const contentType = normalizedContentType(request.response);
|
|
211
|
+
if (contentType && contentType !== "text/event-stream") {
|
|
212
|
+
void request.response.body?.cancel().catch(() => undefined);
|
|
213
|
+
request.cleanup();
|
|
214
|
+
throw protocolError("unexpected_sse_content_type", {
|
|
215
|
+
status: request.response.status,
|
|
216
|
+
contentType,
|
|
217
|
+
cfRayPresent: Boolean(request.response.headers.get("cf-ray")?.trim()),
|
|
218
|
+
redirected: request.response.redirected
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!request.response.body) {
|
|
222
|
+
request.cleanup();
|
|
223
|
+
throw protocolError("missing_sse_body");
|
|
224
|
+
}
|
|
225
|
+
return parseResponsesSse(request.response.body, request.signal, request.cleanup);
|
|
226
|
+
}
|
|
227
|
+
async #tokenRequest(body, signal) {
|
|
228
|
+
const request = await this.#request(endpoint(this.#authOrigin, "/oauth/token"), body, {}, signal);
|
|
229
|
+
try {
|
|
230
|
+
if (!request.response.ok) {
|
|
231
|
+
if (request.response.status === 400) {
|
|
232
|
+
const payload = await strictJsonResponse(request.response, request.signal);
|
|
233
|
+
if (payload.error === "invalid_grant") {
|
|
234
|
+
throw new OpenAISubscriptionTransportHttpError(400, "invalid_grant");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw statusError(request.response);
|
|
238
|
+
}
|
|
239
|
+
const payload = await strictJsonResponse(request.response, request.signal);
|
|
240
|
+
const accessToken = requiredString(payload, "access_token");
|
|
241
|
+
const refreshToken = typeof payload.refresh_token === "string" && payload.refresh_token
|
|
242
|
+
? payload.refresh_token
|
|
243
|
+
: body.refresh_token;
|
|
244
|
+
if (!refreshToken)
|
|
245
|
+
throw protocolError("missing_refresh_token");
|
|
246
|
+
const expiresIn = boundedInteger(payload.expires_in, 60, 31_536_000, "token expiry");
|
|
247
|
+
const idToken = typeof payload.id_token === "string" ? payload.id_token : undefined;
|
|
248
|
+
const claims = idToken ? decodeJwtPayload(idToken) : {};
|
|
249
|
+
return {
|
|
250
|
+
accessToken,
|
|
251
|
+
refreshToken,
|
|
252
|
+
...(idToken ? { idToken } : {}),
|
|
253
|
+
expiresAt: new Date(this.#now().getTime() + expiresIn * 1000).toISOString(),
|
|
254
|
+
...safeIdentityProjection(claims)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
request.cleanup();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async #json(url, body, headers, signal) {
|
|
262
|
+
const request = await this.#request(url, body, headers, signal);
|
|
263
|
+
try {
|
|
264
|
+
if (!request.response.ok)
|
|
265
|
+
throw statusError(request.response);
|
|
266
|
+
return await strictJsonResponse(request.response, request.signal);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
request.cleanup();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async #request(url, body, headers, signal) {
|
|
273
|
+
const request = this.#requestContext(signal);
|
|
274
|
+
try {
|
|
275
|
+
return {
|
|
276
|
+
response: await this.#performRequest(url, body, headers, request.controller.signal),
|
|
277
|
+
signal: request.controller.signal,
|
|
278
|
+
cleanup: request.cleanup
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
request.cleanup();
|
|
283
|
+
throw requestError(error, request.controller.signal);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async #streamRequest(url, body, headers, signal) {
|
|
287
|
+
const request = this.#requestContext(signal);
|
|
288
|
+
try {
|
|
289
|
+
return {
|
|
290
|
+
response: await this.#performRequest(url, body, headers, request.controller.signal),
|
|
291
|
+
signal: request.controller.signal,
|
|
292
|
+
cleanup: request.cleanup
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
request.cleanup();
|
|
297
|
+
throw requestError(error, request.controller.signal);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async #performRequest(url, body, headers, signal) {
|
|
301
|
+
const parsed = new URL(url);
|
|
302
|
+
const destination = assertAllowedUrl(parsed, this.#authOrigin, this.#codexOrigin, "POST");
|
|
303
|
+
const selectedFetch = destination === "auth" ? this.#authFetch : this.#codexFetch;
|
|
304
|
+
return selectedFetch(parsed, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
redirect: "manual",
|
|
307
|
+
headers: allowlistedOutboundHeaders({
|
|
308
|
+
"content-type": "application/json",
|
|
309
|
+
...headers
|
|
310
|
+
}),
|
|
311
|
+
body: JSON.stringify(body),
|
|
312
|
+
signal
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
#requestContext(signal) {
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
const timer = setTimeout(() => controller.abort(), this.#requestTimeoutMs);
|
|
318
|
+
const abort = () => controller.abort();
|
|
319
|
+
if (signal?.aborted)
|
|
320
|
+
controller.abort();
|
|
321
|
+
else
|
|
322
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
323
|
+
let cleaned = false;
|
|
324
|
+
return {
|
|
325
|
+
controller,
|
|
326
|
+
cleanup: () => {
|
|
327
|
+
if (cleaned)
|
|
328
|
+
return;
|
|
329
|
+
cleaned = true;
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
signal?.removeEventListener("abort", abort);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
export function createOpenAISubscriptionTransport(options, env) {
|
|
337
|
+
return createOpenAISubscriptionTransportInternal(options, env);
|
|
338
|
+
}
|
|
339
|
+
export function createOpenAISubscriptionTransportInternal(options, env, testingOptions) {
|
|
340
|
+
if (testingOptions)
|
|
341
|
+
return new CodexSubscriptionTransport(testingOptions);
|
|
342
|
+
if (!options.codexEgress || options.codexEgress.strategy === "direct") {
|
|
343
|
+
return new CodexSubscriptionTransport();
|
|
344
|
+
}
|
|
345
|
+
if (options.codexEgress.strategy === "local-relay") {
|
|
346
|
+
return new CodexSubscriptionTransport({
|
|
347
|
+
codexFetch: localRelayFetch(env)
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const candidate = env[options.codexEgress.binding];
|
|
351
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.fetch !== "function") {
|
|
352
|
+
throw new RaybaseInferenceProviderError({
|
|
353
|
+
code: "codex_egress_binding_missing",
|
|
354
|
+
health: "disconnected",
|
|
355
|
+
actionHref: "/admin/openai-subscription",
|
|
356
|
+
message: "The configured OpenAI subscription Codex egress binding is unavailable."
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const fetcher = candidate;
|
|
360
|
+
return new CodexSubscriptionTransport({
|
|
361
|
+
codexFetch: (input, init) => fetcher.fetch(input, init)
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function localRelayFetch(env) {
|
|
365
|
+
if (env.RAYBASE_ENV !== "local") {
|
|
366
|
+
throw new RaybaseInferenceProviderError({
|
|
367
|
+
code: "codex_local_relay_not_local",
|
|
368
|
+
health: "disconnected",
|
|
369
|
+
actionHref: "/admin/openai-subscription",
|
|
370
|
+
message: "OpenAI subscription local-relay egress is available only under raybase dev. Use cloudflare-vpc for deploy targets."
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const relayOrigin = normalizeLocalRelayOrigin(env.RAYBASE_LOCAL_CODEX_RELAY_URL);
|
|
374
|
+
const relaySecret = env.RAYBASE_LOCAL_CODEX_RELAY_SECRET?.trim();
|
|
375
|
+
if (!relayOrigin || !relaySecret || !/^[A-Za-z0-9_-]{43,}$/.test(relaySecret)) {
|
|
376
|
+
throw new RaybaseInferenceProviderError({
|
|
377
|
+
code: "codex_local_relay_unavailable",
|
|
378
|
+
health: "disconnected",
|
|
379
|
+
actionHref: "/admin/openai-subscription",
|
|
380
|
+
message: "The raybase dev Codex relay is unavailable. Restart raybase dev."
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return (input, init) => {
|
|
384
|
+
const target = requestUrl(input);
|
|
385
|
+
if (target.origin !== codexOrigin || target.pathname !== "/backend-api/codex/responses" || target.search || target.hash) {
|
|
386
|
+
throw new Error("OpenAI subscription local relay target is not allowlisted");
|
|
387
|
+
}
|
|
388
|
+
const headers = new Headers(init?.headers);
|
|
389
|
+
headers.set(localRelaySecretHeader, relaySecret);
|
|
390
|
+
return fetch(new URL(target.pathname, relayOrigin), { ...init, headers, redirect: "manual" });
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function normalizeLocalRelayOrigin(value) {
|
|
394
|
+
if (!value)
|
|
395
|
+
return null;
|
|
396
|
+
try {
|
|
397
|
+
const parsed = new URL(value);
|
|
398
|
+
if (parsed.protocol !== "http:" ||
|
|
399
|
+
parsed.hostname !== "127.0.0.1" ||
|
|
400
|
+
!parsed.port ||
|
|
401
|
+
parsed.username ||
|
|
402
|
+
parsed.password ||
|
|
403
|
+
parsed.pathname !== "/" ||
|
|
404
|
+
parsed.search ||
|
|
405
|
+
parsed.hash)
|
|
406
|
+
return null;
|
|
407
|
+
return parsed.origin;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Drops rather than throws: an unexpected header is a module-side mistake that
|
|
415
|
+
* tests catch, and a live subscription lane should not fail closed over one.
|
|
416
|
+
* Any header the module genuinely needs must be added to the allowlist above.
|
|
417
|
+
*/
|
|
418
|
+
export function allowlistedOutboundHeaders(source) {
|
|
419
|
+
const headers = {};
|
|
420
|
+
for (const [name, value] of Object.entries(source)) {
|
|
421
|
+
if (!outboundHeaderAllowlist.has(name.toLowerCase()))
|
|
422
|
+
continue;
|
|
423
|
+
headers[name] = value;
|
|
424
|
+
}
|
|
425
|
+
return headers;
|
|
426
|
+
}
|
|
427
|
+
function requestUrl(input) {
|
|
428
|
+
if (input instanceof URL)
|
|
429
|
+
return input;
|
|
430
|
+
if (typeof input === "string")
|
|
431
|
+
return new URL(input);
|
|
432
|
+
return new URL(input.url);
|
|
433
|
+
}
|
|
434
|
+
function requestError(error, signal) {
|
|
435
|
+
if (signal.aborted) {
|
|
436
|
+
return new RaybaseInferenceProviderError({
|
|
437
|
+
code: "aborted",
|
|
438
|
+
message: "OpenAI subscription request was cancelled."
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (error instanceof RaybaseInferenceProviderError)
|
|
442
|
+
return error;
|
|
443
|
+
return new RaybaseInferenceProviderError({ code: "transport_error" });
|
|
444
|
+
}
|
|
445
|
+
async function readStreamChunk(reader, signal) {
|
|
446
|
+
if (!signal)
|
|
447
|
+
return reader.read();
|
|
448
|
+
if (signal.aborted)
|
|
449
|
+
throw requestError(undefined, signal);
|
|
450
|
+
let onAbort;
|
|
451
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
452
|
+
onAbort = () => {
|
|
453
|
+
reject(requestError(undefined, signal));
|
|
454
|
+
void reader.cancel().catch(() => undefined);
|
|
455
|
+
};
|
|
456
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
457
|
+
});
|
|
458
|
+
try {
|
|
459
|
+
return await Promise.race([reader.read(), aborted]);
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
if (onAbort)
|
|
463
|
+
signal.removeEventListener("abort", onAbort);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
export function toResponsesRequest(input) {
|
|
467
|
+
validateChatRequest(input);
|
|
468
|
+
const instructions = input.messages
|
|
469
|
+
.filter((message) => message.role === "system")
|
|
470
|
+
.map((message) => message.content)
|
|
471
|
+
.join("\n\n");
|
|
472
|
+
const messages = input.messages
|
|
473
|
+
.filter((message) => message.role !== "system")
|
|
474
|
+
.flatMap((message) => {
|
|
475
|
+
if (message.role === "tool") {
|
|
476
|
+
return [{ type: "function_call_output", call_id: message.toolCallId, output: message.content }];
|
|
477
|
+
}
|
|
478
|
+
const content = message.role === "user"
|
|
479
|
+
? (typeof message.content === "string"
|
|
480
|
+
? [{ type: "input_text", text: message.content }]
|
|
481
|
+
: message.content.map(toResponsesUserContentPart))
|
|
482
|
+
: [{ type: "output_text", text: message.content }];
|
|
483
|
+
return [
|
|
484
|
+
{ role: message.role, content },
|
|
485
|
+
...(message.role === "assistant"
|
|
486
|
+
? (message.toolCalls ?? []).map((call) => ({
|
|
487
|
+
type: "function_call",
|
|
488
|
+
call_id: call.id,
|
|
489
|
+
name: call.name,
|
|
490
|
+
arguments: JSON.stringify(call.arguments ?? {})
|
|
491
|
+
}))
|
|
492
|
+
: [])
|
|
493
|
+
];
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
model: input.model,
|
|
497
|
+
instructions,
|
|
498
|
+
input: messages,
|
|
499
|
+
tools: (input.tools ?? []).map((tool) => ({
|
|
500
|
+
type: "function",
|
|
501
|
+
name: tool.name,
|
|
502
|
+
description: tool.description ?? "",
|
|
503
|
+
parameters: tool.parameters ?? { type: "object", properties: {} },
|
|
504
|
+
strict: true
|
|
505
|
+
})),
|
|
506
|
+
tool_choice: input.tools?.length ? "auto" : "none",
|
|
507
|
+
parallel_tool_calls: true,
|
|
508
|
+
stream: true,
|
|
509
|
+
store: false,
|
|
510
|
+
...(input.responseFormat ? { text: { format: responseFormat(input.responseFormat) } } : {})
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Verified against the private Codex Responses endpoint on 2026-08-11:
|
|
515
|
+
* `input_image.image_url` accepts HTTPS/data URIs and PNG, JPEG, WEBP, and
|
|
516
|
+
* non-animated GIF. Omitted/auto/low/high/original `detail` completed;
|
|
517
|
+
* `quality` was rejected. Valid PNG requests completed through 19,573,864
|
|
518
|
+
* encoded request bytes, a lower bound rather than an endpoint limit. See the
|
|
519
|
+
* compatibility note; Raybase deliberately omits both knobs.
|
|
520
|
+
*/
|
|
521
|
+
function toResponsesUserContentPart(part) {
|
|
522
|
+
if (part.type === "text")
|
|
523
|
+
return { type: "input_text", text: part.text };
|
|
524
|
+
if (part.type === "image") {
|
|
525
|
+
return { type: "input_image", image_url: `data:${part.mediaType};base64,${part.data}` };
|
|
526
|
+
}
|
|
527
|
+
return { type: "input_image", image_url: part.url };
|
|
528
|
+
}
|
|
529
|
+
export async function* parseResponsesSse(body, signal, cleanup = () => undefined) {
|
|
530
|
+
const reader = body.getReader();
|
|
531
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
532
|
+
let buffered = "";
|
|
533
|
+
let total = 0;
|
|
534
|
+
let completed = false;
|
|
535
|
+
const toolArguments = new Map();
|
|
536
|
+
try {
|
|
537
|
+
while (true) {
|
|
538
|
+
if (signal?.aborted)
|
|
539
|
+
throw new RaybaseInferenceProviderError({ code: "aborted", message: "OpenAI subscription stream was cancelled." });
|
|
540
|
+
const { done, value } = await readStreamChunk(reader, signal);
|
|
541
|
+
if (done)
|
|
542
|
+
break;
|
|
543
|
+
total += value.byteLength;
|
|
544
|
+
if (total > maxSseBytes)
|
|
545
|
+
throw protocolError("sse_size_limit");
|
|
546
|
+
buffered += decoder.decode(value, { stream: true });
|
|
547
|
+
let boundary;
|
|
548
|
+
while ((boundary = buffered.search(/\r?\n\r?\n/)) >= 0) {
|
|
549
|
+
const raw = buffered.slice(0, boundary);
|
|
550
|
+
const match = buffered.slice(boundary).match(/^\r?\n\r?\n/);
|
|
551
|
+
buffered = buffered.slice(boundary + (match?.[0].length ?? 2));
|
|
552
|
+
if (raw.length > maxSseEventBytes)
|
|
553
|
+
throw protocolError("sse_event_size_limit");
|
|
554
|
+
const parsed = parseSseEvent(raw);
|
|
555
|
+
if (!parsed)
|
|
556
|
+
continue;
|
|
557
|
+
const event = parseEventJson(parsed.data);
|
|
558
|
+
const kind = requiredString(event, "type");
|
|
559
|
+
if (kind === "response.created" || kind === "response.in_progress")
|
|
560
|
+
continue;
|
|
561
|
+
if (kind === "response.output_item.added") {
|
|
562
|
+
const item = record(event.item);
|
|
563
|
+
if (!["message", "reasoning", "function_call"].includes(requiredString(item, "type"))) {
|
|
564
|
+
throw protocolError("unknown_output_item");
|
|
565
|
+
}
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (kind === "response.output_text.delta") {
|
|
569
|
+
yield { type: "text-delta", text: requiredString(event, "delta", true) };
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (kind === "response.function_call_arguments.delta") {
|
|
573
|
+
const id = optionalString(event.call_id) ?? optionalString(event.item_id);
|
|
574
|
+
if (!id)
|
|
575
|
+
throw protocolError("missing_tool_call_id");
|
|
576
|
+
const current = toolArguments.get(id) ?? { name: optionalString(event.name) ?? "", value: "" };
|
|
577
|
+
current.value += requiredString(event, "delta", true);
|
|
578
|
+
if (current.value.length > maxSseEventBytes)
|
|
579
|
+
throw protocolError("tool_arguments_size_limit");
|
|
580
|
+
toolArguments.set(id, current);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (kind === "response.output_item.done") {
|
|
584
|
+
const item = record(event.item);
|
|
585
|
+
if (item.type === "message" || item.type === "reasoning")
|
|
586
|
+
continue;
|
|
587
|
+
if (item.type !== "function_call")
|
|
588
|
+
throw protocolError("unknown_output_item");
|
|
589
|
+
const id = optionalString(item.call_id) ?? optionalString(item.id);
|
|
590
|
+
const name = optionalString(item.name);
|
|
591
|
+
if (!id || !name)
|
|
592
|
+
throw protocolError("malformed_tool_call");
|
|
593
|
+
const rawArguments = optionalString(item.arguments) ?? toolArguments.get(id)?.value ?? "{}";
|
|
594
|
+
let args;
|
|
595
|
+
try {
|
|
596
|
+
args = JSON.parse(rawArguments);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
throw protocolError("invalid_tool_arguments");
|
|
600
|
+
}
|
|
601
|
+
yield { type: "tool-call", toolCall: { id, name, arguments: args } };
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (kind === "response.completed") {
|
|
605
|
+
const response = record(event.response);
|
|
606
|
+
const usage = record(response.usage);
|
|
607
|
+
const incompleteDetails = response.incomplete_details && typeof response.incomplete_details === "object"
|
|
608
|
+
? record(response.incomplete_details)
|
|
609
|
+
: {};
|
|
610
|
+
completed = true;
|
|
611
|
+
yield {
|
|
612
|
+
type: "done",
|
|
613
|
+
result: {
|
|
614
|
+
finishReason: incompleteDetails.reason === "max_output_tokens" ? "length" : "stop",
|
|
615
|
+
usage: {
|
|
616
|
+
inputTokens: optionalNonNegativeInteger(usage.input_tokens),
|
|
617
|
+
outputTokens: optionalNonNegativeInteger(usage.output_tokens)
|
|
618
|
+
},
|
|
619
|
+
provider: {
|
|
620
|
+
id: "openai-codex-subscription",
|
|
621
|
+
requestId: optionalString(response.id)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (kind === "response.failed" || kind === "error") {
|
|
628
|
+
throw upstreamFailedError(event);
|
|
629
|
+
}
|
|
630
|
+
if (kind === "response.reasoning_summary_text.delta"
|
|
631
|
+
|| kind === "response.reasoning_summary_part.added"
|
|
632
|
+
|| kind === "response.reasoning_summary_text.done"
|
|
633
|
+
|| kind === "response.content_part.added"
|
|
634
|
+
|| kind === "response.content_part.done"
|
|
635
|
+
|| kind === "response.output_text.done"
|
|
636
|
+
// The terminator for a tool call's streamed arguments. It repeats
|
|
637
|
+
// what the deltas already accumulated, and `response.output_item.done`
|
|
638
|
+
// carries the authoritative `item.arguments`, so there is nothing to
|
|
639
|
+
// read here. Without this branch every tool-using turn dies on a
|
|
640
|
+
// complete, successful HTTP 200 stream.
|
|
641
|
+
|| kind === "response.function_call_arguments.done")
|
|
642
|
+
continue;
|
|
643
|
+
throw protocolError("unknown_event");
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
buffered += decoder.decode();
|
|
647
|
+
if (buffered.trim())
|
|
648
|
+
throw protocolError("unterminated_sse_event");
|
|
649
|
+
if (!completed)
|
|
650
|
+
throw protocolError("stream_ended_without_completion");
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
if (error instanceof RaybaseInferenceProviderError)
|
|
654
|
+
throw error;
|
|
655
|
+
throw protocolError("invalid_sse");
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
cleanup();
|
|
659
|
+
try {
|
|
660
|
+
reader.releaseLock();
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// An abort-driven cancel may still be settling; cleanup has already run.
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function validateChatRequest(input) {
|
|
668
|
+
if (!input.model.trim())
|
|
669
|
+
throw protocolError("missing_model");
|
|
670
|
+
if (!input.messages.length)
|
|
671
|
+
throw protocolError("missing_messages");
|
|
672
|
+
const callIds = new Set();
|
|
673
|
+
for (const [index, message] of input.messages.entries()) {
|
|
674
|
+
if (!message || typeof message !== "object" || !["system", "user", "assistant", "tool"].includes(message.role)) {
|
|
675
|
+
throw protocolError("invalid_message_role");
|
|
676
|
+
}
|
|
677
|
+
if (message.role === "user") {
|
|
678
|
+
if (typeof message.content !== "string")
|
|
679
|
+
validateUserContentParts(message.content);
|
|
680
|
+
}
|
|
681
|
+
else if (typeof message.content !== "string") {
|
|
682
|
+
throw callerContentError("invalid_message_content", `OpenAI subscription ${message.role} message content must be a string.`);
|
|
683
|
+
}
|
|
684
|
+
if (message.role === "system" && index > 0 && input.messages[index - 1]?.role !== "system") {
|
|
685
|
+
throw protocolError("system_message_order");
|
|
686
|
+
}
|
|
687
|
+
if (message.role === "assistant") {
|
|
688
|
+
for (const call of message.toolCalls ?? []) {
|
|
689
|
+
if (!call.id || callIds.has(call.id) || !/^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(call.name)) {
|
|
690
|
+
throw protocolError("invalid_tool_call");
|
|
691
|
+
}
|
|
692
|
+
callIds.add(call.id);
|
|
693
|
+
try {
|
|
694
|
+
JSON.stringify(call.arguments ?? {});
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
throw protocolError("invalid_tool_arguments");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (message.role === "tool" && (!message.toolCallId || !callIds.has(message.toolCallId))) {
|
|
702
|
+
throw protocolError("orphan_tool_result");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const names = new Set();
|
|
706
|
+
for (const tool of input.tools ?? []) {
|
|
707
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(tool.name) || names.has(tool.name))
|
|
708
|
+
throw protocolError("invalid_tool_name");
|
|
709
|
+
names.add(tool.name);
|
|
710
|
+
if (tool.parameters !== undefined && (!tool.parameters || typeof tool.parameters !== "object" || Array.isArray(tool.parameters))) {
|
|
711
|
+
throw protocolError("invalid_tool_schema");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (input.responseFormat?.type === "json_schema") {
|
|
715
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(input.responseFormat.name)) {
|
|
716
|
+
throw protocolError("invalid_response_schema_name");
|
|
717
|
+
}
|
|
718
|
+
if (!input.responseFormat.schema || typeof input.responseFormat.schema !== "object" || Array.isArray(input.responseFormat.schema)) {
|
|
719
|
+
throw protocolError("invalid_response_schema");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const supportedImageMediaTypes = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
724
|
+
const supportedUserContentKinds = ["text", "image", "image_url"];
|
|
725
|
+
function validateUserContentParts(content) {
|
|
726
|
+
if (!Array.isArray(content)) {
|
|
727
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription user message content must be a string or an array of content parts.");
|
|
728
|
+
}
|
|
729
|
+
for (const part of content)
|
|
730
|
+
validateUserContentPart(part);
|
|
731
|
+
}
|
|
732
|
+
function validateUserContentPart(value) {
|
|
733
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
734
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription user content parts must be objects with a string type.");
|
|
735
|
+
}
|
|
736
|
+
const part = value;
|
|
737
|
+
if (typeof part.type !== "string" || !part.type) {
|
|
738
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription user content parts must have a non-empty string type.");
|
|
739
|
+
}
|
|
740
|
+
if (!supportedUserContentKinds.includes(part.type)) {
|
|
741
|
+
throw callerContentError("unsupported_message_content", `OpenAI subscription does not support user content part "${part.type}"; supported kinds: ${supportedUserContentKinds.join(", ")}.`);
|
|
742
|
+
}
|
|
743
|
+
if (part.type === "text") {
|
|
744
|
+
assertExactContentFields(part, ["type", "text"]);
|
|
745
|
+
if (typeof part.text !== "string") {
|
|
746
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription text content requires a string text field.");
|
|
747
|
+
}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (part.type === "image") {
|
|
751
|
+
assertExactContentFields(part, ["type", "mediaType", "data"]);
|
|
752
|
+
const mediaType = validateImageMediaType(part.mediaType, "image mediaType");
|
|
753
|
+
if (typeof part.data !== "string" || !isStandardBase64(part.data)) {
|
|
754
|
+
const suffix = typeof part.data === "string" && /[-_]/.test(part.data)
|
|
755
|
+
? " URL-safe base64 is not supported; normalize it to standard base64 first."
|
|
756
|
+
: "";
|
|
757
|
+
throw callerContentError("invalid_message_content", `OpenAI subscription inline ${mediaType} image data must be non-empty standard base64.${suffix}`);
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
assertExactContentFields(part, ["type", "url", "mediaType"]);
|
|
762
|
+
if (part.mediaType !== undefined)
|
|
763
|
+
validateImageMediaType(part.mediaType, "image_url mediaType");
|
|
764
|
+
if (typeof part.url !== "string" || !part.url) {
|
|
765
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription image_url content requires a non-empty URL string.");
|
|
766
|
+
}
|
|
767
|
+
const dataUri = parseImageDataUri(part.url);
|
|
768
|
+
if (dataUri) {
|
|
769
|
+
if (part.mediaType !== undefined && part.mediaType !== dataUri.mediaType) {
|
|
770
|
+
throw callerContentError("invalid_message_content", `OpenAI subscription image_url mediaType "${part.mediaType}" does not match data URI media type "${dataUri.mediaType}".`);
|
|
771
|
+
}
|
|
772
|
+
if (!isStandardBase64(dataUri.data)) {
|
|
773
|
+
const suffix = /[-_]/.test(dataUri.data)
|
|
774
|
+
? " URL-safe base64 is not supported; normalize it to standard base64 first."
|
|
775
|
+
: "";
|
|
776
|
+
throw callerContentError("invalid_message_content", `OpenAI subscription ${dataUri.mediaType} data URI must contain non-empty standard base64.${suffix}`);
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
let url;
|
|
781
|
+
try {
|
|
782
|
+
url = new URL(part.url);
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription image_url must be an absolute HTTPS URL or a supported image data URI.");
|
|
786
|
+
}
|
|
787
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
788
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription image_url must be an absolute HTTPS URL without embedded credentials or a supported image data URI.");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function assertExactContentFields(part, allowed) {
|
|
792
|
+
const extra = Object.keys(part).find((field) => !allowed.includes(field));
|
|
793
|
+
if (extra) {
|
|
794
|
+
throw callerContentError("unsupported_message_content_field", `OpenAI subscription user content part "${String(part.type)}" does not support field "${extra}".`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function validateImageMediaType(value, label) {
|
|
798
|
+
if (typeof value !== "string" || !supportedImageMediaTypes.has(value)) {
|
|
799
|
+
throw callerContentError("invalid_message_content", `OpenAI subscription ${label} "${String(value)}" is unsupported; expected one of: ${[...supportedImageMediaTypes].join(", ")}.`);
|
|
800
|
+
}
|
|
801
|
+
return value;
|
|
802
|
+
}
|
|
803
|
+
function parseImageDataUri(value) {
|
|
804
|
+
if (!value.startsWith("data:"))
|
|
805
|
+
return undefined;
|
|
806
|
+
const match = /^data:(image\/[a-z0-9.+-]+);base64,(.*)$/.exec(value);
|
|
807
|
+
if (!match) {
|
|
808
|
+
throw callerContentError("invalid_message_content", "OpenAI subscription image data URIs must use data:<supported-image-mime>;base64,<standard-base64>.");
|
|
809
|
+
}
|
|
810
|
+
const mediaType = validateImageMediaType(match[1], "data URI media type");
|
|
811
|
+
return { mediaType, data: match[2] };
|
|
812
|
+
}
|
|
813
|
+
function isStandardBase64(value) {
|
|
814
|
+
if (!value || !/^[A-Za-z0-9+/]+={0,2}$/.test(value))
|
|
815
|
+
return false;
|
|
816
|
+
const paddingLength = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
817
|
+
const coreLength = value.length - paddingLength;
|
|
818
|
+
if (coreLength % 4 === 1)
|
|
819
|
+
return false;
|
|
820
|
+
if (paddingLength && value.length % 4 !== 0)
|
|
821
|
+
return false;
|
|
822
|
+
return paddingLength !== 2 || coreLength % 4 === 2;
|
|
823
|
+
}
|
|
824
|
+
function callerContentError(code, message) {
|
|
825
|
+
return new RaybaseInferenceProviderError({ code, message });
|
|
826
|
+
}
|
|
827
|
+
function responseFormat(format) {
|
|
828
|
+
if (!format)
|
|
829
|
+
return {};
|
|
830
|
+
if (format.type === "json_object")
|
|
831
|
+
return { type: "json_object" };
|
|
832
|
+
return {
|
|
833
|
+
type: "json_schema",
|
|
834
|
+
name: format.name,
|
|
835
|
+
schema: format.schema,
|
|
836
|
+
strict: format.strict ?? true,
|
|
837
|
+
...(format.description ? { description: format.description } : {})
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
export function normalizeOrigin(value, label = "origin") {
|
|
841
|
+
let parsed;
|
|
842
|
+
try {
|
|
843
|
+
parsed = new URL(value);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
throw new Error(`OpenAI subscription ${label} is invalid`);
|
|
847
|
+
}
|
|
848
|
+
if (parsed.protocol !== "https:"
|
|
849
|
+
|| parsed.username
|
|
850
|
+
|| parsed.password
|
|
851
|
+
|| parsed.search
|
|
852
|
+
|| parsed.hash
|
|
853
|
+
|| parsed.pathname !== "/"
|
|
854
|
+
|| !parsed.hostname
|
|
855
|
+
|| parsed.hostname.endsWith(".")
|
|
856
|
+
|| parsed.hostname.includes("%")
|
|
857
|
+
|| parsed.hostname.split(".").some((labelPart) => !labelPart || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(labelPart))) {
|
|
858
|
+
throw new Error(`OpenAI subscription ${label} must be a bare HTTPS origin`);
|
|
859
|
+
}
|
|
860
|
+
return parsed.origin;
|
|
861
|
+
}
|
|
862
|
+
function endpoint(origin, path) {
|
|
863
|
+
return new URL(path, `${origin}/`).href;
|
|
864
|
+
}
|
|
865
|
+
function assertAllowedUrl(url, expectedAuthOrigin, expectedCodexOrigin, method) {
|
|
866
|
+
if (method !== "POST"
|
|
867
|
+
||
|
|
868
|
+
url.protocol !== "https:"
|
|
869
|
+
|| url.username
|
|
870
|
+
|| url.password
|
|
871
|
+
|| url.search
|
|
872
|
+
|| url.hash)
|
|
873
|
+
throw new Error("OpenAI subscription transport URL is not allowlisted");
|
|
874
|
+
if (url.origin === expectedAuthOrigin && [
|
|
875
|
+
"/api/accounts/deviceauth/usercode",
|
|
876
|
+
"/api/accounts/deviceauth/token",
|
|
877
|
+
"/oauth/token",
|
|
878
|
+
].includes(url.pathname))
|
|
879
|
+
return "auth";
|
|
880
|
+
if (url.origin === expectedCodexOrigin && url.pathname === "/backend-api/codex/responses")
|
|
881
|
+
return "codex";
|
|
882
|
+
throw new Error("OpenAI subscription transport URL is not allowlisted");
|
|
883
|
+
}
|
|
884
|
+
export function assertOpenAISubscriptionUrlForTesting(value, origins = {}, method = "POST") {
|
|
885
|
+
return assertAllowedUrl(new URL(value), normalizeOrigin(origins.authOrigin ?? authOrigin, "auth origin"), normalizeOrigin(origins.codexOrigin ?? codexOrigin, "Codex origin"), method);
|
|
886
|
+
}
|
|
887
|
+
async function strictJsonResponse(response, signal) {
|
|
888
|
+
const length = Number(response.headers.get("content-length") ?? "0");
|
|
889
|
+
if (length > maxJsonBytes) {
|
|
890
|
+
void response.body?.cancel().catch(() => undefined);
|
|
891
|
+
throw protocolError("json_size_limit");
|
|
892
|
+
}
|
|
893
|
+
const reader = response.body?.getReader();
|
|
894
|
+
if (!reader)
|
|
895
|
+
throw protocolError("invalid_json");
|
|
896
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
897
|
+
const chunks = [];
|
|
898
|
+
let total = 0;
|
|
899
|
+
let complete = false;
|
|
900
|
+
try {
|
|
901
|
+
while (true) {
|
|
902
|
+
const { done, value } = await readStreamChunk(reader, signal);
|
|
903
|
+
if (done) {
|
|
904
|
+
complete = true;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
total += value.byteLength;
|
|
908
|
+
if (total > maxJsonBytes)
|
|
909
|
+
throw protocolError("json_size_limit");
|
|
910
|
+
try {
|
|
911
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
throw protocolError("invalid_json");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
chunks.push(decoder.decode());
|
|
919
|
+
return record(JSON.parse(chunks.join("")));
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
if (error instanceof RaybaseInferenceProviderError)
|
|
923
|
+
throw error;
|
|
924
|
+
throw protocolError("invalid_json");
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
finally {
|
|
928
|
+
if (!complete)
|
|
929
|
+
void reader.cancel().catch(() => undefined);
|
|
930
|
+
try {
|
|
931
|
+
reader.releaseLock();
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// Abort-driven cancellation may still be settling.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function normalizedContentType(response) {
|
|
939
|
+
return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || undefined;
|
|
940
|
+
}
|
|
941
|
+
function statusError(response, destination = "auth") {
|
|
942
|
+
const contentType = normalizedContentType(response);
|
|
943
|
+
const cfRay = response.headers.get("cf-ray")?.trim() || undefined;
|
|
944
|
+
if (response.status === 401)
|
|
945
|
+
return new OpenAISubscriptionTransportHttpError(401, "unauthorized", undefined, contentType, cfRay);
|
|
946
|
+
if (response.status === 403 && destination === "codex") {
|
|
947
|
+
// Two unrelated failures share this status. The Codex API answers with
|
|
948
|
+
// JSON and always stamps `x-oai-request-id`; Cloudflare's front door
|
|
949
|
+
// answers with the HTML blocked page and never reaches the API, so no
|
|
950
|
+
// request id exists. Only the second is an egress problem, and telling an
|
|
951
|
+
// operator to relink for it sends them to a control that cannot help.
|
|
952
|
+
const servedByApi = Boolean(response.headers.get("x-oai-request-id")?.trim())
|
|
953
|
+
|| contentType === "application/json";
|
|
954
|
+
return new OpenAISubscriptionTransportHttpError(403, servedByApi ? "account_forbidden" : "egress_forbidden", undefined, contentType, cfRay);
|
|
955
|
+
}
|
|
956
|
+
if (response.status === 429) {
|
|
957
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
958
|
+
const retryAt = Number.isFinite(retryAfter) ? new Date(Date.now() + retryAfter * 1000).toISOString() : undefined;
|
|
959
|
+
return new OpenAISubscriptionTransportHttpError(429, "rate_limited", retryAt, contentType, cfRay);
|
|
960
|
+
}
|
|
961
|
+
if (response.status >= 300 && response.status < 400)
|
|
962
|
+
return protocolError("redirect_rejected");
|
|
963
|
+
return protocolError(`unexpected_http_status_${response.status}`);
|
|
964
|
+
}
|
|
965
|
+
async function codexStatusError(response, signal) {
|
|
966
|
+
if (response.status !== 400)
|
|
967
|
+
return statusError(response, "codex");
|
|
968
|
+
const metadata = {
|
|
969
|
+
status: 400,
|
|
970
|
+
contentType: normalizedContentType(response),
|
|
971
|
+
oaiRequestId: boundedOaiRequestId(response)
|
|
972
|
+
};
|
|
973
|
+
let payload;
|
|
974
|
+
try {
|
|
975
|
+
payload = await strictJsonResponse(response, signal);
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
if (error instanceof OpenAISubscriptionTransportProtocolError
|
|
979
|
+
&& codex400JsonShapeErrorCodes.has(error.code)) {
|
|
980
|
+
return protocolError("unexpected_http_status_400", metadata);
|
|
981
|
+
}
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
const upstream = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)
|
|
985
|
+
? record(payload.error)
|
|
986
|
+
: {};
|
|
987
|
+
const code = typeof upstream.code === "string" ? upstream.code : undefined;
|
|
988
|
+
const type = typeof upstream.type === "string" ? upstream.type : undefined;
|
|
989
|
+
const param = optionalString(upstream.param);
|
|
990
|
+
if (code === "invalid_function_parameters"
|
|
991
|
+
&& param
|
|
992
|
+
&& /^tools\[\d+\]\.parameters(?:\.|$)/.test(param)) {
|
|
993
|
+
return new OpenAISubscriptionTransportHttpError(400, "invalid_function_parameters", undefined, metadata.contentType, response.headers.get("cf-ray")?.trim() || undefined, metadata.oaiRequestId, param);
|
|
994
|
+
}
|
|
995
|
+
const token = normalizeCodex400Token(code) ?? normalizeCodex400Token(type);
|
|
996
|
+
if (token)
|
|
997
|
+
return new OpenAISubscriptionCodexRequestError(token, metadata);
|
|
998
|
+
return protocolError("unexpected_http_status_400", metadata);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* A `response.failed` or `error` stream event, classified with what the
|
|
1002
|
+
* upstream actually said.
|
|
1003
|
+
*
|
|
1004
|
+
* A bare `upstream_failed` makes a 5xx, a rate limit, a timeout, and a quota
|
|
1005
|
+
* exhaustion indistinguishable everywhere downstream — the safe-error scrub
|
|
1006
|
+
* keeps only the code, so the code is the one channel that reaches an app's
|
|
1007
|
+
* health row. The upstream code or type rides in the code's suffix, normalized
|
|
1008
|
+
* to a bounded token; the upstream message rides on the error itself for logs
|
|
1009
|
+
* that print raw errors, and is never shown to members (the scrub replaces it).
|
|
1010
|
+
*/
|
|
1011
|
+
function upstreamFailedError(event) {
|
|
1012
|
+
const response = record(event.response);
|
|
1013
|
+
const nested = record(response.error);
|
|
1014
|
+
const direct = record(event.error);
|
|
1015
|
+
const upstream = Object.keys(nested).length ? nested : direct;
|
|
1016
|
+
const token = normalizeCodex400Token(optionalString(upstream.code))
|
|
1017
|
+
?? normalizeCodex400Token(optionalString(upstream.type));
|
|
1018
|
+
const detail = optionalString(upstream.message);
|
|
1019
|
+
return new RaybaseInferenceProviderError({
|
|
1020
|
+
code: token ? `upstream_failed:${token}` : "upstream_failed",
|
|
1021
|
+
...(detail ? { message: `OpenAI subscription upstream failure: ${detail.slice(0, 300)}` } : {})
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function normalizeCodex400Token(value) {
|
|
1025
|
+
if (value === undefined)
|
|
1026
|
+
return undefined;
|
|
1027
|
+
const normalized = value
|
|
1028
|
+
.toLowerCase()
|
|
1029
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
1030
|
+
.replace(/^_+|_+$/g, "")
|
|
1031
|
+
.slice(0, 32);
|
|
1032
|
+
return normalized || undefined;
|
|
1033
|
+
}
|
|
1034
|
+
function boundedOaiRequestId(response) {
|
|
1035
|
+
const requestId = response.headers.get("x-oai-request-id")?.trim();
|
|
1036
|
+
return requestId && requestId.length <= 128 ? requestId : undefined;
|
|
1037
|
+
}
|
|
1038
|
+
function protocolError(fingerprint, metadata) {
|
|
1039
|
+
return new OpenAISubscriptionTransportProtocolError(fingerprint, metadata);
|
|
1040
|
+
}
|
|
1041
|
+
function requiredString(value, key, allowEmpty = false) {
|
|
1042
|
+
const found = value[key];
|
|
1043
|
+
if (typeof found !== "string" || (!allowEmpty && !found))
|
|
1044
|
+
throw protocolError(`missing_${key}`);
|
|
1045
|
+
return found;
|
|
1046
|
+
}
|
|
1047
|
+
function optionalString(value) {
|
|
1048
|
+
return typeof value === "string" && value ? value : undefined;
|
|
1049
|
+
}
|
|
1050
|
+
function boundedInteger(value, min, max, label) {
|
|
1051
|
+
const parsed = typeof value === "string" ? Number(value) : value;
|
|
1052
|
+
if (!Number.isInteger(parsed) || Number(parsed) < min || Number(parsed) > max)
|
|
1053
|
+
throw protocolError(`invalid_${label.replace(/\W+/g, "_")}`);
|
|
1054
|
+
return Number(parsed);
|
|
1055
|
+
}
|
|
1056
|
+
function optionalNonNegativeInteger(value) {
|
|
1057
|
+
return Number.isInteger(value) && Number(value) >= 0 ? Number(value) : undefined;
|
|
1058
|
+
}
|
|
1059
|
+
function record(value) {
|
|
1060
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1061
|
+
throw protocolError("expected_object");
|
|
1062
|
+
return value;
|
|
1063
|
+
}
|
|
1064
|
+
function parseSseEvent(raw) {
|
|
1065
|
+
let event;
|
|
1066
|
+
const data = [];
|
|
1067
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1068
|
+
if (!line || line.startsWith(":"))
|
|
1069
|
+
continue;
|
|
1070
|
+
if (line.startsWith("event:"))
|
|
1071
|
+
event = line.slice(6).trim();
|
|
1072
|
+
else if (line.startsWith("data:"))
|
|
1073
|
+
data.push(line.slice(5).trimStart());
|
|
1074
|
+
else
|
|
1075
|
+
throw protocolError("invalid_sse_field");
|
|
1076
|
+
}
|
|
1077
|
+
if (!data.length)
|
|
1078
|
+
return null;
|
|
1079
|
+
return { event, data: data.join("\n") };
|
|
1080
|
+
}
|
|
1081
|
+
function parseEventJson(value) {
|
|
1082
|
+
if (value === "[DONE]")
|
|
1083
|
+
throw protocolError("unexpected_done_sentinel");
|
|
1084
|
+
try {
|
|
1085
|
+
return record(JSON.parse(value));
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
throw protocolError("invalid_sse_json");
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function decodeJwtPayload(token) {
|
|
1092
|
+
try {
|
|
1093
|
+
const payload = token.split(".")[1];
|
|
1094
|
+
if (!payload)
|
|
1095
|
+
return {};
|
|
1096
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "="));
|
|
1097
|
+
return record(JSON.parse(decoded));
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
return {};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function safeIdentityProjection(claims) {
|
|
1104
|
+
const auth = claims["https://api.openai.com/auth"];
|
|
1105
|
+
const nested = auth && typeof auth === "object" && !Array.isArray(auth) ? auth : {};
|
|
1106
|
+
const accountId = optionalString(nested.chatgpt_account_id) ?? optionalString(claims.chatgpt_account_id);
|
|
1107
|
+
const workspaceId = optionalString(nested.chatgpt_workspace_id) ?? optionalString(claims.chatgpt_workspace_id);
|
|
1108
|
+
const plan = optionalString(nested.chatgpt_plan_type) ?? optionalString(claims.chatgpt_plan_type);
|
|
1109
|
+
return {
|
|
1110
|
+
...(accountId ? { accountId } : {}),
|
|
1111
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
1112
|
+
...(plan ? { plan } : {})
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
//# sourceMappingURL=transport.js.map
|