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

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 (202) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/cli.js +4006 -3966
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/advisor/config.d.ts +14 -6
  5. package/dist/types/advisor/runtime.d.ts +20 -11
  6. package/dist/types/autolearn/controller.d.ts +1 -0
  7. package/dist/types/cli-commands.d.ts +14 -1
  8. package/dist/types/config/model-discovery.d.ts +17 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/model-resolver.d.ts +6 -2
  11. package/dist/types/config/models-config-schema.d.ts +10 -0
  12. package/dist/types/config/models-config.d.ts +6 -0
  13. package/dist/types/config/settings-schema.d.ts +32 -7
  14. package/dist/types/config/settings.d.ts +50 -0
  15. package/dist/types/cursor.d.ts +11 -0
  16. package/dist/types/discovery/helpers.d.ts +5 -2
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  20. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  23. package/dist/types/extensibility/shared-events.d.ts +6 -0
  24. package/dist/types/extensibility/utils.d.ts +2 -2
  25. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  26. package/dist/types/lib/xai-http.d.ts +11 -0
  27. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  28. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  29. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  30. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  31. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  32. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  34. package/dist/types/modes/components/hook-editor.d.ts +16 -2
  35. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  36. package/dist/types/modes/components/model-hub.d.ts +5 -2
  37. package/dist/types/modes/components/session-selector.d.ts +4 -0
  38. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  39. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  42. package/dist/types/modes/interactive-mode.d.ts +6 -0
  43. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  44. package/dist/types/modes/print-mode.d.ts +1 -1
  45. package/dist/types/modes/types.d.ts +10 -0
  46. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  47. package/dist/types/modes/warp-events.d.ts +24 -0
  48. package/dist/types/modes/warp-events.test.d.ts +1 -0
  49. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  50. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  51. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  52. package/dist/types/sdk.d.ts +13 -2
  53. package/dist/types/session/agent-session.d.ts +43 -11
  54. package/dist/types/session/session-history-format.d.ts +10 -0
  55. package/dist/types/session/session-manager.d.ts +14 -0
  56. package/dist/types/session/streaming-output.d.ts +2 -0
  57. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  58. package/dist/types/slash-commands/types.d.ts +4 -4
  59. package/dist/types/system-prompt.d.ts +1 -1
  60. package/dist/types/task/label.d.ts +1 -1
  61. package/dist/types/telemetry-export.d.ts +34 -9
  62. package/dist/types/tools/approval.d.ts +8 -0
  63. package/dist/types/tools/bash.d.ts +2 -0
  64. package/dist/types/tools/browser/registry.d.ts +7 -3
  65. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  66. package/dist/types/tools/essential-tools.d.ts +29 -0
  67. package/dist/types/tools/image-gen.d.ts +2 -1
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/manage-skill.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +11 -2
  71. package/dist/types/utils/title-generator.d.ts +2 -1
  72. package/dist/types/web/search/index.d.ts +2 -0
  73. package/dist/types/web/search/providers/base.d.ts +3 -0
  74. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  75. package/dist/types/web/search/types.d.ts +1 -1
  76. package/package.json +21 -16
  77. package/src/advisor/__tests__/advisor.test.ts +1358 -86
  78. package/src/advisor/__tests__/config.test.ts +58 -2
  79. package/src/advisor/advise-tool.ts +7 -3
  80. package/src/advisor/config.ts +76 -24
  81. package/src/advisor/runtime.ts +445 -92
  82. package/src/autolearn/controller.ts +23 -28
  83. package/src/cli/bench-cli.ts +4 -1
  84. package/src/cli/grep-cli.ts +2 -1
  85. package/src/cli-commands.ts +57 -23
  86. package/src/cli.ts +5 -1
  87. package/src/config/model-discovery.ts +81 -21
  88. package/src/config/model-registry.ts +34 -6
  89. package/src/config/model-resolver.ts +57 -12
  90. package/src/config/models-config-schema.ts +1 -0
  91. package/src/config/settings-schema.ts +43 -6
  92. package/src/config/settings.ts +405 -25
  93. package/src/cursor.ts +20 -3
  94. package/src/debug/report-bundle.ts +40 -4
  95. package/src/discovery/codex.ts +14 -4
  96. package/src/discovery/helpers.ts +28 -5
  97. package/src/discovery/substitute-plugin-root.ts +23 -7
  98. package/src/exec/bash-executor.ts +14 -5
  99. package/src/export/html/template.js +2 -0
  100. package/src/extensibility/custom-tools/loader.ts +3 -3
  101. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  102. package/src/extensibility/extensions/loader.ts +3 -3
  103. package/src/extensibility/extensions/managed-timers.ts +83 -0
  104. package/src/extensibility/extensions/runner.ts +26 -0
  105. package/src/extensibility/extensions/types.ts +18 -0
  106. package/src/extensibility/extensions/wrapper.ts +2 -1
  107. package/src/extensibility/hooks/loader.ts +3 -3
  108. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  109. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  110. package/src/extensibility/plugins/manager.ts +2 -2
  111. package/src/extensibility/shared-events.ts +6 -0
  112. package/src/extensibility/utils.ts +91 -25
  113. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  114. package/src/internal-urls/agent-protocol.ts +73 -39
  115. package/src/irc/bus.ts +22 -3
  116. package/src/launch/broker.ts +3 -2
  117. package/src/launch/client.ts +2 -2
  118. package/src/launch/presence.ts +2 -2
  119. package/src/lib/xai-http.ts +28 -2
  120. package/src/lsp/client.ts +1 -1
  121. package/src/main.ts +11 -8
  122. package/src/mcp/manager.ts +9 -3
  123. package/src/mcp/transports/stdio.ts +103 -23
  124. package/src/modes/acp/acp-agent.ts +28 -9
  125. package/src/modes/acp/acp-event-mapper.ts +8 -0
  126. package/src/modes/acp/acp-mode.ts +18 -4
  127. package/src/modes/components/advisor-config.ts +65 -3
  128. package/src/modes/components/ask-dialog.ts +1 -1
  129. package/src/modes/components/assistant-message.ts +35 -14
  130. package/src/modes/components/chat-transcript-builder.ts +2 -0
  131. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  132. package/src/modes/components/custom-editor.ts +62 -1
  133. package/src/modes/components/hook-editor.ts +35 -5
  134. package/src/modes/components/login-dialog.test.ts +56 -0
  135. package/src/modes/components/login-dialog.ts +7 -3
  136. package/src/modes/components/model-hub.ts +138 -42
  137. package/src/modes/components/plan-review-overlay.ts +51 -10
  138. package/src/modes/components/session-selector.ts +10 -0
  139. package/src/modes/components/status-line/component.test.ts +1 -0
  140. package/src/modes/components/status-line/component.ts +2 -4
  141. package/src/modes/components/status-line/segments.ts +21 -6
  142. package/src/modes/components/tool-execution.test.ts +101 -0
  143. package/src/modes/components/tool-execution.ts +117 -9
  144. package/src/modes/components/transcript-container.ts +30 -2
  145. package/src/modes/controllers/command-controller-shared.ts +1 -1
  146. package/src/modes/controllers/command-controller.ts +168 -48
  147. package/src/modes/controllers/event-controller.ts +6 -0
  148. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  149. package/src/modes/controllers/input-controller.ts +12 -12
  150. package/src/modes/controllers/selector-controller.ts +208 -46
  151. package/src/modes/interactive-mode.ts +205 -65
  152. package/src/modes/noninteractive-dispose.test.ts +60 -0
  153. package/src/modes/print-mode.ts +10 -5
  154. package/src/modes/rpc/host-tools.ts +2 -1
  155. package/src/modes/rpc/rpc-mode.ts +31 -7
  156. package/src/modes/theme/theme.ts +2 -2
  157. package/src/modes/types.ts +10 -0
  158. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  159. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  160. package/src/modes/warp-events.test.ts +794 -0
  161. package/src/modes/warp-events.ts +232 -0
  162. package/src/plan-mode/model-transition.test.ts +60 -0
  163. package/src/plan-mode/model-transition.ts +51 -0
  164. package/src/prompts/system/system-prompt.md +1 -1
  165. package/src/registry/agent-lifecycle.ts +133 -18
  166. package/src/sdk.ts +227 -44
  167. package/src/session/agent-session.ts +1364 -382
  168. package/src/session/session-history-format.ts +20 -5
  169. package/src/session/session-listing.ts +6 -5
  170. package/src/session/session-loader.ts +9 -0
  171. package/src/session/session-manager.ts +48 -0
  172. package/src/session/session-persistence.ts +11 -0
  173. package/src/session/streaming-output.ts +3 -1
  174. package/src/slash-commands/builtin-registry.ts +9 -0
  175. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  176. package/src/slash-commands/types.ts +4 -4
  177. package/src/system-prompt.ts +2 -2
  178. package/src/task/executor.ts +1 -1
  179. package/src/task/label.ts +2 -0
  180. package/src/telemetry-export.ts +453 -97
  181. package/src/tools/approval.ts +11 -0
  182. package/src/tools/bash.ts +71 -38
  183. package/src/tools/browser/registry.ts +9 -2
  184. package/src/tools/browser/tab-supervisor.ts +65 -10
  185. package/src/tools/browser.ts +4 -3
  186. package/src/tools/essential-tools.ts +45 -0
  187. package/src/tools/eval.ts +3 -0
  188. package/src/tools/gh.ts +169 -2
  189. package/src/tools/image-gen.ts +624 -513
  190. package/src/tools/index.ts +10 -6
  191. package/src/tools/manage-skill.ts +5 -3
  192. package/src/tools/output-meta.ts +7 -0
  193. package/src/tools/path-utils.ts +8 -5
  194. package/src/tools/read.ts +48 -3
  195. package/src/tools/write.ts +22 -4
  196. package/src/tools/xdev.ts +14 -3
  197. package/src/utils/title-generator.ts +15 -4
  198. package/src/web/search/index.ts +21 -6
  199. package/src/web/search/providers/base.ts +3 -0
  200. package/src/web/search/providers/kimi.ts +18 -12
  201. package/src/web/search/providers/xai.ts +40 -9
  202. package/src/web/search/types.ts +6 -1
