@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.
- package/dist/monetization/adapters/bootstrap.d.ts +82 -0
- package/dist/monetization/adapters/bootstrap.js +115 -0
- package/dist/monetization/adapters/bootstrap.test.d.ts +1 -0
- package/dist/monetization/adapters/bootstrap.test.js +159 -0
- package/dist/monetization/adapters/rate-table.js +21 -0
- package/dist/monetization/adapters/rate-table.test.js +2 -2
- package/dist/monetization/index.d.ts +1 -0
- package/dist/monetization/index.js +2 -0
- package/package.json +1 -1
- package/src/monetization/adapters/bootstrap.test.ts +186 -0
- package/src/monetization/adapters/bootstrap.ts +160 -0
- package/src/monetization/adapters/rate-table.test.ts +2 -2
- package/src/monetization/adapters/rate-table.ts +22 -0
- package/src/monetization/index.ts +7 -0
|
@@ -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("
|
|
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("
|
|
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
|
@@ -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("
|
|
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("
|
|
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";
|