@wopr-network/platform-core 1.7.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,46 @@
1
+ /**
2
+ * Embeddings adapter factory — instantiates all available embeddings adapters
3
+ * 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-embeddings (GPU, cheapest — not yet implemented)
11
+ * → OpenRouter ($0.02/1M tokens via text-embedding-3-small)
12
+ */
13
+ import { type OpenRouterAdapterConfig } from "./openrouter.js";
14
+ import type { ProviderAdapter } from "./types.js";
15
+ /** Top-level factory config. Only providers with an API key are instantiated. */
16
+ export interface EmbeddingsFactoryConfig {
17
+ /** OpenRouter API key. Omit or empty string to skip. */
18
+ openrouterApiKey?: string;
19
+ /** Per-adapter config overrides */
20
+ openrouter?: Omit<Partial<OpenRouterAdapterConfig>, "apiKey">;
21
+ }
22
+ /** Result of the factory — adapters + metadata for observability. */
23
+ export interface EmbeddingsFactoryResult {
24
+ /** All instantiated adapters, ordered by cost priority (cheapest first). */
25
+ adapters: ProviderAdapter[];
26
+ /** Map of adapter name → ProviderAdapter for direct registration. */
27
+ adapterMap: Map<string, ProviderAdapter>;
28
+ /** Names of providers that were skipped (missing config). */
29
+ skipped: string[];
30
+ }
31
+ /**
32
+ * Create embeddings adapters from the provided config.
33
+ *
34
+ * Returns only adapters whose API key is present and non-empty.
35
+ * Order matches arbitrage priority: cheapest first.
36
+ */
37
+ export declare function createEmbeddingsAdapters(config: EmbeddingsFactoryConfig): EmbeddingsFactoryResult;
38
+ /**
39
+ * Create embeddings adapters from environment variables.
40
+ *
41
+ * Reads API keys from:
42
+ * - OPENROUTER_API_KEY
43
+ *
44
+ * Accepts optional per-adapter overrides.
45
+ */
46
+ export declare function createEmbeddingsAdaptersFromEnv(overrides?: Omit<EmbeddingsFactoryConfig, "openrouterApiKey">): EmbeddingsFactoryResult;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Embeddings adapter factory — instantiates all available embeddings adapters
3
+ * 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-embeddings (GPU, cheapest — not yet implemented)
11
+ * → OpenRouter ($0.02/1M tokens via text-embedding-3-small)
12
+ */
13
+ import { createOpenRouterAdapter } from "./openrouter.js";
14
+ /**
15
+ * Create embeddings adapters from the provided config.
16
+ *
17
+ * Returns only adapters whose API key is present and non-empty.
18
+ * Order matches arbitrage priority: cheapest first.
19
+ */
20
+ export function createEmbeddingsAdapters(config) {
21
+ const adapters = [];
22
+ const skipped = [];
23
+ // OpenRouter — $0.02/1M tokens (text-embedding-3-small via OpenAI)
24
+ if (config.openrouterApiKey) {
25
+ adapters.push(createOpenRouterAdapter({ ...config.openrouter, apiKey: config.openrouterApiKey }));
26
+ }
27
+ else {
28
+ skipped.push("openrouter");
29
+ }
30
+ // Future: self-hosted-embeddings will go BEFORE openrouter (GPU tier, cheapest)
31
+ const adapterMap = new Map();
32
+ for (const adapter of adapters) {
33
+ adapterMap.set(adapter.name, adapter);
34
+ }
35
+ return { adapters, adapterMap, skipped };
36
+ }
37
+ /**
38
+ * Create embeddings adapters from environment variables.
39
+ *
40
+ * Reads API keys from:
41
+ * - OPENROUTER_API_KEY
42
+ *
43
+ * Accepts optional per-adapter overrides.
44
+ */
45
+ export function createEmbeddingsAdaptersFromEnv(overrides) {
46
+ return createEmbeddingsAdapters({
47
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
48
+ ...overrides,
49
+ });
50
+ }
@@ -0,0 +1,105 @@
1
+ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv } from "./embeddings-factory.js";
3
+ import * as openrouterModule from "./openrouter.js";
4
+ describe("createEmbeddingsAdapters", () => {
5
+ it("creates adapter when API key provided", () => {
6
+ const result = createEmbeddingsAdapters({
7
+ openrouterApiKey: "sk-or",
8
+ });
9
+ expect(result.adapters).toHaveLength(1);
10
+ expect(result.adapterMap.size).toBe(1);
11
+ expect(result.skipped).toHaveLength(0);
12
+ });
13
+ it("adapter is openrouter", () => {
14
+ const result = createEmbeddingsAdapters({
15
+ openrouterApiKey: "sk-or",
16
+ });
17
+ expect(result.adapters[0].name).toBe("openrouter");
18
+ });
19
+ it("skips openrouter when no API key", () => {
20
+ const result = createEmbeddingsAdapters({});
21
+ expect(result.adapters).toHaveLength(0);
22
+ expect(result.skipped).toEqual(["openrouter"]);
23
+ });
24
+ it("skips adapter with empty string key", () => {
25
+ const result = createEmbeddingsAdapters({
26
+ openrouterApiKey: "",
27
+ });
28
+ expect(result.adapters).toHaveLength(0);
29
+ expect(result.skipped).toContain("openrouter");
30
+ });
31
+ it("adapter supports embeddings capability", () => {
32
+ const result = createEmbeddingsAdapters({
33
+ openrouterApiKey: "sk-or",
34
+ });
35
+ expect(result.adapters[0].capabilities).toContain("embeddings");
36
+ });
37
+ it("adapter implements embed", () => {
38
+ const result = createEmbeddingsAdapters({
39
+ openrouterApiKey: "sk-or",
40
+ });
41
+ expect(typeof result.adapters[0].embed).toBe("function");
42
+ });
43
+ it("adapterMap keys match adapter names", () => {
44
+ const result = createEmbeddingsAdapters({
45
+ openrouterApiKey: "sk-or",
46
+ });
47
+ for (const [key, adapter] of result.adapterMap) {
48
+ expect(key).toBe(adapter.name);
49
+ }
50
+ });
51
+ it("passes per-adapter config overrides to adapter constructor", () => {
52
+ const spy = vi.spyOn(openrouterModule, "createOpenRouterAdapter");
53
+ createEmbeddingsAdapters({
54
+ openrouterApiKey: "sk-or",
55
+ openrouter: { marginMultiplier: 1.5 },
56
+ });
57
+ expect(spy).toHaveBeenCalledWith(expect.objectContaining({
58
+ apiKey: "sk-or",
59
+ marginMultiplier: 1.5,
60
+ }));
61
+ spy.mockRestore();
62
+ });
63
+ it("apiKey cannot be overridden via openrouter config", () => {
64
+ // Ensure apiKey always comes from openrouterApiKey, not from spread
65
+ const result = createEmbeddingsAdapters({
66
+ openrouterApiKey: "sk-real",
67
+ openrouter: { apiKey: "sk-evil" },
68
+ });
69
+ expect(result.adapters).toHaveLength(1);
70
+ expect(result.adapters[0].name).toBe("openrouter");
71
+ });
72
+ });
73
+ describe("createEmbeddingsAdaptersFromEnv", () => {
74
+ beforeEach(() => {
75
+ vi.unstubAllEnvs();
76
+ });
77
+ afterAll(() => {
78
+ vi.unstubAllEnvs();
79
+ });
80
+ it("reads key from environment variable", () => {
81
+ vi.stubEnv("OPENROUTER_API_KEY", "env-or");
82
+ const result = createEmbeddingsAdaptersFromEnv();
83
+ expect(result.adapters).toHaveLength(1);
84
+ expect(result.adapters[0].name).toBe("openrouter");
85
+ expect(result.skipped).toHaveLength(0);
86
+ });
87
+ it("returns empty when no env var set", () => {
88
+ vi.stubEnv("OPENROUTER_API_KEY", "");
89
+ const result = createEmbeddingsAdaptersFromEnv();
90
+ expect(result.adapters).toHaveLength(0);
91
+ expect(result.skipped).toEqual(["openrouter"]);
92
+ });
93
+ it("passes per-adapter overrides alongside env key to adapter constructor", () => {
94
+ vi.stubEnv("OPENROUTER_API_KEY", "env-or");
95
+ const spy = vi.spyOn(openrouterModule, "createOpenRouterAdapter");
96
+ createEmbeddingsAdaptersFromEnv({
97
+ openrouter: { marginMultiplier: 1.2 },
98
+ });
99
+ expect(spy).toHaveBeenCalledWith(expect.objectContaining({
100
+ apiKey: "env-or",
101
+ marginMultiplier: 1.2,
102
+ }));
103
+ spy.mockRestore();
104
+ });
105
+ });
@@ -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
+ });
@@ -73,10 +73,45 @@ export const RATE_TABLE = [
73
73
  margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
74
74
  effectivePrice: 0.00009321, // = costPerUnit * margin ($93.21 per 1M seconds ≈ $5.59 per 1M minutes)
75
75
  },
