@wopr-network/platform-core 1.9.0 → 1.11.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,82 @@
1
+ /**
2
+ * Unified adapter bootstrap — instantiates ALL capability-specific adapter
3
+ * factories from a single config and returns every adapter ready to register.
4
+ *
5
+ * This is the top-level entry point for standing up the full arbitrage stack.
6
+ * Call `bootstrapAdapters(config)` (or `bootstrapAdaptersFromEnv()`) once at
7
+ * startup, then register the returned adapters with an ArbitrageRouter or
8
+ * AdapterSocket.
9
+ *
10
+ * Capabilities wired:
11
+ * - text-generation (DeepSeek, Gemini, MiniMax, Kimi, OpenRouter)
12
+ * - tts (Chatterbox GPU, ElevenLabs)
13
+ * - transcription (Deepgram)
14
+ * - embeddings (OpenRouter)
15
+ * - image-generation (Replicate, Nano Banana)
16
+ */
17
+ import { type EmbeddingsFactoryConfig } from "./embeddings-factory.js";
18
+ import { type ImageGenFactoryConfig } from "./image-gen-factory.js";
19
+ import { type TextGenFactoryConfig } from "./text-gen-factory.js";
20
+ import { type TranscriptionFactoryConfig } from "./transcription-factory.js";
21
+ import { type TTSFactoryConfig } from "./tts-factory.js";
22
+ import type { ProviderAdapter } from "./types.js";
23
+ /** Combined config for all adapter factories. */
24
+ export interface BootstrapConfig {
25
+ /** Text-generation adapter config */
26
+ textGen?: TextGenFactoryConfig;
27
+ /** TTS adapter config */
28
+ tts?: TTSFactoryConfig;
29
+ /** Transcription adapter config */
30
+ transcription?: TranscriptionFactoryConfig;
31
+ /** Embeddings adapter config */
32
+ embeddings?: EmbeddingsFactoryConfig;
33
+ /** Image generation adapter config */
34
+ imageGen?: ImageGenFactoryConfig;
35
+ }
36
+ /** Result of bootstrapping all adapters. */
37
+ export interface BootstrapResult {
38
+ /**
39
+ * All instantiated adapters across all capabilities, ordered by capability then cost.
40
+ * Register these with an ArbitrageRouter or AdapterSocket.
41
+ *
42
+ * NOTE: The same provider may appear multiple times if it serves multiple
43
+ * capabilities (e.g. OpenRouter for both text-gen and embeddings). Each
44
+ * instance is independently configured. Use the per-capability factory
45
+ * results if you need a name→adapter map within a single capability.
46
+ *
47
+ * NOTE: Some adapters advertise more capabilities than the factory that
48
+ * created them (e.g. Replicate advertises text-generation and transcription
49
+ * in addition to image-generation). The ArbitrageRouter should use the
50
+ * factory-assigned capability (reflected in byCapability), not the adapter's
51
+ * own capabilities array, for routing decisions.
52
+ */
53
+ adapters: ProviderAdapter[];
54
+ /** Names of providers that were skipped (missing config), grouped by capability. */
55
+ skipped: Record<string, string[]>;
56
+ /** Summary counts for observability. */
57
+ summary: {
58
+ total: number;
59
+ skipped: number;
60
+ byCapability: Record<string, number>;
61
+ };
62
+ }
63
+ /**
64
+ * Bootstrap all adapter factories from the provided config.
65
+ *
66
+ * Instantiates every capability factory and merges the results into a
67
+ * single unified result. Only adapters with valid config are created.
68
+ */
69
+ export declare function bootstrapAdapters(config: BootstrapConfig): BootstrapResult;
70
+ /**
71
+ * Bootstrap all adapter factories from environment variables.
72
+ *
73
+ * Reads API keys from:
74
+ * - DEEPSEEK_API_KEY, GEMINI_API_KEY, MINIMAX_API_KEY, KIMI_API_KEY, OPENROUTER_API_KEY (text-gen)
75
+ * - CHATTERBOX_BASE_URL, ELEVENLABS_API_KEY (TTS)
76
+ * - DEEPGRAM_API_KEY (transcription)
77
+ * - OPENROUTER_API_KEY (embeddings)
78
+ * - REPLICATE_API_TOKEN, NANO_BANANA_API_KEY (image-gen)
79
+ *
80
+ * Accepts optional per-capability config overrides.
81
+ */
82
+ export declare function bootstrapAdaptersFromEnv(overrides?: Partial<BootstrapConfig>): BootstrapResult;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Unified adapter bootstrap — instantiates ALL capability-specific adapter
3
+ * factories from a single config and returns every adapter ready to register.
4
+ *
5
+ * This is the top-level entry point for standing up the full arbitrage stack.
6
+ * Call `bootstrapAdapters(config)` (or `bootstrapAdaptersFromEnv()`) once at
7
+ * startup, then register the returned adapters with an ArbitrageRouter or
8
+ * AdapterSocket.
9
+ *
10
+ * Capabilities wired:
11
+ * - text-generation (DeepSeek, Gemini, MiniMax, Kimi, OpenRouter)
12
+ * - tts (Chatterbox GPU, ElevenLabs)
13
+ * - transcription (Deepgram)
14
+ * - embeddings (OpenRouter)
15
+ * - image-generation (Replicate, Nano Banana)
16
+ */
17
+ import { createEmbeddingsAdapters } from "./embeddings-factory.js";
18
+ import { createImageGenAdapters } from "./image-gen-factory.js";
19
+ import { createTextGenAdapters } from "./text-gen-factory.js";
20
+ import { createTranscriptionAdapters } from "./transcription-factory.js";
21
+ import { createTTSAdapters } from "./tts-factory.js";
22
+ /**
23
+ * Bootstrap all adapter factories from the provided config.
24
+ *
25
+ * Instantiates every capability factory and merges the results into a
26
+ * single unified result. Only adapters with valid config are created.
27
+ */
28
+ export function bootstrapAdapters(config) {
29
+ const textGen = createTextGenAdapters(config.textGen ?? {});
30
+ const tts = createTTSAdapters(config.tts ?? {});
31
+ const transcription = createTranscriptionAdapters(config.transcription ?? {});
32
+ const embeddings = createEmbeddingsAdapters(config.embeddings ?? {});
33
+ const imageGen = createImageGenAdapters(config.imageGen ?? {});
34
+ const adapters = [
35
+ ...textGen.adapters,
36
+ ...tts.adapters,
37
+ ...transcription.adapters,
38
+ ...embeddings.adapters,
39
+ ...imageGen.adapters,
40
+ ];
41
+ const skipped = {};
42
+ if (textGen.skipped.length > 0)
43
+ skipped["text-generation"] = textGen.skipped;
44
+ if (tts.skipped.length > 0)
45
+ skipped.tts = tts.skipped;
46
+ if (transcription.skipped.length > 0)
47
+ skipped.transcription = transcription.skipped;
48
+ if (embeddings.skipped.length > 0)
49
+ skipped.embeddings = embeddings.skipped;
50
+ if (imageGen.skipped.length > 0)
51
+ skipped["image-generation"] = imageGen.skipped;
52
+ let totalSkipped = 0;
53
+ for (const list of Object.values(skipped)) {
54
+ totalSkipped += list.length;
55
+ }
56
+ return {
57
+ adapters,
58
+ skipped,
59
+ summary: {
60
+ total: adapters.length,
61
+ skipped: totalSkipped,
62
+ byCapability: {
63
+ "text-generation": textGen.adapters.length,
64
+ tts: tts.adapters.length,
65
+ transcription: transcription.adapters.length,
66
+ embeddings: embeddings.adapters.length,
67
+ "image-generation": imageGen.adapters.length,
68
+ },
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * Bootstrap all adapter factories from environment variables.
74
+ *
75
+ * Reads API keys from:
76
+ * - DEEPSEEK_API_KEY, GEMINI_API_KEY, MINIMAX_API_KEY, KIMI_API_KEY, OPENROUTER_API_KEY (text-gen)
77
+ * - CHATTERBOX_BASE_URL, ELEVENLABS_API_KEY (TTS)
78
+ * - DEEPGRAM_API_KEY (transcription)
79
+ * - OPENROUTER_API_KEY (embeddings)
80
+ * - REPLICATE_API_TOKEN, NANO_BANANA_API_KEY (image-gen)
81
+ *
82
+ * Accepts optional per-capability config overrides.
83
+ */
84
+ export function bootstrapAdaptersFromEnv(overrides) {
85
+ return bootstrapAdapters({
86
+ textGen: {
87
+ deepseekApiKey: process.env.DEEPSEEK_API_KEY,
88
+ geminiApiKey: process.env.GEMINI_API_KEY,
89
+ minimaxApiKey: process.env.MINIMAX_API_KEY,
90
+ kimiApiKey: process.env.KIMI_API_KEY,
91
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
92
+ ...overrides?.textGen,
93
+ },
94
+ tts: {
95
+ chatterboxBaseUrl: process.env.CHATTERBOX_BASE_URL,
96
+ elevenlabsApiKey: process.env.ELEVENLABS_API_KEY,
97
+ ...overrides?.tts,
98
+ },
99
+ transcription: {
100
+ deepgramApiKey: process.env.DEEPGRAM_API_KEY,
101
+ ...overrides?.transcription,
102
+ },
103
+ embeddings: {
104
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
105
+ ...overrides?.embeddings,
106
+ },
107
+ imageGen: {
108
+ replicateApiToken: process.env.REPLICATE_API_TOKEN,
109
+ // Separate env var from GEMINI_API_KEY (used for text-gen) to avoid
110
+ // silently enabling image-gen when only text-gen is intended.
111
+ geminiApiKey: process.env.NANO_BANANA_API_KEY,
112
+ ...overrides?.imageGen,
113
+ },
114
+ });
115
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,159 @@
1
+ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { bootstrapAdapters, bootstrapAdaptersFromEnv } from "./bootstrap.js";
3
+ describe("bootstrapAdapters", () => {
4
+ it("creates all adapters when all keys provided", () => {
5
+ const result = bootstrapAdapters({
6
+ textGen: {
7
+ deepseekApiKey: "sk-ds",
8
+ geminiApiKey: "sk-gem",
9
+ minimaxApiKey: "sk-mm",
10
+ kimiApiKey: "sk-kimi",
11
+ openrouterApiKey: "sk-or",
12
+ },
13
+ tts: {
14
+ chatterboxBaseUrl: "http://chatterbox:8000",
15
+ elevenlabsApiKey: "sk-el",
16
+ },
17
+ transcription: {
18
+ deepgramApiKey: "sk-dg",
19
+ },
20
+ embeddings: {
21
+ openrouterApiKey: "sk-or",
22
+ },
23
+ imageGen: {
24
+ replicateApiToken: "r8-rep",
25
+ geminiApiKey: "sk-gem",
26
+ },
27
+ });
28
+ // 5 text-gen + 2 TTS + 1 transcription + 1 embeddings + 2 image-gen = 11
29
+ expect(result.adapters).toHaveLength(11);
30
+ expect(result.summary.total).toBe(11);
31
+ expect(result.summary.skipped).toBe(0);
32
+ });
33
+ it("allows duplicate provider names across capabilities", () => {
34
+ const result = bootstrapAdapters({
35
+ textGen: { openrouterApiKey: "sk-or" },
36
+ embeddings: { openrouterApiKey: "sk-or" },
37
+ });
38
+ // OpenRouter appears twice — once for text-gen, once for embeddings
39
+ const openrouters = result.adapters.filter((a) => a.name === "openrouter");
40
+ expect(openrouters).toHaveLength(2);
41
+ expect(result.summary.total).toBe(2);
42
+ });
43
+ it("returns correct per-capability counts", () => {
44
+ const result = bootstrapAdapters({
45
+ textGen: { deepseekApiKey: "sk-ds" },
46
+ tts: { chatterboxBaseUrl: "http://chatterbox:8000" },
47
+ transcription: { deepgramApiKey: "sk-dg" },
48
+ embeddings: { openrouterApiKey: "sk-or" },
49
+ });
50
+ expect(result.summary.byCapability).toEqual({
51
+ "text-generation": 1,
52
+ tts: 1,
53
+ transcription: 1,
54
+ embeddings: 1,
55
+ "image-generation": 0,
56
+ });
57
+ });
58
+ it("tracks skipped providers by capability", () => {
59
+ const result = bootstrapAdapters({
60
+ textGen: { deepseekApiKey: "sk-ds" },
61
+ tts: {},
62
+ transcription: {},
63
+ embeddings: {},
64
+ });
65
+ expect(result.skipped.tts).toEqual(["chatterbox-tts", "elevenlabs"]);
66
+ expect(result.skipped.transcription).toEqual(["deepgram"]);
67
+ expect(result.skipped.embeddings).toEqual(["openrouter"]);
68
+ expect(result.skipped["text-generation"]).toEqual(["gemini", "minimax", "kimi", "openrouter"]);
69
+ expect(result.skipped["image-generation"]).toEqual(["replicate", "nano-banana"]);
70
+ });
71
+ it("returns empty result when no config provided", () => {
72
+ const result = bootstrapAdapters({});
73
+ expect(result.adapters).toHaveLength(0);
74
+ expect(result.summary.total).toBe(0);
75
+ expect(result.summary.skipped).toBeGreaterThan(0);
76
+ });
77
+ it("omits capability from skipped when all providers created", () => {
78
+ const result = bootstrapAdapters({
79
+ transcription: { deepgramApiKey: "sk-dg" },
80
+ });
81
+ expect(result.skipped.transcription).toBeUndefined();
82
+ });
83
+ it("handles partial config — only text-gen", () => {
84
+ const result = bootstrapAdapters({
85
+ textGen: { openrouterApiKey: "sk-or" },
86
+ });
87
+ expect(result.summary.byCapability["text-generation"]).toBe(1);
88
+ expect(result.summary.byCapability.tts).toBe(0);
89
+ expect(result.summary.byCapability.transcription).toBe(0);
90
+ expect(result.summary.byCapability.embeddings).toBe(0);
91
+ expect(result.summary.byCapability["image-generation"]).toBe(0);
92
+ });
93
+ it("passes per-adapter overrides through", () => {
94
+ const result = bootstrapAdapters({
95
+ textGen: {
96
+ deepseekApiKey: "sk-ds",
97
+ deepseek: { marginMultiplier: 1.5 },
98
+ },
99
+ });
100
+ expect(result.adapters).toHaveLength(1);
101
+ expect(result.adapters[0].name).toBe("deepseek");
102
+ });
103
+ });
104
+ describe("bootstrapAdaptersFromEnv", () => {
105
+ beforeEach(() => {
106
+ vi.unstubAllEnvs();
107
+ });
108
+ afterAll(() => {
109
+ vi.unstubAllEnvs();
110
+ });
111
+ it("reads all keys from environment variables", () => {
112
+ vi.stubEnv("DEEPSEEK_API_KEY", "env-ds");
113
+ vi.stubEnv("GEMINI_API_KEY", "env-gem");
114
+ vi.stubEnv("MINIMAX_API_KEY", "env-mm");
115
+ vi.stubEnv("KIMI_API_KEY", "env-kimi");
116
+ vi.stubEnv("OPENROUTER_API_KEY", "env-or");
117
+ vi.stubEnv("CHATTERBOX_BASE_URL", "http://chatterbox:8000");
118
+ vi.stubEnv("ELEVENLABS_API_KEY", "env-el");
119
+ vi.stubEnv("DEEPGRAM_API_KEY", "env-dg");
120
+ vi.stubEnv("REPLICATE_API_TOKEN", "r8-rep");
121
+ vi.stubEnv("NANO_BANANA_API_KEY", "env-nb");
122
+ const result = bootstrapAdaptersFromEnv();
123
+ // 5 text-gen + 2 TTS + 1 transcription + 1 embeddings + 2 image-gen = 11
124
+ expect(result.adapters).toHaveLength(11);
125
+ expect(result.summary.total).toBe(11);
126
+ });
127
+ it("returns empty when no env vars set", () => {
128
+ vi.stubEnv("DEEPSEEK_API_KEY", "");
129
+ vi.stubEnv("GEMINI_API_KEY", "");
130
+ vi.stubEnv("MINIMAX_API_KEY", "");
131
+ vi.stubEnv("KIMI_API_KEY", "");
132
+ vi.stubEnv("OPENROUTER_API_KEY", "");
133
+ vi.stubEnv("CHATTERBOX_BASE_URL", "");
134
+ vi.stubEnv("ELEVENLABS_API_KEY", "");
135
+ vi.stubEnv("DEEPGRAM_API_KEY", "");
136
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
137
+ vi.stubEnv("NANO_BANANA_API_KEY", "");
138
+ const result = bootstrapAdaptersFromEnv();
139
+ expect(result.adapters).toHaveLength(0);
140
+ expect(result.summary.skipped).toBeGreaterThan(0);
141
+ });
142
+ it("accepts per-capability overrides", () => {
143
+ vi.stubEnv("DEEPSEEK_API_KEY", "env-ds");
144
+ vi.stubEnv("GEMINI_API_KEY", "");
145
+ vi.stubEnv("MINIMAX_API_KEY", "");
146
+ vi.stubEnv("KIMI_API_KEY", "");
147
+ vi.stubEnv("OPENROUTER_API_KEY", "");
148
+ vi.stubEnv("CHATTERBOX_BASE_URL", "");
149
+ vi.stubEnv("ELEVENLABS_API_KEY", "");
150
+ vi.stubEnv("DEEPGRAM_API_KEY", "");
151
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
152
+ vi.stubEnv("NANO_BANANA_API_KEY", "");
153
+ const result = bootstrapAdaptersFromEnv({
154
+ textGen: { deepseek: { marginMultiplier: 2.0 } },
155
+ });
156
+ expect(result.adapters).toHaveLength(1);
157
+ expect(result.adapters[0].name).toBe("deepseek");
158
+ });
159
+ });
@@ -73,6 +73,27 @@ 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
+ // Image Generation
77
+ // NOTE: No self-hosted SDXL adapter yet — only premium tiers available.
78
+ // When self-hosted-sdxl lands, add a standard tier entry here.
79
+ {
80
+ capability: "image-generation",
81
+ tier: "premium",
82
+ provider: "nano-banana",
83
+ costPerUnit: 0.02, // $0.02 per image (Gemini Imagen via Nano Banana)
84
+ billingUnit: "per-image",
85
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
86
+ effectivePrice: 0.026, // = costPerUnit * margin ($26.00 per 1K images)
87
+ },
88
+ {
89
+ capability: "image-generation",
90
+ tier: "premium",
91
+ provider: "replicate",
92
+ costPerUnit: 0.019, // ~$0.019 per image (SDXL on A40, ~8s avg at $0.0023/s)
93
+ billingUnit: "per-image",
94
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
95
+ effectivePrice: 0.0247, // = costPerUnit * margin ($24.70 per 1K images)
96
+ },
76
97
  // Embeddings
77
98
  // NOTE: No self-hosted embeddings adapter yet — only premium (openrouter) available.
78
99
  // When self-hosted-embeddings lands, add a standard tier entry here.
@@ -83,7 +83,7 @@ describe("lookupRate", () => {
83
83
  expect(premium?.provider).toBe("openrouter");
84
84
  });
85
85
  it("returns undefined for non-existent capability", () => {
86
- const rate = lookupRate("image-generation", "standard");
86
+ const rate = lookupRate("video-generation", "standard");
87
87
  expect(rate).toBeUndefined();
88
88
  });
89
89
  it("returns undefined for non-existent tier", () => {
@@ -148,7 +148,7 @@ describe("calculateSavings", () => {
148
148
  });
149
149
  it("returns zero when capability has no premium tier", () => {
150
150
  // This would happen if a capability only has self-hosted, no third-party
151
- const savings = calculateSavings("embeddings", 1000);
151
+ const savings = calculateSavings("voice-cloning", 1000);
152
152
  expect(savings).toBe(0);
153
153
  });
154
154
  it("savings scale linearly with units", () => {
@@ -16,6 +16,7 @@
16
16
  */
17
17
  export type { ChargeOpts, ChargeResult, CheckoutOpts, CheckoutSession, IPaymentProcessor, PortalOpts, SavedPaymentMethod, SetupResult, WebhookResult as ProcessorWebhookResult, } from "@wopr-network/platform-core/billing";
18
18
  export { Credit } from "@wopr-network/platform-core/credits";
19
+ export { type BootstrapConfig, type BootstrapResult, bootstrapAdapters, bootstrapAdaptersFromEnv, } from "./adapters/bootstrap.js";
19
20
  export { type ChatterboxTTSAdapterConfig, createChatterboxTTSAdapter } from "./adapters/chatterbox-tts.js";
20
21
  export { createDeepgramAdapter, type DeepgramAdapterConfig } from "./adapters/deepgram.js";
21
22
  export { createDeepSeekAdapter, type DeepSeekAdapterConfig } from "./adapters/deepseek.js";
@@ -16,6 +16,8 @@
16
16
  */
17
17
  // Credit value object (WOP-983)
18
18
  export { Credit } from "@wopr-network/platform-core/credits";
19
+ // Unified adapter bootstrap (WOP-2194)
20
+ export { bootstrapAdapters, bootstrapAdaptersFromEnv, } from "./adapters/bootstrap.js";
19
21
  // Adapters (WOP-301, WOP-353, WOP-377, WOP-386, WOP-387, WOP-497)
20
22
  export { createChatterboxTTSAdapter } from "./adapters/chatterbox-tts.js";
21
23
  export { createDeepgramAdapter } from "./adapters/deepgram.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,186 @@
1
+ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { bootstrapAdapters, bootstrapAdaptersFromEnv } from "./bootstrap.js";
3
+
4
+ describe("bootstrapAdapters", () => {
5
+ it("creates all adapters when all keys provided", () => {
6
+ const result = bootstrapAdapters({
7
+ textGen: {
8
+ deepseekApiKey: "sk-ds",
9
+ geminiApiKey: "sk-gem",
10
+ minimaxApiKey: "sk-mm",
11
+ kimiApiKey: "sk-kimi",
12
+ openrouterApiKey: "sk-or",
13
+ },
14
+ tts: {
15
+ chatterboxBaseUrl: "http://chatterbox:8000",
16
+ elevenlabsApiKey: "sk-el",
17
+ },
18
+ transcription: {
19
+ deepgramApiKey: "sk-dg",
20
+ },
21
+ embeddings: {
22
+ openrouterApiKey: "sk-or",
23
+ },
24
+ imageGen: {
25
+ replicateApiToken: "r8-rep",
26
+ geminiApiKey: "sk-gem",
27
+ },
28
+ });
29
+
30
+ // 5 text-gen + 2 TTS + 1 transcription + 1 embeddings + 2 image-gen = 11
31
+ expect(result.adapters).toHaveLength(11);
32
+ expect(result.summary.total).toBe(11);
33
+ expect(result.summary.skipped).toBe(0);
34
+ });
35
+
36
+ it("allows duplicate provider names across capabilities", () => {
37
+ const result = bootstrapAdapters({
38
+ textGen: { openrouterApiKey: "sk-or" },
39
+ embeddings: { openrouterApiKey: "sk-or" },
40
+ });
41
+
42
+ // OpenRouter appears twice — once for text-gen, once for embeddings
43
+ const openrouters = result.adapters.filter((a) => a.name === "openrouter");
44
+ expect(openrouters).toHaveLength(2);
45
+ expect(result.summary.total).toBe(2);
46
+ });
47
+
48
+ it("returns correct per-capability counts", () => {
49
+ const result = bootstrapAdapters({
50
+ textGen: { deepseekApiKey: "sk-ds" },
51
+ tts: { chatterboxBaseUrl: "http://chatterbox:8000" },
52
+ transcription: { deepgramApiKey: "sk-dg" },
53
+ embeddings: { openrouterApiKey: "sk-or" },
54
+ });
55
+
56
+ expect(result.summary.byCapability).toEqual({
57
+ "text-generation": 1,
58
+ tts: 1,
59
+ transcription: 1,
60
+ embeddings: 1,
61
+ "image-generation": 0,
62
+ });
63
+ });
64
+
65
+ it("tracks skipped providers by capability", () => {
66
+ const result = bootstrapAdapters({
67
+ textGen: { deepseekApiKey: "sk-ds" },
68
+ tts: {},
69
+ transcription: {},
70
+ embeddings: {},
71
+ });
72
+
73
+ expect(result.skipped.tts).toEqual(["chatterbox-tts", "elevenlabs"]);
74
+ expect(result.skipped.transcription).toEqual(["deepgram"]);
75
+ expect(result.skipped.embeddings).toEqual(["openrouter"]);
76
+ expect(result.skipped["text-generation"]).toEqual(["gemini", "minimax", "kimi", "openrouter"]);
77
+ expect(result.skipped["image-generation"]).toEqual(["replicate", "nano-banana"]);
78
+ });
79
+
80
+ it("returns empty result when no config provided", () => {
81
+ const result = bootstrapAdapters({});
82
+
83
+ expect(result.adapters).toHaveLength(0);
84
+ expect(result.summary.total).toBe(0);
85
+ expect(result.summary.skipped).toBeGreaterThan(0);
86
+ });
87
+
88
+ it("omits capability from skipped when all providers created", () => {
89
+ const result = bootstrapAdapters({
90
+ transcription: { deepgramApiKey: "sk-dg" },
91
+ });
92
+
93
+ expect(result.skipped.transcription).toBeUndefined();
94
+ });
95
+
96
+ it("handles partial config — only text-gen", () => {
97
+ const result = bootstrapAdapters({
98
+ textGen: { openrouterApiKey: "sk-or" },
99
+ });
100
+
101
+ expect(result.summary.byCapability["text-generation"]).toBe(1);
102
+ expect(result.summary.byCapability.tts).toBe(0);
103
+ expect(result.summary.byCapability.transcription).toBe(0);
104
+ expect(result.summary.byCapability.embeddings).toBe(0);
105
+ expect(result.summary.byCapability["image-generation"]).toBe(0);
106
+ });
107
+
108
+ it("passes per-adapter overrides through", () => {
109
+ const result = bootstrapAdapters({
110
+ textGen: {
111
+ deepseekApiKey: "sk-ds",
112
+ deepseek: { marginMultiplier: 1.5 },
113
+ },
114
+ });
115
+
116
+ expect(result.adapters).toHaveLength(1);
117
+ expect(result.adapters[0].name).toBe("deepseek");
118
+ });
119
+ });
120
+
121
+ describe("bootstrapAdaptersFromEnv", () => {
122
+ beforeEach(() => {
123
+ vi.unstubAllEnvs();
124
+ });
125
+
126
+ afterAll(() => {
127
+ vi.unstubAllEnvs();
128
+ });
129
+
130
+ it("reads all keys from environment variables", () => {
131
+ vi.stubEnv("DEEPSEEK_API_KEY", "env-ds");
132
+ vi.stubEnv("GEMINI_API_KEY", "env-gem");
133
+ vi.stubEnv("MINIMAX_API_KEY", "env-mm");
134
+ vi.stubEnv("KIMI_API_KEY", "env-kimi");
135
+ vi.stubEnv("OPENROUTER_API_KEY", "env-or");
136
+ vi.stubEnv("CHATTERBOX_BASE_URL", "http://chatterbox:8000");
137
+ vi.stubEnv("ELEVENLABS_API_KEY", "env-el");
138
+ vi.stubEnv("DEEPGRAM_API_KEY", "env-dg");
139
+ vi.stubEnv("REPLICATE_API_TOKEN", "r8-rep");
140
+ vi.stubEnv("NANO_BANANA_API_KEY", "env-nb");
141
+
142
+ const result = bootstrapAdaptersFromEnv();
143
+
144
+ // 5 text-gen + 2 TTS + 1 transcription + 1 embeddings + 2 image-gen = 11
145
+ expect(result.adapters).toHaveLength(11);
146
+ expect(result.summary.total).toBe(11);
147
+ });
148
+
149
+ it("returns empty when no env vars set", () => {
150
+ vi.stubEnv("DEEPSEEK_API_KEY", "");
151
+ vi.stubEnv("GEMINI_API_KEY", "");
152
+ vi.stubEnv("MINIMAX_API_KEY", "");
153
+ vi.stubEnv("KIMI_API_KEY", "");
154
+ vi.stubEnv("OPENROUTER_API_KEY", "");
155
+ vi.stubEnv("CHATTERBOX_BASE_URL", "");
156
+ vi.stubEnv("ELEVENLABS_API_KEY", "");
157
+ vi.stubEnv("DEEPGRAM_API_KEY", "");
158
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
159
+ vi.stubEnv("NANO_BANANA_API_KEY", "");
160
+
161
+ const result = bootstrapAdaptersFromEnv();
162
+
163
+ expect(result.adapters).toHaveLength(0);
164
+ expect(result.summary.skipped).toBeGreaterThan(0);
165
+ });
166
+
167
+ it("accepts per-capability overrides", () => {
168
+ vi.stubEnv("DEEPSEEK_API_KEY", "env-ds");
169
+ vi.stubEnv("GEMINI_API_KEY", "");
170
+ vi.stubEnv("MINIMAX_API_KEY", "");
171
+ vi.stubEnv("KIMI_API_KEY", "");
172
+ vi.stubEnv("OPENROUTER_API_KEY", "");
173
+ vi.stubEnv("CHATTERBOX_BASE_URL", "");
174
+ vi.stubEnv("ELEVENLABS_API_KEY", "");
175
+ vi.stubEnv("DEEPGRAM_API_KEY", "");
176
+ vi.stubEnv("REPLICATE_API_TOKEN", "");
177
+ vi.stubEnv("NANO_BANANA_API_KEY", "");
178
+
179
+ const result = bootstrapAdaptersFromEnv({
180
+ textGen: { deepseek: { marginMultiplier: 2.0 } },
181
+ });
182
+
183
+ expect(result.adapters).toHaveLength(1);
184
+ expect(result.adapters[0].name).toBe("deepseek");
185
+ });
186
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Unified adapter bootstrap — instantiates ALL capability-specific adapter
3
+ * factories from a single config and returns every adapter ready to register.
4
+ *
5
+ * This is the top-level entry point for standing up the full arbitrage stack.
6
+ * Call `bootstrapAdapters(config)` (or `bootstrapAdaptersFromEnv()`) once at
7
+ * startup, then register the returned adapters with an ArbitrageRouter or
8
+ * AdapterSocket.
9
+ *
10
+ * Capabilities wired:
11
+ * - text-generation (DeepSeek, Gemini, MiniMax, Kimi, OpenRouter)
12
+ * - tts (Chatterbox GPU, ElevenLabs)
13
+ * - transcription (Deepgram)
14
+ * - embeddings (OpenRouter)
15
+ * - image-generation (Replicate, Nano Banana)
16
+ */
17
+
18
+ import { createEmbeddingsAdapters, type EmbeddingsFactoryConfig } from "./embeddings-factory.js";
19
+ import { createImageGenAdapters, type ImageGenFactoryConfig } from "./image-gen-factory.js";
20
+ import { createTextGenAdapters, type TextGenFactoryConfig } from "./text-gen-factory.js";
21
+ import { createTranscriptionAdapters, type TranscriptionFactoryConfig } from "./transcription-factory.js";
22
+ import { createTTSAdapters, type TTSFactoryConfig } from "./tts-factory.js";
23
+ import type { ProviderAdapter } from "./types.js";
24
+
25
+ /** Combined config for all adapter factories. */
26
+ export interface BootstrapConfig {
27
+ /** Text-generation adapter config */
28
+ textGen?: TextGenFactoryConfig;
29
+ /** TTS adapter config */
30
+ tts?: TTSFactoryConfig;
31
+ /** Transcription adapter config */
32
+ transcription?: TranscriptionFactoryConfig;
33
+ /** Embeddings adapter config */
34
+ embeddings?: EmbeddingsFactoryConfig;
35
+ /** Image generation adapter config */
36
+ imageGen?: ImageGenFactoryConfig;
37
+ }
38
+
39
+ /** Result of bootstrapping all adapters. */
40
+ export interface BootstrapResult {
41
+ /**
42
+ * All instantiated adapters across all capabilities, ordered by capability then cost.
43
+ * Register these with an ArbitrageRouter or AdapterSocket.
44
+ *
45
+ * NOTE: The same provider may appear multiple times if it serves multiple
46
+ * capabilities (e.g. OpenRouter for both text-gen and embeddings). Each
47
+ * instance is independently configured. Use the per-capability factory
48
+ * results if you need a name→adapter map within a single capability.
49
+ *
50
+ * NOTE: Some adapters advertise more capabilities than the factory that
51
+ * created them (e.g. Replicate advertises text-generation and transcription
52
+ * in addition to image-generation). The ArbitrageRouter should use the
53
+ * factory-assigned capability (reflected in byCapability), not the adapter's
54
+ * own capabilities array, for routing decisions.
55
+ */
56
+ adapters: ProviderAdapter[];
57
+ /** Names of providers that were skipped (missing config), grouped by capability. */
58
+ skipped: Record<string, string[]>;
59
+ /** Summary counts for observability. */
60
+ summary: {
61
+ total: number;
62
+ skipped: number;
63
+ byCapability: Record<string, number>;
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Bootstrap all adapter factories from the provided config.
69
+ *
70
+ * Instantiates every capability factory and merges the results into a
71
+ * single unified result. Only adapters with valid config are created.
72
+ */
73
+ export function bootstrapAdapters(config: BootstrapConfig): BootstrapResult {
74
+ const textGen = createTextGenAdapters(config.textGen ?? {});
75
+ const tts = createTTSAdapters(config.tts ?? {});
76
+ const transcription = createTranscriptionAdapters(config.transcription ?? {});
77
+ const embeddings = createEmbeddingsAdapters(config.embeddings ?? {});
78
+ const imageGen = createImageGenAdapters(config.imageGen ?? {});
79
+
80
+ const adapters: ProviderAdapter[] = [
81
+ ...textGen.adapters,
82
+ ...tts.adapters,
83
+ ...transcription.adapters,
84
+ ...embeddings.adapters,
85
+ ...imageGen.adapters,
86
+ ];
87
+
88
+ const skipped: Record<string, string[]> = {};
89
+ if (textGen.skipped.length > 0) skipped["text-generation"] = textGen.skipped;
90
+ if (tts.skipped.length > 0) skipped.tts = tts.skipped;
91
+ if (transcription.skipped.length > 0) skipped.transcription = transcription.skipped;
92
+ if (embeddings.skipped.length > 0) skipped.embeddings = embeddings.skipped;
93
+ if (imageGen.skipped.length > 0) skipped["image-generation"] = imageGen.skipped;
94
+
95
+ let totalSkipped = 0;
96
+ for (const list of Object.values(skipped)) {
97
+ totalSkipped += list.length;
98
+ }
99
+
100
+ return {
101
+ adapters,
102
+ skipped,
103
+ summary: {
104
+ total: adapters.length,
105
+ skipped: totalSkipped,
106
+ byCapability: {
107
+ "text-generation": textGen.adapters.length,
108
+ tts: tts.adapters.length,
109
+ transcription: transcription.adapters.length,
110
+ embeddings: embeddings.adapters.length,
111
+ "image-generation": imageGen.adapters.length,
112
+ },
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Bootstrap all adapter factories from environment variables.
119
+ *
120
+ * Reads API keys from:
121
+ * - DEEPSEEK_API_KEY, GEMINI_API_KEY, MINIMAX_API_KEY, KIMI_API_KEY, OPENROUTER_API_KEY (text-gen)
122
+ * - CHATTERBOX_BASE_URL, ELEVENLABS_API_KEY (TTS)
123
+ * - DEEPGRAM_API_KEY (transcription)
124
+ * - OPENROUTER_API_KEY (embeddings)
125
+ * - REPLICATE_API_TOKEN, NANO_BANANA_API_KEY (image-gen)
126
+ *
127
+ * Accepts optional per-capability config overrides.
128
+ */
129
+ export function bootstrapAdaptersFromEnv(overrides?: Partial<BootstrapConfig>): BootstrapResult {
130
+ return bootstrapAdapters({
131
+ textGen: {
132
+ deepseekApiKey: process.env.DEEPSEEK_API_KEY,
133
+ geminiApiKey: process.env.GEMINI_API_KEY,
134
+ minimaxApiKey: process.env.MINIMAX_API_KEY,
135
+ kimiApiKey: process.env.KIMI_API_KEY,
136
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
137
+ ...overrides?.textGen,
138
+ },
139
+ tts: {
140
+ chatterboxBaseUrl: process.env.CHATTERBOX_BASE_URL,
141
+ elevenlabsApiKey: process.env.ELEVENLABS_API_KEY,
142
+ ...overrides?.tts,
143
+ },
144
+ transcription: {
145
+ deepgramApiKey: process.env.DEEPGRAM_API_KEY,
146
+ ...overrides?.transcription,
147
+ },
148
+ embeddings: {
149
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
150
+ ...overrides?.embeddings,
151
+ },
152
+ imageGen: {
153
+ replicateApiToken: process.env.REPLICATE_API_TOKEN,
154
+ // Separate env var from GEMINI_API_KEY (used for text-gen) to avoid
155
+ // silently enabling image-gen when only text-gen is intended.
156
+ geminiApiKey: process.env.NANO_BANANA_API_KEY,
157
+ ...overrides?.imageGen,
158
+ },
159
+ });
160
+ }
@@ -115,7 +115,7 @@ describe("lookupRate", () => {
115
115
  });
116
116
 
117
117
  it("returns undefined for non-existent capability", () => {
118
- const rate = lookupRate("image-generation" as unknown as AdapterCapability, "standard");
118
+ const rate = lookupRate("video-generation" as unknown as AdapterCapability, "standard");
119
119
  expect(rate).toBeUndefined();
120
120
  });
121
121
 
@@ -194,7 +194,7 @@ describe("calculateSavings", () => {
194
194
 
195
195
  it("returns zero when capability has no premium tier", () => {
196
196
  // This would happen if a capability only has self-hosted, no third-party
197
- const savings = calculateSavings("embeddings" as unknown as AdapterCapability, 1000);
197
+ const savings = calculateSavings("voice-cloning" as unknown as AdapterCapability, 1000);
198
198
  expect(savings).toBe(0);
199
199
  });
200
200
 
@@ -96,6 +96,28 @@ 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
+ // Image Generation
100
+ // NOTE: No self-hosted SDXL adapter yet — only premium tiers available.
101
+ // When self-hosted-sdxl lands, add a standard tier entry here.
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
+ },
111
+ {
112
+ capability: "image-generation",
113
+ tier: "premium",
114
+ provider: "replicate",
115
+ costPerUnit: 0.019, // ~$0.019 per image (SDXL on A40, ~8s avg at $0.0023/s)
116
+ billingUnit: "per-image",
117
+ margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
118
+ effectivePrice: 0.0247, // = costPerUnit * margin ($24.70 per 1K images)
119
+ },
120
+
99
121
  // Embeddings
100
122
  // NOTE: No self-hosted embeddings adapter yet — only premium (openrouter) available.
101
123
  // When self-hosted-embeddings lands, add a standard tier entry here.
@@ -29,6 +29,13 @@ export type {
29
29
  } from "@wopr-network/platform-core/billing";
30
30
  // Credit value object (WOP-983)
31
31
  export { Credit } from "@wopr-network/platform-core/credits";
32
+ // Unified adapter bootstrap (WOP-2194)
33
+ export {
34
+ type BootstrapConfig,
35
+ type BootstrapResult,
36
+ bootstrapAdapters,
37
+ bootstrapAdaptersFromEnv,
38
+ } from "./adapters/bootstrap.js";
32
39
  // Adapters (WOP-301, WOP-353, WOP-377, WOP-386, WOP-387, WOP-497)
33
40
  export { type ChatterboxTTSAdapterConfig, createChatterboxTTSAdapter } from "./adapters/chatterbox-tts.js";
34
41
  export { createDeepgramAdapter, type DeepgramAdapterConfig } from "./adapters/deepgram.js";