@blockrun/clawrouter 0.12.32 → 0.12.33
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 +33 -12
- package/dist/cli.js +261 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +272 -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,27 @@ 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). ClawRouter reads the local file, converts it to base64, and sends it to BlockRun's `/v1/images/image2image` endpoint with automatic x402 payment.
|
|
123
|
+
|
|
124
|
+
**API endpoint:** `POST http://localhost:8402/v1/images/image2image` is also available for programmatic use — see [Image Generation docs](docs/image-generation.md#post-v1imagesimage2image) for API reference and code examples.
|
|
125
|
+
|
|
105
126
|
---
|
|
106
127
|
|
|
107
128
|
## ⚡ How It Works
|
|
@@ -330,17 +351,17 @@ npm test
|
|
|
330
351
|
|
|
331
352
|
## 📚 More Resources
|
|
332
353
|
|
|
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)
|
|
354
|
+
| Resource | Description |
|
|
355
|
+
| ------------------------------------------------------ | ------------------------ |
|
|
356
|
+
| [Documentation](https://blockrun.ai/docs) | Full docs |
|
|
357
|
+
| [Model Pricing](https://blockrun.ai/models) | All models & prices |
|
|
358
|
+
| [Image Generation & Editing](docs/image-generation.md) | API examples, 5 models |
|
|
359
|
+
| [Routing Profiles](docs/routing-profiles.md) | ECO/AUTO/PREMIUM details |
|
|
360
|
+
| [Architecture](docs/architecture.md) | Technical deep dive |
|
|
361
|
+
| [Configuration](docs/configuration.md) | Environment variables |
|
|
362
|
+
| [vs OpenRouter](docs/vs-openrouter.md) | Why ClawRouter wins |
|
|
363
|
+
| [Features](docs/features.md) | All features |
|
|
364
|
+
| [Troubleshooting](docs/troubleshooting.md) | Common issues |
|
|
344
365
|
|
|
345
366
|
---
|
|
346
367
|
|
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,76 @@ 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 reqBody = Buffer.concat(chunks);
|
|
5953
|
+
try {
|
|
5954
|
+
const upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
5955
|
+
method: "POST",
|
|
5956
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
5957
|
+
body: reqBody
|
|
5958
|
+
});
|
|
5959
|
+
const text = await upstream.text();
|
|
5960
|
+
if (!upstream.ok) {
|
|
5961
|
+
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
5962
|
+
res.end(text);
|
|
5963
|
+
return;
|
|
5964
|
+
}
|
|
5965
|
+
let result;
|
|
5966
|
+
try {
|
|
5967
|
+
result = JSON.parse(text);
|
|
5968
|
+
} catch {
|
|
5969
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5970
|
+
res.end(text);
|
|
5971
|
+
return;
|
|
5972
|
+
}
|
|
5973
|
+
if (result.data?.length) {
|
|
5974
|
+
await mkdir3(IMAGE_DIR, { recursive: true });
|
|
5975
|
+
const port2 = server.address()?.port ?? 8402;
|
|
5976
|
+
for (const img of result.data) {
|
|
5977
|
+
const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
|
|
5978
|
+
if (dataUriMatch) {
|
|
5979
|
+
const [, mimeType, b64] = dataUriMatch;
|
|
5980
|
+
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
|
|
5981
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
5982
|
+
await writeFile2(join5(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
|
|
5983
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
5984
|
+
console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
|
|
5985
|
+
} else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
|
|
5986
|
+
try {
|
|
5987
|
+
const imgResp = await fetch(img.url);
|
|
5988
|
+
if (imgResp.ok) {
|
|
5989
|
+
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
5990
|
+
const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
5991
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
5992
|
+
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
5993
|
+
await writeFile2(join5(IMAGE_DIR, filename), buf);
|
|
5994
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
5995
|
+
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
5996
|
+
}
|
|
5997
|
+
} catch (downloadErr) {
|
|
5998
|
+
console.warn(
|
|
5999
|
+
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
6000
|
+
);
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6006
|
+
res.end(JSON.stringify(result));
|
|
6007
|
+
} catch (err) {
|
|
6008
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6009
|
+
console.error(`[ClawRouter] Image editing error: ${msg}`);
|
|
6010
|
+
if (!res.headersSent) {
|
|
6011
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
6012
|
+
res.end(JSON.stringify({ error: "Image editing failed", details: msg }));
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
return;
|
|
6016
|
+
}
|
|
5928
6017
|
if (req.url?.match(/^\/v1\/(?:x|partner)\//)) {
|
|
5929
6018
|
try {
|
|
5930
6019
|
await proxyPartnerRequest(req, res, apiBase, payFetch);
|
|
@@ -6539,6 +6628,177 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6539
6628
|
}
|
|
6540
6629
|
return;
|
|
6541
6630
|
}
|
|
6631
|
+
if (lastContent.startsWith("/img2img")) {
|
|
6632
|
+
const imgArgs = lastContent.slice("/img2img".length).trim();
|
|
6633
|
+
let img2imgModel = "openai/gpt-image-1";
|
|
6634
|
+
let img2imgSize = "1024x1024";
|
|
6635
|
+
let imagePath = null;
|
|
6636
|
+
let maskPath = null;
|
|
6637
|
+
let img2imgPrompt = imgArgs;
|
|
6638
|
+
const imageMatch = imgArgs.match(/--image\s+(\S+)/);
|
|
6639
|
+
if (imageMatch) {
|
|
6640
|
+
imagePath = imageMatch[1];
|
|
6641
|
+
img2imgPrompt = img2imgPrompt.replace(/--image\s+\S+/, "").trim();
|
|
6642
|
+
}
|
|
6643
|
+
const maskMatch = imgArgs.match(/--mask\s+(\S+)/);
|
|
6644
|
+
if (maskMatch) {
|
|
6645
|
+
maskPath = maskMatch[1];
|
|
6646
|
+
img2imgPrompt = img2imgPrompt.replace(/--mask\s+\S+/, "").trim();
|
|
6647
|
+
}
|
|
6648
|
+
const img2imgSizeMatch = imgArgs.match(/--size\s+(\d+x\d+)/);
|
|
6649
|
+
if (img2imgSizeMatch) {
|
|
6650
|
+
img2imgSize = img2imgSizeMatch[1];
|
|
6651
|
+
img2imgPrompt = img2imgPrompt.replace(/--size\s+\d+x\d+/, "").trim();
|
|
6652
|
+
}
|
|
6653
|
+
const img2imgModelMatch = imgArgs.match(/--model\s+(\S+)/);
|
|
6654
|
+
if (img2imgModelMatch) {
|
|
6655
|
+
const raw = img2imgModelMatch[1];
|
|
6656
|
+
const IMG2IMG_ALIASES = {
|
|
6657
|
+
"gpt-image": "openai/gpt-image-1",
|
|
6658
|
+
"gpt-image-1": "openai/gpt-image-1"
|
|
6659
|
+
};
|
|
6660
|
+
img2imgModel = IMG2IMG_ALIASES[raw] ?? raw;
|
|
6661
|
+
img2imgPrompt = img2imgPrompt.replace(/--model\s+\S+/, "").trim();
|
|
6662
|
+
}
|
|
6663
|
+
const usageText = [
|
|
6664
|
+
"Usage: /img2img --image <path> <prompt>",
|
|
6665
|
+
"",
|
|
6666
|
+
"Options:",
|
|
6667
|
+
" --image <path> Source image path (required)",
|
|
6668
|
+
" --mask <path> Mask image path (optional, white = area to edit)",
|
|
6669
|
+
" --model <model> Model (default: gpt-image-1)",
|
|
6670
|
+
" --size <WxH> Output size (default: 1024x1024)",
|
|
6671
|
+
"",
|
|
6672
|
+
"Models:",
|
|
6673
|
+
" gpt-image-1 OpenAI GPT Image 1 \u2014 $0.02/image",
|
|
6674
|
+
"",
|
|
6675
|
+
"Examples:",
|
|
6676
|
+
" /img2img --image ~/photo.png change background to starry sky",
|
|
6677
|
+
" /img2img --image ./cat.jpg --mask ./mask.png remove the background",
|
|
6678
|
+
" /img2img --image /tmp/portrait.png --size 1536x1024 add a hat"
|
|
6679
|
+
].join("\n");
|
|
6680
|
+
const sendImg2ImgText = (text) => {
|
|
6681
|
+
const completionId = `chatcmpl-img2img-${Date.now()}`;
|
|
6682
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
6683
|
+
if (isStreaming) {
|
|
6684
|
+
res.writeHead(200, {
|
|
6685
|
+
"Content-Type": "text/event-stream",
|
|
6686
|
+
"Cache-Control": "no-cache",
|
|
6687
|
+
Connection: "keep-alive"
|
|
6688
|
+
});
|
|
6689
|
+
res.write(
|
|
6690
|
+
`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 }] })}
|
|
6691
|
+
|
|
6692
|
+
`
|
|
6693
|
+
);
|
|
6694
|
+
res.write(
|
|
6695
|
+
`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/img2img", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
|
|
6696
|
+
|
|
6697
|
+
`
|
|
6698
|
+
);
|
|
6699
|
+
res.write("data: [DONE]\n\n");
|
|
6700
|
+
res.end();
|
|
6701
|
+
} else {
|
|
6702
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6703
|
+
res.end(
|
|
6704
|
+
JSON.stringify({
|
|
6705
|
+
id: completionId,
|
|
6706
|
+
object: "chat.completion",
|
|
6707
|
+
created: timestamp,
|
|
6708
|
+
model: "clawrouter/img2img",
|
|
6709
|
+
choices: [
|
|
6710
|
+
{
|
|
6711
|
+
index: 0,
|
|
6712
|
+
message: { role: "assistant", content: text },
|
|
6713
|
+
finish_reason: "stop"
|
|
6714
|
+
}
|
|
6715
|
+
],
|
|
6716
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
6717
|
+
})
|
|
6718
|
+
);
|
|
6719
|
+
}
|
|
6720
|
+
};
|
|
6721
|
+
if (!imagePath || !img2imgPrompt) {
|
|
6722
|
+
sendImg2ImgText(usageText);
|
|
6723
|
+
return;
|
|
6724
|
+
}
|
|
6725
|
+
let imageDataUri;
|
|
6726
|
+
let maskDataUri;
|
|
6727
|
+
try {
|
|
6728
|
+
imageDataUri = readImageFileAsDataUri(imagePath);
|
|
6729
|
+
if (maskPath) maskDataUri = readImageFileAsDataUri(maskPath);
|
|
6730
|
+
} catch (fileErr) {
|
|
6731
|
+
const fileErrMsg = fileErr instanceof Error ? fileErr.message : String(fileErr);
|
|
6732
|
+
sendImg2ImgText(`Failed to read image file: ${fileErrMsg}`);
|
|
6733
|
+
return;
|
|
6734
|
+
}
|
|
6735
|
+
console.log(
|
|
6736
|
+
`[ClawRouter] /img2img \u2192 ${img2imgModel} (${img2imgSize}): ${img2imgPrompt.slice(0, 80)}`
|
|
6737
|
+
);
|
|
6738
|
+
try {
|
|
6739
|
+
const img2imgBody = JSON.stringify({
|
|
6740
|
+
model: img2imgModel,
|
|
6741
|
+
prompt: img2imgPrompt,
|
|
6742
|
+
image: imageDataUri,
|
|
6743
|
+
...maskDataUri ? { mask: maskDataUri } : {},
|
|
6744
|
+
size: img2imgSize,
|
|
6745
|
+
n: 1
|
|
6746
|
+
});
|
|
6747
|
+
const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
6748
|
+
method: "POST",
|
|
6749
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
6750
|
+
body: img2imgBody
|
|
6751
|
+
});
|
|
6752
|
+
const img2imgResult = await img2imgResponse.json();
|
|
6753
|
+
let responseText;
|
|
6754
|
+
if (!img2imgResponse.ok || img2imgResult.error) {
|
|
6755
|
+
const errMsg = typeof img2imgResult.error === "string" ? img2imgResult.error : img2imgResult.error?.message ?? `HTTP ${img2imgResponse.status}`;
|
|
6756
|
+
responseText = `Image editing failed: ${errMsg}`;
|
|
6757
|
+
console.log(`[ClawRouter] /img2img error: ${errMsg}`);
|
|
6758
|
+
} else {
|
|
6759
|
+
const images = img2imgResult.data ?? [];
|
|
6760
|
+
if (images.length === 0) {
|
|
6761
|
+
responseText = "Image editing returned no results.";
|
|
6762
|
+
} else {
|
|
6763
|
+
const lines = [];
|
|
6764
|
+
for (const img of images) {
|
|
6765
|
+
if (img.url) {
|
|
6766
|
+
if (img.url.startsWith("data:")) {
|
|
6767
|
+
try {
|
|
6768
|
+
const hostedUrl = await uploadDataUriToHost(img.url);
|
|
6769
|
+
lines.push(hostedUrl);
|
|
6770
|
+
} catch (uploadErr) {
|
|
6771
|
+
console.error(
|
|
6772
|
+
`[ClawRouter] /img2img: failed to upload data URI: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
|
|
6773
|
+
);
|
|
6774
|
+
lines.push("Image edited but upload failed. Try again.");
|
|
6775
|
+
}
|
|
6776
|
+
} else {
|
|
6777
|
+
lines.push(img.url);
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
if (img.revised_prompt) lines.push(`Revised prompt: ${img.revised_prompt}`);
|
|
6781
|
+
}
|
|
6782
|
+
lines.push("", `Model: ${img2imgModel} | Size: ${img2imgSize}`);
|
|
6783
|
+
responseText = lines.join("\n");
|
|
6784
|
+
}
|
|
6785
|
+
console.log(`[ClawRouter] /img2img success: ${images.length} image(s)`);
|
|
6786
|
+
}
|
|
6787
|
+
sendImg2ImgText(responseText);
|
|
6788
|
+
} catch (err) {
|
|
6789
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6790
|
+
console.error(`[ClawRouter] /img2img error: ${errMsg}`);
|
|
6791
|
+
if (!res.headersSent) {
|
|
6792
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6793
|
+
res.end(
|
|
6794
|
+
JSON.stringify({
|
|
6795
|
+
error: { message: `Image editing failed: ${errMsg}`, type: "img2img_error" }
|
|
6796
|
+
})
|
|
6797
|
+
);
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
6542
6802
|
if (parsed.stream === true) {
|
|
6543
6803
|
parsed.stream = false;
|
|
6544
6804
|
bodyModified = true;
|