@gajae-code/coding-agent 0.13.2 → 0.13.3

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +33 -8
  2. package/dist/types/config/model-registry.d.ts +1 -0
  3. package/dist/types/config/settings-schema.d.ts +3 -3
  4. package/dist/types/main.d.ts +1 -0
  5. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  6. package/dist/types/modes/components/gajae-pet-widget.d.ts +5 -5
  7. package/dist/types/modes/components/pet-capability.d.ts +5 -5
  8. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  9. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  10. package/dist/types/session/session-manager.d.ts +7 -0
  11. package/dist/types/tools/image-gen.d.ts +1 -0
  12. package/package.json +8 -8
  13. package/src/config/model-profile-activation.ts +3 -1
  14. package/src/config/model-profiles.ts +59 -2
  15. package/src/config/model-registry.ts +16 -1
  16. package/src/config/settings-schema.ts +1 -1
  17. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  18. package/src/internal-urls/docs-index.generated.ts +3 -2
  19. package/src/main.ts +4 -0
  20. package/src/modes/components/chrome.ts +4 -3
  21. package/src/modes/components/dynamic-theme-text.ts +22 -0
  22. package/src/modes/components/gajae-pet-widget.ts +130 -42
  23. package/src/modes/components/model-selector.ts +1 -6
  24. package/src/modes/components/pet-capability.ts +10 -5
  25. package/src/modes/components/settings-selector.ts +20 -5
  26. package/src/modes/components/status-line/segments.ts +3 -3
  27. package/src/modes/controllers/selector-controller.ts +61 -5
  28. package/src/modes/theme/defaults/index.ts +2 -0
  29. package/src/modes/theme/defaults/ouroboros.json +118 -0
  30. package/src/session/agent-session.ts +39 -0
  31. package/src/session/internal/managed-session-storage.ts +26 -19
  32. package/src/session/session-manager.ts +28 -0
  33. package/src/slash-commands/builtin-registry.ts +19 -3
  34. package/src/tools/image-gen.ts +322 -126
  35. package/src/tools/todo-contract.ts +55 -3
  36. package/src/tools/todo-write.ts +4 -3
@@ -41,6 +41,124 @@ const IMAGE_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
41
41
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
42
42
  const OPENAI_IMAGE_OUTPUT_FORMAT = "webp";
43
43
  const OPENAI_IMAGE_MIME_TYPE = "image/webp";
