@wopr-network/platform-core 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Image generation adapter factory — instantiates all available image-gen
3
+ * adapters from environment config and returns them ready to register.
4
+ *
5
+ * Only adapters with valid config are created. The factory never touches
6
+ * the database — it returns plain ProviderAdapter instances that the caller
7
+ * registers with an ArbitrageRouter or AdapterSocket.
8
+ *
9
+ * Priority order (cheapest first, when all adapters available):
10
+ * self-hosted-sdxl (GPU, cheapest — not yet implemented)
11
+ * → Replicate (~$0.019/image, SDXL on A40)
12
+ * → Nano Banana / Gemini ($0.02/image)
13
+ */
14
+ import { type NanoBananaAdapterConfig } from "./nano-banana.js";
15
+ import { type ReplicateAdapterConfig } from "./replicate.js";
16
+ import type { ProviderAdapter } from "./types.js";
17
+ /** Top-level factory config. Only providers with an API key/token are instantiated. */
18
+ export interface ImageGenFactoryConfig {
19
+ /** Gemini API key (for Nano Banana image generation). Omit or empty string to skip. */
20
+ geminiApiKey?: string;
21
+ /** Replicate API token. Omit or empty string to skip. */
22
+ replicateApiToken?: string;
23
+ /** Per-adapter config overrides */
24
+ nanoBanana?: Omit<Partial<NanoBananaAdapterConfig>, "apiKey">;
25
+ replicate?: Omit<Partial<ReplicateAdapterConfig>, "apiToken">;
26
+ }
27
+ /** Result of the factory — adapters + metadata for observability. */
28
+ export interface ImageGenFactoryResult {
29
+ /** All instantiated adapters, ordered by cost priority (cheapest first). */
30
+ adapters: ProviderAdapter[];
31
+ /** Map of adapter name → ProviderAdapter for direct registration. */
32
+ adapterMap: Map<string, ProviderAdapter>;
33
+ /** Names of providers that were skipped (missing config). */
34
+ skipped: string[];
35
+ }
36
+ /**
37
+ * Create image generation adapters from the provided config.
38
+ *
39
+ * Returns only adapters whose API key/token is present and non-empty.
40
+ * Order matches arbitrage priority: cheapest first.
41
+ */
42
+ export declare function createImageGenAdapters(config: ImageGenFactoryConfig): ImageGenFactoryResult;
43
+ /**
44
+ * Create image generation adapters from environment variables.
45
+ *
46
+ * Reads config from:
47
+ * - GEMINI_API_KEY (for Nano Banana)
48
+ * - REPLICATE_API_TOKEN
49
+ *
50
+ * Accepts optional per-adapter overrides.
51
+ */
52
+ export declare function createImageGenAdaptersFromEnv(overrides?: Omit<ImageGenFactoryConfig, "geminiApiKey" | "replicateApiToken">): ImageGenFactoryResult;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Image generation adapter factory — instantiates all available image-gen
3
+ * adapters from environment config and returns them ready to register.
4
+ *
5
+ * Only adapters with valid config are created. The factory never touches
6
+ * the database — it returns plain ProviderAdapter instances that the caller
7
+ * registers with an ArbitrageRouter or AdapterSocket.
8
+ *
9
+ * Priority order (cheapest first, when all adapters available):
10
+ * self-hosted-sdxl (GPU, cheapest — not yet implemented)
11
+ * → Replicate (~$0.019/image, SDXL on A40)
12
+ * → Nano Banana / Gemini ($0.02/image)
13
+ */
14
+ import { createNanoBananaAdapter } from "./nano-banana.js";
15
+ import { createReplicateAdapter } from "./replicate.js";
16
+ /**
17
+ * Create image generation adapters from the provided config.
18
+ *
19
+ * Returns only adapters whose API key/token is present and non-empty.
20
+ * Order matches arbitrage priority: cheapest first.
21
+ */
22
+ export function createImageGenAdapters(config) {
23
+ const adapters = [];
24
+ const skipped = [];
25
+ // Replicate — ~$0.019/image SDXL (cheapest)
26
+ if (config.replicateApiToken) {
27
+ adapters.push(createReplicateAdapter({ apiToken: config.replicateApiToken, ...config.replicate }));
28
+ }
29
+ else {
30
+ skipped.push("replicate");
31
+ }
32
+ // Nano Banana (Gemini) — $0.02/image (slightly more expensive fallback)
33
+ if (config.geminiApiKey) {
34
+ adapters.push(createNanoBananaAdapter({ apiKey: config.geminiApiKey, ...config.nanoBanana }));
35
+ }
36
+ else {
37
+ skipped.push("nano-banana");
38
+ }
39
+ const adapterMap = new Map();
40
+ for (const adapter of adapters) {
41
+ adapterMap.set(adapter.name, adapter);
42
+ }
43
+ return { adapters, adapterMap, skipped };
44
+ }
45
+ /**
46
+ * Create image generation adapters from environment variables.
47
+ *
48
+ * Reads config from:
49
+ * - GEMINI_API_KEY (for Nano Banana)
50
+ * - REPLICATE_API_TOKEN
51
+ *
52
+ * Accepts optional per-adapter overrides.
53
+ */
54
+ export function createImageGenAdaptersFromEnv(overrides) {
55
+ return createImageGenAdapters({
56
+ geminiApiKey: process.env.GEMINI_API_KEY,
57
+ replicateApiToken: process.env.REPLICATE_API_TOKEN,
58
+ ...overrides,
59
+ });
60
+ }
@@ -0,0 +1,126 @@
1
+ import { afterAll, describe, expect, it, vi } from "vitest";
2
+ import { createImageGenAdapters, createImageGenAdaptersFromEnv } from "./image-gen-factory.js";
3
+ describe("createImageGenAdapters", () => {
4
+ it("creates both adapters when all keys provided", () => {
5
+ const result = createImageGenAdapters({
6
+ geminiApiKey: "sk-gem",
7
+ replicateApiToken: "r8-rep",
8
+ });
9
+ expect(result.adapters).toHaveLength(2);
10
+ expect(result.adapterMap.size).toBe(2);
11
+ expect(result.skipped).toHaveLength(0);
12
+ });
13
+ it("returns adapters in cost-priority order (cheapest first)", () => {
14
+ const result = createImageGenAdapters({
15
+ geminiApiKey: "sk-gem",
16
+ replicateApiToken: "r8-rep",
17
+ });
18
+ const names = result.adapters.map((a) => a.name);
19
+ expect(names).toEqual(["replicate", "nano-banana"]);
20
+ });
21
+ it("skips nano-banana when no Gemini API key", () => {
22
+ const result = createImageGenAdapters({
23
+ replicateApiToken: "r8-rep",
24
+ });
25
+ expect(result.adapters).toHaveLength(1);
26
+ expect(result.adapters[0].name).toBe("replicate");
27
+ expect(result.skipped).toEqual(["nano-banana"]);
28
+ });
29
+ it("skips replicate when no API token", () => {
30
+ const result = createImageGenAdapters({
31
+ geminiApiKey: "sk-gem",
32
+ });
33
+ expect(result.adapters).toHaveLength(1);
34
+ expect(result.adapters[0].name).toBe("nano-banana");
35
+ expect(result.skipped).toEqual(["replicate"]);
36
+ });
37
+ it("skips adapters with empty string keys", () => {
38
+ const result = createImageGenAdapters({
39
+ geminiApiKey: "",
40
+ replicateApiToken: "r8-rep",
41
+ });
42
+ expect(result.adapters).toHaveLength(1);
43
+ expect(result.adapters[0].name).toBe("replicate");
44
+ expect(result.skipped).toContain("nano-banana");
45
+ });
46
+ it("returns empty result when no keys provided", () => {
47
+ const result = createImageGenAdapters({});
48
+ expect(result.adapters).toHaveLength(0);
49
+ expect(result.adapterMap.size).toBe(0);
50
+ expect(result.skipped).toEqual(["replicate", "nano-banana"]);
51
+ });
52
+ it("all adapters support image-generation capability", () => {
53
+ const result = createImageGenAdapters({
54
+ geminiApiKey: "sk-gem",
55
+ replicateApiToken: "r8-rep",
56
+ });
57
+ for (const adapter of result.adapters) {
58
+ expect(adapter.capabilities).toContain("image-generation");
59
+ }
60
+ });
61
+ it("all adapters implement generateImage", () => {
62
+ const result = createImageGenAdapters({
63
+ geminiApiKey: "sk-gem",
64
+ replicateApiToken: "r8-rep",
65
+ });
66
+ for (const adapter of result.adapters) {
67
+ expect(typeof adapter.generateImage).toBe("function");
68
+ }
69
+ });
70
+ it("adapterMap keys match adapter names", () => {
71
+ const result = createImageGenAdapters({
72
+ geminiApiKey: "sk-gem",
73
+ replicateApiToken: "r8-rep",
74
+ });
75
+ for (const [key, adapter] of result.adapterMap) {
76
+ expect(key).toBe(adapter.name);
77
+ }
78
+ });
79
+ it("passes per-adapter config overrides", () => {
80
+ const result = createImageGenAdapters({
81
+ geminiApiKey: "sk-gem",
82
+ nanoBanana: { costPerImage: 0.01 },
83
+ replicateApiToken: "r8-rep",
84
+ replicate: { marginMultiplier: 1.5 },
85
+ });
86
+ expect(result.adapters).toHaveLength(2);
87
+ });
88
+ it("creates only one adapter for single-provider config", () => {
89
+ const result = createImageGenAdapters({
90
+ geminiApiKey: "sk-gem",
91
+ });
92
+ expect(result.adapters).toHaveLength(1);
93
+ expect(result.adapters[0].name).toBe("nano-banana");
94
+ expect(result.skipped).toEqual(["replicate"]);
95
+ });
96
+ });
97
+ describe("createImageGenAdaptersFromEnv", () => {
98
+ afterAll(() => {
99
+ vi.unstubAllEnvs();
100
+ });
101
+ it("reads keys from environment variables", () => {
102
+ vi.stubEnv("GEMINI_API_KEY", "env-gem");
103
+ vi.stubEnv("REPLICATE_API_TOKEN", "env-rep");
104
+ const result = createImageGenAdaptersFromEnv();
105
+ expect(result.adapters).toHaveLength(2);
106
+ const names = result.adapters.map((a) => a.name);
107
+ expect(names).toEqual(["replicate", "nano-banana"]);
108
+ expect(result.skipped).toHaveLength(0);
109
+ });
110
+ it("returns empty when no env vars set", () => {
111
+ vi.stubEnv("GEMINI_API_KEY", "");
112
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
113
+ const result = createImageGenAdaptersFromEnv();
114
+ expect(result.adapters).toHaveLength(0);
115
+ expect(result.skipped).toHaveLength(2);
116
+ });
117
+ it("accepts per-adapter overrides alongside env keys", () => {
118
+ vi.stubEnv("GEMINI_API_KEY", "env-gem");
119
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
120
+ const result = createImageGenAdaptersFromEnv({
121
+ nanoBanana: { costPerImage: 0.01 },
122
+ });
123
+ expect(result.adapters).toHaveLength(1);
124
+ expect(result.adapters[0].name).toBe("nano-banana");
125
+ });
126
+ });
@@ -85,10 +85,33 @@ export const RATE_TABLE = [
85
85
  margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
86
86
  effectivePrice: 0.000000026, // = costPerUnit * margin ($0.026 per 1M tokens)
87
87
  },
