@wopr-network/platform-core 1.8.0 → 1.10.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 +74 -0
- package/dist/monetization/adapters/bootstrap.js +103 -0
- package/dist/monetization/adapters/bootstrap.test.d.ts +1 -0
- package/dist/monetization/adapters/bootstrap.test.js +146 -0
- package/dist/monetization/adapters/image-gen-factory.d.ts +52 -0
- package/dist/monetization/adapters/image-gen-factory.js +60 -0
- package/dist/monetization/adapters/image-gen-factory.test.d.ts +1 -0
- package/dist/monetization/adapters/image-gen-factory.test.js +126 -0
- package/dist/monetization/adapters/rate-table.js +45 -1
- package/dist/monetization/adapters/rate-table.test.js +4 -4
- package/dist/monetization/index.d.ts +2 -0
- package/dist/monetization/index.js +4 -0
- package/package.json +1 -1
- package/src/monetization/adapters/bootstrap.test.ts +173 -0
- package/src/monetization/adapters/bootstrap.ts +141 -0
- package/src/monetization/adapters/image-gen-factory.test.ts +158 -0
- package/src/monetization/adapters/image-gen-factory.ts +89 -0
- package/src/monetization/adapters/rate-table.test.ts +6 -4
- package/src/monetization/adapters/rate-table.ts +47 -1
- package/src/monetization/index.ts +14 -0
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
*
|
|
16
|
+
* Image-generation (Nano Banana, Replicate) will be wired once the
|
|
17
|
+
* image-gen-factory PR merges.
|
|
18
|
+
*/
|
|
19
|
+
import { type EmbeddingsFactoryConfig } from "./embeddings-factory.js";
|
|
20
|
+
import { type TextGenFactoryConfig } from "./text-gen-factory.js";
|
|
21
|
+
import { type TranscriptionFactoryConfig } from "./transcription-factory.js";
|
|
22
|
+
import { type TTSFactoryConfig } from "./tts-factory.js";
|
|
23
|
+
import type { ProviderAdapter } from "./types.js";
|
|
24
|
+
/** Combined config for all adapter factories. */
|
|
25
|
+
export interface BootstrapConfig {
|
|
26
|
+
/** Text-generation adapter config */
|
|
27
|
+
textGen?: TextGenFactoryConfig;
|
|
28
|
+
/** TTS adapter config */
|
|
29
|
+
tts?: TTSFactoryConfig;
|
|
30
|
+
/** Transcription adapter config */
|
|
31
|
+
transcription?: TranscriptionFactoryConfig;
|
|
32
|
+
/** Embeddings adapter config */
|
|
33
|
+
embeddings?: EmbeddingsFactoryConfig;
|
|
34
|
+
}
|
|
35
|
+
/** Result of bootstrapping all adapters. */
|
|
36
|
+
export interface BootstrapResult {
|
|
37
|
+
/**
|
|
38
|
+
* All instantiated adapters across all capabilities, ordered by capability then cost.
|
|
39
|
+
* Register these with an ArbitrageRouter or AdapterSocket.
|
|
40
|
+
*
|
|
41
|
+
* NOTE: The same provider may appear multiple times if it serves multiple
|
|
42
|
+
* capabilities (e.g. OpenRouter for both text-gen and embeddings). Each
|
|
43
|
+
* instance is independently configured. Use the per-capability factory
|
|
44
|
+
* results if you need a name→adapter map within a single capability.
|
|
45
|
+
*/
|
|
46
|
+
adapters: ProviderAdapter[];
|
|
47
|
+
/** Names of providers that were skipped (missing config), grouped by capability. */
|
|
48
|
+
skipped: Record<string, string[]>;
|
|
49
|
+
/** Summary counts for observability. */
|
|
50
|
+
summary: {
|
|
51
|
+
total: number;
|
|
52
|
+
skipped: number;
|
|
53
|
+
byCapability: Record<string, number>;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Bootstrap all adapter factories from the provided config.
|
|
58
|
+
*
|
|
59
|
+
* Instantiates every capability factory and merges the results into a
|
|
60
|
+
* single unified result. Only adapters with valid config are created.
|
|
61
|
+
*/
|
|
62
|
+
export declare function bootstrapAdapters(config: BootstrapConfig): BootstrapResult;
|
|
63
|
+
/**
|
|
64
|
+
* Bootstrap all adapter factories from environment variables.
|
|
65
|
+
*
|
|
66
|
+
* Reads API keys from:
|
|
67
|
+
* - DEEPSEEK_API_KEY, GEMINI_API_KEY, MINIMAX_API_KEY, KIMI_API_KEY, OPENROUTER_API_KEY (text-gen)
|
|
68
|
+
* - CHATTERBOX_BASE_URL, ELEVENLABS_API_KEY (TTS)
|
|
69
|
+
* - DEEPGRAM_API_KEY (transcription)
|
|
70
|
+
* - OPENROUTER_API_KEY (embeddings)
|
|
71
|
+
*
|
|
72
|
+
* Accepts optional per-capability config overrides.
|
|
73
|
+
*/
|
|
74
|
+
export declare function bootstrapAdaptersFromEnv(overrides?: Partial<BootstrapConfig>): BootstrapResult;
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
*
|
|
16
|
+
* Image-generation (Nano Banana, Replicate) will be wired once the
|
|
17
|
+
* image-gen-factory PR merges.
|
|
18
|
+
*/
|
|
19
|
+
import { createEmbeddingsAdapters } from "./embeddings-factory.js";
|
|
20
|
+
import { createTextGenAdapters } from "./text-gen-factory.js";
|
|
21
|
+
import { createTranscriptionAdapters } from "./transcription-factory.js";
|
|
22
|
+
import { createTTSAdapters } from "./tts-factory.js";
|
|
23
|
+
/**
|
|
24
|
+
* Bootstrap all adapter factories from the provided config.
|
|
25
|
+
*
|
|
26
|
+
* Instantiates every capability factory and merges the results into a
|
|
27
|
+
* single unified result. Only adapters with valid config are created.
|
|
28
|
+
*/
|
|
29
|
+
export function bootstrapAdapters(config) {
|
|
30
|
+
const textGen = createTextGenAdapters(config.textGen ?? {});
|
|
31
|
+
const tts = createTTSAdapters(config.tts ?? {});
|
|
32
|
+
const transcription = createTranscriptionAdapters(config.transcription ?? {});
|
|
33
|
+
const embeddings = createEmbeddingsAdapters(config.embeddings ?? {});
|
|
34
|
+
const adapters = [
|
|
35
|
+
...textGen.adapters,
|
|
36
|
+
...tts.adapters,
|
|
37
|
+
...transcription.adapters,
|
|
38
|
+
...embeddings.adapters,
|
|
39
|
+
];
|
|
40
|
+
const skipped = {};
|
|
41
|
+
if (textGen.skipped.length > 0)
|
|
42
|
+
skipped["text-generation"] = textGen.skipped;
|
|
43
|
+
if (tts.skipped.length > 0)
|
|
44
|
+
skipped.tts = tts.skipped;
|
|
45
|
+
if (transcription.skipped.length > 0)
|
|
46
|
+
skipped.transcription = transcription.skipped;
|
|
47
|
+
if (embeddings.skipped.length > 0)
|
|
48
|
+
skipped.embeddings = embeddings.skipped;
|
|
49
|
+
let totalSkipped = 0;
|
|
50
|
+
for (const list of Object.values(skipped)) {
|
|
51
|
+
totalSkipped += list.length;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
adapters,
|
|
55
|
+
skipped,
|
|
56
|
+
summary: {
|
|
57
|
+
total: adapters.length,
|
|
58
|
+
skipped: totalSkipped,
|
|
59
|
+
byCapability: {
|
|
60
|
+
"text-generation": textGen.adapters.length,
|
|
61
|
+
tts: tts.adapters.length,
|
|
62
|
+
transcription: transcription.adapters.length,
|
|
63
|
+
embeddings: embeddings.adapters.length,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Bootstrap all adapter factories from environment variables.
|
|
70
|
+
*
|
|
71
|
+
* Reads API keys from:
|
|
72
|
+
* - DEEPSEEK_API_KEY, GEMINI_API_KEY, MINIMAX_API_KEY, KIMI_API_KEY, OPENROUTER_API_KEY (text-gen)
|
|
73
|
+
* - CHATTERBOX_BASE_URL, ELEVENLABS_API_KEY (TTS)
|
|
74
|
+
* - DEEPGRAM_API_KEY (transcription)
|
|
75
|
+
* - OPENROUTER_API_KEY (embeddings)
|
|
76
|
+
*
|
|
77
|
+
* Accepts optional per-capability config overrides.
|
|
78
|
+
*/
|
|
79
|
+
export function bootstrapAdaptersFromEnv(overrides) {
|
|
80
|
+
return bootstrapAdapters({
|
|
81
|
+
textGen: {
|
|
82
|
+
deepseekApiKey: process.env.DEEPSEEK_API_KEY,
|
|
83
|
+
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
84
|
+
minimaxApiKey: process.env.MINIMAX_API_KEY,
|
|
85
|
+
kimiApiKey: process.env.KIMI_API_KEY,
|
|
86
|
+
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
87
|
+
...overrides?.textGen,
|
|
88
|
+
},
|
|
89
|
+
tts: {
|
|
90
|
+
chatterboxBaseUrl: process.env.CHATTERBOX_BASE_URL,
|
|
91
|
+
elevenlabsApiKey: process.env.ELEVENLABS_API_KEY,
|
|
92
|
+
...overrides?.tts,
|
|
93
|
+
},
|
|
94
|
+
transcription: {
|
|
95
|
+
deepgramApiKey: process.env.DEEPGRAM_API_KEY,
|
|
96
|
+
...overrides?.transcription,
|
|
97
|
+
},
|
|
98
|
+
embeddings: {
|
|
99
|
+
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
100
|
+
...overrides?.embeddings,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
});
|
|
24
|
+
// 5 text-gen + 2 TTS + 1 transcription + 1 embeddings = 9
|
|
25
|
+
expect(result.adapters).toHaveLength(9);
|
|
26
|
+
expect(result.summary.total).toBe(9);
|
|
27
|
+
expect(result.summary.skipped).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
it("allows duplicate provider names across capabilities", () => {
|
|
30
|
+
const result = bootstrapAdapters({
|
|
31
|
+
textGen: { openrouterApiKey: "sk-or" },
|
|
32
|
+
embeddings: { openrouterApiKey: "sk-or" },
|
|
33
|
+
});
|
|
34
|
+
// OpenRouter appears twice — once for text-gen, once for embeddings
|
|
35
|
+
const openrouters = result.adapters.filter((a) => a.name === "openrouter");
|
|
36
|
+
expect(openrouters).toHaveLength(2);
|
|
37
|
+
expect(result.summary.total).toBe(2);
|
|
38
|
+
});
|
|
39
|
+
it("returns correct per-capability counts", () => {
|
|
40
|
+
const result = bootstrapAdapters({
|
|
41
|
+
textGen: { deepseekApiKey: "sk-ds" },
|
|
42
|
+
tts: { chatterboxBaseUrl: "http://chatterbox:8000" },
|
|
43
|
+
transcription: { deepgramApiKey: "sk-dg" },
|
|
44
|
+
embeddings: { openrouterApiKey: "sk-or" },
|
|
45
|
+
});
|
|
46
|
+
expect(result.summary.byCapability).toEqual({
|
|
47
|
+
"text-generation": 1,
|
|
48
|
+
tts: 1,
|
|
49
|
+
transcription: 1,
|
|
50
|
+
embeddings: 1,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it("tracks skipped providers by capability", () => {
|
|
54
|
+
const result = bootstrapAdapters({
|
|
55
|
+
textGen: { deepseekApiKey: "sk-ds" },
|
|
56
|
+
tts: {},
|
|
57
|
+
transcription: {},
|
|
58
|
+
embeddings: {},
|
|
59
|
+
});
|
|
60
|
+
expect(result.skipped.tts).toEqual(["chatterbox-tts", "elevenlabs"]);
|
|
61
|
+
expect(result.skipped.transcription).toEqual(["deepgram"]);
|
|
62
|
+
expect(result.skipped.embeddings).toEqual(["openrouter"]);
|
|
63
|
+
expect(result.skipped["text-generation"]).toEqual(["gemini", "minimax", "kimi", "openrouter"]);
|
|
64
|
+
});
|
|
65
|
+
it("returns empty result when no config provided", () => {
|
|
66
|
+
const result = bootstrapAdapters({});
|
|
67
|
+
expect(result.adapters).toHaveLength(0);
|
|
68
|
+
expect(result.summary.total).toBe(0);
|
|
69
|
+
expect(result.summary.skipped).toBeGreaterThan(0);
|
|
70
|
+
});
|
|
71
|
+
it("omits capability from skipped when all providers created", () => {
|
|
72
|
+
const result = bootstrapAdapters({
|
|
73
|
+
transcription: { deepgramApiKey: "sk-dg" },
|
|
74
|
+
});
|
|
75
|
+
expect(result.skipped.transcription).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
it("handles partial config — only text-gen", () => {
|
|
78
|
+
const result = bootstrapAdapters({
|
|
79
|
+
textGen: { openrouterApiKey: "sk-or" },
|
|
80
|
+
});
|
|
81
|
+
expect(result.summary.byCapability["text-generation"]).toBe(1);
|
|
82
|
+
expect(result.summary.byCapability.tts).toBe(0);
|
|
83
|
+
expect(result.summary.byCapability.transcription).toBe(0);
|
|
84
|
+
expect(result.summary.byCapability.embeddings).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
it("passes per-adapter overrides through", () => {
|
|
87
|
+
const result = bootstrapAdapters({
|
|
88
|
+
textGen: {
|
|
89
|
+
deepseekApiKey: "sk-ds",
|
|
90
|
+
deepseek: { marginMultiplier: 1.5 },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
expect(result.adapters).toHaveLength(1);
|
|
94
|
+
expect(result.adapters[0].name).toBe("deepseek");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("bootstrapAdaptersFromEnv", () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
vi.unstubAllEnvs();
|
|
100
|
+
});
|
|
101
|
+
afterAll(() => {
|
|
102
|
+
vi.unstubAllEnvs();
|
|
103
|
+
});
|
|
104
|
+
it("reads all keys from environment variables", () => {
|
|
105
|
+
vi.stubEnv("DEEPSEEK_API_KEY", "env-ds");
|
|
106
|
+
vi.stubEnv("GEMINI_API_KEY", "env-gem");
|
|
107
|
+
vi.stubEnv("MINIMAX_API_KEY", "env-mm");
|
|
108
|
+
vi.stubEnv("KIMI_API_KEY", "env-kimi");
|
|
109
|
+
vi.stubEnv("OPENROUTER_API_KEY", "env-or");
|
|
110
|
+
vi.stubEnv("CHATTERBOX_BASE_URL", "http://chatterbox:8000");
|
|
111
|
+
vi.stubEnv("ELEVENLABS_API_KEY", "env-el");
|
|
112
|
+
vi.stubEnv("DEEPGRAM_API_KEY", "env-dg");
|
|
113
|
+
const result = bootstrapAdaptersFromEnv();
|
|
114
|
+
// 5 text-gen + 2 TTS + 1 transcription + 1 embeddings = 9
|
|
115
|
+
expect(result.adapters).toHaveLength(9);
|
|
116
|
+
expect(result.summary.total).toBe(9);
|
|
117
|
+
});
|
|
118
|
+
it("returns empty when no env vars set", () => {
|
|
119
|
+
vi.stubEnv("DEEPSEEK_API_KEY", "");
|
|
120
|
+
vi.stubEnv("GEMINI_API_KEY", "");
|
|
121
|
+
vi.stubEnv("MINIMAX_API_KEY", "");
|
|
122
|
+
vi.stubEnv("KIMI_API_KEY", "");
|
|
123
|
+
vi.stubEnv("OPENROUTER_API_KEY", "");
|
|
124
|
+
vi.stubEnv("CHATTERBOX_BASE_URL", "");
|
|
125
|
+
vi.stubEnv("ELEVENLABS_API_KEY", "");
|
|
126
|
+
vi.stubEnv("DEEPGRAM_API_KEY", "");
|
|
127
|
+
const result = bootstrapAdaptersFromEnv();
|
|
128
|
+
expect(result.adapters).toHaveLength(0);
|
|
129
|
+
expect(result.summary.skipped).toBeGreaterThan(0);
|
|
130
|
+
});
|
|
131
|
+
it("accepts per-capability overrides", () => {
|
|
132
|
+
vi.stubEnv("DEEPSEEK_API_KEY", "env-ds");
|
|
133
|
+
vi.stubEnv("GEMINI_API_KEY", "");
|
|
134
|
+
vi.stubEnv("MINIMAX_API_KEY", "");
|
|
135
|
+
vi.stubEnv("KIMI_API_KEY", "");
|
|
136
|
+
vi.stubEnv("OPENROUTER_API_KEY", "");
|
|
137
|
+
vi.stubEnv("CHATTERBOX_BASE_URL", "");
|
|
138
|
+
vi.stubEnv("ELEVENLABS_API_KEY", "");
|
|
139
|
+
vi.stubEnv("DEEPGRAM_API_KEY", "");
|
|
140
|
+
const result = bootstrapAdaptersFromEnv({
|
|
141
|
+
textGen: { deepseek: { marginMultiplier: 2.0 } },
|
|
142
|
+
});
|
|
143
|
+
expect(result.adapters).toHaveLength(1);
|
|
144
|
+
expect(result.adapters[0].name).toBe("deepseek");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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.
|
|
@@ -85,10 +106,33 @@ export const RATE_TABLE = [
|
|
|
85
106
|
margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
|
|
86
107
|
effectivePrice: 0.000000026, // = costPerUnit * margin ($0.026 per 1M tokens)
|
|
87
108
|
},
|
|
109
|
+
// Image Generation
|
|
110
|
+
// NOTE: No self-hosted SDXL adapter yet — only premium tiers available.
|
|
111
|
+
// When self-hosted-sdxl lands, add a standard tier entry here.
|
|
112
|
+
// Multiple premium providers exist — cheapest first so lookupRate() returns
|
|
113
|
+
// the best rate. Use getRatesForCapability() for per-provider breakdowns.
|
|
114
|
+
{
|
|
115
|
+
capability: "image-generation",
|
|
116
|
+
tier: "premium",
|
|
117
|
+
provider: "replicate",
|
|
118
|
+
costPerUnit: 0.019, // ~$0.019 per image (SDXL on A40, ~8s avg at $0.0023/s)
|
|
119
|
+
billingUnit: "per-image",
|
|
120
|
+
margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
|
|
121
|
+
effectivePrice: 0.0247, // = costPerUnit * margin ($24.70 per 1K images)
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
capability: "image-generation",
|
|
125
|
+
tier: "premium",
|
|
126
|
+
provider: "nano-banana",
|
|
127
|
+
costPerUnit: 0.02, // $0.02 per image (Gemini Imagen via Nano Banana)
|
|
128
|
+
billingUnit: "per-image",
|
|
129
|
+
margin: 1.3, // 30% — dashboard default; runtime uses getMargin()
|
|
130
|
+
effectivePrice: 0.026, // = costPerUnit * margin ($26.00 per 1K images)
|
|
131
|
+
},
|
|
88
132
|
// Future self-hosted adapters will add more entries here:
|
|
89
133
|
// - transcription: self-hosted-whisper (standard) — when GPU adapter exists
|
|
90
134
|
// - embeddings: self-hosted-embeddings (standard) — when GPU adapter exists
|
|
91
|
-
// - image-generation: self-hosted-sdxl (standard)
|
|
135
|
+
// - image-generation: self-hosted-sdxl (standard) — when GPU adapter exists
|
|
92
136
|
];
|
|
93
137
|
/**
|
|
94
138
|
* 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", "
|
|
47
|
+
const isThirdParty = ["elevenlabs", "deepgram", "openrouter", "replicate", "nano-banana"].includes(entry.provider);
|
|
48
48
|
expect(isThirdParty).toBe(true);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
@@ -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", () => {
|
|
@@ -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("
|
|
114
|
+
const rates = getRatesForCapability("video-generation");
|
|
115
115
|
expect(rates).toHaveLength(0);
|
|
116
116
|
});
|
|
117
117
|
it("all returned rates have the requested capability", () => {
|
|
@@ -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", () => {
|