@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.1

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 (101) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/cli.js +3305 -3302
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/cli-commands.d.ts +14 -1
  5. package/dist/types/config/model-registry.d.ts +4 -0
  6. package/dist/types/config/models-config-schema.d.ts +10 -0
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +1 -1
  9. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  10. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  11. package/dist/types/lib/xai-http.d.ts +11 -0
  12. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  13. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  14. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  15. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  16. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  17. package/dist/types/modes/components/hook-editor.d.ts +9 -2
  18. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  19. package/dist/types/modes/components/session-selector.d.ts +2 -0
  20. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  21. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  22. package/dist/types/modes/interactive-mode.d.ts +6 -0
  23. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  24. package/dist/types/modes/types.d.ts +10 -0
  25. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  26. package/dist/types/session/agent-session.d.ts +9 -1
  27. package/dist/types/session/streaming-output.d.ts +2 -0
  28. package/dist/types/slash-commands/types.d.ts +4 -4
  29. package/dist/types/system-prompt.d.ts +1 -1
  30. package/dist/types/tools/browser/registry.d.ts +7 -3
  31. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  32. package/dist/types/tools/index.d.ts +3 -1
  33. package/dist/types/tools/manage-skill.d.ts +2 -0
  34. package/dist/types/web/search/index.d.ts +2 -0
  35. package/dist/types/web/search/providers/base.d.ts +3 -0
  36. package/package.json +12 -12
  37. package/src/advisor/__tests__/advisor.test.ts +22 -12
  38. package/src/advisor/advise-tool.ts +7 -3
  39. package/src/cli/bench-cli.ts +4 -1
  40. package/src/cli/grep-cli.ts +2 -1
  41. package/src/cli-commands.ts +57 -23
  42. package/src/config/model-registry.ts +9 -0
  43. package/src/config/model-resolver.ts +43 -5
  44. package/src/config/models-config-schema.ts +1 -0
  45. package/src/config/settings-schema.ts +2 -1
  46. package/src/discovery/codex.ts +14 -4
  47. package/src/discovery/substitute-plugin-root.ts +23 -7
  48. package/src/export/html/template.js +2 -0
  49. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  50. package/src/internal-urls/agent-protocol.ts +73 -39
  51. package/src/lib/xai-http.ts +28 -2
  52. package/src/modes/acp/acp-agent.ts +28 -9
  53. package/src/modes/acp/acp-event-mapper.ts +8 -0
  54. package/src/modes/acp/acp-mode.ts +18 -4
  55. package/src/modes/components/assistant-message.ts +35 -14
  56. package/src/modes/components/chat-transcript-builder.ts +2 -0
  57. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  58. package/src/modes/components/custom-editor.ts +62 -1
  59. package/src/modes/components/hook-editor.ts +17 -2
  60. package/src/modes/components/login-dialog.test.ts +56 -0
  61. package/src/modes/components/login-dialog.ts +7 -3
  62. package/src/modes/components/plan-review-overlay.ts +51 -10
  63. package/src/modes/components/session-selector.ts +6 -0
  64. package/src/modes/components/status-line/component.ts +2 -4
  65. package/src/modes/components/tool-execution.test.ts +101 -0
  66. package/src/modes/components/tool-execution.ts +117 -9
  67. package/src/modes/components/transcript-container.ts +30 -2
  68. package/src/modes/controllers/command-controller-shared.ts +1 -1
  69. package/src/modes/controllers/command-controller.ts +1 -1
  70. package/src/modes/controllers/event-controller.ts +1 -0
  71. package/src/modes/controllers/selector-controller.ts +19 -17
  72. package/src/modes/interactive-mode.ts +66 -11
  73. package/src/modes/noninteractive-dispose.test.ts +60 -0
  74. package/src/modes/print-mode.ts +8 -3
  75. package/src/modes/rpc/rpc-mode.ts +12 -3
  76. package/src/modes/theme/theme.ts +2 -2
  77. package/src/modes/types.ts +10 -0
  78. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  79. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  80. package/src/prompts/system/system-prompt.md +1 -1
  81. package/src/sdk.ts +6 -2
  82. package/src/session/agent-session.ts +79 -34
  83. package/src/session/session-listing.ts +6 -5
  84. package/src/session/session-loader.ts +9 -0
  85. package/src/session/session-persistence.ts +11 -0
  86. package/src/session/streaming-output.ts +3 -1
  87. package/src/slash-commands/builtin-registry.ts +2 -0
  88. package/src/slash-commands/types.ts +4 -4
  89. package/src/system-prompt.ts +2 -2
  90. package/src/tools/browser/registry.ts +9 -2
  91. package/src/tools/browser/tab-supervisor.ts +65 -10
  92. package/src/tools/browser.ts +4 -3
  93. package/src/tools/eval.ts +3 -0
  94. package/src/tools/image-gen.ts +561 -512
  95. package/src/tools/index.ts +3 -1
  96. package/src/tools/manage-skill.ts +5 -3
  97. package/src/tools/output-meta.ts +7 -0
  98. package/src/tools/path-utils.ts +8 -5
  99. package/src/web/search/index.ts +21 -6
  100. package/src/web/search/providers/base.ts +3 -0
  101. package/src/web/search/providers/xai.ts +40 -9
@@ -58,6 +58,7 @@ const COMMON_IMAGE_ASPECT_RATIOS = ["1:1", "3:4", "4:3", "9:16", "16:9"] as cons
58
58
  const XAI_IMAGE_ASPECT_RATIOS = [...COMMON_IMAGE_ASPECT_RATIOS, "3:2", "2:3"] as const;
59
59
  const COMMON_IMAGE_ASPECT_RATIO_SET = new Set<string>(COMMON_IMAGE_ASPECT_RATIOS);
60
60
  const IMAGE_PROVIDER_PREFERENCES = new Set<string>(["auto", "antigravity", "gemini", "openai", "openrouter", "xai"]);
61
+ const AUTO_IMAGE_PROVIDER_ORDER = ["openai", "antigravity", "xai", "openrouter", "gemini"] as const;
61
62
 
62
63
  const responseModalitySchema = type('"IMAGE" | "TEXT"');
63
64
 
@@ -547,53 +548,58 @@ async function findOpenAIHostedImageCredentials(
547
548
  };
548
549
  }
