@rol1n/pi-kit 0.0.0-stage → 0.1.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/README.md +98 -2
- package/extensions/ask-u-questions/README.md +32 -0
- package/extensions/ask-u-questions/answers.ts +53 -0
- package/extensions/ask-u-questions/dialog-ui.ts +57 -0
- package/extensions/ask-u-questions/index.ts +88 -0
- package/extensions/ask-u-questions/multi-choice.ts +260 -0
- package/extensions/ask-u-questions/results.ts +83 -0
- package/extensions/ask-u-questions/schema.ts +17 -0
- package/extensions/ask-u-questions/single-choice.ts +182 -0
- package/extensions/ask-u-questions/tool-renderers.ts +69 -0
- package/extensions/ask-u-questions/types.ts +45 -0
- package/extensions/ask-u-questions/ui-lock.ts +24 -0
- package/extensions/continue-signal/README.md +28 -0
- package/extensions/continue-signal/index.ts +51 -0
- package/extensions/continue-signal/renderer.ts +21 -0
- package/extensions/custom-providers/README.md +28 -0
- package/extensions/custom-providers/asr/handlers/dashscope.ts +154 -0
- package/extensions/custom-providers/asr/index.ts +37 -0
- package/extensions/custom-providers/asr/models.ts +18 -0
- package/extensions/custom-providers/asr/types.ts +20 -0
- package/extensions/custom-providers/chat/index.ts +33 -0
- package/extensions/custom-providers/chat/magpie.ts +258 -0
- package/extensions/custom-providers/chat/models.ts +111 -0
- package/extensions/custom-providers/classifier/index.ts +6 -0
- package/extensions/custom-providers/classifier/models.ts +1 -0
- package/extensions/custom-providers/image/handlers/dashscope.ts +240 -0
- package/extensions/custom-providers/image/handlers/openrouter.ts +185 -0
- package/extensions/custom-providers/image/index.ts +96 -0
- package/extensions/custom-providers/image/models.ts +71 -0
- package/extensions/custom-providers/image/types.ts +22 -0
- package/extensions/custom-providers/index.ts +50 -0
- package/extensions/custom-providers/types.ts +17 -0
- package/extensions/image-generate/README.md +29 -0
- package/extensions/image-generate/files.ts +49 -0
- package/extensions/image-generate/index.ts +200 -0
- package/extensions/image-generate/model-selector.ts +150 -0
- package/extensions/image-generate/models.ts +19 -0
- package/extensions/memory-log/README.md +54 -0
- package/extensions/memory-log/agent/consolidator/prompt.ts +47 -0
- package/extensions/memory-log/agent/consolidator/tools.ts +160 -0
- package/extensions/memory-log/agent/cost.ts +32 -0
- package/extensions/memory-log/agent/index.ts +42 -0
- package/extensions/memory-log/agent/observer/prompt.ts +73 -0
- package/extensions/memory-log/agent/observer/tool.ts +73 -0
- package/extensions/memory-log/commands/compact.ts +35 -0
- package/extensions/memory-log/commands/consolidate.ts +47 -0
- package/extensions/memory-log/commands/status.ts +50 -0
- package/extensions/memory-log/config.ts +253 -0
- package/extensions/memory-log/debug-log.ts +76 -0
- package/extensions/memory-log/hooks/compaction-hook.ts +161 -0
- package/extensions/memory-log/hooks/compaction-trigger.ts +160 -0
- package/extensions/memory-log/hooks/consolidator-trigger.ts +143 -0
- package/extensions/memory-log/hooks/observer-trigger.ts +176 -0
- package/extensions/memory-log/ids.ts +78 -0
- package/extensions/memory-log/index.ts +149 -0
- package/extensions/memory-log/ledger/fold.ts +77 -0
- package/extensions/memory-log/ledger/index.ts +7 -0
- package/extensions/memory-log/ledger/pool.ts +38 -0
- package/extensions/memory-log/ledger/progress.ts +181 -0
- package/extensions/memory-log/ledger/projection.ts +115 -0
- package/extensions/memory-log/ledger/render.ts +38 -0
- package/extensions/memory-log/ledger/serialize.ts +161 -0
- package/extensions/memory-log/ledger/types.ts +201 -0
- package/extensions/memory-log/memory/index-render.ts +45 -0
- package/extensions/memory-log/memory/paths.ts +120 -0
- package/extensions/memory-log/memory/session.ts +64 -0
- package/extensions/memory-log/runtime.ts +127 -0
- package/extensions/memory-log/spawn/launch.ts +133 -0
- package/extensions/memory-log/spawn/runs.ts +83 -0
- package/extensions/memory-log/tokens.ts +66 -0
- package/extensions/memory-log/ui/status-controller.ts +225 -0
- package/extensions/memory-log/ui/timeline.ts +103 -0
- package/extensions/model-vision/README.md +28 -0
- package/extensions/multi-harness/README.md +63 -0
- package/extensions/multi-harness/adapters.mjs +72 -0
- package/extensions/multi-harness/harness-done.mjs +14 -0
- package/extensions/multi-harness/index.ts +375 -0
- package/extensions/multi-harness/ipc.mjs +37 -0
- package/extensions/multi-harness/opencode-plugin.mjs +99 -0
- package/extensions/multi-harness/process-runner.mjs +115 -0
- package/extensions/multi-harness/turn-hook.mjs +29 -0
- package/extensions/multi-harness/worker-bridge.ts +52 -0
- package/extensions/oai-subquota/README.md +30 -0
- package/extensions/oai-subquota/api.ts +27 -0
- package/extensions/oai-subquota/auth.ts +34 -0
- package/extensions/oai-subquota/cache.ts +30 -0
- package/extensions/oai-subquota/index.ts +276 -0
- package/extensions/oai-subquota/snapshot.ts +167 -0
- package/extensions/oai-subquota/types.ts +40 -0
- package/extensions/oai-subquota/ui.ts +183 -0
- package/extensions/prompt-snipaste/README.md +33 -0
- package/extensions/prompt-snipaste/index.ts +143 -0
- package/extensions/prompt-snipaste/menu.ts +159 -0
- package/extensions/prompt-snipaste/renderers.ts +88 -0
- package/extensions/prompt-snipaste/snippets.ts +203 -0
- package/extensions/prompt-snipaste/types.ts +19 -0
- package/extensions/screenshot/README.md +25 -0
- package/extensions/srp-theme/README.md +24 -0
- package/extensions/srp-theme/footer.ts +194 -0
- package/extensions/srp-theme/index.ts +172 -0
- package/extensions/srp-theme/tps-meter.ts +442 -0
- package/extensions/subagent/README.md +35 -0
- package/extensions/subagent/bridge.ts +171 -0
- package/extensions/subagent/index.ts +590 -0
- package/extensions/subagent/mux.ts +621 -0
- package/extensions/subagent/session.ts +166 -0
- package/extensions/subagent/skills/subagent/SKILL.md +70 -0
- package/extensions/subagent/ui.ts +100 -0
- package/extensions/tui-asr/README.md +28 -0
- package/extensions/tui-asr/audio.ts +40 -0
- package/extensions/tui-asr/clipboard.ts +30 -0
- package/extensions/tui-asr/dictation.ts +205 -0
- package/extensions/tui-asr/index.ts +181 -0
- package/extensions/tui-asr/input.ts +58 -0
- package/extensions/tui-asr/model-selector.ts +163 -0
- package/extensions/tui-asr/providers.ts +25 -0
- package/extensions/tui-asr/status.ts +79 -0
- package/extensions/web-reach/README.md +37 -0
- package/extensions/web-reach/html.ts +83 -0
- package/extensions/web-reach/index.ts +82 -0
- package/extensions/web-reach/page.ts +132 -0
- package/extensions/web-reach/pdf.ts +42 -0
- package/extensions/web-reach/reader.ts +42 -0
- package/extensions/web-reach/rsc.ts +161 -0
- package/extensions/web-reach/search.ts +85 -0
- package/extensions/web-reach/transport.ts +35 -0
- package/extensions/windows-wheel-fix/README.md +21 -0
- package/extensions/windows-wheel-fix/index.ts +44 -0
- package/package.json +93 -4
- package/prompts/ask-questions.md +8 -0
- package/prompts/delegate-exploration.md +7 -0
- package/prompts/diagnose-report.md +7 -0
- package/prompts/frontend-tech-preset.md +18 -0
- package/prompts/orchestrator-mode.md +7 -0
- package/prompts/session-kickoff.md +7 -0
- package/prompts/verify-not-assume.md +7 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter image generation over OpenRouter's dedicated Images API (`POST /api/v1/images`).
|
|
3
|
+
*
|
|
4
|
+
* This deliberately does NOT reuse Pi's built-in `openrouter-images` api: Pi implements that
|
|
5
|
+
* on top of chat completions with `modalities: ["image"]`, which carries neither `aspect_ratio`
|
|
6
|
+
* nor `input_references`. Both are supported here, so `image-generate.ts` calls this handler
|
|
7
|
+
* directly instead of `ctx.modelRegistry.generateImages()`.
|
|
8
|
+
* The model list still comes from Pi's registry (`getModelsOfType("image")`) and auth from
|
|
9
|
+
* `getApiKeyForProvider("openrouter")` -- the same credential Pi's built-in provider uses.
|
|
10
|
+
*/
|
|
11
|
+
import { open } from "node:fs/promises";
|
|
12
|
+
import { extname, resolve } from "node:path";
|
|
13
|
+
import type { GenerateImageContext, GeneratedImageOutput, ImageModelDefinition } from "../types.ts";
|
|
14
|
+
|
|
15
|
+
const OPENROUTER_IMAGE_ENDPOINT = "https://openrouter.ai/api/v1/images";
|
|
16
|
+
const OPENROUTER_MODELS_ENDPOINT = "https://openrouter.ai/api/v1/images/models";
|
|
17
|
+
const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
18
|
+
const MAX_REDIRECTS = 5;
|
|
19
|
+
|
|
20
|
+
const MIME_BY_EXTENSION: Record<string, string | undefined> = {
|
|
21
|
+
".png": "image/png",
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".jpeg": "image/jpeg",
|
|
24
|
+
".webp": "image/webp",
|
|
25
|
+
".gif": "image/gif",
|
|
26
|
+
".bmp": "image/bmp",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function fetchRemoteImageBuffer(
|
|
30
|
+
url: string,
|
|
31
|
+
signal?: AbortSignal,
|
|
32
|
+
): Promise<{ buffer: Buffer; mimeType: string }> {
|
|
33
|
+
let currentUrl = url;
|
|
34
|
+
for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect++) {
|
|
35
|
+
const parsed = new URL(currentUrl);
|
|
36
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
37
|
+
throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = await fetch(currentUrl, {
|
|
41
|
+
redirect: "manual",
|
|
42
|
+
headers: { Accept: "image/png,image/jpeg,image/webp,image/gif,image/bmp,*/*" },
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
47
|
+
const loc = response.headers.get("location");
|
|
48
|
+
if (!loc) throw new Error(`Redirect missing location header (HTTP ${response.status})`);
|
|
49
|
+
if (redirect === MAX_REDIRECTS) throw new Error("Too many redirects downloading image");
|
|
50
|
+
currentUrl = new URL(loc, currentUrl).href;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`Failed to download image (HTTP ${response.status})`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rawMime = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() || "image/png";
|
|
59
|
+
const arrayBuf = await response.arrayBuffer();
|
|
60
|
+
if (arrayBuf.byteLength > MAX_IMAGE_BYTES) {
|
|
61
|
+
throw new Error("Image exceeds maximum allowed size (25 MB)");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { buffer: Buffer.from(arrayBuf), mimeType: rawMime };
|
|
65
|
+
}
|
|
66
|
+
throw new Error("Failed to download image: too many redirects");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function generateOpenrouterImage(
|
|
70
|
+
ctx: GenerateImageContext,
|
|
71
|
+
options: { apiKey: string; signal?: AbortSignal; cwd?: string },
|
|
72
|
+
): Promise<GeneratedImageOutput> {
|
|
73
|
+
const payload: Record<string, unknown> = {
|
|
74
|
+
model: ctx.model,
|
|
75
|
+
prompt: ctx.prompt,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (ctx.aspectRatio) {
|
|
79
|
+
payload.aspect_ratio = ctx.aspectRatio;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (ctx.image) {
|
|
83
|
+
const imgRef = ctx.image.trim();
|
|
84
|
+
if (imgRef.startsWith("http://") || imgRef.startsWith("https://")) {
|
|
85
|
+
payload.input_references = [{ type: "image_url", image_url: { url: imgRef } }];
|
|
86
|
+
} else {
|
|
87
|
+
const localImgPath = resolve(options.cwd || process.cwd(), imgRef);
|
|
88
|
+
let buf: Buffer;
|
|
89
|
+
try {
|
|
90
|
+
const fileHandle = await open(localImgPath, "r");
|
|
91
|
+
buf = await fileHandle.readFile();
|
|
92
|
+
await fileHandle.close();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
95
|
+
throw new Error(`Reference image not readable at ${localImgPath}: ${reason}`);
|
|
96
|
+
}
|
|
97
|
+
const ext = extname(localImgPath).toLowerCase();
|
|
98
|
+
const mime = MIME_BY_EXTENSION[ext];
|
|
99
|
+
if (!mime) {
|
|
100
|
+
throw new Error(`Unsupported reference image type: ${ext || localImgPath}`);
|
|
101
|
+
}
|
|
102
|
+
payload.input_references = [
|
|
103
|
+
{
|
|
104
|
+
type: "image_url",
|
|
105
|
+
image_url: { url: `data:${mime};base64,${buf.toString("base64")}` },
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const response = await fetch(OPENROUTER_IMAGE_ENDPOINT, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"HTTP-Referer": "https://pi.dev",
|
|
117
|
+
"X-Title": "Pi Image Generate Extension",
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify(payload),
|
|
120
|
+
signal: options.signal,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errText = await response.text();
|
|
125
|
+
throw new Error(`OpenRouter image request failed (HTTP ${response.status}): ${errText.slice(0, 400)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = (await response.json()) as {
|
|
129
|
+
data?: Array<{ b64_json?: string; url?: string; revised_prompt?: string }>;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const item = data.data?.[0];
|
|
133
|
+
if (!item) {
|
|
134
|
+
throw new Error("OpenRouter returned an empty image list.");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let imgBuffer: Buffer;
|
|
138
|
+
let mimeType = "image/png";
|
|
139
|
+
|
|
140
|
+
if (item.b64_json) {
|
|
141
|
+
imgBuffer = Buffer.from(item.b64_json, "base64");
|
|
142
|
+
} else if (item.url) {
|
|
143
|
+
const downloaded = await fetchRemoteImageBuffer(item.url, options.signal);
|
|
144
|
+
imgBuffer = downloaded.buffer;
|
|
145
|
+
mimeType = downloaded.mimeType;
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error("OpenRouter response did not contain image data or URL.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
buffer: imgBuffer,
|
|
152
|
+
mimeType,
|
|
153
|
+
model: ctx.model,
|
|
154
|
+
provider: "openrouter",
|
|
155
|
+
revisedPrompt: item.revised_prompt,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function fetchOpenrouterCatalog(signal?: AbortSignal): Promise<ImageModelDefinition[]> {
|
|
160
|
+
const list: ImageModelDefinition[] = [];
|
|
161
|
+
try {
|
|
162
|
+
const timeoutSig = AbortSignal.any([AbortSignal.timeout(4000), ...(signal ? [signal] : [])]);
|
|
163
|
+
const resp = await fetch(OPENROUTER_MODELS_ENDPOINT, {
|
|
164
|
+
signal: timeoutSig,
|
|
165
|
+
headers: { Accept: "application/json" },
|
|
166
|
+
});
|
|
167
|
+
if (resp.ok) {
|
|
168
|
+
const data = (await resp.json()) as { data?: Array<{ id?: string; name?: string; description?: string }> };
|
|
169
|
+
if (Array.isArray(data?.data)) {
|
|
170
|
+
for (const item of data.data) {
|
|
171
|
+
if (!item?.id || typeof item.id !== "string") continue;
|
|
172
|
+
const id = item.id.trim();
|
|
173
|
+
const name = typeof item.name === "string" ? item.name.trim() : id;
|
|
174
|
+
const desc =
|
|
175
|
+
typeof item.description === "string" && item.description.trim()
|
|
176
|
+
? item.description.trim().replace(/\s+/g, " ").slice(0, 60) + "..."
|
|
177
|
+
: "OpenRouter Image Model";
|
|
178
|
+
|
|
179
|
+
list.push({ id, name, provider: "openrouter", description: desc });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
return list;
|
|
185
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ModalityProvidersRecord } from "../types.ts";
|
|
2
|
+
import {
|
|
3
|
+
DASHSCOPE_IMAGE_URL,
|
|
4
|
+
DASHSCOPE_IMAGE_MODELS,
|
|
5
|
+
resolveImageModel,
|
|
6
|
+
toImageModelDefinition,
|
|
7
|
+
} from "./models.ts";
|
|
8
|
+
import { generateDashscopeImages, generateDashscopeImage } from "./handlers/dashscope.ts";
|
|
9
|
+
import { generateOpenrouterImage, fetchOpenrouterCatalog } from "./handlers/openrouter.ts";
|
|
10
|
+
import type { GenerateImageContext, GeneratedImageOutput, ImageModelDefinition } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export * from "./types.ts";
|
|
13
|
+
export * from "./models.ts";
|
|
14
|
+
export * from "./handlers/dashscope.ts";
|
|
15
|
+
export * from "./handlers/openrouter.ts";
|
|
16
|
+
|
|
17
|
+
/** Minimal shape of a Pi registry image model. */
|
|
18
|
+
export type RegistryImageModel = { id: string; name?: string; provider: string };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Only DashScope is registered here.
|
|
22
|
+
*
|
|
23
|
+
* `openrouter` must NOT be registered: `pi.registerProvider()` with a `models` list
|
|
24
|
+
* replaces that provider's entire built-in catalog (chat included). Pi already ships
|
|
25
|
+
* the OpenRouter image models natively under the same provider + credential.
|
|
26
|
+
*/
|
|
27
|
+
export const imageProviders: ModalityProvidersRecord = {
|
|
28
|
+
dashscope: {
|
|
29
|
+
name: "Aliyun DashScope",
|
|
30
|
+
models: DASHSCOPE_IMAGE_MODELS.map((m) => ({
|
|
31
|
+
type: "image",
|
|
32
|
+
id: m.id,
|
|
33
|
+
name: m.name,
|
|
34
|
+
api: "dashscope-images",
|
|
35
|
+
baseUrl: DASHSCOPE_IMAGE_URL,
|
|
36
|
+
input: ["text", "image"],
|
|
37
|
+
output: ["image"],
|
|
38
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
39
|
+
})),
|
|
40
|
+
images: {
|
|
41
|
+
"dashscope-images": {
|
|
42
|
+
generateImages: generateDashscopeImages as any,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the catalog shown by the model selector.
|
|
50
|
+
*
|
|
51
|
+
* Sources, in priority order: the provider registry (`ctx.modelRegistry.getModelsOfType("image")`,
|
|
52
|
+
* which already covers both OpenRouter's built-in catalog and the DashScope registration above),
|
|
53
|
+
* followed by the live OpenRouter catalog for models newer than the bundled snapshot.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchAllAvailableImageModels(
|
|
56
|
+
registryModels: readonly RegistryImageModel[] = [],
|
|
57
|
+
signal?: AbortSignal,
|
|
58
|
+
): Promise<ImageModelDefinition[]> {
|
|
59
|
+
const map = new Map<string, ImageModelDefinition>();
|
|
60
|
+
const add = (model: ImageModelDefinition) => {
|
|
61
|
+
const key = model.id.toLowerCase();
|
|
62
|
+
if (!map.has(key)) map.set(key, model);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (const model of DASHSCOPE_IMAGE_MODELS) add(model);
|
|
66
|
+
for (const model of registryModels) add(toImageModelDefinition(model));
|
|
67
|
+
for (const model of await fetchOpenrouterCatalog(signal)) add(model);
|
|
68
|
+
|
|
69
|
+
return Array.from(map.values());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function generateImage(
|
|
73
|
+
ctx: GenerateImageContext,
|
|
74
|
+
options: {
|
|
75
|
+
apiKey: string;
|
|
76
|
+
provider?: string;
|
|
77
|
+
baseUrl?: string;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
cwd?: string;
|
|
80
|
+
},
|
|
81
|
+
): Promise<GeneratedImageOutput> {
|
|
82
|
+
const model = resolveImageModel(ctx.model);
|
|
83
|
+
const provider = options.provider ?? (model.startsWith("qwen-image") ? "dashscope" : "openrouter");
|
|
84
|
+
|
|
85
|
+
if (provider === "dashscope") {
|
|
86
|
+
return generateDashscopeImage(
|
|
87
|
+
{ prompt: ctx.prompt, model, aspectRatio: ctx.aspectRatio, image: ctx.image },
|
|
88
|
+
{ apiKey: options.apiKey, baseUrl: options.baseUrl || DASHSCOPE_IMAGE_URL, cwd: options.cwd, signal: options.signal },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return generateOpenrouterImage(
|
|
93
|
+
{ ...ctx, model },
|
|
94
|
+
{ apiKey: options.apiKey, signal: options.signal, cwd: options.cwd },
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ImageModelDefinition } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const DASHSCOPE_IMAGE_URL =
|
|
4
|
+
process.env.DASHSCOPE_IMAGE_URL?.trim().replace(/\/+$/, "") ||
|
|
5
|
+
"https://dashscope.aliyuncs.com/api/v1";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DashScope is not a Pi built-in image provider, so its catalog is declared here
|
|
9
|
+
* and registered through `imageProviders`.
|
|
10
|
+
*
|
|
11
|
+
* OpenRouter is deliberately absent: Pi already ships its image catalog (61 models)
|
|
12
|
+
* under its built-in `openrouter` provider, using the same `/login openrouter` credential.
|
|
13
|
+
*/
|
|
14
|
+
export const DASHSCOPE_IMAGE_MODELS: ImageModelDefinition[] = [
|
|
15
|
+
{
|
|
16
|
+
id: "qwen-image-3.0-pro",
|
|
17
|
+
name: "Qwen Image 3.0 Pro",
|
|
18
|
+
provider: "dashscope",
|
|
19
|
+
description: "Aliyun DashScope flagship image generation model with strong typography and rendering capabilities",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Project a Pi registry image model into the selector's view model. */
|
|
24
|
+
export function toImageModelDefinition(model: {
|
|
25
|
+
id: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
provider: string;
|
|
28
|
+
}): ImageModelDefinition {
|
|
29
|
+
return {
|
|
30
|
+
id: model.id,
|
|
31
|
+
name: model.name?.trim() || model.id,
|
|
32
|
+
provider: model.provider,
|
|
33
|
+
description: `${model.provider} image model`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a user-supplied model argument against the provider registry.
|
|
39
|
+
*
|
|
40
|
+
* The registry is the single source of truth, so no hand-written alias table is kept:
|
|
41
|
+
* OpenRouter's catalog belongs to Pi and changes without notice, and a stale alias would
|
|
42
|
+
* silently return a model id that no longer exists.
|
|
43
|
+
*
|
|
44
|
+
* Order: exact id -> exact display name -> substring (shortest match wins, so `gpt-image-1`
|
|
45
|
+
* prefers `openai/gpt-image-1` over `openai/gpt-image-1-mini`). Anything unresolved is
|
|
46
|
+
* returned verbatim so the provider reports the real error.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveImageModel(
|
|
49
|
+
model?: string,
|
|
50
|
+
availableModels?: readonly ImageModelDefinition[],
|
|
51
|
+
defaultModel = "google/gemini-3.1-flash-image",
|
|
52
|
+
): string {
|
|
53
|
+
const trimmed = model?.trim() ?? "";
|
|
54
|
+
if (!trimmed) return defaultModel;
|
|
55
|
+
if (!availableModels?.length) return trimmed;
|
|
56
|
+
|
|
57
|
+
const lower = trimmed.toLowerCase();
|
|
58
|
+
|
|
59
|
+
const exact = availableModels.find((m) => m.id.toLowerCase() === lower);
|
|
60
|
+
if (exact) return exact.id;
|
|
61
|
+
|
|
62
|
+
const byName = availableModels.find((m) => m.name.toLowerCase() === lower);
|
|
63
|
+
if (byName) return byName.id;
|
|
64
|
+
|
|
65
|
+
const partial = availableModels
|
|
66
|
+
.filter((m) => m.id.toLowerCase().includes(lower))
|
|
67
|
+
.sort((a, b) => a.id.length - b.id.length);
|
|
68
|
+
if (partial.length) return partial[0].id;
|
|
69
|
+
|
|
70
|
+
return trimmed;
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ImageModelDefinition {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
provider: "openrouter" | "dashscope" | string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GenerateImageContext {
|
|
9
|
+
prompt: string;
|
|
10
|
+
model: string;
|
|
11
|
+
aspectRatio?: string;
|
|
12
|
+
image?: string; // local file path or remote image url
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GeneratedImageOutput {
|
|
17
|
+
buffer: Buffer;
|
|
18
|
+
mimeType: string;
|
|
19
|
+
model: string;
|
|
20
|
+
provider: string;
|
|
21
|
+
revisedPrompt?: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register custom providers through Pi's native model and /login APIs.
|
|
3
|
+
* Organizes models and providers by Pi's core model types: chat, image, classifier, and asr.
|
|
4
|
+
* Credentials belong in Pi's auth.json, not here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { chatProviders } from "./chat/index.ts";
|
|
9
|
+
import { imageProviders } from "./image/index.ts";
|
|
10
|
+
import { classifierProviders } from "./classifier/index.ts";
|
|
11
|
+
import { asrProviders } from "./asr/index.ts";
|
|
12
|
+
|
|
13
|
+
export { asrProviders, ALL_ASR_MODELS } from "./asr/index.ts";
|
|
14
|
+
|
|
15
|
+
export default function customProviders(pi: ExtensionAPI): void {
|
|
16
|
+
const providerIds = new Set<string>([
|
|
17
|
+
...Object.keys(chatProviders),
|
|
18
|
+
...Object.keys(imageProviders),
|
|
19
|
+
...Object.keys(classifierProviders),
|
|
20
|
+
...Object.keys(asrProviders),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
for (const id of providerIds) {
|
|
24
|
+
const chat = chatProviders[id];
|
|
25
|
+
const image = imageProviders[id];
|
|
26
|
+
const classifier = classifierProviders[id];
|
|
27
|
+
const asr = asrProviders[id];
|
|
28
|
+
|
|
29
|
+
const config: ProviderConfig = {
|
|
30
|
+
name: chat?.name ?? image?.name ?? classifier?.name ?? asr?.name ?? id,
|
|
31
|
+
baseUrl: chat?.baseUrl ?? image?.baseUrl ?? classifier?.baseUrl,
|
|
32
|
+
api: (chat?.api ?? "openai-completions") as any,
|
|
33
|
+
...(chat?.apiKey ? { apiKey: chat.apiKey } : {}),
|
|
34
|
+
...(chat?.refreshModels ? { refreshModels: chat.refreshModels } : {}),
|
|
35
|
+
models: [
|
|
36
|
+
...(chat?.models ?? []),
|
|
37
|
+
...(image?.models ?? []),
|
|
38
|
+
...(classifier?.models ?? []),
|
|
39
|
+
],
|
|
40
|
+
...(image?.images || chat?.images || classifier?.images
|
|
41
|
+
? { images: { ...chat?.images, ...image?.images, ...classifier?.images } }
|
|
42
|
+
: {}),
|
|
43
|
+
...(classifier?.classifiers || chat?.classifiers || image?.classifiers
|
|
44
|
+
? { classifiers: { ...chat?.classifiers, ...image?.classifiers, ...classifier?.classifiers } }
|
|
45
|
+
: {}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
pi.registerProvider(id, config);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AsrModelDefinition } from "./asr/models.ts";
|
|
3
|
+
|
|
4
|
+
export interface ModalityProviderConfig {
|
|
5
|
+
name?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
api?: string;
|
|
8
|
+
apiKey?: ProviderConfig["apiKey"];
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
models?: Record<string, unknown>[];
|
|
11
|
+
refreshModels?: ProviderConfig["refreshModels"];
|
|
12
|
+
images?: ProviderConfig["images"];
|
|
13
|
+
classifiers?: ProviderConfig["classifiers"];
|
|
14
|
+
asr?: AsrModelDefinition[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ModalityProvidersRecord = Record<string, ModalityProviderConfig>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# image-generate
|
|
2
|
+
|
|
3
|
+
Generate images through OpenRouter or DashScope and save them to the current project. Registers `image_generate` and the `/image-generate` model selector.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pi install npm:@rol1n/image-generate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Also included in `@rol1n/pi-kit`. The extension entrypoint is `extensions/image-generate/index.ts`; model lookup, the selector UI, and image persistence live in `models.ts`, `model-selector.ts`, and `files.ts` beside it.
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
`/image-generate model` opens the searchable selector; `/image-generate model qwen-image-3.0-pro` selects a named model for the current session.
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"prompt": "A clear technical illustration of a terminal workflow",
|
|
20
|
+
"aspectRatio": "16:9",
|
|
21
|
+
"model": "google/gemini-3.1-flash-image"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `image_generate` tool also accepts `image` as a local path or remote reference URL where the selected provider supports reference images. The default model is `google/gemini-3.1-flash-image`. Outputs are saved under `.pi/generated-images/` relative to the working directory and the tool returns the saved path.
|
|
26
|
+
|
|
27
|
+
## Authentication and providers
|
|
28
|
+
|
|
29
|
+
Use `/login openrouter` or `/login dashscope`, or the corresponding `OPENROUTER_API_KEY` / `DASHSCOPE_API_KEY` environment variable. DashScope registry entries are supplied by [custom-providers](https://github.com/RolinShmily/pi-kit/tree/main/extensions/custom-providers). Helper adapters ship with this package, but copying helpers does not register that extension. Generation requests use the selected provider's account and pricing.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, open, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OUTPUT_DIR = ".pi/generated-images";
|
|
6
|
+
|
|
7
|
+
function detectExtension(mimeType: string): string {
|
|
8
|
+
switch (mimeType) {
|
|
9
|
+
case "image/jpeg":
|
|
10
|
+
case "image/jpg":
|
|
11
|
+
return ".jpg";
|
|
12
|
+
case "image/webp":
|
|
13
|
+
return ".webp";
|
|
14
|
+
case "image/gif":
|
|
15
|
+
return ".gif";
|
|
16
|
+
case "image/bmp":
|
|
17
|
+
return ".bmp";
|
|
18
|
+
default:
|
|
19
|
+
return ".png";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function saveImageAtomically(cwd: string, buffer: Buffer, mimeType: string): Promise<string> {
|
|
24
|
+
const outputDir = resolve(cwd, DEFAULT_OUTPUT_DIR);
|
|
25
|
+
if (!existsSync(outputDir)) {
|
|
26
|
+
await mkdir(outputDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const date = new Date().toISOString().replace(/T/, "-").replace(/:/g, "").slice(0, 15);
|
|
30
|
+
const ext = detectExtension(mimeType);
|
|
31
|
+
|
|
32
|
+
for (let i = 1; i <= 999; i++) {
|
|
33
|
+
const filename = `${date}-${String(i).padStart(3, "0")}${ext}`;
|
|
34
|
+
const targetPath = join(outputDir, filename);
|
|
35
|
+
try {
|
|
36
|
+
const handle = await open(targetPath, "wx");
|
|
37
|
+
await handle.writeFile(buffer);
|
|
38
|
+
await handle.close();
|
|
39
|
+
return targetPath;
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
if (err?.code === "EEXIST") continue;
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fallback = join(outputDir, `${date}-${Date.now()}${ext}`);
|
|
47
|
+
await writeFile(fallback, buffer);
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|