88
+ // Image Generation
89
+ // NOTE: No self-hosted SDXL adapter yet — only premium tiers available.
90
+ // When self-hosted-sdxl lands, add a standard tier entry here.
91
+ // Multiple premium providers exist — cheapest first so lookupRate() returns
92
+ // the best rate. Use getRatesForCapability() for per-provider breakdowns.
93
+ {
94
+ capability: "image-generation",
95
+ tier: "premium",
96
+ provider: "replicate",
97
+ costPerUnit: 0.019, // ~$0.019 per image (SDXL on A40, ~8s avg at $0.0023/s)
98
+ billingUnit: "per-image",
99
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
100
+ effectivePrice: 0.0247, // = costPerUnit * margin ($24.70 per 1K images)
101
+ },
102
+ {
103
+ capability: "image-generation",
104
+ tier: "premium",
105
+ provider: "nano-banana",
106
+ costPerUnit: 0.02, // $0.02 per image (Gemini Imagen via Nano Banana)
107
+ billingUnit: "per-image",
108
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
109
+ effectivePrice: 0.026, // = costPerUnit * margin ($26.00 per 1K images)
110
+ },
88
111
  // Future self-hosted adapters will add more entries here:
89
112
  // - transcription: self-hosted-whisper (standard) — when GPU adapter exists