549
550
 
551
+ function activeImageProvider(model: Model | undefined): Exclude<ImageProviderPreference, "auto"> | null {
552
+ switch (model?.provider) {
553
+ case "openai":
554
+ case "openai-codex":
555
+ return "openai";
556
+ case "google-antigravity":
557
+ return "antigravity";
558
+ case "xai":
559
+ case "xai-oauth":
560
+ return "xai";
561
+ case "openrouter":
562
+ return "openrouter";
563
+ case "google":
564
+ return "gemini";
565
+ default:
566
+ return null;
567
+ }
568
+ }
569
+
570
+ function imageProviderOrder(activeModel: Model | undefined): Array<Exclude<ImageProviderPreference, "auto">> {
571
+ const providers: Array<Exclude<ImageProviderPreference, "auto">> = [];
572
+ const added = new Set<Exclude<ImageProviderPreference, "auto">>();
573
+ const add = (provider: Exclude<ImageProviderPreference, "auto"> | null): void => {
574
+ if (!provider || added.has(provider)) return;
575
+ added.add(provider);
576
+ providers.push(provider);
577
+ };
578
+
579
+ if (preferredImageProvider !== "auto") add(preferredImageProvider);
580
+ add(activeImageProvider(activeModel));
581
+ for (const provider of AUTO_IMAGE_PROVIDER_ORDER) add(provider);
582
+ return providers;
583
+ }
584
+
550
585
  async function findImageApiKey(
586
+ provider: Exclude<ImageProviderPreference, "auto">,
551
587
  modelRegistry?: ModelRegistry,
552
588
  activeModel?: Model,
553
589
  sessionId?: string,
554
590
  ): Promise<ImageApiKey | null> {
555
- // If a specific provider is preferred, try it first.
556
- if (preferredImageProvider === "openai") {
557
- const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
558
- if (openAI) return openAI;
559
- // Fall through to auto-detect if preferred provider key not found.
560
- } else if (preferredImageProvider === "antigravity" && modelRegistry) {
561
- const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
562
- if (antigravity) return antigravity;
563
- // Fall through to auto-detect if preferred provider key not found.
564
- } else if (preferredImageProvider === "gemini") {
565
- const gemini = await findGeminiImageCredentials(modelRegistry, sessionId);
566
- if (gemini) return gemini;
567
- // Fall through to auto-detect if preferred provider key not found.
568
- } else if (preferredImageProvider === "openrouter") {
569
- const openRouter = await findOpenRouterImageCredentials(modelRegistry, sessionId);
570
- if (openRouter) return openRouter;
571
- // Fall through to auto-detect if preferred provider key not found.
572
- } else if (preferredImageProvider === "xai") {
573
- const xai = await findXAIImageCredentials(modelRegistry);
574
- if (xai) return xai;
575
- // Fall through to auto-detect if preferred provider key not found.
576
- }
577
-
578
- // Auto-detect: GPT hosted image generation, then Antigravity, xAI, OpenRouter, Gemini.
579
- const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
580
- if (openAI) return openAI;
581
-
582
- if (modelRegistry) {
583
- const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
584
- if (antigravity) return antigravity;
591
+ switch (provider) {
592
+ case "openai":
593
+ return findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
594
+ case "antigravity":
595
+ return modelRegistry ? findAntigravityCredentials(modelRegistry, sessionId) : null;
596
+ case "xai":
597
+ return findXAIImageCredentials(modelRegistry);
598
+ case "openrouter":
599
+ return findOpenRouterImageCredentials(modelRegistry, sessionId);
600
+ case "gemini":
601
+ return findGeminiImageCredentials(modelRegistry, sessionId);
585
602
  }
586
-
587
- const xai = await findXAIImageCredentials(modelRegistry);
588
- if (xai) return xai;
589
-
590
- const openRouter = await findOpenRouterImageCredentials(modelRegistry, sessionId);
591
- if (openRouter) return openRouter;
592
-
593
- const gemini = await findGeminiImageCredentials(modelRegistry, sessionId);
594
- if (gemini) return gemini;
595
-
596
- return null;
597
603
  }
598
604
 
599
605
  async function loadImageFromPath(imagePath: string, cwd: string): Promise<InlineImageData> {
@@ -900,9 +906,10 @@ async function generateOpenAIHostedImage(
900
906
 
901
907
  if (!response.ok) {
902
908
  const errorText = await response.text();
903
- throw Object.assign(
904
- new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`),
905
- { status: response.status },
909
+ throw new ProviderHttpError(
910
+ `OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`,
911
+ response.status,
912
+ { headers: response.headers },
906
913
  );
907
914
  }
908
915
 
@@ -1040,533 +1047,575 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1040
1047
  async execute(_toolCallId, params, _onUpdate, ctx, signal) {
1041
1048
  return untilAborted(signal, async () => {
1042
1049
  const sessionId = ctx.sessionManager.getSessionId();
1043
- const apiKey = await findImageApiKey(ctx.modelRegistry, ctx.model, sessionId);
1044
- if (!apiKey) {
1045
- throw new Error(
1046
- "No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity or xAI Grok OAuth, or set XAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY.",
1047
- );
1048
- }
1049
-
1050
- const provider = apiKey.provider;
1051
- const model =
1052
- provider === "openai" || provider === "openai-codex"
1053
- ? (apiKey.model?.id ?? "gpt")
1054
- : provider === "antigravity"
1055
- ? DEFAULT_ANTIGRAVITY_MODEL
1056
- : provider === "openrouter"
1057
- ? DEFAULT_OPENROUTER_MODEL
1058
- : provider === "xai"
1059
- ? DEFAULT_XAI_IMAGE_MODEL
1060
- : DEFAULT_MODEL;
1061
- const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
1062
- assertImageAspectRatioSupported(provider, params.aspect_ratio);
1050
+ const providerOrder = imageProviderOrder(ctx.model);
1063
1051
  const cwd = ctx.sessionManager.getCwd();
1064
-
1065
- const resolvedImages: InlineImageData[] = [];
1066
- if (params.input?.length) {
1067
- for (const input of params.input) {
1068
- resolvedImages.push(await resolveInputImage(input, cwd));
1069
- }
1070
- }
1071
-
1072
1052
  const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
1073
1053
  const fetchImpl = ctx.fetch ?? fetch;
1074
-
1075
- if (provider === "openai" || provider === "openai-codex") {
1076
- if (!apiKey.model) {
1077
- throw new Error("Missing active GPT model for OpenAI image generation");
1054
+ const failures: Array<{ provider: ImageProvider; error: ProviderHttpError }> = [];
1055
+ let unsupportedAspectRatioProvider: ImageProvider | undefined;
1056
+ let foundCredentials = false;
1057
+ let resolvedImageCache: InlineImageData[] | undefined;
1058
+
1059
+ for (const preferredProvider of providerOrder) {
1060
+ const apiKey = await findImageApiKey(preferredProvider, ctx.modelRegistry, ctx.model, sessionId);
1061
+ if (!apiKey) continue;
1062
+ foundCredentials = true;
1063
+ if (!resolvedImageCache) {
1064
+ resolvedImageCache = [];
1065
+ if (params.input?.length) {
1066
+ for (const input of params.input) {
1067
+ resolvedImageCache.push(await resolveInputImage(input, cwd));
1068
+ }
1069
+ }
1078
1070
  }
1071
+ const resolvedImages = resolvedImageCache;
1072
+
1073
+ const provider = apiKey.provider;
1074
+ try {
1075
+ const model =
1076
+ provider === "openai" || provider === "openai-codex"
1077
+ ? (apiKey.model?.id ?? "gpt")
1078
+ : provider === "antigravity"
1079
+ ? DEFAULT_ANTIGRAVITY_MODEL
1080
+ : provider === "openrouter"
1081
+ ? DEFAULT_OPENROUTER_MODEL
1082
+ : provider === "xai"
1083
+ ? DEFAULT_XAI_IMAGE_MODEL
1084
+ : DEFAULT_MODEL;
1085
+ const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
1086
+ if (
1087
+ params.aspect_ratio &&
1088
+ provider !== "xai" &&
1089
+ !COMMON_IMAGE_ASPECT_RATIO_SET.has(params.aspect_ratio)
1090
+ ) {
1091
+ unsupportedAspectRatioProvider ??= provider;
1092
+ continue;
1093
+ }
1094
+ if (provider === "openai" || provider === "openai-codex") {
1095
+ if (!apiKey.model) {
1096
+ throw new Error("Missing active GPT model for OpenAI image generation");
1097
+ }
1098
+
1099
+ const hostedModel = apiKey.model;
1100
+ const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel, sessionId);
1101
+
1102
+ const parsed = await withAuth(
1103
+ hostedKey,
1104
+ key =>
1105
+ generateOpenAIHostedImage(
1106
+ key,
1107
+ hostedModel,
1108
+ params,
1109
+ resolvedImages,
1110
+ fetchImpl,
1111
+ requestSignal,
1112
+ sessionId,
1113
+ ),
1114
+ { signal: requestSignal },
1115
+ );
1116
+
1117
+ if (parsed.images.length === 0) {
1118
+ const messageText = parsed.responseText ? `\n\n${parsed.responseText}` : "";
1119
+ return {
1120
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1121
+ details: {
1122
+ provider,
1123
+ model,
1124
+ imageCount: 0,
1125
+ imagePaths: [],
1126
+ images: [],
1127
+ responseText: parsed.responseText,
1128
+ revisedPrompt: parsed.revisedPrompt,
1129
+ usage: parsed.usage,
1130
+ },
1131
+ };
1132
+ }
1133
+
1134
+ const imagePaths = await saveImagesToTemp(parsed.images);
1135
+
1136
+ return {
1137
+ content: [
1138
+ { type: "text", text: buildResponseSummary(provider, model, imagePaths, parsed.responseText) },
1139
+ ],
1140
+ details: {
1141
+ provider,
1142
+ model,
1143
+ imageCount: parsed.images.length,
1144
+ imagePaths,
1145
+ images: parsed.images,
1146
+ responseText: parsed.responseText,
1147
+ revisedPrompt: parsed.revisedPrompt,
1148
+ usage: parsed.usage,
1149
+ },
1150
+ };
1151
+ }
1152
+
1153
+ if (provider === "antigravity") {
1154
+ if (!apiKey.projectId) {
1155
+ throw new Error("Missing projectId in antigravity credentials");
1156
+ }
1079
1157
 
1080
- const hostedModel = apiKey.model;
1081
- const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel, sessionId);
1082
-
1083
- const parsed = await withAuth(
1084
- hostedKey,
1085
- key =>
1086
- generateOpenAIHostedImage(
1087
- key,
1088
- hostedModel,
1089
- params,
1090
- resolvedImages,
1091
- fetchImpl,
1092
- requestSignal,
1158
+ const prompt = assemblePrompt(params);
1159
+ const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
1093
1160
  sessionId,
1094
- ),
1095
- { signal: requestSignal },
1096
- );
1161
+ modelId: DEFAULT_ANTIGRAVITY_MODEL,
1162
+ });
1097
1163
 
1098
- if (parsed.images.length === 0) {
1099
- const messageText = parsed.responseText ? `\n\n${parsed.responseText}` : "";
1100
- return {
1101
- content: [{ type: "text", text: `No image data returned.${messageText}` }],
1102
- details: {
1103
- provider,
1104
- model,
1105
- imageCount: 0,
1106
- imagePaths: [],
1107
- images: [],
1108
- responseText: parsed.responseText,
1109
- revisedPrompt: parsed.revisedPrompt,
1110
- usage: parsed.usage,
1111
- },
1112
- };
1113
- }
1164
+ const response = await withAuth(
1165
+ antigravityKey,
1166
+ async key => {
1167
+ // On a retry the resolver yields the raw stored credential JSON
1168
+ // ({ token, projectId }); the initial seed is the already-parsed
1169
+ // access token. Tolerate both, falling back to the seed projectId.
1170
+ const rotated = parseAntigravityCredentials(key);
1171
+ const bearer = rotated?.accessToken ?? key;
1172
+ const projectId = rotated?.projectId ?? apiKey.projectId!;
1173
+ const requestBody = buildAntigravityRequest(
1174
+ prompt,
1175
+ model,
1176
+ projectId,
1177
+ params.aspect_ratio,
1178
+ params.image_size,
1179
+ resolvedImages,
1180
+ );
1114
1181
 
1115
- const imagePaths = await saveImagesToTemp(parsed.images);
1116
-
1117
- return {
1118
- content: [
1119
- { type: "text", text: buildResponseSummary(provider, model, imagePaths, parsed.responseText) },
1120
- ],
1121
- details: {
1122
- provider,
1123
- model,
1124
- imageCount: parsed.images.length,
1125
- imagePaths,
1126
- images: parsed.images,
1127
- responseText: parsed.responseText,
1128
- revisedPrompt: parsed.revisedPrompt,
1129
- usage: parsed.usage,
1130
- },
1131
- };
1132
- }
1182
+ let endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_PROD, DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX];
1183
+ try {
1184
+ const mode = settings.get("providers.antigravityEndpoint");
1185
+ if (mode === "production") {
1186
+ endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_PROD];
1187
+ } else if (mode === "sandbox") {
1188
+ endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX];
1189
+ }
1190
+ } catch {
1191
+ // Ignored
1192
+ }
1133
1193
 
1134
- if (provider === "antigravity") {
1135
- if (!apiKey.projectId) {
1136
- throw new Error("Missing projectId in antigravity credentials");
1137
- }
1194
+ let resp: Response | undefined;
1195
+ let lastError: Error | undefined;
1196
+
1197
+ for (let i = 0; i < endpoints.length; i++) {
1198
+ const endpoint = endpoints[i];
1199
+ const isLastEndpoint = i === endpoints.length - 1;
1200
+ try {
1201
+ resp = await fetchImpl(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
1202
+ method: "POST",
1203
+ headers: {
1204
+ Authorization: `Bearer ${bearer}`,
1205
+ "Content-Type": "application/json",
1206
+ Accept: "text/event-stream",
1207
+ "User-Agent": getAntigravityUserAgent(),
1208
+ },
1209
+ body: JSON.stringify(requestBody),
1210
+ signal: requestSignal,
1211
+ });
1212
+
1213
+ if (resp.ok) {
1214
+ break;
1215
+ }
1216
+
1217
+ const errorText = await resp.text();
1218
+ let message = errorText;
1219
+ try {
1220
+ const parsedErr = JSON.parse(errorText) as { error?: { message?: string } };
1221
+ message = parsedErr.error?.message ?? message;
1222
+ } catch {
1223
+ // Keep raw text.
1224
+ }
1225
+
1226
+ lastError = new ProviderHttpError(
1227
+ `Antigravity image request failed (${resp.status}): ${message}`,
1228
+ resp.status,
1229
+ { headers: resp.headers },
1230
+ );
1231
+
1232
+ if (resp.status === 429 || (resp.status >= 500 && resp.status < 600)) {
1233
+ if (!isLastEndpoint) {
1234
+ continue;
1235
+ }
1236
+ }
1237
+ break;
1238
+ } catch (error) {
1239
+ lastError = error as Error;
1240
+ if (isLastEndpoint) {
1241
+ break;
1242
+ }
1243
+ }
1244
+ }
1138
1245
 
1139
- const prompt = assemblePrompt(params);
1140
- const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
1141
- sessionId,
1142
- modelId: DEFAULT_ANTIGRAVITY_MODEL,
1143
- });
1144
-
1145
- const response = await withAuth(
1146
- antigravityKey,
1147
- async key => {
1148
- // On a retry the resolver yields the raw stored credential JSON
1149
- // ({ token, projectId }); the initial seed is the already-parsed
1150
- // access token. Tolerate both, falling back to the seed projectId.
1151
- const rotated = parseAntigravityCredentials(key);
1152
- const bearer = rotated?.accessToken ?? key;
1153
- const projectId = rotated?.projectId ?? apiKey.projectId!;
1154
- const requestBody = buildAntigravityRequest(
1155
- prompt,
1156
- model,
1157
- projectId,
1158
- params.aspect_ratio,
1159
- params.image_size,
1160
- resolvedImages,
1246
+ if (!resp?.ok) {
1247
+ throw lastError ?? new Error("Antigravity image generation failed");
1248
+ }
1249
+
1250
+ return resp;
1251
+ },
1252
+ { signal: requestSignal },
1161
1253
  );
1162
1254
 
1163
- let endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_PROD, DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX];
1164
- try {
1165
- const mode = settings.get("providers.antigravityEndpoint");
1166
- if (mode === "production") {
1167
- endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_PROD];
1168
- } else if (mode === "sandbox") {
1169
- endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX];
1170
- }
1171
- } catch {
1172
- // Ignored
1255
+ const parsed = await parseAntigravitySseForImage(response, requestSignal);
1256
+ const responseText = parsed.text.length > 0 ? parsed.text.join(" ") : undefined;
1257
+
1258
+ if (parsed.images.length === 0) {
1259
+ const messageText = responseText ? `\n\n${responseText}` : "";
1260
+ return {
1261
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1262
+ details: {
1263
+ provider,
1264
+ model,
1265
+ imageCount: 0,
1266
+ imagePaths: [],
1267
+ images: [],
1268
+ responseText,
1269
+ usage: parsed.usage,
1270
+ },
1271
+ };
1272
+ }
1273
+
1274
+ const imagePaths = await saveImagesToTemp(parsed.images);
1275
+
1276
+ return {
1277
+ content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1278
+ details: {
1279
+ provider,
1280
+ model,
1281
+ imageCount: parsed.images.length,
1282
+ imagePaths,
1283
+ images: parsed.images,
1284
+ responseText,
1285
+ usage: parsed.usage,
1286
+ },
1287
+ };
1288
+ }
1289
+
1290
+ if (provider === "xai") {
1291
+ if (!ctx.modelRegistry) {
1292
+ throw new Error("Missing modelRegistry for xAI image generation");
1293
+ }
1294
+ const xaiCreds = await resolveXAIHttpCredentials(ctx.modelRegistry, resolvedModel);
1295
+ if (!xaiCreds) {
1296
+ throw new Error(
1297
+ "No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok or X Premium+) or set XAI_API_KEY.",
1298
+ );
1299
+ }
1300
+
1301
+ const prompt = assemblePrompt(params);
1302
+ const aspectRatio = params.aspect_ratio ?? "1:1";
1303
+ const xaiResolution = resolveXAIResolution(params.image_size);
1304
+
1305
+ const isEdit = resolvedImages.length > 0;
1306
+ if (isEdit && resolvedImages.length > XAI_MAX_EDIT_IMAGES) {
1307
+ throw new Error(
1308
+ `xAI image edits accept up to ${XAI_MAX_EDIT_IMAGES} reference images; got ${resolvedImages.length}.`,
1309
+ );
1173
1310
  }
1174
1311
 
1175
- let resp: Response | undefined;
1176
- let lastError: Error | undefined;
1312
+ const xaiBaseBody: XAIImageRequestBase = {
1313
+ model: resolvedModel,
1314
+ prompt,
1315
+ aspect_ratio: aspectRatio,
1316
+ resolution: xaiResolution,
1317
+ n: 1,
1318
+ response_format: "b64_json",
1319
+ };
1320
+ const xaiBody: XAIImageRequestBody = isEdit
1321
+ ? buildXAIEditPayload(xaiBaseBody, resolvedImages)
1322
+ : xaiBaseBody;
1323
+ const xaiEndpoint = isEdit ? "/images/edits" : "/images/generations";
1324
+
1325
+ const xaiKey: ApiKey = ctx.modelRegistry.resolver(xaiCreds.provider, {
1326
+ sessionId,
1327
+ baseUrl: xaiCreds.baseURL,
1328
+ });
1177
1329
 
1178
- for (let i = 0; i < endpoints.length; i++) {
1179
- const endpoint = endpoints[i];
1180
- const isLastEndpoint = i === endpoints.length - 1;
1181
- try {
1182
- resp = await fetchImpl(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
1330
+ const xaiRawText = await withAuth(
1331
+ xaiKey,
1332
+ async key => {
1333
+ const resp = await fetchImpl(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1183
1334
  method: "POST",
1184
1335
  headers: {
1185
- Authorization: `Bearer ${bearer}`,
1336
+ Authorization: `Bearer ${key}`,
1186
1337
  "Content-Type": "application/json",
1187
- Accept: "text/event-stream",
1188
- "User-Agent": getAntigravityUserAgent(),
1338
+ "User-Agent": ohMyPiXAIUserAgent(),
1189
1339
  },
1190
- body: JSON.stringify(requestBody),
1340
+ body: JSON.stringify(xaiBody),
1191
1341
  signal: requestSignal,
1192
1342
  });
1193
-
1194
- if (resp.ok) {
1195
- break;
1196
- }
1197
-
1198
- const errorText = await resp.text();
1199
- let message = errorText;
1200
- try {
1201
- const parsedErr = JSON.parse(errorText) as { error?: { message?: string } };
1202
- message = parsedErr.error?.message ?? message;
1203
- } catch {
1204
- // Keep raw text.
1205
- }
1206
-
1207
- lastError = new ProviderHttpError(
1208
- `Antigravity image request failed (${resp.status}): ${message}`,
1209
- resp.status,
1210
- { headers: resp.headers },
1211
- );
1212
-
1213
- if (resp.status === 429 || (resp.status >= 500 && resp.status < 600)) {
1214
- if (!isLastEndpoint) {
1215
- continue;
1343
+ const rawText = await resp.text();
1344
+ if (!resp.ok) {
1345
+ let message = rawText;
1346
+ try {
1347
+ const parsedErr = JSON.parse(rawText) as { error?: { message?: string } };
1348
+ message = parsedErr.error?.message ?? message;
1349
+ } catch {
1350
+ // Keep raw text.
1216
1351
  }
1352
+ throw new ProviderHttpError(
1353
+ `xAI image request failed (${resp.status}): ${message}`,
1354
+ resp.status,
1355
+ {
1356
+ headers: resp.headers,
1357
+ },
1358
+ );
1217
1359
  }
1218
- break;
1219
- } catch (error) {
1220
- lastError = error as Error;
1221
- if (isLastEndpoint) {
1222
- break;
1223
- }
1360
+ return rawText;
1361
+ },
1362
+ { signal: requestSignal },
1363
+ );
1364
+
1365
+ const xaiData = JSON.parse(xaiRawText) as {
1366
+ data?: Array<{ b64_json?: string; url?: string }>;
1367
+ };
1368
+ const xaiInlineImages: InlineImageData[] = [];
1369
+ for (const entry of xaiData.data ?? []) {
1370
+ if (entry.b64_json) {
1371
+ const bytes = Buffer.from(entry.b64_json, "base64");
1372
+ const mimeType = parseImageMetadata(bytes)?.mimeType ?? "image/png";
1373
+ xaiInlineImages.push({ data: entry.b64_json, mimeType });
1374
+ } else if (entry.url) {
1375
+ xaiInlineImages.push(await loadImageFromUrl(entry.url, fetchImpl, requestSignal));
1224
1376
  }
1225
1377
  }
1226
1378
 
1227
- if (!resp?.ok) {
1228
- throw lastError ?? new Error("Antigravity image generation failed");
1379
+ if (xaiInlineImages.length === 0) {
1380
+ return {
1381
+ content: [{ type: "text", text: "No image data returned." }],
1382
+ details: {
1383
+ provider,
1384
+ model: resolvedModel,
1385
+ imageCount: 0,
1386
+ imagePaths: [],
1387
+ images: [],
1388
+ },
1389
+ };
1229
1390
  }
1230
1391
 
1231
- return resp;
1232
- },
1233
- { signal: requestSignal },
1234
- );
1235
-
1236
- const parsed = await parseAntigravitySseForImage(response, requestSignal);
1237
- const responseText = parsed.text.length > 0 ? parsed.text.join(" ") : undefined;
1392
+ const xaiImagePaths = await saveImagesToTemp(xaiInlineImages);
1393
+
1394
+ return {
1395
+ content: [
1396
+ { type: "text", text: buildResponseSummary(provider, resolvedModel, xaiImagePaths, undefined) },
1397
+ ],
1398
+ details: {
1399
+ provider,
1400
+ model: resolvedModel,
1401
+ imageCount: xaiInlineImages.length,
1402
+ imagePaths: xaiImagePaths,
1403
+ images: xaiInlineImages,
1404
+ },
1405
+ };
1406
+ }
1238
1407
 
1239
- if (parsed.images.length === 0) {
1240
- const messageText = responseText ? `\n\n${responseText}` : "";
1241
- return {
1242
- content: [{ type: "text", text: `No image data returned.${messageText}` }],
1243
- details: {
1244
- provider,
1245
- model,
1246
- imageCount: 0,
1247
- imagePaths: [],
1248
- images: [],
1249
- responseText,
1250
- usage: parsed.usage,
1251
- },
1252
- };
1253
- }
1408
+ if (provider === "openrouter") {
1409
+ const prompt = assemblePrompt(params);
1410
+ const contentParts: OpenRouterContentPart[] = [{ type: "text", text: prompt }];
1411
+ for (const image of resolvedImages) {
1412
+ contentParts.push({ type: "image_url", image_url: { url: toDataUrl(image) } });
1413
+ }
1254
1414
 
1255
- const imagePaths = await saveImagesToTemp(parsed.images);
1256
-
1257
- return {
1258
- content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1259
- details: {
1260
- provider,
1261
- model,
1262
- imageCount: parsed.images.length,
1263
- imagePaths,
1264
- images: parsed.images,
1265
- responseText,
1266
- usage: parsed.usage,
1267
- },
1268
- };
1269
- }
1415
+ const requestBody = {
1416
+ model: resolvedModel,
1417
+ messages: [{ role: "user" as const, content: contentParts }],
1418
+ };
1270
1419
 
1271
- if (provider === "xai") {
1272
- if (!ctx.modelRegistry) {
1273
- throw new Error("Missing modelRegistry for xAI image generation");
1274
- }
1275
- const xaiCreds = await resolveXAIHttpCredentials(ctx.modelRegistry, resolvedModel);
1276
- if (!xaiCreds) {
1277
- throw new Error(
1278
- "No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok or X Premium+) or set XAI_API_KEY.",
1279
- );
1280
- }
1420
+ const rawText = await withAuth(
1421
+ apiKey.apiKey,
1422
+ async key => {
1423
+ const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
1424
+ method: "POST",
1425
+ headers: {
1426
+ "Content-Type": "application/json",
1427
+ Authorization: `Bearer ${key}`,
1428
+ "HTTP-Referer": "https://omp.sh/",
1429
+ "X-OpenRouter-Title": "Oh-My-Pi",
1430
+ "X-OpenRouter-Categories": "cli-agent",
1431
+ },
1432
+ body: JSON.stringify(requestBody),
1433
+ signal: requestSignal,
1434
+ });
1435
+ const text = await resp.text();
1436
+ if (!resp.ok) {
1437
+ let message = text;
1438
+ try {
1439
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1440
+ message = parsed.error?.message ?? message;
1441
+ } catch {
1442
+ // Keep raw text.
1443
+ }
1444
+ throw new ProviderHttpError(
1445
+ `OpenRouter image request failed (${resp.status}): ${message}`,
1446
+ resp.status,
1447
+ { headers: resp.headers },
1448
+ );
1449
+ }
1450
+ return text;
1451
+ },
1452
+ { signal: requestSignal },
1453
+ );
1281
1454
 
1282
- const prompt = assemblePrompt(params);
1283
- const aspectRatio = params.aspect_ratio ?? "1:1";
1284
- const xaiResolution = resolveXAIResolution(params.image_size);
1455
+ const data = JSON.parse(rawText) as OpenRouterResponse;
1456
+ const message = data.choices?.[0]?.message;
1457
+ const responseText = collectOpenRouterResponseText(message);
1458
+ const imageUrls = extractOpenRouterImageUrls(message);
1459
+ const inlineImages: InlineImageData[] = [];
1460
+ for (const imageUrl of imageUrls) {
1461
+ inlineImages.push(await loadImageFromUrl(imageUrl, fetchImpl, requestSignal));
1462
+ }
1285
1463
 
1286
- const isEdit = resolvedImages.length > 0;
1287
- if (isEdit && resolvedImages.length > XAI_MAX_EDIT_IMAGES) {
1288
- throw new Error(
1289
- `xAI image edits accept up to ${XAI_MAX_EDIT_IMAGES} reference images; got ${resolvedImages.length}.`,
1290
- );
1291
- }
1464
+ if (inlineImages.length === 0) {
1465
+ const messageText = responseText ? `\n\n${responseText}` : "";
1466
+ return {
1467
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1468
+ details: {
1469
+ provider,
1470
+ model: resolvedModel,
1471
+ imageCount: 0,
1472
+ imagePaths: [],
1473
+ images: [],
1474
+ responseText,
1475
+ },
1476
+ };
1477
+ }
1292
1478
 
1293
- const xaiBaseBody: XAIImageRequestBase = {
1294
- model: resolvedModel,
1295
- prompt,
1296
- aspect_ratio: aspectRatio,
1297
- resolution: xaiResolution,
1298
- n: 1,
1299
- response_format: "b64_json",
1300
- };
1301
- const xaiBody: XAIImageRequestBody = isEdit
1302
- ? buildXAIEditPayload(xaiBaseBody, resolvedImages)
1303
- : xaiBaseBody;
1304
- const xaiEndpoint = isEdit ? "/images/edits" : "/images/generations";
1305
-
1306
- const xaiKey: ApiKey = ctx.modelRegistry.resolver(xaiCreds.provider, {
1307
- sessionId,
1308
- baseUrl: xaiCreds.baseURL,
1309
- });
1310
-
1311
- const xaiRawText = await withAuth(
1312
- xaiKey,
1313
- async key => {
1314
- const resp = await fetchImpl(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1315
- method: "POST",
1316
- headers: {
1317
- Authorization: `Bearer ${key}`,
1318
- "Content-Type": "application/json",
1319
- "User-Agent": ohMyPiXAIUserAgent(),
1479
+ const imagePaths = await saveImagesToTemp(inlineImages);
1480
+
1481
+ return {
1482
+ content: [
1483
+ { type: "text", text: buildResponseSummary(provider, resolvedModel, imagePaths, responseText) },
1484
+ ],
1485
+ details: {
1486
+ provider,
1487
+ model: resolvedModel,
1488
+ imageCount: inlineImages.length,
1489
+ imagePaths,
1490
+ images: inlineImages,
1491
+ responseText,
1320
1492
  },
1321
- body: JSON.stringify(xaiBody),
1322
- signal: requestSignal,
1323
- });
1324
- const rawText = await resp.text();
1325
- if (!resp.ok) {
1326
- let message = rawText;
1327
- try {
1328
- const parsedErr = JSON.parse(rawText) as { error?: { message?: string } };
1329
- message = parsedErr.error?.message ?? message;
1330
- } catch {
1331
- // Keep raw text.
1332
- }
1333
- throw new ProviderHttpError(`xAI image request failed (${resp.status}): ${message}`, resp.status, {
1334
- headers: resp.headers,
1335
- });
1336
- }
1337
- return rawText;
1338
- },
1339
- { signal: requestSignal },
1340
- );
1493
+ };
1494
+ }
1341
1495
 
1342
- const xaiData = JSON.parse(xaiRawText) as {
1343
- data?: Array<{ b64_json?: string; url?: string }>;
1344
- };
1345
- const xaiInlineImages: InlineImageData[] = [];
1346
- for (const entry of xaiData.data ?? []) {
1347
- if (entry.b64_json) {
1348
- const bytes = Buffer.from(entry.b64_json, "base64");
1349
- const mimeType = parseImageMetadata(bytes)?.mimeType ?? "image/png";
1350
- xaiInlineImages.push({ data: entry.b64_json, mimeType });
1351
- } else if (entry.url) {
1352
- xaiInlineImages.push(await loadImageFromUrl(entry.url, fetchImpl, requestSignal));
1496
+ const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
1497
+ for (const image of resolvedImages) {
1498
+ parts.push({ inlineData: image });
1353
1499
  }
1354
- }
1500
+ parts.push({ text: assemblePrompt(params) });
1355
1501
 
1356
- if (xaiInlineImages.length === 0) {
1357
- return {
1358
- content: [{ type: "text", text: "No image data returned." }],
1359
- details: {
1360
- provider,
1361
- model: resolvedModel,
1362
- imageCount: 0,
1363
- imagePaths: [],
1364
- images: [],
1365
- },
1502
+ const generationConfig: {
1503
+ responseModalities: GeminiResponseModality[];
1504
+ imageConfig?: { aspectRatio?: string; imageSize?: string };
1505
+ } = {
1506
+ responseModalities: ["IMAGE"],
1366
1507
  };
1367
- }
1368
1508
 
1369
- const xaiImagePaths = await saveImagesToTemp(xaiInlineImages);
1370
-
1371
- return {
1372
- content: [
1373
- { type: "text", text: buildResponseSummary(provider, resolvedModel, xaiImagePaths, undefined) },
1374
- ],
1375
- details: {
1376
- provider,
1377
- model: resolvedModel,
1378
- imageCount: xaiInlineImages.length,
1379
- imagePaths: xaiImagePaths,
1380
- images: xaiInlineImages,
1381
- },
1382
- };
1383
- }
1509
+ if (params.aspect_ratio || params.image_size) {
1510
+ generationConfig.imageConfig = {
1511
+ aspectRatio: params.aspect_ratio,
1512
+ imageSize: params.image_size,
1513
+ };
1514
+ }
1384
1515
 
1385
- if (provider === "openrouter") {
1386
- const prompt = assemblePrompt(params);
1387
- const contentParts: OpenRouterContentPart[] = [{ type: "text", text: prompt }];
1388
- for (const image of resolvedImages) {
1389
- contentParts.push({ type: "image_url", image_url: { url: toDataUrl(image) } });
1390
- }
1516
+ const requestBody = {
1517
+ contents: [{ role: "user" as const, parts }],
1518
+ generationConfig,
1519
+ };
1391
1520
 
1392
- const requestBody = {
1393
- model: resolvedModel,
1394
- messages: [{ role: "user" as const, content: contentParts }],
1395
- };
1396
-
1397
- const rawText = await withAuth(
1398
- apiKey.apiKey,
1399
- async key => {
1400
- const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
1401
- method: "POST",
1402
- headers: {
1403
- "Content-Type": "application/json",
1404
- Authorization: `Bearer ${key}`,
1405
- "HTTP-Referer": "https://omp.sh/",
1406
- "X-OpenRouter-Title": "Oh-My-Pi",
1407
- "X-OpenRouter-Categories": "cli-agent",
1408
- },
1409
- body: JSON.stringify(requestBody),
1410
- signal: requestSignal,
1411
- });
1412
- const text = await resp.text();
1413
- if (!resp.ok) {
1414
- let message = text;
1415
- try {
1416
- const parsed = JSON.parse(text) as { error?: { message?: string } };
1417
- message = parsed.error?.message ?? message;
1418
- } catch {
1419
- // Keep raw text.
1420
- }
1421
- throw new ProviderHttpError(
1422
- `OpenRouter image request failed (${resp.status}): ${message}`,
1423
- resp.status,
1424
- { headers: resp.headers },
1521
+ const rawText = await withAuth(
1522
+ apiKey.apiKey,
1523
+ async key => {
1524
+ const resp = await fetchImpl(
1525
+ `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1526
+ {
1527
+ method: "POST",
1528
+ headers: {
1529
+ "Content-Type": "application/json",
1530
+ "x-goog-api-key": key,
1531
+ },
1532
+ body: JSON.stringify(requestBody),
1533
+ signal: requestSignal,
1534
+ },
1425
1535
  );
1426
- }
1427
- return text;
1428
- },
1429
- { signal: requestSignal },
1430
- );
1536
+ const text = await resp.text();
1537
+ if (!resp.ok) {
1538
+ let message = text;
1539
+ try {
1540
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1541
+ message = parsed.error?.message ?? message;
1542
+ } catch {
1543
+ // Keep raw text.
1544
+ }
1545
+ throw new ProviderHttpError(
1546
+ `Gemini image request failed (${resp.status}): ${message}`,
1547
+ resp.status,
1548
+ {
1549
+ headers: resp.headers,
1550
+ },
1551
+ );
1552
+ }
1553
+ return text;
1554
+ },
1555
+ { signal: requestSignal },
1556
+ );
1431
1557
 
