runline 0.27.1 → 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/core/engine.js +95 -1
- 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
package/dist/core/engine.js
CHANGED
|
@@ -459,6 +459,12 @@ function formatError(cause) {
|
|
|
459
459
|
// code gets a cwd-relative `require`, so a bare specifier would resolve
|
|
460
460
|
// against the caller's cwd instead of runline's own node_modules.
|
|
461
461
|
const ferrosearchPath = createRequire(import.meta.url).resolve("@shift-labs/ferrosearch");
|
|
462
|
+
// typebox ships ESM only; require() of an .mjs works everywhere runline
|
|
463
|
+
// runs (bun, and node since 22.12's require(esm)). Resolved host-side
|
|
464
|
+
// for the same reason as ferrosearch: eval'd worker code gets a
|
|
465
|
+
// cwd-relative require, so a bare specifier would resolve against the
|
|
466
|
+
// caller's cwd instead of runline's own node_modules.
|
|
467
|
+
const typeboxValuePath = createRequire(import.meta.url).resolve("typebox/value");
|
|
462
468
|
function buildHelpData(plugins) {
|
|
463
469
|
const data = {};
|
|
464
470
|
for (const p of plugins) {
|
|
@@ -467,6 +473,7 @@ function buildHelpData(plugins) {
|
|
|
467
473
|
access: a.access,
|
|
468
474
|
description: a.description,
|
|
469
475
|
inputs: helpInputs(a.inputSchema),
|
|
476
|
+
...(isTypedInputSchema(a.inputSchema) ? { schema: a.inputSchema } : {}),
|
|
470
477
|
}));
|
|
471
478
|
}
|
|
472
479
|
return data;
|
|
@@ -594,6 +601,9 @@ const __toJson = (v) => v === undefined ? undefined : JSON.parse(JSON.stringify(
|
|
|
594
601
|
// also means an index built by agent code is invisible to that limit.
|
|
595
602
|
const { FerroSearch } = require(${JSON.stringify(ferrosearchPath)});
|
|
596
603
|
|
|
604
|
+
// The host's own validator, for actions.check on typed schemas.
|
|
605
|
+
const { Check: __schemaCheck, Errors: __schemaErrors } = require(${JSON.stringify(typeboxValuePath)});
|
|
606
|
+
|
|
597
607
|
const __help = ${JSON.stringify(helpData)};
|
|
598
608
|
|
|
599
609
|
const __makeProxy = (path = []) => new Proxy(() => undefined, {
|
|
@@ -653,6 +663,82 @@ const __search = (() => {
|
|
|
653
663
|
return index;
|
|
654
664
|
})();
|
|
655
665
|
|
|
666
|
+
// instancePath "/members/0/externalId" → ["members", "0", "externalId"],
|
|
667
|
+
// with JSON-pointer escapes (~1 for /, ~0 for ~) undone per part.
|
|
668
|
+
const __pointerParts = (instancePath) =>
|
|
669
|
+
String(instancePath || '').split('/').filter((p) => p.length)
|
|
670
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
671
|
+
|
|
672
|
+
// ["members", "0", "externalId"] → "members[0].externalId"
|
|
673
|
+
const __fieldPath = (instancePath, prop) => {
|
|
674
|
+
const parts = __pointerParts(instancePath);
|
|
675
|
+
if (prop !== undefined) parts.push(String(prop));
|
|
676
|
+
let out = '';
|
|
677
|
+
for (const part of parts) {
|
|
678
|
+
if (/^\\d+$/.test(part)) out += '[' + part + ']';
|
|
679
|
+
else out += out ? '.' + part : part;
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const __valueAt = (root, instancePath) => {
|
|
685
|
+
let v = root;
|
|
686
|
+
for (const part of __pointerParts(instancePath)) {
|
|
687
|
+
if (v == null) return undefined;
|
|
688
|
+
v = v[part];
|
|
689
|
+
}
|
|
690
|
+
return v;
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const __valueType = (v) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v;
|
|
694
|
+
|
|
695
|
+
// Deep validation for typed schemas (SHFT-1187): the same Check/Errors
|
|
696
|
+
// the host runs before execute, on the same schema, so the preflight
|
|
697
|
+
// verdict and the execution verdict cannot diverge. The familiar
|
|
698
|
+
// missing/unknown/typeErrors buckets are derived from the validator's
|
|
699
|
+
// structured errors — keyword and params, never message parsing — and
|
|
700
|
+
// the errors array carries each raw "<path> <message>" line exactly as
|
|
701
|
+
// a rejected execution would report it. Grouping type errors by instance
|
|
702
|
+
// path keeps a failed union one typeError instead of one per branch.
|
|
703
|
+
const __checkTyped = (entry, provided, signature) => {
|
|
704
|
+
if (__schemaCheck(entry.schema, provided)) {
|
|
705
|
+
return { ok: true, missing: [], unknown: [], typeErrors: [], errors: [], signature };
|
|
706
|
+
}
|
|
707
|
+
const missing = [];
|
|
708
|
+
const unknown = [];
|
|
709
|
+
const errors = [];
|
|
710
|
+
const byPath = new Map();
|
|
711
|
+
for (const err of __schemaErrors(entry.schema, provided)) {
|
|
712
|
+
errors.push((err.instancePath || '/') + ' ' + err.message);
|
|
713
|
+
const params = err.params || {};
|
|
714
|
+
if (err.keyword === 'required') {
|
|
715
|
+
for (const prop of params.requiredProperties || []) missing.push(__fieldPath(err.instancePath, prop));
|
|
716
|
+
} else if (err.keyword === 'additionalProperties') {
|
|
717
|
+
for (const prop of params.additionalProperties || []) unknown.push(__fieldPath(err.instancePath, prop));
|
|
718
|
+
} else {
|
|
719
|
+
const group = byPath.get(err.instancePath) || [];
|
|
720
|
+
group.push(err);
|
|
721
|
+
byPath.set(err.instancePath, group);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const typeErrors = [];
|
|
725
|
+
for (const [instancePath, group] of byPath) {
|
|
726
|
+
const field = __fieldPath(instancePath) || '(input)';
|
|
727
|
+
const value = __valueAt(provided, instancePath);
|
|
728
|
+
const consts = group.filter((e) => e.keyword === 'const' && 'allowedValue' in (e.params || {}));
|
|
729
|
+
const typeErr = group.find((e) => e.keyword === 'type' && (e.params || {}).type);
|
|
730
|
+
if (consts.length) {
|
|
731
|
+
// A union of literals: the branches ARE the allowed values.
|
|
732
|
+
typeErrors.push({ field, expected: consts.map((e) => String(e.params.allowedValue)).join(' | '), actual: __fmt(value) });
|
|
733
|
+
} else if (typeErr) {
|
|
734
|
+
typeErrors.push({ field, expected: String(typeErr.params.type), actual: __valueType(value) });
|
|
735
|
+
} else {
|
|
736
|
+
typeErrors.push({ field, expected: group[0].message, actual: __fmt(value) });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return { ok: false, missing, unknown, typeErrors, errors, signature };
|
|
740
|
+
};
|
|
741
|
+
|
|
656
742
|
const __actionsApi = {
|
|
657
743
|
list(plugin) {
|
|
658
744
|
const paths = Object.keys(__index);
|
|
@@ -691,6 +777,13 @@ const __actionsApi = {
|
|
|
691
777
|
const near = __actionsApi.find(path, 3).map((n) => n.path);
|
|
692
778
|
return { ok: false, error: 'Unknown action: ' + path, suggestions: near };
|
|
693
779
|
}
|
|
780
|
+
const signature = __formatSignature(hit.plugin, hit.entry);
|
|
781
|
+
if (hit.entry.schema) {
|
|
782
|
+
// Mirror the host's normalizeActionInput: a check with no args is
|
|
783
|
+
// a call with an empty argument set; anything else — null, a
|
|
784
|
+
// scalar — is validated as given, exactly as execute would.
|
|
785
|
+
return __checkTyped(hit.entry, args === undefined ? {} : args, signature);
|
|
786
|
+
}
|
|
694
787
|
const inputs = hit.entry.inputs || {};
|
|
695
788
|
const provided = args && typeof args === 'object' ? args : {};
|
|
696
789
|
const missing = [];
|
|
@@ -719,7 +812,8 @@ const __actionsApi = {
|
|
|
719
812
|
missing,
|
|
720
813
|
unknown,
|
|
721
814
|
typeErrors,
|
|
722
|
-
|
|
815
|
+
errors: [],
|
|
816
|
+
signature,
|
|
723
817
|
};
|
|
724
818
|
},
|
|
725
819
|
};
|
|
@@ -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
|
}
|