@blockrun/clawrouter 0.12.32 → 0.12.34
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/README.md +41 -12
- package/dist/cli.js +288 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +299 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/imagegen/SKILL.md +7 -7
- package/skills/x-api/SKILL.md +5 -5
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ One wallet, 41+ models, zero API keys.
|
|
|
48
48
|
| [vs OpenRouter](#-vs-openrouter) | Why ClawRouter wins |
|
|
49
49
|
| [Support](#-support) | Telegram, X, founders |
|
|
50
50
|
|
|
51
|
-
**API Docs:** [Image Generation](docs/image-generation.md) · [Architecture](docs/architecture.md) · [Configuration](docs/configuration.md)
|
|
51
|
+
**API Docs:** [Image Generation & Editing](docs/image-generation.md) · [Architecture](docs/architecture.md) · [Configuration](docs/configuration.md)
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
@@ -102,6 +102,35 @@ Generate images directly from chat with `/imagegen`:
|
|
|
102
102
|
|
|
103
103
|
Default model: `nano-banana`. Images are returned as hosted URLs for compatibility with Telegram, Discord, and other clients.
|
|
104
104
|
|
|
105
|
+
## ✏️ Image Editing (img2img)
|
|
106
|
+
|
|
107
|
+
Edit existing images with `/img2img` — pass a local file and describe what to change:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
/img2img --image ~/photo.png change the background to a starry sky
|
|
111
|
+
/img2img --image ./cat.jpg --mask ./mask.png remove the background
|
|
112
|
+
/img2img --image /tmp/portrait.png --size 1536x1024 add a hat
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Option | Required | Description |
|
|
116
|
+
| ----------------- | -------- | ------------------------------------- |
|
|
117
|
+
| `--image <path>` | Yes | Local image file path (supports `~/`) |
|
|
118
|
+
| `--mask <path>` | No | Mask image (white = area to edit) |
|
|
119
|
+
| `--model <model>` | No | Model to use (default: `gpt-image-1`) |
|
|
120
|
+
| `--size <WxH>` | No | Output size (default: `1024x1024`) |
|
|
121
|
+
|
|
122
|
+
Supported model: `gpt-image-1` (OpenAI GPT Image 1, $0.02/image).
|
|
123
|
+
|
|
124
|
+
**API endpoint:** `POST http://localhost:8402/v1/images/image2image` — accepts local file paths, URLs, or base64 data URIs:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
curl -X POST http://localhost:8402/v1/images/image2image \
|
|
128
|
+
-H "Content-Type: application/json" \
|
|
129
|
+
-d '{"prompt":"add sunglasses","image":"~/photo.png"}'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
See [Image Generation & Editing docs](docs/image-generation.md#post-v1imagesimage2image) for full API reference and code examples.
|
|
133
|
+
|
|
105
134
|
---
|
|
106
135
|
|
|
107
136
|
## ⚡ How It Works
|
|
@@ -330,17 +359,17 @@ npm test
|
|
|
330
359
|
|
|
331
360
|
## 📚 More Resources
|
|
332
361
|
|
|
333
|
-
| Resource
|
|
334
|
-
|
|
|
335
|
-
| [Documentation](https://blockrun.ai/docs)
|
|
336
|
-
| [Model Pricing](https://blockrun.ai/models)
|
|
337
|
-
| [Image Generation](docs/image-generation.md) | API examples, 5 models |
|
|
338
|
-
| [Routing Profiles](docs/routing-profiles.md)
|
|
339
|
-
| [Architecture](docs/architecture.md)
|
|
340
|
-
| [Configuration](docs/configuration.md)
|
|
341
|
-
| [vs OpenRouter](docs/vs-openrouter.md)
|
|
342
|
-
| [Features](docs/features.md)
|
|
343
|
-
| [Troubleshooting](docs/troubleshooting.md)
|
|
362
|
+
| Resource | Description |
|
|
363
|
+
| ------------------------------------------------------ | ------------------------ |
|
|
364
|
+
| [Documentation](https://blockrun.ai/docs) | Full docs |
|
|
365
|
+
| [Model Pricing](https://blockrun.ai/models) | All models & prices |
|
|
366
|
+
| [Image Generation & Editing](docs/image-generation.md) | API examples, 5 models |
|
|
367
|
+
| [Routing Profiles](docs/routing-profiles.md) | ECO/AUTO/PREMIUM details |
|
|
368
|
+
| [Architecture](docs/architecture.md) | Technical deep dive |
|
|
369
|
+
| [Configuration](docs/configuration.md) | Environment variables |
|
|
370
|
+
| [vs OpenRouter](docs/vs-openrouter.md) | Why ClawRouter wins |
|
|
371
|
+
| [Features](docs/features.md) | All features |
|
|
372
|
+
| [Troubleshooting](docs/troubleshooting.md) | Common issues |
|
|
344
373
|
|
|
345
374
|
---
|
|
346
375
|
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { finished } from "stream";
|
|
|
6
6
|
import { homedir as homedir4 } from "os";
|
|
7
7
|
import { join as join5 } from "path";
|
|
8
8
|
import { mkdir as mkdir3, writeFile as writeFile2, readFile, stat as fsStat } from "fs/promises";
|
|
9
|
+
import { readFileSync, existsSync } from "fs";
|
|
9
10
|
import { createPublicClient as createPublicClient2, http as http2 } from "viem";
|
|
10
11
|
import { base as base2 } from "viem/chains";
|
|
11
12
|
import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
@@ -5614,6 +5615,22 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
|
|
|
5614
5615
|
}).catch(() => {
|
|
5615
5616
|
});
|
|
5616
5617
|
}
|
|
5618
|
+
function readImageFileAsDataUri(filePath) {
|
|
5619
|
+
const resolved = filePath.startsWith("~/") ? join5(homedir4(), filePath.slice(2)) : filePath;
|
|
5620
|
+
if (!existsSync(resolved)) {
|
|
5621
|
+
throw new Error(`Image file not found: ${resolved}`);
|
|
5622
|
+
}
|
|
5623
|
+
const ext = resolved.split(".").pop()?.toLowerCase() ?? "png";
|
|
5624
|
+
const mimeMap = {
|
|
5625
|
+
png: "image/png",
|
|
5626
|
+
jpg: "image/jpeg",
|
|
5627
|
+
jpeg: "image/jpeg",
|
|
5628
|
+
webp: "image/webp"
|
|
5629
|
+
};
|
|
5630
|
+
const mime = mimeMap[ext] ?? "image/png";
|
|
5631
|
+
const data = readFileSync(resolved);
|
|
5632
|
+
return `data:${mime};base64,${data.toString("base64")}`;
|
|
5633
|
+
}
|
|
5617
5634
|
async function uploadDataUriToHost(dataUri) {
|
|
5618
5635
|
const match = dataUri.match(/^data:(image\/\w+);base64,(.+)$/);
|
|
5619
5636
|
if (!match) throw new Error("Invalid data URI format");
|
|
@@ -5908,7 +5925,9 @@ async function startProxy(options) {
|
|
|
5908
5925
|
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
5909
5926
|
}
|
|
5910
5927
|
} catch (downloadErr) {
|
|
5911
|
-
console.warn(
|
|
5928
|
+
console.warn(
|
|
5929
|
+
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
5930
|
+
);
|
|
5912
5931
|
}
|
|
5913
5932
|
}
|
|
5914
5933
|
}
|
|
@@ -5925,6 +5944,103 @@ async function startProxy(options) {
|
|
|
5925
5944
|
}
|
|
5926
5945
|
return;
|
|
5927
5946
|
}
|
|
5947
|
+
if (req.url === "/v1/images/image2image" && req.method === "POST") {
|
|
5948
|
+
const chunks = [];
|
|
5949
|
+
for await (const chunk of req) {
|
|
5950
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5951
|
+
}
|
|
5952
|
+
const rawBody = Buffer.concat(chunks);
|
|
5953
|
+
let reqBody;
|
|
5954
|
+
try {
|
|
5955
|
+
const parsed = JSON.parse(rawBody.toString());
|
|
5956
|
+
for (const field of ["image", "mask"]) {
|
|
5957
|
+
const val = parsed[field];
|
|
5958
|
+
if (typeof val !== "string" || !val) continue;
|
|
5959
|
+
if (val.startsWith("data:")) {
|
|
5960
|
+
} else if (val.startsWith("https://") || val.startsWith("http://")) {
|
|
5961
|
+
const imgResp = await fetch(val);
|
|
5962
|
+
if (!imgResp.ok) throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
|
|
5963
|
+
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
5964
|
+
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
5965
|
+
parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
|
|
5966
|
+
console.log(`[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`);
|
|
5967
|
+
} else {
|
|
5968
|
+
parsed[field] = readImageFileAsDataUri(val);
|
|
5969
|
+
console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
if (!parsed.model) parsed.model = "openai/gpt-image-1";
|
|
5973
|
+
reqBody = JSON.stringify(parsed);
|
|
5974
|
+
} catch (parseErr) {
|
|
5975
|
+
const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
5976
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5977
|
+
res.end(JSON.stringify({ error: "Invalid request", details: msg }));
|
|
5978
|
+
return;
|
|
5979
|
+
}
|
|
5980
|
+
try {
|
|
5981
|
+
const upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
5982
|
+
method: "POST",
|
|
5983
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
5984
|
+
body: reqBody
|
|
5985
|
+
});
|
|
5986
|
+
const text = await upstream.text();
|
|
5987
|
+
if (!upstream.ok) {
|
|
5988
|
+
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
5989
|
+
res.end(text);
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5992
|
+
let result;
|
|
5993
|
+
try {
|
|
5994
|
+
result = JSON.parse(text);
|
|
5995
|
+
} catch {
|
|
5996
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5997
|
+
res.end(text);
|
|
5998
|
+
return;
|
|
5999
|
+
}
|
|
6000
|
+
if (result.data?.length) {
|
|
6001
|
+
await mkdir3(IMAGE_DIR, { recursive: true });
|
|
6002
|
+
const port2 = server.address()?.port ?? 8402;
|
|
6003
|
+
for (const img of result.data) {
|
|
6004
|
+
const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
|
|
6005
|
+
if (dataUriMatch) {
|
|
6006
|
+
const [, mimeType, b64] = dataUriMatch;
|
|
6007
|
+
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
|
|
6008
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
6009
|
+
await writeFile2(join5(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
|
|
6010
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
6011
|
+
console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
|
|
6012
|
+
} else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
|
|
6013
|
+
try {
|
|
6014
|
+
const imgResp = await fetch(img.url);
|
|
6015
|
+
if (imgResp.ok) {
|
|
6016
|
+
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
6017
|
+
const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
6018
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
6019
|
+
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
6020
|
+
await writeFile2(join5(IMAGE_DIR, filename), buf);
|
|
6021
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
6022
|
+
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
6023
|
+
}
|
|
6024
|
+
} catch (downloadErr) {
|
|
6025
|
+
console.warn(
|
|
6026
|
+
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
6027
|
+
);
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6033
|
+
res.end(JSON.stringify(result));
|
|
6034
|
+
} catch (err) {
|
|
6035
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6036
|
+
console.error(`[ClawRouter] Image editing error: ${msg}`);
|
|
6037
|
+
if (!res.headersSent) {
|
|
6038
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
6039
|
+
res.end(JSON.stringify({ error: "Image editing failed", details: msg }));
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
return;
|
|
6043
|
+
}
|
|
5928
6044
|
if (req.url?.match(/^\/v1\/(?:x|partner)\//)) {
|
|
5929
6045
|
try {
|
|
5930
6046
|
await proxyPartnerRequest(req, res, apiBase, payFetch);
|
|
@@ -6539,6 +6655,177 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6539
6655
|
}
|
|
6540
6656
|
return;
|
|
6541
6657
|
}
|
|
6658
|
+
if (lastContent.startsWith("/img2img")) {
|
|
6659
|
+
const imgArgs = lastContent.slice("/img2img".length).trim();
|
|
6660
|
+
let img2imgModel = "openai/gpt-image-1";
|
|
6661
|
+
let img2imgSize = "1024x1024";
|
|
6662
|
+
let imagePath = null;
|
|
6663
|
+
let maskPath = null;
|
|
6664
|
+
let img2imgPrompt = imgArgs;
|
|
6665
|
+
const imageMatch = imgArgs.match(/--image\s+(\S+)/);
|
|
6666
|
+
if (imageMatch) {
|
|
6667
|
+
imagePath = imageMatch[1];
|
|
6668
|
+
img2imgPrompt = img2imgPrompt.replace(/--image\s+\S+/, "").trim();
|
|
6669
|
+
}
|
|
6670
|
+
const maskMatch = imgArgs.match(/--mask\s+(\S+)/);
|
|
6671
|
+
if (maskMatch) {
|
|
6672
|
+
maskPath = maskMatch[1];
|
|
6673
|
+
img2imgPrompt = img2imgPrompt.replace(/--mask\s+\S+/, "").trim();
|
|
6674
|
+
}
|
|
6675
|
+
const img2imgSizeMatch = imgArgs.match(/--size\s+(\d+x\d+)/);
|
|
6676
|
+
if (img2imgSizeMatch) {
|
|
6677
|
+
img2imgSize = img2imgSizeMatch[1];
|
|
6678
|
+
img2imgPrompt = img2imgPrompt.replace(/--size\s+\d+x\d+/, "").trim();
|
|
6679
|
+
}
|
|
6680
|
+
const img2imgModelMatch = imgArgs.match(/--model\s+(\S+)/);
|
|
6681
|
+
if (img2imgModelMatch) {
|
|
6682
|
+
const raw = img2imgModelMatch[1];
|
|
6683
|
+
const IMG2IMG_ALIASES = {
|
|
6684
|
+
"gpt-image": "openai/gpt-image-1",
|
|
6685
|
+
"gpt-image-1": "openai/gpt-image-1"
|
|
6686
|
+
};
|
|
6687
|
+
img2imgModel = IMG2IMG_ALIASES[raw] ?? raw;
|
|
6688
|
+
img2imgPrompt = img2imgPrompt.replace(/--model\s+\S+/, "").trim();
|
|
6689
|
+
}
|
|
6690
|
+
const usageText = [
|
|
6691
|
+
"Usage: /img2img --image <path> <prompt>",
|
|
6692
|
+
"",
|
|
6693
|
+
"Options:",
|
|
6694
|
+
" --image <path> Source image path (required)",
|
|
6695
|
+
" --mask <path> Mask image path (optional, white = area to edit)",
|
|
6696
|
+
" --model <model> Model (default: gpt-image-1)",
|
|
6697
|
+
" --size <WxH> Output size (default: 1024x1024)",
|
|
6698
|
+
"",
|
|
6699
|
+
"Models:",
|
|
6700
|
+
" gpt-image-1 OpenAI GPT Image 1 \u2014 $0.02/image",
|
|
6701
|
+
"",
|
|
6702
|
+
"Examples:",
|
|
6703
|
+
" /img2img --image ~/photo.png change background to starry sky",
|
|
6704
|
+
" /img2img --image ./cat.jpg --mask ./mask.png remove the background",
|
|
6705
|
+
" /img2img --image /tmp/portrait.png --size 1536x1024 add a hat"
|
|
6706
|
+
].join("\n");
|
|
6707
|
+
const sendImg2ImgText = (text) => {
|
|
6708
|
+
const completionId = `chatcmpl-img2img-${Date.now()}`;
|
|
6709
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
6710
|
+
if (isStreaming) {
|
|
6711
|
+
res.writeHead(200, {
|
|
6712
|
+
"Content-Type": "text/event-stream",
|
|
6713
|
+
"Cache-Control": "no-cache",
|
|
6714
|
+
Connection: "keep-alive"
|
|
6715
|
+
});
|
|
6716
|
+
res.write(
|
|
6717
|
+
`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/img2img", choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }] })}
|
|
6718
|
+
|
|
6719
|
+
`
|
|
6720
|
+
);
|
|
6721
|
+
res.write(
|
|
6722
|
+
`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/img2img", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
|
|
6723
|
+
|
|
6724
|
+
`
|
|
6725
|
+
);
|
|
6726
|
+
res.write("data: [DONE]\n\n");
|
|
6727
|
+
res.end();
|
|
6728
|
+
} else {
|
|
6729
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6730
|
+
res.end(
|
|
6731
|
+
JSON.stringify({
|
|
6732
|
+
id: completionId,
|
|
6733
|
+
object: "chat.completion",
|
|
6734
|
+
created: timestamp,
|
|
6735
|
+
model: "clawrouter/img2img",
|
|
6736
|
+
choices: [
|
|
6737
|
+
{
|
|
6738
|
+
index: 0,
|
|
6739
|
+
message: { role: "assistant", content: text },
|
|
6740
|
+
finish_reason: "stop"
|
|
6741
|
+
}
|
|
6742
|
+
],
|
|
6743
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
6744
|
+
})
|
|
6745
|
+
);
|
|
6746
|
+
}
|
|
6747
|
+
};
|
|
6748
|
+
if (!imagePath || !img2imgPrompt) {
|
|
6749
|
+
sendImg2ImgText(usageText);
|
|
6750
|
+
return;
|
|
6751
|
+
}
|
|
6752
|
+
let imageDataUri;
|
|
6753
|
+
let maskDataUri;
|
|
6754
|
+
try {
|
|
6755
|
+
imageDataUri = readImageFileAsDataUri(imagePath);
|
|
6756
|
+
if (maskPath) maskDataUri = readImageFileAsDataUri(maskPath);
|
|
6757
|
+
} catch (fileErr) {
|
|
6758
|
+
const fileErrMsg = fileErr instanceof Error ? fileErr.message : String(fileErr);
|
|
6759
|
+
sendImg2ImgText(`Failed to read image file: ${fileErrMsg}`);
|
|
6760
|
+
return;
|
|
6761
|
+
}
|
|
6762
|
+
console.log(
|
|
6763
|
+
`[ClawRouter] /img2img \u2192 ${img2imgModel} (${img2imgSize}): ${img2imgPrompt.slice(0, 80)}`
|
|
6764
|
+
);
|
|
6765
|
+
try {
|
|
6766
|
+
const img2imgBody = JSON.stringify({
|
|
6767
|
+
model: img2imgModel,
|
|
6768
|
+
prompt: img2imgPrompt,
|
|
6769
|
+
image: imageDataUri,
|
|
6770
|
+
...maskDataUri ? { mask: maskDataUri } : {},
|
|
6771
|
+
size: img2imgSize,
|
|
6772
|
+
n: 1
|
|
6773
|
+
});
|
|
6774
|
+
const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
6775
|
+
method: "POST",
|
|
6776
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
6777
|
+
body: img2imgBody
|
|
6778
|
+
});
|
|
6779
|
+
const img2imgResult = await img2imgResponse.json();
|
|
6780
|
+
let responseText;
|
|
6781
|
+
if (!img2imgResponse.ok || img2imgResult.error) {
|
|
6782
|
+
const errMsg = typeof img2imgResult.error === "string" ? img2imgResult.error : img2imgResult.error?.message ?? `HTTP ${img2imgResponse.status}`;
|
|
6783
|
+
responseText = `Image editing failed: ${errMsg}`;
|
|
6784
|
+
console.log(`[ClawRouter] /img2img error: ${errMsg}`);
|
|
6785
|
+
} else {
|
|
6786
|
+
const images = img2imgResult.data ?? [];
|
|
6787
|
+
if (images.length === 0) {
|
|
6788
|
+
responseText = "Image editing returned no results.";
|
|
6789
|
+
} else {
|
|
6790
|
+
const lines = [];
|
|
6791
|
+
for (const img of images) {
|
|
6792
|
+
if (img.url) {
|
|
6793
|
+
if (img.url.startsWith("data:")) {
|
|
6794
|
+
try {
|
|
6795
|
+
const hostedUrl = await uploadDataUriToHost(img.url);
|
|
6796
|
+
lines.push(hostedUrl);
|
|
6797
|
+
} catch (uploadErr) {
|
|
6798
|
+
console.error(
|
|
6799
|
+
`[ClawRouter] /img2img: failed to upload data URI: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
|
|
6800
|
+
);
|
|
6801
|
+
lines.push("Image edited but upload failed. Try again.");
|
|
6802
|
+
}
|
|
6803
|
+
} else {
|
|
6804
|
+
lines.push(img.url);
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
if (img.revised_prompt) lines.push(`Revised prompt: ${img.revised_prompt}`);
|
|
6808
|
+
}
|
|
6809
|
+
lines.push("", `Model: ${img2imgModel} | Size: ${img2imgSize}`);
|
|
6810
|
+
responseText = lines.join("\n");
|
|
6811
|
+
}
|
|
6812
|
+
console.log(`[ClawRouter] /img2img success: ${images.length} image(s)`);
|
|
6813
|
+
}
|
|
6814
|
+
sendImg2ImgText(responseText);
|
|
6815
|
+
} catch (err) {
|
|
6816
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6817
|
+
console.error(`[ClawRouter] /img2img error: ${errMsg}`);
|
|
6818
|
+
if (!res.headersSent) {
|
|
6819
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6820
|
+
res.end(
|
|
6821
|
+
JSON.stringify({
|
|
6822
|
+
error: { message: `Image editing failed: ${errMsg}`, type: "img2img_error" }
|
|
6823
|
+
})
|
|
6824
|
+
);
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6542
6829
|
if (parsed.stream === true) {
|
|
6543
6830
|
parsed.stream = false;
|
|
6544
6831
|
bodyModified = true;
|