1432
- const data = JSON.parse(rawText) as OpenRouterResponse;
1433
- const message = data.choices?.[0]?.message;
1434
- const responseText = collectOpenRouterResponseText(message);
1435
- const imageUrls = extractOpenRouterImageUrls(message);
1436
- const inlineImages: InlineImageData[] = [];
1437
- for (const imageUrl of imageUrls) {
1438
- inlineImages.push(await loadImageFromUrl(imageUrl, fetchImpl, requestSignal));
1439
- }
1558
+ const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1559
+ const responseParts = combineParts(data);
1560
+ const responseText = collectResponseText(responseParts);
1561
+ const inlineImages = collectInlineImages(responseParts);
1562
+
1563
+ if (inlineImages.length === 0) {
1564
+ const blocked = data.promptFeedback?.blockReason
1565
+ ? `Blocked: ${data.promptFeedback.blockReason}`
1566
+ : "No image data returned.";
1567
+ return {
1568
+ content: [{ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` }],
1569
+ details: {
1570
+ provider,
1571
+ model,
1572
+ imageCount: 0,
1573
+ imagePaths: [],
1574
+ images: [],
1575
+ responseText,
1576
+ promptFeedback: data.promptFeedback,
1577
+ usage: data.usageMetadata,
1578
+ },
1579
+ };
1580
+ }
1581
+
1582
+ const imagePaths = await saveImagesToTemp(inlineImages);
1440
1583
 
1441
- if (inlineImages.length === 0) {
1442
- const messageText = responseText ? `\n\n${responseText}` : "";
1443
1584
  return {
1444
- content: [{ type: "text", text: `No image data returned.${messageText}` }],
1585
+ content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1445
1586
  details: {
1446
1587
  provider,
1447
- model: resolvedModel,
1448
- imageCount: 0,
1449
- imagePaths: [],
1450
- images: [],
1588
+ model,
1589
+ imageCount: inlineImages.length,
1590
+ imagePaths,
1591
+ images: inlineImages,
1451
1592
  responseText,
1593
+ promptFeedback: data.promptFeedback,
1594
+ usage: data.usageMetadata,
1452
1595
  },
1453
1596
  };
1597
+ } catch (error) {
1598
+ if (!(error instanceof ProviderHttpError) || requestSignal?.aborted) {
1599
+ throw error;
1600
+ }
1601
+ failures.push({ provider, error });
1454
1602
  }
1455
-
1456
- const imagePaths = await saveImagesToTemp(inlineImages);
1457
-
1458
- return {
1459
- content: [
1460
- { type: "text", text: buildResponseSummary(provider, resolvedModel, imagePaths, responseText) },
1461
- ],
1462
- details: {
1463
- provider,
1464
- model: resolvedModel,
1465
- imageCount: inlineImages.length,
1466
- imagePaths,
1467
- images: inlineImages,
1468
- responseText,
1469
- },
1470
- };
1471
1603
  }
1472
1604
 
1473
- const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
1474
- for (const image of resolvedImages) {
1475
- parts.push({ inlineData: image });
1605
+ if (!foundCredentials) {
1606
+ throw new Error(
1607
+ "No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity or xAI Grok OAuth, or set XAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY.",
1608
+ );
1476
1609
  }
1477
- parts.push({ text: assemblePrompt(params) });
1478
-
1479
- const generationConfig: {
1480
- responseModalities: GeminiResponseModality[];
1481
- imageConfig?: { aspectRatio?: string; imageSize?: string };
1482
- } = {
1483
- responseModalities: ["IMAGE"],
1484
- };
1485
1610
 
1486
- if (params.aspect_ratio || params.image_size) {
1487
- generationConfig.imageConfig = {
1488
- aspectRatio: params.aspect_ratio,
1489
- imageSize: params.image_size,
1490
- };
1611
+ if (failures.length === 0 && unsupportedAspectRatioProvider) {
1612
+ assertImageAspectRatioSupported(unsupportedAspectRatioProvider, params.aspect_ratio);
1491
1613
  }
1492
1614
 
1493
- const requestBody = {
1494
- contents: [{ role: "user" as const, parts }],
1495
- generationConfig,
1496
- };
1497
-
1498
- const rawText = await withAuth(
1499
- apiKey.apiKey,
1500
- async key => {
1501
- const resp = await fetchImpl(
1502
- `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1503
- {
1504
- method: "POST",
1505
- headers: {
1506
- "Content-Type": "application/json",
1507
- "x-goog-api-key": key,
1508
- },
1509
- body: JSON.stringify(requestBody),
1510
- signal: requestSignal,
1511
- },
1512
- );
1513
- const text = await resp.text();
1514
- if (!resp.ok) {
1515
- let message = text;
1516
- try {
1517
- const parsed = JSON.parse(text) as { error?: { message?: string } };
1518
- message = parsed.error?.message ?? message;
1519
- } catch {
1520
- // Keep raw text.
1521
- }
1522
- throw new ProviderHttpError(`Gemini image request failed (${resp.status}): ${message}`, resp.status, {
1523
- headers: resp.headers,
1524
- });
1525
- }
1526
- return text;
1527
- },
1528
- { signal: requestSignal },
1615
+ throw new AggregateError(
1616
+ failures.map(failure => failure.error),
1617
+ `Image generation failed for all credentialed providers: ${failures.map(failure => failure.provider).join(", ")}`,
1529
1618
  );
1530
-
1531
- const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1532
- const responseParts = combineParts(data);
1533
- const responseText = collectResponseText(responseParts);
1534
- const inlineImages = collectInlineImages(responseParts);
1535
-
1536
- if (inlineImages.length === 0) {
1537
- const blocked = data.promptFeedback?.blockReason
1538
- ? `Blocked: ${data.promptFeedback.blockReason}`
1539
- : "No image data returned.";
1540
- return {
1541
- content: [{ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` }],
1542
- details: {
1543
- provider,
1544
- model,
1545
- imageCount: 0,
1546
- imagePaths: [],
1547
- images: [],
1548
- responseText,
1549
- promptFeedback: data.promptFeedback,
1550
- usage: data.usageMetadata,
1551
- },
1552
- };
1553
- }
1554
-
1555
- const imagePaths = await saveImagesToTemp(inlineImages);
1556
-
1557
- return {
1558
- content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1559
- details: {
1560
- provider,
1561
- model,
1562
- imageCount: inlineImages.length,
1563
- imagePaths,
1564
- images: inlineImages,
1565
- responseText,
1566
- promptFeedback: data.promptFeedback,
1567
- usage: data.usageMetadata,
1568
- },
1569
- };
1570
1619
  });
1571
1620
  },
1572
1621
  };