@@ -45,7 +45,7 @@ const IMAGE_SYSTEM_INSTRUCTION =
45
45
  "You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
46
46
 
47
47
  export type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter" | "xai";
48
- export type ImageProviderPreference = Exclude<ImageProvider, "openai-codex"> | "auto";
48
+ export type ImageProviderPreference = ImageProvider | "auto";
49
49
 
50
50
  interface ImageApiKey {
51
51
  provider: ImageProvider;
@@ -57,7 +57,17 @@ interface ImageApiKey {
57
57
  const COMMON_IMAGE_ASPECT_RATIOS = ["1:1", "3:4", "4:3", "9:16", "16:9"] as const;
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
- const IMAGE_PROVIDER_PREFERENCES = new Set<string>(["auto", "antigravity", "gemini", "openai", "openrouter", "xai"]);
60
+ const IMAGE_PROVIDER_CHOICES = [
61
+ "auto",
62
+ "antigravity",
63
+ "gemini",
64
+ "openai",
65
+ "openai-codex",
66
+ "openrouter",
67
+ "xai",
68
+ ] as const;
69
+ const IMAGE_PROVIDER_PREFERENCES = new Set<string>(IMAGE_PROVIDER_CHOICES);
70
+ const AUTO_IMAGE_PROVIDER_ORDER = ["openai", "openai-codex", "antigravity", "xai", "openrouter", "gemini"] as const;
61
71
 
62
72
  const responseModalitySchema = type('"IMAGE" | "TEXT"');
63
73
 
@@ -70,6 +80,10 @@ const inputImageSchema = type({
70
80
  "mime_type?": type("string").describe("mime type"),
71
81
  });
72
82
 
83
+ const imageProviderSchema = type
84
+ .enumerated(...IMAGE_PROVIDER_CHOICES)
85
+ .describe("image provider for this request; overrides the providers.image setting (default: use the setting)");
86
+
73
87
  export const imageGenSchema = type({
74
88
  subject: type("string").describe("main subject"),
75
89
  "action?": type("string").describe("what subject is doing"),
@@ -82,6 +96,7 @@ export const imageGenSchema = type({
82
96
  "aspect_ratio?": aspectRatioSchema,
83
97
  "image_size?": imageSizeSchema,
84
98
  "input?": inputImageSchema.array().describe("input images"),
99
+ "provider?": imageProviderSchema,
85
100
  });
86
101
  export type ImageGenParams = typeof imageGenSchema.infer;
87
102
  export type GeminiResponseModality = typeof responseModalitySchema.infer;
@@ -547,53 +562,106 @@ async function findOpenAIHostedImageCredentials(
547
562
  };
548
563
  }
549
564
 
550
- async function findImageApiKey(
551
- modelRegistry?: ModelRegistry,
552
- activeModel?: Model,
565
+ // Codex (ChatGPT subscription) chat models that carry OpenAI's hosted
566
+ // `image_generation` tool. Priority: newest general model first, then Codex
567
+ // variants; any available openai-codex hosted-image model is the last resort.
568
+ const CODEX_IMAGE_MODEL_PRIORITY = ["gpt-5.5", "gpt-5.4", "gpt-5.1", "gpt-5", "gpt-5-codex"] as const;
569
+
570
+ function resolveDefaultCodexImageModel(modelRegistry: ModelRegistry): Model | undefined {
571
+ for (const id of CODEX_IMAGE_MODEL_PRIORITY) {
572
+ const model = modelRegistry.find("openai-codex", id);
573
+ if (model && isOpenAIHostedImageModel(model)) return model;
574
+ }
575
+ return modelRegistry.getAll().find(model => model.provider === "openai-codex" && isOpenAIHostedImageModel(model));
576
+ }
577
+
578
+ /**
579
+ * Codex subscription (ChatGPT OAuth) image credentials — engages OpenAI's hosted
580
+ * `image_generation` tool through a CONNECTED Codex account, independent of the
581
+ * active chat model. This is what lets image generation run on a ChatGPT
582
+ * subscription (no metered OPENAI_API_KEY) even when the active model is, e.g.,
583
+ * Claude. The active-model-is-codex case is already served by
584
+ * {@link findOpenAIHostedImageCredentials}, so it is skipped here to avoid a
585
+ * duplicate resolution.
586
+ */
587
+ async function findCodexSubscriptionImageCredentials(
588
+ modelRegistry: ModelRegistry | undefined,
589
+ activeModel: Model | undefined,
553
590
  sessionId?: string,
554
591
  ): 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.
592
+ if (!modelRegistry) return null;
593
+ if (isOpenAIHostedImageModel(activeModel) && getOpenAIHostedImageProvider(activeModel) === "openai-codex") {
594
+ return null;
576
595
  }
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;
596
+ // A Codex subscription credential is an OAuth JWT with an account claim. API
597
+ // keys stored under this provider cannot use the ChatGPT backend and must not
598
+ // prevent fallback providers from being selected.
599
+ const token = await modelRegistry.getApiKeyForProvider("openai-codex", sessionId);
600
+ if (!token || !getCodexAccountId(token)) return null;
601
+ const model = resolveDefaultCodexImageModel(modelRegistry);
602
+ if (!model) return null;
603
+ const apiKey = await modelRegistry.getApiKey(model, sessionId);
604
+ if (!isAuthenticated(apiKey) || !getCodexAccountId(apiKey)) return null;
605
+ return { provider: "openai-codex", apiKey, model };
606
+ }
607
+
608
+ function activeImageProvider(model: Model | undefined): Exclude<ImageProviderPreference, "auto"> | null {
609
+ switch (model?.provider) {
610
+ case "openai":
611
+ case "openai-codex":
612
+ return "openai";
613
+ case "google-antigravity":
614
+ return "antigravity";
615
+ case "xai":
616
+ case "xai-oauth":
617
+ return "xai";
618
+ case "openrouter":
619
+ return "openrouter";
620
+ case "google":
621
+ return "gemini";
622
+ default:
623
+ return null;
585
624
  }
625
+ }
586
626
 
587
- const xai = await findXAIImageCredentials(modelRegistry);
588
- if (xai) return xai;
589
-
590
- const openRouter = await findOpenRouterImageCredentials(modelRegistry, sessionId);
591
- if (openRouter) return openRouter;
627
+ function imageProviderOrder(
628
+ activeModel: Model | undefined,
629
+ preference: ImageProviderPreference = preferredImageProvider,
630
+ ): Array<Exclude<ImageProviderPreference, "auto">> {
631
+ const providers: Array<Exclude<ImageProviderPreference, "auto">> = [];
632
+ const added = new Set<Exclude<ImageProviderPreference, "auto">>();
633
+ const add = (provider: Exclude<ImageProviderPreference, "auto"> | null): void => {
634
+ if (!provider || added.has(provider)) return;
635
+ added.add(provider);
636
+ providers.push(provider);
637
+ };
592
638
 
593
- const gemini = await findGeminiImageCredentials(modelRegistry, sessionId);
594
- if (gemini) return gemini;
639
+ if (preference !== "auto") add(preference);
640
+ add(activeImageProvider(activeModel));
641
+ for (const provider of AUTO_IMAGE_PROVIDER_ORDER) add(provider);
642
+ return providers;
643
+ }
595
644
 
596
- return null;
645
+ async function findImageApiKey(
646
+ provider: Exclude<ImageProviderPreference, "auto">,
647
+ modelRegistry?: ModelRegistry,
648
+ activeModel?: Model,
649
+ sessionId?: string,
650
+ ): Promise<ImageApiKey | null> {
651
+ switch (provider) {
652
+ case "openai":
653
+ return findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
654
+ case "openai-codex":
655
+ return findCodexSubscriptionImageCredentials(modelRegistry, activeModel, sessionId);
656
+ case "antigravity":
657
+ return modelRegistry ? findAntigravityCredentials(modelRegistry, sessionId) : null;
658
+ case "xai":
659
+ return findXAIImageCredentials(modelRegistry);
660
+ case "openrouter":
661
+ return findOpenRouterImageCredentials(modelRegistry, sessionId);
662
+ case "gemini":
663
+ return findGeminiImageCredentials(modelRegistry, sessionId);
664
+ }
597
665
  }
598
666
 
599
667
  async function loadImageFromPath(imagePath: string, cwd: string): Promise<InlineImageData> {
@@ -900,9 +968,10 @@ async function generateOpenAIHostedImage(
900
968
 
901
969
  if (!response.ok) {
902
970
  const errorText = await response.text();
903
- throw Object.assign(
904
- new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`),
905
- { status: response.status },
971
+ throw new ProviderHttpError(
972
+ `OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`,
973
+ response.status,
974
+ { headers: response.headers },
906
975
  );
907
976
  }
908
977
 
@@ -1040,533 +1109,575 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1040
1109
  async execute(_toolCallId, params, _onUpdate, ctx, signal) {
1041
1110
  return untilAborted(signal, async () => {
1042
1111
  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);
1112
+ const providerOrder = imageProviderOrder(ctx.model, params.provider ?? preferredImageProvider);
1063
1113
  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
1114
  const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
1073
1115
  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");
1116
+ const failures: Array<{ provider: ImageProvider; error: ProviderHttpError }> = [];
1117
+ let unsupportedAspectRatioProvider: ImageProvider | undefined;
1118
+ let foundCredentials = false;
1119
+ let resolvedImageCache: InlineImageData[] | undefined;
1120
+
1121
+ for (const preferredProvider of providerOrder) {
1122
+ const apiKey = await findImageApiKey(preferredProvider, ctx.modelRegistry, ctx.model, sessionId);
1123
+ if (!apiKey) continue;
1124
+ foundCredentials = true;
1125
+ if (!resolvedImageCache) {
1126
+ resolvedImageCache = [];
1127
+ if (params.input?.length) {
1128
+ for (const input of params.input) {
1129
+ resolvedImageCache.push(await resolveInputImage(input, cwd));
1130
+ }
1131
+ }
1078
1132
  }
1133
+ const resolvedImages = resolvedImageCache;
1134
+
1135
+ const provider = apiKey.provider;
1136
+ try {
1137
+ const model =
1138
+ provider === "openai" || provider === "openai-codex"
1139
+ ? (apiKey.model?.id ?? "gpt")
1140
+ : provider === "antigravity"
1141
+ ? DEFAULT_ANTIGRAVITY_MODEL
1142
+ : provider === "openrouter"
1143
+ ? DEFAULT_OPENROUTER_MODEL
1144
+ : provider === "xai"
1145
+ ? DEFAULT_XAI_IMAGE_MODEL
1146
+ : DEFAULT_MODEL;
1147
+ const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
1148
+ if (
1149
+ params.aspect_ratio &&
1150
+ provider !== "xai" &&
1151
+ !COMMON_IMAGE_ASPECT_RATIO_SET.has(params.aspect_ratio)
1152
+ ) {
1153
+ unsupportedAspectRatioProvider ??= provider;
1154
+ continue;
1155
+ }
1156
+ if (provider === "openai" || provider === "openai-codex") {
1157
+ if (!apiKey.model) {
1158
+ throw new Error("Missing active GPT model for OpenAI image generation");
1159
+ }
1160
+
1161
+ const hostedModel = apiKey.model;
1162
+ const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel, sessionId);
1163
+
1164
+ const parsed = await withAuth(
1165
+ hostedKey,
1166
+ key =>
1167
+ generateOpenAIHostedImage(
1168
+ key,
1169
+ hostedModel,
1170
+ params,
1171
+ resolvedImages,
1172
+ fetchImpl,
1173
+ requestSignal,
1174
+ sessionId,
1175
+ ),
1176
+ { signal: requestSignal },
1177
+ );
1178
+
1179
+ if (parsed.images.length === 0) {
1180
+ const messageText = parsed.responseText ? `\n\n${parsed.responseText}` : "";
1181
+ return {
1182
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1183
+ details: {
1184
+ provider,
1185
+ model,
1186
+ imageCount: 0,
1187
+ imagePaths: [],
1188
+ images: [],
1189
+ responseText: parsed.responseText,
1190
+ revisedPrompt: parsed.revisedPrompt,
1191
+ usage: parsed.usage,
1192
+ },
1193
+ };
1194
+ }
1195
+
1196
+ const imagePaths = await saveImagesToTemp(parsed.images);
1197
+
1198
+ return {
1199
+ content: [
1200
+ { type: "text", text: buildResponseSummary(provider, model, imagePaths, parsed.responseText) },
1201
+ ],
1202
+ details: {
1203
+ provider,
1204
+ model,
1205
+ imageCount: parsed.images.length,
1206
+ imagePaths,
1207
+ images: parsed.images,
1208
+ responseText: parsed.responseText,
1209
+ revisedPrompt: parsed.revisedPrompt,
1210
+ usage: parsed.usage,
1211
+ },
1212
+ };
1213
+ }
1214
+
1215
+ if (provider === "antigravity") {
1216
+ if (!apiKey.projectId) {
1217
+ throw new Error("Missing projectId in antigravity credentials");
1218
+ }
1079
1219
 
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,
1220
+ const prompt = assemblePrompt(params);
1221
+ const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
1093
1222
  sessionId,
1094
- ),
1095
- { signal: requestSignal },
1096
- );
1223
+ modelId: DEFAULT_ANTIGRAVITY_MODEL,
1224
+ });
1097
1225
 
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
- }
1226
+ const response = await withAuth(
1227
+ antigravityKey,
1228
+ async key => {
1229
+ // On a retry the resolver yields the raw stored credential JSON
1230
+ // ({ token, projectId }); the initial seed is the already-parsed
1231
+ // access token. Tolerate both, falling back to the seed projectId.
1232
+ const rotated = parseAntigravityCredentials(key);
1233
+ const bearer = rotated?.accessToken ?? key;
1234
+ const projectId = rotated?.projectId ?? apiKey.projectId!;
1235
+ const requestBody = buildAntigravityRequest(
1236
+ prompt,
1237
+ model,
1238
+ projectId,
1239
+ params.aspect_ratio,
1240
+ params.image_size,
1241
+ resolvedImages,
1242
+ );
1114
1243
 
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
- }
1244
+ let endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_PROD, DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX];
1245
+ try {
1246
+ const mode = settings.get("providers.antigravityEndpoint");
1247
+ if (mode === "production") {
1248
+ endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_PROD];
1249
+ } else if (mode === "sandbox") {
1250
+ endpoints = [DEFAULT_ANTIGRAVITY_ENDPOINT_SANDBOX];
1251
+ }
1252
+ } catch {
1253
+ // Ignored
1254
+ }
1133
1255
 
1134
- if (provider === "antigravity") {
1135
- if (!apiKey.projectId) {
1136
- throw new Error("Missing projectId in antigravity credentials");
1137
- }
1256
+ let resp: Response | undefined;
1257
+ let lastError: Error | undefined;
1258
+
1259
+ for (let i = 0; i < endpoints.length; i++) {
1260
+ const endpoint = endpoints[i];
1261
+ const isLastEndpoint = i === endpoints.length - 1;
1262
+ try {
1263
+ resp = await fetchImpl(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
1264
+ method: "POST",
1265
+ headers: {
1266
+ Authorization: `Bearer ${bearer}`,
1267
+ "Content-Type": "application/json",
1268
+ Accept: "text/event-stream",
1269
+ "User-Agent": getAntigravityUserAgent(),
1270
+ },
1271
+ body: JSON.stringify(requestBody),
1272
+ signal: requestSignal,
1273
+ });
1274
+
1275
+ if (resp.ok) {
1276
+ break;
1277
+ }
1278
+
1279
+ const errorText = await resp.text();
1280
+ let message = errorText;
1281
+ try {
1282
+ const parsedErr = JSON.parse(errorText) as { error?: { message?: string } };
1283
+ message = parsedErr.error?.message ?? message;
1284
+ } catch {
1285
+ // Keep raw text.
1286
+ }
1287
+
1288
+ lastError = new ProviderHttpError(
1289
+ `Antigravity image request failed (${resp.status}): ${message}`,
1290
+ resp.status,
1291
+ { headers: resp.headers },
1292
+ );
1293
+
1294
+ if (resp.status === 429 || (resp.status >= 500 && resp.status < 600)) {
1295
+ if (!isLastEndpoint) {
1296
+ continue;
1297
+ }
1298
+ }
1299
+ break;
1300
+ } catch (error) {
1301
+ lastError = error as Error;
1302
+ if (isLastEndpoint) {
1303
+ break;
1304
+ }
1305
+ }
1306
+ }
1138
1307
 
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,
1308
+ if (!resp?.ok) {
1309
+ throw lastError ?? new Error("Antigravity image generation failed");
1310
+ }
1311
+
1312
+ return resp;
1313
+ },
1314
+ { signal: requestSignal },
1161
1315
  );
1162
1316
 
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
1317
+ const parsed = await parseAntigravitySseForImage(response, requestSignal);
1318
+ const responseText = parsed.text.length > 0 ? parsed.text.join(" ") : undefined;
1319
+
1320
+ if (parsed.images.length === 0) {
1321
+ const messageText = responseText ? `\n\n${responseText}` : "";
1322
+ return {
1323
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1324
+ details: {
1325
+ provider,
1326
+ model,
1327
+ imageCount: 0,
1328
+ imagePaths: [],
1329
+ images: [],
1330
+ responseText,
1331
+ usage: parsed.usage,
1332
+ },
1333
+ };
1334
+ }
1335
+
1336
+ const imagePaths = await saveImagesToTemp(parsed.images);
1337
+
1338
+ return {
1339
+ content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1340
+ details: {
1341
+ provider,
1342
+ model,
1343
+ imageCount: parsed.images.length,
1344
+ imagePaths,
1345
+ images: parsed.images,
1346
+ responseText,
1347
+ usage: parsed.usage,
1348
+ },
1349
+ };
1350
+ }
1351
+
1352
+ if (provider === "xai") {
1353
+ if (!ctx.modelRegistry) {
1354
+ throw new Error("Missing modelRegistry for xAI image generation");
1355
+ }
1356
+ const xaiCreds = await resolveXAIHttpCredentials(ctx.modelRegistry, resolvedModel);
1357
+ if (!xaiCreds) {
1358
+ throw new Error(
1359
+ "No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok or X Premium+) or set XAI_API_KEY.",
1360
+ );
1173
1361
  }
1174
1362
 
1175
- let resp: Response | undefined;
1176
- let lastError: Error | undefined;
1363
+ const prompt = assemblePrompt(params);
1364
+ const aspectRatio = params.aspect_ratio ?? "1:1";
1365
+ const xaiResolution = resolveXAIResolution(params.image_size);
1177
1366
 
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`, {
1367
+ const isEdit = resolvedImages.length > 0;
1368
+ if (isEdit && resolvedImages.length > XAI_MAX_EDIT_IMAGES) {
1369
+ throw new Error(
1370
+ `xAI image edits accept up to ${XAI_MAX_EDIT_IMAGES} reference images; got ${resolvedImages.length}.`,
1371
+ );
1372
+ }
1373
+
1374
+ const xaiBaseBody: XAIImageRequestBase = {
1375
+ model: resolvedModel,
1376
+ prompt,
1377
+ aspect_ratio: aspectRatio,
1378
+ resolution: xaiResolution,
1379
+ n: 1,
1380
+ response_format: "b64_json",
1381
+ };
1382
+ const xaiBody: XAIImageRequestBody = isEdit
1383
+ ? buildXAIEditPayload(xaiBaseBody, resolvedImages)
1384
+ : xaiBaseBody;
1385
+ const xaiEndpoint = isEdit ? "/images/edits" : "/images/generations";
1386
+
1387
+ const xaiKey: ApiKey = ctx.modelRegistry.resolver(xaiCreds.provider, {
1388
+ sessionId,
1389
+ baseUrl: xaiCreds.baseURL,
1390
+ });
1391
+
1392
+ const xaiRawText = await withAuth(
1393
+ xaiKey,
1394
+ async key => {
1395
+ const resp = await fetchImpl(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1183
1396
  method: "POST",
1184
1397
  headers: {
1185
- Authorization: `Bearer ${bearer}`,
1398
+ Authorization: `Bearer ${key}`,
1186
1399
  "Content-Type": "application/json",
1187
- Accept: "text/event-stream",
1188
- "User-Agent": getAntigravityUserAgent(),
1400
+ "User-Agent": ohMyPiXAIUserAgent(),
1189
1401
  },
1190
- body: JSON.stringify(requestBody),
1402
+ body: JSON.stringify(xaiBody),
1191
1403
  signal: requestSignal,
1192
1404
  });
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;
1405
+ const rawText = await resp.text();
1406
+ if (!resp.ok) {
1407
+ let message = rawText;
1408
+ try {
1409
+ const parsedErr = JSON.parse(rawText) as { error?: { message?: string } };
1410
+ message = parsedErr.error?.message ?? message;
1411
+ } catch {
1412
+ // Keep raw text.
1216
1413
  }
1414
+ throw new ProviderHttpError(
1415
+ `xAI image request failed (${resp.status}): ${message}`,
1416
+ resp.status,
1417
+ {
1418
+ headers: resp.headers,
1419
+ },
1420
+ );
1217
1421
  }
1218
- break;
1219
- } catch (error) {
1220
- lastError = error as Error;
1221
- if (isLastEndpoint) {
1222
- break;
1223
- }
1422
+ return rawText;
1423
+ },
1424
+ { signal: requestSignal },
1425
+ );
1426
+
1427
+ const xaiData = JSON.parse(xaiRawText) as {
1428
+ data?: Array<{ b64_json?: string; url?: string }>;
1429
+ };
1430
+ const xaiInlineImages: InlineImageData[] = [];
1431
+ for (const entry of xaiData.data ?? []) {
1432
+ if (entry.b64_json) {
1433
+ const bytes = Buffer.from(entry.b64_json, "base64");
1434
+ const mimeType = parseImageMetadata(bytes)?.mimeType ?? "image/png";
1435
+ xaiInlineImages.push({ data: entry.b64_json, mimeType });
1436
+ } else if (entry.url) {
1437
+ xaiInlineImages.push(await loadImageFromUrl(entry.url, fetchImpl, requestSignal));
1224
1438
  }
1225
1439
  }
1226
1440
 
1227
- if (!resp?.ok) {
1228
- throw lastError ?? new Error("Antigravity image generation failed");
1441
+ if (xaiInlineImages.length === 0) {
1442
+ return {
1443
+ content: [{ type: "text", text: "No image data returned." }],
1444
+ details: {
1445
+ provider,
1446
+ model: resolvedModel,
1447
+ imageCount: 0,
1448
+ imagePaths: [],
1449
+ images: [],
1450
+ },
1451
+ };
1229
1452
  }
1230
1453
 
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;
1454
+ const xaiImagePaths = await saveImagesToTemp(xaiInlineImages);
1455
+
1456
+ return {
1457
+ content: [
1458
+ { type: "text", text: buildResponseSummary(provider, resolvedModel, xaiImagePaths, undefined) },
1459
+ ],
1460
+ details: {
1461
+ provider,
1462
+ model: resolvedModel,
1463
+ imageCount: xaiInlineImages.length,
1464
+ imagePaths: xaiImagePaths,
1465
+ images: xaiInlineImages,
1466
+ },
1467
+ };
1468
+ }
1238
1469
 
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
- }
1470
+ if (provider === "openrouter") {
1471
+ const prompt = assemblePrompt(params);
1472
+ const contentParts: OpenRouterContentPart[] = [{ type: "text", text: prompt }];
1473
+ for (const image of resolvedImages) {
1474
+ contentParts.push({ type: "image_url", image_url: { url: toDataUrl(image) } });
1475
+ }
1254
1476
 
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
- }
1477
+ const requestBody = {
1478
+ model: resolvedModel,
1479
+ messages: [{ role: "user" as const, content: contentParts }],
1480
+ };
1270
1481
 
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
- }
1482
+ const rawText = await withAuth(
1483
+ apiKey.apiKey,
1484
+ async key => {
1485
+ const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
1486
+ method: "POST",
1487
+ headers: {
1488
+ "Content-Type": "application/json",
1489
+ Authorization: `Bearer ${key}`,
1490
+ "HTTP-Referer": "https://omp.sh/",
1491
+ "X-OpenRouter-Title": "Oh-My-Pi",
1492
+ "X-OpenRouter-Categories": "cli-agent",
1493
+ },
1494
+ body: JSON.stringify(requestBody),
1495
+ signal: requestSignal,
1496
+ });
1497
+ const text = await resp.text();
1498
+ if (!resp.ok) {
1499
+ let message = text;
1500
+ try {
1501
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1502
+ message = parsed.error?.message ?? message;
1503
+ } catch {
1504
+ // Keep raw text.
1505
+ }
1506
+ throw new ProviderHttpError(
1507
+ `OpenRouter image request failed (${resp.status}): ${message}`,
1508
+ resp.status,
1509
+ { headers: resp.headers },
1510
+ );
1511
+ }
1512
+ return text;
1513
+ },
1514
+ { signal: requestSignal },
1515
+ );
1281
1516
 
1282
- const prompt = assemblePrompt(params);
1283
- const aspectRatio = params.aspect_ratio ?? "1:1";
1284
- const xaiResolution = resolveXAIResolution(params.image_size);
1517
+ const data = JSON.parse(rawText) as OpenRouterResponse;
1518
+ const message = data.choices?.[0]?.message;
1519
+ const responseText = collectOpenRouterResponseText(message);
1520
+ const imageUrls = extractOpenRouterImageUrls(message);
1521
+ const inlineImages: InlineImageData[] = [];
1522
+ for (const imageUrl of imageUrls) {
1523
+ inlineImages.push(await loadImageFromUrl(imageUrl, fetchImpl, requestSignal));
1524
+ }
1285
1525
 
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
- }
1526
+ if (inlineImages.length === 0) {
1527
+ const messageText = responseText ? `\n\n${responseText}` : "";
1528
+ return {
1529
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1530
+ details: {
1531
+ provider,
1532
+ model: resolvedModel,
1533
+ imageCount: 0,
1534
+ imagePaths: [],
1535
+ images: [],
1536
+ responseText,
1537
+ },
1538
+ };
1539
+ }
1292
1540
 
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(),
1541
+ const imagePaths = await saveImagesToTemp(inlineImages);
1542
+
1543
+ return {
1544
+ content: [
1545
+ { type: "text", text: buildResponseSummary(provider, resolvedModel, imagePaths, responseText) },
1546
+ ],
1547
+ details: {
1548
+ provider,
1549
+ model: resolvedModel,
1550
+ imageCount: inlineImages.length,
1551
+ imagePaths,
1552
+ images: inlineImages,
1553
+ responseText,
1320
1554
  },
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
- );
1555
+ };
1556
+ }
1341
1557
 
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));
1558
+ const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
1559
+ for (const image of resolvedImages) {
1560
+ parts.push({ inlineData: image });
1353
1561
  }
