@crewhaus/tool-image-generation 0.1.3 → 0.1.5

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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Catalog R3 — tool-image-generation. M4.1 of the heavy-hitter plan.
3
+ *
4
+ * Exposes an `ImageGenerate(prompt, size?, style?)` tool that calls a
5
+ * remote image-generation API. Today supports OpenAI's image-generation
6
+ * endpoint (DALL-E 3 family); Replicate / Flux / SD are stubs that the
7
+ * future provider router fills in.
8
+ *
9
+ * Why a remote API rather than a local model: this is a runtime layer,
10
+ * not an inference layer. Local image generation requires GPU + 5-20GB
11
+ * model weights — not realistic to ship by default. Operators who want
12
+ * a self-hosted backend point `provider: "replicate"` at a custom
13
+ * endpoint (Replicate, Together, or their own hosted Flux/SD).
14
+ *
15
+ * Returns: a URL string (when the provider returns one) or a base64
16
+ * data URI (DALL-E with response_format=b64_json). The model can
17
+ * include the URL in its reply; vision-capable models can see the
18
+ * image inline on subsequent turns via WebFetch.
19
+ *
20
+ * Pillar 3: this tool is non-destructive (`destructive: false`) — it
21
+ * doesn't write to the user's host filesystem; it issues a remote HTTP
22
+ * request. Tool output is classified post-execution by runtime-core's
23
+ * `tool` origin classifier (the generated image URL is host-untrusted).
24
+ */
25
+ import { CrewhausError } from "@crewhaus/errors";
26
+ import type { RegisteredTool } from "@crewhaus/tool-catalog";
27
+ export type ImageGenerationProvider = "openai" | "replicate" | "mock";
28
+ export type ImageGenerationConfig = {
29
+ /** Which provider to route to. Defaults to "openai" when OPENAI_API_KEY is set. */
30
+ readonly provider?: ImageGenerationProvider;
31
+ /** Provider-specific model id. Defaults: openai → "dall-e-3", replicate → "stability-ai/sdxl". */
32
+ readonly model?: string;
33
+ /** Override the OpenAI base URL (for proxies / Azure OpenAI). */
34
+ readonly openaiBaseUrl?: string;
35
+ /** Override fetch implementation for tests. */
36
+ readonly fetch?: typeof globalThis.fetch;
37
+ };
38
+ export declare class ImageGenerationError extends CrewhausError {
39
+ readonly name = "ImageGenerationError";
40
+ constructor(message: string, cause?: unknown);
41
+ }
42
+ /**
43
+ * Section-14 style config registration. The compiled bundle calls
44
+ * `registerImageGenerationConfig({ provider, model, ... })` at boot;
45
+ * the tool's execute() reads from registeredConfig at call time so
46
+ * env-driven defaults work without re-registering.
47
+ */
48
+ export declare function registerImageGenerationConfig(config: ImageGenerationConfig): void;
49
+ export declare const imageGenerate: RegisteredTool;
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Catalog R3 — tool-image-generation. M4.1 of the heavy-hitter plan.
3
+ *
4
+ * Exposes an `ImageGenerate(prompt, size?, style?)` tool that calls a
5
+ * remote image-generation API. Today supports OpenAI's image-generation
6
+ * endpoint (DALL-E 3 family); Replicate / Flux / SD are stubs that the
7
+ * future provider router fills in.
8
+ *
9
+ * Why a remote API rather than a local model: this is a runtime layer,
10
+ * not an inference layer. Local image generation requires GPU + 5-20GB
11
+ * model weights — not realistic to ship by default. Operators who want
12
+ * a self-hosted backend point `provider: "replicate"` at a custom
13
+ * endpoint (Replicate, Together, or their own hosted Flux/SD).
14
+ *
15
+ * Returns: a URL string (when the provider returns one) or a base64
16
+ * data URI (DALL-E with response_format=b64_json). The model can
17
+ * include the URL in its reply; vision-capable models can see the
18
+ * image inline on subsequent turns via WebFetch.
19
+ *
20
+ * Pillar 3: this tool is non-destructive (`destructive: false`) — it
21
+ * doesn't write to the user's host filesystem; it issues a remote HTTP
22
+ * request. Tool output is classified post-execution by runtime-core's
23
+ * `tool` origin classifier (the generated image URL is host-untrusted).
24
+ */
25
+ import { CrewhausError } from "@crewhaus/errors";
26
+ import { buildTool } from "@crewhaus/tool-builder";
27
+ import { z } from "zod";
28
+ const inputSchema = z.object({
29
+ prompt: z
30
+ .string()
31
+ .min(1)
32
+ .describe("What to generate. Be specific; vague prompts produce vague images."),
33
+ size: z
34
+ .enum(["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"])
35
+ .optional()
36
+ .describe("Output dimensions. Defaults to 1024x1024."),
37
+ style: z.enum(["vivid", "natural"]).optional().describe("DALL-E 3 style. Defaults to vivid."),
38
+ responseFormat: z
39
+ .enum(["url", "b64_json"])
40
+ .optional()
41
+ .describe("How to return the image. Default 'url'; b64_json is useful for offline / no-CDN deployments."),
42
+ });
43
+ export class ImageGenerationError extends CrewhausError {
44
+ name = "ImageGenerationError";
45
+ constructor(message, cause) {
46
+ super("tool", message, cause);
47
+ }
48
+ }
49
+ let registeredConfig;
50
+ /**
51
+ * Section-14 style config registration. The compiled bundle calls
52
+ * `registerImageGenerationConfig({ provider, model, ... })` at boot;
53
+ * the tool's execute() reads from registeredConfig at call time so
54
+ * env-driven defaults work without re-registering.
55
+ */
56
+ export function registerImageGenerationConfig(config) {
57
+ registeredConfig = config;
58
+ }
59
+ export const imageGenerate = buildTool({
60
+ name: "ImageGenerate",
61
+ description: "Generate an image from a text prompt via a remote API. Returns a URL or base64 data URI. Use for: visual concepts, mockups, illustrations, social posts. Don't use for: 'edit this existing photo' (different tool needed).",
62
+ inputSchema,
63
+ destructive: false,
64
+ readOnly: false, // not idempotent — each call mints a new image
65
+ // Pillar 3 sink-side: the prompt is sent to a remote provider; lineage
66
+ // exfiltration via prompt smuggling is real.
67
+ scope: "external",
68
+ // FR-002 — declare the io-capability fact (remote image-gen API call).
69
+ ioCapability: "network",
70
+ execute: async (input) => {
71
+ const cfg = registeredConfig ?? {};
72
+ const provider = cfg.provider ?? defaultProvider(process.env);
73
+ const responseFormat = input.responseFormat ?? "url";
74
+ if (provider === "openai") {
75
+ return await generateOpenAI(input, cfg, responseFormat);
76
+ }
77
+ if (provider === "mock") {
78
+ return await generateMock(input);
79
+ }
80
+ throw new ImageGenerationError(`provider "${provider}" is not yet implemented in tool-image-generation v0`);
81
+ },
82
+ });
83
+ function defaultProvider(env) {
84
+ if (env["OPENAI_API_KEY"])
85
+ return "openai";
86
+ if (env["CREWHAUS_IMAGE_PROVIDER"] === "mock")
87
+ return "mock";
88
+ return "openai"; // surfaces a clear "missing OPENAI_API_KEY" error at call time
89
+ }
90
+ async function generateOpenAI(input, cfg, responseFormat) {
91
+ const apiKey = process.env["OPENAI_API_KEY"];
92
+ if (!apiKey) {
93
+ throw new ImageGenerationError("OPENAI_API_KEY is not set — required for provider=openai. Set the env var or switch to provider=mock for offline testing.");
94
+ }
95
+ const baseUrl = cfg.openaiBaseUrl ?? "https://api.openai.com/v1";
96
+ const model = cfg.model ?? "dall-e-3";
97
+ const fetchFn = cfg.fetch ?? globalThis.fetch;
98
+ const body = JSON.stringify({
99
+ model,
100
+ prompt: input.prompt,
101
+ n: 1,
102
+ size: input.size ?? "1024x1024",
103
+ response_format: responseFormat,
104
+ ...(model === "dall-e-3" ? { style: input.style ?? "vivid" } : {}),
105
+ });
106
+ const res = await fetchFn(`${baseUrl}/images/generations`, {
107
+ method: "POST",
108
+ headers: {
109
+ Authorization: `Bearer ${apiKey}`,
110
+ "Content-Type": "application/json",
111
+ },
112
+ body,
113
+ });
114
+ if (!res.ok) {
115
+ const text = await res.text().catch(() => "");
116
+ throw new ImageGenerationError(`OpenAI image-generation request failed (${res.status} ${res.statusText}): ${text.slice(0, 500)}`);
117
+ }
118
+ const json = (await res.json());
119
+ const first = json.data?.[0];
120
+ if (!first) {
121
+ throw new ImageGenerationError("OpenAI response missing data[0] — unexpected format");
122
+ }
123
+ if (responseFormat === "url") {
124
+ if (typeof first.url !== "string") {
125
+ throw new ImageGenerationError("OpenAI response missing data[0].url");
126
+ }
127
+ return `image URL: ${first.url}`;
128
+ }
129
+ if (typeof first.b64_json !== "string") {
130
+ throw new ImageGenerationError("OpenAI response missing data[0].b64_json");
131
+ }
132
+ return `image base64 data: data:image/png;base64,${first.b64_json.slice(0, 80)}…[truncated ${first.b64_json.length} bytes total]`;
133
+ }
134
+ async function generateMock(input) {
135
+ // Deterministic stub — useful for tests + offline development.
136
+ return `mock image generated for prompt: "${input.prompt}" (no provider configured; set OPENAI_API_KEY or CREWHAUS_IMAGE_PROVIDER)`;
137
+ }
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "@crewhaus/tool-image-generation",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "M4.1 — image-generation tool (DALL-E 3, Replicate, future Flux/SD). Calls remote API; no host writes; non-destructive.",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
8
  "exports": {
9
- ".": "./src/index.ts"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
10
13
  },
