@kenkaiiii/gg-ai 4.3.237 → 4.3.238
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/index.cjs +266 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +265 -171
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
+
function readHeader(headers, ...names) {
|
|
3
|
+
if (!headers) return void 0;
|
|
4
|
+
const getter = typeof headers.get === "function" ? (name) => headers.get(name) ?? void 0 : typeof headers === "object" ? (name) => {
|
|
5
|
+
const rec = headers;
|
|
6
|
+
const value = rec[name] ?? rec[name.toLowerCase()];
|
|
7
|
+
return typeof value === "string" ? value : void 0;
|
|
8
|
+
} : void 0;
|
|
9
|
+
if (!getter) return void 0;
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
const value = getter(name);
|
|
12
|
+
if (value != null) return value;
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
2
16
|
var GGAIError = class extends Error {
|
|
3
17
|
source;
|
|
4
18
|
requestId;
|
|
@@ -51,6 +65,10 @@ function isUsageLimitError(err) {
|
|
|
51
65
|
if (!(err instanceof Error)) return false;
|
|
52
66
|
return /usage limit reached/i.test(err.message);
|
|
53
67
|
}
|
|
68
|
+
function isHardBillingMessage(message) {
|
|
69
|
+
const lower = message.toLowerCase();
|
|
70
|
+
return lower.includes("insufficient balance") || lower.includes("insufficient credits") || lower.includes("more credits") || lower.includes("insufficient_quota") || lower.includes("exceeded your current quota") || lower.includes("quota exceeded") || lower.includes("no resource package") || lower.includes("recharge") || lower.includes("balance is too low") || lower.includes("out of credits") || lower.includes("arrears") || lower.includes("arrearage") || lower.includes("token quota") || lower.includes("exceeded_current_quota_error") || lower.includes("check your account balance") || lower.includes("does not yet include access") || lower.includes("subscription plan") || lower.includes("billing");
|
|
71
|
+
}
|
|
54
72
|
function formatResetTime(resetsAt) {
|
|
55
73
|
const when = new Date(resetsAt * 1e3);
|
|
56
74
|
const sameDay = when.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
|
|
@@ -300,6 +318,9 @@ function zodToJsonSchema(schema) {
|
|
|
300
318
|
const { $schema: _schema, ...rest } = jsonSchema;
|
|
301
319
|
return normalizeRootForAnthropic(rest);
|
|
302
320
|
}
|
|
321
|
+
function resolveToolSchema(tool) {
|
|
322
|
+
return tool.rawInputSchema ?? zodToJsonSchema(tool.parameters);
|
|
323
|
+
}
|
|
303
324
|
function normalizeRootForAnthropic(schema) {
|
|
304
325
|
const branches = schema.oneOf ?? schema.anyOf;
|
|
305
326
|
if (!branches || branches.length === 0) {
|
|
@@ -413,6 +434,7 @@ function toAnthropicAssistantContent(content, isLatest, idMap) {
|
|
|
413
434
|
}
|
|
414
435
|
var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
415
436
|
var NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
437
|
+
var NON_VIDEO_USER_PLACEHOLDER = "(video omitted: model does not support video)";
|
|
416
438
|
function stripImages(content, placeholder) {
|
|
417
439
|
const out = [];
|
|
418
440
|
let lastWasPlaceholder = false;
|
|
@@ -423,10 +445,33 @@ function stripImages(content, placeholder) {
|
|
|
423
445
|
continue;
|
|
424
446
|
}
|
|
425
447
|
out.push(block);
|
|
426
|
-
lastWasPlaceholder = block.text === placeholder;
|
|
448
|
+
lastWasPlaceholder = block.type === "text" && block.text === placeholder;
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
function stripVideos(content, placeholder) {
|
|
453
|
+
const out = [];
|
|
454
|
+
let lastWasPlaceholder = false;
|
|
455
|
+
for (const block of content) {
|
|
456
|
+
if (block.type === "video") {
|
|
457
|
+
if (!lastWasPlaceholder) out.push({ type: "text", text: placeholder });
|
|
458
|
+
lastWasPlaceholder = true;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
out.push(block);
|
|
462
|
+
lastWasPlaceholder = block.type === "text" && block.text === placeholder;
|
|
427
463
|
}
|
|
428
464
|
return out;
|
|
429
465
|
}
|
|
466
|
+
function downgradeUnsupportedVideos(messages, supportsVideo) {
|
|
467
|
+
if (supportsVideo === true) return messages;
|
|
468
|
+
return messages.map((msg) => {
|
|
469
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
470
|
+
return { ...msg, content: stripVideos(msg.content, NON_VIDEO_USER_PLACEHOLDER) };
|
|
471
|
+
}
|
|
472
|
+
return msg;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
430
475
|
function downgradeUnsupportedImages(messages, supportsImages) {
|
|
431
476
|
if (supportsImages !== false) return messages;
|
|
432
477
|
return messages.map((msg) => {
|
|
@@ -503,6 +548,16 @@ function toAnthropicMessages(messages, cacheControl) {
|
|
|
503
548
|
role: "user",
|
|
504
549
|
content: typeof msg.content === "string" ? msg.content : msg.content.map((part) => {
|
|
505
550
|
if (part.type === "text") return { type: "text", text: part.text };
|
|
551
|
+
if (part.type === "video") {
|
|
552
|
+
return {
|
|
553
|
+
type: "video",
|
|
554
|
+
source: {
|
|
555
|
+
type: "base64",
|
|
556
|
+
media_type: part.mediaType,
|
|
557
|
+
data: part.data
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
506
561
|
return {
|
|
507
562
|
type: "image",
|
|
508
563
|
source: {
|
|
@@ -602,11 +657,11 @@ function toAnthropicToolChoice(choice) {
|
|
|
602
657
|
if (choice === "required") return { type: "any" };
|
|
603
658
|
return { type: "tool", name: choice.name };
|
|
604
659
|
}
|
|
605
|
-
function
|
|
606
|
-
return /opus-4
|
|
660
|
+
function isAdaptiveThinkingModel(model) {
|
|
661
|
+
return /opus-4[-.]8|opus-4[-.]7|opus-4[-.]6|sonnet-4[-.]6/.test(model);
|
|
607
662
|
}
|
|
608
663
|
function toAnthropicThinking(level, maxTokens, model) {
|
|
609
|
-
if (
|
|
664
|
+
if (isAdaptiveThinkingModel(model)) {
|
|
610
665
|
let effort = level;
|
|
611
666
|
if (effort === "xhigh" && !/opus-4-8|opus-4-7/.test(model)) {
|
|
612
667
|
effort = "high";
|
|
@@ -663,6 +718,14 @@ function toOpenAIMessages(messages, options) {
|
|
|
663
718
|
content: msg.content.map(
|
|
664
719
|
(part) => {
|
|
665
720
|
if (part.type === "text") return { type: "text", text: part.text };
|
|
721
|
+
if (part.type === "video") {
|
|
722
|
+
return {
|
|
723
|
+
type: "video_url",
|
|
724
|
+
video_url: {
|
|
725
|
+
url: `data:${part.mediaType};base64,${part.data}`
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
666
729
|
return {
|
|
667
730
|
type: "image_url",
|
|
668
731
|
image_url: {
|
|
@@ -740,7 +803,7 @@ function toOpenAITools(tools) {
|
|
|
740
803
|
function: {
|
|
741
804
|
name: tool.name,
|
|
742
805
|
description: tool.description,
|
|
743
|
-
parameters:
|
|
806
|
+
parameters: resolveToolSchema(tool)
|
|
744
807
|
}
|
|
745
808
|
}));
|
|
746
809
|
}
|
|
@@ -782,10 +845,22 @@ function normalizeOpenAIStopReason(reason) {
|
|
|
782
845
|
}
|
|
783
846
|
}
|
|
784
847
|
|
|
785
|
-
// src/
|
|
848
|
+
// src/utils/json.ts
|
|
786
849
|
function isJsonObject(value) {
|
|
787
850
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
788
851
|
}
|
|
852
|
+
function parseToolArguments(argsJson) {
|
|
853
|
+
if (!argsJson) return {};
|
|
854
|
+
try {
|
|
855
|
+
const parsed = JSON.parse(argsJson);
|
|
856
|
+
const unwrapped = typeof parsed === "string" ? JSON.parse(parsed) : parsed;
|
|
857
|
+
return isJsonObject(unwrapped) ? unwrapped : {};
|
|
858
|
+
} catch {
|
|
859
|
+
return {};
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/providers/anthropic.ts
|
|
789
864
|
function createClient(options) {
|
|
790
865
|
const isOAuth = options.apiKey?.startsWith("sk-ant-oat");
|
|
791
866
|
return new Anthropic({
|
|
@@ -816,7 +891,8 @@ async function* runStream(options) {
|
|
|
816
891
|
const useStreaming = options.streaming !== false;
|
|
817
892
|
const cacheControl = toAnthropicCacheControl(options.cacheRetention, options.baseUrl);
|
|
818
893
|
const supportsFirstPartyToolExtras = !options.baseUrl || options.baseUrl.includes("api.anthropic.com");
|
|
819
|
-
const
|
|
894
|
+
const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
|
|
895
|
+
const downgradedMessages = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
|
|
820
896
|
const { system: rawSystem, messages } = toAnthropicMessages(downgradedMessages, cacheControl);
|
|
821
897
|
const system = isOAuth ? [
|
|
822
898
|
{
|
|
@@ -878,7 +954,7 @@ async function* runStream(options) {
|
|
|
878
954
|
})(),
|
|
879
955
|
stream: useStreaming
|
|
880
956
|
};
|
|
881
|
-
const hasAdaptiveThinking =
|
|
957
|
+
const hasAdaptiveThinking = isAdaptiveThinkingModel(options.model);
|
|
882
958
|
const betaHeaders = [
|
|
883
959
|
...isOAuth ? ["claude-code-20250219", "oauth-2025-04-20"] : [],
|
|
884
960
|
...options.compaction ? ["compact-2026-01-12"] : [],
|
|
@@ -1203,19 +1279,13 @@ function messageToResponse(message) {
|
|
|
1203
1279
|
};
|
|
1204
1280
|
}
|
|
1205
1281
|
function readUnifiedRateLimit(headers) {
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
return typeof value === "string" ? value : null;
|
|
1214
|
-
}
|
|
1215
|
-
return null;
|
|
1216
|
-
};
|
|
1217
|
-
const status = get("anthropic-ratelimit-unified-status");
|
|
1218
|
-
const resetRaw = get("anthropic-ratelimit-unified-reset") ?? get("anthropic-ratelimit-unified-5h-reset") ?? get("anthropic-ratelimit-unified-7d-reset");
|
|
1282
|
+
const status = readHeader(headers, "anthropic-ratelimit-unified-status");
|
|
1283
|
+
const resetRaw = readHeader(
|
|
1284
|
+
headers,
|
|
1285
|
+
"anthropic-ratelimit-unified-reset",
|
|
1286
|
+
"anthropic-ratelimit-unified-5h-reset",
|
|
1287
|
+
"anthropic-ratelimit-unified-7d-reset"
|
|
1288
|
+
);
|
|
1219
1289
|
const resetNum = resetRaw != null ? Number(resetRaw) : Number.NaN;
|
|
1220
1290
|
const resetsAt = Number.isFinite(resetNum) && resetNum > 0 ? resetNum : void 0;
|
|
1221
1291
|
return { rejected: status === "rejected", ...resetsAt ? { resetsAt } : {} };
|
|
@@ -1240,6 +1310,14 @@ function toError(err) {
|
|
|
1240
1310
|
});
|
|
1241
1311
|
}
|
|
1242
1312
|
}
|
|
1313
|
+
if (isHardBillingMessage(message)) {
|
|
1314
|
+
const usageMessage = /usage limit reached/i.test(message) ? message : `usage limit reached: ${message}`;
|
|
1315
|
+
return new ProviderError("anthropic", usageMessage, {
|
|
1316
|
+
statusCode: err.status,
|
|
1317
|
+
...requestId ? { requestId } : {},
|
|
1318
|
+
cause: err
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1243
1321
|
return new ProviderError("anthropic", message, {
|
|
1244
1322
|
statusCode: err.status,
|
|
1245
1323
|
...requestId ? { requestId } : {},
|
|
@@ -1272,19 +1350,30 @@ function fnv1aHash(value) {
|
|
|
1272
1350
|
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
1273
1351
|
}
|
|
1274
1352
|
|
|
1275
|
-
// src/
|
|
1276
|
-
function
|
|
1277
|
-
return
|
|
1353
|
+
// src/utils/env.ts
|
|
1354
|
+
function getEnvironment() {
|
|
1355
|
+
return globalThis.process?.env;
|
|
1278
1356
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1357
|
+
|
|
1358
|
+
// src/providers/openai.ts
|
|
1359
|
+
function extractOpenAIUsage(usage) {
|
|
1360
|
+
let cacheRead = 0;
|
|
1361
|
+
const details = usage.prompt_tokens_details;
|
|
1362
|
+
if (details?.cached_tokens) {
|
|
1363
|
+
cacheRead = details.cached_tokens;
|
|
1364
|
+
}
|
|
1365
|
+
const usageAny = usage;
|
|
1366
|
+
if (!cacheRead && typeof usageAny.cached_tokens === "number" && usageAny.cached_tokens > 0) {
|
|
1367
|
+
cacheRead = usageAny.cached_tokens;
|
|
1287
1368
|
}
|
|
1369
|
+
if (!cacheRead && typeof usageAny.prompt_cache_hit_tokens === "number" && usageAny.prompt_cache_hit_tokens > 0) {
|
|
1370
|
+
cacheRead = usageAny.prompt_cache_hit_tokens;
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
inputTokens: usage.prompt_tokens - cacheRead,
|
|
1374
|
+
outputTokens: usage.completion_tokens,
|
|
1375
|
+
cacheRead
|
|
1376
|
+
};
|
|
1288
1377
|
}
|
|
1289
1378
|
function createClient2(options) {
|
|
1290
1379
|
return new OpenAI({
|
|
@@ -1301,7 +1390,8 @@ async function* runStream2(options) {
|
|
|
1301
1390
|
const useStreaming = options.streaming !== false;
|
|
1302
1391
|
const client = createClient2(options);
|
|
1303
1392
|
const usesThinkingParam = options.provider === "glm" || options.provider === "moonshot" || options.provider === "xiaomi";
|
|
1304
|
-
const
|
|
1393
|
+
const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
|
|
1394
|
+
const downgradedMessages = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
|
|
1305
1395
|
const messages = toOpenAIMessages(downgradedMessages, {
|
|
1306
1396
|
provider: options.provider,
|
|
1307
1397
|
thinking: !!options.thinking,
|
|
@@ -1340,7 +1430,7 @@ async function* runStream2(options) {
|
|
|
1340
1430
|
params.thinking = { type: "disabled" };
|
|
1341
1431
|
}
|
|
1342
1432
|
}
|
|
1343
|
-
if (
|
|
1433
|
+
if (getEnvironment()?.GGAI_DUMP_REQUEST) {
|
|
1344
1434
|
const fs = await import("fs");
|
|
1345
1435
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1346
1436
|
const dumpPath = `/tmp/ggai-request-${ts}.json`;
|
|
@@ -1381,19 +1471,7 @@ async function* runStream2(options) {
|
|
|
1381
1471
|
for await (const chunk of stream2) {
|
|
1382
1472
|
const choice = chunk.choices?.[0];
|
|
1383
1473
|
if (chunk.usage) {
|
|
1384
|
-
outputTokens = chunk.usage
|
|
1385
|
-
const details = chunk.usage.prompt_tokens_details;
|
|
1386
|
-
if (details?.cached_tokens) {
|
|
1387
|
-
cacheRead = details.cached_tokens;
|
|
1388
|
-
}
|
|
1389
|
-
const usageAny = chunk.usage;
|
|
1390
|
-
if (!cacheRead && typeof usageAny.cached_tokens === "number" && usageAny.cached_tokens > 0) {
|
|
1391
|
-
cacheRead = usageAny.cached_tokens;
|
|
1392
|
-
}
|
|
1393
|
-
if (!cacheRead && typeof usageAny.prompt_cache_hit_tokens === "number" && usageAny.prompt_cache_hit_tokens > 0) {
|
|
1394
|
-
cacheRead = usageAny.prompt_cache_hit_tokens;
|
|
1395
|
-
}
|
|
1396
|
-
inputTokens = chunk.usage.prompt_tokens - cacheRead;
|
|
1474
|
+
({ inputTokens, outputTokens, cacheRead } = extractOpenAIUsage(chunk.usage));
|
|
1397
1475
|
}
|
|
1398
1476
|
if (!choice) continue;
|
|
1399
1477
|
if (choice.finish_reason) {
|
|
@@ -1539,17 +1617,7 @@ function completionToResponse(completion) {
|
|
|
1539
1617
|
let outputTokens = 0;
|
|
1540
1618
|
let cacheRead = 0;
|
|
1541
1619
|
if (completion.usage) {
|
|
1542
|
-
outputTokens = completion.usage
|
|
1543
|
-
const details = completion.usage.prompt_tokens_details;
|
|
1544
|
-
if (details?.cached_tokens) cacheRead = details.cached_tokens;
|
|
1545
|
-
const usageAny = completion.usage;
|
|
1546
|
-
if (!cacheRead && typeof usageAny.cached_tokens === "number" && usageAny.cached_tokens > 0) {
|
|
1547
|
-
cacheRead = usageAny.cached_tokens;
|
|
1548
|
-
}
|
|
1549
|
-
if (!cacheRead && typeof usageAny.prompt_cache_hit_tokens === "number" && usageAny.prompt_cache_hit_tokens > 0) {
|
|
1550
|
-
cacheRead = usageAny.prompt_cache_hit_tokens;
|
|
1551
|
-
}
|
|
1552
|
-
inputTokens = completion.usage.prompt_tokens - cacheRead;
|
|
1620
|
+
({ inputTokens, outputTokens, cacheRead } = extractOpenAIUsage(completion.usage));
|
|
1553
1621
|
}
|
|
1554
1622
|
const stopReason = normalizeOpenAIStopReason(choice?.finish_reason ?? null);
|
|
1555
1623
|
return {
|
|
@@ -1561,6 +1629,16 @@ function completionToResponse(completion) {
|
|
|
1561
1629
|
usage: { inputTokens, outputTokens, ...cacheRead > 0 && { cacheRead } }
|
|
1562
1630
|
};
|
|
1563
1631
|
}
|
|
1632
|
+
function classifyOpenAICompatLimit(args) {
|
|
1633
|
+
const { status, code, type, message } = args;
|
|
1634
|
+
const codeType = `${code ?? ""} ${type ?? ""}`.toLowerCase();
|
|
1635
|
+
const isHard = status === 402 || codeType.includes("insufficient_quota") || isHardBillingMessage(message);
|
|
1636
|
+
if (isHard) return "hard";
|
|
1637
|
+
if (status === 429 || codeType.includes("rate_limit_exceeded") || codeType.includes("too_many_requests")) {
|
|
1638
|
+
return "transient";
|
|
1639
|
+
}
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1564
1642
|
function toError2(err, provider = "openai") {
|
|
1565
1643
|
if (err instanceof OpenAI.APIError) {
|
|
1566
1644
|
const body = err.error;
|
|
@@ -1572,6 +1650,35 @@ function toError2(err, provider = "openai") {
|
|
|
1572
1650
|
hint = "codex-mini-latest requires an OpenAI Pro or Max subscription. Your account currently has access to GPT-5.4 and GPT-5.4 Mini.";
|
|
1573
1651
|
}
|
|
1574
1652
|
const requestId = err.request_id ?? (typeof body?.request_id === "string" ? body.request_id : void 0);
|
|
1653
|
+
const code = typeof err.code === "string" ? err.code : void 0;
|
|
1654
|
+
const type = typeof err.type === "string" ? err.type : void 0;
|
|
1655
|
+
const limit = classifyOpenAICompatLimit({
|
|
1656
|
+
status: err.status,
|
|
1657
|
+
code,
|
|
1658
|
+
type,
|
|
1659
|
+
message: cleanMessage
|
|
1660
|
+
});
|
|
1661
|
+
if (limit === "hard") {
|
|
1662
|
+
const message = /usage limit reached/i.test(cleanMessage) ? cleanMessage : `usage limit reached: ${cleanMessage}`;
|
|
1663
|
+
return new ProviderError(provider, message, {
|
|
1664
|
+
statusCode: err.status,
|
|
1665
|
+
...requestId ? { requestId } : {},
|
|
1666
|
+
...hint ? { hint } : {},
|
|
1667
|
+
cause: err
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
if (limit === "transient") {
|
|
1671
|
+
const retryAfterRaw = readHeader(err.headers, "retry-after");
|
|
1672
|
+
const retryAfterSec = retryAfterRaw != null ? Number(retryAfterRaw) : Number.NaN;
|
|
1673
|
+
const resetsAt = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? Math.floor(Date.now() / 1e3) + retryAfterSec : void 0;
|
|
1674
|
+
return new ProviderError(provider, cleanMessage, {
|
|
1675
|
+
statusCode: err.status,
|
|
1676
|
+
...requestId ? { requestId } : {},
|
|
1677
|
+
...hint ? { hint } : {},
|
|
1678
|
+
...resetsAt ? { resetsAt } : {},
|
|
1679
|
+
cause: err
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1575
1682
|
return new ProviderError(provider, cleanMessage, {
|
|
1576
1683
|
statusCode: err.status,
|
|
1577
1684
|
...requestId ? { requestId } : {},
|
|
@@ -1597,21 +1704,59 @@ function providerDiag(phase, data) {
|
|
|
1597
1704
|
_diagFn?.(phase, data);
|
|
1598
1705
|
}
|
|
1599
1706
|
|
|
1600
|
-
// src/
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1707
|
+
// src/utils/sse.ts
|
|
1708
|
+
function parseSseBuffer(buffer) {
|
|
1709
|
+
const events = [];
|
|
1710
|
+
let cursor = 0;
|
|
1711
|
+
while (true) {
|
|
1712
|
+
const next = buffer.indexOf("\n\n", cursor);
|
|
1713
|
+
if (next === -1) break;
|
|
1714
|
+
const raw = buffer.slice(cursor, next);
|
|
1715
|
+
cursor = next + 2;
|
|
1716
|
+
let eventName;
|
|
1717
|
+
const dataLines = [];
|
|
1718
|
+
for (const line of raw.split("\n")) {
|
|
1719
|
+
if (line.startsWith("event:")) {
|
|
1720
|
+
eventName = line.slice("event:".length).trim();
|
|
1721
|
+
} else if (line.startsWith("data:")) {
|
|
1722
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (dataLines.length > 0) {
|
|
1726
|
+
events.push({ event: eventName, data: dataLines.join("\n") });
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return { events, remaining: buffer.slice(cursor) };
|
|
1604
1730
|
}
|
|
1605
|
-
function
|
|
1606
|
-
|
|
1731
|
+
async function* readSseStream(body) {
|
|
1732
|
+
const reader = body.getReader();
|
|
1733
|
+
const decoder = new TextDecoder();
|
|
1734
|
+
let buffer = "";
|
|
1607
1735
|
try {
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1736
|
+
while (true) {
|
|
1737
|
+
const { done, value } = await reader.read();
|
|
1738
|
+
if (done) break;
|
|
1739
|
+
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
|
1740
|
+
const parsed2 = parseSseBuffer(buffer);
|
|
1741
|
+
buffer = parsed2.remaining;
|
|
1742
|
+
yield* parsed2.events;
|
|
1743
|
+
}
|
|
1744
|
+
buffer += decoder.decode().replace(/\r\n/g, "\n");
|
|
1745
|
+
const parsed = parseSseBuffer(buffer + "\n\n");
|
|
1746
|
+
yield* parsed.events;
|
|
1747
|
+
} finally {
|
|
1748
|
+
reader.releaseLock();
|
|
1613
1749
|
}
|
|
1614
1750
|
}
|
|
1751
|
+
|
|
1752
|
+
// src/utils/request-id.ts
|
|
1753
|
+
function extractRequestIdFromMessage(message) {
|
|
1754
|
+
const match = message.match(/request ID ([a-z0-9-]{8,})/i);
|
|
1755
|
+
return match?.[1];
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/providers/openai-codex.ts
|
|
1759
|
+
var DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
|
|
1615
1760
|
function outputTextKey(itemId, contentIndex) {
|
|
1616
1761
|
return `${itemId ?? ""}:${contentIndex ?? 0}`;
|
|
1617
1762
|
}
|
|
@@ -1624,7 +1769,8 @@ function streamOpenAICodex(options) {
|
|
|
1624
1769
|
async function* runStream3(options) {
|
|
1625
1770
|
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
1626
1771
|
const url = `${baseUrl}/codex/responses`;
|
|
1627
|
-
const
|
|
1772
|
+
const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
|
|
1773
|
+
const downgraded = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
|
|
1628
1774
|
const { system, input } = toCodexInput(downgraded, { supportsImages: options.supportsImages });
|
|
1629
1775
|
const body = {
|
|
1630
1776
|
model: options.model,
|
|
@@ -1676,7 +1822,7 @@ async function* runStream3(options) {
|
|
|
1676
1822
|
const text = await response.text().catch(() => "");
|
|
1677
1823
|
const parsed = parseCodexErrorBody(text);
|
|
1678
1824
|
const message = parsed.message ?? `Codex API returned HTTP ${response.status}.`;
|
|
1679
|
-
const requestId = parsed.requestId ?? response.headers
|
|
1825
|
+
const requestId = parsed.requestId ?? readHeader(response.headers, "x-request-id", "openai-request-id", "x-oai-request-id");
|
|
1680
1826
|
const usageLimit = codexUsageLimitError(parsed.errorObj, response.status, requestId);
|
|
1681
1827
|
if (usageLimit) throw usageLimit;
|
|
1682
1828
|
let hint;
|
|
@@ -1720,7 +1866,7 @@ async function* runStream3(options) {
|
|
|
1720
1866
|
const nested = event.error ?? void 0;
|
|
1721
1867
|
const message = nested?.message ?? event.message ?? "Codex stream emitted an error chunk without a message.";
|
|
1722
1868
|
const code = nested?.code ?? nested?.type ?? event.code ?? "server_error";
|
|
1723
|
-
const requestId =
|
|
1869
|
+
const requestId = extractRequestIdFromMessage(message) ?? event.request_id;
|
|
1724
1870
|
const usageLimit = codexUsageLimitError(
|
|
1725
1871
|
nested ?? event,
|
|
1726
1872
|
void 0,
|
|
@@ -1735,7 +1881,7 @@ async function* runStream3(options) {
|
|
|
1735
1881
|
if (type === "response.failed") {
|
|
1736
1882
|
const nested = event.error;
|
|
1737
1883
|
const message = nested?.message ?? "Codex response failed.";
|
|
1738
|
-
const requestId =
|
|
1884
|
+
const requestId = extractRequestIdFromMessage(message) ?? event.request_id;
|
|
1739
1885
|
throw new ProviderError("openai", message, {
|
|
1740
1886
|
...requestId != null ? { requestId } : {}
|
|
1741
1887
|
});
|
|
@@ -1854,7 +2000,7 @@ async function* runStream3(options) {
|
|
|
1854
2000
|
const id = `${callId}|${itemId}`;
|
|
1855
2001
|
const tc = toolCalls.get(id);
|
|
1856
2002
|
if (tc) {
|
|
1857
|
-
const args =
|
|
2003
|
+
const args = parseToolArguments(tc.argsJson);
|
|
1858
2004
|
yield {
|
|
1859
2005
|
type: "toolcall_done",
|
|
1860
2006
|
id: tc.id,
|
|
@@ -1878,7 +2024,7 @@ async function* runStream3(options) {
|
|
|
1878
2024
|
contentParts.push({ type: "text", text: textAccum });
|
|
1879
2025
|
}
|
|
1880
2026
|
for (const [, tc] of toolCalls) {
|
|
1881
|
-
const args =
|
|
2027
|
+
const args = parseToolArguments(tc.argsJson);
|
|
1882
2028
|
const toolCall = {
|
|
1883
2029
|
type: "tool_call",
|
|
1884
2030
|
id: tc.id,
|
|
@@ -1901,33 +2047,13 @@ async function* runStream3(options) {
|
|
|
1901
2047
|
return streamResponse;
|
|
1902
2048
|
}
|
|
1903
2049
|
async function* parseSSE(body) {
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
if (done) break;
|
|
1911
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1912
|
-
let idx = buffer.indexOf("\n\n");
|
|
1913
|
-
while (idx !== -1) {
|
|
1914
|
-
const chunk = buffer.slice(0, idx);
|
|
1915
|
-
buffer = buffer.slice(idx + 2);
|
|
1916
|
-
const dataLines = chunk.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim());
|
|
1917
|
-
if (dataLines.length > 0) {
|
|
1918
|
-
const data = dataLines.join("\n").trim();
|
|
1919
|
-
if (data && data !== "[DONE]") {
|
|
1920
|
-
try {
|
|
1921
|
-
yield JSON.parse(data);
|
|
1922
|
-
} catch {
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
idx = buffer.indexOf("\n\n");
|
|
1927
|
-
}
|
|
2050
|
+
for await (const event of readSseStream(body)) {
|
|
2051
|
+
const data = event.data.trim();
|
|
2052
|
+
if (!data || data === "[DONE]") continue;
|
|
2053
|
+
try {
|
|
2054
|
+
yield JSON.parse(data);
|
|
2055
|
+
} catch {
|
|
1928
2056
|
}
|
|
1929
|
-
} finally {
|
|
1930
|
-
reader.releaseLock();
|
|
1931
2057
|
}
|
|
1932
2058
|
}
|
|
1933
2059
|
function remapCodexId(id, idMap) {
|
|
@@ -1938,10 +2064,6 @@ function remapCodexId(id, idMap) {
|
|
|
1938
2064
|
idMap.set(id, mapped);
|
|
1939
2065
|
return mapped;
|
|
1940
2066
|
}
|
|
1941
|
-
function codexToolResultText(content) {
|
|
1942
|
-
if (typeof content === "string") return content;
|
|
1943
|
-
return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
1944
|
-
}
|
|
1945
2067
|
function toCodexInput(messages, options) {
|
|
1946
2068
|
let system;
|
|
1947
2069
|
const input = [];
|
|
@@ -1998,7 +2120,7 @@ function toCodexInput(messages, options) {
|
|
|
1998
2120
|
const toolImages = [];
|
|
1999
2121
|
for (const result of msg.content) {
|
|
2000
2122
|
const [callId] = result.toolCallId.includes("|") ? result.toolCallId.split("|", 2) : [result.toolCallId];
|
|
2001
|
-
const text =
|
|
2123
|
+
const text = toolResultText(result.content);
|
|
2002
2124
|
input.push({
|
|
2003
2125
|
type: "function_call_output",
|
|
2004
2126
|
call_id: remapCodexId(callId, idMap),
|
|
@@ -2033,14 +2155,10 @@ function toCodexTools(tools) {
|
|
|
2033
2155
|
type: "function",
|
|
2034
2156
|
name: tool.name,
|
|
2035
2157
|
description: tool.description,
|
|
2036
|
-
parameters:
|
|
2158
|
+
parameters: resolveToolSchema(tool),
|
|
2037
2159
|
strict: null
|
|
2038
2160
|
}));
|
|
2039
2161
|
}
|
|
2040
|
-
function extractCodexRequestId(message) {
|
|
2041
|
-
const match = message.match(/request ID ([a-z0-9-]{8,})/i);
|
|
2042
|
-
return match?.[1];
|
|
2043
|
-
}
|
|
2044
2162
|
function parseCodexErrorBody(text) {
|
|
2045
2163
|
if (!text) return {};
|
|
2046
2164
|
try {
|
|
@@ -2048,7 +2166,7 @@ function parseCodexErrorBody(text) {
|
|
|
2048
2166
|
const error = parsed.error;
|
|
2049
2167
|
const detail = parsed.detail;
|
|
2050
2168
|
const message = error?.message ?? parsed.message ?? (typeof detail === "string" ? detail : void 0);
|
|
2051
|
-
const requestId = parsed.request_id ?? error?.request_id ?? (message ?
|
|
2169
|
+
const requestId = parsed.request_id ?? error?.request_id ?? (message ? extractRequestIdFromMessage(message) : void 0);
|
|
2052
2170
|
const errorObj = error ?? parsed;
|
|
2053
2171
|
return {
|
|
2054
2172
|
...message ? { message } : {},
|
|
@@ -2098,12 +2216,6 @@ var CODE_ASSIST_SUPPORTED_MODELS = /* @__PURE__ */ new Set([
|
|
|
2098
2216
|
"gemma-4-31b-it",
|
|
2099
2217
|
"gemma-4-26b-a4b-it"
|
|
2100
2218
|
]);
|
|
2101
|
-
function isJsonObject4(value) {
|
|
2102
|
-
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2103
|
-
}
|
|
2104
|
-
function getEnvironment() {
|
|
2105
|
-
return globalThis.process?.env;
|
|
2106
|
-
}
|
|
2107
2219
|
function getGoogleProject(options) {
|
|
2108
2220
|
const env = getEnvironment();
|
|
2109
2221
|
return options.projectId ?? env?.GOOGLE_CLOUD_PROJECT ?? env?.GOOGLE_CLOUD_PROJECT_ID;
|
|
@@ -2125,6 +2237,20 @@ ${formatUnsupportedModelMessage(model)}`;
|
|
|
2125
2237
|
}
|
|
2126
2238
|
return `Gemini API error (${status}): ${body}`;
|
|
2127
2239
|
}
|
|
2240
|
+
function parseRetryDelaySeconds(body) {
|
|
2241
|
+
const match = body.match(/"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/);
|
|
2242
|
+
if (!match) return void 0;
|
|
2243
|
+
const seconds = Number(match[1]);
|
|
2244
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
2245
|
+
}
|
|
2246
|
+
function parseGeminiQuota(status, body) {
|
|
2247
|
+
if (status !== 429) return null;
|
|
2248
|
+
const lower = body.toLowerCase();
|
|
2249
|
+
if (!lower.includes("resource_exhausted") && !lower.includes("quota")) return null;
|
|
2250
|
+
const retryDelaySeconds = parseRetryDelaySeconds(body);
|
|
2251
|
+
const exhausted = retryDelaySeconds === void 0;
|
|
2252
|
+
return { exhausted, retryDelaySeconds };
|
|
2253
|
+
}
|
|
2128
2254
|
function toSystemAndContents(messages) {
|
|
2129
2255
|
let systemText = "";
|
|
2130
2256
|
const contents = [];
|
|
@@ -2203,7 +2329,7 @@ function toGeminiTools(tools) {
|
|
|
2203
2329
|
functionDeclarations: tools.map((tool) => ({
|
|
2204
2330
|
name: tool.name,
|
|
2205
2331
|
description: tool.description,
|
|
2206
|
-
parameters: sanitizeSchema(
|
|
2332
|
+
parameters: sanitizeSchema(resolveToolSchema(tool))
|
|
2207
2333
|
}))
|
|
2208
2334
|
}
|
|
2209
2335
|
];
|
|
@@ -2214,7 +2340,7 @@ function sanitizeSchema(schema) {
|
|
|
2214
2340
|
return clone;
|
|
2215
2341
|
}
|
|
2216
2342
|
function stripUnsupportedSchemaFields(value) {
|
|
2217
|
-
if (!
|
|
2343
|
+
if (!isJsonObject(value)) {
|
|
2218
2344
|
if (Array.isArray(value)) {
|
|
2219
2345
|
for (const item of value) stripUnsupportedSchemaFields(item);
|
|
2220
2346
|
}
|
|
@@ -2223,7 +2349,7 @@ function stripUnsupportedSchemaFields(value) {
|
|
|
2223
2349
|
delete value.$schema;
|
|
2224
2350
|
delete value.additionalProperties;
|
|
2225
2351
|
for (const item of Object.values(value)) {
|
|
2226
|
-
if (
|
|
2352
|
+
if (isJsonObject(item) || Array.isArray(item)) {
|
|
2227
2353
|
stripUnsupportedSchemaFields(item);
|
|
2228
2354
|
}
|
|
2229
2355
|
}
|
|
@@ -2276,7 +2402,8 @@ function toThinkingConfig(model, level) {
|
|
|
2276
2402
|
};
|
|
2277
2403
|
}
|
|
2278
2404
|
function buildGenerateRequest(options) {
|
|
2279
|
-
const
|
|
2405
|
+
const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
|
|
2406
|
+
const downgradedMessages = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
|
|
2280
2407
|
const { systemInstruction, contents } = toSystemAndContents(downgradedMessages);
|
|
2281
2408
|
const tools = toGeminiTools(options.tools);
|
|
2282
2409
|
const toolConfig = toGeminiToolConfig(options.toolChoice, options.tools);
|
|
@@ -2338,54 +2465,11 @@ function normalizeGeminiStopReason(reason) {
|
|
|
2338
2465
|
return "end_turn";
|
|
2339
2466
|
}
|
|
2340
2467
|
}
|
|
2341
|
-
function parseSseEvents(buffer) {
|
|
2342
|
-
const events = [];
|
|
2343
|
-
let cursor = 0;
|
|
2344
|
-
while (true) {
|
|
2345
|
-
const next = buffer.indexOf("\n\n", cursor);
|
|
2346
|
-
if (next === -1) break;
|
|
2347
|
-
const raw = buffer.slice(cursor, next);
|
|
2348
|
-
cursor = next + 2;
|
|
2349
|
-
let eventName;
|
|
2350
|
-
const dataLines = [];
|
|
2351
|
-
for (const line of raw.split("\n")) {
|
|
2352
|
-
if (line.startsWith("event:")) {
|
|
2353
|
-
eventName = line.slice("event:".length).trim();
|
|
2354
|
-
} else if (line.startsWith("data:")) {
|
|
2355
|
-
dataLines.push(line.slice("data:".length).trimStart());
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
if (dataLines.length > 0) {
|
|
2359
|
-
events.push({ event: eventName, data: dataLines.join("\n") });
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
return { events, remaining: buffer.slice(cursor) };
|
|
2363
|
-
}
|
|
2364
2468
|
async function* streamSse(response) {
|
|
2365
2469
|
if (!response.body) return;
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
try {
|
|
2370
|
-
while (true) {
|
|
2371
|
-
const { done, value } = await reader.read();
|
|
2372
|
-
if (done) break;
|
|
2373
|
-
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
|
2374
|
-
const parsed2 = parseSseEvents(buffer);
|
|
2375
|
-
buffer = parsed2.remaining;
|
|
2376
|
-
for (const event of parsed2.events) {
|
|
2377
|
-
if (event.data === "[DONE]") continue;
|
|
2378
|
-
yield JSON.parse(event.data);
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
buffer += decoder.decode().replace(/\r\n/g, "\n");
|
|
2382
|
-
const parsed = parseSseEvents(buffer + "\n\n");
|
|
2383
|
-
for (const event of parsed.events) {
|
|
2384
|
-
if (event.data === "[DONE]") continue;
|
|
2385
|
-
yield JSON.parse(event.data);
|
|
2386
|
-
}
|
|
2387
|
-
} finally {
|
|
2388
|
-
reader.releaseLock();
|
|
2470
|
+
for await (const event of readSseStream(response.body)) {
|
|
2471
|
+
if (event.data === "[DONE]") continue;
|
|
2472
|
+
yield JSON.parse(event.data);
|
|
2389
2473
|
}
|
|
2390
2474
|
}
|
|
2391
2475
|
function candidatesFromResponse(response) {
|
|
@@ -2408,7 +2492,7 @@ function readFunctionCallPart(part) {
|
|
|
2408
2492
|
return {
|
|
2409
2493
|
...part.functionCall.id ? { id: part.functionCall.id } : {},
|
|
2410
2494
|
name: part.functionCall.name,
|
|
2411
|
-
args:
|
|
2495
|
+
args: isJsonObject(part.functionCall.args) ? part.functionCall.args : {}
|
|
2412
2496
|
};
|
|
2413
2497
|
}
|
|
2414
2498
|
function makeToolCallId(index, providerId) {
|
|
@@ -2447,8 +2531,17 @@ async function fetchCodeAssist(plan, options) {
|
|
|
2447
2531
|
});
|
|
2448
2532
|
if (!response.ok) {
|
|
2449
2533
|
const text = await response.text().catch(() => "");
|
|
2450
|
-
|
|
2451
|
-
|
|
2534
|
+
const quota = parseGeminiQuota(response.status, text);
|
|
2535
|
+
let message = formatErrorMessage(response.status, text, options.model);
|
|
2536
|
+
let resetsAt;
|
|
2537
|
+
if (quota?.exhausted) {
|
|
2538
|
+
message = `Gemini quota exhausted \u2014 usage limit reached. ${message}`;
|
|
2539
|
+
} else if (quota?.retryDelaySeconds !== void 0) {
|
|
2540
|
+
resetsAt = Math.floor(Date.now() / 1e3) + Math.ceil(quota.retryDelaySeconds);
|
|
2541
|
+
}
|
|
2542
|
+
throw new ProviderError("gemini", message, {
|
|
2543
|
+
statusCode: response.status,
|
|
2544
|
+
...resetsAt !== void 0 ? { resetsAt } : {}
|
|
2452
2545
|
});
|
|
2453
2546
|
}
|
|
2454
2547
|
return response;
|
|
@@ -2839,6 +2932,7 @@ export {
|
|
|
2839
2932
|
StreamResult,
|
|
2840
2933
|
formatError,
|
|
2841
2934
|
formatErrorForDisplay,
|
|
2935
|
+
isHardBillingMessage,
|
|
2842
2936
|
isUsageLimitError,
|
|
2843
2937
|
palsuAssistantMessage,
|
|
2844
2938
|
palsuText,
|