1354
- }
1562
+ parts.push({ text: assemblePrompt(params) });
1355
1563
 
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
- },
1564
+ const generationConfig: {
1565
+ responseModalities: GeminiResponseModality[];
1566
+ imageConfig?: { aspectRatio?: string; imageSize?: string };
1567
+ } = {
1568
+ responseModalities: ["IMAGE"],
1366
1569
  };
1367
- }
1368
1570
 
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
- }
1571
+ if (params.aspect_ratio || params.image_size) {
1572
+ generationConfig.imageConfig = {
1573
+ aspectRatio: params.aspect_ratio,
1574
+ imageSize: params.image_size,
1575
+ };
1576
+ }
1384
1577
 
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
- }
1578
+ const requestBody = {
1579
+ contents: [{ role: "user" as const, parts }],
1580
+ generationConfig,
1581
+ };
1391
1582
 
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 },
1583
+ const rawText = await withAuth(
1584
+ apiKey.apiKey,
1585
+ async key => {
1586
+ const resp = await fetchImpl(
1587
+ `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1588
+ {
1589
+ method: "POST",
1590
+ headers: {
1591
+ "Content-Type": "application/json",
1592
+ "x-goog-api-key": key,
1593
+ },
1594
+ body: JSON.stringify(requestBody),
1595
+ signal: requestSignal,
1596
+ },
1425
1597
  );
1426
- }
1427
- return text;
1428
- },
1429
- { signal: requestSignal },
1430
- );
1598
+ const text = await resp.text();
1599
+ if (!resp.ok) {
1600
+ let message = text;
1601
+ try {
1602
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1603
+ message = parsed.error?.message ?? message;
1604
+ } catch {
1605
+ // Keep raw text.
1606
+ }
1607
+ throw new ProviderHttpError(
1608
+ `Gemini image request failed (${resp.status}): ${message}`,
1609
+ resp.status,
1610
+ {
1611
+ headers: resp.headers,
1612
+ },
1613
+ );
1614
+ }
1615
+ return text;
1616
+ },
1617
+ { signal: requestSignal },
1618
+ );
1431
1619
 
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
- }
1620
+ const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1621
+ const responseParts = combineParts(data);
1622
+ const responseText = collectResponseText(responseParts);
1623
+ const inlineImages = collectInlineImages(responseParts);
1624
+
1625
+ if (inlineImages.length === 0) {
1626
+ const blocked = data.promptFeedback?.blockReason
1627
+ ? `Blocked: ${data.promptFeedback.blockReason}`
1628
+ : "No image data returned.";
1629
+ return {
1630
+ content: [{ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` }],
1631
+ details: {
1632
+ provider,
1633
+ model,
1634
+ imageCount: 0,
1635
+ imagePaths: [],
1636
+ images: [],
1637
+ responseText,
1638
+ promptFeedback: data.promptFeedback,
1639
+ usage: data.usageMetadata,
1640
+ },
1641
+ };
1642
+ }
1643
+
1644
+ const imagePaths = await saveImagesToTemp(inlineImages);
1440
1645
 