11
14
  "scripts": {
12
15
  "test": "bun test src"
13
16
  },
14
17
  "dependencies": {
15
- "@crewhaus/errors": "0.1.3",
16
- "@crewhaus/tool-builder": "0.1.3",
17
- "@crewhaus/tool-catalog": "0.1.3",
18
+ "@crewhaus/errors": "0.1.5",
19
+ "@crewhaus/tool-builder": "0.1.5",
20
+ "@crewhaus/tool-catalog": "0.1.5",
18
21
  "zod": "^3.23.8"
19
22
  },
20
23
  "license": "Apache-2.0",
@@ -35,5 +38,5 @@
35
38
  "publishConfig": {
36
39
  "access": "public"
37
40
  },
38
- "files": ["src", "README.md", "LICENSE", "NOTICE"]
41
+ "files": ["dist", "README.md", "LICENSE", "NOTICE"]
39
42
  }
package/src/index.test.ts DELETED
@@ -1,169 +0,0 @@
1
- /**
2
- * Tests for tool-image-generation. The mock provider gives us
3
- * deterministic test coverage; the openai provider is exercised via
4
- * an injected fetch.
5
- */
6
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
7
- import { ImageGenerationError, imageGenerate, registerImageGenerationConfig } from "./index";
8
-
9
- let savedEnv: NodeJS.ProcessEnv;
10
-
11
- beforeEach(() => {
12
- savedEnv = { ...process.env };
13
- process.env["OPENAI_API_KEY"] = undefined;
14
- registerImageGenerationConfig({});
15
- });
16
-
17
- afterEach(() => {
18
- process.env = savedEnv;
19
- registerImageGenerationConfig({});
20
- });
21
-
22
- describe("imageGenerate — schema + flags", () => {
23
- test("is non-destructive", () => {
24
- expect(imageGenerate.destructive).toBe(false);
25
- });
26
-
27
- test("name is 'ImageGenerate'", () => {
28
- expect(imageGenerate.name).toBe("ImageGenerate");
29
- });
30
-
31
- test("schema requires non-empty prompt", () => {
32
- expect(() => imageGenerate.inputSchema.parse({ prompt: "" })).toThrow();
33
- expect(() => imageGenerate.inputSchema.parse({ prompt: "ok" })).not.toThrow();
34
- });
35
-
36
- test("schema rejects unknown size", () => {
37
- expect(() => imageGenerate.inputSchema.parse({ prompt: "x", size: "999x999" })).toThrow();
38
- });
39
- });
40
-
41
- describe("imageGenerate — mock provider", () => {
42
- test("returns a deterministic stub", async () => {
43
- registerImageGenerationConfig({ provider: "mock" });
44
- const out = await imageGenerate.execute({ prompt: "a lobster on a beach" });
45
- expect(out).toContain("mock image generated");
46
- expect(out).toContain("a lobster on a beach");
47
- });
48
- });
49
-
50
- describe("imageGenerate — openai provider", () => {
51
- test("throws ImageGenerationError when OPENAI_API_KEY is unset", async () => {
52
- registerImageGenerationConfig({ provider: "openai" });
53
- await expect(imageGenerate.execute({ prompt: "x" })).rejects.toThrow(ImageGenerationError);
54
- });
55
-
56
- test("calls the openai endpoint with the configured model", async () => {
57
- process.env["OPENAI_API_KEY"] = "sk-test";
58
- let capturedUrl = "";
59
- let capturedBody = "";
60
- const mockFetch: typeof globalThis.fetch = async (input, init) => {
61
- capturedUrl = String(input);
62
- capturedBody = String(init?.body ?? "");
63
- return new Response(JSON.stringify({ data: [{ url: "https://example.com/img.png" }] }), {
64
- status: 200,
65
- headers: { "content-type": "application/json" },
66
- });
67
- };
68
- registerImageGenerationConfig({ provider: "openai", fetch: mockFetch });
69
-
70
- const out = await imageGenerate.execute({ prompt: "lobster" });
71
- expect(out).toContain("https://example.com/img.png");
72
- expect(capturedUrl).toBe("https://api.openai.com/v1/images/generations");
73
- const body = JSON.parse(capturedBody) as { model: string; prompt: string; size: string };
74
- expect(body.model).toBe("dall-e-3");
75
- expect(body.prompt).toBe("lobster");
76
- expect(body.size).toBe("1024x1024");
77
- });
78
-
79
- test("honors size + style overrides", async () => {
80
- process.env["OPENAI_API_KEY"] = "sk-test";
81
- let capturedBody = "";
82
- const mockFetch: typeof globalThis.fetch = async (_input, init) => {
83
- capturedBody = String(init?.body ?? "");
84
- return new Response(JSON.stringify({ data: [{ url: "https://example.com/x.png" }] }), {
85
- status: 200,
86
- });
87
- };
88
- registerImageGenerationConfig({ provider: "openai", fetch: mockFetch });
89
- await imageGenerate.execute({ prompt: "x", size: "1792x1024", style: "natural" });
90
- const body = JSON.parse(capturedBody) as { size: string; style: string };
91
- expect(body.size).toBe("1792x1024");
92
- expect(body.style).toBe("natural");
93
- });
94
-
95
- test("surfaces upstream errors", async () => {
96
- process.env["OPENAI_API_KEY"] = "sk-test";
97
- const mockFetch: typeof globalThis.fetch = async () =>
98
- new Response("rate limited", { status: 429, statusText: "Too Many Requests" });
99
- registerImageGenerationConfig({ provider: "openai", fetch: mockFetch });
100
- await expect(imageGenerate.execute({ prompt: "x" })).rejects.toThrow(ImageGenerationError);
101
- });
102
-
103
- test("error body read failure is swallowed by .catch(() => '')", async () => {
104
- process.env["OPENAI_API_KEY"] = "sk-test";
105
- // A non-ok response whose .text() itself rejects exercises the
106
- // `.catch(() => "")` fallback on the error path (line ~145).
107
- const badResponse = {
108
- ok: false,
109
- status: 500,
110
- statusText: "Internal Server Error",
111
- text: async () => {
112
- throw new Error("stream already consumed");
113
- },
114
- } as unknown as Response;
115
- const mockFetch: typeof globalThis.fetch = async () => badResponse;
116
- registerImageGenerationConfig({ provider: "openai", fetch: mockFetch });
117
- await expect(imageGenerate.execute({ prompt: "x" })).rejects.toThrow(
118
- /OpenAI image-generation request failed \(500/,
119
- );
120
- });
121
-
122
- test("b64_json response format returns a data URI preview", async () => {
123
- process.env["OPENAI_API_KEY"] = "sk-test";
124
- const longB64 = "A".repeat(500);
125
- const mockFetch: typeof globalThis.fetch = async () =>
126
- new Response(JSON.stringify({ data: [{ b64_json: longB64 }] }), { status: 200 });
127
- registerImageGenerationConfig({ provider: "openai", fetch: mockFetch });
128
- const out = await imageGenerate.execute({ prompt: "x", responseFormat: "b64_json" });
129
- expect(out).toContain("data:image/png;base64,");
130
- expect(out).toContain("[truncated 500 bytes total]");
131
- });
132
- });
133
-
134
- describe("imageGenerate — defaultProvider env routing (no explicit provider)", () => {
135
- test("OPENAI_API_KEY present → routes to openai (uses injected fetch)", async () => {
136
- // No `provider` in config forces execute() through defaultProvider(env).
137
- process.env["OPENAI_API_KEY"] = "sk-test";
138
- process.env["CREWHAUS_IMAGE_PROVIDER"] = undefined;
139
- let called = false;
140
- const mockFetch: typeof globalThis.fetch = async () => {
141
- called = true;
142
- return new Response(JSON.stringify({ data: [{ url: "https://example.com/auto.png" }] }), {
143
- status: 200,
144
- });
145
- };
146
- registerImageGenerationConfig({ fetch: mockFetch });
147
- const out = await imageGenerate.execute({ prompt: "auto-routed" });
148
- expect(called).toBe(true);
149
- expect(out).toContain("https://example.com/auto.png");
150
- });
151
-
152
- test("no API key but CREWHAUS_IMAGE_PROVIDER=mock → routes to mock", async () => {
153
- process.env["OPENAI_API_KEY"] = undefined;
154
- process.env["CREWHAUS_IMAGE_PROVIDER"] = "mock";
155
- registerImageGenerationConfig({});
156
- const out = await imageGenerate.execute({ prompt: "offline lobster" });
157
- expect(out).toContain("mock image generated");
158
- expect(out).toContain("offline lobster");
159
- });
160
-
161
- test("neither env var set → falls through to openai, surfacing missing-key error", async () => {
162
- process.env["OPENAI_API_KEY"] = undefined;
163
- process.env["CREWHAUS_IMAGE_PROVIDER"] = undefined;
164
- registerImageGenerationConfig({});
165
- // defaultProvider returns "openai" (the documented fall-through); execute
166
- // then throws the clear missing-key error from generateOpenAI.
167
- await expect(imageGenerate.execute({ prompt: "x" })).rejects.toThrow(ImageGenerationError);
168
- });
169
- });
package/src/index.ts DELETED
@@ -1,174 +0,0 @@
1
- /**
2
- * Catalog R3 — tool-image-generation. M4.1 of the heavy-hitter plan.
3
- *
4
- * Exposes an `ImageGenerate(prompt, size?, style?)` tool that calls a
5
- * remote image-generation API. Today supports OpenAI's image-generation
6
- * endpoint (DALL-E 3 family); Replicate / Flux / SD are stubs that the
7
- * future provider router fills in.
8
- *
9
- * Why a remote API rather than a local model: this is a runtime layer,
10
- * not an inference layer. Local image generation requires GPU + 5-20GB
11
- * model weights — not realistic to ship by default. Operators who want
12
- * a self-hosted backend point `provider: "replicate"` at a custom
13
- * endpoint (Replicate, Together, or their own hosted Flux/SD).
14
- *
15
- * Returns: a URL string (when the provider returns one) or a base64
16
- * data URI (DALL-E with response_format=b64_json). The model can
17
- * include the URL in its reply; vision-capable models can see the
18
- * image inline on subsequent turns via WebFetch.
19
- *
20
- * Pillar 3: this tool is non-destructive (`destructive: false`) — it
21
- * doesn't write to the user's host filesystem; it issues a remote HTTP
22
- * request. Tool output is classified post-execution by runtime-core's
23
- * `tool` origin classifier (the generated image URL is host-untrusted).
24
- */
25
- import { CrewhausError } from "@crewhaus/errors";
26
- import { buildTool } from "@crewhaus/tool-builder";
27
- import type { RegisteredTool } from "@crewhaus/tool-catalog";
28
- import { z } from "zod";
29
-
30
- export type ImageGenerationProvider = "openai" | "replicate" | "mock";
31
-
32
- export type ImageGenerationConfig = {
33
- /** Which provider to route to. Defaults to "openai" when OPENAI_API_KEY is set. */
34
- readonly provider?: ImageGenerationProvider;
35
- /** Provider-specific model id. Defaults: openai → "dall-e-3", replicate → "stability-ai/sdxl". */
36
- readonly model?: string;
37
- /** Override the OpenAI base URL (for proxies / Azure OpenAI). */
38
- readonly openaiBaseUrl?: string;
39
- /** Override fetch implementation for tests. */
40
- readonly fetch?: typeof globalThis.fetch;
41
- };
42
-
43
- const inputSchema = z.object({
44
- prompt: z
45
- .string()
46
- .min(1)
47
- .describe("What to generate. Be specific; vague prompts produce vague images."),
48
- size: z
49
- .enum(["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"])
50
- .optional()
51
- .describe("Output dimensions. Defaults to 1024x1024."),
52
- style: z.enum(["vivid", "natural"]).optional().describe("DALL-E 3 style. Defaults to vivid."),
53
- responseFormat: z
54
- .enum(["url", "b64_json"])
55
- .optional()
56
- .describe(
57
- "How to return the image. Default 'url'; b64_json is useful for offline / no-CDN deployments.",
58
- ),
59
- });
60
-
61
- export class ImageGenerationError extends CrewhausError {
62
- override readonly name = "ImageGenerationError";
63
- constructor(message: string, cause?: unknown) {
64
- super("tool", message, cause);
65
- }
66
- }
67
-
68
- let registeredConfig: ImageGenerationConfig | undefined;
69
-
70
- /**
71
- * Section-14 style config registration. The compiled bundle calls
72
- * `registerImageGenerationConfig({ provider, model, ... })` at boot;
73
- * the tool's execute() reads from registeredConfig at call time so
74
- * env-driven defaults work without re-registering.
75
- */
76
- export function registerImageGenerationConfig(config: ImageGenerationConfig): void {
77
- registeredConfig = config;
78
- }
79
-
80
- export const imageGenerate: RegisteredTool = buildTool({
81
- name: "ImageGenerate",
82
- description:
83
- "Generate an image from a text prompt via a remote API. Returns a URL or base64 data URI. Use for: visual concepts, mockups, illustrations, social posts. Don't use for: 'edit this existing photo' (different tool needed).",
84
- inputSchema,
85
- destructive: false,
86
- readOnly: false, // not idempotent — each call mints a new image
87
- // Pillar 3 sink-side: the prompt is sent to a remote provider; lineage
88
- // exfiltration via prompt smuggling is real.
89
- scope: "external",
90
- // FR-002 — declare the io-capability fact (remote image-gen API call).
91
- ioCapability: "network",
92
- execute: async (input) => {
93
- const cfg = registeredConfig ?? {};
94
- const provider = cfg.provider ?? defaultProvider(process.env);
95
- const responseFormat = input.responseFormat ?? "url";
96
- if (provider === "openai") {
97
- return await generateOpenAI(input, cfg, responseFormat);
98
- }
99
- if (provider === "mock") {
100
- return await generateMock(input);
101
- }
102
- throw new ImageGenerationError(
103
- `provider "${provider}" is not yet implemented in tool-image-generation v0`,
104
- );
105
- },
106
- });
107
-
108
- function defaultProvider(env: NodeJS.ProcessEnv): ImageGenerationProvider {
109
- if (env["OPENAI_API_KEY"]) return "openai";
110
- if (env["CREWHAUS_IMAGE_PROVIDER"] === "mock") return "mock";
111
- return "openai"; // surfaces a clear "missing OPENAI_API_KEY" error at call time
112
- }
113
-
114
- async function generateOpenAI(
115
- input: z.infer<typeof inputSchema>,
116
- cfg: ImageGenerationConfig,
117
- responseFormat: "url" | "b64_json",
118
- ): Promise<string> {
119
- const apiKey = process.env["OPENAI_API_KEY"];
120
- if (!apiKey) {
121
- throw new ImageGenerationError(
122
- "OPENAI_API_KEY is not set — required for provider=openai. Set the env var or switch to provider=mock for offline testing.",
123
- );
124
- }
125
- const baseUrl = cfg.openaiBaseUrl ?? "https://api.openai.com/v1";
126
- const model = cfg.model ?? "dall-e-3";
127
- const fetchFn = cfg.fetch ?? globalThis.fetch;
128
- const body = JSON.stringify({
129
- model,
130
- prompt: input.prompt,
131
- n: 1,
132
- size: input.size ?? "1024x1024",
133
- response_format: responseFormat,
134
- ...(model === "dall-e-3" ? { style: input.style ?? "vivid" } : {}),
135
- });
136
- const res = await fetchFn(`${baseUrl}/images/generations`, {
137
- method: "POST",
138
- headers: {
139
- Authorization: `Bearer ${apiKey}`,
140
- "Content-Type": "application/json",
141
- },
142
- body,
143
- });
144
- if (!res.ok) {
145
- const text = await res.text().catch(() => "");
146
- throw new ImageGenerationError(
147
- `OpenAI image-generation request failed (${res.status} ${res.statusText}): ${text.slice(0, 500)}`,
148
- );
149
- }
150
- const json = (await res.json()) as {
151
- data?: ReadonlyArray<{ url?: string; b64_json?: string }>;
152
- };
153
- const first = json.data?.[0];
154
- if (!first) {
155
- throw new ImageGenerationError("OpenAI response missing data[0] — unexpected format");
156
- }
157
- if (responseFormat === "url") {
158
- if (typeof first.url !== "string") {
159
- throw new ImageGenerationError("OpenAI response missing data[0].url");
160
- }
161
- return `image URL: ${first.url}`;
162
- }
163
- if (typeof first.b64_json !== "string") {
164
- throw new ImageGenerationError("OpenAI response missing data[0].b64_json");
165
- }
166
- return `image base64 data: data:image/png;base64,${first.b64_json.slice(0, 80)}…[truncated ${
167
- first.b64_json.length
168
- } bytes total]`;
169
- }
170
-
171
- async function generateMock(input: z.infer<typeof inputSchema>): Promise<string> {
172
- // Deterministic stub — useful for tests + offline development.
173
- return `mock image generated for prompt: "${input.prompt}" (no provider configured; set OPENAI_API_KEY or CREWHAUS_IMAGE_PROVIDER)`;
174
- }