44
+ const MAX_PROVIDER_TEXT_LENGTH = 4096;
45
+ const REDACTED_PROVIDER_SECRET = "[redacted]";
46
+ const MAX_PROVIDER_BODY_BYTES = 64 * 1024 * 1024;
47
+ const MAX_PROVIDER_SSE_EVENT_BYTES = MAX_PROVIDER_BODY_BYTES;
48
+ const PROVIDER_MALFORMED_RESPONSE = "Provider returned a malformed image response.";
49
+ const PROVIDER_OVERSIZED_RESPONSE = "Provider image response exceeded the supported size.";
50
+
51
+ function escapeRegExp(value: string): string {
52
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
53
+ }
54
+
55
+ export function redactImageProviderText(value: unknown, activeApiKey?: string): string {
56
+ let text = typeof value === "string" ? value : value === undefined || value === null ? "" : String(value);
57
+ const key = activeApiKey?.trim();
58
+ if (key) {
59
+ text = text.replace(new RegExp(escapeRegExp(key), "g"), REDACTED_PROVIDER_SECRET);
60
+ const keyChars = [...key].filter(character => !/[\s\p{Cc}\p{Cf}]/u.test(character));
61
+ if (keyChars.length > 0) {
62
+ const separatorTolerantKey = keyChars.map(escapeRegExp).join("[\\s\\p{Cc}\\p{Cf}]*");
63
+ text = text.replace(new RegExp(separatorTolerantKey, "gu"), REDACTED_PROVIDER_SECRET);
64
+ }
65
+ }
66
+ text = text.replace(/[\p{Cc}\p{Cf}]/gu, " ");
67
+ text = text
68
+ .replace(/\b(?:bearer|basic)\s+[^\s,;]+/gi, match => `${match.split(/\s+/, 1)[0]} ${REDACTED_PROVIDER_SECRET}`)
69
+ .replace(
70
+ /(?:\b|["'])(?:authorization|api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token)["']?\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi,
71
+ match => match.replace(/([:=]\s*).*/, `$1${REDACTED_PROVIDER_SECRET}`),
72
+ )
73
+ .replace(/\b(?:sk|rk|pk|sess|ghp|gho|github_pat|xox[baprs])-[-A-Za-z0-9._]{8,}\b/gi, REDACTED_PROVIDER_SECRET)
74
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, REDACTED_PROVIDER_SECRET)
75
+ .replace(/\b[A-Za-z0-9+/_-]{40,}={0,2}\b/g, REDACTED_PROVIDER_SECRET);
76
+ return text.length > MAX_PROVIDER_TEXT_LENGTH ? `${text.slice(0, MAX_PROVIDER_TEXT_LENGTH - 1)}…` : text;
77
+ }
78
+
79
+ function tryParseProviderJson<T>(rawText: string): T | undefined {
80
+ if (Buffer.byteLength(rawText, "utf8") > MAX_PROVIDER_BODY_BYTES) return undefined;
81
+ try {
82
+ const parsed: unknown = JSON.parse(rawText);
83
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as T) : undefined;
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ }
88
+
89
+ function parseProviderJson<T>(rawText: string, activeApiKey?: string): T {
90
+ const parsed = tryParseProviderJson<T>(rawText);
91
+ if (parsed !== undefined) return parsed;
92
+ const message =
93
+ Buffer.byteLength(rawText, "utf8") > MAX_PROVIDER_BODY_BYTES
94
+ ? PROVIDER_OVERSIZED_RESPONSE
95
+ : PROVIDER_MALFORMED_RESPONSE;
96
+ throw new Error(redactImageProviderText(message, activeApiKey));
97
+ }
98
+
99
+ async function readProviderResponseText(
100
+ response: Response,
101
+ activeApiKey?: string,
102
+ signal?: AbortSignal,
103
+ ): Promise<string> {
104
+ const contentLength = response.headers.get("content-length");
105
+ if (contentLength !== null) {
106
+ const declaredLength = Number(contentLength);
107
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_PROVIDER_BODY_BYTES) {
108
+ await response.body?.cancel().catch(() => undefined);
109
+ throw new Error(PROVIDER_OVERSIZED_RESPONSE);
110
+ }
111
+ }
112
+
113
+ if (!response.body) return "";
114
+
115
+ const reader = response.body.getReader();
116
+ const decoder = new TextDecoder("utf-8");
117
+ const chunks: string[] = [];
118
+ let totalBytes = 0;
119
+ try {
120
+ while (true) {
121
+ const { done, value } = await reader.read();
122
+ if (done) break;
123
+ if (!value || value.byteLength === 0) continue;
124
+
125
+ if (totalBytes > MAX_PROVIDER_BODY_BYTES - value.byteLength) {
126
+ await reader.cancel().catch(() => undefined);
127
+ throw new Error(PROVIDER_OVERSIZED_RESPONSE);
128
+ }
129
+ totalBytes += value.byteLength;
130
+ const decoded = decoder.decode(value, { stream: true });
131
+ if (decoded.length > 0) chunks.push(decoded);
132
+ }
133
+
134
+ const trailingText = decoder.decode();
135
+ if (trailingText.length > 0) chunks.push(trailingText);
136
+ return chunks.join("");
137
+ } catch (error) {
138
+ if (signal?.aborted) throw signal.reason ?? error;
139
+ throw new Error(redactImageProviderText(error instanceof Error ? error.message : error, activeApiKey));
140
+ } finally {
141
+ reader.releaseLock();
142
+ }
143
+ }
144
+
145
+ const PROVIDER_SSE_READ_OPTIONS = {
146
+ maxEventBytes: MAX_PROVIDER_SSE_EVENT_BYTES,
147
+ maxTotalBytes: MAX_PROVIDER_BODY_BYTES,
148
+ } as const;
149
+
150
+ async function fetchImageProvider(
151
+ input: string | URL | Request,
152
+ init: RequestInit,
153
+ activeApiKey: string,
154
+ ): Promise<Response> {
155
+ try {
156
+ return await fetch(input, init);
157
+ } catch (error) {
158
+ if (init.signal?.aborted) throw init.signal.reason ?? error;
159
+ throw new Error(redactImageProviderText(error instanceof Error ? error.message : error, activeApiKey));
160
+ }
161
+ }
44
162
 