1441
- if (inlineImages.length === 0) {
1442
- const messageText = responseText ? `\n\n${responseText}` : "";
1443
1646
  return {
1444
- content: [{ type: "text", text: `No image data returned.${messageText}` }],
1647
+ content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1445
1648
  details: {
1446
1649
  provider,
1447
- model: resolvedModel,
1448
- imageCount: 0,
1449
- imagePaths: [],
1450
- images: [],
1650
+ model,
1651
+ imageCount: inlineImages.length,
1652
+ imagePaths,
1653
+ images: inlineImages,
1451
1654
  responseText,
1655
+ promptFeedback: data.promptFeedback,
1656
+ usage: data.usageMetadata,
1452
1657
  },
1453
1658
  };
1659
+ } catch (error) {
1660
+ if (!(error instanceof ProviderHttpError) || requestSignal?.aborted) {
1661
+ throw error;
1662
+ }
1663
+ failures.push({ provider, error });
1454
1664
  }
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
1665
  }
1472
1666
 
1473
- const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
1474
- for (const image of resolvedImages) {
1475
- parts.push({ inlineData: image });
1667
+ if (!foundCredentials) {
1668
+ throw new Error(
1669
+ "No image API credentials found. Connect a Codex (ChatGPT) subscription, use a GPT Responses/Codex model with OpenAI credentials, log in with google-antigravity or xAI Grok OAuth, or set OPENAI_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY.",
1670
+ );
1476
1671
  }
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
1672
 
1486
- if (params.aspect_ratio || params.image_size) {
1487
- generationConfig.imageConfig = {
1488
- aspectRatio: params.aspect_ratio,
1489
- imageSize: params.image_size,
1490
- };
1673
+ if (failures.length === 0 && unsupportedAspectRatioProvider) {
1674
+ assertImageAspectRatioSupported(unsupportedAspectRatioProvider, params.aspect_ratio);
1491
1675
  }
1492
1676
 
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 },
1677
+ throw new AggregateError(
1678
+ failures.map(failure => failure.error),
1679
+ `Image generation failed for all credentialed providers: ${failures.map(failure => failure.provider).join(", ")}`,
1529
1680
  );
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
1681
  });
1571
1682
  },
1572
1683
  };