76
+ // Embeddings
77
+ // NOTE: No self-hosted embeddings adapter yet — only premium (openrouter) available.
78
+ // When self-hosted-embeddings lands, add a standard tier entry here.
79
+ {
80
+ capability: "embeddings",
81
+ tier: "premium",
82
+ provider: "openrouter",
83
+ costPerUnit: 0.00000002, // $0.02 per 1M tokens (text-embedding-3-small via OpenRouter)
84
+ billingUnit: "per-token",
85
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
86
+ effectivePrice: 0.000000026, // = costPerUnit * margin ($0.026 per 1M tokens)
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
+ },
76
111
  // Future self-hosted adapters will add more entries here:
77
112
  // - transcription: self-hosted-whisper (standard) — when GPU adapter exists
78
- // - embeddings: self-hosted-embeddings (standard) vs openrouter (premium)
79
- // - image-generation: self-hosted-sdxl (standard) vs replicate (premium)
113
+ // - embeddings: self-hosted-embeddings (standard) when GPU adapter exists
114
+ // - image-generation: self-hosted-sdxl (standard) when GPU adapter exists
80
115
  ];
81
116
  /**
82
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", () => {
@@ -20,7 +20,9 @@ export { type ChatterboxTTSAdapterConfig, createChatterboxTTSAdapter } from "./a
20
20
  export { createDeepgramAdapter, type DeepgramAdapterConfig } from "./adapters/deepgram.js";
21
21
  export { createDeepSeekAdapter, type DeepSeekAdapterConfig } from "./adapters/deepseek.js";
22
22
  export { createElevenLabsAdapter, type ElevenLabsAdapterConfig } from "./adapters/elevenlabs.js";
23
+ export { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv, type EmbeddingsFactoryConfig, type EmbeddingsFactoryResult, } from "./adapters/embeddings-factory.js";
23
24
  export { createGeminiAdapter, type GeminiAdapterConfig } from "./adapters/gemini.js";
25
+ export { createImageGenAdapters, createImageGenAdaptersFromEnv, type ImageGenFactoryConfig, type ImageGenFactoryResult, } from "./adapters/image-gen-factory.js";
24
26
  export { createKimiAdapter, type KimiAdapterConfig } from "./adapters/kimi.js";
25
27
  export { getMargin, loadMarginConfig, type MarginConfig, type MarginRule, withMarginConfig, } from "./adapters/margin-config.js";
26
28
  export { createMiniMaxAdapter, type MiniMaxAdapterConfig } from "./adapters/minimax.js";
@@ -21,7 +21,11 @@ export { createChatterboxTTSAdapter } from "./adapters/chatterbox-tts.js";
21
21
  export { createDeepgramAdapter } from "./adapters/deepgram.js";
22
22
  export { createDeepSeekAdapter } from "./adapters/deepseek.js";
23
23
  export { createElevenLabsAdapter } from "./adapters/elevenlabs.js";
24
+ // Embeddings adapter factory (WOP-2190)
25
+ export { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv, } from "./adapters/embeddings-factory.js";
24
26
  export { createGeminiAdapter } from "./adapters/gemini.js";
27
+ // Image-generation adapter factory (WOP-2188)
28
+ export { createImageGenAdapters, createImageGenAdaptersFromEnv, } from "./adapters/image-gen-factory.js";
25
29
  export { createKimiAdapter } from "./adapters/kimi.js";
26
30
  // Margin config (WOP-364)
27
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.7.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,141 @@
1
+ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv } from "./embeddings-factory.js";
3
+ import * as openrouterModule from "./openrouter.js";
4
+
5
+ describe("createEmbeddingsAdapters", () => {
6
+ it("creates adapter when API key provided", () => {
7
+ const result = createEmbeddingsAdapters({
8
+ openrouterApiKey: "sk-or",
9
+ });
10
+
11
+ expect(result.adapters).toHaveLength(1);
12
+ expect(result.adapterMap.size).toBe(1);
13
+ expect(result.skipped).toHaveLength(0);
14
+ });
15
+
16
+ it("adapter is openrouter", () => {
17
+ const result = createEmbeddingsAdapters({
18
+ openrouterApiKey: "sk-or",
19
+ });
20
+
21
+ expect(result.adapters[0].name).toBe("openrouter");
22
+ });
23
+
24
+ it("skips openrouter when no API key", () => {
25
+ const result = createEmbeddingsAdapters({});
26
+
27
+ expect(result.adapters).toHaveLength(0);
28
+ expect(result.skipped).toEqual(["openrouter"]);
29
+ });
30
+
31
+ it("skips adapter with empty string key", () => {
32
+ const result = createEmbeddingsAdapters({
33
+ openrouterApiKey: "",
34
+ });
35
+
36
+ expect(result.adapters).toHaveLength(0);
37
+ expect(result.skipped).toContain("openrouter");
38
+ });
39
+
40
+ it("adapter supports embeddings capability", () => {
41
+ const result = createEmbeddingsAdapters({
42
+ openrouterApiKey: "sk-or",
43
+ });
44
+
45
+ expect(result.adapters[0].capabilities).toContain("embeddings");
46
+ });
47
+
48
+ it("adapter implements embed", () => {
49
+ const result = createEmbeddingsAdapters({
50
+ openrouterApiKey: "sk-or",
51
+ });
52
+
53
+ expect(typeof result.adapters[0].embed).toBe("function");
54
+ });
55
+
56
+ it("adapterMap keys match adapter names", () => {
57
+ const result = createEmbeddingsAdapters({
58
+ openrouterApiKey: "sk-or",
59
+ });
60
+
61
+ for (const [key, adapter] of result.adapterMap) {
62
+ expect(key).toBe(adapter.name);
63
+ }
64
+ });
65
+
66
+ it("passes per-adapter config overrides to adapter constructor", () => {
67
+ const spy = vi.spyOn(openrouterModule, "createOpenRouterAdapter");
68
+
69
+ createEmbeddingsAdapters({
70
+ openrouterApiKey: "sk-or",
71
+ openrouter: { marginMultiplier: 1.5 },
72
+ });
73
+
74
+ expect(spy).toHaveBeenCalledWith(
75
+ expect.objectContaining({
76
+ apiKey: "sk-or",
77
+ marginMultiplier: 1.5,
78
+ }),
79
+ );
80
+
81
+ spy.mockRestore();
82
+ });
83
+
84
+ it("apiKey cannot be overridden via openrouter config", () => {
85
+ // Ensure apiKey always comes from openrouterApiKey, not from spread
86
+ const result = createEmbeddingsAdapters({
87
+ openrouterApiKey: "sk-real",
88
+ openrouter: { apiKey: "sk-evil" } as never,
89
+ });
90
+
91
+ expect(result.adapters).toHaveLength(1);
92
+ expect(result.adapters[0].name).toBe("openrouter");
93
+ });
94
+ });
95
+
96
+ describe("createEmbeddingsAdaptersFromEnv", () => {
97
+ beforeEach(() => {
98
+ vi.unstubAllEnvs();
99
+ });
100
+
101
+ afterAll(() => {
102
+ vi.unstubAllEnvs();
103
+ });
104
+
105
+ it("reads key from environment variable", () => {
106
+ vi.stubEnv("OPENROUTER_API_KEY", "env-or");
107
+
108
+ const result = createEmbeddingsAdaptersFromEnv();
109
+
110
+ expect(result.adapters).toHaveLength(1);
111
+ expect(result.adapters[0].name).toBe("openrouter");
112
+ expect(result.skipped).toHaveLength(0);
113
+ });
114
+
115
+ it("returns empty when no env var set", () => {
116
+ vi.stubEnv("OPENROUTER_API_KEY", "");
117
+
118
+ const result = createEmbeddingsAdaptersFromEnv();
119
+
120
+ expect(result.adapters).toHaveLength(0);
121
+ expect(result.skipped).toEqual(["openrouter"]);
122
+ });
123
+
124
+ it("passes per-adapter overrides alongside env key to adapter constructor", () => {
125
+ vi.stubEnv("OPENROUTER_API_KEY", "env-or");
126
+ const spy = vi.spyOn(openrouterModule, "createOpenRouterAdapter");
127
+
128
+ createEmbeddingsAdaptersFromEnv({
129
+ openrouter: { marginMultiplier: 1.2 },
130
+ });
131
+
132
+ expect(spy).toHaveBeenCalledWith(
133
+ expect.objectContaining({
134
+ apiKey: "env-or",
135
+ marginMultiplier: 1.2,
136
+ }),
137
+ );
138
+
139
+ spy.mockRestore();
140
+ });
141
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Embeddings adapter factory — instantiates all available embeddings adapters
3
+ * 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-embeddings (GPU, cheapest — not yet implemented)
11
+ * → OpenRouter ($0.02/1M tokens via text-embedding-3-small)
12
+ */
13
+
14
+ import { createOpenRouterAdapter, type OpenRouterAdapterConfig } from "./openrouter.js";
15
+ import type { ProviderAdapter } from "./types.js";
16
+
17
+ /** Top-level factory config. Only providers with an API key are instantiated. */
18
+ export interface EmbeddingsFactoryConfig {
19
+ /** OpenRouter API key. Omit or empty string to skip. */
20
+ openrouterApiKey?: string;
21
+ /** Per-adapter config overrides */
22
+ openrouter?: Omit<Partial<OpenRouterAdapterConfig>, "apiKey">;
23
+ }
24
+
25
+ /** Result of the factory — adapters + metadata for observability. */
26
+ export interface EmbeddingsFactoryResult {
27
+ /** All instantiated adapters, ordered by cost priority (cheapest first). */
28
+ adapters: ProviderAdapter[];
29
+ /** Map of adapter name → ProviderAdapter for direct registration. */
30
+ adapterMap: Map<string, ProviderAdapter>;
31
+ /** Names of providers that were skipped (missing config). */
32
+ skipped: string[];
33
+ }
34
+
35
+ /**
36
+ * Create embeddings adapters from the provided config.
37
+ *
38
+ * Returns only adapters whose API key is present and non-empty.
39
+ * Order matches arbitrage priority: cheapest first.
40
+ */
41
+ export function createEmbeddingsAdapters(config: EmbeddingsFactoryConfig): EmbeddingsFactoryResult {
42
+ const adapters: ProviderAdapter[] = [];
43
+ const skipped: string[] = [];
44
+
45
+ // OpenRouter — $0.02/1M tokens (text-embedding-3-small via OpenAI)
46
+ if (config.openrouterApiKey) {
47
+ adapters.push(createOpenRouterAdapter({ ...config.openrouter, apiKey: config.openrouterApiKey }));
48
+ } else {
49
+ skipped.push("openrouter");
50
+ }
51
+
52
+ // Future: self-hosted-embeddings will go BEFORE openrouter (GPU tier, cheapest)
53
+
54
+ const adapterMap = new Map<string, ProviderAdapter>();
55
+ for (const adapter of adapters) {
56
+ adapterMap.set(adapter.name, adapter);
57
+ }
58
+
59
+ return { adapters, adapterMap, skipped };
60
+ }
61
+
62
+ /**
63
+ * Create embeddings adapters from environment variables.
64
+ *
65
+ * Reads API keys from:
66
+ * - OPENROUTER_API_KEY
67
+ *
68
+ * Accepts optional per-adapter overrides.
69
+ */
70
+ export function createEmbeddingsAdaptersFromEnv(
71
+ overrides?: Omit<EmbeddingsFactoryConfig, "openrouterApiKey">,
72
+ ): EmbeddingsFactoryResult {
73
+ return createEmbeddingsAdapters({
74
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
75
+ ...overrides,
76
+ });
77
+ }
@@ -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
 
