@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.
- package/CHANGELOG.md +56 -0
- package/dist/cli.js +3305 -3302
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +9 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +9 -1
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +22 -12
- package/src/advisor/advise-tool.ts +7 -3
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/config/model-registry.ts +9 -0
- package/src/config/model-resolver.ts +43 -5
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +2 -1
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/export/html/template.js +2 -0
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/lib/xai-http.ts +28 -2
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +17 -2
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +6 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +19 -17
- package/src/modes/interactive-mode.ts +66 -11
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +8 -3
- package/src/modes/rpc/rpc-mode.ts +12 -3
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/sdk.ts +6 -2
- package/src/session/agent-session.ts +79 -34
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +2 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/eval.ts +3 -0
- package/src/tools/image-gen.ts +561 -512
- package/src/tools/index.ts +3 -1
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/xai.ts +40 -9
package/src/tools/image-gen.ts
CHANGED
|
@@ -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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
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
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1096
|
-
);
|
|
1161
|
+
modelId: DEFAULT_ANTIGRAVITY_MODEL,
|
|
1162
|
+
});
|
|
1097
1163
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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 ${
|
|
1336
|
+
Authorization: `Bearer ${key}`,
|
|
1186
1337
|
"Content-Type": "application/json",
|
|
1187
|
-
|
|
1188
|
-
"User-Agent": getAntigravityUserAgent(),
|
|
1338
|
+
"User-Agent": ohMyPiXAIUserAgent(),
|
|
1189
1339
|
},
|
|
1190
|
-
body: JSON.stringify(
|
|
1340
|
+
body: JSON.stringify(xaiBody),
|
|
1191
1341
|
signal: requestSignal,
|
|
1192
1342
|
});
|
|
1193
|
-
|
|
1194
|
-
if (resp.ok) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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 (
|
|
1228
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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:
|
|
1585
|
+
content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
|
|
1445
1586
|
details: {
|
|
1446
1587
|
provider,
|
|
1447
|
-
model
|
|
1448
|
-
imageCount:
|
|
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
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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 (
|
|
1487
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
};
|