45
163
  const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
46
164
  const ALIBABA_TOKEN_PLAN_HOST = "https://token-plan.ap-southeast-1.maas.aliyuncs.com";
@@ -244,6 +362,30 @@ interface OpenAIHostedImageResult {
244
362
  usage?: OpenAIResponsesUsage;
245
363
  }
246
364
 
365
+ function redactOpenAIHostedImageResult(result: OpenAIHostedImageResult, activeApiKey: string): OpenAIHostedImageResult {
366
+ return {
367
+ ...result,
368
+ responseText: result.responseText ? redactImageProviderText(result.responseText, activeApiKey) : undefined,
369
+ revisedPrompt: result.revisedPrompt ? redactImageProviderText(result.revisedPrompt, activeApiKey) : undefined,
370
+ };
371
+ }
372
+
373
+ function redactGeminiPromptFeedback(
374
+ promptFeedback: GeminiPromptFeedback | undefined,
375
+ activeApiKey: string,
376
+ ): GeminiPromptFeedback | undefined {
377
+ if (!promptFeedback) return undefined;
378
+ return {
379
+ blockReason: promptFeedback.blockReason
380
+ ? redactImageProviderText(promptFeedback.blockReason, activeApiKey)
381
+ : undefined,
382
+ safetyRatings: promptFeedback.safetyRatings?.map(rating => ({
383
+ category: rating.category ? redactImageProviderText(rating.category, activeApiKey) : undefined,
384
+ probability: rating.probability ? redactImageProviderText(rating.probability, activeApiKey) : undefined,
385
+ })),
386
+ };
387
+ }
388
+
247
389
  interface OpenRouterImageUrl {
248
390
  url: string;
249
391
  }
@@ -566,7 +708,11 @@ async function readImageResponse(
566
708
  }
567
709
  }
568
710
 
