@wopr-network/platform-core 1.7.0 → 1.8.0

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