@ai-agent-forge/plugin-image-generation 0.85.14 → 0.85.16

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 CHANGED
@@ -10,10 +10,12 @@ host-configured provider's OpenAI-compatible images API:
10
10
  - `provider`/`model` come from the tool parameters or the
11
11
  `AGENT_FORGE_IMAGEGEN_PROVIDER` / `AGENT_FORGE_IMAGEGEN_MODEL` environment
12
12
  variables; missing configuration fails closed with `config_missing`.
13
- - Generated and edited PNG files are written under the workspace
14
- `generated-images/` directory; the tool result returns their
15
- workspace-relative paths plus one `image` content part per image so
16
- vision-capable models can review and revise.
13
+ - Generated and edited image files are written under the workspace
14
+ `generated-images/` directory, named and typed by their actual format
15
+ (magic-byte sniff: png/jpg/webp/gif; unknown falls back to png); the tool
16
+ result returns their workspace-relative paths plus one `image` content part
17
+ per image (mime matching the real format) so vision-capable models can
18
+ review and revise.
17
19
  - Cancellation cascades through `api.sessionAbortSignal()` and the invocation
18
20
  signal; requests time out after 180s and error messages never echo the API
19
21
  key or request headers.
@@ -36,3 +38,15 @@ Windows system-proxy (registry) settings are not read; export the port
36
38
  manually. The global dispatcher is never replaced — only this plugin's
37
39
  requests are affected.
38
40
 
41
+ Notes:
42
+
43
+ - `HTTP(S)_PROXY` must point at an **http** proxy (most VPN clients expose a
44
+ mixed http/socks port; use that port with an `http://` URL). `socks5://`
45
+ URLs are rejected with an explicit error — undici speaks http proxies only.
46
+ - `ALL_PROXY` is not read (undici limitation); set `HTTP_PROXY`/`HTTPS_PROXY`.
47
+ - Only this plugin honors these variables today; the host's own network paths
48
+ (model API traffic, plugin-market downloads) do not read proxy environment
49
+ variables yet.
50
+ - Enterprise proxies that re-sign TLS require `NODE_EXTRA_CA_CERTS` with the
51
+ proxy's root certificate.
52
+
package/agent-forge.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageFormatVersion": 1,
3
3
  "id": "agent-forge.plugin.image-generation",
4
- "pluginVersion": "0.85.14",
4
+ "pluginVersion": "0.85.16",
5
5
  "apiVersion": "1",
6
6
  "minHostVersion": "0.85.14",
