@wopr-network/platform-core 1.12.2 → 1.13.1

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.
Files changed (123) hide show
  1. package/dist/api/routes/activity.d.ts +9 -0
  2. package/dist/api/routes/activity.js +68 -0
  3. package/dist/api/routes/admin-audit-helper.d.ts +7 -0
  4. package/dist/api/routes/admin-audit-helper.js +13 -0
  5. package/dist/api/routes/admin-audit.d.ts +13 -0
  6. package/dist/api/routes/admin-audit.js +61 -0
  7. package/dist/api/routes/admin-backups.d.ts +19 -0
  8. package/dist/api/routes/admin-backups.js +116 -0
  9. package/dist/api/routes/admin-compliance.d.ts +9 -0
  10. package/dist/api/routes/admin-compliance.js +27 -0
  11. package/dist/api/routes/admin-credits.d.ts +9 -0
  12. package/dist/api/routes/admin-credits.js +255 -0
  13. package/dist/api/routes/admin-gpu.d.ts +46 -0
  14. package/dist/api/routes/admin-gpu.js +140 -0
  15. package/dist/api/routes/admin-inference.d.ts +16 -0
  16. package/dist/api/routes/admin-inference.js +98 -0
  17. package/dist/api/routes/admin-marketplace.d.ts +36 -0
  18. package/dist/api/routes/admin-marketplace.js +181 -0
  19. package/dist/api/routes/admin-migration.d.ts +10 -0
  20. package/dist/api/routes/admin-migration.js +46 -0
  21. package/dist/api/routes/admin-notes.d.ts +34 -0
  22. package/dist/api/routes/admin-notes.js +131 -0
  23. package/dist/api/routes/admin-onboarding.d.ts +7 -0
  24. package/dist/api/routes/admin-onboarding.js +49 -0
  25. package/dist/api/routes/admin-rates.d.ts +9 -0
  26. package/dist/api/routes/admin-rates.js +427 -0
  27. package/dist/api/routes/admin-recovery.d.ts +91 -0
  28. package/dist/api/routes/admin-recovery.js +246 -0
  29. package/dist/api/routes/admin-roles.d.ts +27 -0
  30. package/dist/api/routes/admin-roles.js +157 -0
  31. package/dist/api/routes/audit.d.ts +19 -0
  32. package/dist/api/routes/audit.js +95 -0
  33. package/dist/api/routes/auth.d.ts +19 -0
  34. package/dist/api/routes/auth.js +25 -0
  35. package/dist/api/routes/channel-validate.d.ts +11 -0
  36. package/dist/api/routes/channel-validate.js +148 -0
  37. package/dist/api/routes/fleet-events.d.ts +4 -0
  38. package/dist/api/routes/fleet-events.js +53 -0
  39. package/dist/api/routes/friends-proxy.d.ts +28 -0
  40. package/dist/api/routes/friends-proxy.js +63 -0
  41. package/dist/api/routes/friends-types.d.ts +34 -0
  42. package/dist/api/routes/friends-types.js +28 -0
  43. package/dist/api/routes/health.d.ts +14 -0
  44. package/dist/api/routes/health.js +32 -0
  45. package/dist/api/routes/health.test.d.ts +1 -0
  46. package/dist/api/routes/health.test.js +70 -0
  47. package/dist/api/routes/incident-response.d.ts +9 -0
  48. package/dist/api/routes/incident-response.js +148 -0
  49. package/dist/api/routes/internal-gpu.d.ts +12 -0
  50. package/dist/api/routes/internal-gpu.js +70 -0
  51. package/dist/api/routes/internal-nodes.d.ts +41 -0
  52. package/dist/api/routes/internal-nodes.js +105 -0
  53. package/dist/api/routes/login-history.d.ts +11 -0
  54. package/dist/api/routes/login-history.js +22 -0
  55. package/dist/api/routes/public-pricing.d.ts +9 -0
  56. package/dist/api/routes/public-pricing.js +32 -0
  57. package/dist/api/routes/quota.d.ts +8 -0
  58. package/dist/api/routes/quota.js +113 -0
  59. package/dist/api/routes/secret-audit.d.ts +12 -0
  60. package/dist/api/routes/secret-audit.js +41 -0
  61. package/dist/api/routes/secrets.d.ts +31 -0
  62. package/dist/api/routes/secrets.js +135 -0
  63. package/dist/api/routes/tenant-keys.d.ts +16 -0
  64. package/dist/api/routes/tenant-keys.js +142 -0
  65. package/dist/api/routes/verify-email.d.ts +19 -0
  66. package/dist/api/routes/verify-email.js +70 -0
  67. package/dist/api/routes/ws-auth.d.ts +21 -0
  68. package/dist/api/routes/ws-auth.js +24 -0
  69. package/dist/monetization/adapters/bootstrap.d.ts +2 -2
  70. package/dist/monetization/adapters/bootstrap.js +3 -2
  71. package/dist/monetization/adapters/bootstrap.test.js +11 -7
  72. package/dist/monetization/adapters/embeddings-factory.d.ts +10 -5
  73. package/dist/monetization/adapters/embeddings-factory.js +17 -4
  74. package/dist/monetization/adapters/embeddings-factory.test.js +85 -31
  75. package/dist/monetization/adapters/ollama-embeddings.d.ts +40 -0
  76. package/dist/monetization/adapters/ollama-embeddings.js +76 -0
  77. package/dist/monetization/adapters/ollama-embeddings.test.d.ts +1 -0
  78. package/dist/monetization/adapters/ollama-embeddings.test.js +178 -0
  79. package/dist/monetization/adapters/rate-table.js +9 -3
  80. package/dist/monetization/adapters/rate-table.test.js +22 -1
  81. package/package.json +35 -1
  82. package/src/api/routes/activity.ts +77 -0
  83. package/src/api/routes/admin-audit-helper.ts +18 -0
  84. package/src/api/routes/admin-audit.ts +67 -0
  85. package/src/api/routes/admin-backups.ts +134 -0
  86. package/src/api/routes/admin-compliance.ts +35 -0
  87. package/src/api/routes/admin-credits.ts +280 -0
  88. package/src/api/routes/admin-gpu.ts +202 -0
  89. package/src/api/routes/admin-inference.ts +109 -0
  90. package/src/api/routes/admin-marketplace.ts +233 -0
  91. package/src/api/routes/admin-migration.ts +61 -0
  92. package/src/api/routes/admin-notes.ts +145 -0
  93. package/src/api/routes/admin-onboarding.ts +62 -0
  94. package/src/api/routes/admin-rates.ts +462 -0
  95. package/src/api/routes/admin-recovery.ts +376 -0
  96. package/src/api/routes/admin-roles.ts +205 -0
  97. package/src/api/routes/audit.ts +106 -0
  98. package/src/api/routes/auth.ts +30 -0
  99. package/src/api/routes/channel-validate.ts +182 -0
  100. package/src/api/routes/fleet-events.ts +66 -0
  101. package/src/api/routes/friends-proxy.ts +94 -0
  102. package/src/api/routes/friends-types.ts +37 -0
  103. package/src/api/routes/health.test.ts +80 -0
  104. package/src/api/routes/health.ts +48 -0
  105. package/src/api/routes/incident-response.ts +159 -0
  106. package/src/api/routes/internal-gpu.ts +92 -0
  107. package/src/api/routes/internal-nodes.ts +157 -0
  108. package/src/api/routes/login-history.ts +28 -0
  109. package/src/api/routes/public-pricing.ts +36 -0
  110. package/src/api/routes/quota.ts +136 -0
  111. package/src/api/routes/secret-audit.ts +55 -0
  112. package/src/api/routes/secrets.ts +178 -0
  113. package/src/api/routes/tenant-keys.ts +178 -0
  114. package/src/api/routes/verify-email.ts +102 -0
  115. package/src/api/routes/ws-auth.ts +44 -0
  116. package/src/monetization/adapters/bootstrap.test.ts +11 -7
  117. package/src/monetization/adapters/bootstrap.ts +3 -2
  118. package/src/monetization/adapters/embeddings-factory.test.ts +102 -33
  119. package/src/monetization/adapters/embeddings-factory.ts +24 -7
  120. package/src/monetization/adapters/ollama-embeddings.test.ts +235 -0
  121. package/src/monetization/adapters/ollama-embeddings.ts +120 -0
  122. package/src/monetization/adapters/rate-table.test.ts +32 -1
  123. package/src/monetization/adapters/rate-table.ts +9 -3
