runline 0.6.0 → 0.7.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/parseSize.js +26 -0
- package/dist/plugins/googleImage/src/index.js +79 -0
- package/dist/plugins/openai/src/index.js +112 -0
- package/dist/plugins/recraft/src/index.js +100 -0
- package/dist/plugins/replicate/src/index.js +168 -0
- package/dist/plugins/together/src/index.js +93 -0
- package/dist/plugins/xai/src/index.js +77 -0
- package/package.json +2 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a `WxH` string into a { width, height } pair.
|
|
3
|
+
*
|
|
4
|
+
* Used by image-gen plugins (replicate, together, …) that take a
|
|
5
|
+
* single `size` input but talk to APIs which want explicit
|
|
6
|
+
* dimensions. Throws with the caller's plugin name in the message
|
|
7
|
+
* so error output points back at the right tool.
|
|
8
|
+
*
|
|
9
|
+
* parseSize("1024x1024", "replicate") // { width: 1024, height: 1024 }
|
|
10
|
+
* parseSize(undefined, "replicate") // defaults to 1024x1024
|
|
11
|
+
* parseSize("1024", "replicate") // throws
|
|
12
|
+
*/
|
|
13
|
+
export function parseSize(size, pluginName, defaults = { width: 1024, height: 1024 }) {
|
|
14
|
+
if (!size)
|
|
15
|
+
return { ...defaults };
|
|
16
|
+
const parts = size.split("x").map((s) => Number(s.trim()));
|
|
17
|
+
if (parts.length !== 2 ||
|
|
18
|
+
!Number.isFinite(parts[0]) ||
|
|
19
|
+
!Number.isFinite(parts[1])) {
|
|
20
|
+
throw new Error(`${pluginName}: invalid size "${size}", expected WxH`);
|
|
21
|
+
}
|
|
22
|
+
if (parts[0] <= 0 || parts[1] <= 0) {
|
|
23
|
+
throw new Error(`${pluginName}: size dimensions must be positive`);
|
|
24
|
+
}
|
|
25
|
+
return { width: parts[0], height: parts[1] };
|
|
26
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini image generation (Nano Banana / Imagen) for runline.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from the rest of the googleX family — those wrap Workspace
|
|
5
|
+
* APIs over OAuth2, this one wraps Generative Language over a single
|
|
6
|
+
* API key. Kept under the `googleImage` namespace so it doesn't
|
|
7
|
+
* collide with `googleDrive`, `googleDocs`, etc.
|
|
8
|
+
*
|
|
9
|
+
* await googleImage.image.create({ prompt: "a watercolor fox" })
|
|
10
|
+
* await googleImage.image.create({
|
|
11
|
+
* prompt: "edit: make the sky stormier",
|
|
12
|
+
* model: "gemini-3-pro-image-preview",
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* Nano Banana supports conversational editing — chain prompts in
|
|
16
|
+
* follow-up calls and it'll keep iterating on the last image.
|
|
17
|
+
*/
|
|
18
|
+
const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
19
|
+
export default function googleImage(rl) {
|
|
20
|
+
rl.setName("googleImage");
|
|
21
|
+
rl.setVersion("0.1.0");
|
|
22
|
+
rl.setConnectionSchema({
|
|
23
|
+
apiKey: {
|
|
24
|
+
type: "string",
|
|
25
|
+
required: true,
|
|
26
|
+
description: "Google AI API key (Gemini)",
|
|
27
|
+
env: "GOOGLE_API_KEY",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
rl.registerAction("image.create", {
|
|
31
|
+
description: "Generate an image with Google's Gemini image models (Nano Banana / Imagen). Returns base64 bytes per candidate.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
prompt: {
|
|
34
|
+
type: "string",
|
|
35
|
+
required: true,
|
|
36
|
+
description: "Detailed description of the image",
|
|
37
|
+
},
|
|
38
|
+
model: {
|
|
39
|
+
type: "string",
|
|
40
|
+
required: false,
|
|
41
|
+
description: "gemini-2.5-flash-image (Nano Banana, default) | gemini-3-pro-image-preview | gemini-3.1-flash-image-preview",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
async execute(input, ctx) {
|
|
45
|
+
const p = (input ?? {});
|
|
46
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
47
|
+
throw new Error("googleImage: prompt is required");
|
|
48
|
+
}
|
|
49
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
50
|
+
const model = p.model ?? "gemini-2.5-flash-image";
|
|
51
|
+
const url = `${BASE}/${model}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
52
|
+
const body = {
|
|
53
|
+
contents: [{ parts: [{ text: p.prompt }] }],
|
|
54
|
+
generationConfig: { responseModalities: ["IMAGE", "TEXT"] },
|
|
55
|
+
};
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(`Google API error ${res.status}: ${await res.text()}`);
|
|
63
|
+
}
|
|
64
|
+
const data = (await res.json());
|
|
65
|
+
const images = [];
|
|
66
|
+
for (const candidate of data.candidates ?? []) {
|
|
67
|
+
for (const part of candidate.content?.parts ?? []) {
|
|
68
|
+
if (part.inlineData?.data) {
|
|
69
|
+
images.push({
|
|
70
|
+
base64: part.inlineData.data,
|
|
71
|
+
mimeType: part.inlineData.mimeType ?? "image/png",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { provider: "googleImage", model, images };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI image generation for runline.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the GPT Image / DALL-E line at /v1/images/generations and
|
|
5
|
+
* returns base64 bytes alongside the (optional) revised prompt the
|
|
6
|
+
* model wrote for itself.
|
|
7
|
+
*
|
|
8
|
+
* Quality leader for text rendering and prompt adherence — pair with
|
|
9
|
+
* any other plugin you'd compose images for (storyblok, github,
|
|
10
|
+
* notion, slack uploads, …).
|
|
11
|
+
*
|
|
12
|
+
* await openai.image.create({ prompt: "a red bicycle on snow" })
|
|
13
|
+
* await openai.image.create({
|
|
14
|
+
* prompt: "logo for a coffee shop",
|
|
15
|
+
* model: "dall-e-3",
|
|
16
|
+
* style: "vivid",
|
|
17
|
+
* quality: "high",
|
|
18
|
+
* size: "1024x1024",
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
const ENDPOINT = "https://api.openai.com/v1/images/generations";
|
|
22
|
+
export default function openai(rl) {
|
|
23
|
+
rl.setName("openai");
|
|
24
|
+
rl.setVersion("0.1.0");
|
|
25
|
+
rl.setConnectionSchema({
|
|
26
|
+
apiKey: {
|
|
27
|
+
type: "string",
|
|
28
|
+
required: true,
|
|
29
|
+
description: "OpenAI API key",
|
|
30
|
+
env: "OPENAI_API_KEY",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
rl.registerAction("image.create", {
|
|
34
|
+
description: "Generate an image with OpenAI (GPT Image / DALL-E). Returns base64-encoded PNGs and any revised prompt the model produced.",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
prompt: {
|
|
37
|
+
type: "string",
|
|
38
|
+
required: true,
|
|
39
|
+
description: "Detailed description of the image",
|
|
40
|
+
},
|
|
41
|
+
model: {
|
|
42
|
+
type: "string",
|
|
43
|
+
required: false,
|
|
44
|
+
description: "gpt-image-1 (default) | gpt-image-1-mini | dall-e-3 | dall-e-2",
|
|
45
|
+
},
|
|
46
|
+
size: {
|
|
47
|
+
type: "string",
|
|
48
|
+
required: false,
|
|
49
|
+
description: "WxH (default: 1024x1024). Allowed sizes vary by model.",
|
|
50
|
+
},
|
|
51
|
+
quality: {
|
|
52
|
+
type: "string",
|
|
53
|
+
required: false,
|
|
54
|
+
description: "low | medium | high (gpt-image) or standard | hd (dall-e-3)",
|
|
55
|
+
},
|
|
56
|
+
style: {
|
|
57
|
+
type: "string",
|
|
58
|
+
required: false,
|
|
59
|
+
description: "vivid | natural — DALL-E 3 only",
|
|
60
|
+
},
|
|
61
|
+
n: {
|
|
62
|
+
type: "number",
|
|
63
|
+
required: false,
|
|
64
|
+
description: "Number of images (default: 1, max: 4)",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
async execute(input, ctx) {
|
|
68
|
+
const p = (input ?? {});
|
|
69
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
70
|
+
throw new Error("openai: prompt is required");
|
|
71
|
+
}
|
|
72
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
73
|
+
const model = p.model ?? "gpt-image-1";
|
|
74
|
+
const body = {
|
|
75
|
+
model,
|
|
76
|
+
prompt: p.prompt,
|
|
77
|
+
n: Math.min(p.n ?? 1, 4),
|
|
78
|
+
size: p.size ?? "1024x1024",
|
|
79
|
+
};
|
|
80
|
+
// gpt-image-* uses output_format; dall-e-* uses response_format.
|
|
81
|
+
// Sending the wrong key 400s, so the model name decides.
|
|
82
|
+
if (model.startsWith("dall-e")) {
|
|
83
|
+
body.response_format = "b64_json";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
body.output_format = "png";
|
|
87
|
+
}
|
|
88
|
+
if (p.quality)
|
|
89
|
+
body.quality = p.quality;
|
|
90
|
+
if (p.style)
|
|
91
|
+
body.style = p.style;
|
|
92
|
+
const res = await fetch(ENDPOINT, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
Authorization: `Bearer ${apiKey}`,
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
102
|
+
}
|
|
103
|
+
const data = (await res.json());
|
|
104
|
+
const images = (data.data ?? []).map((d) => ({
|
|
105
|
+
base64: d.b64_json,
|
|
106
|
+
mimeType: "image/png",
|
|
107
|
+
...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
|
|
108
|
+
}));
|
|
109
|
+
return { provider: "openai", model, images };
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recraft image generation for runline.
|
|
3
|
+
*
|
|
4
|
+
* await recraft.image.create({ prompt: "minimalist line-art coffee cup" })
|
|
5
|
+
* await recraft.image.create({
|
|
6
|
+
* prompt: "rocket logo, flat vector",
|
|
7
|
+
* model: "recraftv3_vector", // → SVG
|
|
8
|
+
* style: "Vector art",
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* Recraft is the design-oriented provider: vector output, brand-
|
|
12
|
+
* consistent style libraries, typography-aware. V4 models drop the
|
|
13
|
+
* legacy `style` knob — pass `styleId` against your own custom
|
|
14
|
+
* style if you've set one up.
|
|
15
|
+
*/
|
|
16
|
+
const ENDPOINT = "https://external.api.recraft.ai/v1/images/generations";
|
|
17
|
+
export default function recraft(rl) {
|
|
18
|
+
rl.setName("recraft");
|
|
19
|
+
rl.setVersion("0.1.0");
|
|
20
|
+
rl.setConnectionSchema({
|
|
21
|
+
apiKey: {
|
|
22
|
+
type: "string",
|
|
23
|
+
required: true,
|
|
24
|
+
description: "Recraft API key",
|
|
25
|
+
env: "RECRAFT_API_KEY",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
rl.registerAction("image.create", {
|
|
29
|
+
description: "Generate an image with Recraft. Best for design, vector graphics, and brand-consistent work. Returns base64-encoded PNGs.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
prompt: {
|
|
32
|
+
type: "string",
|
|
33
|
+
required: true,
|
|
34
|
+
description: "Detailed description of the image",
|
|
35
|
+
},
|
|
36
|
+
model: {
|
|
37
|
+
type: "string",
|
|
38
|
+
required: false,
|
|
39
|
+
description: "recraftv3 (default) | recraftv3_vector | recraftv4 | recraftv4_pro | recraftv4_vector | recraftv4_pro_vector",
|
|
40
|
+
},
|
|
41
|
+
style: {
|
|
42
|
+
type: "string",
|
|
43
|
+
required: false,
|
|
44
|
+
description: "V2/V3 only — Photorealism | Illustration | Vector art | Hand-drawn | Icon | Recraft V3 Raw",
|
|
45
|
+
},
|
|
46
|
+
styleId: {
|
|
47
|
+
type: "string",
|
|
48
|
+
required: false,
|
|
49
|
+
description: "ID of a custom style created in your Recraft account",
|
|
50
|
+
},
|
|
51
|
+
size: {
|
|
52
|
+
type: "string",
|
|
53
|
+
required: false,
|
|
54
|
+
description: "WxH (default: 1024x1024). E.g. 1280x1024, 1024x1280",
|
|
55
|
+
},
|
|
56
|
+
n: {
|
|
57
|
+
type: "number",
|
|
58
|
+
required: false,
|
|
59
|
+
description: "Number of images (default: 1, max: 6)",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
async execute(input, ctx) {
|
|
63
|
+
const p = (input ?? {});
|
|
64
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
65
|
+
throw new Error("recraft: prompt is required");
|
|
66
|
+
}
|
|
67
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
68
|
+
const model = p.model ?? "recraftv3";
|
|
69
|
+
const body = {
|
|
70
|
+
prompt: p.prompt,
|
|
71
|
+
model,
|
|
72
|
+
response_format: "b64_json",
|
|
73
|
+
n: Math.min(p.n ?? 1, 6),
|
|
74
|
+
};
|
|
75
|
+
if (p.size)
|
|
76
|
+
body.size = p.size;
|
|
77
|
+
if (p.style)
|
|
78
|
+
body.style = p.style;
|
|
79
|
+
if (p.styleId)
|
|
80
|
+
body.style_id = p.styleId;
|
|
81
|
+
const res = await fetch(ENDPOINT, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
Authorization: `Bearer ${apiKey}`,
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(`Recraft API error ${res.status}: ${await res.text()}`);
|
|
91
|
+
}
|
|
92
|
+
const data = (await res.json());
|
|
93
|
+
const images = (data.data ?? []).map((d) => ({
|
|
94
|
+
base64: d.b64_json,
|
|
95
|
+
mimeType: "image/png",
|
|
96
|
+
}));
|
|
97
|
+
return { provider: "recraft", model, images };
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replicate image generation for runline.
|
|
3
|
+
*
|
|
4
|
+
* await replicate.image.create({ prompt: "a samurai under cherry blossoms" })
|
|
5
|
+
* await replicate.image.create({
|
|
6
|
+
* prompt: "studio portrait, soft lighting",
|
|
7
|
+
* model: "stability-ai/stable-diffusion-3.5-large",
|
|
8
|
+
* size: "1024x1024",
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* Default model is black-forest-labs/flux-dev. Any text-to-image
|
|
12
|
+
* Replicate model that accepts `prompt` / `width` / `height` /
|
|
13
|
+
* `num_outputs` works the same way.
|
|
14
|
+
*
|
|
15
|
+
* Predictions are created with `Prefer: wait` so simple jobs return
|
|
16
|
+
* synchronously; anything still processing is polled until terminal
|
|
17
|
+
* or until `timeoutMs` elapses (default: 5 minutes). Output URLs are
|
|
18
|
+
* downloaded and base64-encoded so callers don't have to fetch them
|
|
19
|
+
* separately.
|
|
20
|
+
*/
|
|
21
|
+
import { Buffer } from "node:buffer";
|
|
22
|
+
import { parseSize } from "../../_shared/parseSize.js";
|
|
23
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
25
|
+
function stringifyError(err) {
|
|
26
|
+
if (err == null)
|
|
27
|
+
return "unknown error";
|
|
28
|
+
if (typeof err === "string")
|
|
29
|
+
return err;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(err);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return String(err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export default function replicate(rl) {
|
|
38
|
+
rl.setName("replicate");
|
|
39
|
+
rl.setVersion("0.1.0");
|
|
40
|
+
rl.setConnectionSchema({
|
|
41
|
+
apiToken: {
|
|
42
|
+
type: "string",
|
|
43
|
+
required: true,
|
|
44
|
+
description: "Replicate API token",
|
|
45
|
+
env: "REPLICATE_API_TOKEN",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
rl.registerAction("image.create", {
|
|
49
|
+
description: "Generate an image via Replicate. Default model is black-forest-labs/flux-dev. Returns base64 bytes from the model's output URLs.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
prompt: {
|
|
52
|
+
type: "string",
|
|
53
|
+
required: true,
|
|
54
|
+
description: "Detailed description of the image",
|
|
55
|
+
},
|
|
56
|
+
model: {
|
|
57
|
+
type: "string",
|
|
58
|
+
required: false,
|
|
59
|
+
description: "Replicate model id, e.g. black-forest-labs/flux-dev (default), black-forest-labs/flux-schnell, stability-ai/stable-diffusion-3.5-large",
|
|
60
|
+
},
|
|
61
|
+
size: {
|
|
62
|
+
type: "string",
|
|
63
|
+
required: false,
|
|
64
|
+
description: "WxH (default: 1024x1024)",
|
|
65
|
+
},
|
|
66
|
+
n: {
|
|
67
|
+
type: "number",
|
|
68
|
+
required: false,
|
|
69
|
+
description: "Number of images (default: 1, max: 4)",
|
|
70
|
+
},
|
|
71
|
+
timeoutMs: {
|
|
72
|
+
type: "number",
|
|
73
|
+
required: false,
|
|
74
|
+
description: "Max ms to wait for the prediction to finish (default: 300000 = 5 minutes)",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
async execute(input, ctx) {
|
|
78
|
+
const p = (input ?? {});
|
|
79
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
80
|
+
throw new Error("replicate: prompt is required");
|
|
81
|
+
}
|
|
82
|
+
const apiToken = ctx.connection.config.apiToken;
|
|
83
|
+
const model = p.model ?? "black-forest-labs/flux-dev";
|
|
84
|
+
const { width, height } = parseSize(p.size, "replicate");
|
|
85
|
+
const timeoutMs = p.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
const createRes = await fetch(`https://api.replicate.com/v1/models/${model}/predictions`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Authorization: `Bearer ${apiToken}`,
|
|
92
|
+
// `Prefer: wait` lets the server hold the connection open for
|
|
93
|
+
// fast jobs so we don't have to poll at all on the happy path.
|
|
94
|
+
Prefer: "wait",
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
input: {
|
|
98
|
+
prompt: p.prompt,
|
|
99
|
+
width,
|
|
100
|
+
height,
|
|
101
|
+
num_outputs: Math.min(p.n ?? 1, 4),
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
if (!createRes.ok) {
|
|
106
|
+
throw new Error(`Replicate API error ${createRes.status}: ${await createRes.text()}`);
|
|
107
|
+
}
|
|
108
|
+
let prediction = (await createRes.json());
|
|
109
|
+
while (prediction.status !== "succeeded" &&
|
|
110
|
+
prediction.status !== "failed" &&
|
|
111
|
+
prediction.status !== "canceled") {
|
|
112
|
+
if (Date.now() >= deadline) {
|
|
113
|
+
throw new Error(`Replicate generation timed out after ${timeoutMs}ms (still ${prediction.status})`);
|
|
114
|
+
}
|
|
115
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
116
|
+
const pollRes = await fetch(prediction.urls.get, {
|
|
117
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
118
|
+
});
|
|
119
|
+
if (!pollRes.ok) {
|
|
120
|
+
throw new Error(`Replicate poll error ${pollRes.status}: ${await pollRes.text()}`);
|
|
121
|
+
}
|
|
122
|
+
prediction = (await pollRes.json());
|
|
123
|
+
}
|
|
124
|
+
if (prediction.status !== "succeeded") {
|
|
125
|
+
throw new Error(`Replicate generation ${prediction.status}: ${stringifyError(prediction.error)}`);
|
|
126
|
+
}
|
|
127
|
+
// Output is either a single URL or an array of them. Download
|
|
128
|
+
// each and base64-encode so the caller gets bytes back, not
|
|
129
|
+
// pre-signed URLs that expire. Track per-URL failures and
|
|
130
|
+
// surface them: silent partial success would let an agent
|
|
131
|
+
// think it got 3 images when one 404'd.
|
|
132
|
+
const outputs = Array.isArray(prediction.output)
|
|
133
|
+
? prediction.output
|
|
134
|
+
: prediction.output
|
|
135
|
+
? [prediction.output]
|
|
136
|
+
: [];
|
|
137
|
+
const images = [];
|
|
138
|
+
const failures = [];
|
|
139
|
+
for (const url of outputs) {
|
|
140
|
+
if (typeof url !== "string") {
|
|
141
|
+
failures.push({ url: String(url), reason: "non-string output" });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const imgRes = await fetch(url);
|
|
145
|
+
if (!imgRes.ok) {
|
|
146
|
+
failures.push({
|
|
147
|
+
url,
|
|
148
|
+
reason: `download failed (${imgRes.status})`,
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const buf = Buffer.from(await imgRes.arrayBuffer());
|
|
153
|
+
const contentType = (imgRes.headers.get("content-type") ?? "image/webp")
|
|
154
|
+
.split(";")[0]
|
|
155
|
+
.trim();
|
|
156
|
+
images.push({ base64: buf.toString("base64"), mimeType: contentType });
|
|
157
|
+
}
|
|
158
|
+
if (images.length === 0 && outputs.length > 0) {
|
|
159
|
+
const detail = failures.map((f) => `${f.url}: ${f.reason}`).join("; ");
|
|
160
|
+
throw new Error(`Replicate succeeded but all ${outputs.length} output URLs failed to download — ${detail}`);
|
|
161
|
+
}
|
|
162
|
+
const result = { provider: "replicate", model, images };
|
|
163
|
+
if (failures.length > 0)
|
|
164
|
+
result.failures = failures;
|
|
165
|
+
return result;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Together AI image generation for runline.
|
|
3
|
+
*
|
|
4
|
+
* await together.image.create({ prompt: "a cyberpunk skyline at dusk" })
|
|
5
|
+
* await together.image.create({
|
|
6
|
+
* prompt: "studio product shot, white background",
|
|
7
|
+
* model: "black-forest-labs/FLUX.1-dev",
|
|
8
|
+
* steps: 28,
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* Default model is FLUX.1-schnell — fastest, 4 steps. For better
|
|
12
|
+
* fidelity switch to FLUX.1-dev / Ideogram / Qwen-Image and bump
|
|
13
|
+
* `steps` accordingly (20–30 is typical for non-schnell models).
|
|
14
|
+
*/
|
|
15
|
+
import { parseSize } from "../../_shared/parseSize.js";
|
|
16
|
+
const ENDPOINT = "https://api.together.xyz/v1/images/generations";
|
|
17
|
+
export default function together(rl) {
|
|
18
|
+
rl.setName("together");
|
|
19
|
+
rl.setVersion("0.1.0");
|
|
20
|
+
rl.setConnectionSchema({
|
|
21
|
+
apiKey: {
|
|
22
|
+
type: "string",
|
|
23
|
+
required: true,
|
|
24
|
+
description: "Together AI API key",
|
|
25
|
+
env: "TOGETHER_API_KEY",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
rl.registerAction("image.create", {
|
|
29
|
+
description: "Generate an image with Together AI (Flux, Ideogram, Qwen-Image, …). Returns base64-encoded PNGs.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
prompt: {
|
|
32
|
+
type: "string",
|
|
33
|
+
required: true,
|
|
34
|
+
description: "Detailed description of the image",
|
|
35
|
+
},
|
|
36
|
+
model: {
|
|
37
|
+
type: "string",
|
|
38
|
+
required: false,
|
|
39
|
+
description: "Together model id, e.g. black-forest-labs/FLUX.1-schnell (default), black-forest-labs/FLUX.1-dev, ideogram/ideogram-3.0, Qwen/Qwen-Image",
|
|
40
|
+
},
|
|
41
|
+
size: {
|
|
42
|
+
type: "string",
|
|
43
|
+
required: false,
|
|
44
|
+
description: "WxH (default: 1024x1024)",
|
|
45
|
+
},
|
|
46
|
+
steps: {
|
|
47
|
+
type: "number",
|
|
48
|
+
required: false,
|
|
49
|
+
description: "Inference steps (default: 4 for FLUX.1-schnell). Use 20–30 for FLUX.1-dev or Ideogram.",
|
|
50
|
+
},
|
|
51
|
+
n: {
|
|
52
|
+
type: "number",
|
|
53
|
+
required: false,
|
|
54
|
+
description: "Number of images (default: 1, max: 4)",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async execute(input, ctx) {
|
|
58
|
+
const p = (input ?? {});
|
|
59
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
60
|
+
throw new Error("together: prompt is required");
|
|
61
|
+
}
|
|
62
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
63
|
+
const model = p.model ?? "black-forest-labs/FLUX.1-schnell";
|
|
64
|
+
const { width, height } = parseSize(p.size, "together");
|
|
65
|
+
const body = {
|
|
66
|
+
model,
|
|
67
|
+
prompt: p.prompt,
|
|
68
|
+
width,
|
|
69
|
+
height,
|
|
70
|
+
steps: p.steps ?? 4,
|
|
71
|
+
n: Math.min(p.n ?? 1, 4),
|
|
72
|
+
response_format: "base64",
|
|
73
|
+
};
|
|
74
|
+
const res = await fetch(ENDPOINT, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
Authorization: `Bearer ${apiKey}`,
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify(body),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error(`Together API error ${res.status}: ${await res.text()}`);
|
|
84
|
+
}
|
|
85
|
+
const data = (await res.json());
|
|
86
|
+
const images = (data.data ?? []).map((d) => ({
|
|
87
|
+
base64: d.b64_json,
|
|
88
|
+
mimeType: "image/png",
|
|
89
|
+
}));
|
|
90
|
+
return { provider: "together", model, images };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xAI image generation (Grok Imagine / Aurora) for runline.
|
|
3
|
+
*
|
|
4
|
+
* await xai.image.create({ prompt: "ultra-realistic close-up of a dragonfly" })
|
|
5
|
+
* await xai.image.create({ prompt: "movie poster", aspectRatio: "9:16" })
|
|
6
|
+
*
|
|
7
|
+
* Aurora leans photorealistic and handles real-world entities and
|
|
8
|
+
* text rendering well. Sized via aspect_ratio rather than W×H —
|
|
9
|
+
* the API does the math.
|
|
10
|
+
*/
|
|
11
|
+
const ENDPOINT = "https://api.x.ai/v1/images/generations";
|
|
12
|
+
const MODEL = "grok-imagine-image";
|
|
13
|
+
export default function xai(rl) {
|
|
14
|
+
rl.setName("xai");
|
|
15
|
+
rl.setVersion("0.1.0");
|
|
16
|
+
rl.setConnectionSchema({
|
|
17
|
+
apiKey: {
|
|
18
|
+
type: "string",
|
|
19
|
+
required: true,
|
|
20
|
+
description: "xAI API key",
|
|
21
|
+
env: "XAI_API_KEY",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
rl.registerAction("image.create", {
|
|
25
|
+
description: "Generate an image with xAI Grok Imagine (Aurora). Returns base64-encoded JPEGs and any revised prompt the model produced.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
prompt: {
|
|
28
|
+
type: "string",
|
|
29
|
+
required: true,
|
|
30
|
+
description: "Detailed description of the image",
|
|
31
|
+
},
|
|
32
|
+
aspectRatio: {
|
|
33
|
+
type: "string",
|
|
34
|
+
required: false,
|
|
35
|
+
description: "1:1 | 16:9 | 9:16 | 4:3 | 3:4 | 3:2 | 2:3 | auto (default: auto)",
|
|
36
|
+
},
|
|
37
|
+
n: {
|
|
38
|
+
type: "number",
|
|
39
|
+
required: false,
|
|
40
|
+
description: "Number of images (default: 1, max: 10)",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async execute(input, ctx) {
|
|
44
|
+
const p = (input ?? {});
|
|
45
|
+
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
46
|
+
throw new Error("xai: prompt is required");
|
|
47
|
+
}
|
|
48
|
+
const apiKey = ctx.connection.config.apiKey;
|
|
49
|
+
const body = {
|
|
50
|
+
model: MODEL,
|
|
51
|
+
prompt: p.prompt,
|
|
52
|
+
n: Math.min(p.n ?? 1, 10),
|
|
53
|
+
response_format: "b64_json",
|
|
54
|
+
};
|
|
55
|
+
if (p.aspectRatio)
|
|
56
|
+
body.aspect_ratio = p.aspectRatio;
|
|
57
|
+
const res = await fetch(ENDPOINT, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Authorization: `Bearer ${apiKey}`,
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
throw new Error(`xAI API error ${res.status}: ${await res.text()}`);
|
|
67
|
+
}
|
|
68
|
+
const data = (await res.json());
|
|
69
|
+
const images = (data.data ?? []).map((d) => ({
|
|
70
|
+
base64: d.b64_json,
|
|
71
|
+
mimeType: "image/jpeg",
|
|
72
|
+
...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
|
|
73
|
+
}));
|
|
74
|
+
return { provider: "xai", model: MODEL, images };
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Code mode for agents
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Code mode for agents — turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|