90
113
  // - embeddings: self-hosted-embeddings (standard) — when GPU adapter exists
91
- // - image-generation: self-hosted-sdxl (standard) vs replicate (premium)
114
+ // - image-generation: self-hosted-sdxl (standard) when GPU adapter exists
92
115
  ];
93
116
  /**
94
117
  * Look up a rate entry by capability and tier.
@@ -44,7 +44,7 @@ describe("RATE_TABLE", () => {
44
44
  const premiumEntries = RATE_TABLE.filter((e) => e.tier === "premium");
45
45
  for (const entry of premiumEntries) {
46
46
  // Third-party providers are well-known brand names
47
- const isThirdParty = ["elevenlabs", "deepgram", "openrouter", "replicate", "gemini"].includes(entry.provider);
47
+ const isThirdParty = ["elevenlabs", "deepgram", "openrouter", "replicate", "nano-banana"].includes(entry.provider);
48
48
  expect(isThirdParty).toBe(true);
49
49
  }
50
50
  });
@@ -111,7 +111,7 @@ describe("getRatesForCapability", () => {
111
111
  expect(rates[0].provider).toBe("deepgram");
112
112
  });
113
113
  it("returns empty array for non-existent capability", () => {
114
- const rates = getRatesForCapability("image-generation");
114
+ const rates = getRatesForCapability("video-generation");
115
115
  expect(rates).toHaveLength(0);
116
116
  });
117
117
  it("all returned rates have the requested capability", () => {
@@ -22,6 +22,7 @@ export { createDeepSeekAdapter, type DeepSeekAdapterConfig } from "./adapters/de
22
22
  export { createElevenLabsAdapter, type ElevenLabsAdapterConfig } from "./adapters/elevenlabs.js";
23
23
  export { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv, type EmbeddingsFactoryConfig, type EmbeddingsFactoryResult, } from "./adapters/embeddings-factory.js";
24
24
  export { createGeminiAdapter, type GeminiAdapterConfig } from "./adapters/gemini.js";
25
+ export { createImageGenAdapters, createImageGenAdaptersFromEnv, type ImageGenFactoryConfig, type ImageGenFactoryResult, } from "./adapters/image-gen-factory.js";
25
26
  export { createKimiAdapter, type KimiAdapterConfig } from "./adapters/kimi.js";
26
27
  export { getMargin, loadMarginConfig, type MarginConfig, type MarginRule, withMarginConfig, } from "./adapters/margin-config.js";
27
28
  export { createMiniMaxAdapter, type MiniMaxAdapterConfig } from "./adapters/minimax.js";
@@ -24,6 +24,8 @@ export { createElevenLabsAdapter } from "./adapters/elevenlabs.js";
24
24
  // Embeddings adapter factory (WOP-2190)
25
25
  export { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv, } from "./adapters/embeddings-factory.js";
26
26
  export { createGeminiAdapter } from "./adapters/gemini.js";
27
+ // Image-generation adapter factory (WOP-2188)
28
+ export { createImageGenAdapters, createImageGenAdaptersFromEnv, } from "./adapters/image-gen-factory.js";
27
29
  export { createKimiAdapter } from "./adapters/kimi.js";
28
30
  // Margin config (WOP-364)
29
31
  export { getMargin, loadMarginConfig, withMarginConfig, } from "./adapters/margin-config.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,158 @@
1
+ import { afterAll, describe, expect, it, vi } from "vitest";
2
+ import { createImageGenAdapters, createImageGenAdaptersFromEnv } from "./image-gen-factory.js";
3
+
4
+ describe("createImageGenAdapters", () => {
5
+ it("creates both adapters when all keys provided", () => {
6
+ const result = createImageGenAdapters({
7
+ geminiApiKey: "sk-gem",
8
+ replicateApiToken: "r8-rep",
9
+ });
10
+
11
+ expect(result.adapters).toHaveLength(2);
12
+ expect(result.adapterMap.size).toBe(2);
13
+ expect(result.skipped).toHaveLength(0);
14
+ });
15
+
16
+ it("returns adapters in cost-priority order (cheapest first)", () => {
17
+ const result = createImageGenAdapters({
18
+ geminiApiKey: "sk-gem",
19
+ replicateApiToken: "r8-rep",
20
+ });
21
+
22
+ const names = result.adapters.map((a) => a.name);
23
+ expect(names).toEqual(["replicate", "nano-banana"]);
24
+ });
25
+
26
+ it("skips nano-banana when no Gemini API key", () => {
27
+ const result = createImageGenAdapters({
28
+ replicateApiToken: "r8-rep",
29
+ });
30
+
31
+ expect(result.adapters).toHaveLength(1);
32
+ expect(result.adapters[0].name).toBe("replicate");
33
+ expect(result.skipped).toEqual(["nano-banana"]);
34
+ });
35
+
36
+ it("skips replicate when no API token", () => {
37
+ const result = createImageGenAdapters({
38
+ geminiApiKey: "sk-gem",
39
+ });
40
+
41
+ expect(result.adapters).toHaveLength(1);
42
+ expect(result.adapters[0].name).toBe("nano-banana");
43
+ expect(result.skipped).toEqual(["replicate"]);
44
+ });
45
+
46
+ it("skips adapters with empty string keys", () => {
47
+ const result = createImageGenAdapters({
48
+ geminiApiKey: "",
49
+ replicateApiToken: "r8-rep",
50
+ });
51
+
52
+ expect(result.adapters).toHaveLength(1);
53
+ expect(result.adapters[0].name).toBe("replicate");
54
+ expect(result.skipped).toContain("nano-banana");
55
+ });
56
+
57
+ it("returns empty result when no keys provided", () => {
58
+ const result = createImageGenAdapters({});
59
+
60
+ expect(result.adapters).toHaveLength(0);
61
+ expect(result.adapterMap.size).toBe(0);
62
+ expect(result.skipped).toEqual(["replicate", "nano-banana"]);
63
+ });
64
+
65
+ it("all adapters support image-generation capability", () => {
66
+ const result = createImageGenAdapters({
67
+ geminiApiKey: "sk-gem",
68
+ replicateApiToken: "r8-rep",
69
+ });
70
+
71
+ for (const adapter of result.adapters) {
72
+ expect(adapter.capabilities).toContain("image-generation");
73
+ }
74
+ });
75
+
76
+ it("all adapters implement generateImage", () => {
77
+ const result = createImageGenAdapters({
78
+ geminiApiKey: "sk-gem",
79
+ replicateApiToken: "r8-rep",
80
+ });
81
+
82
+ for (const adapter of result.adapters) {
83
+ expect(typeof adapter.generateImage).toBe("function");
84
+ }
85
+ });
86
+
87
+ it("adapterMap keys match adapter names", () => {
88
+ const result = createImageGenAdapters({
89
+ geminiApiKey: "sk-gem",
90
+ replicateApiToken: "r8-rep",
91
+ });
92
+
93
+ for (const [key, adapter] of result.adapterMap) {
94
+ expect(key).toBe(adapter.name);
95
+ }
96
+ });
97
+
98
+ it("passes per-adapter config overrides", () => {
99
+ const result = createImageGenAdapters({
100
+ geminiApiKey: "sk-gem",
101
+ nanoBanana: { costPerImage: 0.01 },
102
+ replicateApiToken: "r8-rep",
103
+ replicate: { marginMultiplier: 1.5 },
104
+ });
105
+
106
+ expect(result.adapters).toHaveLength(2);
107
+ });
108
+
109
+ it("creates only one adapter for single-provider config", () => {
110
+ const result = createImageGenAdapters({
111
+ geminiApiKey: "sk-gem",
112
+ });
113
+
114
+ expect(result.adapters).toHaveLength(1);
115
+ expect(result.adapters[0].name).toBe("nano-banana");
116
+ expect(result.skipped).toEqual(["replicate"]);
117
+ });
118
+ });
119
+
120
+ describe("createImageGenAdaptersFromEnv", () => {
121
+ afterAll(() => {
122
+ vi.unstubAllEnvs();
123
+ });
124
+
125
+ it("reads keys from environment variables", () => {
126
+ vi.stubEnv("GEMINI_API_KEY", "env-gem");
127
+ vi.stubEnv("REPLICATE_API_TOKEN", "env-rep");
128
+
129
+ const result = createImageGenAdaptersFromEnv();
130
+
131
+ expect(result.adapters).toHaveLength(2);
132
+ const names = result.adapters.map((a) => a.name);
133
+ expect(names).toEqual(["replicate", "nano-banana"]);
134
+ expect(result.skipped).toHaveLength(0);
135
+ });
136
+
137
+ it("returns empty when no env vars set", () => {
138
+ vi.stubEnv("GEMINI_API_KEY", "");
139
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
140
+
141
+ const result = createImageGenAdaptersFromEnv();
142
+
143
+ expect(result.adapters).toHaveLength(0);
144
+ expect(result.skipped).toHaveLength(2);
145
+ });
146
+
147
+ it("accepts per-adapter overrides alongside env keys", () => {
148
+ vi.stubEnv("GEMINI_API_KEY", "env-gem");
149
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
150
+
151
+ const result = createImageGenAdaptersFromEnv({
152
+ nanoBanana: { costPerImage: 0.01 },
153
+ });
154
+
155
+ expect(result.adapters).toHaveLength(1);
156
+ expect(result.adapters[0].name).toBe("nano-banana");
157
+ });
158
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Image generation adapter factory — instantiates all available image-gen
3
+ * adapters from environment config and returns them ready to register.
4
+ *
5
+ * Only adapters with valid config are created. The factory never touches
6
+ * the database — it returns plain ProviderAdapter instances that the caller
7
+ * registers with an ArbitrageRouter or AdapterSocket.
8
+ *
9
+ * Priority order (cheapest first, when all adapters available):
10
+ * self-hosted-sdxl (GPU, cheapest — not yet implemented)
11
+ * → Replicate (~$0.019/image, SDXL on A40)
12
+ * → Nano Banana / Gemini ($0.02/image)
13
+ */
14
+
15
+ import { createNanoBananaAdapter, type NanoBananaAdapterConfig } from "./nano-banana.js";
16
+ import { createReplicateAdapter, type ReplicateAdapterConfig } from "./replicate.js";
17
+ import type { ProviderAdapter } from "./types.js";
18
+
19
+ /** Top-level factory config. Only providers with an API key/token are instantiated. */
20
+ export interface ImageGenFactoryConfig {
21
+ /** Gemini API key (for Nano Banana image generation). Omit or empty string to skip. */
22
+ geminiApiKey?: string;
23
+ /** Replicate API token. Omit or empty string to skip. */
24
+ replicateApiToken?: string;
25
+ /** Per-adapter config overrides */
26
+ nanoBanana?: Omit<Partial<NanoBananaAdapterConfig>, "apiKey">;
27
+ replicate?: Omit<Partial<ReplicateAdapterConfig>, "apiToken">;
28
+ }
29
+
30
+ /** Result of the factory — adapters + metadata for observability. */
31
+ export interface ImageGenFactoryResult {
32
+ /** All instantiated adapters, ordered by cost priority (cheapest first). */
33
+ adapters: ProviderAdapter[];
34
+ /** Map of adapter name → ProviderAdapter for direct registration. */
35
+ adapterMap: Map<string, ProviderAdapter>;
36
+ /** Names of providers that were skipped (missing config). */
37
+ skipped: string[];
38
+ }
39
+
40
+ /**
41
+ * Create image generation adapters from the provided config.
42
+ *
43
+ * Returns only adapters whose API key/token is present and non-empty.
44
+ * Order matches arbitrage priority: cheapest first.
45
+ */
46
+ export function createImageGenAdapters(config: ImageGenFactoryConfig): ImageGenFactoryResult {
47
+ const adapters: ProviderAdapter[] = [];
48
+ const skipped: string[] = [];
49
+
50
+ // Replicate — ~$0.019/image SDXL (cheapest)
51
+ if (config.replicateApiToken) {
52
+ adapters.push(createReplicateAdapter({ apiToken: config.replicateApiToken, ...config.replicate }));
53
+ } else {
54
+ skipped.push("replicate");
55
+ }
56
+
57
+ // Nano Banana (Gemini) — $0.02/image (slightly more expensive fallback)
58
+ if (config.geminiApiKey) {
59
+ adapters.push(createNanoBananaAdapter({ apiKey: config.geminiApiKey, ...config.nanoBanana }));
60
+ } else {
61
+ skipped.push("nano-banana");
62
+ }
63
+
64
+ const adapterMap = new Map<string, ProviderAdapter>();
65
+ for (const adapter of adapters) {
66
+ adapterMap.set(adapter.name, adapter);
67
+ }
68
+
69
+ return { adapters, adapterMap, skipped };
70
+ }
71
+
72
+ /**
73
+ * Create image generation adapters from environment variables.
74
+ *
75
+ * Reads config from:
76
+ * - GEMINI_API_KEY (for Nano Banana)
77
+ * - REPLICATE_API_TOKEN
78
+ *
79
+ * Accepts optional per-adapter overrides.
80
+ */
81
+ export function createImageGenAdaptersFromEnv(
82
+ overrides?: Omit<ImageGenFactoryConfig, "geminiApiKey" | "replicateApiToken">,
83
+ ): ImageGenFactoryResult {
84
+ return createImageGenAdapters({
85
+ geminiApiKey: process.env.GEMINI_API_KEY,
86
+ replicateApiToken: process.env.REPLICATE_API_TOKEN,
87
+ ...overrides,
88
+ });
89
+ }
@@ -62,7 +62,9 @@ describe("RATE_TABLE", () => {
62
62
 
63
63
  for (const entry of premiumEntries) {
64
64
  // Third-party providers are well-known brand names
65
- const isThirdParty = ["elevenlabs", "deepgram", "openrouter", "replicate", "gemini"].includes(entry.provider);
65
+ const isThirdParty = ["elevenlabs", "deepgram", "openrouter", "replicate", "nano-banana"].includes(
66
+ entry.provider,
67
+ );
66
68
  expect(isThirdParty).toBe(true);
67
69
  }
68
70
  });