@@ -1,54 +1,106 @@
1
1
  import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { createEmbeddingsAdapters, createEmbeddingsAdaptersFromEnv } from "./embeddings-factory.js";
3
+ import * as ollamaModule from "./ollama-embeddings.js";
3
4
  import * as openrouterModule from "./openrouter.js";
4
5
  describe("createEmbeddingsAdapters", () => {
5
- it("creates adapter when API key provided", () => {
6
+ it("creates all adapters when all config provided", () => {
6
7
  const result = createEmbeddingsAdapters({
8
+ ollamaBaseUrl: "http://ollama:11434",
7
9
  openrouterApiKey: "sk-or",
8
10
  });
9
- expect(result.adapters).toHaveLength(1);
10
- expect(result.adapterMap.size).toBe(1);
11
+ expect(result.adapters).toHaveLength(2);
12
+ expect(result.adapterMap.size).toBe(2);
11
13
  expect(result.skipped).toHaveLength(0);
12
14
  });
13
- it("adapter is openrouter", () => {
15
+ it("orders adapters cheapest first (ollama before openrouter)", () => {
14
16
  const result = createEmbeddingsAdapters({
17
+ ollamaBaseUrl: "http://ollama:11434",
15
18
  openrouterApiKey: "sk-or",
16
19
  });
20
+ expect(result.adapters[0].name).toBe("ollama-embeddings");
21
+ expect(result.adapters[1].name).toBe("openrouter");
22
+ });
23
+ it("ollama adapter is self-hosted", () => {
24
+ const result = createEmbeddingsAdapters({
25
+ ollamaBaseUrl: "http://ollama:11434",
26
+ });
27
+ expect(result.adapters[0].selfHosted).toBe(true);
28
+ });
29
+ it("creates only openrouter when no ollama URL", () => {
30
+ const result = createEmbeddingsAdapters({
31
+ openrouterApiKey: "sk-or",
32
+ });
33
+ expect(result.adapters).toHaveLength(1);
17
34
  expect(result.adapters[0].name).toBe("openrouter");
35
+ expect(result.skipped).toEqual(["ollama-embeddings"]);
18
36
  });
19
- it("skips openrouter when no API key", () => {
37
+ it("creates only ollama when no openrouter key", () => {
38
+ const result = createEmbeddingsAdapters({
39
+ ollamaBaseUrl: "http://ollama:11434",
40
+ });
41
+ expect(result.adapters).toHaveLength(1);
42
+ expect(result.adapters[0].name).toBe("ollama-embeddings");
43
+ expect(result.skipped).toEqual(["openrouter"]);
44
+ });
45
+ it("skips both when no config", () => {
20
46
  const result = createEmbeddingsAdapters({});
21
47
  expect(result.adapters).toHaveLength(0);
22
- expect(result.skipped).toEqual(["openrouter"]);
48
+ expect(result.skipped).toEqual(["ollama-embeddings", "openrouter"]);
49
+ });
50
+ it("skips ollama with empty string URL", () => {
51
+ const result = createEmbeddingsAdapters({
52
+ ollamaBaseUrl: "",
53
+ });
54
+ expect(result.adapters).toHaveLength(0);
55
+ expect(result.skipped).toContain("ollama-embeddings");
23
56
  });
24
- it("skips adapter with empty string key", () => {
57
+ it("skips openrouter with empty string key", () => {
25
58
  const result = createEmbeddingsAdapters({
26
59
  openrouterApiKey: "",
27
60
  });
28
61
  expect(result.adapters).toHaveLength(0);
29
62
  expect(result.skipped).toContain("openrouter");
30
63
  });
31
- it("adapter supports embeddings capability", () => {
64
+ it("both adapters support embeddings capability", () => {
32
65
  const result = createEmbeddingsAdapters({
66
+ ollamaBaseUrl: "http://ollama:11434",
33
67
  openrouterApiKey: "sk-or",
34
68
  });
35
- expect(result.adapters[0].capabilities).toContain("embeddings");
69
+ for (const adapter of result.adapters) {
70
+ expect(adapter.capabilities).toContain("embeddings");
71
+ }
36
72
  });
37
- it("adapter implements embed", () => {
73
+ it("both adapters implement embed", () => {
38
74
  const result = createEmbeddingsAdapters({
75
+ ollamaBaseUrl: "http://ollama:11434",
39
76
  openrouterApiKey: "sk-or",
40
77
  });
41
- expect(typeof result.adapters[0].embed).toBe("function");
78
+ for (const adapter of result.adapters) {
79
+ expect(typeof adapter.embed).toBe("function");
80
+ }
42
81
  });
43
82
  it("adapterMap keys match adapter names", () => {
44
83
  const result = createEmbeddingsAdapters({
84
+ ollamaBaseUrl: "http://ollama:11434",
45
85
  openrouterApiKey: "sk-or",
46
86
  });
47
87
  for (const [key, adapter] of result.adapterMap) {
48
88
  expect(key).toBe(adapter.name);
49
89
  }
50
90
  });
51
- it("passes per-adapter config overrides to adapter constructor", () => {
91
+ it("passes per-adapter config overrides to ollama constructor", () => {
92
+ const spy = vi.spyOn(ollamaModule, "createOllamaEmbeddingsAdapter");
93
+ createEmbeddingsAdapters({
94
+ ollamaBaseUrl: "http://ollama:11434",
95
+ ollama: { marginMultiplier: 1.5 },
96
+ });
97
+ expect(spy).toHaveBeenCalledWith(expect.objectContaining({
98
+ baseUrl: "http://ollama:11434",
99
+ marginMultiplier: 1.5,
100
+ }));
101
+ spy.mockRestore();
102
+ });
103
+ it("passes per-adapter config overrides to openrouter constructor", () => {
52
104
  const spy = vi.spyOn(openrouterModule, "createOpenRouterAdapter");
53
105
  createEmbeddingsAdapters({
54
106
  openrouterApiKey: "sk-or",
@@ -60,15 +112,6 @@ describe("createEmbeddingsAdapters", () => {
60
112
  }));
61
113
  spy.mockRestore();
62
114
  });
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
115
  });
73
116
  describe("createEmbeddingsAdaptersFromEnv", () => {
74
117
  beforeEach(() => {
@@ -77,28 +120,39 @@ describe("createEmbeddingsAdaptersFromEnv", () => {
77
120
  afterAll(() => {
78
121
  vi.unstubAllEnvs();
79
122
  });
80
- it("reads key from environment variable", () => {
123
+ it("reads keys from environment variables", () => {
124
+ vi.stubEnv("OLLAMA_BASE_URL", "http://ollama:11434");
81
125
  vi.stubEnv("OPENROUTER_API_KEY", "env-or");
82
126
  const result = createEmbeddingsAdaptersFromEnv();
83
- expect(result.adapters).toHaveLength(1);
84
- expect(result.adapters[0].name).toBe("openrouter");
127
+ expect(result.adapters).toHaveLength(2);
128
+ expect(result.adapters[0].name).toBe("ollama-embeddings");
129
+ expect(result.adapters[1].name).toBe("openrouter");
85
130
  expect(result.skipped).toHaveLength(0);
86
131
  });
87
- it("returns empty when no env var set", () => {
132
+ it("returns empty when no env vars set", () => {
133
+ vi.stubEnv("OLLAMA_BASE_URL", "");
88
134
  vi.stubEnv("OPENROUTER_API_KEY", "");
89
135
  const result = createEmbeddingsAdaptersFromEnv();
90
136
  expect(result.adapters).toHaveLength(0);
91
- expect(result.skipped).toEqual(["openrouter"]);
137
+ expect(result.skipped).toEqual(["ollama-embeddings", "openrouter"]);
92
138
  });
93
- it("passes per-adapter overrides alongside env key to adapter constructor", () => {
139
+ it("creates only ollama when only OLLAMA_BASE_URL set", () => {
140
+ vi.stubEnv("OLLAMA_BASE_URL", "http://ollama:11434");
141
+ vi.stubEnv("OPENROUTER_API_KEY", "");
142
+ const result = createEmbeddingsAdaptersFromEnv();
143
+ expect(result.adapters).toHaveLength(1);
144
+ expect(result.adapters[0].name).toBe("ollama-embeddings");
145
+ });
146
+ it("passes per-adapter overrides alongside env vars", () => {
147
+ vi.stubEnv("OLLAMA_BASE_URL", "http://ollama:11434");
94
148
  vi.stubEnv("OPENROUTER_API_KEY", "env-or");
95
- const spy = vi.spyOn(openrouterModule, "createOpenRouterAdapter");
149
+ const spy = vi.spyOn(ollamaModule, "createOllamaEmbeddingsAdapter");
96
150
  createEmbeddingsAdaptersFromEnv({
97
- openrouter: { marginMultiplier: 1.2 },
151
+ ollama: { marginMultiplier: 1.1 },
98
152
  });
99
153
  expect(spy).toHaveBeenCalledWith(expect.objectContaining({
100
- apiKey: "env-or",
101
- marginMultiplier: 1.2,
154
+ baseUrl: "http://ollama:11434",
155
+ marginMultiplier: 1.1,
102
156
  }));
103
157
  spy.mockRestore();
104
158
  });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Ollama self-hosted embeddings adapter — embeddings on our own GPU infrastructure.
3
+ *
4
+ * Points at a self-hosted Ollama container running on our internal network.
5
+ * Same ProviderAdapter interface as OpenRouter embeddings, but with:
6
+ * - No API key required (internal container-to-container)
7
+ * - Amortized GPU cost instead of third-party API invoicing
8
+ * - Lower margin (cheaper for users = the standard pricing tier)
9
+ *
10
+ * Uses Ollama's OpenAI-compatible /v1/embeddings endpoint, so it works with
11
+ * any Ollama-hosted embedding model (nomic-embed-text, mxbai-embed-large, etc.).
12
+ *
13
+ * Cost model:
14
+ * Base cost = total_tokens * costPerToken
15
+ * Default costPerToken = $0.000000005 (GPU depreciation + electricity)
16
+ * Charge = base_cost * marginMultiplier (e.g., 1.2 = 20% margin vs 30% for third-party)
17
+ */
18
+ import type { FetchFn, SelfHostedAdapterConfig } from "./self-hosted-base.js";
19
+ import type { ProviderAdapter } from "./types.js";
20
+ export type { FetchFn };
21
+ /**
22
+ * Configuration for the Ollama embeddings adapter.
23
+ *
24
+ * Cost precedence: `costPerToken` (if set) > `costPerUnit` (from SelfHostedAdapterConfig).
25
+ * Use `costPerToken` for adapter-specific overrides; `costPerUnit` is the base config
26
+ * shared across all self-hosted adapters.
27
+ */
28
+ export interface OllamaEmbeddingsAdapterConfig extends SelfHostedAdapterConfig {
29
+ /** Cost per token in USD (amortized GPU time, default: $0.000000005). Takes precedence over costPerUnit. */
30
+ costPerToken?: number;
31
+ /** Default embedding model (default: "nomic-embed-text") */
32
+ defaultModel?: string;
33
+ }
34
+ /**
35
+ * Create an Ollama self-hosted embeddings adapter.
36
+ *
37
+ * Uses factory function pattern (not class) for minimal API surface and easy
38
+ * dependency injection of fetch for testing.
39
+ */
40
+ export declare function createOllamaEmbeddingsAdapter(config: OllamaEmbeddingsAdapterConfig, fetchFn?: FetchFn): ProviderAdapter & Required<Pick<ProviderAdapter, "embed">>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Ollama self-hosted embeddings adapter — embeddings on our own GPU infrastructure.
3
+ *
4
+ * Points at a self-hosted Ollama container running on our internal network.
5
+ * Same ProviderAdapter interface as OpenRouter embeddings, but with:
6
+ * - No API key required (internal container-to-container)
7
+ * - Amortized GPU cost instead of third-party API invoicing
8
+ * - Lower margin (cheaper for users = the standard pricing tier)
9
+ *
10
+ * Uses Ollama's OpenAI-compatible /v1/embeddings endpoint, so it works with
11
+ * any Ollama-hosted embedding model (nomic-embed-text, mxbai-embed-large, etc.).
12
+ *
13
+ * Cost model:
14
+ * Base cost = total_tokens * costPerToken
15
+ * Default costPerToken = $0.000000005 (GPU depreciation + electricity)
16
+ * Charge = base_cost * marginMultiplier (e.g., 1.2 = 20% margin vs 30% for third-party)
17
+ */
18
+ import { Credit } from "@wopr-network/platform-core/credits";
19
+ import { withMargin } from "./types.js";
20
+ // ~4x cheaper than OpenRouter's text-embedding-3-small ($0.02/1M tokens)
21
+ const DEFAULT_COST_PER_TOKEN = 0.000000005; // $0.005 per 1M tokens
22
+ const DEFAULT_MARGIN = 1.2; // 20% vs 30% for third-party
23
+ const DEFAULT_MODEL = "nomic-embed-text";
24
+ /**
25
+ * Create an Ollama self-hosted embeddings adapter.
26
+ *
27
+ * Uses factory function pattern (not class) for minimal API surface and easy
28
+ * dependency injection of fetch for testing.
29
+ */
30
+ export function createOllamaEmbeddingsAdapter(config, fetchFn = fetch) {
31
+ const costPerToken = config.costPerToken ?? config.costPerUnit ?? DEFAULT_COST_PER_TOKEN;
32
+ const marginMultiplier = config.marginMultiplier ?? DEFAULT_MARGIN;
33
+ const defaultModel = config.defaultModel ?? DEFAULT_MODEL;
34
+ const timeoutMs = config.timeoutMs ?? 30000;
35
+ return {
36
+ name: "ollama-embeddings",
37
+ capabilities: ["embeddings"],
38
+ selfHosted: true,
39
+ async embed(input) {
40
+ const model = input.model ?? defaultModel;
41
+ const body = {
42
+ input: input.input,
43
+ model,
44
+ };
45
+ if (input.dimensions !== undefined) {
46
+ body.dimensions = input.dimensions;
47
+ }
48
+ const base = config.baseUrl.replace(/\/+$/, "");
49
+ const res = await fetchFn(`${base}/v1/embeddings`, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ },
54
+ body: JSON.stringify(body),
55
+ signal: AbortSignal.timeout(timeoutMs),
56
+ });
57
+ if (!res.ok) {
58
+ const text = await res.text();
59
+ throw new Error(`Ollama embeddings error (${res.status}): ${text}`);
60
+ }
61
+ const data = (await res.json());
62
+ const totalTokens = data.usage?.total_tokens ?? 0;
63
+ const cost = Credit.fromDollars(totalTokens * costPerToken);
64
+ const charge = withMargin(cost, marginMultiplier);
65
+ return {
66
+ result: {
67
+ embeddings: data.data.map((d) => d.embedding),
68
+ model: data.model,
69
+ totalTokens,
70
+ },
71
+ cost,
72
+ charge,
73
+ };
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,178 @@
1
+ import { Credit } from "@wopr-network/platform-core/credits";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { createOllamaEmbeddingsAdapter } from "./ollama-embeddings.js";
4
+ import { withMargin } from "./types.js";
5
+ /** Helper to create a mock Response */
6
+ function mockResponse(body, status = 200) {
7
+ return {
8
+ ok: status >= 200 && status < 300,
9
+ status,
10
+ json: () => Promise.resolve(body),
11
+ text: () => Promise.resolve(JSON.stringify(body)),
12
+ headers: new Headers(),
13
+ };
14
+ }
15
+ /** A successful OpenAI-compatible embeddings response */
16
+ function embeddingsResponse(overrides = {}) {
17
+ return {
18
+ model: "nomic-embed-text",
19
+ data: [{ embedding: [0.1, 0.2, 0.3, 0.4] }],
20
+ usage: { total_tokens: 5 },
21
+ ...overrides,
22
+ };
23
+ }
24
+ function makeConfig(overrides = {}) {
25
+ return {
26
+ baseUrl: "http://ollama:11434",
27
+ costPerUnit: 0.000000005,
28
+ ...overrides,
29
+ };
30
+ }
31
+ describe("createOllamaEmbeddingsAdapter", () => {
32
+ it("returns adapter with correct name and capabilities", () => {
33
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig());
34
+ expect(adapter.name).toBe("ollama-embeddings");
35
+ expect(adapter.capabilities).toEqual(["embeddings"]);
36
+ expect(adapter.selfHosted).toBe(true);
37
+ });
38
+ it("calls /v1/embeddings endpoint", async () => {
39
+ const body = embeddingsResponse();
40
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
41
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
42
+ await adapter.embed({ input: "Hello world" });
43
+ const [url, init] = fetchFn.mock.calls[0];
44
+ expect(url).toBe("http://ollama:11434/v1/embeddings");
45
+ expect(init?.method).toBe("POST");
46
+ expect((init?.headers)["Content-Type"]).toBe("application/json");
47
+ });
48
+ it("calculates cost from token count and costPerToken", async () => {
49
+ const body = embeddingsResponse({ usage: { total_tokens: 100 } });
50
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
51
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ costPerToken: 0.000000005 }), fetchFn);
52
+ const result = await adapter.embed({ input: "test" });
53
+ // 100 tokens * $0.000000005 = $0.0000005
54
+ expect(result.cost.toDollars()).toBeCloseTo(0.0000005, 10);
55
+ });
56
+ it("applies margin to cost", async () => {
57
+ const body = embeddingsResponse({ usage: { total_tokens: 1000 } });
58
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
59
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ marginMultiplier: 1.5 }), fetchFn);
60
+ const result = await adapter.embed({ input: "test" });
61
+ const expectedCost = Credit.fromDollars(1000 * 0.000000005);
62
+ expect(result.cost.toDollars()).toBeCloseTo(expectedCost.toDollars(), 10);
63
+ expect(result.charge?.toDollars()).toBeCloseTo(withMargin(expectedCost, 1.5).toDollars(), 10);
64
+ });
65
+ it("uses default 1.2 margin (lower than third-party)", async () => {
66
+ const body = embeddingsResponse({ usage: { total_tokens: 1000 } });
67
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
68
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
69
+ const result = await adapter.embed({ input: "test" });
70
+ const expectedCost = Credit.fromDollars(1000 * 0.000000005);
71
+ expect(result.charge?.toDollars()).toBeCloseTo(withMargin(expectedCost, 1.2).toDollars(), 10);
72
+ });
73
+ it("uses default model (nomic-embed-text) when none specified", async () => {
74
+ const body = embeddingsResponse();
75
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
76
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
77
+ await adapter.embed({ input: "test" });
78
+ const reqBody = JSON.parse(fetchFn.mock.calls[0][1]?.body);
79
+ expect(reqBody.model).toBe("nomic-embed-text");
80
+ });
81
+ it("passes requested model through to request", async () => {
82
+ const body = embeddingsResponse({ model: "mxbai-embed-large" });
83
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
84
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
85
+ const result = await adapter.embed({ input: "test", model: "mxbai-embed-large" });
86
+ const reqBody = JSON.parse(fetchFn.mock.calls[0][1]?.body);
87
+ expect(reqBody.model).toBe("mxbai-embed-large");
88
+ expect(result.result.model).toBe("mxbai-embed-large");
89
+ });
90
+ it("passes dimensions through to request", async () => {
91
+ const body = embeddingsResponse();
92
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
93
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
94
+ await adapter.embed({ input: "test", dimensions: 256 });
95
+ const reqBody = JSON.parse(fetchFn.mock.calls[0][1]?.body);
96
+ expect(reqBody.dimensions).toBe(256);
97
+ });
98
+ it("does not send dimensions when not specified", async () => {
99
+ const body = embeddingsResponse();
100
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
101
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
102
+ await adapter.embed({ input: "test" });
103
+ const reqBody = JSON.parse(fetchFn.mock.calls[0][1]?.body);
104
+ expect(reqBody.dimensions).toBeUndefined();
105
+ });
106
+ it("handles batch input (string[])", async () => {
107
+ const body = embeddingsResponse({
108
+ data: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],
109
+ usage: { total_tokens: 10 },
110
+ });
111
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
112
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
113
+ const result = await adapter.embed({ input: ["Hello", "World"] });
114
+ const reqBody = JSON.parse(fetchFn.mock.calls[0][1]?.body);
115
+ expect(reqBody.input).toEqual(["Hello", "World"]);
116
+ expect(result.result.embeddings).toHaveLength(2);
117
+ expect(result.result.embeddings[0]).toEqual([0.1, 0.2, 0.3]);
118
+ expect(result.result.embeddings[1]).toEqual([0.4, 0.5, 0.6]);
119
+ expect(result.result.totalTokens).toBe(10);
120
+ });
121
+ it("throws on non-2xx response", async () => {
122
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse({ error: "model not found" }, 404));
123
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
124
+ await expect(adapter.embed({ input: "test" })).rejects.toThrow("Ollama embeddings error (404)");
125
+ });
126
+ it("throws on 500 server error", async () => {
127
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse({ error: "internal error" }, 500));
128
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
129
+ await expect(adapter.embed({ input: "test" })).rejects.toThrow("Ollama embeddings error (500)");
130
+ });
131
+ it("uses custom baseUrl", async () => {
132
+ const body = embeddingsResponse();
133
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
134
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ baseUrl: "http://gpu-node:11434" }), fetchFn);
135
+ await adapter.embed({ input: "test" });
136
+ const [url] = fetchFn.mock.calls[0];
137
+ expect(url).toBe("http://gpu-node:11434/v1/embeddings");
138
+ });
139
+ it("uses costPerUnit from SelfHostedAdapterConfig when costPerToken not set", async () => {
140
+ const body = embeddingsResponse({ usage: { total_tokens: 1000 } });
141
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
142
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ costPerUnit: 0.00000001 }), fetchFn);
143
+ const result = await adapter.embed({ input: "test" });
144
+ // 1000 tokens * $0.00000001 = $0.00001
145
+ expect(result.cost.toDollars()).toBeCloseTo(0.00001, 8);
146
+ });
147
+ it("costPerToken takes precedence over costPerUnit", async () => {
148
+ const body = embeddingsResponse({ usage: { total_tokens: 1000 } });
149
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
150
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ costPerUnit: 0.00000001, costPerToken: 0.000000005 }), fetchFn);
151
+ const result = await adapter.embed({ input: "test" });
152
+ // costPerToken wins: 1000 * $0.000000005 = $0.000005
153
+ expect(result.cost.toDollars()).toBeCloseTo(0.000005, 10);
154
+ });
155
+ it("uses custom defaultModel from config", async () => {
156
+ const body = embeddingsResponse({ model: "mxbai-embed-large" });
157
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
158
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ defaultModel: "mxbai-embed-large" }), fetchFn);
159
+ await adapter.embed({ input: "test" });
160
+ const reqBody = JSON.parse(fetchFn.mock.calls[0][1]?.body);
161
+ expect(reqBody.model).toBe("mxbai-embed-large");
162
+ });
163
+ it("normalizes trailing slash in baseUrl", async () => {
164
+ const body = embeddingsResponse();
165
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
166
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig({ baseUrl: "http://ollama:11434/" }), fetchFn);
167
+ await adapter.embed({ input: "test" });
168
+ const [url] = fetchFn.mock.calls[0];
169
+ expect(url).toBe("http://ollama:11434/v1/embeddings");
170
+ });
171
+ it("returns correct totalTokens from response", async () => {
172
+ const body = embeddingsResponse({ usage: { total_tokens: 42 } });
173
+ const fetchFn = vi.fn().mockResolvedValueOnce(mockResponse(body));
174
+ const adapter = createOllamaEmbeddingsAdapter(makeConfig(), fetchFn);
175
+ const result = await adapter.embed({ input: "test" });
176
+ expect(result.result.totalTokens).toBe(42);
177
+ });
178
+ });
@@ -95,8 +95,15 @@ export const RATE_TABLE = [
95
95
  effectivePrice: 0.0247, // = costPerUnit * margin ($24.70 per 1K images)
96
96
  },
97
97
  // Embeddings
98
- // NOTE: No self-hosted embeddings adapter yet — only premium (openrouter) available.
99
- // When self-hosted-embeddings lands, add a standard tier entry here.
98
+ {
99
+ capability: "embeddings",
100
+ tier: "standard",
101
+ provider: "ollama-embeddings",
102
+ costPerUnit: 0.000000005, // Amortized GPU cost per token ($0.005 per 1M tokens)
103
+ billingUnit: "per-token",
104
+ margin: 1.2, // 20% — dashboard default; runtime uses getMargin()
105
+ effectivePrice: 0.000000006, // = costPerUnit * margin ($0.006 per 1M tokens)
106
+ },
100
107
  {
101
108
  capability: "embeddings",
102
109
  tier: "premium",
@@ -131,7 +138,6 @@ export const RATE_TABLE = [
131
138
  },
132
139
  // Future self-hosted adapters will add more entries here:
133
140
  // - transcription: self-hosted-whisper (standard) — when GPU adapter exists
134
- // - embeddings: self-hosted-embeddings (standard) — when GPU adapter exists
135
141
  // - image-generation: self-hosted-sdxl (standard) — when GPU adapter exists
136
142
  ];
137
143
  /**
@@ -7,6 +7,12 @@ describe("RATE_TABLE", () => {
7
7
  expect(standardTTS).toEqual(expect.objectContaining({ capability: "tts", tier: "standard" }));
8
8
  expect(premiumTTS).toEqual(expect.objectContaining({ capability: "tts", tier: "premium" }));
9
9
  });
10
+ it("contains both standard and premium tiers for embeddings", () => {
11
+ const standard = RATE_TABLE.find((e) => e.capability === "embeddings" && e.tier === "standard");
12
+ const premium = RATE_TABLE.find((e) => e.capability === "embeddings" && e.tier === "premium");
13
+ expect(standard).toEqual(expect.objectContaining({ capability: "embeddings", tier: "standard", provider: "ollama-embeddings" }));
14
+ expect(premium).toEqual(expect.objectContaining({ capability: "embeddings", tier: "premium", provider: "openrouter" }));
15
+ });
10
16
  it("contains both standard and premium tiers for text-generation", () => {
11
17
  const standard = RATE_TABLE.find((e) => e.capability === "text-generation" && e.tier === "standard");
12
18
  const premium = RATE_TABLE.find((e) => e.capability === "text-generation" && e.tier === "premium");
@@ -36,7 +42,9 @@ describe("RATE_TABLE", () => {
36
42
  const standardEntries = RATE_TABLE.filter((e) => e.tier === "standard");
37
43
  for (const entry of standardEntries) {
38
44
  // Self-hosted providers include "self-hosted-" prefix or are known self-hosted names
39
- const isSelfHosted = entry.provider.startsWith("self-hosted-") || entry.provider === "chatterbox-tts";
45
+ const isSelfHosted = entry.provider.startsWith("self-hosted-") ||
46
+ entry.provider === "chatterbox-tts" ||
47
+ entry.provider === "ollama-embeddings";
40
48
  expect(isSelfHosted).toBe(true);
41
49
  }
42
50
  });
@@ -104,6 +112,12 @@ describe("getRatesForCapability", () => {
104
112
  expect(rates.map((r) => r.tier)).toContain("standard");
105
113
  expect(rates.map((r) => r.tier)).toContain("premium");
106
114
  });
115
+ it("returns both standard and premium for embeddings", () => {
116
+ const rates = getRatesForCapability("embeddings");
117
+ expect(rates).toHaveLength(2);
118
+ expect(rates.map((r) => r.tier)).toContain("standard");
119
+ expect(rates.map((r) => r.tier)).toContain("premium");
120
+ });
107
121
  it("returns premium-only for transcription (no standard tier yet)", () => {
108
122
  const rates = getRatesForCapability("transcription");
109
123
  expect(rates).toHaveLength(1);
@@ -141,6 +155,13 @@ describe("calculateSavings", () => {
141
155
  // Savings: $2.01 per 100K chars
142
156
  expect(savings).toBeCloseTo(2.01, 2);
143
157
  });
158
+ it("calculates savings for embeddings at 1M tokens", () => {
159
+ const savings = calculateSavings("embeddings", 1_000_000);
160
+ // Standard (ollama-embeddings): $0.006 per 1M tokens
161
+ // Premium (openrouter): $0.026 per 1M tokens
162
+ // Savings: $0.020 per 1M tokens
163
+ expect(savings).toBeCloseTo(0.02, 3);
164
+ });
144
165
  it("returns zero when capability has no standard tier", () => {
145
166
  // Transcription only has premium (deepgram) — no self-hosted whisper yet
146
167
  const savings = calculateSavings("transcription", 1000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.12.2",
3
+ "version": "1.13.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,6 +21,7 @@
21
21
  "./discovery": "./dist/discovery/index.js",
22
22
  "./email": "./dist/email/index.js",
23
23
  "./fleet": "./dist/fleet/index.js",
24
+ "./fleet/profile-store": "./dist/fleet/profile-store.js",
24
25
  "./gateway": "./dist/gateway/index.js",
25
26
  "./inference": "./dist/inference/index.js",
26
27
  "./marketplace": "./dist/marketplace/index.js",
@@ -36,6 +37,39 @@
36
37
  "./security": "./dist/security/index.js",
37
38
  "./setup": "./dist/setup/index.js",
38
39
  "./tenancy": "./dist/tenancy/index.js",
40
+ "./api/routes/audit": "./dist/api/routes/audit.js",
41
+ "./api/routes/auth": "./dist/api/routes/auth.js",
42
+ "./api/routes/health": "./dist/api/routes/health.js",
43
+ "./api/routes/login-history": "./dist/api/routes/login-history.js",
44
+ "./api/routes/admin-audit": "./dist/api/routes/admin-audit.js",
45
+ "./api/routes/admin-audit-helper": "./dist/api/routes/admin-audit-helper.js",
46
+ "./api/routes/admin-backups": "./dist/api/routes/admin-backups.js",
47
+ "./api/routes/admin-compliance": "./dist/api/routes/admin-compliance.js",
48
+ "./api/routes/admin-credits": "./dist/api/routes/admin-credits.js",
49
+ "./api/routes/admin-inference": "./dist/api/routes/admin-inference.js",
50
+ "./api/routes/admin-migration": "./dist/api/routes/admin-migration.js",
51
+ "./api/routes/admin-gpu": "./dist/api/routes/admin-gpu.js",
52
+ "./api/routes/admin-marketplace": "./dist/api/routes/admin-marketplace.js",
53
+ "./api/routes/admin-notes": "./dist/api/routes/admin-notes.js",
54
+ "./api/routes/admin-onboarding": "./dist/api/routes/admin-onboarding.js",
55
+ "./api/routes/admin-rates": "./dist/api/routes/admin-rates.js",
56
+ "./api/routes/admin-recovery": "./dist/api/routes/admin-recovery.js",
57
+ "./api/routes/admin-roles": "./dist/api/routes/admin-roles.js",
58
+ "./api/routes/activity": "./dist/api/routes/activity.js",
59
+ "./api/routes/channel-validate": "./dist/api/routes/channel-validate.js",
60
+ "./api/routes/friends-proxy": "./dist/api/routes/friends-proxy.js",
61
+ "./api/routes/friends-types": "./dist/api/routes/friends-types.js",
62
+ "./api/routes/incident-response": "./dist/api/routes/incident-response.js",
63
+ "./api/routes/internal-nodes": "./dist/api/routes/internal-nodes.js",
64
+ "./api/routes/quota": "./dist/api/routes/quota.js",
65
+ "./api/routes/secrets": "./dist/api/routes/secrets.js",
66
+ "./api/routes/tenant-keys": "./dist/api/routes/tenant-keys.js",
67
+ "./api/routes/fleet-events": "./dist/api/routes/fleet-events.js",
68
+ "./api/routes/internal-gpu": "./dist/api/routes/internal-gpu.js",
69
+ "./api/routes/public-pricing": "./dist/api/routes/public-pricing.js",
70
+ "./api/routes/secret-audit": "./dist/api/routes/secret-audit.js",
71
+ "./api/routes/verify-email": "./dist/api/routes/verify-email.js",
72
+ "./api/routes/ws-auth": "./dist/api/routes/ws-auth.js",
39
73
  "./trpc": "./dist/trpc/index.js",
40
74
  "./*": "./dist/*.js"
41
75
  },