569
- async function loadImageFromUrl(imageUrl: string, signal?: AbortSignal): Promise<InlineImageData> {
711
+ async function loadImageFromUrl(
712
+ imageUrl: string,
713
+ signal?: AbortSignal,
714
+ activeApiKey?: string,
715
+ ): Promise<InlineImageData> {
570
716
  if (imageUrl.startsWith("data:")) {
571
717
  const normalized = normalizeDataUrl(imageUrl.trim());
572
718
  if (!normalized.mimeType) {
@@ -599,8 +745,11 @@ async function loadImageFromUrl(imageUrl: string, signal?: AbortSignal): Promise
599
745
  }
600
746
  validateImageResponseFraming(response);
601
747
  if (status < 200 || status >= 300) {
602
- const preview = (await readImageResponse(response, MAX_IMAGE_ERROR_PREVIEW_SIZE, signal)).toString("utf8");
603
- throw new Error(`Image download failed (${status}): ${preview}`);
748
+ const preview = redactImageProviderText(
749
+ (await readImageResponse(response, MAX_IMAGE_ERROR_PREVIEW_SIZE, signal)).toString("utf8"),
750
+ activeApiKey,
751
+ );
752
+ throw new Error(`Image download failed (${status}): ${preview || "provider returned an error"}`);
604
753
  }
605
754
  const rawContentType = response.headers["content-type"];
606
755
  const contentType =
@@ -1049,13 +1198,9 @@ function collectOpenAIHostedImageResult(response: OpenAIHostedImageResponse): Op
1049
1198
  };
1050
1199
  }
1051
1200
 
1052
- function getOpenAIResponseErrorMessage(rawText: string): string {
1053
- try {
1054
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1055
- return parsed.error?.message ?? rawText;
1056
- } catch {
1057
- return rawText;
1058
- }
1201
+ function getOpenAIResponseErrorMessage(rawText: string, activeApiKey?: string): string {
1202
+ const parsed = tryParseProviderJson<{ error?: { message?: string } }>(rawText);
1203
+ return redactImageProviderText(parsed?.error?.message ?? "provider returned an error", activeApiKey);
1059
1204
  }
1060
1205
 
1061
1206
  function getOpenAIBaseUrl(model: Model, authCredentialType?: "api_key" | "oauth"): string {
@@ -1113,7 +1258,11 @@ function buildOpenAIImageHeaders(model: Model, apiKey: string, sessionId: string
1113
1258
  return headers;
1114
1259
  }
1115
1260
 
1116
- async function parseOpenAIHostedImageSse(response: Response, signal?: AbortSignal): Promise<OpenAIHostedImageResult> {
1261
+ async function parseOpenAIHostedImageSse(
1262
+ response: Response,
1263
+ signal: AbortSignal | undefined,
1264
+ activeApiKey: string,
1265
+ ): Promise<OpenAIHostedImageResult> {
1117
1266
  if (!response.body) {
1118
1267
  throw new Error("No response body");
1119
1268
  }
@@ -1121,21 +1270,33 @@ async function parseOpenAIHostedImageSse(response: Response, signal?: AbortSigna
1121
1270
  const fallbackOutput: OpenAIResponseOutput[] = [];
1122
1271
  let completedResponse: OpenAIHostedImageResponse | undefined;
1123
1272
 
1124
- for await (const event of readSseJson<OpenAISseEvent>(response.body, signal)) {
1125
- if (event.type === "error") {
1126
- const message = event.error?.message ?? event.message ?? "OpenAI image request failed";
1127
- throw new Error(message);
1128
- }
1129
- if (event.type === "response.failed") {
1130
- const message = event.response?.error?.message ?? "OpenAI image request failed";
1131
- throw new Error(message);
1132
- }
1133
- if (event.type === "response.output_item.done" && event.item) {
1134
- fallbackOutput.push(event.item);
1135
- }
1136
- if ((event.type === "response.completed" || event.type === "response.done") && event.response) {
1137
- completedResponse = event.response;
1273
+ try {
1274
+ for await (const event of readSseJson<OpenAISseEvent>(
1275
+ response.body,
1276
+ signal,
1277
+ undefined,
1278
+ PROVIDER_SSE_READ_OPTIONS,
1279
+ )) {
1280
+ if (!event || typeof event !== "object" || Array.isArray(event)) throw new Error(PROVIDER_MALFORMED_RESPONSE);
1281
+ if (event.type === "error") {
1282
+ const message = event.error?.message ?? event.message ?? "OpenAI image request failed";
1283
+ throw new Error(redactImageProviderText(message, activeApiKey));
1284
+ }
1285
+ if (event.type === "response.failed") {
1286
+ const message = event.response?.error?.message ?? "OpenAI image request failed";
1287
+ throw new Error(redactImageProviderText(message, activeApiKey));
1288
+ }
1289
+ if (event.type === "response.output_item.done" && event.item) {
1290
+ fallbackOutput.push(event.item);
1291
+ }
1292
+ if ((event.type === "response.completed" || event.type === "response.done") && event.response) {
1293
+ completedResponse = event.response;
1294
+ }
1138
1295
  }
1296
+ } catch (error) {
1297
+ if (signal?.aborted) throw signal.reason ?? error;
1298
+ if (error instanceof Error && error.message !== PROVIDER_MALFORMED_RESPONSE) throw error;
1299
+ throw new Error(redactImageProviderText(PROVIDER_MALFORMED_RESPONSE, activeApiKey));
1139
1300
  }
1140
1301
 
1141
1302
  return collectOpenAIHostedImageResult(
@@ -1157,25 +1318,32 @@ async function generateOpenAIHostedImage(
1157
1318
  const promptText = assemblePrompt(params);
1158
1319
  const stream = model.api === "openai-codex-responses" || model.provider === "openai-codex";
1159
1320
  const requestBody = buildOpenAIHostedImageRequest(model, promptText, params, inputImages, stream);
1160
- const response = await fetch(getOpenAIResponsesUrl(model, options?.authCredentialType), {
1161
- method: "POST",
1162
- headers: buildOpenAIImageHeaders(model, apiKey, sessionId),
1163
- body: JSON.stringify(requestBody),
1164
- signal,
1165
- });
1321
+ const response = await fetchImageProvider(
1322
+ getOpenAIResponsesUrl(model, options?.authCredentialType),
1323
+ {
1324
+ method: "POST",
1325
+ headers: buildOpenAIImageHeaders(model, apiKey, sessionId),
1326
+ body: JSON.stringify(requestBody),
1327
+ signal,
1328
+ },
1329
+ apiKey,
1330
+ );
1166
1331
 
1167
1332
  if (!response.ok) {
1168
- const errorText = await response.text();
1169
- throw new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`);
1333
+ const errorText = await readProviderResponseText(response, apiKey, signal);
1334
+ throw new Error(
1335
+ `OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText, apiKey)}`,
1336
+ );
1170
1337
  }
1171
1338
 
1172
1339
  const contentType = response.headers.get("content-type") ?? "";
1173
1340
  if (stream || contentType.includes("text/event-stream")) {
1174
- return parseOpenAIHostedImageSse(response, signal);
1341
+ return redactOpenAIHostedImageResult(await parseOpenAIHostedImageSse(response, signal, apiKey), apiKey);
1175
1342
  }
1176
1343
 
1177
- const data = (await response.json()) as OpenAIHostedImageResponse;
1178
- return collectOpenAIHostedImageResult(data);
1344
+ const rawText = await readProviderResponseText(response, apiKey, signal);
1345
+ const data = parseProviderJson<OpenAIHostedImageResponse>(rawText, apiKey);
1346
+ return redactOpenAIHostedImageResult(collectOpenAIHostedImageResult(data), apiKey);
1179
1347
  }
1180
1348
 
1181
1349
  function combineParts(response: GeminiGenerateContentResponse): GeminiPart[] {
@@ -1234,7 +1402,11 @@ interface AntigravitySseResult {
1234
1402
  usage?: GeminiUsageMetadata;
1235
1403
  }
1236
1404
 
1237
- async function parseAntigravitySseForImage(response: Response, signal?: AbortSignal): Promise<AntigravitySseResult> {
1405
+ async function parseAntigravitySseForImage(
1406
+ response: Response,
1407
+ signal: AbortSignal | undefined,
1408
+ activeApiKey?: string,
1409
+ ): Promise<AntigravitySseResult> {
1238
1410
  if (!response.body) {
1239
1411
  throw new Error("No response body");
1240
1412
  }
@@ -1242,25 +1414,42 @@ async function parseAntigravitySseForImage(response: Response, signal?: AbortSig
1242
1414
  const textParts: string[] = [];
1243
1415
  const images: InlineImageData[] = [];
1244
1416
  let usage: GeminiUsageMetadata | undefined;
1417
+ let chunks: AntigravityResponseChunk[];
1418
+ try {
1419
+ chunks = [];
1420
+ for await (const chunk of readSseJson<AntigravityResponseChunk>(
1421
+ response.body,
1422
+ signal,
1423
+ undefined,
1424
+ PROVIDER_SSE_READ_OPTIONS,
1425
+ )) {
1426
+ if (!chunk || typeof chunk !== "object" || Array.isArray(chunk)) throw new Error(PROVIDER_MALFORMED_RESPONSE);
1427
+ chunks.push(chunk);
1428
+ }
1429
+ } catch (error) {
1430
+ if (signal?.aborted) throw signal.reason ?? error;
1431
+ throw new Error(redactImageProviderText(PROVIDER_MALFORMED_RESPONSE, activeApiKey));
1432
+ }
1245
1433
 
1246
- for await (const chunk of readSseJson<AntigravityResponseChunk>(response.body, signal)) {
1434
+ for (const chunk of chunks) {
1247
1435
  const responseData = chunk.response;
1248
- if (!responseData) continue;
1249
- if (!responseData.candidates) continue;
1250
- for (const candidate of responseData.candidates) {
1251
- const parts = candidate.content?.parts;
1252
- if (!parts) continue;
1436
+ if (!responseData || typeof responseData !== "object") continue;
1437
+ const candidates = Array.isArray(responseData.candidates) ? responseData.candidates : [];
1438
+ for (const candidate of candidates) {
1439
+ if (!candidate || typeof candidate !== "object") continue;
1440
+ const parts = Array.isArray(candidate.content?.parts) ? candidate.content.parts : [];
1253
1441
  for (const part of parts) {
1254
- if (part.text) {
1442
+ if (!part || typeof part !== "object") continue;
1443
+ if (typeof part.text === "string" && part.text.length > 0) {
1255
1444
  textParts.push(part.text);
1256
1445
  }
1257
1446
  const inlineData = part.inlineData;
1258
- if (inlineData?.data && inlineData.mimeType) {
1447
+ if (inlineData && typeof inlineData === "object" && inlineData.data && inlineData.mimeType) {
1259
1448
  images.push({ data: inlineData.data, mimeType: inlineData.mimeType });
1260
1449
  }
1261
1450
  }
1262
1451
  }
1263
- if (responseData.usageMetadata) {
1452
+ if (responseData.usageMetadata && typeof responseData.usageMetadata === "object") {
1264
1453
  usage = responseData.usageMetadata;
1265
1454
  }
1266
1455
  }
@@ -1381,32 +1570,34 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1381
1570
 
1382
1571
  const { getAntigravityUserAgent } =
1383
1572
  require("@gajae-code/ai/providers/google-gemini-headers") as typeof import("@gajae-code/ai/providers/google-gemini-headers");
1384
- const response = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
1385
- method: "POST",
1386
- headers: {
1387
- Authorization: `Bearer ${apiKey.apiKey}`,
1388
- "Content-Type": "application/json",
1389
- Accept: "text/event-stream",
1390
- "User-Agent": getAntigravityUserAgent(),
1573
+ const response = await fetchImageProvider(
1574
+ `${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`,
1575
+ {
1576
+ method: "POST",
1577
+ headers: {
1578
+ Authorization: `Bearer ${apiKey.apiKey}`,
1579
+ "Content-Type": "application/json",
1580
+ Accept: "text/event-stream",
1581
+ "User-Agent": getAntigravityUserAgent(),
1582
+ },
1583
+ body: JSON.stringify(requestBody),
1584
+ signal: requestSignal,
1391
1585
  },
1392
- body: JSON.stringify(requestBody),
1393
- signal: requestSignal,
1394
- });
1586
+ apiKey.apiKey,
1587
+ );
1395
1588
 
1396
1589
  if (!response.ok) {
1397
- const errorText = await response.text();
1398
- let message = errorText;
1399
- try {
1400
- const parsed = JSON.parse(errorText) as { error?: { message?: string } };
1401
- message = parsed.error?.message ?? message;
1402
- } catch {
1403
- // Keep raw text.
1404
- }
1405
- throw new Error(`Antigravity image request failed (${response.status}): ${message}`);
1590
+ const errorText = await readProviderResponseText(response, apiKey.apiKey, requestSignal);
1591
+ const parsed = tryParseProviderJson<{ error?: { message?: string } }>(errorText);
1592
+ const message = parsed?.error?.message;
1593
+ throw new Error(
1594
+ `Antigravity image request failed (${response.status}): ${redactImageProviderText(message ?? "provider returned an error", apiKey.apiKey)}`,
1595
+ );
1406
1596
  }
1407
1597
 
1408
- const parsed = await parseAntigravitySseForImage(response, requestSignal);
1409
- const responseText = parsed.text.length > 0 ? parsed.text.join(" ") : undefined;
1598
+ const parsed = await parseAntigravitySseForImage(response, requestSignal, apiKey.apiKey);
1599
+ const responseText =
1600
+ parsed.text.length > 0 ? redactImageProviderText(parsed.text.join(" "), apiKey.apiKey) : undefined;
1410
1601
 
1411
1602
  if (parsed.images.length === 0) {
1412
1603
  const messageText = responseText ? `\n\n${responseText}` : "";
@@ -1452,38 +1643,40 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1452
1643
  messages: [{ role: "user" as const, content: contentParts }],
1453
1644
  };
1454
1645
 
1455
- const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1456
- method: "POST",
1457
- headers: {
1458
- "Content-Type": "application/json",
1459
- Authorization: `Bearer ${apiKey.apiKey}`,
1460
- "HTTP-Referer": "https://gaebal-gajae.dev/",
1461
- "X-OpenRouter-Title": "Gajae Code",
1462
- "X-OpenRouter-Categories": "cli-agent",
1646
+ const response = await fetchImageProvider(
1647
+ "https://openrouter.ai/api/v1/chat/completions",
1648
+ {
1649
+ method: "POST",
1650
+ headers: {
1651
+ "Content-Type": "application/json",
1652
+ Authorization: `Bearer ${apiKey.apiKey}`,
1653
+ "HTTP-Referer": "https://gaebal-gajae.dev/",
1654
+ "X-OpenRouter-Title": "Gajae Code",
1655
+ "X-OpenRouter-Categories": "cli-agent",
1656
+ },
1657
+ body: JSON.stringify(requestBody),
1658
+ signal: requestSignal,
1463
1659
  },
1464
- body: JSON.stringify(requestBody),
1465
- signal: requestSignal,
1466
- });
1660
+ apiKey.apiKey,
1661
+ );
1467
1662
 
1468
- const rawText = await response.text();
1663
+ const rawText = await readProviderResponseText(response, apiKey.apiKey, requestSignal);
1469
1664
  if (!response.ok) {
1470
- let message = rawText;
1471
- try {
1472
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1473
- message = parsed.error?.message ?? message;
1474
- } catch {
1475
- // Keep raw text.
1476
- }
1477
- throw new Error(`OpenRouter image request failed (${response.status}): ${message}`);
1665
+ const parsed = tryParseProviderJson<{ error?: { message?: string } }>(rawText);
1666
+ const message = parsed?.error?.message;
1667
+ throw new Error(
1668
+ `OpenRouter image request failed (${response.status}): ${redactImageProviderText(message ?? "provider returned an error", apiKey.apiKey)}`,
1669
+ );
1478
1670
  }
1479
1671
 
1480
- const data = JSON.parse(rawText) as OpenRouterResponse;
1672
+ const data = parseProviderJson<OpenRouterResponse>(rawText, apiKey.apiKey);
1481
1673
  const message = data.choices?.[0]?.message;
1482
- const responseText = collectOpenRouterResponseText(message);
1674
+ const rawResponseText = collectOpenRouterResponseText(message);
1675
+ const responseText = rawResponseText ? redactImageProviderText(rawResponseText, apiKey.apiKey) : undefined;
1483
1676
  const imageUrls = extractOpenRouterImageUrls(message);
1484
1677
  const inlineImages: InlineImageData[] = [];
1485
1678
  for (const imageUrl of imageUrls) {
1486
- inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
1679
+ inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal, apiKey.apiKey));
1487
1680
  }
1488
1681
 
1489
1682
  if (inlineImages.length === 0) {
@@ -1526,38 +1719,42 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1526
1719
  params.image_size,
1527
1720
  );
1528
1721
 
1529
- const response = await fetch(ALIBABA_IMAGE_GENERATION_URL, {
1530
- method: "POST",
1531
- headers: {
1532
- "Content-Type": "application/json",
1533
- Authorization: `Bearer ${apiKey.apiKey}`,
1722
+ const response = await fetchImageProvider(
1723
+ ALIBABA_IMAGE_GENERATION_URL,
1724
+ {
1725
+ method: "POST",
1726
+ headers: {
1727
+ "Content-Type": "application/json",
1728
+ Authorization: `Bearer ${apiKey.apiKey}`,
1729
+ },
1730
+ body: JSON.stringify(requestBody),
1731
+ signal: requestSignal,
1534
1732
  },
1535
- body: JSON.stringify(requestBody),
1536
- signal: requestSignal,
1537
- });
1733
+ apiKey.apiKey,
1734
+ );
1538
1735
 
1539
- const rawText = await response.text();
1736
+ const rawText = await readProviderResponseText(response, apiKey.apiKey, requestSignal);
1540
1737
  if (!response.ok) {
1541
- let message = rawText;
1542
- try {
1543
- const parsed = JSON.parse(rawText) as { message?: string; error?: { message?: string } };
1544
- message = parsed.error?.message ?? parsed.message ?? message;
1545
- } catch {
1546
- // Keep raw text.
1547
- }
1548
- throw new Error(`Alibaba image request failed (${response.status}): ${message}`);
1738
+ const parsed = tryParseProviderJson<{ message?: string; error?: { message?: string } }>(rawText);
1739
+ const message = parsed?.error?.message ?? parsed?.message;
1740
+ throw new Error(
1741
+ `Alibaba image request failed (${response.status}): ${redactImageProviderText(message ?? "provider returned an error", apiKey.apiKey)}`,
1742
+ );
1549
1743
  }
1550
1744
 
1551
- const data = JSON.parse(rawText) as AlibabaImageResponse;
1745
+ const data = parseProviderJson<AlibabaImageResponse>(rawText, apiKey.apiKey);
1552
1746
  if (data.code) {
1553
- throw new Error(`Alibaba image request failed: ${data.code}: ${data.message ?? ""}`.trim());
1747
+ throw new Error(
1748
+ `Alibaba image request failed: ${redactImageProviderText(`${data.code}: ${data.message ?? ""}`, apiKey.apiKey)}`,
1749
+ );
1554
1750
  }
1555
1751
 
1556
- const { imageUrls, responseText } = collectAlibabaImageResult(data);
1752
+ const { imageUrls, responseText: rawResponseText } = collectAlibabaImageResult(data);
1753
+ const responseText = rawResponseText ? redactImageProviderText(rawResponseText, apiKey.apiKey) : undefined;
1557
1754
  // Result URLs are short-lived OSS-signed URLs (24h); download immediately.
1558
1755
  const inlineImages: InlineImageData[] = [];
1559
1756
  for (const imageUrl of imageUrls) {
1560
- inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
1757
+ inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal, apiKey.apiKey));
1561
1758
  }
1562
1759
 
1563
1760
  if (inlineImages.length === 0) {
@@ -1615,7 +1812,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1615
1812
  generationConfig,
1616
1813
  };
1617
1814
 
1618
- const response = await fetch(
1815
+ const response = await fetchImageProvider(
1619
1816
  `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1620
1817
  {
1621
1818
  method: "POST",
@@ -1626,28 +1823,27 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1626
1823
  body: JSON.stringify(requestBody),
1627
1824
  signal: requestSignal,
1628
1825
  },
1826
+ apiKey.apiKey,
1629
1827
  );
1630
1828
 
1631
- const rawText = await response.text();
1829
+ const rawText = await readProviderResponseText(response, apiKey.apiKey, requestSignal);
1632
1830
  if (!response.ok) {
1633
- let message = rawText;
1634
- try {
1635
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1636
- message = parsed.error?.message ?? message;
1637
- } catch {
1638
- // Keep raw text.
1639
- }
1640
- throw new Error(`Gemini image request failed (${response.status}): ${message}`);
1831
+ const parsed = tryParseProviderJson<{ error?: { message?: string } }>(rawText);
1832
+ const message = parsed?.error?.message;
1833
+ throw new Error(
1834
+ `Gemini image request failed (${response.status}): ${redactImageProviderText(message ?? "provider returned an error", apiKey.apiKey)}`,
1835
+ );
1641
1836
  }
1642
1837
 
1643
- const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1838
+ const data = parseProviderJson<GeminiGenerateContentResponse>(rawText, apiKey.apiKey);
1644
1839
  const responseParts = combineParts(data);
1645
- const responseText = collectResponseText(responseParts);
1840
+ const rawResponseText = collectResponseText(responseParts);
1841
+ const responseText = rawResponseText ? redactImageProviderText(rawResponseText, apiKey.apiKey) : undefined;
1646
1842
  const inlineImages = collectInlineImages(responseParts);
1647
1843
 
1648
1844
  if (inlineImages.length === 0) {
1649
1845
  const blocked = data.promptFeedback?.blockReason
1650
- ? `Blocked: ${data.promptFeedback.blockReason}`
1846
+ ? `Blocked: ${redactImageProviderText(data.promptFeedback.blockReason, apiKey.apiKey)}`
1651
1847
  : "No image data returned.";
1652
1848
  return {
1653
1849
  content: [{ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` }],
@@ -1658,7 +1854,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1658
1854
  imagePaths: [],
1659
1855
  images: [],
1660
1856
  responseText,
1661
- promptFeedback: data.promptFeedback,
1857
+ promptFeedback: redactGeminiPromptFeedback(data.promptFeedback, apiKey.apiKey),
1662
1858
  usage: data.usageMetadata,
1663
1859
  },
1664
1860
  };
@@ -1675,7 +1871,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1675
1871
  imagePaths,
1676
1872
  images: inlineImages,
1677
1873
  responseText,
1678
- promptFeedback: data.promptFeedback,
1874
+ promptFeedback: redactGeminiPromptFeedback(data.promptFeedback, apiKey.apiKey),
1679
1875
  usage: data.usageMetadata,
1680
1876
  },
1681
1877
  };