Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions skills/openrouter-images/scripts/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
readImageAsDataUrl,
saveImage,
defaultOutputPath,
toDataUrl,
} from "./lib.js";

const apiKey = requireApiKey();
Expand Down Expand Up @@ -57,15 +58,15 @@ if (message.content) {
console.error(`Model: ${message.content}`);
}

const images: string[] = message.images ?? [];
const images: unknown[] = message.images ?? [];
if (images.length === 0) {
console.error("Error: No images returned by model.");
process.exit(1);
}

const saved: string[] = [];
for (let i = 0; i < images.length; i++) {
const img = images[i].startsWith("data:") ? images[i] : `data:image/png;base64,${images[i]}`;
const img = toDataUrl(images[i]);
let outPath: string;
if (images.length === 1) {
outPath = outputBase;
Expand Down
5 changes: 3 additions & 2 deletions skills/openrouter-images/scripts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
postChatCompletion,
saveImage,
defaultOutputPath,
toDataUrl,
} from "./lib.js";

const apiKey = requireApiKey();
Expand Down Expand Up @@ -44,15 +45,15 @@ if (message.content) {
console.error(`Model: ${message.content}`);
}

const images: string[] = message.images ?? [];
const images: unknown[] = message.images ?? [];
if (images.length === 0) {
console.error("Error: No images returned by model.");
process.exit(1);
}

const saved: string[] = [];
for (let i = 0; i < images.length; i++) {
const dataUrl = images[i].startsWith("data:") ? images[i] : `data:image/png;base64,${images[i]}`;
const dataUrl = toDataUrl(images[i]);
let outPath: string;
if (images.length === 1) {
outPath = outputBase;
Expand Down
30 changes: 30 additions & 0 deletions skills/openrouter-images/scripts/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,33 @@ export function defaultOutputPath(): string {
`-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
return `image-${stamp}.png`;
}

/**
* Normalize an entry from `message.images` into a data-URL string.
*
* The chat-completions response shape for `images` varies by model:
* - some models return a raw base64 string,
* - some return a data URL string,
* - others (e.g. openai/gpt-5.4-image-2, recent google/gemini-*-image
* variants) return an object: `{ type: "image_url", image_url: { url } }`
* or `{ b64_json: "..." }`.
*
* This helper accepts any of those and returns a `data:...;base64,...` URL
* suitable for `saveImage()`. It exits with a clean error on unrecognized
* shapes rather than throwing a `TypeError`.
*/
export function toDataUrl(entry: unknown): string {
let str: string | undefined;
if (typeof entry === "string") {
str = entry;
} else if (entry && typeof entry === "object") {
const e = entry as { image_url?: { url?: string }; url?: string; b64_json?: string; data?: string };
str = e.image_url?.url ?? e.url ?? e.b64_json ?? e.data;
}
if (typeof str !== "string" || str.length === 0) {
const preview = JSON.stringify(entry).slice(0, 200);
console.error(`Error: Unrecognized image payload shape in response: ${preview}`);
process.exit(1);
}
return str.startsWith("data:") ? str : `data:image/png;base64,${str}`;
}