@@ -146,7 +148,7 @@ describe("getRatesForCapability", () => {
146
148
  });
147
149
 
148
150
  it("returns empty array for non-existent capability", () => {
149
- const rates = getRatesForCapability("image-generation" as unknown as AdapterCapability);
151
+ const rates = getRatesForCapability("video-generation" as unknown as AdapterCapability);
150
152
  expect(rates).toHaveLength(0);
151
153
  });
152
154
 
@@ -109,10 +109,34 @@ export const RATE_TABLE: RateEntry[] = [
109
109
  effectivePrice: 0.000000026, // = costPerUnit * margin ($0.026 per 1M tokens)
110
110
  },
111
111
 
112
+ // Image Generation
113
+ // NOTE: No self-hosted SDXL adapter yet — only premium tiers available.
114
+ // When self-hosted-sdxl lands, add a standard tier entry here.
115
+ // Multiple premium providers exist — cheapest first so lookupRate() returns
116
+ // the best rate. Use getRatesForCapability() for per-provider breakdowns.
117
+ {
118
+ capability: "image-generation",
119
+ tier: "premium",
120
+ provider: "replicate",
121
+ costPerUnit: 0.019, // ~$0.019 per image (SDXL on A40, ~8s avg at $0.0023/s)
122
+ billingUnit: "per-image",
123
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
124
+ effectivePrice: 0.0247, // = costPerUnit * margin ($24.70 per 1K images)
125
+ },
126
+ {
127
+ capability: "image-generation",
128
+ tier: "premium",
129
+ provider: "nano-banana",
130
+ costPerUnit: 0.02, // $0.02 per image (Gemini Imagen via Nano Banana)
131
+ billingUnit: "per-image",
132
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
133
+ effectivePrice: 0.026, // = costPerUnit * margin ($26.00 per 1K images)
134
+ },
135
+
112
136
  // Future self-hosted adapters will add more entries here:
113
137
  // - transcription: self-hosted-whisper (standard) — when GPU adapter exists
114
138
  // - embeddings: self-hosted-embeddings (standard) — when GPU adapter exists
115
- // - image-generation: self-hosted-sdxl (standard) vs replicate (premium)
139
+ // - image-generation: self-hosted-sdxl (standard) when GPU adapter exists
116
140
  ];
117
141
 
118
142
  /**
@@ -42,6 +42,13 @@ export {
42
42
  type EmbeddingsFactoryResult,
43
43
  } from "./adapters/embeddings-factory.js";
44
44
  export { createGeminiAdapter, type GeminiAdapterConfig } from "./adapters/gemini.js";
45
+ // Image-generation adapter factory (WOP-2188)
46
+ export {
47
+ createImageGenAdapters,
48
+ createImageGenAdaptersFromEnv,
49
+ type ImageGenFactoryConfig,
50
+ type ImageGenFactoryResult,
51
+ } from "./adapters/image-gen-factory.js";
45
52
  export { createKimiAdapter, type KimiAdapterConfig } from "./adapters/kimi.js";
46
53
  // Margin config (WOP-364)
47
54
  export {