@wopr-network/platform-core 1.74.1 → 1.75.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/gateway/proxy.js +10 -2
- package/dist/gateway/types.d.ts +3 -1
- package/dist/index.js +4 -0
- package/dist/server/__tests__/container.test.js +5 -1
- package/dist/server/container.d.ts +2 -0
- package/dist/server/container.js +6 -1
- package/dist/server/index.js +1 -1
- package/dist/server/mount-routes.d.ts +1 -1
- package/dist/server/mount-routes.js +35 -2
- package/dist/server/routes/__tests__/admin.test.js +3 -3
- package/package.json +1 -1
- package/src/gateway/proxy.ts +9 -2
- package/src/gateway/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/server/__tests__/container.test.ts +5 -1
- package/src/server/container.ts +9 -1
- package/src/server/index.ts +1 -1
- package/src/server/mount-routes.ts +41 -3
- package/src/server/routes/__tests__/admin.test.ts +3 -3
package/dist/gateway/proxy.js
CHANGED
|
@@ -19,7 +19,11 @@ import { creditBalanceCheck, debitCredits } from "./credit-gate.js";
|
|
|
19
19
|
import { mapBudgetError, mapProviderError } from "./error-mapping.js";
|
|
20
20
|
import { resolveTokenRates } from "./rate-lookup.js";
|
|
21
21
|
import { proxySSEStream } from "./streaming.js";
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Fallback only used when resolveMargin is not provided (tests only).
|
|
24
|
+
* Production MUST provide resolveMargin — mountRoutes enforces this.
|
|
25
|
+
*/
|
|
26
|
+
const TEST_ONLY_MARGIN = 1.3;
|
|
23
27
|
/** Max call duration cap: 4 hours = 240 minutes. */
|
|
24
28
|
const MAX_CALL_DURATION_MINUTES = 240;
|
|
25
29
|
const phoneInboundBodySchema = z.object({
|
|
@@ -54,7 +58,11 @@ export function buildProxyDeps(config) {
|
|
|
54
58
|
providers: config.providers,
|
|
55
59
|
defaultModel: config.defaultModel,
|
|
56
60
|
resolveDefaultModel: config.resolveDefaultModel,
|
|
57
|
-
|
|
61
|
+
get defaultMargin() {
|
|
62
|
+
if (config.resolveMargin)
|
|
63
|
+
return config.resolveMargin();
|
|
64
|
+
return config.defaultMargin ?? TEST_ONLY_MARGIN;
|
|
65
|
+
},
|
|
58
66
|
fetchFn: config.fetchFn ?? fetch,
|
|
59
67
|
arbitrageRouter: config.arbitrageRouter,
|
|
60
68
|
rateLookupFn: config.rateLookupFn,
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -119,8 +119,10 @@ export interface GatewayConfig {
|
|
|
119
119
|
graceBufferCents?: number;
|
|
120
120
|
/** Upstream provider credentials */
|
|
121
121
|
providers: ProviderConfig;
|
|
122
|
-
/**
|
|
122
|
+
/** Static margin (for tests only). Production should use resolveMargin. */
|
|
123
123
|
defaultMargin?: number;
|
|
124
|
+
/** Live margin resolver — called per-request, reads from DB. Takes priority over defaultMargin. */
|
|
125
|
+
resolveMargin?: () => number;
|
|
124
126
|
/** Optional arbitrage router for multi-provider cost optimization (WOP-463) */
|
|
125
127
|
arbitrageRouter?: import("../monetization/arbitrage/router.js").ArbitrageRouter;
|
|
126
128
|
/** Injectable fetch for testing */
|
package/dist/index.js
CHANGED
|
@@ -142,6 +142,8 @@ describe("createTestContainer", () => {
|
|
|
142
142
|
};
|
|
143
143
|
const gateway = {
|
|
144
144
|
serviceKeyRepo: {},
|
|
145
|
+
meter: {},
|
|
146
|
+
budgetChecker: {},
|
|
145
147
|
};
|
|
146
148
|
const hotPool = {
|
|
147
149
|
start: async () => ({ stop: () => { } }),
|
|
@@ -158,7 +160,9 @@ describe("createTestContainer", () => {
|
|
|
158
160
|
expect(c.hotPool).not.toBeNull();
|
|
159
161
|
});
|
|
160
162
|
it("overrides merge without affecting other defaults", () => {
|
|
161
|
-
const c = createTestContainer({
|
|
163
|
+
const c = createTestContainer({
|
|
164
|
+
gateway: { serviceKeyRepo: {}, meter: {}, budgetChecker: {} },
|
|
165
|
+
});
|
|
162
166
|
// Overridden field
|
|
163
167
|
expect(c.gateway).not.toBeNull();
|
|
164
168
|
// Other feature services remain null
|
|
@@ -44,6 +44,8 @@ export interface StripeServices {
|
|
|
44
44
|
}
|
|
45
45
|
export interface GatewayServices {
|
|
46
46
|
serviceKeyRepo: IServiceKeyRepository;
|
|
47
|
+
meter: import("../metering/emitter.js").MeterEmitter;
|
|
48
|
+
budgetChecker: import("../monetization/budget/budget-checker.js").IBudgetChecker;
|
|
47
49
|
}
|
|
48
50
|
export interface HotPoolServices {
|
|
49
51
|
/** Start the pool manager (replenish loop + cleanup). */
|
package/dist/server/container.js
CHANGED
|
@@ -126,8 +126,13 @@ export async function buildContainer(bootConfig) {
|
|
|
126
126
|
let gateway = null;
|
|
127
127
|
if (bootConfig.features.gateway) {
|
|
128
128
|
const { DrizzleServiceKeyRepository } = await import("../gateway/service-key-repository.js");
|
|
129
|
+
const { MeterEmitter } = await import("../metering/emitter.js");
|
|
130
|
+
const { DrizzleMeterEventRepository } = await import("../metering/meter-event-repository.js");
|
|
131
|
+
const { DrizzleBudgetChecker } = await import("../monetization/budget/budget-checker.js");
|
|
129
132
|
const serviceKeyRepo = new DrizzleServiceKeyRepository(db);
|
|
130
|
-
|
|
133
|
+
const meter = new MeterEmitter(new DrizzleMeterEventRepository(db), { flushIntervalMs: 5_000 });
|
|
134
|
+
const budgetChecker = new DrizzleBudgetChecker(db, { cacheTtlMs: 30_000 });
|
|
135
|
+
gateway = { serviceKeyRepo, meter, budgetChecker };
|
|
131
136
|
}
|
|
132
137
|
// 12. Build the container (hotPool bound after construction)
|
|
133
138
|
const result = {
|
package/dist/server/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export { createTestContainer } from "./test-container.js";
|
|
|
37
37
|
export async function bootPlatformServer(config) {
|
|
38
38
|
const container = await buildContainer(config);
|
|
39
39
|
const app = new Hono();
|
|
40
|
-
mountRoutes(app, container, {
|
|
40
|
+
await mountRoutes(app, container, {
|
|
41
41
|
provisionSecret: config.provisionSecret,
|
|
42
42
|
cryptoServiceKey: config.cryptoServiceKey,
|
|
43
43
|
platformDomain: container.productConfig.product?.domain ?? "localhost",
|
|
@@ -27,4 +27,4 @@ export interface MountConfig {
|
|
|
27
27
|
* 6. Product-specific route plugins
|
|
28
28
|
* 7. Tenant proxy middleware (catch-all — must be last)
|
|
29
29
|
*/
|
|
30
|
-
export declare function mountRoutes(app: Hono, container: PlatformContainer, config: MountConfig, plugins?: RoutePlugin[]): void
|
|
30
|
+
export declare function mountRoutes(app: Hono, container: PlatformContainer, config: MountConfig, plugins?: RoutePlugin[]): Promise<void>;
|
|
@@ -28,7 +28,7 @@ import { createStripeWebhookRoutes } from "./routes/stripe-webhook.js";
|
|
|
28
28
|
* 6. Product-specific route plugins
|
|
29
29
|
* 7. Tenant proxy middleware (catch-all — must be last)
|
|
30
30
|
*/
|
|
31
|
-
export function mountRoutes(app, container, config, plugins = []) {
|
|
31
|
+
export async function mountRoutes(app, container, config, plugins = []) {
|
|
32
32
|
// 1. CORS middleware
|
|
33
33
|
const origins = deriveCorsOrigins(container.productConfig.product, container.productConfig.domains);
|
|
34
34
|
app.use("*", cors({
|
|
@@ -60,7 +60,40 @@ export function mountRoutes(app, container, config, plugins = []) {
|
|
|
60
60
|
maxInstancesPerTenant: fleetConfig?.maxInstances ?? 5,
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
|
-
// 6.
|
|
63
|
+
// 6. Metered inference gateway (when gateway is enabled)
|
|
64
|
+
if (container.gateway) {
|
|
65
|
+
// Validate billing config exists in DB — fail hard, no silent defaults
|
|
66
|
+
const billingConfig = container.productConfig.billing;
|
|
67
|
+
const marginConfig = billingConfig?.marginConfig;
|
|
68
|
+
if (!marginConfig?.default) {
|
|
69
|
+
throw new Error("Gateway enabled but product_billing_config.margin_config.default is not set. " +
|
|
70
|
+
"Seed the DB: INSERT INTO product_billing_config (product_id, margin_config) VALUES ('<id>', '{\"default\": 4.0}')");
|
|
71
|
+
}
|
|
72
|
+
// Live margin — reads from productConfig per-request (DB-cached with TTL)
|
|
73
|
+
const initialMargin = marginConfig.default;
|
|
74
|
+
const resolveMargin = () => {
|
|
75
|
+
const cfg = container.productConfig.billing?.marginConfig;
|
|
76
|
+
return cfg?.default ?? initialMargin;
|
|
77
|
+
};
|
|
78
|
+
const gw = container.gateway;
|
|
79
|
+
const { mountGateway } = await import("../gateway/index.js");
|
|
80
|
+
mountGateway(app, {
|
|
81
|
+
meter: gw.meter,
|
|
82
|
+
budgetChecker: gw.budgetChecker,
|
|
83
|
+
creditLedger: container.creditLedger,
|
|
84
|
+
resolveMargin,
|
|
85
|
+
providers: {
|
|
86
|
+
openrouter: process.env.OPENROUTER_API_KEY
|
|
87
|
+
? { apiKey: process.env.OPENROUTER_API_KEY, baseUrl: process.env.OPENROUTER_BASE_URL || undefined }
|
|
88
|
+
: undefined,
|
|
89
|
+
},
|
|
90
|
+
resolveServiceKey: async (key) => {
|
|
91
|
+
const tenant = await gw.serviceKeyRepo.resolve(key);
|
|
92
|
+
return tenant ?? null;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// 7. Product-specific route plugins
|
|
64
97
|
for (const plugin of plugins) {
|
|
65
98
|
app.route(plugin.path, plugin.handler(container));
|
|
66
99
|
}
|
|
@@ -144,6 +144,7 @@ describe("createAdminRouter", () => {
|
|
|
144
144
|
},
|
|
145
145
|
serviceKeyRepo: {},
|
|
146
146
|
},
|
|
147
|
+
gateway: { serviceKeyRepo: {}, meter: {}, budgetChecker: {} },
|
|
147
148
|
});
|
|
148
149
|
const caller = makeCaller(container);
|
|
149
150
|
const result = await caller.admin.listAllInstances();
|
|
@@ -185,6 +186,7 @@ describe("createAdminRouter", () => {
|
|
|
185
186
|
},
|
|
186
187
|
serviceKeyRepo: {},
|
|
187
188
|
},
|
|
189
|
+
gateway: { serviceKeyRepo: {}, meter: {}, budgetChecker: {} },
|
|
188
190
|
});
|
|
189
191
|
const caller = makeCaller(container);
|
|
190
192
|
const result = await caller.admin.listAllInstances();
|
|
@@ -229,9 +231,7 @@ describe("createAdminRouter", () => {
|
|
|
229
231
|
};
|
|
230
232
|
const container = createTestContainer({
|
|
231
233
|
pool: mockPool,
|
|
232
|
-
gateway: {
|
|
233
|
-
serviceKeyRepo: {},
|
|
234
|
-
},
|
|
234
|
+
gateway: { serviceKeyRepo: {}, meter: {}, budgetChecker: {} },
|
|
235
235
|
});
|
|
236
236
|
const caller = makeCaller(container);
|
|
237
237
|
const result = await caller.admin.billingOverview();
|
package/package.json
CHANGED
package/src/gateway/proxy.ts
CHANGED
|
@@ -29,7 +29,11 @@ import type { GatewayAuthEnv } from "./service-key-auth.js";
|
|
|
29
29
|
import { proxySSEStream } from "./streaming.js";
|
|
30
30
|
import type { FetchFn, GatewayConfig, ProviderConfig } from "./types.js";
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Fallback only used when resolveMargin is not provided (tests only).
|
|
34
|
+
* Production MUST provide resolveMargin — mountRoutes enforces this.
|
|
35
|
+
*/
|
|
36
|
+
const TEST_ONLY_MARGIN = 1.3;
|
|
33
37
|
|
|
34
38
|
/** Max call duration cap: 4 hours = 240 minutes. */
|
|
35
39
|
const MAX_CALL_DURATION_MINUTES = 240;
|
|
@@ -96,7 +100,10 @@ export function buildProxyDeps(config: GatewayConfig): ProxyDeps {
|
|
|
96
100
|
providers: config.providers,
|
|
97
101
|
defaultModel: config.defaultModel,
|
|
98
102
|
resolveDefaultModel: config.resolveDefaultModel,
|
|
99
|
-
|
|
103
|
+
get defaultMargin() {
|
|
104
|
+
if (config.resolveMargin) return config.resolveMargin();
|
|
105
|
+
return config.defaultMargin ?? TEST_ONLY_MARGIN;
|
|
106
|
+
},
|
|
100
107
|
fetchFn: config.fetchFn ?? fetch,
|
|
101
108
|
arbitrageRouter: config.arbitrageRouter,
|
|
102
109
|
rateLookupFn: config.rateLookupFn,
|
package/src/gateway/types.ts
CHANGED
|
@@ -115,8 +115,10 @@ export interface GatewayConfig {
|
|
|
115
115
|
graceBufferCents?: number;
|
|
116
116
|
/** Upstream provider credentials */
|
|
117
117
|
providers: ProviderConfig;
|
|
118
|
-
/**
|
|
118
|
+
/** Static margin (for tests only). Production should use resolveMargin. */
|
|
119
119
|
defaultMargin?: number;
|
|
120
|
+
/** Live margin resolver — called per-request, reads from DB. Takes priority over defaultMargin. */
|
|
121
|
+
resolveMargin?: () => number;
|
|
120
122
|
/** Optional arbitrage router for multi-provider cost optimization (WOP-463) */
|
|
121
123
|
arbitrageRouter?: import("../monetization/arbitrage/router.js").ArbitrageRouter;
|
|
122
124
|
/** Injectable fetch for testing */
|
package/src/index.ts
CHANGED
|
@@ -169,6 +169,8 @@ describe("createTestContainer", () => {
|
|
|
169
169
|
|
|
170
170
|
const gateway: GatewayServices = {
|
|
171
171
|
serviceKeyRepo: {} as never,
|
|
172
|
+
meter: {} as never,
|
|
173
|
+
budgetChecker: {} as never,
|
|
172
174
|
};
|
|
173
175
|
|
|
174
176
|
const hotPool: HotPoolServices = {
|
|
@@ -189,7 +191,9 @@ describe("createTestContainer", () => {
|
|
|
189
191
|
});
|
|
190
192
|
|
|
191
193
|
it("overrides merge without affecting other defaults", () => {
|
|
192
|
-
const c = createTestContainer({
|
|
194
|
+
const c = createTestContainer({
|
|
195
|
+
gateway: { serviceKeyRepo: {} as never, meter: {} as never, budgetChecker: {} as never },
|
|
196
|
+
});
|
|
193
197
|
|
|
194
198
|
// Overridden field
|
|
195
199
|
expect(c.gateway).not.toBeNull();
|
package/src/server/container.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface StripeServices {
|
|
|
51
51
|
|
|
52
52
|
export interface GatewayServices {
|
|
53
53
|
serviceKeyRepo: IServiceKeyRepository;
|
|
54
|
+
meter: import("../metering/emitter.js").MeterEmitter;
|
|
55
|
+
budgetChecker: import("../monetization/budget/budget-checker.js").IBudgetChecker;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface HotPoolServices {
|
|
@@ -239,8 +241,14 @@ export async function buildContainer(bootConfig: BootConfig): Promise<PlatformCo
|
|
|
239
241
|
let gateway: GatewayServices | null = null;
|
|
240
242
|
if (bootConfig.features.gateway) {
|
|
241
243
|
const { DrizzleServiceKeyRepository } = await import("../gateway/service-key-repository.js");
|
|
244
|
+
const { MeterEmitter } = await import("../metering/emitter.js");
|
|
245
|
+
const { DrizzleMeterEventRepository } = await import("../metering/meter-event-repository.js");
|
|
246
|
+
const { DrizzleBudgetChecker } = await import("../monetization/budget/budget-checker.js");
|
|
247
|
+
|
|
242
248
|
const serviceKeyRepo: IServiceKeyRepository = new DrizzleServiceKeyRepository(db as never);
|
|
243
|
-
|
|
249
|
+
const meter = new MeterEmitter(new DrizzleMeterEventRepository(db as never), { flushIntervalMs: 5_000 });
|
|
250
|
+
const budgetChecker = new DrizzleBudgetChecker(db as never, { cacheTtlMs: 30_000 });
|
|
251
|
+
gateway = { serviceKeyRepo, meter, budgetChecker };
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
// 12. Build the container (hotPool bound after construction)
|
package/src/server/index.ts
CHANGED
|
@@ -44,12 +44,12 @@ export interface MountConfig {
|
|
|
44
44
|
* 6. Product-specific route plugins
|
|
45
45
|
* 7. Tenant proxy middleware (catch-all — must be last)
|
|
46
46
|
*/
|
|
47
|
-
export function mountRoutes(
|
|
47
|
+
export async function mountRoutes(
|
|
48
48
|
app: Hono,
|
|
49
49
|
container: PlatformContainer,
|
|
50
50
|
config: MountConfig,
|
|
51
51
|
plugins: RoutePlugin[] = [],
|
|
52
|
-
): void {
|
|
52
|
+
): Promise<void> {
|
|
53
53
|
// 1. CORS middleware
|
|
54
54
|
const origins = deriveCorsOrigins(container.productConfig.product, container.productConfig.domains);
|
|
55
55
|
app.use(
|
|
@@ -95,7 +95,45 @@ export function mountRoutes(
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// 6.
|
|
98
|
+
// 6. Metered inference gateway (when gateway is enabled)
|
|
99
|
+
if (container.gateway) {
|
|
100
|
+
// Validate billing config exists in DB — fail hard, no silent defaults
|
|
101
|
+
const billingConfig = container.productConfig.billing;
|
|
102
|
+
const marginConfig = billingConfig?.marginConfig as { default?: number } | null;
|
|
103
|
+
if (!marginConfig?.default) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Gateway enabled but product_billing_config.margin_config.default is not set. " +
|
|
106
|
+
"Seed the DB: INSERT INTO product_billing_config (product_id, margin_config) VALUES ('<id>', '{\"default\": 4.0}')",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Live margin — reads from productConfig per-request (DB-cached with TTL)
|
|
111
|
+
const initialMargin = marginConfig.default;
|
|
112
|
+
const resolveMargin = (): number => {
|
|
113
|
+
const cfg = container.productConfig.billing?.marginConfig as { default?: number } | null;
|
|
114
|
+
return cfg?.default ?? initialMargin;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const gw = container.gateway;
|
|
118
|
+
const { mountGateway } = await import("../gateway/index.js");
|
|
119
|
+
mountGateway(app, {
|
|
120
|
+
meter: gw.meter,
|
|
121
|
+
budgetChecker: gw.budgetChecker,
|
|
122
|
+
creditLedger: container.creditLedger,
|
|
123
|
+
resolveMargin,
|
|
124
|
+
providers: {
|
|
125
|
+
openrouter: process.env.OPENROUTER_API_KEY
|
|
126
|
+
? { apiKey: process.env.OPENROUTER_API_KEY, baseUrl: process.env.OPENROUTER_BASE_URL || undefined }
|
|
127
|
+
: undefined,
|
|
128
|
+
},
|
|
129
|
+
resolveServiceKey: async (key: string) => {
|
|
130
|
+
const tenant = await gw.serviceKeyRepo.resolve(key);
|
|
131
|
+
return tenant ?? null;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 7. Product-specific route plugins
|
|
99
137
|
for (const plugin of plugins) {
|
|
100
138
|
app.route(plugin.path, plugin.handler(container));
|
|
101
139
|
}
|
|
@@ -173,6 +173,7 @@ describe("createAdminRouter", () => {
|
|
|
173
173
|
},
|
|
174
174
|
serviceKeyRepo: {} as never,
|
|
175
175
|
},
|
|
176
|
+
gateway: { serviceKeyRepo: {} as never, meter: {} as never, budgetChecker: {} as never },
|
|
176
177
|
});
|
|
177
178
|
|
|
178
179
|
const caller = makeCaller(container);
|
|
@@ -220,6 +221,7 @@ describe("createAdminRouter", () => {
|
|
|
220
221
|
},
|
|
221
222
|
serviceKeyRepo: {} as never,
|
|
222
223
|
},
|
|
224
|
+
gateway: { serviceKeyRepo: {} as never, meter: {} as never, budgetChecker: {} as never },
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
const caller = makeCaller(container);
|
|
@@ -273,9 +275,7 @@ describe("createAdminRouter", () => {
|
|
|
273
275
|
|
|
274
276
|
const container = createTestContainer({
|
|
275
277
|
pool: mockPool as never,
|
|
276
|
-
gateway: {
|
|
277
|
-
serviceKeyRepo: {} as never,
|
|
278
|
-
},
|
|
278
|
+
gateway: { serviceKeyRepo: {} as never, meter: {} as never, budgetChecker: {} as never },
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
const caller = makeCaller(container);
|