7
7
  "capabilityRefs": [
@@ -1 +1 @@
1
- {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AA6Q1F;;;;;GAKG;AACH,wBAAgB,wCAAwC,CAAC,OAAO,CAAC,EAAE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC,GAAG,2BAA2B,CAiI9B","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { CapabilityAPI, CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { undiciFetchV1, wrapFetchWithEnvProxyV1 } from \"./env-proxy-fetch.ts\";\nimport { createImagesClientV1 } from \"./images-client.ts\";\nimport {\n\tboundedImageMessageV1,\n\tIMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1,\n\ttype ImageGenResultV1,\n\ttype ImagesClientResponseV1,\n} from \"./types.ts\";\n\nconst IMAGE_GENERATE_PARAMETERS = Type.Object(\n\t{\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tsize: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tquality: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tn: Type.Optional(Type.Integer({ minimum: 1, maximum: 4 })),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_EDIT_PARAMETERS = Type.Object(\n\t{\n\t\timagePath: Type.String({ minLength: 1, maxLength: 512, pattern: \"^[A-Za-z0-9_][A-Za-z0-9._/-]*$\" }),\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_PATH_PATTERN_V1 = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/u;\n\nconst GENERATED_IMAGES_DIRECTORY = \"generated-images\";\n\ninterface ImageFailureV1 {\n\treadonly code: string;\n\treadonly message: string;\n}\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\t// Shape-based dual-convention read (aligned with subagent-delegate): the\n\t// runtime invoke face passes the input object first and the host\n\t// CapabilityToolContext second; the agent tool face passes the tool call\n\t// id (a string) first and the params object second.\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(\"Image generation input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction combineSignals(signals: readonly (AbortSignal | undefined)[]): AbortSignal | undefined {\n\tconst present = signals.filter((signal): signal is AbortSignal => signal !== undefined);\n\tif (present.length === 0) return undefined;\n\tif (present.length === 1) return present[0];\n\treturn AbortSignal.any(present);\n}\n\nfunction failed(model: string, code: string, message: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"failed\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code, message: boundedImageMessageV1(message) },\n\t};\n}\n\nfunction cancelled(model: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"cancelled\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code: \"cancelled\", message: \"Image generation was cancelled\" },\n\t};\n}\n\n/** `param` (non-empty string) wins, then the env fallback, else undefined. */\nfunction paramOrEnv(value: unknown, envName: string): string | undefined {\n\tif (typeof value === \"string\" && value.trim() !== \"\") return value;\n\tconst fromEnv = process.env[envName];\n\treturn typeof fromEnv === \"string\" && fromEnv.trim() !== \"\" ? fromEnv : undefined;\n}\n\nfunction boundedString(value: unknown, maxLength: number): string | undefined {\n\treturn typeof value === \"string\" && value !== \"\" && value.length <= maxLength ? value : undefined;\n}\n\nfunction promptFrom(value: unknown): string | undefined {\n\treturn boundedString(value, 4000);\n}\n\nfunction countFrom(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1 && value <= 4 ? value : undefined;\n}\n\n/** Workspace-relative identifier confined to the generated-images directory. */\nfunction validImagePath(imagePath: string): boolean {\n\tif (!IMAGE_PATH_PATTERN_V1.test(imagePath)) return false;\n\tif (!imagePath.startsWith(`${GENERATED_IMAGES_DIRECTORY}/`)) return false;\n\treturn !imagePath.split(\"/\").includes(\"..\");\n}\n\n/** UTC `yyyymmdd-HHmmss` stamp shared by every file of one invocation. */\nfunction utcStampV1(date: Date): string {\n\tconst pad = (value: number): string => String(value).padStart(2, \"0\");\n\treturn `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(\n\t\tdate.getUTCHours(),\n\t)}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;\n}\n\ninterface ImageContentPartV1 {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\ninterface ImageToolSuccessV1 {\n\treadonly content: readonly ({ readonly type: \"text\"; readonly text: string } | ImageContentPartV1)[];\n\treadonly details: Record<string, never>;\n}\n\nfunction successResult(result: ImageGenResultV1, images: readonly Uint8Array[]): ImageToolSuccessV1 | ImageGenResultV1 {\n\tif (result.status !== \"succeeded\") return result;\n\treturn {\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: JSON.stringify(\n\t\t\t\t\t{\n\t\t\t\t\t\tfiles: result.files,\n\t\t\t\t\t\tmodel: result.model,\n\t\t\t\t\t\t...(result.usage === undefined ? {} : { usage: result.usage }),\n\t\t\t\t\t},\n\t\t\t\t\tnull,\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t},\n\t\t\t...images.map(\n\t\t\t\t(image): ImageContentPartV1 => ({\n\t\t\t\t\ttype: \"image\",\n\t\t\t\t\tdata: Buffer.from(image).toString(\"base64\"),\n\t\t\t\t\tmimeType: \"image/png\",\n\t\t\t\t}),\n\t\t\t),\n\t\t],\n\t\tdetails: {},\n\t};\n}\n\n/** Write one image, skipping sequence numbers already taken in the same second. */\nasync function writeImage(\n\tworkspace: string,\n\tstamp: string,\n\tstartSequence: number,\n\tbytes: Uint8Array,\n): Promise<{ readonly relativePath: string; readonly sequence: number }> {\n\tconst directory = join(workspace, GENERATED_IMAGES_DIRECTORY);\n\tawait mkdir(directory, { recursive: true });\n\tlet sequence = startSequence;\n\twhile (existsSync(join(directory, `img-${stamp}-${sequence}.png`))) sequence += 1;\n\tconst name = `img-${stamp}-${sequence}.png`;\n\tawait writeFile(join(directory, name), bytes);\n\treturn { relativePath: `${GENERATED_IMAGES_DIRECTORY}/${name}`, sequence };\n}\n\n/** Persist every produced image file for a settled response. */\nasync function persistImages(\n\tworkspace: string,\n\tproduced: readonly Uint8Array[],\n): Promise<{ readonly files: string[]; readonly images: Uint8Array[] }> {\n\tconst stamp = utcStampV1(new Date());\n\tconst files: string[] = [];\n\tconst images: Uint8Array[] = [];\n\tlet sequence = 1;\n\tfor (const image of produced) {\n\t\tconst written = await writeImage(workspace, stamp, sequence, image);\n\t\tfiles.push(written.relativePath);\n\t\timages.push(image);\n\t\tsequence = written.sequence + 1;\n\t}\n\treturn { files, images };\n}\n\n/** Inline b64 items pass through; url items are downloaded with the same fetch injection. */\nasync function collectImages(\n\tresponse: ImagesClientResponseV1,\n\tfetchImpl: typeof fetch,\n\tsignal: AbortSignal | undefined,\n): Promise<Uint8Array[] | ImageFailureV1> {\n\tconst images: Uint8Array[] = [];\n\tfor (const item of response.data) {\n\t\tif (item.b64 !== undefined) {\n\t\t\timages.push(Buffer.from(item.b64, \"base64\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (item.url === undefined) continue;\n\t\tconst download = signal === undefined ? await fetchImpl(item.url) : await fetchImpl(item.url, { signal });\n\t\tif (!download.ok)\n\t\t\treturn {\n\t\t\t\tcode: \"download_failed\",\n\t\t\t\tmessage: `image download from the provider url failed with status ${download.status}`,\n\t\t\t};\n\t\tconst bytes = Buffer.from(await download.arrayBuffer());\n\t\tif (bytes.byteLength > IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1)\n\t\t\treturn { code: \"response_too_large\", message: \"downloaded image exceeds the 20 MiB response limit\" };\n\t\timages.push(bytes);\n\t}\n\treturn images;\n}\n\nfunction usageOf(response: ImagesClientResponseV1): { readonly usage?: Record<string, unknown> } {\n\treturn response.usage === undefined ? {} : { usage: response.usage };\n}\n\n/** Resolve provider config/credentials/workspace; a miss returns the failed result directly. */\nasync function resolveInvocation(\n\tapi: CapabilityAPI,\n\tproviderId: string,\n\tfetchImpl: typeof fetch,\n\tmodel: string,\n): Promise<\n\t| { readonly status: \"ok\"; readonly client: ReturnType<typeof createImagesClientV1>; readonly workspace: string }\n\t| { readonly status: \"failed\"; readonly result: ImageGenResultV1 }\n> {\n\tconst workspace = api.host.workspaceDirectory;\n\tif (typeof workspace !== \"string\" || workspace === \"\")\n\t\treturn {\n\t\t\tstatus: \"failed\",\n\t\t\tresult: failed(\n\t\t\t\tmodel,\n\t\t\t\t\"workspace_unavailable\",\n\t\t\t\t\"Image generation requires the host workspaceDirectory service to write generated-images/\",\n\t\t\t),\n\t\t};\n\tconst resolution = api.credentials === undefined ? undefined : await api.credentials.resolve({ providerId });\n\tif (resolution === undefined) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\tif (\n\t\ttypeof resolution.apiKey !== \"string\" ||\n\t\tresolution.apiKey === \"\" ||\n\t\ttypeof resolution.baseUrl !== \"string\" ||\n\t\tresolution.baseUrl === \"\"\n\t) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\treturn {\n\t\tstatus: \"ok\",\n\t\tworkspace,\n\t\tclient: createImagesClientV1({ baseUrl: resolution.baseUrl, apiKey: resolution.apiKey, fetchImpl }),\n\t};\n}\n\nfunction credentialsFailure(model: string, providerId: string): ImageGenResultV1 {\n\treturn failed(\n\t\tmodel,\n\t\t\"credentials_unavailable\",\n\t\t`Image generation credentials are unavailable for provider ${providerId}: the host must resolve apiKey and baseUrl for this provider (auth.json/models.json)`,\n\t);\n}\n\n/**\n * Registers the full-control `image_generate` and `image_edit` tools. The\n * factory takes no credentials: provider auth (`apiKey` + `baseUrl`) is\n * resolved at call time through the host's public `CredentialAPI`. The\n * optional `fetchImpl` is a test seam only; production hosts use global fetch.\n */\nexport function createImageGenerationCapabilityFactoryV1(options?: {\n\treadonly fetchImpl?: typeof fetch;\n}): CapabilityPluginSyncFactory {\n\t// Every outbound request (generation and url download alike) rides the\n\t// same proxy-aware fetch: HTTPS_PROXY/HTTP_PROXY/NO_PROXY are honored\n\t// per request without touching the host's global dispatcher. The default\n\t// base is the undici package's fetch — the global fetch bundles another\n\t// undici version and rejects this package's dispatchers.\n\tconst fetchImpl: typeof fetch = wrapFetchWithEnvProxyV1(options?.fetchImpl ?? undiciFetchV1);\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"image_generate\",\n\t\t\tlabel: \"Image generate\",\n\t\t\tdescription:\n\t\t\t\t\"Generate images from a prompt via the host-configured provider's OpenAI-compatible images API; saves PNG files under generated-images/ and returns their workspace-relative paths\",\n\t\t\tparameters: IMAGE_GENERATE_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst size = boundedString(input.size, 32);\n\t\t\t\tconst quality = boundedString(input.quality, 32);\n\t\t\t\tif (input.n !== undefined && countFrom(input.n) === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"n must be an integer between 1 and 4\");\n\t\t\t\tconst n = countFrom(input.n) ?? 1;\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst response = await resolved.client.generateImages(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\tn,\n\t\t\t\t\t\t\t...(size === undefined ? {} : { size }),\n\t\t\t\t\t\t\t...(quality === undefined ? {} : { quality }),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image generation request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tapi.registerTool({\n\t\t\tname: \"image_edit\",\n\t\t\tlabel: \"Image edit\",\n\t\t\tdescription:\n\t\t\t\t\"Edit a previously generated image (workspace-relative path under generated-images/) with a revision prompt; returns the new file path\",\n\t\t\tparameters: IMAGE_EDIT_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst imagePath = typeof input.imagePath === \"string\" ? input.imagePath : undefined;\n\t\t\t\tif (imagePath === undefined || !validImagePath(imagePath))\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"invalid_path\",\n\t\t\t\t\t\t\"imagePath must be a workspace-relative identifier inside the generated-images/ directory (no .. segments)\",\n\t\t\t\t\t);\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst source = await readFile(join(resolved.workspace, imagePath)).catch(() => undefined);\n\t\t\t\t\tif (source === undefined)\n\t\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\"image_not_found\",\n\t\t\t\t\t\t\t`image ${imagePath} was not found under the workspace generated-images/ directory`,\n\t\t\t\t\t\t);\n\t\t\t\t\tconst response = await resolved.client.editImage(\n\t\t\t\t\t\t{ image: source, mimeType: \"image/png\", prompt, model },\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image edit request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n\nfunction failureFrom(\n\terror: unknown,\n\tsignal: AbortSignal | undefined,\n\tmodel: string,\n\tfallbackMessage: string,\n): ImageGenResultV1 {\n\tif (signal?.aborted) return cancelled(model);\n\treturn failed(model, \"image_request_failed\", error instanceof Error ? error.message : fallbackMessage);\n}\n"]}
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AA6R1F;;;;;GAKG;AACH,wBAAgB,wCAAwC,CAAC,OAAO,CAAC,EAAE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC,GAAG,2BAA2B,CAyI9B","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { CapabilityAPI, CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { undiciFetchV1, wrapFetchWithEnvProxyV1 } from \"./env-proxy-fetch.ts\";\nimport { createImagesClientV1 } from \"./images-client.ts\";\nimport {\n\tboundedImageMessageV1,\n\tdetectImageFormatV1,\n\tIMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1,\n\tIMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1,\n\ttype ImageFormatV1,\n\ttype ImageGenResultV1,\n\ttype ImagesClientResponseV1,\n} from \"./types.ts\";\n\nconst IMAGE_GENERATE_PARAMETERS = Type.Object(\n\t{\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tsize: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tquality: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tn: Type.Optional(Type.Integer({ minimum: 1, maximum: 4 })),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_EDIT_PARAMETERS = Type.Object(\n\t{\n\t\timagePath: Type.String({ minLength: 1, maxLength: 512, pattern: \"^[A-Za-z0-9_][A-Za-z0-9._/-]*$\" }),\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_PATH_PATTERN_V1 = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/u;\n\nconst GENERATED_IMAGES_DIRECTORY = \"generated-images\";\n\ninterface ImageFailureV1 {\n\treadonly code: string;\n\treadonly message: string;\n}\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\t// Shape-based dual-convention read (aligned with subagent-delegate): the\n\t// runtime invoke face passes the input object first and the host\n\t// CapabilityToolContext second; the agent tool face passes the tool call\n\t// id (a string) first and the params object second.\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(\"Image generation input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction combineSignals(signals: readonly (AbortSignal | undefined)[]): AbortSignal | undefined {\n\tconst present = signals.filter((signal): signal is AbortSignal => signal !== undefined);\n\tif (present.length === 0) return undefined;\n\tif (present.length === 1) return present[0];\n\treturn AbortSignal.any(present);\n}\n\nfunction failed(model: string, code: string, message: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"failed\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code, message: boundedImageMessageV1(message) },\n\t};\n}\n\nfunction cancelled(model: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"cancelled\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code: \"cancelled\", message: \"Image generation was cancelled\" },\n\t};\n}\n\n/** `param` (non-empty string) wins, then the env fallback, else undefined. */\nfunction paramOrEnv(value: unknown, envName: string): string | undefined {\n\tif (typeof value === \"string\" && value.trim() !== \"\") return value;\n\tconst fromEnv = process.env[envName];\n\treturn typeof fromEnv === \"string\" && fromEnv.trim() !== \"\" ? fromEnv : undefined;\n}\n\nfunction boundedString(value: unknown, maxLength: number): string | undefined {\n\treturn typeof value === \"string\" && value !== \"\" && value.length <= maxLength ? value : undefined;\n}\n\nfunction promptFrom(value: unknown): string | undefined {\n\treturn boundedString(value, 4000);\n}\n\nfunction countFrom(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1 && value <= 4 ? value : undefined;\n}\n\n/** Workspace-relative identifier confined to the generated-images directory. */\nfunction validImagePath(imagePath: string): boolean {\n\tif (!IMAGE_PATH_PATTERN_V1.test(imagePath)) return false;\n\tif (!imagePath.startsWith(`${GENERATED_IMAGES_DIRECTORY}/`)) return false;\n\treturn !imagePath.split(\"/\").includes(\"..\");\n}\n\n/** UTC `yyyymmdd-HHmmss` stamp shared by every file of one invocation. */\nfunction utcStampV1(date: Date): string {\n\tconst pad = (value: number): string => String(value).padStart(2, \"0\");\n\treturn `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(\n\t\tdate.getUTCHours(),\n\t)}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;\n}\n\ninterface ImageContentPartV1 {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\ninterface ImageToolSuccessV1 {\n\treadonly content: readonly ({ readonly type: \"text\"; readonly text: string } | ImageContentPartV1)[];\n\treadonly details: Record<string, never>;\n}\n\ninterface ProducedImageV1 {\n\treadonly bytes: Uint8Array;\n\treadonly format: ImageFormatV1;\n}\n\nfunction successResult(\n\tresult: ImageGenResultV1,\n\timages: readonly ProducedImageV1[],\n): ImageToolSuccessV1 | ImageGenResultV1 {\n\tif (result.status !== \"succeeded\") return result;\n\treturn {\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: JSON.stringify(\n\t\t\t\t\t{\n\t\t\t\t\t\tfiles: result.files,\n\t\t\t\t\t\tmodel: result.model,\n\t\t\t\t\t\t...(result.usage === undefined ? {} : { usage: result.usage }),\n\t\t\t\t\t},\n\t\t\t\t\tnull,\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t},\n\t\t\t...images.map(\n\t\t\t\t(image): ImageContentPartV1 => ({\n\t\t\t\t\ttype: \"image\",\n\t\t\t\t\tdata: Buffer.from(image.bytes).toString(\"base64\"),\n\t\t\t\t\tmimeType: image.format.mimeType,\n\t\t\t\t}),\n\t\t\t),\n\t\t],\n\t\tdetails: {},\n\t};\n}\n\n/** Write one image, skipping sequence numbers already taken in the same second. */\nasync function writeImage(\n\tworkspace: string,\n\tstamp: string,\n\tstartSequence: number,\n\tproduced: ProducedImageV1,\n): Promise<{ readonly relativePath: string; readonly sequence: number }> {\n\tconst directory = join(workspace, GENERATED_IMAGES_DIRECTORY);\n\tawait mkdir(directory, { recursive: true });\n\tlet sequence = startSequence;\n\twhile (existsSync(join(directory, `img-${stamp}-${sequence}.${produced.format.extension}`))) sequence += 1;\n\tconst name = `img-${stamp}-${sequence}.${produced.format.extension}`;\n\tawait writeFile(join(directory, name), produced.bytes);\n\treturn { relativePath: `${GENERATED_IMAGES_DIRECTORY}/${name}`, sequence };\n}\n\n/** Persist every produced image file for a settled response. */\nasync function persistImages(\n\tworkspace: string,\n\tproduced: readonly ProducedImageV1[],\n): Promise<{ readonly files: string[]; readonly images: ProducedImageV1[] }> {\n\tconst stamp = utcStampV1(new Date());\n\tconst files: string[] = [];\n\tconst images: ProducedImageV1[] = [];\n\tlet sequence = 1;\n\tfor (const image of produced) {\n\t\tconst written = await writeImage(workspace, stamp, sequence, image);\n\t\tfiles.push(written.relativePath);\n\t\timages.push(image);\n\t\tsequence = written.sequence + 1;\n\t}\n\treturn { files, images };\n}\n\n/** Inline b64 items pass through; url items are downloaded with the same fetch injection. */\nasync function collectImages(\n\tresponse: ImagesClientResponseV1,\n\tfetchImpl: typeof fetch,\n\tsignal: AbortSignal | undefined,\n): Promise<ProducedImageV1[] | ImageFailureV1> {\n\tconst images: ProducedImageV1[] = [];\n\tfor (const item of response.data) {\n\t\tif (item.b64 !== undefined) {\n\t\t\tconst bytes = Buffer.from(item.b64, \"base64\");\n\t\t\timages.push({ bytes, format: detectImageFormatV1(bytes) });\n\t\t\tcontinue;\n\t\t}\n\t\tif (item.url === undefined) continue;\n\t\t// The download carries its own 120s budget: a slow proxy route must\n\t\t// not hang the tool on undici's default 300s body timeout.\n\t\tconst downloadSignal =\n\t\t\tsignal === undefined ? AbortSignal.timeout(IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1) : AbortSignal.any([signal, AbortSignal.timeout(IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1)]);\n\t\tconst download = await fetchImpl(item.url, { signal: downloadSignal });\n\t\tif (!download.ok)\n\t\t\treturn {\n\t\t\t\tcode: \"download_failed\",\n\t\t\t\tmessage: `image download from the provider url failed with status ${download.status} (if this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY — see the image-generation plugin README)`,\n\t\t\t};\n\t\tconst bytes = Buffer.from(await download.arrayBuffer());\n\t\tif (bytes.byteLength > IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1)\n\t\t\treturn { code: \"response_too_large\", message: \"downloaded image exceeds the 20 MiB response limit\" };\n\t\timages.push({ bytes, format: detectImageFormatV1(bytes) });\n\t}\n\treturn images;\n}\n\nfunction usageOf(response: ImagesClientResponseV1): { readonly usage?: Record<string, unknown> } {\n\treturn response.usage === undefined ? {} : { usage: response.usage };\n}\n\n/** Resolve provider config/credentials/workspace; a miss returns the failed result directly. */\nasync function resolveInvocation(\n\tapi: CapabilityAPI,\n\tproviderId: string,\n\tfetchImpl: typeof fetch,\n\tmodel: string,\n): Promise<\n\t| { readonly status: \"ok\"; readonly client: ReturnType<typeof createImagesClientV1>; readonly workspace: string }\n\t| { readonly status: \"failed\"; readonly result: ImageGenResultV1 }\n> {\n\tconst workspace = api.host.workspaceDirectory;\n\tif (typeof workspace !== \"string\" || workspace === \"\")\n\t\treturn {\n\t\t\tstatus: \"failed\",\n\t\t\tresult: failed(\n\t\t\t\tmodel,\n\t\t\t\t\"workspace_unavailable\",\n\t\t\t\t\"Image generation requires the host workspaceDirectory service to write generated-images/\",\n\t\t\t),\n\t\t};\n\tconst resolution = api.credentials === undefined ? undefined : await api.credentials.resolve({ providerId });\n\tif (resolution === undefined) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\tif (\n\t\ttypeof resolution.apiKey !== \"string\" ||\n\t\tresolution.apiKey === \"\" ||\n\t\ttypeof resolution.baseUrl !== \"string\" ||\n\t\tresolution.baseUrl === \"\"\n\t) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\treturn {\n\t\tstatus: \"ok\",\n\t\tworkspace,\n\t\tclient: createImagesClientV1({ baseUrl: resolution.baseUrl, apiKey: resolution.apiKey, fetchImpl }),\n\t};\n}\n\nfunction credentialsFailure(model: string, providerId: string): ImageGenResultV1 {\n\treturn failed(\n\t\tmodel,\n\t\t\"credentials_unavailable\",\n\t\t`Image generation credentials are unavailable for provider ${providerId}: the host must resolve apiKey and baseUrl for this provider (auth.json/models.json)`,\n\t);\n}\n\n/**\n * Registers the full-control `image_generate` and `image_edit` tools. The\n * factory takes no credentials: provider auth (`apiKey` + `baseUrl`) is\n * resolved at call time through the host's public `CredentialAPI`. The\n * optional `fetchImpl` is a test seam only; production hosts use global fetch.\n */\nexport function createImageGenerationCapabilityFactoryV1(options?: {\n\treadonly fetchImpl?: typeof fetch;\n}): CapabilityPluginSyncFactory {\n\t// Every outbound request (generation and url download alike) rides the\n\t// same proxy-aware fetch: HTTPS_PROXY/HTTP_PROXY/NO_PROXY are honored\n\t// per request without touching the host's global dispatcher. The default\n\t// base is the undici package's fetch — the global fetch bundles another\n\t// undici version and rejects this package's dispatchers.\n\tconst fetchImpl: typeof fetch = wrapFetchWithEnvProxyV1(options?.fetchImpl ?? undiciFetchV1);\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"image_generate\",\n\t\t\tlabel: \"Image generate\",\n\t\t\tdescription:\n\t\t\t\t\"Generate images from a prompt via the host-configured provider's OpenAI-compatible images API; saves image files (png/jpg/webp/gif as produced) under generated-images/ and returns their workspace-relative paths\",\n\t\t\tparameters: IMAGE_GENERATE_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst size = boundedString(input.size, 32);\n\t\t\t\tconst quality = boundedString(input.quality, 32);\n\t\t\t\tif (input.n !== undefined && countFrom(input.n) === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"n must be an integer between 1 and 4\");\n\t\t\t\tconst n = countFrom(input.n) ?? 1;\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst response = await resolved.client.generateImages(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\tn,\n\t\t\t\t\t\t\t...(size === undefined ? {} : { size }),\n\t\t\t\t\t\t\t...(quality === undefined ? {} : { quality }),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image generation request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tapi.registerTool({\n\t\t\tname: \"image_edit\",\n\t\t\tlabel: \"Image edit\",\n\t\t\tdescription:\n\t\t\t\t\"Edit a previously generated image (workspace-relative path under generated-images/) with a revision prompt; returns the new file path\",\n\t\t\tparameters: IMAGE_EDIT_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst imagePath = typeof input.imagePath === \"string\" ? input.imagePath : undefined;\n\t\t\t\tif (imagePath === undefined || !validImagePath(imagePath))\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"invalid_path\",\n\t\t\t\t\t\t\"imagePath must be a workspace-relative identifier inside the generated-images/ directory (no .. segments)\",\n\t\t\t\t\t);\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst source = await readFile(join(resolved.workspace, imagePath)).catch(() => undefined);\n\t\t\t\t\tif (source === undefined)\n\t\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\"image_not_found\",\n\t\t\t\t\t\t\t`image ${imagePath} was not found under the workspace generated-images/ directory`,\n\t\t\t\t\t\t);\n\t\t\t\t\tconst response = await resolved.client.editImage(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\timage: source,\n\t\t\t\t\t\t\t// The edits endpoint accepts png/jpeg/webp inputs; the\n\t\t\t\t\t\t\t// multipart blob must carry the source's real format\n\t\t\t\t\t\t\t// (a prior url-form download may be JPEG).\n\t\t\t\t\t\t\tmimeType: detectImageFormatV1(source).mimeType,\n\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image edit request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n\nfunction failureFrom(\n\terror: unknown,\n\tsignal: AbortSignal | undefined,\n\tmodel: string,\n\tfallbackMessage: string,\n): ImageGenResultV1 {\n\tif (signal?.aborted) return cancelled(model);\n\tlet message = error instanceof Error ? error.message : fallbackMessage;\n\t// Append (never replace) a proxy hint for opaque network failures: the\n\t// single most common cause on machines that route out through a VPN.\n\tif (/fetch failed|terminated|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|UND_ERR/u.test(message))\n\t\tmessage = `${message} (if this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY — see the image-generation plugin README)`;\n\treturn failed(model, \"image_request_failed\", boundedImageMessageV1(message));\n}\n"]}
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { Type } from "typebox";
5
5
  import { undiciFetchV1, wrapFetchWithEnvProxyV1 } from "./env-proxy-fetch.js";
6
6
  import { createImagesClientV1 } from "./images-client.js";
