runline 0.27.2 → 0.28.0
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/dist/plugins/_shared/imageFile.js +37 -2
- package/dist/plugins/googleImage/src/index.js +79 -24
- package/dist/plugins/openai/src/index.js +110 -19
- package/dist/plugins/recraft/src/index.js +81 -1
- package/dist/plugins/replicate/src/index.js +157 -83
- package/dist/plugins/together/src/index.js +69 -1
- package/dist/plugins/xai/src/index.js +68 -2
- package/package.json +1 -1
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* send_file/attachment tool. Plugins load in-process via jiti, so node:fs is
|
|
9
9
|
* available (same pattern as googleDrive/googleSlides).
|
|
10
10
|
*/
|
|
11
|
-
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { basename, extname, join } from "node:path";
|
|
14
14
|
const MIME_EXT = {
|
|
15
15
|
"image/png": "png",
|
|
16
16
|
"image/jpeg": "jpg",
|
|
@@ -38,3 +38,38 @@ export function writeImageFile(opts) {
|
|
|
38
38
|
return { path, mimeType, byteLength: bytes.length };
|
|
39
39
|
}
|
|
40
40
|
export const SEND_FILE_NOTE = "Image(s) written to disk. Deliver each to the user with send_file using its `path`.";
|
|
41
|
+
const EXT_MIME = {
|
|
42
|
+
png: "image/png",
|
|
43
|
+
jpg: "image/jpeg",
|
|
44
|
+
jpeg: "image/jpeg",
|
|
45
|
+
webp: "image/webp",
|
|
46
|
+
gif: "image/gif",
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Read a local image for an `image.edit` call: bytes + base64 + mime
|
|
50
|
+
* (from the extension) + a `data:` URI for JSON APIs. Throws with the
|
|
51
|
+
* plugin's name when the file can't be read so errors point at the
|
|
52
|
+
* right tool.
|
|
53
|
+
*/
|
|
54
|
+
export function readImageInput(imagePath, pluginName) {
|
|
55
|
+
if (typeof imagePath !== "string" || imagePath.trim().length === 0) {
|
|
56
|
+
throw new Error(`${pluginName}: imagePath is required`);
|
|
57
|
+
}
|
|
58
|
+
let bytes;
|
|
59
|
+
try {
|
|
60
|
+
bytes = readFileSync(imagePath);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
throw new Error(`${pluginName}: cannot read image at ${imagePath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
const ext = extname(imagePath).slice(1).toLowerCase();
|
|
66
|
+
const mimeType = EXT_MIME[ext] ?? "image/png";
|
|
67
|
+
const base64 = bytes.toString("base64");
|
|
68
|
+
return {
|
|
69
|
+
bytes,
|
|
70
|
+
base64,
|
|
71
|
+
mimeType,
|
|
72
|
+
dataUri: `data:${mimeType};base64,${base64}`,
|
|
73
|
+
fileName: basename(imagePath),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -14,14 +14,32 @@
|
|
|
14
14
|
* stripped before delivery. Hand each `path` to the host's file-sending
|
|
15
15
|
* tool (e.g. send_file) to deliver the image.
|
|
16
16
|
*
|
|
17
|
-
* Nano Banana
|
|
18
|
-
*
|
|
17
|
+
* Nano Banana is strong at editing — `image.edit` sends a local image
|
|
18
|
+
* inline next to the instruction and writes the edited result to disk.
|
|
19
19
|
*/
|
|
20
|
-
import { writeFileSync } from "node:fs";
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
|
-
import { join } from "node:path";
|
|
23
20
|
import * as t from "typebox";
|
|
21
|
+
import { readImageInput, SEND_FILE_NOTE, writeImageFile, } from "../../_shared/imageFile.js";
|
|
24
22
|
const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
23
|
+
/** Write every inline image part of a Gemini response to disk. */
|
|
24
|
+
function saveInlineImages(data, saveDir) {
|
|
25
|
+
const stamp = Date.now();
|
|
26
|
+
const images = [];
|
|
27
|
+
for (const candidate of data.candidates ?? []) {
|
|
28
|
+
for (const part of candidate.content?.parts ?? []) {
|
|
29
|
+
if (part.inlineData?.data) {
|
|
30
|
+
images.push(writeImageFile({
|
|
31
|
+
base64: part.inlineData.data,
|
|
32
|
+
mimeType: part.inlineData.mimeType ?? "image/png",
|
|
33
|
+
provider: "googleImage",
|
|
34
|
+
index: images.length,
|
|
35
|
+
saveDir,
|
|
36
|
+
stamp,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return images;
|
|
42
|
+
}
|
|
25
43
|
export default function googleImage(rl) {
|
|
26
44
|
rl.setName("googleImage");
|
|
27
45
|
rl.setVersion("0.1.0");
|
|
@@ -70,27 +88,64 @@ export default function googleImage(rl) {
|
|
|
70
88
|
throw new Error(`Google API error ${res.status}: ${await res.text()}`);
|
|
71
89
|
}
|
|
72
90
|
const data = (await res.json());
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const images = saveInlineImages(data, p.saveDir);
|
|
92
|
+
return { provider: "googleImage", model, images, note: SEND_FILE_NOTE };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
rl.registerAction("image.edit", {
|
|
96
|
+
access: "write",
|
|
97
|
+
description: "Edit a local image with Gemini (Nano Banana): give the file path and describe the change. Sends the image inline with the instruction, writes the edited image(s) to disk, and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
|
|
98
|
+
inputSchema: t.Object({
|
|
99
|
+
prompt: t.String({
|
|
100
|
+
minLength: 1,
|
|
101
|
+
description: "Instruction describing the edit to apply",
|
|
102
|
+
}),
|
|
103
|
+
imagePath: t.String({
|
|
104
|
+
minLength: 1,
|
|
105
|
+
description: "Path to the source image file",
|
|
106
|
+
}),
|
|
107
|
+
model: t.Optional(t.String({
|
|
108
|
+
minLength: 1,
|
|
109
|
+
description: "Gemini image model ID. Defaults to gemini-2.5-flash-image.",
|
|
110
|
+
})),
|
|
111
|
+
saveDir: t.Optional(t.String({
|
|
112
|
+
description: "Directory to write the image file(s) into. Empty or omitted uses the OS temp directory.",
|
|
113
|
+
})),
|
|
114
|
+
}, { additionalProperties: false }),
|
|
115
|
+
async execute(input, ctx) {
|
|
116
|
+
const p = (input ?? {});
|
|
117
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
118
|
+
throw new Error("googleImage: prompt is required");
|
|
87
119
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
const img = readImageInput(p.imagePath, "googleImage");
|
|
121
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
122
|
+
const model = p.model ?? "gemini-2.5-flash-image";
|
|
123
|
+
const url = `${BASE}/${model}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
124
|
+
const body = {
|
|
125
|
+
contents: [
|
|
126
|
+
{
|
|
127
|
+
parts: [
|
|
128
|
+
{ text: p.prompt },
|
|
129
|
+
{ inlineData: { mimeType: img.mimeType, data: img.base64 } },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
generationConfig: { responseModalities: ["IMAGE", "TEXT"] },
|
|
93
134
|
};
|
|
135
|
+
const res = await fetch(url, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
throw new Error(`Google API error ${res.status}: ${await res.text()}`);
|
|
142
|
+
}
|
|
143
|
+
const data = (await res.json());
|
|
144
|
+
const images = saveInlineImages(data, p.saveDir);
|
|
145
|
+
if (images.length === 0) {
|
|
146
|
+
throw new Error("googleImage: the model returned no edited image — it may have refused; try a more specific instruction");
|
|
147
|
+
}
|
|
148
|
+
return { provider: "googleImage", model, images, note: SEND_FILE_NOTE };
|
|
94
149
|
},
|
|
95
150
|
});
|
|
96
151
|
}
|
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* const { images } = await openai.image.create({ prompt: "a red bicycle on snow" })
|
|
15
15
|
* // images[0].path -> "/tmp/openai-image-….png"
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import { tmpdir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
17
|
+
import { readImageInput, SEND_FILE_NOTE, writeImageFile, } from "../../_shared/imageFile.js";
|
|
20
18
|
const ENDPOINT = "https://api.openai.com/v1/images/generations";
|
|
19
|
+
const EDIT_ENDPOINT = "https://api.openai.com/v1/images/edits";
|
|
21
20
|
export default function openai(rl) {
|
|
22
21
|
rl.setName("openai");
|
|
23
22
|
rl.setVersion("0.1.0");
|
|
@@ -114,25 +113,117 @@ export default function openai(rl) {
|
|
|
114
113
|
throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
115
114
|
}
|
|
116
115
|
const data = (await res.json());
|
|
117
|
-
const dir = (typeof p.saveDir === "string" && p.saveDir.trim()) || tmpdir();
|
|
118
116
|
const stamp = Date.now();
|
|
119
|
-
const images = (data.data ?? []).map((d, i) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
writeFileSync(path, bytes);
|
|
123
|
-
return {
|
|
124
|
-
path,
|
|
117
|
+
const images = (data.data ?? []).map((d, i) => ({
|
|
118
|
+
...writeImageFile({
|
|
119
|
+
base64: d.b64_json,
|
|
125
120
|
mimeType: "image/png",
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
provider: "openai-image",
|
|
122
|
+
index: i,
|
|
123
|
+
saveDir: p.saveDir,
|
|
124
|
+
stamp,
|
|
125
|
+
}),
|
|
126
|
+
...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
|
|
127
|
+
}));
|
|
128
|
+
return { provider: "openai", model, images, note: SEND_FILE_NOTE };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
rl.registerAction("image.edit", {
|
|
132
|
+
access: "write",
|
|
133
|
+
description: "Edit a local image with OpenAI (gpt-image-*) via /v1/images/edits: give the file path(s) and describe the change. Writes the edited PNG(s) to disk and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
prompt: {
|
|
136
|
+
type: "string",
|
|
137
|
+
required: true,
|
|
138
|
+
description: "Instruction describing the edit to apply",
|
|
139
|
+
},
|
|
140
|
+
imagePath: {
|
|
141
|
+
type: "string",
|
|
142
|
+
required: false,
|
|
143
|
+
description: "Path to the source image file. Either this or imagePaths is required.",
|
|
144
|
+
},
|
|
145
|
+
imagePaths: {
|
|
146
|
+
type: "array",
|
|
147
|
+
required: false,
|
|
148
|
+
description: "Paths to multiple source images (gpt-image models accept up to 16; the first is the primary).",
|
|
149
|
+
},
|
|
150
|
+
saveDir: {
|
|
151
|
+
type: "string",
|
|
152
|
+
required: false,
|
|
153
|
+
description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
|
|
154
|
+
},
|
|
155
|
+
model: {
|
|
156
|
+
type: "string",
|
|
157
|
+
required: false,
|
|
158
|
+
description: "gpt-image-2 | gpt-image-1 | gpt-image-1-mini. Omit to use the connection default.",
|
|
159
|
+
},
|
|
160
|
+
size: {
|
|
161
|
+
type: "string",
|
|
162
|
+
required: false,
|
|
163
|
+
description: "WxH output size. Omit to let the API match the input.",
|
|
164
|
+
},
|
|
165
|
+
quality: {
|
|
166
|
+
type: "string",
|
|
167
|
+
required: false,
|
|
168
|
+
description: "low | medium | high",
|
|
169
|
+
},
|
|
170
|
+
n: {
|
|
171
|
+
type: "number",
|
|
172
|
+
required: false,
|
|
173
|
+
description: "Number of edited variants (default: 1, max: 4)",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
async execute(input, ctx) {
|
|
177
|
+
const p = (input ?? {});
|
|
178
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
179
|
+
throw new Error("openai: prompt is required");
|
|
180
|
+
}
|
|
181
|
+
const paths = Array.isArray(p.imagePaths) && p.imagePaths.length > 0
|
|
182
|
+
? p.imagePaths
|
|
183
|
+
: p.imagePath
|
|
184
|
+
? [p.imagePath]
|
|
185
|
+
: [];
|
|
186
|
+
if (paths.length === 0) {
|
|
187
|
+
throw new Error("openai: imagePath (or imagePaths) is required");
|
|
188
|
+
}
|
|
189
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
190
|
+
const model = p.model ??
|
|
191
|
+
ctx.connection.config.defaultModel ??
|
|
192
|
+
"gpt-image-1";
|
|
193
|
+
const form = new FormData();
|
|
194
|
+
form.append("model", model);
|
|
195
|
+
form.append("prompt", p.prompt);
|
|
196
|
+
form.append("n", String(Math.min(p.n ?? 1, 4)));
|
|
197
|
+
if (p.size)
|
|
198
|
+
form.append("size", p.size);
|
|
199
|
+
if (p.quality)
|
|
200
|
+
form.append("quality", p.quality);
|
|
201
|
+
for (const path of paths) {
|
|
202
|
+
const img = readImageInput(path, "openai");
|
|
203
|
+
form.append("image[]", new Blob([img.bytes], { type: img.mimeType }), img.fileName);
|
|
204
|
+
}
|
|
205
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
208
|
+
body: form,
|
|
129
209
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
212
|
+
}
|
|
213
|
+
const data = (await res.json());
|
|
214
|
+
const stamp = Date.now();
|
|
215
|
+
const images = (data.data ?? []).map((d, i) => ({
|
|
216
|
+
...writeImageFile({
|
|
217
|
+
base64: d.b64_json,
|
|
218
|
+
mimeType: "image/png",
|
|
219
|
+
provider: "openai-image",
|
|
220
|
+
index: i,
|
|
221
|
+
saveDir: p.saveDir,
|
|
222
|
+
stamp,
|
|
223
|
+
}),
|
|
224
|
+
...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
|
|
225
|
+
}));
|
|
226
|
+
return { provider: "openai", model, images, note: SEND_FILE_NOTE };
|
|
136
227
|
},
|
|
137
228
|
});
|
|
138
229
|
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* legacy `style` knob — pass `styleId` against your own custom
|
|
14
14
|
* style if you've set one up.
|
|
15
15
|
*/
|
|
16
|
-
import { SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
16
|
+
import { readImageInput, SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
17
17
|
const ENDPOINT = "https://external.api.recraft.ai/v1/images/generations";
|
|
18
|
+
const EDIT_ENDPOINT = "https://external.api.recraft.ai/v1/images/imageToImage";
|
|
18
19
|
export default function recraft(rl) {
|
|
19
20
|
rl.setName("recraft");
|
|
20
21
|
rl.setVersion("0.1.0");
|
|
@@ -102,4 +103,83 @@ export default function recraft(rl) {
|
|
|
102
103
|
return { provider: "recraft", model, images, note: SEND_FILE_NOTE };
|
|
103
104
|
},
|
|
104
105
|
});
|
|
106
|
+
rl.registerAction("image.edit", {
|
|
107
|
+
access: "write",
|
|
108
|
+
description: "Edit a local image with Recraft (imageToImage): give the file path, describe the change, and tune `strength` for how far to depart from the original. Writes the edited image(s) to disk and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
prompt: {
|
|
111
|
+
type: "string",
|
|
112
|
+
required: true,
|
|
113
|
+
description: "Instruction describing the transformation",
|
|
114
|
+
},
|
|
115
|
+
imagePath: {
|
|
116
|
+
type: "string",
|
|
117
|
+
required: true,
|
|
118
|
+
description: "Path to the source image file (PNG, JPG, WEBP, or SVG; max 10 MB)",
|
|
119
|
+
},
|
|
120
|
+
strength: {
|
|
121
|
+
type: "number",
|
|
122
|
+
required: false,
|
|
123
|
+
description: "0–1: how strongly to alter the input (default: 0.2 — close to the original)",
|
|
124
|
+
},
|
|
125
|
+
saveDir: {
|
|
126
|
+
type: "string",
|
|
127
|
+
required: false,
|
|
128
|
+
description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
|
|
129
|
+
},
|
|
130
|
+
model: {
|
|
131
|
+
type: "string",
|
|
132
|
+
required: false,
|
|
133
|
+
description: "Any Recraft V3 / V4 / V4.1 model, raster or vector (e.g. recraftv3, recraftv4_pro, recraftv4_1_vector). API default: recraftv4_1.",
|
|
134
|
+
},
|
|
135
|
+
style: {
|
|
136
|
+
type: "string",
|
|
137
|
+
required: false,
|
|
138
|
+
description: "Photorealism | Illustration | Vector art | Hand-drawn | Icon | Recraft V3 Raw",
|
|
139
|
+
},
|
|
140
|
+
styleId: {
|
|
141
|
+
type: "string",
|
|
142
|
+
required: false,
|
|
143
|
+
description: "ID of a custom style created in your Recraft account",
|
|
144
|
+
},
|
|
145
|
+
n: {
|
|
146
|
+
type: "number",
|
|
147
|
+
required: false,
|
|
148
|
+
description: "Number of edited variants (default: 1, max: 6)",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
async execute(input, ctx) {
|
|
152
|
+
const p = (input ?? {});
|
|
153
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
154
|
+
throw new Error("recraft: prompt is required");
|
|
155
|
+
}
|
|
156
|
+
const img = readImageInput(p.imagePath, "recraft");
|
|
157
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
158
|
+
const form = new FormData();
|
|
159
|
+
form.append("image", new Blob([img.bytes], { type: img.mimeType }), img.fileName);
|
|
160
|
+
form.append("prompt", p.prompt);
|
|
161
|
+
form.append("strength", String(p.strength ?? 0.2));
|
|
162
|
+
form.append("response_format", "b64_json");
|
|
163
|
+
form.append("n", String(Math.min(p.n ?? 1, 6)));
|
|
164
|
+
if (p.model)
|
|
165
|
+
form.append("model", p.model);
|
|
166
|
+
if (p.style)
|
|
167
|
+
form.append("style", p.style);
|
|
168
|
+
if (p.styleId)
|
|
169
|
+
form.append("style_id", p.styleId);
|
|
170
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
173
|
+
body: form,
|
|
174
|
+
});
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
throw new Error(`Recraft API error ${res.status}: ${await res.text()}`);
|
|
177
|
+
}
|
|
178
|
+
const data = (await res.json());
|
|
179
|
+
const stamp = Date.now();
|
|
180
|
+
const images = (data.data ?? []).map((d, i) => writeImageFile({ base64: d.b64_json, mimeType: "image/png", provider: "recraft", index: i, saveDir: p.saveDir, stamp }));
|
|
181
|
+
// The imageToImage endpoint's own default model is recraftv4_1.
|
|
182
|
+
return { provider: "recraft", model: p.model ?? "recraftv4_1", images, note: SEND_FILE_NOTE };
|
|
183
|
+
},
|
|
184
|
+
});
|
|
105
185
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* separately.
|
|
20
20
|
*/
|
|
21
21
|
import { Buffer } from "node:buffer";
|
|
22
|
-
import { SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
22
|
+
import { readImageInput, SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
23
23
|
import { parseSize } from "../../_shared/parseSize.js";
|
|
24
24
|
const POLL_INTERVAL_MS = 2_000;
|
|
25
25
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -35,6 +35,93 @@ function stringifyError(err) {
|
|
|
35
35
|
return String(err);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a prediction for `model`, wait for it to reach a terminal
|
|
40
|
+
* state (Prefer: wait + polling), download the output URL(s), and
|
|
41
|
+
* write them to disk. Shared by image.create and image.edit — the
|
|
42
|
+
* only thing that differs between them is the prediction `input`.
|
|
43
|
+
*/
|
|
44
|
+
async function runPrediction(opts) {
|
|
45
|
+
const { apiToken, model, input, timeoutMs, saveDir } = opts;
|
|
46
|
+
const deadline = Date.now() + timeoutMs;
|
|
47
|
+
const createRes = await fetch(`https://api.replicate.com/v1/models/${model}/predictions`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
Authorization: `Bearer ${apiToken}`,
|
|
52
|
+
// `Prefer: wait` lets the server hold the connection open for
|
|
53
|
+
// fast jobs so we don't have to poll at all on the happy path.
|
|
54
|
+
Prefer: "wait",
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({ input }),
|
|
57
|
+
});
|
|
58
|
+
if (!createRes.ok) {
|
|
59
|
+
throw new Error(`Replicate API error ${createRes.status}: ${await createRes.text()}`);
|
|
60
|
+
}
|
|
61
|
+
let prediction = (await createRes.json());
|
|
62
|
+
while (prediction.status !== "succeeded" &&
|
|
63
|
+
prediction.status !== "failed" &&
|
|
64
|
+
prediction.status !== "canceled") {
|
|
65
|
+
if (Date.now() >= deadline) {
|
|
66
|
+
throw new Error(`Replicate generation timed out after ${timeoutMs}ms (still ${prediction.status})`);
|
|
67
|
+
}
|
|
68
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
69
|
+
const pollRes = await fetch(prediction.urls.get, {
|
|
70
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
71
|
+
});
|
|
72
|
+
if (!pollRes.ok) {
|
|
73
|
+
throw new Error(`Replicate poll error ${pollRes.status}: ${await pollRes.text()}`);
|
|
74
|
+
}
|
|
75
|
+
prediction = (await pollRes.json());
|
|
76
|
+
}
|
|
77
|
+
if (prediction.status !== "succeeded") {
|
|
78
|
+
throw new Error(`Replicate generation ${prediction.status}: ${stringifyError(prediction.error)}`);
|
|
79
|
+
}
|
|
80
|
+
// Output is either a single URL or an array of them. Download
|
|
81
|
+
// each and base64-encode so the caller gets bytes back, not
|
|
82
|
+
// pre-signed URLs that expire. Track per-URL failures and
|
|
83
|
+
// surface them: silent partial success would let an agent
|
|
84
|
+
// think it got 3 images when one 404'd.
|
|
85
|
+
const outputs = Array.isArray(prediction.output)
|
|
86
|
+
? prediction.output
|
|
87
|
+
: prediction.output
|
|
88
|
+
? [prediction.output]
|
|
89
|
+
: [];
|
|
90
|
+
const images = [];
|
|
91
|
+
const failures = [];
|
|
92
|
+
const stamp = Date.now();
|
|
93
|
+
for (const url of outputs) {
|
|
94
|
+
if (typeof url !== "string") {
|
|
95
|
+
failures.push({ url: String(url), reason: "non-string output" });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const imgRes = await fetch(url);
|
|
99
|
+
if (!imgRes.ok) {
|
|
100
|
+
failures.push({
|
|
101
|
+
url,
|
|
102
|
+
reason: `download failed (${imgRes.status})`,
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const buf = Buffer.from(await imgRes.arrayBuffer());
|
|
107
|
+
const contentType = (imgRes.headers.get("content-type") ?? "image/webp")
|
|
108
|
+
.split(";")[0]
|
|
109
|
+
.trim();
|
|
110
|
+
images.push(writeImageFile({
|
|
111
|
+
base64: buf.toString("base64"),
|
|
112
|
+
mimeType: contentType,
|
|
113
|
+
provider: "replicate",
|
|
114
|
+
index: images.length,
|
|
115
|
+
saveDir,
|
|
116
|
+
stamp,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
if (images.length === 0 && outputs.length > 0) {
|
|
120
|
+
const detail = failures.map((f) => `${f.url}: ${f.reason}`).join("; ");
|
|
121
|
+
throw new Error(`Replicate succeeded but all ${outputs.length} output URLs failed to download — ${detail}`);
|
|
122
|
+
}
|
|
123
|
+
return { images, failures };
|
|
124
|
+
}
|
|
38
125
|
export default function replicate(rl) {
|
|
39
126
|
rl.setName("replicate");
|
|
40
127
|
rl.setVersion("0.1.0");
|
|
@@ -89,91 +176,78 @@ export default function replicate(rl) {
|
|
|
89
176
|
const apiToken = ctx.connection.config.apiToken;
|
|
90
177
|
const model = p.model ?? "black-forest-labs/flux-dev";
|
|
91
178
|
const { width, height } = parseSize(p.size, "replicate");
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// fast jobs so we don't have to poll at all on the happy path.
|
|
101
|
-
Prefer: "wait",
|
|
179
|
+
const { images, failures } = await runPrediction({
|
|
180
|
+
apiToken,
|
|
181
|
+
model,
|
|
182
|
+
input: {
|
|
183
|
+
prompt: p.prompt,
|
|
184
|
+
width,
|
|
185
|
+
height,
|
|
186
|
+
num_outputs: Math.min(p.n ?? 1, 4),
|
|
102
187
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
prompt: p.prompt,
|
|
106
|
-
width,
|
|
107
|
-
height,
|
|
108
|
-
num_outputs: Math.min(p.n ?? 1, 4),
|
|
109
|
-
},
|
|
110
|
-
}),
|
|
188
|
+
timeoutMs: p.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
189
|
+
saveDir: p.saveDir,
|
|
111
190
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const buf = Buffer.from(await imgRes.arrayBuffer());
|
|
161
|
-
const contentType = (imgRes.headers.get("content-type") ?? "image/webp")
|
|
162
|
-
.split(";")[0]
|
|
163
|
-
.trim();
|
|
164
|
-
images.push(writeImageFile({
|
|
165
|
-
base64: buf.toString("base64"),
|
|
166
|
-
mimeType: contentType,
|
|
167
|
-
provider: "replicate",
|
|
168
|
-
index: images.length,
|
|
169
|
-
saveDir: p.saveDir,
|
|
170
|
-
stamp,
|
|
171
|
-
}));
|
|
172
|
-
}
|
|
173
|
-
if (images.length === 0 && outputs.length > 0) {
|
|
174
|
-
const detail = failures.map((f) => `${f.url}: ${f.reason}`).join("; ");
|
|
175
|
-
throw new Error(`Replicate succeeded but all ${outputs.length} output URLs failed to download — ${detail}`);
|
|
191
|
+
const result = { provider: "replicate", model, images, note: SEND_FILE_NOTE };
|
|
192
|
+
if (failures.length > 0)
|
|
193
|
+
result.failures = failures;
|
|
194
|
+
return result;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
rl.registerAction("image.edit", {
|
|
198
|
+
access: "write",
|
|
199
|
+
description: "Edit a local image via Replicate: give the file path and describe the change. Default model is black-forest-labs/flux-kontext-pro (input key `input_image`); other models may name their image input differently — override with `imageInputKey` (e.g. google/nano-banana uses `image_input`, which is sent as an array). Writes the edited image(s) to disk and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
|
|
200
|
+
inputSchema: {
|
|
201
|
+
prompt: {
|
|
202
|
+
type: "string",
|
|
203
|
+
required: true,
|
|
204
|
+
description: "Instruction describing the edit to apply",
|
|
205
|
+
},
|
|
206
|
+
imagePath: {
|
|
207
|
+
type: "string",
|
|
208
|
+
required: true,
|
|
209
|
+
description: "Path to the source image file (jpeg/png/gif/webp). Sent as a data URI — Replicate recommends hosted URLs for files over ~256 KB, so very large images may be rejected.",
|
|
210
|
+
},
|
|
211
|
+
saveDir: {
|
|
212
|
+
type: "string",
|
|
213
|
+
required: false,
|
|
214
|
+
description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
|
|
215
|
+
},
|
|
216
|
+
model: {
|
|
217
|
+
type: "string",
|
|
218
|
+
required: false,
|
|
219
|
+
description: "Edit-capable Replicate model id, e.g. black-forest-labs/flux-kontext-pro (default), black-forest-labs/flux-kontext-max, google/nano-banana",
|
|
220
|
+
},
|
|
221
|
+
imageInputKey: {
|
|
222
|
+
type: "string",
|
|
223
|
+
required: false,
|
|
224
|
+
description: "Name of the model's image input field (default: input_image). Keys ending in `_input` are sent as an array.",
|
|
225
|
+
},
|
|
226
|
+
timeoutMs: {
|
|
227
|
+
type: "number",
|
|
228
|
+
required: false,
|
|
229
|
+
description: "Max ms to wait for the prediction to finish (default: 300000 = 5 minutes)",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
async execute(input, ctx) {
|
|
233
|
+
const p = (input ?? {});
|
|
234
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
235
|
+
throw new Error("replicate: prompt is required");
|
|
176
236
|
}
|
|
237
|
+
const img = readImageInput(p.imagePath, "replicate");
|
|
238
|
+
const apiToken = ctx.connection.config.apiToken;
|
|
239
|
+
const model = p.model ?? "black-forest-labs/flux-kontext-pro";
|
|
240
|
+
const imageKey = p.imageInputKey ?? "input_image";
|
|
241
|
+
const { images, failures } = await runPrediction({
|
|
242
|
+
apiToken,
|
|
243
|
+
model,
|
|
244
|
+
input: {
|
|
245
|
+
prompt: p.prompt,
|
|
246
|
+
[imageKey]: imageKey.endsWith("_input") ? [img.dataUri] : img.dataUri,
|
|
247
|
+
},
|
|
248
|
+
timeoutMs: p.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
249
|
+
saveDir: p.saveDir,
|
|
250
|
+
});
|
|
177
251
|
const result = { provider: "replicate", model, images, note: SEND_FILE_NOTE };
|
|
178
252
|
if (failures.length > 0)
|
|
179
253
|
result.failures = failures;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* fidelity switch to FLUX.1-dev / Ideogram / Qwen-Image and bump
|
|
13
13
|
* `steps` accordingly (20–30 is typical for non-schnell models).
|
|
14
14
|
*/
|
|
15
|
-
import { SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
15
|
+
import { readImageInput, SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
16
16
|
import { parseSize } from "../../_shared/parseSize.js";
|
|
17
17
|
const ENDPOINT = "https://api.together.xyz/v1/images/generations";
|
|
18
18
|
export default function together(rl) {
|
|
@@ -95,4 +95,72 @@ export default function together(rl) {
|
|
|
95
95
|
return { provider: "together", model, images, note: SEND_FILE_NOTE };
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
|
+
rl.registerAction("image.edit", {
|
|
99
|
+
access: "write",
|
|
100
|
+
description: "Edit a local image with Together AI (FLUX.1 Kontext): give the file path and describe the change. Sends the image as a data URI via `image_url` on the generations endpoint, writes the edited image(s) to disk, and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
prompt: {
|
|
103
|
+
type: "string",
|
|
104
|
+
required: true,
|
|
105
|
+
description: "Instruction describing the edit to apply",
|
|
106
|
+
},
|
|
107
|
+
imagePath: {
|
|
108
|
+
type: "string",
|
|
109
|
+
required: true,
|
|
110
|
+
description: "Path to the source image file",
|
|
111
|
+
},
|
|
112
|
+
saveDir: {
|
|
113
|
+
type: "string",
|
|
114
|
+
required: false,
|
|
115
|
+
description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
|
|
116
|
+
},
|
|
117
|
+
model: {
|
|
118
|
+
type: "string",
|
|
119
|
+
required: false,
|
|
120
|
+
description: "Edit-capable model, e.g. black-forest-labs/FLUX.1-kontext-pro (default), black-forest-labs/FLUX.1-kontext-max",
|
|
121
|
+
},
|
|
122
|
+
steps: {
|
|
123
|
+
type: "number",
|
|
124
|
+
required: false,
|
|
125
|
+
description: "Inference steps (default: 28)",
|
|
126
|
+
},
|
|
127
|
+
n: {
|
|
128
|
+
type: "number",
|
|
129
|
+
required: false,
|
|
130
|
+
description: "Number of edited variants (default: 1, max: 4)",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
async execute(input, ctx) {
|
|
134
|
+
const p = (input ?? {});
|
|
135
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
136
|
+
throw new Error("together: prompt is required");
|
|
137
|
+
}
|
|
138
|
+
const img = readImageInput(p.imagePath, "together");
|
|
139
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
140
|
+
const model = p.model ?? "black-forest-labs/FLUX.1-kontext-pro";
|
|
141
|
+
const body = {
|
|
142
|
+
model,
|
|
143
|
+
prompt: p.prompt,
|
|
144
|
+
image_url: img.dataUri,
|
|
145
|
+
steps: p.steps ?? 28,
|
|
146
|
+
n: Math.min(p.n ?? 1, 4),
|
|
147
|
+
response_format: "base64",
|
|
148
|
+
};
|
|
149
|
+
const res = await fetch(ENDPOINT, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
"Content-Type": "application/json",
|
|
153
|
+
Authorization: `Bearer ${apiKey}`,
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify(body),
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
throw new Error(`Together API error ${res.status}: ${await res.text()}`);
|
|
159
|
+
}
|
|
160
|
+
const data = (await res.json());
|
|
161
|
+
const stamp = Date.now();
|
|
162
|
+
const images = (data.data ?? []).map((d, i) => writeImageFile({ base64: d.b64_json, mimeType: "image/png", provider: "together", index: i, saveDir: p.saveDir, stamp }));
|
|
163
|
+
return { provider: "together", model, images, note: SEND_FILE_NOTE };
|
|
164
|
+
},
|
|
165
|
+
});
|
|
98
166
|
}
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* text rendering well. Sized via aspect_ratio rather than W×H —
|
|
9
9
|
* the API does the math.
|
|
10
10
|
*/
|
|
11
|
-
import { SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
11
|
+
import { readImageInput, SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
|
|
12
12
|
const ENDPOINT = "https://api.x.ai/v1/images/generations";
|
|
13
|
+
const EDIT_ENDPOINT = "https://api.x.ai/v1/images/edits";
|
|
13
14
|
const MODEL = "grok-imagine-image";
|
|
15
|
+
const EDIT_MODEL = "grok-imagine-image-2.0";
|
|
14
16
|
export default function xai(rl) {
|
|
15
17
|
rl.setName("xai");
|
|
16
18
|
rl.setVersion("0.1.0");
|
|
@@ -75,10 +77,74 @@ export default function xai(rl) {
|
|
|
75
77
|
const data = (await res.json());
|
|
76
78
|
const stamp = Date.now();
|
|
77
79
|
const images = (data.data ?? []).map((d, i) => ({
|
|
78
|
-
...writeImageFile({ base64: d.b64_json, mimeType: "image/jpeg", provider: "xai", index: i, saveDir: p.saveDir, stamp }),
|
|
80
|
+
...writeImageFile({ base64: d.b64_json, mimeType: d.mime_type ?? "image/jpeg", provider: "xai", index: i, saveDir: p.saveDir, stamp }),
|
|
79
81
|
...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
|
|
80
82
|
}));
|
|
81
83
|
return { provider: "xai", model: MODEL, images, note: SEND_FILE_NOTE };
|
|
82
84
|
},
|
|
83
85
|
});
|
|
86
|
+
rl.registerAction("image.edit", {
|
|
87
|
+
access: "write",
|
|
88
|
+
description: "Edit a local image with xAI Grok Imagine: give the file path and describe the change. Sends the image as a data URI to /v1/images/edits (JSON, not multipart), writes the edited image(s) to disk, and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
prompt: {
|
|
91
|
+
type: "string",
|
|
92
|
+
required: true,
|
|
93
|
+
description: "Instruction describing the edit to apply",
|
|
94
|
+
},
|
|
95
|
+
imagePath: {
|
|
96
|
+
type: "string",
|
|
97
|
+
required: true,
|
|
98
|
+
description: "Path to the source image file",
|
|
99
|
+
},
|
|
100
|
+
saveDir: {
|
|
101
|
+
type: "string",
|
|
102
|
+
required: false,
|
|
103
|
+
description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
|
|
104
|
+
},
|
|
105
|
+
model: {
|
|
106
|
+
type: "string",
|
|
107
|
+
required: false,
|
|
108
|
+
description: "Edit-capable model (default: grok-imagine-image-2.0)",
|
|
109
|
+
},
|
|
110
|
+
n: {
|
|
111
|
+
type: "number",
|
|
112
|
+
required: false,
|
|
113
|
+
description: "Number of edited variants (default: 1, max: 10)",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
async execute(input, ctx) {
|
|
117
|
+
const p = (input ?? {});
|
|
118
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
119
|
+
throw new Error("xai: prompt is required");
|
|
120
|
+
}
|
|
121
|
+
const img = readImageInput(p.imagePath, "xai");
|
|
122
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
123
|
+
const model = p.model ?? EDIT_MODEL;
|
|
124
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
Authorization: `Bearer ${apiKey}`,
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
model,
|
|
132
|
+
prompt: p.prompt,
|
|
133
|
+
image: { url: img.dataUri },
|
|
134
|
+
n: Math.min(p.n ?? 1, 10),
|
|
135
|
+
response_format: "b64_json",
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new Error(`xAI API error ${res.status}: ${await res.text()}`);
|
|
140
|
+
}
|
|
141
|
+
const data = (await res.json());
|
|
142
|
+
const stamp = Date.now();
|
|
143
|
+
const images = (data.data ?? []).map((d, i) => ({
|
|
144
|
+
...writeImageFile({ base64: d.b64_json, mimeType: d.mime_type ?? "image/jpeg", provider: "xai", index: i, saveDir: p.saveDir, stamp }),
|
|
145
|
+
...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
|
|
146
|
+
}));
|
|
147
|
+
return { provider: "xai", model, images, note: SEND_FILE_NOTE };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
84
150
|
}
|