@@ -96,10 +96,47 @@ export const RATE_TABLE: RateEntry[] = [
96
96
  effectivePrice: 0.00009321, // = costPerUnit * margin ($93.21 per 1M seconds ≈ $5.59 per 1M minutes)
97
97
  },
98
98
 
99
+ // Embeddings
100
+ // NOTE: No self-hosted embeddings adapter yet — only premium (openrouter) available.
101
+ // When self-hosted-embeddings lands, add a standard tier entry here.
102
+ {
103
+ capability: "embeddings",
104
+ tier: "premium",
105
+ provider: "openrouter",
106
+ costPerUnit: 0.00000002, // $0.02 per 1M tokens (text-embedding-3-small via OpenRouter)
107
+ billingUnit: "per-token",
108
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
109
+ effectivePrice: 0.000000026, // = costPerUnit * margin ($0.026 per 1M tokens)
110
+ },
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
+
99
136
  // Future self-hosted adapters will add more entries here:
100
137
  // - transcription: self-hosted-whisper (standard) — when GPU adapter exists
101
- // - embeddings: self-hosted-embeddings (standard) vs openrouter (premium)
102
- // - image-generation: self-hosted-sdxl (standard) vs replicate (premium)
138
+ // - embeddings: self-hosted-embeddings (standard) when GPU adapter exists
139
+ // - image-generation: self-hosted-sdxl (standard) when GPU adapter exists
103
140
  ];
104
141
 
105
142
  /**
@@ -34,7 +34,21 @@ export { type ChatterboxTTSAdapterConfig, createChatterboxTTSAdapter } from "./a
34
34
  export { createDeepgramAdapter, type DeepgramAdapterConfig } from "./adapters/deepgram.js";
35
35
  export { createDeepSeekAdapter, type DeepSeekAdapterConfig } from "./adapters/deepseek.js";
36
36
  export { createElevenLabsAdapter, type ElevenLabsAdapterConfig } from "./adapters/elevenlabs.js";
37
+ // Embeddings adapter factory (WOP-2190)
38
+ export {
39
+ createEmbeddingsAdapters,
40
+ createEmbeddingsAdaptersFromEnv,
41
+ type EmbeddingsFactoryConfig,
42
+ type EmbeddingsFactoryResult,
43
+ } from "./adapters/embeddings-factory.js";
37
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";
38
52
  export { createKimiAdapter, type KimiAdapterConfig } from "./adapters/kimi.js";
39
53
  // Margin config (WOP-364)
40
54
  export {