7
- import { boundedImageMessageV1, IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1, } from "./types.js";
7
+ import { boundedImageMessageV1, detectImageFormatV1, IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1, IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1, } from "./types.js";
8
8
  const IMAGE_GENERATE_PARAMETERS = Type.Object({
9
9
  prompt: Type.String({ minLength: 1, maxLength: 4000 }),
10
10
  size: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),
@@ -104,22 +104,22 @@ function successResult(result, images) {
104
104
  },
105
105
  ...images.map((image) => ({
106
106
  type: "image",
107
- data: Buffer.from(image).toString("base64"),
108
- mimeType: "image/png",
107
+ data: Buffer.from(image.bytes).toString("base64"),
108
+ mimeType: image.format.mimeType,
109
109
  })),
110
110
  ],
111
111
  details: {},
112
112
  };
113
113
  }
114
114
  /** Write one image, skipping sequence numbers already taken in the same second. */
115
- async function writeImage(workspace, stamp, startSequence, bytes) {
115
+ async function writeImage(workspace, stamp, startSequence, produced) {
116
116
  const directory = join(workspace, GENERATED_IMAGES_DIRECTORY);
117
117
  await mkdir(directory, { recursive: true });
118
118
  let sequence = startSequence;
119
- while (existsSync(join(directory, `img-${stamp}-${sequence}.png`)))
119
+ while (existsSync(join(directory, `img-${stamp}-${sequence}.${produced.format.extension}`)))
120
120
  sequence += 1;
121
- const name = `img-${stamp}-${sequence}.png`;
122
- await writeFile(join(directory, name), bytes);
121
+ const name = `img-${stamp}-${sequence}.${produced.format.extension}`;
122
+ await writeFile(join(directory, name), produced.bytes);
123
123
  return { relativePath: `${GENERATED_IMAGES_DIRECTORY}/${name}`, sequence };
124
124
  }
125
125
  /** Persist every produced image file for a settled response. */
@@ -141,21 +141,25 @@ async function collectImages(response, fetchImpl, signal) {
141
141
  const images = [];
142
142
  for (const item of response.data) {
143
143
  if (item.b64 !== undefined) {
144
- images.push(Buffer.from(item.b64, "base64"));
144
+ const bytes = Buffer.from(item.b64, "base64");
145
+ images.push({ bytes, format: detectImageFormatV1(bytes) });
145
146
  continue;
146
147
  }
147
148
  if (item.url === undefined)
148
149
  continue;
149
- const download = signal === undefined ? await fetchImpl(item.url) : await fetchImpl(item.url, { signal });
150
+ // The download carries its own 120s budget: a slow proxy route must
151
+ // not hang the tool on undici's default 300s body timeout.
152
+ const downloadSignal = signal === undefined ? AbortSignal.timeout(IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1) : AbortSignal.any([signal, AbortSignal.timeout(IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1)]);
153
+ const download = await fetchImpl(item.url, { signal: downloadSignal });
150
154
  if (!download.ok)
151
155
  return {
152
156
  code: "download_failed",
153
- message: `image download from the provider url failed with status ${download.status}`,
157
+ message: `image download from the provider url failed with status ${download.status} (if this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY — see the image-generation plugin README)`,
154
158
  };
155
159
  const bytes = Buffer.from(await download.arrayBuffer());
156
160
  if (bytes.byteLength > IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1)
157
161
  return { code: "response_too_large", message: "downloaded image exceeds the 20 MiB response limit" };
158
- images.push(bytes);
162
+ images.push({ bytes, format: detectImageFormatV1(bytes) });
159
163
  }
160
164
  return images;
161
165
  }
@@ -208,7 +212,7 @@ export function createImageGenerationCapabilityFactoryV1(options) {
208
212
  api.registerTool({
209
213
  name: "image_generate",
210
214
  label: "Image generate",
211
- description: "Generate images from a prompt via the host-configured provider's OpenAI-compatible images API; saves PNG files under generated-images/ and returns their workspace-relative paths",
215
+ description: "Generate images from a prompt via the host-configured provider's OpenAI-compatible images API; saves image files (png/jpg/webp/gif as produced) under generated-images/ and returns their workspace-relative paths",
212
216
  parameters: IMAGE_GENERATE_PARAMETERS,
213
217
  execute: async (first, second, third) => {
214
218
  let input;
@@ -292,7 +296,15 @@ export function createImageGenerationCapabilityFactoryV1(options) {
292
296
  const source = await readFile(join(resolved.workspace, imagePath)).catch(() => undefined);
293
297
  if (source === undefined)
294
298
  return failed(model, "image_not_found", `image ${imagePath} was not found under the workspace generated-images/ directory`);
295
- const response = await resolved.client.editImage({ image: source, mimeType: "image/png", prompt, model }, clientSignal);
299
+ const response = await resolved.client.editImage({
300
+ image: source,
301
+ // The edits endpoint accepts png/jpeg/webp inputs; the
302
+ // multipart blob must carry the source's real format
303
+ // (a prior url-form download may be JPEG).
304
+ mimeType: detectImageFormatV1(source).mimeType,
305
+ prompt,
306
+ model,
307
+ }, clientSignal);
296
308
  const collected = await collectImages(response, fetchImpl, clientSignal);
297
309
  if (!Array.isArray(collected))
298
310
  return failed(model, collected.code, collected.message);
@@ -309,6 +321,11 @@ export function createImageGenerationCapabilityFactoryV1(options) {
309
321
  function failureFrom(error, signal, model, fallbackMessage) {
310
322
  if (signal?.aborted)
311
323
  return cancelled(model);
312
- return failed(model, "image_request_failed", error instanceof Error ? error.message : fallbackMessage);
324
+ let message = error instanceof Error ? error.message : fallbackMessage;
325
+ // Append (never replace) a proxy hint for opaque network failures: the
326
+ // single most common cause on machines that route out through a VPN.
327
+ if (/fetch failed|terminated|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|UND_ERR/u.test(message))
328
+ message = `${message} (if this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY — see the image-generation plugin README)`;
329
+ return failed(model, "image_request_failed", boundedImageMessageV1(message));
313
330
  }
314
331
  //# sourceMappingURL=capability.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { CapabilityAPI, CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { undiciFetchV1, wrapFetchWithEnvProxyV1 } from \"./env-proxy-fetch.ts\";\nimport { createImagesClientV1 } from \"./images-client.ts\";\nimport {\n\tboundedImageMessageV1,\n\tIMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1,\n\ttype ImageGenResultV1,\n\ttype ImagesClientResponseV1,\n} from \"./types.ts\";\n\nconst IMAGE_GENERATE_PARAMETERS = Type.Object(\n\t{\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tsize: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tquality: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tn: Type.Optional(Type.Integer({ minimum: 1, maximum: 4 })),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_EDIT_PARAMETERS = Type.Object(\n\t{\n\t\timagePath: Type.String({ minLength: 1, maxLength: 512, pattern: \"^[A-Za-z0-9_][A-Za-z0-9._/-]*$\" }),\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_PATH_PATTERN_V1 = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/u;\n\nconst GENERATED_IMAGES_DIRECTORY = \"generated-images\";\n\ninterface ImageFailureV1 {\n\treadonly code: string;\n\treadonly message: string;\n}\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\t// Shape-based dual-convention read (aligned with subagent-delegate): the\n\t// runtime invoke face passes the input object first and the host\n\t// CapabilityToolContext second; the agent tool face passes the tool call\n\t// id (a string) first and the params object second.\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(\"Image generation input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction combineSignals(signals: readonly (AbortSignal | undefined)[]): AbortSignal | undefined {\n\tconst present = signals.filter((signal): signal is AbortSignal => signal !== undefined);\n\tif (present.length === 0) return undefined;\n\tif (present.length === 1) return present[0];\n\treturn AbortSignal.any(present);\n}\n\nfunction failed(model: string, code: string, message: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"failed\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code, message: boundedImageMessageV1(message) },\n\t};\n}\n\nfunction cancelled(model: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"cancelled\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code: \"cancelled\", message: \"Image generation was cancelled\" },\n\t};\n}\n\n/** `param` (non-empty string) wins, then the env fallback, else undefined. */\nfunction paramOrEnv(value: unknown, envName: string): string | undefined {\n\tif (typeof value === \"string\" && value.trim() !== \"\") return value;\n\tconst fromEnv = process.env[envName];\n\treturn typeof fromEnv === \"string\" && fromEnv.trim() !== \"\" ? fromEnv : undefined;\n}\n\nfunction boundedString(value: unknown, maxLength: number): string | undefined {\n\treturn typeof value === \"string\" && value !== \"\" && value.length <= maxLength ? value : undefined;\n}\n\nfunction promptFrom(value: unknown): string | undefined {\n\treturn boundedString(value, 4000);\n}\n\nfunction countFrom(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1 && value <= 4 ? value : undefined;\n}\n\n/** Workspace-relative identifier confined to the generated-images directory. */\nfunction validImagePath(imagePath: string): boolean {\n\tif (!IMAGE_PATH_PATTERN_V1.test(imagePath)) return false;\n\tif (!imagePath.startsWith(`${GENERATED_IMAGES_DIRECTORY}/`)) return false;\n\treturn !imagePath.split(\"/\").includes(\"..\");\n}\n\n/** UTC `yyyymmdd-HHmmss` stamp shared by every file of one invocation. */\nfunction utcStampV1(date: Date): string {\n\tconst pad = (value: number): string => String(value).padStart(2, \"0\");\n\treturn `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(\n\t\tdate.getUTCHours(),\n\t)}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;\n}\n\ninterface ImageContentPartV1 {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\ninterface ImageToolSuccessV1 {\n\treadonly content: readonly ({ readonly type: \"text\"; readonly text: string } | ImageContentPartV1)[];\n\treadonly details: Record<string, never>;\n}\n\nfunction successResult(result: ImageGenResultV1, images: readonly Uint8Array[]): ImageToolSuccessV1 | ImageGenResultV1 {\n\tif (result.status !== \"succeeded\") return result;\n\treturn {\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: JSON.stringify(\n\t\t\t\t\t{\n\t\t\t\t\t\tfiles: result.files,\n\t\t\t\t\t\tmodel: result.model,\n\t\t\t\t\t\t...(result.usage === undefined ? {} : { usage: result.usage }),\n\t\t\t\t\t},\n\t\t\t\t\tnull,\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t},\n\t\t\t...images.map(\n\t\t\t\t(image): ImageContentPartV1 => ({\n\t\t\t\t\ttype: \"image\",\n\t\t\t\t\tdata: Buffer.from(image).toString(\"base64\"),\n\t\t\t\t\tmimeType: \"image/png\",\n\t\t\t\t}),\n\t\t\t),\n\t\t],\n\t\tdetails: {},\n\t};\n}\n\n/** Write one image, skipping sequence numbers already taken in the same second. */\nasync function writeImage(\n\tworkspace: string,\n\tstamp: string,\n\tstartSequence: number,\n\tbytes: Uint8Array,\n): Promise<{ readonly relativePath: string; readonly sequence: number }> {\n\tconst directory = join(workspace, GENERATED_IMAGES_DIRECTORY);\n\tawait mkdir(directory, { recursive: true });\n\tlet sequence = startSequence;\n\twhile (existsSync(join(directory, `img-${stamp}-${sequence}.png`))) sequence += 1;\n\tconst name = `img-${stamp}-${sequence}.png`;\n\tawait writeFile(join(directory, name), bytes);\n\treturn { relativePath: `${GENERATED_IMAGES_DIRECTORY}/${name}`, sequence };\n}\n\n/** Persist every produced image file for a settled response. */\nasync function persistImages(\n\tworkspace: string,\n\tproduced: readonly Uint8Array[],\n): Promise<{ readonly files: string[]; readonly images: Uint8Array[] }> {\n\tconst stamp = utcStampV1(new Date());\n\tconst files: string[] = [];\n\tconst images: Uint8Array[] = [];\n\tlet sequence = 1;\n\tfor (const image of produced) {\n\t\tconst written = await writeImage(workspace, stamp, sequence, image);\n\t\tfiles.push(written.relativePath);\n\t\timages.push(image);\n\t\tsequence = written.sequence + 1;\n\t}\n\treturn { files, images };\n}\n\n/** Inline b64 items pass through; url items are downloaded with the same fetch injection. */\nasync function collectImages(\n\tresponse: ImagesClientResponseV1,\n\tfetchImpl: typeof fetch,\n\tsignal: AbortSignal | undefined,\n): Promise<Uint8Array[] | ImageFailureV1> {\n\tconst images: Uint8Array[] = [];\n\tfor (const item of response.data) {\n\t\tif (item.b64 !== undefined) {\n\t\t\timages.push(Buffer.from(item.b64, \"base64\"));\n\t\t\tcontinue;\n\t\t}\n\t\tif (item.url === undefined) continue;\n\t\tconst download = signal === undefined ? await fetchImpl(item.url) : await fetchImpl(item.url, { signal });\n\t\tif (!download.ok)\n\t\t\treturn {\n\t\t\t\tcode: \"download_failed\",\n\t\t\t\tmessage: `image download from the provider url failed with status ${download.status}`,\n\t\t\t};\n\t\tconst bytes = Buffer.from(await download.arrayBuffer());\n\t\tif (bytes.byteLength > IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1)\n\t\t\treturn { code: \"response_too_large\", message: \"downloaded image exceeds the 20 MiB response limit\" };\n\t\timages.push(bytes);\n\t}\n\treturn images;\n}\n\nfunction usageOf(response: ImagesClientResponseV1): { readonly usage?: Record<string, unknown> } {\n\treturn response.usage === undefined ? {} : { usage: response.usage };\n}\n\n/** Resolve provider config/credentials/workspace; a miss returns the failed result directly. */\nasync function resolveInvocation(\n\tapi: CapabilityAPI,\n\tproviderId: string,\n\tfetchImpl: typeof fetch,\n\tmodel: string,\n): Promise<\n\t| { readonly status: \"ok\"; readonly client: ReturnType<typeof createImagesClientV1>; readonly workspace: string }\n\t| { readonly status: \"failed\"; readonly result: ImageGenResultV1 }\n> {\n\tconst workspace = api.host.workspaceDirectory;\n\tif (typeof workspace !== \"string\" || workspace === \"\")\n\t\treturn {\n\t\t\tstatus: \"failed\",\n\t\t\tresult: failed(\n\t\t\t\tmodel,\n\t\t\t\t\"workspace_unavailable\",\n\t\t\t\t\"Image generation requires the host workspaceDirectory service to write generated-images/\",\n\t\t\t),\n\t\t};\n\tconst resolution = api.credentials === undefined ? undefined : await api.credentials.resolve({ providerId });\n\tif (resolution === undefined) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\tif (\n\t\ttypeof resolution.apiKey !== \"string\" ||\n\t\tresolution.apiKey === \"\" ||\n\t\ttypeof resolution.baseUrl !== \"string\" ||\n\t\tresolution.baseUrl === \"\"\n\t) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\treturn {\n\t\tstatus: \"ok\",\n\t\tworkspace,\n\t\tclient: createImagesClientV1({ baseUrl: resolution.baseUrl, apiKey: resolution.apiKey, fetchImpl }),\n\t};\n}\n\nfunction credentialsFailure(model: string, providerId: string): ImageGenResultV1 {\n\treturn failed(\n\t\tmodel,\n\t\t\"credentials_unavailable\",\n\t\t`Image generation credentials are unavailable for provider ${providerId}: the host must resolve apiKey and baseUrl for this provider (auth.json/models.json)`,\n\t);\n}\n\n/**\n * Registers the full-control `image_generate` and `image_edit` tools. The\n * factory takes no credentials: provider auth (`apiKey` + `baseUrl`) is\n * resolved at call time through the host's public `CredentialAPI`. The\n * optional `fetchImpl` is a test seam only; production hosts use global fetch.\n */\nexport function createImageGenerationCapabilityFactoryV1(options?: {\n\treadonly fetchImpl?: typeof fetch;\n}): CapabilityPluginSyncFactory {\n\t// Every outbound request (generation and url download alike) rides the\n\t// same proxy-aware fetch: HTTPS_PROXY/HTTP_PROXY/NO_PROXY are honored\n\t// per request without touching the host's global dispatcher. The default\n\t// base is the undici package's fetch — the global fetch bundles another\n\t// undici version and rejects this package's dispatchers.\n\tconst fetchImpl: typeof fetch = wrapFetchWithEnvProxyV1(options?.fetchImpl ?? undiciFetchV1);\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"image_generate\",\n\t\t\tlabel: \"Image generate\",\n\t\t\tdescription:\n\t\t\t\t\"Generate images from a prompt via the host-configured provider's OpenAI-compatible images API; saves PNG files under generated-images/ and returns their workspace-relative paths\",\n\t\t\tparameters: IMAGE_GENERATE_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst size = boundedString(input.size, 32);\n\t\t\t\tconst quality = boundedString(input.quality, 32);\n\t\t\t\tif (input.n !== undefined && countFrom(input.n) === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"n must be an integer between 1 and 4\");\n\t\t\t\tconst n = countFrom(input.n) ?? 1;\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst response = await resolved.client.generateImages(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\tn,\n\t\t\t\t\t\t\t...(size === undefined ? {} : { size }),\n\t\t\t\t\t\t\t...(quality === undefined ? {} : { quality }),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image generation request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tapi.registerTool({\n\t\t\tname: \"image_edit\",\n\t\t\tlabel: \"Image edit\",\n\t\t\tdescription:\n\t\t\t\t\"Edit a previously generated image (workspace-relative path under generated-images/) with a revision prompt; returns the new file path\",\n\t\t\tparameters: IMAGE_EDIT_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst imagePath = typeof input.imagePath === \"string\" ? input.imagePath : undefined;\n\t\t\t\tif (imagePath === undefined || !validImagePath(imagePath))\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"invalid_path\",\n\t\t\t\t\t\t\"imagePath must be a workspace-relative identifier inside the generated-images/ directory (no .. segments)\",\n\t\t\t\t\t);\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst source = await readFile(join(resolved.workspace, imagePath)).catch(() => undefined);\n\t\t\t\t\tif (source === undefined)\n\t\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\"image_not_found\",\n\t\t\t\t\t\t\t`image ${imagePath} was not found under the workspace generated-images/ directory`,\n\t\t\t\t\t\t);\n\t\t\t\t\tconst response = await resolved.client.editImage(\n\t\t\t\t\t\t{ image: source, mimeType: \"image/png\", prompt, model },\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image edit request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n\nfunction failureFrom(\n\terror: unknown,\n\tsignal: AbortSignal | undefined,\n\tmodel: string,\n\tfallbackMessage: string,\n): ImageGenResultV1 {\n\tif (signal?.aborted) return cancelled(model);\n\treturn failed(model, \"image_request_failed\", error instanceof Error ? error.message : fallbackMessage);\n}\n"]}
1
+ {"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { CapabilityAPI, CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { undiciFetchV1, wrapFetchWithEnvProxyV1 } from \"./env-proxy-fetch.ts\";\nimport { createImagesClientV1 } from \"./images-client.ts\";\nimport {\n\tboundedImageMessageV1,\n\tdetectImageFormatV1,\n\tIMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1,\n\tIMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1,\n\ttype ImageFormatV1,\n\ttype ImageGenResultV1,\n\ttype ImagesClientResponseV1,\n} from \"./types.ts\";\n\nconst IMAGE_GENERATE_PARAMETERS = Type.Object(\n\t{\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tsize: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tquality: Type.Optional(Type.String({ minLength: 1, maxLength: 32 })),\n\t\tn: Type.Optional(Type.Integer({ minimum: 1, maximum: 4 })),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_EDIT_PARAMETERS = Type.Object(\n\t{\n\t\timagePath: Type.String({ minLength: 1, maxLength: 512, pattern: \"^[A-Za-z0-9_][A-Za-z0-9._/-]*$\" }),\n\t\tprompt: Type.String({ minLength: 1, maxLength: 4000 }),\n\t\tprovider: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),\n\t\tmodel: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst IMAGE_PATH_PATTERN_V1 = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/u;\n\nconst GENERATED_IMAGES_DIRECTORY = \"generated-images\";\n\ninterface ImageFailureV1 {\n\treadonly code: string;\n\treadonly message: string;\n}\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\t// Shape-based dual-convention read (aligned with subagent-delegate): the\n\t// runtime invoke face passes the input object first and the host\n\t// CapabilityToolContext second; the agent tool face passes the tool call\n\t// id (a string) first and the params object second.\n\tconst value = first !== null && typeof first === \"object\" && !Array.isArray(first) ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(\"Image generation input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction combineSignals(signals: readonly (AbortSignal | undefined)[]): AbortSignal | undefined {\n\tconst present = signals.filter((signal): signal is AbortSignal => signal !== undefined);\n\tif (present.length === 0) return undefined;\n\tif (present.length === 1) return present[0];\n\treturn AbortSignal.any(present);\n}\n\nfunction failed(model: string, code: string, message: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"failed\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code, message: boundedImageMessageV1(message) },\n\t};\n}\n\nfunction cancelled(model: string): ImageGenResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\tstatus: \"cancelled\",\n\t\tfiles: [],\n\t\tmodel,\n\t\terror: { code: \"cancelled\", message: \"Image generation was cancelled\" },\n\t};\n}\n\n/** `param` (non-empty string) wins, then the env fallback, else undefined. */\nfunction paramOrEnv(value: unknown, envName: string): string | undefined {\n\tif (typeof value === \"string\" && value.trim() !== \"\") return value;\n\tconst fromEnv = process.env[envName];\n\treturn typeof fromEnv === \"string\" && fromEnv.trim() !== \"\" ? fromEnv : undefined;\n}\n\nfunction boundedString(value: unknown, maxLength: number): string | undefined {\n\treturn typeof value === \"string\" && value !== \"\" && value.length <= maxLength ? value : undefined;\n}\n\nfunction promptFrom(value: unknown): string | undefined {\n\treturn boundedString(value, 4000);\n}\n\nfunction countFrom(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1 && value <= 4 ? value : undefined;\n}\n\n/** Workspace-relative identifier confined to the generated-images directory. */\nfunction validImagePath(imagePath: string): boolean {\n\tif (!IMAGE_PATH_PATTERN_V1.test(imagePath)) return false;\n\tif (!imagePath.startsWith(`${GENERATED_IMAGES_DIRECTORY}/`)) return false;\n\treturn !imagePath.split(\"/\").includes(\"..\");\n}\n\n/** UTC `yyyymmdd-HHmmss` stamp shared by every file of one invocation. */\nfunction utcStampV1(date: Date): string {\n\tconst pad = (value: number): string => String(value).padStart(2, \"0\");\n\treturn `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(\n\t\tdate.getUTCHours(),\n\t)}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;\n}\n\ninterface ImageContentPartV1 {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\ninterface ImageToolSuccessV1 {\n\treadonly content: readonly ({ readonly type: \"text\"; readonly text: string } | ImageContentPartV1)[];\n\treadonly details: Record<string, never>;\n}\n\ninterface ProducedImageV1 {\n\treadonly bytes: Uint8Array;\n\treadonly format: ImageFormatV1;\n}\n\nfunction successResult(\n\tresult: ImageGenResultV1,\n\timages: readonly ProducedImageV1[],\n): ImageToolSuccessV1 | ImageGenResultV1 {\n\tif (result.status !== \"succeeded\") return result;\n\treturn {\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: JSON.stringify(\n\t\t\t\t\t{\n\t\t\t\t\t\tfiles: result.files,\n\t\t\t\t\t\tmodel: result.model,\n\t\t\t\t\t\t...(result.usage === undefined ? {} : { usage: result.usage }),\n\t\t\t\t\t},\n\t\t\t\t\tnull,\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t},\n\t\t\t...images.map(\n\t\t\t\t(image): ImageContentPartV1 => ({\n\t\t\t\t\ttype: \"image\",\n\t\t\t\t\tdata: Buffer.from(image.bytes).toString(\"base64\"),\n\t\t\t\t\tmimeType: image.format.mimeType,\n\t\t\t\t}),\n\t\t\t),\n\t\t],\n\t\tdetails: {},\n\t};\n}\n\n/** Write one image, skipping sequence numbers already taken in the same second. */\nasync function writeImage(\n\tworkspace: string,\n\tstamp: string,\n\tstartSequence: number,\n\tproduced: ProducedImageV1,\n): Promise<{ readonly relativePath: string; readonly sequence: number }> {\n\tconst directory = join(workspace, GENERATED_IMAGES_DIRECTORY);\n\tawait mkdir(directory, { recursive: true });\n\tlet sequence = startSequence;\n\twhile (existsSync(join(directory, `img-${stamp}-${sequence}.${produced.format.extension}`))) sequence += 1;\n\tconst name = `img-${stamp}-${sequence}.${produced.format.extension}`;\n\tawait writeFile(join(directory, name), produced.bytes);\n\treturn { relativePath: `${GENERATED_IMAGES_DIRECTORY}/${name}`, sequence };\n}\n\n/** Persist every produced image file for a settled response. */\nasync function persistImages(\n\tworkspace: string,\n\tproduced: readonly ProducedImageV1[],\n): Promise<{ readonly files: string[]; readonly images: ProducedImageV1[] }> {\n\tconst stamp = utcStampV1(new Date());\n\tconst files: string[] = [];\n\tconst images: ProducedImageV1[] = [];\n\tlet sequence = 1;\n\tfor (const image of produced) {\n\t\tconst written = await writeImage(workspace, stamp, sequence, image);\n\t\tfiles.push(written.relativePath);\n\t\timages.push(image);\n\t\tsequence = written.sequence + 1;\n\t}\n\treturn { files, images };\n}\n\n/** Inline b64 items pass through; url items are downloaded with the same fetch injection. */\nasync function collectImages(\n\tresponse: ImagesClientResponseV1,\n\tfetchImpl: typeof fetch,\n\tsignal: AbortSignal | undefined,\n): Promise<ProducedImageV1[] | ImageFailureV1> {\n\tconst images: ProducedImageV1[] = [];\n\tfor (const item of response.data) {\n\t\tif (item.b64 !== undefined) {\n\t\t\tconst bytes = Buffer.from(item.b64, \"base64\");\n\t\t\timages.push({ bytes, format: detectImageFormatV1(bytes) });\n\t\t\tcontinue;\n\t\t}\n\t\tif (item.url === undefined) continue;\n\t\t// The download carries its own 120s budget: a slow proxy route must\n\t\t// not hang the tool on undici's default 300s body timeout.\n\t\tconst downloadSignal =\n\t\t\tsignal === undefined ? AbortSignal.timeout(IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1) : AbortSignal.any([signal, AbortSignal.timeout(IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1)]);\n\t\tconst download = await fetchImpl(item.url, { signal: downloadSignal });\n\t\tif (!download.ok)\n\t\t\treturn {\n\t\t\t\tcode: \"download_failed\",\n\t\t\t\tmessage: `image download from the provider url failed with status ${download.status} (if this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY — see the image-generation plugin README)`,\n\t\t\t};\n\t\tconst bytes = Buffer.from(await download.arrayBuffer());\n\t\tif (bytes.byteLength > IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1)\n\t\t\treturn { code: \"response_too_large\", message: \"downloaded image exceeds the 20 MiB response limit\" };\n\t\timages.push({ bytes, format: detectImageFormatV1(bytes) });\n\t}\n\treturn images;\n}\n\nfunction usageOf(response: ImagesClientResponseV1): { readonly usage?: Record<string, unknown> } {\n\treturn response.usage === undefined ? {} : { usage: response.usage };\n}\n\n/** Resolve provider config/credentials/workspace; a miss returns the failed result directly. */\nasync function resolveInvocation(\n\tapi: CapabilityAPI,\n\tproviderId: string,\n\tfetchImpl: typeof fetch,\n\tmodel: string,\n): Promise<\n\t| { readonly status: \"ok\"; readonly client: ReturnType<typeof createImagesClientV1>; readonly workspace: string }\n\t| { readonly status: \"failed\"; readonly result: ImageGenResultV1 }\n> {\n\tconst workspace = api.host.workspaceDirectory;\n\tif (typeof workspace !== \"string\" || workspace === \"\")\n\t\treturn {\n\t\t\tstatus: \"failed\",\n\t\t\tresult: failed(\n\t\t\t\tmodel,\n\t\t\t\t\"workspace_unavailable\",\n\t\t\t\t\"Image generation requires the host workspaceDirectory service to write generated-images/\",\n\t\t\t),\n\t\t};\n\tconst resolution = api.credentials === undefined ? undefined : await api.credentials.resolve({ providerId });\n\tif (resolution === undefined) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\tif (\n\t\ttypeof resolution.apiKey !== \"string\" ||\n\t\tresolution.apiKey === \"\" ||\n\t\ttypeof resolution.baseUrl !== \"string\" ||\n\t\tresolution.baseUrl === \"\"\n\t) {\n\t\treturn { status: \"failed\", result: credentialsFailure(model, providerId) };\n\t}\n\treturn {\n\t\tstatus: \"ok\",\n\t\tworkspace,\n\t\tclient: createImagesClientV1({ baseUrl: resolution.baseUrl, apiKey: resolution.apiKey, fetchImpl }),\n\t};\n}\n\nfunction credentialsFailure(model: string, providerId: string): ImageGenResultV1 {\n\treturn failed(\n\t\tmodel,\n\t\t\"credentials_unavailable\",\n\t\t`Image generation credentials are unavailable for provider ${providerId}: the host must resolve apiKey and baseUrl for this provider (auth.json/models.json)`,\n\t);\n}\n\n/**\n * Registers the full-control `image_generate` and `image_edit` tools. The\n * factory takes no credentials: provider auth (`apiKey` + `baseUrl`) is\n * resolved at call time through the host's public `CredentialAPI`. The\n * optional `fetchImpl` is a test seam only; production hosts use global fetch.\n */\nexport function createImageGenerationCapabilityFactoryV1(options?: {\n\treadonly fetchImpl?: typeof fetch;\n}): CapabilityPluginSyncFactory {\n\t// Every outbound request (generation and url download alike) rides the\n\t// same proxy-aware fetch: HTTPS_PROXY/HTTP_PROXY/NO_PROXY are honored\n\t// per request without touching the host's global dispatcher. The default\n\t// base is the undici package's fetch — the global fetch bundles another\n\t// undici version and rejects this package's dispatchers.\n\tconst fetchImpl: typeof fetch = wrapFetchWithEnvProxyV1(options?.fetchImpl ?? undiciFetchV1);\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"image_generate\",\n\t\t\tlabel: \"Image generate\",\n\t\t\tdescription:\n\t\t\t\t\"Generate images from a prompt via the host-configured provider's OpenAI-compatible images API; saves image files (png/jpg/webp/gif as produced) under generated-images/ and returns their workspace-relative paths\",\n\t\t\tparameters: IMAGE_GENERATE_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst size = boundedString(input.size, 32);\n\t\t\t\tconst quality = boundedString(input.quality, 32);\n\t\t\t\tif (input.n !== undefined && countFrom(input.n) === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"n must be an integer between 1 and 4\");\n\t\t\t\tconst n = countFrom(input.n) ?? 1;\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst response = await resolved.client.generateImages(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\tn,\n\t\t\t\t\t\t\t...(size === undefined ? {} : { size }),\n\t\t\t\t\t\t\t...(quality === undefined ? {} : { quality }),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image generation request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tapi.registerTool({\n\t\t\tname: \"image_edit\",\n\t\t\tlabel: \"Image edit\",\n\t\t\tdescription:\n\t\t\t\t\"Edit a previously generated image (workspace-relative path under generated-images/) with a revision prompt; returns the new file path\",\n\t\t\tparameters: IMAGE_EDIT_PARAMETERS,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet input: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tinput = objectInput(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"Image generation input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst prompt = promptFrom(input.prompt);\n\t\t\t\tif (prompt === undefined)\n\t\t\t\t\treturn failed(\"\", \"invalid_input\", \"prompt must be a non-empty string of at most 4000 characters\");\n\t\t\t\tconst imagePath = typeof input.imagePath === \"string\" ? input.imagePath : undefined;\n\t\t\t\tif (imagePath === undefined || !validImagePath(imagePath))\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"invalid_path\",\n\t\t\t\t\t\t\"imagePath must be a workspace-relative identifier inside the generated-images/ directory (no .. segments)\",\n\t\t\t\t\t);\n\t\t\t\tconst sessionSignal = api.sessionAbortSignal?.();\n\t\t\t\tconst invocationSignal = signalFrom(third);\n\t\t\t\tconst clientSignal = combineSignals([invocationSignal, sessionSignal]);\n\t\t\t\tif (clientSignal?.aborted) return cancelled(\"\");\n\t\t\t\tconst provider = paramOrEnv(input.provider, \"AGENT_FORGE_IMAGEGEN_PROVIDER\");\n\t\t\t\tconst model = paramOrEnv(input.model, \"AGENT_FORGE_IMAGEGEN_MODEL\");\n\t\t\t\tif (provider === undefined || model === undefined)\n\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\"config_missing\",\n\t\t\t\t\t\t\"Image generation is not configured: pass provider and model via the tool parameters or set AGENT_FORGE_IMAGEGEN_PROVIDER and AGENT_FORGE_IMAGEGEN_MODEL in the environment\",\n\t\t\t\t\t);\n\t\t\t\ttry {\n\t\t\t\t\tconst resolved = await resolveInvocation(api, provider, fetchImpl, model);\n\t\t\t\t\tif (resolved.status === \"failed\") return resolved.result;\n\t\t\t\t\tconst source = await readFile(join(resolved.workspace, imagePath)).catch(() => undefined);\n\t\t\t\t\tif (source === undefined)\n\t\t\t\t\t\treturn failed(\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t\"image_not_found\",\n\t\t\t\t\t\t\t`image ${imagePath} was not found under the workspace generated-images/ directory`,\n\t\t\t\t\t\t);\n\t\t\t\t\tconst response = await resolved.client.editImage(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\timage: source,\n\t\t\t\t\t\t\t// The edits endpoint accepts png/jpeg/webp inputs; the\n\t\t\t\t\t\t\t// multipart blob must carry the source's real format\n\t\t\t\t\t\t\t// (a prior url-form download may be JPEG).\n\t\t\t\t\t\t\tmimeType: detectImageFormatV1(source).mimeType,\n\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tclientSignal,\n\t\t\t\t\t);\n\t\t\t\t\tconst collected = await collectImages(response, fetchImpl, clientSignal);\n\t\t\t\t\tif (!Array.isArray(collected)) return failed(model, collected.code, collected.message);\n\t\t\t\t\tconst persisted = await persistImages(resolved.workspace, collected);\n\t\t\t\t\treturn successResult(\n\t\t\t\t\t\t{ version: 1, status: \"succeeded\", files: persisted.files, model, ...usageOf(response) },\n\t\t\t\t\t\tpersisted.images,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn failureFrom(error, clientSignal, model, \"Image edit request failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n\nfunction failureFrom(\n\terror: unknown,\n\tsignal: AbortSignal | undefined,\n\tmodel: string,\n\tfallbackMessage: string,\n): ImageGenResultV1 {\n\tif (signal?.aborted) return cancelled(model);\n\tlet message = error instanceof Error ? error.message : fallbackMessage;\n\t// Append (never replace) a proxy hint for opaque network failures: the\n\t// single most common cause on machines that route out through a VPN.\n\tif (/fetch failed|terminated|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|UND_ERR/u.test(message))\n\t\tmessage = `${message} (if this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY — see the image-generation plugin README)`;\n\treturn failed(model, \"image_request_failed\", boundedImageMessageV1(message));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"env-proxy-fetch.d.ts","sourceRoot":"","sources":["../src/env-proxy-fetch.ts"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,OAAO,KAA8C,CAAC;AAElF;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,OAAO,KAAqB,GAAG,OAAO,KAAK,CAUxF","sourcesContent":["import { EnvHttpProxyAgent, fetch as undiciFetch } from \"undici\";\n\n/**\n * Standard proxy environment variables (curl/git/npm convention, both cases\n * honored by undici's EnvHttpProxyAgent, which also reads NO_PROXY).\n */\nconst PROXY_ENV_KEYS = [\"HTTPS_PROXY\", \"https_proxy\", \"HTTP_PROXY\", \"http_proxy\"] as const;\n\nlet sharedDispatcher: EnvHttpProxyAgent | undefined;\n\nfunction envProxyConfigured(): boolean {\n\treturn PROXY_ENV_KEYS.some((key) => {\n\t\tconst value = process.env[key];\n\t\treturn typeof value === \"string\" && value.trim() !== \"\";\n\t});\n}\n\n/**\n * Default fetch for the plugin: the undici package's own fetch. Node's global\n * fetch bundles a different undici version and rejects dispatchers from the\n * npm package (UND_ERR_INVALID_ARG brand check), so the dispatcher and the\n * fetch implementation must come from the same undici copy.\n */\nexport const undiciFetchV1: typeof fetch = undiciFetch as unknown as typeof fetch;\n\n/**\n * Wrap a fetch implementation so every request honors the standard proxy\n * environment variables via undici's EnvHttpProxyAgent (per-request\n * dispatcher, NO_PROXY exempted by the agent itself). The global dispatcher\n * is never replaced: only this plugin's requests are affected.\n *\n * Node's fetch ignores proxy environment variables entirely, so\n * provider-CDN downloads fail on machines whose only route out is a proxy\n * (seat-verified 2026-09-30: local HTTP proxy reachable, raw fetch failed).\n * The dispatcher is injected lazily and shared; EnvHttpProxyAgent re-reads\n * the environment on every dispatch, so later env changes are honored.\n */\nexport function wrapFetchWithEnvProxyV1(base: typeof fetch = undiciFetchV1): typeof fetch {\n\tconst wrapped = (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {\n\t\tif (!envProxyConfigured()) return base(input, init);\n\t\tsharedDispatcher ??= new EnvHttpProxyAgent();\n\t\t// RequestInit (undici-types) has no dispatcher field; the runtime\n\t\t// accepts undici's non-standard dispatcher option, so cast through\n\t\t// unknown to bridge the two undici type copies.\n\t\treturn base(input, { ...init, dispatcher: sharedDispatcher } as unknown as RequestInit);\n\t};\n\treturn wrapped as typeof fetch;\n}\n"]}
1
+ {"version":3,"file":"env-proxy-fetch.d.ts","sourceRoot":"","sources":["../src/env-proxy-fetch.ts"],"names":[],"mappings":"AAyBA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,OAAO,KAA8C,CAAC;AAElF;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,OAAO,KAAqB,GAAG,OAAO,KAAK,CAiBxF","sourcesContent":["import { EnvHttpProxyAgent, fetch as undiciFetch } from \"undici\";\n\n/**\n * Standard proxy environment variables (curl/git/npm convention, both cases\n * honored by undici's EnvHttpProxyAgent, which also reads NO_PROXY).\n */\nconst PROXY_ENV_KEYS = [\"HTTPS_PROXY\", \"https_proxy\", \"HTTP_PROXY\", \"http_proxy\"] as const;\n\nlet sharedDispatcher: EnvHttpProxyAgent | undefined;\n\nfunction envProxyConfigured(): boolean {\n\treturn PROXY_ENV_KEYS.some((key) => {\n\t\tconst value = process.env[key];\n\t\treturn typeof value === \"string\" && value.trim() !== \"\";\n\t});\n}\n\n/** A configured proxy URL using a protocol undici cannot speak. */\nfunction socksProxyConfigured(): boolean {\n\treturn PROXY_ENV_KEYS.some((key) => {\n\t\tconst value = process.env[key];\n\t\treturn typeof value === \"string\" && /^socks[45]?:\\/\\//iu.test(value.trim());\n\t});\n}\n\n/**\n * Default fetch for the plugin: the undici package's own fetch. Node's global\n * fetch bundles a different undici version and rejects dispatchers from the\n * npm package (UND_ERR_INVALID_ARG brand check), so the dispatcher and the\n * fetch implementation must come from the same undici copy.\n */\nexport const undiciFetchV1: typeof fetch = undiciFetch as unknown as typeof fetch;\n\n/**\n * Wrap a fetch implementation so every request honors the standard proxy\n * environment variables via undici's EnvHttpProxyAgent (per-request\n * dispatcher, NO_PROXY exempted by the agent itself). The global dispatcher\n * is never replaced: only this plugin's requests are affected.\n *\n * Node's fetch ignores proxy environment variables entirely, so\n * provider-CDN downloads fail on machines whose only route out is a proxy\n * (seat-verified 2026-09-30: local HTTP proxy reachable, raw fetch failed).\n * The dispatcher is injected lazily and shared; EnvHttpProxyAgent re-reads\n * the environment on every dispatch, so later env changes are honored.\n */\nexport function wrapFetchWithEnvProxyV1(base: typeof fetch = undiciFetchV1): typeof fetch {\n\tconst wrapped = (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {\n\t\tif (!envProxyConfigured()) return base(input, init);\n\t\t// undici speaks http(s) proxies only; a socks URL would be treated as\n\t\t// an http proxy and fail with an opaque network error, so fail with a\n\t\t// message that names the fix (VPN clients expose a mixed http port).\n\t\tif (socksProxyConfigured())\n\t\t\tthrow new Error(\n\t\t\t\t\"socks proxy URLs are not supported by the image-generation plugin; set HTTP(S)_PROXY to the VPN client's http/mixed port (e.g. http://127.0.0.1:7897)\",\n\t\t\t);\n\t\tsharedDispatcher ??= new EnvHttpProxyAgent();\n\t\t// RequestInit (undici-types) has no dispatcher field; the runtime\n\t\t// accepts undici's non-standard dispatcher option, so cast through\n\t\t// unknown to bridge the two undici type copies.\n\t\treturn base(input, { ...init, dispatcher: sharedDispatcher } as unknown as RequestInit);\n\t};\n\treturn wrapped as typeof fetch;\n}\n"]}
@@ -11,6 +11,13 @@ function envProxyConfigured() {
11
11
  return typeof value === "string" && value.trim() !== "";
12
12
  });
13
13
  }
14
+ /** A configured proxy URL using a protocol undici cannot speak. */
15
+ function socksProxyConfigured() {
16
+ return PROXY_ENV_KEYS.some((key) => {
17
+ const value = process.env[key];
18
+ return typeof value === "string" && /^socks[45]?:\/\//iu.test(value.trim());
19
+ });
20
+ }
14
21
  /**
15
22
  * Default fetch for the plugin: the undici package's own fetch. Node's global
16
23
  * fetch bundles a different undici version and rejects dispatchers from the
@@ -34,6 +41,11 @@ export function wrapFetchWithEnvProxyV1(base = undiciFetchV1) {
34
41
  const wrapped = (input, init) => {
35
42
  if (!envProxyConfigured())
36
43
  return base(input, init);
44
+ // undici speaks http(s) proxies only; a socks URL would be treated as
45
+ // an http proxy and fail with an opaque network error, so fail with a
46
+ // message that names the fix (VPN clients expose a mixed http port).
47
+ if (socksProxyConfigured())
48
+ throw new Error("socks proxy URLs are not supported by the image-generation plugin; set HTTP(S)_PROXY to the VPN client's http/mixed port (e.g. http://127.0.0.1:7897)");
37
49
  sharedDispatcher ??= new EnvHttpProxyAgent();
38
50
  // RequestInit (undici-types) has no dispatcher field; the runtime
39
51
  // accepts undici's non-standard dispatcher option, so cast through
@@ -1 +1 @@
1
- {"version":3,"file":"env-proxy-fetch.js","sourceRoot":"","sources":["../src/env-proxy-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAEjE;;;GAGG;AACH,MAAM,cAAc,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,CAAU,CAAC;AAE3F,IAAI,gBAA+C,CAAC;AAEpD,SAAS,kBAAkB,GAAY;IACtC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAAA,CACxD,CAAC,CAAC;AAAA,CACH;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB,WAAsC,CAAC;AAElF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAI,GAAiB,aAAa,EAAgB;IACzF,MAAM,OAAO,GAAG,CAAC,KAAkC,EAAE,IAAkB,EAAqB,EAAE,CAAC;QAC9F,IAAI,CAAC,kBAAkB,EAAE;YAAE,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpD,gBAAgB,KAAK,IAAI,iBAAiB,EAAE,CAAC;QAC7C,kEAAkE;QAClE,mEAAmE;QACnE,gDAAgD;QAChD,OAAO,IAAI,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAA4B,CAAC,CAAC;IAAA,CACxF,CAAC;IACF,OAAO,OAAuB,CAAC;AAAA,CAC/B","sourcesContent":["import { EnvHttpProxyAgent, fetch as undiciFetch } from \"undici\";\n\n/**\n * Standard proxy environment variables (curl/git/npm convention, both cases\n * honored by undici's EnvHttpProxyAgent, which also reads NO_PROXY).\n */\nconst PROXY_ENV_KEYS = [\"HTTPS_PROXY\", \"https_proxy\", \"HTTP_PROXY\", \"http_proxy\"] as const;\n\nlet sharedDispatcher: EnvHttpProxyAgent | undefined;\n\nfunction envProxyConfigured(): boolean {\n\treturn PROXY_ENV_KEYS.some((key) => {\n\t\tconst value = process.env[key];\n\t\treturn typeof value === \"string\" && value.trim() !== \"\";\n\t});\n}\n\n/**\n * Default fetch for the plugin: the undici package's own fetch. Node's global\n * fetch bundles a different undici version and rejects dispatchers from the\n * npm package (UND_ERR_INVALID_ARG brand check), so the dispatcher and the\n * fetch implementation must come from the same undici copy.\n */\nexport const undiciFetchV1: typeof fetch = undiciFetch as unknown as typeof fetch;\n\n/**\n * Wrap a fetch implementation so every request honors the standard proxy\n * environment variables via undici's EnvHttpProxyAgent (per-request\n * dispatcher, NO_PROXY exempted by the agent itself). The global dispatcher\n * is never replaced: only this plugin's requests are affected.\n *\n * Node's fetch ignores proxy environment variables entirely, so\n * provider-CDN downloads fail on machines whose only route out is a proxy\n * (seat-verified 2026-09-30: local HTTP proxy reachable, raw fetch failed).\n * The dispatcher is injected lazily and shared; EnvHttpProxyAgent re-reads\n * the environment on every dispatch, so later env changes are honored.\n */\nexport function wrapFetchWithEnvProxyV1(base: typeof fetch = undiciFetchV1): typeof fetch {\n\tconst wrapped = (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {\n\t\tif (!envProxyConfigured()) return base(input, init);\n\t\tsharedDispatcher ??= new EnvHttpProxyAgent();\n\t\t// RequestInit (undici-types) has no dispatcher field; the runtime\n\t\t// accepts undici's non-standard dispatcher option, so cast through\n\t\t// unknown to bridge the two undici type copies.\n\t\treturn base(input, { ...init, dispatcher: sharedDispatcher } as unknown as RequestInit);\n\t};\n\treturn wrapped as typeof fetch;\n}\n"]}
1
+ {"version":3,"file":"env-proxy-fetch.js","sourceRoot":"","sources":["../src/env-proxy-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAEjE;;;GAGG;AACH,MAAM,cAAc,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,CAAU,CAAC;AAE3F,IAAI,gBAA+C,CAAC;AAEpD,SAAS,kBAAkB,GAAY;IACtC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAAA,CACxD,CAAC,CAAC;AAAA,CACH;AAED,mEAAmE;AACnE,SAAS,oBAAoB,GAAY;IACxC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAAA,CAC5E,CAAC,CAAC;AAAA,CACH;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB,WAAsC,CAAC;AAElF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAI,GAAiB,aAAa,EAAgB;IACzF,MAAM,OAAO,GAAG,CAAC,KAAkC,EAAE,IAAkB,EAAqB,EAAE,CAAC;QAC9F,IAAI,CAAC,kBAAkB,EAAE;YAAE,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpD,sEAAsE;QACtE,sEAAsE;QACtE,qEAAqE;QACrE,IAAI,oBAAoB,EAAE;YACzB,MAAM,IAAI,KAAK,CACd,uJAAuJ,CACvJ,CAAC;QACH,gBAAgB,KAAK,IAAI,iBAAiB,EAAE,CAAC;QAC7C,kEAAkE;QAClE,mEAAmE;QACnE,gDAAgD;QAChD,OAAO,IAAI,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAA4B,CAAC,CAAC;IAAA,CACxF,CAAC;IACF,OAAO,OAAuB,CAAC;AAAA,CAC/B","sourcesContent":["import { EnvHttpProxyAgent, fetch as undiciFetch } from \"undici\";\n\n/**\n * Standard proxy environment variables (curl/git/npm convention, both cases\n * honored by undici's EnvHttpProxyAgent, which also reads NO_PROXY).\n */\nconst PROXY_ENV_KEYS = [\"HTTPS_PROXY\", \"https_proxy\", \"HTTP_PROXY\", \"http_proxy\"] as const;\n\nlet sharedDispatcher: EnvHttpProxyAgent | undefined;\n\nfunction envProxyConfigured(): boolean {\n\treturn PROXY_ENV_KEYS.some((key) => {\n\t\tconst value = process.env[key];\n\t\treturn typeof value === \"string\" && value.trim() !== \"\";\n\t});\n}\n\n/** A configured proxy URL using a protocol undici cannot speak. */\nfunction socksProxyConfigured(): boolean {\n\treturn PROXY_ENV_KEYS.some((key) => {\n\t\tconst value = process.env[key];\n\t\treturn typeof value === \"string\" && /^socks[45]?:\\/\\//iu.test(value.trim());\n\t});\n}\n\n/**\n * Default fetch for the plugin: the undici package's own fetch. Node's global\n * fetch bundles a different undici version and rejects dispatchers from the\n * npm package (UND_ERR_INVALID_ARG brand check), so the dispatcher and the\n * fetch implementation must come from the same undici copy.\n */\nexport const undiciFetchV1: typeof fetch = undiciFetch as unknown as typeof fetch;\n\n/**\n * Wrap a fetch implementation so every request honors the standard proxy\n * environment variables via undici's EnvHttpProxyAgent (per-request\n * dispatcher, NO_PROXY exempted by the agent itself). The global dispatcher\n * is never replaced: only this plugin's requests are affected.\n *\n * Node's fetch ignores proxy environment variables entirely, so\n * provider-CDN downloads fail on machines whose only route out is a proxy\n * (seat-verified 2026-09-30: local HTTP proxy reachable, raw fetch failed).\n * The dispatcher is injected lazily and shared; EnvHttpProxyAgent re-reads\n * the environment on every dispatch, so later env changes are honored.\n */\nexport function wrapFetchWithEnvProxyV1(base: typeof fetch = undiciFetchV1): typeof fetch {\n\tconst wrapped = (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {\n\t\tif (!envProxyConfigured()) return base(input, init);\n\t\t// undici speaks http(s) proxies only; a socks URL would be treated as\n\t\t// an http proxy and fail with an opaque network error, so fail with a\n\t\t// message that names the fix (VPN clients expose a mixed http port).\n\t\tif (socksProxyConfigured())\n\t\t\tthrow new Error(\n\t\t\t\t\"socks proxy URLs are not supported by the image-generation plugin; set HTTP(S)_PROXY to the VPN client's http/mixed port (e.g. http://127.0.0.1:7897)\",\n\t\t\t);\n\t\tsharedDispatcher ??= new EnvHttpProxyAgent();\n\t\t// RequestInit (undici-types) has no dispatcher field; the runtime\n\t\t// accepts undici's non-standard dispatcher option, so cast through\n\t\t// unknown to bridge the two undici type copies.\n\t\treturn base(input, { ...init, dispatcher: sharedDispatcher } as unknown as RequestInit);\n\t};\n\treturn wrapped as typeof fetch;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"images-client.d.ts","sourceRoot":"","sources":["../src/images-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA0B,cAAc,EAAyB,MAAM,YAAY,CAAC;AAKhG,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC;AAyCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAuDnF","sourcesContent":["import type { ImagesClientResponseV1, ImagesClientV1, ImagesGeneratedItemV1 } from \"./types.ts\";\n\n/** Per-request budget: image edits with large inputs legitimately take minutes. */\nconst IMAGES_REQUEST_TIMEOUT_MS_V1 = 180_000;\n\nexport interface ImagesClientOptionsV1 {\n\treadonly baseUrl: string;\n\treadonly apiKey: string;\n\treadonly fetchImpl?: typeof fetch;\n}\n\n/** Combine the caller's signal with the fixed request timeout. */\nfunction requestSignal(signal: AbortSignal | undefined): AbortSignal {\n\tconst timeout = AbortSignal.timeout(IMAGES_REQUEST_TIMEOUT_MS_V1);\n\treturn signal === undefined ? timeout : AbortSignal.any([signal, timeout]);\n}\n\nfunction trimTrailingSlash(baseUrl: string): string {\n\treturn baseUrl.replace(/\\/+$/u, \"\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Map the OpenAI images response onto the client's normalized shape:\n * `b64_json`/`b64` stays inline, `url` is left for the capability layer to\n * download; unknown item shapes are dropped rather than guessed at.\n */\nfunction parseResponse(payload: unknown): ImagesClientResponseV1 {\n\tif (!isPlainObject(payload) || !Array.isArray(payload.data))\n\t\tthrow new Error(\"images api response has no data array\");\n\tconst data: ImagesGeneratedItemV1[] = [];\n\tfor (const entry of payload.data) {\n\t\tif (!isPlainObject(entry)) continue;\n\t\tif (typeof entry.b64_json === \"string\") data.push({ b64: entry.b64_json });\n\t\telse if (typeof entry.b64 === \"string\") data.push({ b64: entry.b64 });\n\t\telse if (typeof entry.url === \"string\")\n\t\t\tdata.push({\n\t\t\t\turl: entry.url,\n\t\t\t\t...(typeof entry.mime_type === \"string\" ? { mimeType: entry.mime_type } : {}),\n\t\t\t});\n\t}\n\treturn {\n\t\tdata,\n\t\t...(isPlainObject(payload.usage) ? { usage: payload.usage as Record<string, unknown> } : {}),\n\t};\n}\n\n/**\n * Minimal OpenAI-compatible images API client (globals only: fetch, FormData,\n * Blob, AbortSignal.timeout/any on Node 22). Error messages carry the status\n * and the first 300 characters of the response body only — never the API key\n * or request headers.\n */\nexport function createImagesClientV1(options: ImagesClientOptionsV1): ImagesClientV1 {\n\tconst fetchImpl: typeof fetch = options.fetchImpl ?? fetch;\n\tconst base = trimTrailingSlash(options.baseUrl);\n\n\tasync function post(path: string, init: RequestInit, signal: AbortSignal | undefined): Promise<unknown> {\n\t\tconst response = await fetchImpl(`${base}${path}`, { ...init, signal: requestSignal(signal) });\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text().catch(() => \"\");\n\t\t\tthrow new Error(`images api ${path} failed with status ${response.status}: ${body.slice(0, 300)}`);\n\t\t}\n\t\treturn response.json();\n\t}\n\n\treturn {\n\t\tgenerateImages: async (request, signal) =>\n\t\t\tparseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/generations\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tmodel: request.model,\n\t\t\t\t\t\t\tprompt: request.prompt,\n\t\t\t\t\t\t\tn: request.n,\n\t\t\t\t\t\t\t...(request.size === undefined ? {} : { size: request.size }),\n\t\t\t\t\t\t\t...(request.quality === undefined ? {} : { quality: request.quality }),\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t),\n\t\teditImage: async (request, signal) => {\n\t\t\t// Native FormData + Blob: fetch sets the multipart Content-Type with\n\t\t\t// its own boundary, so it must not be set manually here.\n\t\t\tconst form = new FormData();\n\t\t\tform.set(\"model\", request.model);\n\t\t\tform.set(\"prompt\", request.prompt);\n\t\t\tif (request.size !== undefined) form.set(\"size\", request.size);\n\t\t\tif (request.quality !== undefined) form.set(\"quality\", request.quality);\n\t\t\tform.set(\"image\", new Blob([request.image], { type: request.mimeType }), \"input.png\");\n\t\t\treturn parseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/edits\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t// No content-type header: the boundary is owned by fetch.\n\t\t\t\t\t\theaders: { authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: form,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"images-client.d.ts","sourceRoot":"","sources":["../src/images-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA0B,cAAc,EAAyB,MAAM,YAAY,CAAC;AAKhG,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC;AAyCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAiEnF","sourcesContent":["import type { ImagesClientResponseV1, ImagesClientV1, ImagesGeneratedItemV1 } from \"./types.ts\";\n\n/** Per-request budget: image edits with large inputs legitimately take minutes. */\nconst IMAGES_REQUEST_TIMEOUT_MS_V1 = 180_000;\n\nexport interface ImagesClientOptionsV1 {\n\treadonly baseUrl: string;\n\treadonly apiKey: string;\n\treadonly fetchImpl?: typeof fetch;\n}\n\n/** Combine the caller's signal with the fixed request timeout. */\nfunction requestSignal(signal: AbortSignal | undefined): AbortSignal {\n\tconst timeout = AbortSignal.timeout(IMAGES_REQUEST_TIMEOUT_MS_V1);\n\treturn signal === undefined ? timeout : AbortSignal.any([signal, timeout]);\n}\n\nfunction trimTrailingSlash(baseUrl: string): string {\n\treturn baseUrl.replace(/\\/+$/u, \"\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Map the OpenAI images response onto the client's normalized shape:\n * `b64_json`/`b64` stays inline, `url` is left for the capability layer to\n * download; unknown item shapes are dropped rather than guessed at.\n */\nfunction parseResponse(payload: unknown): ImagesClientResponseV1 {\n\tif (!isPlainObject(payload) || !Array.isArray(payload.data))\n\t\tthrow new Error(\"images api response has no data array\");\n\tconst data: ImagesGeneratedItemV1[] = [];\n\tfor (const entry of payload.data) {\n\t\tif (!isPlainObject(entry)) continue;\n\t\tif (typeof entry.b64_json === \"string\") data.push({ b64: entry.b64_json });\n\t\telse if (typeof entry.b64 === \"string\") data.push({ b64: entry.b64 });\n\t\telse if (typeof entry.url === \"string\")\n\t\t\tdata.push({\n\t\t\t\turl: entry.url,\n\t\t\t\t...(typeof entry.mime_type === \"string\" ? { mimeType: entry.mime_type } : {}),\n\t\t\t});\n\t}\n\treturn {\n\t\tdata,\n\t\t...(isPlainObject(payload.usage) ? { usage: payload.usage as Record<string, unknown> } : {}),\n\t};\n}\n\n/**\n * Minimal OpenAI-compatible images API client (globals only: fetch, FormData,\n * Blob, AbortSignal.timeout/any on Node 22). Error messages carry the status\n * and the first 300 characters of the response body only — never the API key\n * or request headers.\n */\nexport function createImagesClientV1(options: ImagesClientOptionsV1): ImagesClientV1 {\n\tconst fetchImpl: typeof fetch = options.fetchImpl ?? fetch;\n\tconst base = trimTrailingSlash(options.baseUrl);\n\n\tasync function post(path: string, init: RequestInit, signal: AbortSignal | undefined): Promise<unknown> {\n\t\tconst response = await fetchImpl(`${base}${path}`, { ...init, signal: requestSignal(signal) });\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text().catch(() => \"\");\n\t\t\tthrow new Error(`images api ${path} failed with status ${response.status}: ${body.slice(0, 300)}`);\n\t\t}\n\t\treturn response.json();\n\t}\n\n\treturn {\n\t\tgenerateImages: async (request, signal) =>\n\t\t\tparseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/generations\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tmodel: request.model,\n\t\t\t\t\t\t\tprompt: request.prompt,\n\t\t\t\t\t\t\tn: request.n,\n\t\t\t\t\t\t\t...(request.size === undefined ? {} : { size: request.size }),\n\t\t\t\t\t\t\t...(request.quality === undefined ? {} : { quality: request.quality }),\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t),\n\t\teditImage: async (request, signal) => {\n\t\t\t// Native FormData + Blob: fetch sets the multipart Content-Type with\n\t\t\t// its own boundary, so it must not be set manually here. The part\n\t\t\t// filename follows the declared mime so the payload is not named\n\t\t\t// .png while carrying JPEG bytes (providers sniff either way).\n\t\t\tconst extension =\n\t\t\t\trequest.mimeType === \"image/jpeg\"\n\t\t\t\t\t? \"jpg\"\n\t\t\t\t\t: request.mimeType === \"image/webp\"\n\t\t\t\t\t\t? \"webp\"\n\t\t\t\t\t\t: request.mimeType === \"image/gif\"\n\t\t\t\t\t\t\t? \"gif\"\n\t\t\t\t\t\t\t: \"png\";\n\t\t\tconst form = new FormData();\n\t\t\tform.set(\"model\", request.model);\n\t\t\tform.set(\"prompt\", request.prompt);\n\t\t\tif (request.size !== undefined) form.set(\"size\", request.size);\n\t\t\tif (request.quality !== undefined) form.set(\"quality\", request.quality);\n\t\t\tform.set(\"image\", new Blob([request.image], { type: request.mimeType }), `input.${extension}`);\n\t\t\treturn parseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/edits\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t// No content-type header: the boundary is owned by fetch.\n\t\t\t\t\t\theaders: { authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: form,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n"]}
@@ -69,7 +69,16 @@ export function createImagesClientV1(options) {
69
69
  }, signal)),
70
70
  editImage: async (request, signal) => {
71
71
  // Native FormData + Blob: fetch sets the multipart Content-Type with
72
- // its own boundary, so it must not be set manually here.
72
+ // its own boundary, so it must not be set manually here. The part
73
+ // filename follows the declared mime so the payload is not named
74
+ // .png while carrying JPEG bytes (providers sniff either way).
75
+ const extension = request.mimeType === "image/jpeg"
76
+ ? "jpg"
77
+ : request.mimeType === "image/webp"
78
+ ? "webp"
79
+ : request.mimeType === "image/gif"
80
+ ? "gif"
81
+ : "png";
73
82
  const form = new FormData();
74
83
  form.set("model", request.model);
75
84
  form.set("prompt", request.prompt);
@@ -77,7 +86,7 @@ export function createImagesClientV1(options) {
77
86
  form.set("size", request.size);
78
87
  if (request.quality !== undefined)
79
88
  form.set("quality", request.quality);
80
- form.set("image", new Blob([request.image], { type: request.mimeType }), "input.png");
89
+ form.set("image", new Blob([request.image], { type: request.mimeType }), `input.${extension}`);
81
90
  return parseResponse(await post("/images/edits", {
82
91
  method: "POST",
83
92
  // No content-type header: the boundary is owned by fetch.
@@ -1 +1 @@
1
- {"version":3,"file":"images-client.js","sourceRoot":"","sources":["../src/images-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImagesClientResponseV1, ImagesClientV1, ImagesGeneratedItemV1 } from \"./types.ts\";\n\n/** Per-request budget: image edits with large inputs legitimately take minutes. */\nconst IMAGES_REQUEST_TIMEOUT_MS_V1 = 180_000;\n\nexport interface ImagesClientOptionsV1 {\n\treadonly baseUrl: string;\n\treadonly apiKey: string;\n\treadonly fetchImpl?: typeof fetch;\n}\n\n/** Combine the caller's signal with the fixed request timeout. */\nfunction requestSignal(signal: AbortSignal | undefined): AbortSignal {\n\tconst timeout = AbortSignal.timeout(IMAGES_REQUEST_TIMEOUT_MS_V1);\n\treturn signal === undefined ? timeout : AbortSignal.any([signal, timeout]);\n}\n\nfunction trimTrailingSlash(baseUrl: string): string {\n\treturn baseUrl.replace(/\\/+$/u, \"\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Map the OpenAI images response onto the client's normalized shape:\n * `b64_json`/`b64` stays inline, `url` is left for the capability layer to\n * download; unknown item shapes are dropped rather than guessed at.\n */\nfunction parseResponse(payload: unknown): ImagesClientResponseV1 {\n\tif (!isPlainObject(payload) || !Array.isArray(payload.data))\n\t\tthrow new Error(\"images api response has no data array\");\n\tconst data: ImagesGeneratedItemV1[] = [];\n\tfor (const entry of payload.data) {\n\t\tif (!isPlainObject(entry)) continue;\n\t\tif (typeof entry.b64_json === \"string\") data.push({ b64: entry.b64_json });\n\t\telse if (typeof entry.b64 === \"string\") data.push({ b64: entry.b64 });\n\t\telse if (typeof entry.url === \"string\")\n\t\t\tdata.push({\n\t\t\t\turl: entry.url,\n\t\t\t\t...(typeof entry.mime_type === \"string\" ? { mimeType: entry.mime_type } : {}),\n\t\t\t});\n\t}\n\treturn {\n\t\tdata,\n\t\t...(isPlainObject(payload.usage) ? { usage: payload.usage as Record<string, unknown> } : {}),\n\t};\n}\n\n/**\n * Minimal OpenAI-compatible images API client (globals only: fetch, FormData,\n * Blob, AbortSignal.timeout/any on Node 22). Error messages carry the status\n * and the first 300 characters of the response body only — never the API key\n * or request headers.\n */\nexport function createImagesClientV1(options: ImagesClientOptionsV1): ImagesClientV1 {\n\tconst fetchImpl: typeof fetch = options.fetchImpl ?? fetch;\n\tconst base = trimTrailingSlash(options.baseUrl);\n\n\tasync function post(path: string, init: RequestInit, signal: AbortSignal | undefined): Promise<unknown> {\n\t\tconst response = await fetchImpl(`${base}${path}`, { ...init, signal: requestSignal(signal) });\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text().catch(() => \"\");\n\t\t\tthrow new Error(`images api ${path} failed with status ${response.status}: ${body.slice(0, 300)}`);\n\t\t}\n\t\treturn response.json();\n\t}\n\n\treturn {\n\t\tgenerateImages: async (request, signal) =>\n\t\t\tparseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/generations\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tmodel: request.model,\n\t\t\t\t\t\t\tprompt: request.prompt,\n\t\t\t\t\t\t\tn: request.n,\n\t\t\t\t\t\t\t...(request.size === undefined ? {} : { size: request.size }),\n\t\t\t\t\t\t\t...(request.quality === undefined ? {} : { quality: request.quality }),\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t),\n\t\teditImage: async (request, signal) => {\n\t\t\t// Native FormData + Blob: fetch sets the multipart Content-Type with\n\t\t\t// its own boundary, so it must not be set manually here.\n\t\t\tconst form = new FormData();\n\t\t\tform.set(\"model\", request.model);\n\t\t\tform.set(\"prompt\", request.prompt);\n\t\t\tif (request.size !== undefined) form.set(\"size\", request.size);\n\t\t\tif (request.quality !== undefined) form.set(\"quality\", request.quality);\n\t\t\tform.set(\"image\", new Blob([request.image], { type: request.mimeType }), \"input.png\");\n\t\t\treturn parseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/edits\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t// No content-type header: the boundary is owned by fetch.\n\t\t\t\t\t\theaders: { authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: form,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"images-client.js","sourceRoot":"","sources":["../src/images-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImagesClientResponseV1, ImagesClientV1, ImagesGeneratedItemV1 } from \"./types.ts\";\n\n/** Per-request budget: image edits with large inputs legitimately take minutes. */\nconst IMAGES_REQUEST_TIMEOUT_MS_V1 = 180_000;\n\nexport interface ImagesClientOptionsV1 {\n\treadonly baseUrl: string;\n\treadonly apiKey: string;\n\treadonly fetchImpl?: typeof fetch;\n}\n\n/** Combine the caller's signal with the fixed request timeout. */\nfunction requestSignal(signal: AbortSignal | undefined): AbortSignal {\n\tconst timeout = AbortSignal.timeout(IMAGES_REQUEST_TIMEOUT_MS_V1);\n\treturn signal === undefined ? timeout : AbortSignal.any([signal, timeout]);\n}\n\nfunction trimTrailingSlash(baseUrl: string): string {\n\treturn baseUrl.replace(/\\/+$/u, \"\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Map the OpenAI images response onto the client's normalized shape:\n * `b64_json`/`b64` stays inline, `url` is left for the capability layer to\n * download; unknown item shapes are dropped rather than guessed at.\n */\nfunction parseResponse(payload: unknown): ImagesClientResponseV1 {\n\tif (!isPlainObject(payload) || !Array.isArray(payload.data))\n\t\tthrow new Error(\"images api response has no data array\");\n\tconst data: ImagesGeneratedItemV1[] = [];\n\tfor (const entry of payload.data) {\n\t\tif (!isPlainObject(entry)) continue;\n\t\tif (typeof entry.b64_json === \"string\") data.push({ b64: entry.b64_json });\n\t\telse if (typeof entry.b64 === \"string\") data.push({ b64: entry.b64 });\n\t\telse if (typeof entry.url === \"string\")\n\t\t\tdata.push({\n\t\t\t\turl: entry.url,\n\t\t\t\t...(typeof entry.mime_type === \"string\" ? { mimeType: entry.mime_type } : {}),\n\t\t\t});\n\t}\n\treturn {\n\t\tdata,\n\t\t...(isPlainObject(payload.usage) ? { usage: payload.usage as Record<string, unknown> } : {}),\n\t};\n}\n\n/**\n * Minimal OpenAI-compatible images API client (globals only: fetch, FormData,\n * Blob, AbortSignal.timeout/any on Node 22). Error messages carry the status\n * and the first 300 characters of the response body only — never the API key\n * or request headers.\n */\nexport function createImagesClientV1(options: ImagesClientOptionsV1): ImagesClientV1 {\n\tconst fetchImpl: typeof fetch = options.fetchImpl ?? fetch;\n\tconst base = trimTrailingSlash(options.baseUrl);\n\n\tasync function post(path: string, init: RequestInit, signal: AbortSignal | undefined): Promise<unknown> {\n\t\tconst response = await fetchImpl(`${base}${path}`, { ...init, signal: requestSignal(signal) });\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text().catch(() => \"\");\n\t\t\tthrow new Error(`images api ${path} failed with status ${response.status}: ${body.slice(0, 300)}`);\n\t\t}\n\t\treturn response.json();\n\t}\n\n\treturn {\n\t\tgenerateImages: async (request, signal) =>\n\t\t\tparseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/generations\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tmodel: request.model,\n\t\t\t\t\t\t\tprompt: request.prompt,\n\t\t\t\t\t\t\tn: request.n,\n\t\t\t\t\t\t\t...(request.size === undefined ? {} : { size: request.size }),\n\t\t\t\t\t\t\t...(request.quality === undefined ? {} : { quality: request.quality }),\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t),\n\t\teditImage: async (request, signal) => {\n\t\t\t// Native FormData + Blob: fetch sets the multipart Content-Type with\n\t\t\t// its own boundary, so it must not be set manually here. The part\n\t\t\t// filename follows the declared mime so the payload is not named\n\t\t\t// .png while carrying JPEG bytes (providers sniff either way).\n\t\t\tconst extension =\n\t\t\t\trequest.mimeType === \"image/jpeg\"\n\t\t\t\t\t? \"jpg\"\n\t\t\t\t\t: request.mimeType === \"image/webp\"\n\t\t\t\t\t\t? \"webp\"\n\t\t\t\t\t\t: request.mimeType === \"image/gif\"\n\t\t\t\t\t\t\t? \"gif\"\n\t\t\t\t\t\t\t: \"png\";\n\t\t\tconst form = new FormData();\n\t\t\tform.set(\"model\", request.model);\n\t\t\tform.set(\"prompt\", request.prompt);\n\t\t\tif (request.size !== undefined) form.set(\"size\", request.size);\n\t\t\tif (request.quality !== undefined) form.set(\"quality\", request.quality);\n\t\t\tform.set(\"image\", new Blob([request.image], { type: request.mimeType }), `input.${extension}`);\n\t\t\treturn parseResponse(\n\t\t\t\tawait post(\n\t\t\t\t\t\"/images/edits\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t// No content-type header: the boundary is owned by fetch.\n\t\t\t\t\t\theaders: { authorization: `Bearer ${options.apiKey}` },\n\t\t\t\t\t\tbody: form,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -2,6 +2,12 @@
2
2
  export declare const IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 = 512;
3
3
  /** Hard cap for a single downloaded image response (20 MiB). */
4
4
  export declare const IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1: number;
5
+ /**
6
+ * Budget for one provider-CDN download. The generation request carries its
7
+ * own 180s timeout; without this a slow proxy route could hang a download on
8
+ * undici's default 300s body timeout.
9
+ */
10
+ export declare const IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1 = 120000;
5
11
  /** One generated image in either wire form the OpenAI-compatible API returns. */
6
12
  export interface ImagesGeneratedItemV1 {
7
13
  /** Inline base64 image payload (`data[i].b64_json` / `data[i].b64`). */
@@ -47,4 +53,18 @@ export interface ImageGenResultV1 {
47
53
  }
48
54
  /** Truncate a message to the bounded error budget; never grows the input. */
49
55
  export declare function boundedImageMessageV1(message: string): string;
56
+ /** File naming + content-part mime for one produced image. */
57
+ export interface ImageFormatV1 {
58
+ readonly extension: string;
59
+ readonly mimeType: string;
60
+ }
61
+ /**
62
+ * Detect the actual image format from magic bytes. Providers return whatever
63
+ * the model produced regardless of the requested form: gpt-image-2 b64 is
64
+ * PNG while grok-imagine-image url downloads are JPEG — naming every file
65
+ * `.png` and labeling every content part `image/png` mislabels JPEG payloads
66
+ * and breaks vision review for the revision loop. Unknown magic keeps the
67
+ * legacy PNG assumption (fail-soft, same as before this detection existed).
68
+ */
69
+ export declare function detectImageFormatV1(bytes: Uint8Array): ImageFormatV1;
50
70
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,eAAO,MAAM,qCAAqC,MAAM,CAAC;AAEzD,gEAAgE;AAChE,eAAO,MAAM,sCAAsC,QAAmB,CAAC;AAEvE,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACrC,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC9B,cAAc,CAAC,OAAO,EAAE,uBAAuB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACxG,SAAS,CAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC/F;AAED,qGAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACrE;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAI7D","sourcesContent":["/** Hard cap for user-facing error messages (truncated, never grown). */\nexport const IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 = 512;\n\n/** Hard cap for a single downloaded image response (20 MiB). */\nexport const IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1 = 20 * 1024 * 1024;\n\n/** One generated image in either wire form the OpenAI-compatible API returns. */\nexport interface ImagesGeneratedItemV1 {\n\t/** Inline base64 image payload (`data[i].b64_json` / `data[i].b64`). */\n\treadonly b64?: string;\n\t/** Remote image URL; the capability layer downloads it. */\n\treadonly url?: string;\n\treadonly mimeType?: string;\n}\n\nexport interface ImagesClientResponseV1 {\n\treadonly data: readonly ImagesGeneratedItemV1[];\n\treadonly usage?: Record<string, unknown>;\n}\n\nexport interface ImagesGenerateRequestV1 {\n\treadonly model: string;\n\treadonly prompt: string;\n\treadonly n: number;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesEditRequestV1 {\n\treadonly image: Uint8Array;\n\treadonly mimeType: string;\n\treadonly prompt: string;\n\treadonly model: string;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesClientV1 {\n\tgenerateImages(request: ImagesGenerateRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n\teditImage(request: ImagesEditRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n}\n\n/** Normalized image-generation tool result (design `基础插件生态与控制能力设计.md`). */\nexport interface ImageGenResultV1 {\n\treadonly version: 1;\n\treadonly status: \"succeeded\" | \"failed\" | \"cancelled\";\n\treadonly files: readonly string[];\n\treadonly model: string;\n\treadonly usage?: Record<string, unknown>;\n\treadonly error?: { readonly code: string; readonly message: string };\n}\n\n/** Truncate a message to the bounded error budget; never grows the input. */\nexport function boundedImageMessageV1(message: string): string {\n\treturn message.length <= IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1\n\t\t? message\n\t\t: `${message.slice(0, IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 - 3)}...`;\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,eAAO,MAAM,qCAAqC,MAAM,CAAC;AAEzD,gEAAgE;AAChE,eAAO,MAAM,sCAAsC,QAAmB,CAAC;AAEvE;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,SAAU,CAAC;AAE/D,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACrC,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC9B,cAAc,CAAC,OAAO,EAAE,uBAAuB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACxG,SAAS,CAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC/F;AAED,qGAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACrE;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAqBpE","sourcesContent":["/** Hard cap for user-facing error messages (truncated, never grown). */\nexport const IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 = 512;\n\n/** Hard cap for a single downloaded image response (20 MiB). */\nexport const IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1 = 20 * 1024 * 1024;\n\n/**\n * Budget for one provider-CDN download. The generation request carries its\n * own 180s timeout; without this a slow proxy route could hang a download on\n * undici's default 300s body timeout.\n */\nexport const IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1 = 120_000;\n\n/** One generated image in either wire form the OpenAI-compatible API returns. */\nexport interface ImagesGeneratedItemV1 {\n\t/** Inline base64 image payload (`data[i].b64_json` / `data[i].b64`). */\n\treadonly b64?: string;\n\t/** Remote image URL; the capability layer downloads it. */\n\treadonly url?: string;\n\treadonly mimeType?: string;\n}\n\nexport interface ImagesClientResponseV1 {\n\treadonly data: readonly ImagesGeneratedItemV1[];\n\treadonly usage?: Record<string, unknown>;\n}\n\nexport interface ImagesGenerateRequestV1 {\n\treadonly model: string;\n\treadonly prompt: string;\n\treadonly n: number;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesEditRequestV1 {\n\treadonly image: Uint8Array;\n\treadonly mimeType: string;\n\treadonly prompt: string;\n\treadonly model: string;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesClientV1 {\n\tgenerateImages(request: ImagesGenerateRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n\teditImage(request: ImagesEditRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n}\n\n/** Normalized image-generation tool result (design `基础插件生态与控制能力设计.md`). */\nexport interface ImageGenResultV1 {\n\treadonly version: 1;\n\treadonly status: \"succeeded\" | \"failed\" | \"cancelled\";\n\treadonly files: readonly string[];\n\treadonly model: string;\n\treadonly usage?: Record<string, unknown>;\n\treadonly error?: { readonly code: string; readonly message: string };\n}\n\n/** Truncate a message to the bounded error budget; never grows the input. */\nexport function boundedImageMessageV1(message: string): string {\n\treturn message.length <= IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1\n\t\t? message\n\t\t: `${message.slice(0, IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 - 3)}...`;\n}\n\n/** File naming + content-part mime for one produced image. */\nexport interface ImageFormatV1 {\n\treadonly extension: string;\n\treadonly mimeType: string;\n}\n\n/**\n * Detect the actual image format from magic bytes. Providers return whatever\n * the model produced regardless of the requested form: gpt-image-2 b64 is\n * PNG while grok-imagine-image url downloads are JPEG — naming every file\n * `.png` and labeling every content part `image/png` mislabels JPEG payloads\n * and breaks vision review for the revision loop. Unknown magic keeps the\n * legacy PNG assumption (fail-soft, same as before this detection existed).\n */\nexport function detectImageFormatV1(bytes: Uint8Array): ImageFormatV1 {\n\tif (bytes.length >= 4 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47)\n\t\treturn { extension: \"png\", mimeType: \"image/png\" };\n\tif (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)\n\t\treturn { extension: \"jpg\", mimeType: \"image/jpeg\" };\n\t// RIFF....WEBP: both halves of the container magic, not just the WEBP tag.\n\tif (\n\t\tbytes.length >= 12 &&\n\t\tbytes[0] === 0x52 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x46 &&\n\t\tbytes[8] === 0x57 &&\n\t\tbytes[9] === 0x45 &&\n\t\tbytes[10] === 0x42 &&\n\t\tbytes[11] === 0x50\n\t)\n\t\treturn { extension: \"webp\", mimeType: \"image/webp\" };\n\tif (bytes.length >= 3 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46)\n\t\treturn { extension: \"gif\", mimeType: \"image/gif\" };\n\treturn { extension: \"png\", mimeType: \"image/png\" };\n}\n"]}
package/dist/types.js CHANGED
@@ -2,10 +2,44 @@
2
2
  export const IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 = 512;
3
3
  /** Hard cap for a single downloaded image response (20 MiB). */
4
4
  export const IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1 = 20 * 1024 * 1024;
5
+ /**
6
+ * Budget for one provider-CDN download. The generation request carries its
7
+ * own 180s timeout; without this a slow proxy route could hang a download on
8
+ * undici's default 300s body timeout.
9
+ */
10
+ export const IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1 = 120_000;
5
11
  /** Truncate a message to the bounded error budget; never grows the input. */
6
12
  export function boundedImageMessageV1(message) {
7
13
  return message.length <= IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1
8
14
  ? message
9
15
  : `${message.slice(0, IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 - 3)}...`;
10
16
  }
17
+ /**
18
+ * Detect the actual image format from magic bytes. Providers return whatever
19
+ * the model produced regardless of the requested form: gpt-image-2 b64 is
20
+ * PNG while grok-imagine-image url downloads are JPEG — naming every file
21
+ * `.png` and labeling every content part `image/png` mislabels JPEG payloads
22
+ * and breaks vision review for the revision loop. Unknown magic keeps the
23
+ * legacy PNG assumption (fail-soft, same as before this detection existed).
24
+ */
25
+ export function detectImageFormatV1(bytes) {
26
+ if (bytes.length >= 4 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47)
27
+ return { extension: "png", mimeType: "image/png" };
28
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
29
+ return { extension: "jpg", mimeType: "image/jpeg" };
30
+ // RIFF....WEBP: both halves of the container magic, not just the WEBP tag.
31
+ if (bytes.length >= 12 &&
32
+ bytes[0] === 0x52 &&
33
+ bytes[1] === 0x49 &&
34
+ bytes[2] === 0x46 &&
35
+ bytes[3] === 0x46 &&
36
+ bytes[8] === 0x57 &&
37
+ bytes[9] === 0x45 &&
38
+ bytes[10] === 0x42 &&
39
+ bytes[11] === 0x50)
40
+ return { extension: "webp", mimeType: "image/webp" };
41
+ if (bytes.length >= 3 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46)
42
+ return { extension: "gif", mimeType: "image/gif" };
43
+ return { extension: "png", mimeType: "image/png" };
44
+ }
11
45
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,qCAAqC,GAAG,GAAG,CAAC;AAEzD,gEAAgE;AAChE,MAAM,CAAC,MAAM,sCAAsC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAgDvE,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CAAC,OAAe,EAAU;IAC9D,OAAO,OAAO,CAAC,MAAM,IAAI,qCAAqC;QAC7D,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,qCAAqC,GAAG,CAAC,CAAC,KAAK,CAAC;AAAA,CACvE","sourcesContent":["/** Hard cap for user-facing error messages (truncated, never grown). */\nexport const IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 = 512;\n\n/** Hard cap for a single downloaded image response (20 MiB). */\nexport const IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1 = 20 * 1024 * 1024;\n\n/** One generated image in either wire form the OpenAI-compatible API returns. */\nexport interface ImagesGeneratedItemV1 {\n\t/** Inline base64 image payload (`data[i].b64_json` / `data[i].b64`). */\n\treadonly b64?: string;\n\t/** Remote image URL; the capability layer downloads it. */\n\treadonly url?: string;\n\treadonly mimeType?: string;\n}\n\nexport interface ImagesClientResponseV1 {\n\treadonly data: readonly ImagesGeneratedItemV1[];\n\treadonly usage?: Record<string, unknown>;\n}\n\nexport interface ImagesGenerateRequestV1 {\n\treadonly model: string;\n\treadonly prompt: string;\n\treadonly n: number;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesEditRequestV1 {\n\treadonly image: Uint8Array;\n\treadonly mimeType: string;\n\treadonly prompt: string;\n\treadonly model: string;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesClientV1 {\n\tgenerateImages(request: ImagesGenerateRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n\teditImage(request: ImagesEditRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n}\n\n/** Normalized image-generation tool result (design `基础插件生态与控制能力设计.md`). */\nexport interface ImageGenResultV1 {\n\treadonly version: 1;\n\treadonly status: \"succeeded\" | \"failed\" | \"cancelled\";\n\treadonly files: readonly string[];\n\treadonly model: string;\n\treadonly usage?: Record<string, unknown>;\n\treadonly error?: { readonly code: string; readonly message: string };\n}\n\n/** Truncate a message to the bounded error budget; never grows the input. */\nexport function boundedImageMessageV1(message: string): string {\n\treturn message.length <= IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1\n\t\t? message\n\t\t: `${message.slice(0, IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 - 3)}...`;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,qCAAqC,GAAG,GAAG,CAAC;AAEzD,gEAAgE;AAChE,MAAM,CAAC,MAAM,sCAAsC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvE;;;;GAIG;AACH,MAAM,CAAC,MAAM,uCAAuC,GAAG,OAAO,CAAC;AAgD/D,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CAAC,OAAe,EAAU;IAC9D,OAAO,OAAO,CAAC,MAAM,IAAI,qCAAqC;QAC7D,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,qCAAqC,GAAG,CAAC,CAAC,KAAK,CAAC;AAAA,CACvE;AAQD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAiB,EAAiB;IACrE,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACxG,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACpD,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACnF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACrD,2EAA2E;IAC3E,IACC,KAAK,CAAC,MAAM,IAAI,EAAE;QAClB,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACjB,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACjB,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACjB,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACjB,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACjB,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACjB,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI;QAClB,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI;QAElB,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACtD,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;QACnF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACpD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AAAA,CACnD","sourcesContent":["/** Hard cap for user-facing error messages (truncated, never grown). */\nexport const IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 = 512;\n\n/** Hard cap for a single downloaded image response (20 MiB). */\nexport const IMAGE_GENERATION_MAX_DOWNLOAD_BYTES_V1 = 20 * 1024 * 1024;\n\n/**\n * Budget for one provider-CDN download. The generation request carries its\n * own 180s timeout; without this a slow proxy route could hang a download on\n * undici's default 300s body timeout.\n */\nexport const IMAGE_GENERATION_DOWNLOAD_TIMEOUT_MS_V1 = 120_000;\n\n/** One generated image in either wire form the OpenAI-compatible API returns. */\nexport interface ImagesGeneratedItemV1 {\n\t/** Inline base64 image payload (`data[i].b64_json` / `data[i].b64`). */\n\treadonly b64?: string;\n\t/** Remote image URL; the capability layer downloads it. */\n\treadonly url?: string;\n\treadonly mimeType?: string;\n}\n\nexport interface ImagesClientResponseV1 {\n\treadonly data: readonly ImagesGeneratedItemV1[];\n\treadonly usage?: Record<string, unknown>;\n}\n\nexport interface ImagesGenerateRequestV1 {\n\treadonly model: string;\n\treadonly prompt: string;\n\treadonly n: number;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesEditRequestV1 {\n\treadonly image: Uint8Array;\n\treadonly mimeType: string;\n\treadonly prompt: string;\n\treadonly model: string;\n\treadonly size?: string;\n\treadonly quality?: string;\n}\n\nexport interface ImagesClientV1 {\n\tgenerateImages(request: ImagesGenerateRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n\teditImage(request: ImagesEditRequestV1, signal?: AbortSignal): Promise<ImagesClientResponseV1>;\n}\n\n/** Normalized image-generation tool result (design `基础插件生态与控制能力设计.md`). */\nexport interface ImageGenResultV1 {\n\treadonly version: 1;\n\treadonly status: \"succeeded\" | \"failed\" | \"cancelled\";\n\treadonly files: readonly string[];\n\treadonly model: string;\n\treadonly usage?: Record<string, unknown>;\n\treadonly error?: { readonly code: string; readonly message: string };\n}\n\n/** Truncate a message to the bounded error budget; never grows the input. */\nexport function boundedImageMessageV1(message: string): string {\n\treturn message.length <= IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1\n\t\t? message\n\t\t: `${message.slice(0, IMAGE_GENERATION_MAX_MESSAGE_CHARS_V1 - 3)}...`;\n}\n\n/** File naming + content-part mime for one produced image. */\nexport interface ImageFormatV1 {\n\treadonly extension: string;\n\treadonly mimeType: string;\n}\n\n/**\n * Detect the actual image format from magic bytes. Providers return whatever\n * the model produced regardless of the requested form: gpt-image-2 b64 is\n * PNG while grok-imagine-image url downloads are JPEG — naming every file\n * `.png` and labeling every content part `image/png` mislabels JPEG payloads\n * and breaks vision review for the revision loop. Unknown magic keeps the\n * legacy PNG assumption (fail-soft, same as before this detection existed).\n */\nexport function detectImageFormatV1(bytes: Uint8Array): ImageFormatV1 {\n\tif (bytes.length >= 4 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47)\n\t\treturn { extension: \"png\", mimeType: \"image/png\" };\n\tif (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)\n\t\treturn { extension: \"jpg\", mimeType: \"image/jpeg\" };\n\t// RIFF....WEBP: both halves of the container magic, not just the WEBP tag.\n\tif (\n\t\tbytes.length >= 12 &&\n\t\tbytes[0] === 0x52 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x46 &&\n\t\tbytes[8] === 0x57 &&\n\t\tbytes[9] === 0x45 &&\n\t\tbytes[10] === 0x42 &&\n\t\tbytes[11] === 0x50\n\t)\n\t\treturn { extension: \"webp\", mimeType: \"image/webp\" };\n\tif (bytes.length >= 3 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46)\n\t\treturn { extension: \"gif\", mimeType: \"image/gif\" };\n\treturn { extension: \"png\", mimeType: \"image/png\" };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-agent-forge/plugin-image-generation",
3
- "version": "0.85.14",
3
+ "version": "0.85.16",
4
4
  "description": "Image generation capability plugin for Agent Forge (image_generate / image_edit tools)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,11 +46,11 @@
46
46
  "dependencies": {
47
47
  "typebox": "1.3.27",
48
48
  "undici": "8.9.0",
49
- "@ai-agent-forge/plugin-sdk": "^0.85.14"
49
+ "@ai-agent-forge/plugin-sdk": "^0.85.16"
50
50
  },
51
51
  "devDependencies": {
52
52
  "shx": "0.4.0",
53
53
  "vitest": "4.1.9",
54
- "@ai-agent-forge/agent-forge": "^0.85.14"
54
+ "@ai-agent-forge/agent-forge": "^0.85.16"
55
55
  }
56
56
  }
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "agent-forge.plugin.image-generation",
3
- "version": "0.85.14",
3
+ "version": "0.85.16",
4
4
  "apiVersion": "1-draft",
5
5
  "entry": "./dist/entry.js",
6
6
  "hostVersion": ">=0.84.4",