@wopr-network/platform-core 1.39.7 → 1.42.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/db/schema/tenants.js +2 -2
- package/dist/db/seed-platform-service-tenant.d.ts +6 -0
- package/dist/db/seed-platform-service-tenant.js +42 -0
- package/dist/gateway/capability-rate-limit.js +2 -1
- package/dist/gateway/credit-gate.d.ts +1 -1
- package/dist/gateway/credit-gate.js +7 -1
- package/dist/gateway/protocol/anthropic.js +2 -1
- package/dist/gateway/protocol/openai.js +5 -3
- package/dist/gateway/service-key-auth.d.ts +1 -0
- package/dist/gateway/service-key-auth.js +2 -0
- package/dist/gateway/types.d.ts +2 -0
- package/drizzle/migrations/0013_platform_service_tenant_type.sql +3 -0
- package/package.json +1 -1
- package/src/db/schema/tenants.ts +2 -2
- package/src/db/seed-platform-service-tenant.ts +55 -0
- package/src/gateway/capability-rate-limit.ts +2 -1
- package/src/gateway/credit-gate.ts +9 -0
- package/src/gateway/protocol/anthropic.ts +11 -1
- package/src/gateway/protocol/openai.ts +32 -3
- package/src/gateway/service-key-auth.ts +3 -0
- package/src/gateway/types.ts +2 -0
|
@@ -4,7 +4,7 @@ export const tenants = pgTable("tenants", {
|
|
|
4
4
|
id: text("id").primaryKey(), // nanoid or crypto.randomUUID()
|
|
5
5
|
name: text("name").notNull(),
|
|
6
6
|
slug: text("slug").unique(),
|
|
7
|
-
type: text("type").notNull(), // "personal" | "org"
|
|
7
|
+
type: text("type").notNull(), // "personal" | "org" | "platform_service"
|
|
8
8
|
ownerId: text("owner_id").notNull(), // user who created it
|
|
9
9
|
billingEmail: text("billing_email"),
|
|
10
10
|
createdAt: bigint("created_at", { mode: "number" })
|
|
@@ -14,5 +14,5 @@ export const tenants = pgTable("tenants", {
|
|
|
14
14
|
index("idx_tenants_slug").on(table.slug),
|
|
15
15
|
index("idx_tenants_owner").on(table.ownerId),
|
|
16
16
|
index("idx_tenants_type").on(table.type),
|
|
17
|
-
check("chk_tenants_type", sql `${table.type} IN ('personal', 'org')`),
|
|
17
|
+
check("chk_tenants_type", sql `${table.type} IN ('personal', 'org', 'platform_service')`),
|
|
18
18
|
]);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent seed: creates the holyship-platform internal billing tenant
|
|
3
|
+
* with a stable service key for platform-to-gateway LLM billing.
|
|
4
|
+
*
|
|
5
|
+
* Safe to call on every boot — skips if the tenant already exists.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { eq } from "drizzle-orm";
|
|
9
|
+
import { gatewayServiceKeys } from "./schema/gateway-service-keys.js";
|
|
10
|
+
import { tenants } from "./schema/tenants.js";
|
|
11
|
+
const PLATFORM_TENANT_ID = "holyship-platform";
|
|
12
|
+
const PLATFORM_TENANT_SLUG = "holyship-platform";
|
|
13
|
+
const PLATFORM_INSTANCE_ID = "holyship-platform-internal";
|
|
14
|
+
export async function seedPlatformServiceTenant(db) {
|
|
15
|
+
// Check if tenant already exists
|
|
16
|
+
const existing = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.id, PLATFORM_TENANT_ID)).limit(1);
|
|
17
|
+
if (existing.length > 0) {
|
|
18
|
+
return { tenantId: PLATFORM_TENANT_ID, serviceKey: null };
|
|
19
|
+
}
|
|
20
|
+
// Create the platform_service tenant
|
|
21
|
+
await db.insert(tenants).values({
|
|
22
|
+
id: PLATFORM_TENANT_ID,
|
|
23
|
+
name: "Holy Ship Platform",
|
|
24
|
+
slug: PLATFORM_TENANT_SLUG,
|
|
25
|
+
type: "platform_service",
|
|
26
|
+
ownerId: "system",
|
|
27
|
+
billingEmail: null,
|
|
28
|
+
createdAt: Date.now(),
|
|
29
|
+
});
|
|
30
|
+
// Generate a service key and store its hash
|
|
31
|
+
const rawKey = `sk-hs-${randomBytes(24).toString("hex")}`;
|
|
32
|
+
const keyHash = createHash("sha256").update(rawKey).digest("hex");
|
|
33
|
+
await db.insert(gatewayServiceKeys).values({
|
|
34
|
+
id: crypto.randomUUID(),
|
|
35
|
+
keyHash,
|
|
36
|
+
tenantId: PLATFORM_TENANT_ID,
|
|
37
|
+
instanceId: PLATFORM_INSTANCE_ID,
|
|
38
|
+
createdAt: Date.now(),
|
|
39
|
+
revokedAt: null,
|
|
40
|
+
});
|
|
41
|
+
return { tenantId: PLATFORM_TENANT_ID, serviceKey: rawKey };
|
|
42
|
+
}
|
|
@@ -63,7 +63,8 @@ export function capabilityRateLimit(config, repo) {
|
|
|
63
63
|
return next();
|
|
64
64
|
}
|
|
65
65
|
const tenant = c.get("gatewayTenant");
|
|
66
|
-
const
|
|
66
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
67
|
+
const tenantId = attributedTenantId ?? tenant?.id ?? "unknown";
|
|
67
68
|
const max = limits[category];
|
|
68
69
|
const scope = `cap:${category}`;
|
|
69
70
|
const now = Date.now();
|
|
@@ -43,4 +43,4 @@ export declare function creditBalanceCheck(c: Context<GatewayAuthEnv>, deps: Cre
|
|
|
43
43
|
/**
|
|
44
44
|
* Post-call credit debit. Fire-and-forget — never fails the response.
|
|
45
45
|
*/
|
|
46
|
-
export declare function debitCredits(deps: CreditGateDeps, tenantId: string, costUsd: number, margin: number, capability: string, provider: string, attributedUserId?: string): Promise<void>;
|
|
46
|
+
export declare function debitCredits(deps: CreditGateDeps, tenantId: string, costUsd: number, margin: number, capability: string, provider: string, attributedUserId?: string, attributedTenantId?: string | null): Promise<void>;
|
|
@@ -21,6 +21,11 @@ export async function creditBalanceCheck(c, deps, estimatedCostCents = 0) {
|
|
|
21
21
|
if (!deps.creditLedger)
|
|
22
22
|
return null;
|
|
23
23
|
const tenant = c.get("gatewayTenant");
|
|
24
|
+
// Platform service accounts bypass the pre-call balance gate.
|
|
25
|
+
// The company never 402s itself. Debit still runs post-call for P&L tracking.
|
|
26
|
+
if (tenant.type === "platform_service") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
24
29
|
const balance = await deps.creditLedger.balance(tenant.id);
|
|
25
30
|
const required = Math.max(0, estimatedCostCents);
|
|
26
31
|
const graceBuffer = deps.graceBufferCents ?? 50; // default -$0.50
|
|
@@ -55,7 +60,7 @@ export async function creditBalanceCheck(c, deps, estimatedCostCents = 0) {
|
|
|
55
60
|
/**
|
|
56
61
|
* Post-call credit debit. Fire-and-forget — never fails the response.
|
|
57
62
|
*/
|
|
58
|
-
export async function debitCredits(deps, tenantId, costUsd, margin, capability, provider, attributedUserId) {
|
|
63
|
+
export async function debitCredits(deps, tenantId, costUsd, margin, capability, provider, attributedUserId, attributedTenantId) {
|
|
59
64
|
if (!deps.creditLedger)
|
|
60
65
|
return;
|
|
61
66
|
const chargeCredit = withMargin(Credit.fromDollars(costUsd), margin);
|
|
@@ -75,6 +80,7 @@ export async function debitCredits(deps, tenantId, costUsd, margin, capability,
|
|
|
75
80
|
description: `Gateway ${capability} via ${provider}`,
|
|
76
81
|
allowNegative: true,
|
|
77
82
|
attributedUserId,
|
|
83
|
+
...(attributedTenantId ? { attributed_tenant_id: attributedTenantId } : {}),
|
|
78
84
|
});
|
|
79
85
|
// Only fire on first zero-crossing (balance was positive before, now ≤ 0)
|
|
80
86
|
if (deps.onBalanceExhausted) {
|
|
@@ -96,6 +96,7 @@ export function createAnthropicRoutes(deps) {
|
|
|
96
96
|
function messagesHandler(deps) {
|
|
97
97
|
return async (c) => {
|
|
98
98
|
const tenant = c.get("gatewayTenant");
|
|
99
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
99
100
|
// Budget check
|
|
100
101
|
const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
|
|
101
102
|
if (!budgetResult.allowed) {
|
|
@@ -174,7 +175,7 @@ function messagesHandler(deps) {
|
|
|
174
175
|
provider: "openrouter",
|
|
175
176
|
timestamp: Date.now(),
|
|
176
177
|
});
|
|
177
|
-
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
|
|
178
|
+
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter", undefined, attributedTenantId);
|
|
178
179
|
}
|
|
179
180
|
return new Response(res.body, {
|
|
180
181
|
status: res.status,
|
|
@@ -109,6 +109,7 @@ export function createOpenAIRoutes(deps) {
|
|
|
109
109
|
function chatCompletionsHandler(deps) {
|
|
110
110
|
return async (c) => {
|
|
111
111
|
const tenant = c.get("gatewayTenant");
|
|
112
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
112
113
|
// Budget check
|
|
113
114
|
const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
|
|
114
115
|
if (!budgetResult.allowed) {
|
|
@@ -187,7 +188,7 @@ function chatCompletionsHandler(deps) {
|
|
|
187
188
|
provider: "openrouter",
|
|
188
189
|
timestamp: Date.now(),
|
|
189
190
|
});
|
|
190
|
-
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
|
|
191
|
+
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter", undefined, attributedTenantId);
|
|
191
192
|
}
|
|
192
193
|
return new Response(res.body, {
|
|
193
194
|
status: res.status,
|
|
@@ -219,7 +220,7 @@ function chatCompletionsHandler(deps) {
|
|
|
219
220
|
provider: "openrouter",
|
|
220
221
|
timestamp: Date.now(),
|
|
221
222
|
});
|
|
222
|
-
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
|
|
223
|
+
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter", undefined, attributedTenantId);
|
|
223
224
|
}
|
|
224
225
|
return new Response(responseBody, {
|
|
225
226
|
status: res.status,
|
|
@@ -246,6 +247,7 @@ function chatCompletionsHandler(deps) {
|
|
|
246
247
|
function embeddingsHandler(deps) {
|
|
247
248
|
return async (c) => {
|
|
248
249
|
const tenant = c.get("gatewayTenant");
|
|
250
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
249
251
|
// Budget check
|
|
250
252
|
const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
|
|
251
253
|
if (!budgetResult.allowed) {
|
|
@@ -311,7 +313,7 @@ function embeddingsHandler(deps) {
|
|
|
311
313
|
provider: "openrouter",
|
|
312
314
|
timestamp: Date.now(),
|
|
313
315
|
});
|
|
314
|
-
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "embeddings", "openrouter");
|
|
316
|
+
debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "embeddings", "openrouter", undefined, attributedTenantId);
|
|
315
317
|
}
|
|
316
318
|
return new Response(responseBody, {
|
|
317
319
|
status: res.status,
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface GatewayEndpoint {
|
|
|
34
34
|
export interface GatewayTenant {
|
|
35
35
|
/** Tenant ID */
|
|
36
36
|
id: string;
|
|
37
|
+
/** Tenant type — platform_service accounts bypass the pre-call credit gate */
|
|
38
|
+
type?: "personal" | "org" | "platform_service";
|
|
37
39
|
/** Spend limits for budget checking */
|
|
38
40
|
spendLimits: SpendLimits;
|
|
39
41
|
/** Plan tier for rate limit lookup */
|
package/package.json
CHANGED
package/src/db/schema/tenants.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const tenants = pgTable(
|
|
|
7
7
|
id: text("id").primaryKey(), // nanoid or crypto.randomUUID()
|
|
8
8
|
name: text("name").notNull(),
|
|
9
9
|
slug: text("slug").unique(),
|
|
10
|
-
type: text("type").notNull(), // "personal" | "org"
|
|
10
|
+
type: text("type").notNull(), // "personal" | "org" | "platform_service"
|
|
11
11
|
ownerId: text("owner_id").notNull(), // user who created it
|
|
12
12
|
billingEmail: text("billing_email"),
|
|
13
13
|
createdAt: bigint("created_at", { mode: "number" })
|
|
@@ -18,6 +18,6 @@ export const tenants = pgTable(
|
|
|
18
18
|
index("idx_tenants_slug").on(table.slug),
|
|
19
19
|
index("idx_tenants_owner").on(table.ownerId),
|
|
20
20
|
index("idx_tenants_type").on(table.type),
|
|
21
|
-
check("chk_tenants_type", sql`${table.type} IN ('personal', 'org')`),
|
|
21
|
+
check("chk_tenants_type", sql`${table.type} IN ('personal', 'org', 'platform_service')`),
|
|
22
22
|
],
|
|
23
23
|
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent seed: creates the holyship-platform internal billing tenant
|
|
3
|
+
* with a stable service key for platform-to-gateway LLM billing.
|
|
4
|
+
*
|
|
5
|
+
* Safe to call on every boot — skips if the tenant already exists.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { eq } from "drizzle-orm";
|
|
9
|
+
import type { DrizzleDb } from "./index.js";
|
|
10
|
+
import { gatewayServiceKeys } from "./schema/gateway-service-keys.js";
|
|
11
|
+
import { tenants } from "./schema/tenants.js";
|
|
12
|
+
|
|
13
|
+
const PLATFORM_TENANT_ID = "holyship-platform";
|
|
14
|
+
const PLATFORM_TENANT_SLUG = "holyship-platform";
|
|
15
|
+
const PLATFORM_INSTANCE_ID = "holyship-platform-internal";
|
|
16
|
+
|
|
17
|
+
export interface PlatformServiceSeedResult {
|
|
18
|
+
tenantId: string;
|
|
19
|
+
serviceKey: string | null; // null if tenant + key already existed
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function seedPlatformServiceTenant(db: DrizzleDb): Promise<PlatformServiceSeedResult> {
|
|
23
|
+
// Check if tenant already exists
|
|
24
|
+
const existing = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.id, PLATFORM_TENANT_ID)).limit(1);
|
|
25
|
+
|
|
26
|
+
if (existing.length > 0) {
|
|
27
|
+
return { tenantId: PLATFORM_TENANT_ID, serviceKey: null };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create the platform_service tenant
|
|
31
|
+
await db.insert(tenants).values({
|
|
32
|
+
id: PLATFORM_TENANT_ID,
|
|
33
|
+
name: "Holy Ship Platform",
|
|
34
|
+
slug: PLATFORM_TENANT_SLUG,
|
|
35
|
+
type: "platform_service",
|
|
36
|
+
ownerId: "system",
|
|
37
|
+
billingEmail: null,
|
|
38
|
+
createdAt: Date.now(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Generate a service key and store its hash
|
|
42
|
+
const rawKey = `sk-hs-${randomBytes(24).toString("hex")}`;
|
|
43
|
+
const keyHash = createHash("sha256").update(rawKey).digest("hex");
|
|
44
|
+
|
|
45
|
+
await db.insert(gatewayServiceKeys).values({
|
|
46
|
+
id: crypto.randomUUID(),
|
|
47
|
+
keyHash,
|
|
48
|
+
tenantId: PLATFORM_TENANT_ID,
|
|
49
|
+
instanceId: PLATFORM_INSTANCE_ID,
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
revokedAt: null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return { tenantId: PLATFORM_TENANT_ID, serviceKey: rawKey };
|
|
55
|
+
}
|
|
@@ -94,7 +94,8 @@ export function capabilityRateLimit(
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const tenant = c.get("gatewayTenant") as GatewayTenant | undefined;
|
|
97
|
-
const
|
|
97
|
+
const attributedTenantId = c.get("attributedTenantId") as string | null | undefined;
|
|
98
|
+
const tenantId = attributedTenantId ?? tenant?.id ?? "unknown";
|
|
98
99
|
const max = limits[category];
|
|
99
100
|
const scope = `cap:${category}`;
|
|
100
101
|
const now = Date.now();
|
|
@@ -54,6 +54,13 @@ export async function creditBalanceCheck(
|
|
|
54
54
|
if (!deps.creditLedger) return null;
|
|
55
55
|
|
|
56
56
|
const tenant = c.get("gatewayTenant");
|
|
57
|
+
|
|
58
|
+
// Platform service accounts bypass the pre-call balance gate.
|
|
59
|
+
// The company never 402s itself. Debit still runs post-call for P&L tracking.
|
|
60
|
+
if (tenant.type === "platform_service") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
const balance = await deps.creditLedger.balance(tenant.id);
|
|
58
65
|
const required = Math.max(0, estimatedCostCents);
|
|
59
66
|
const graceBuffer = deps.graceBufferCents ?? 50; // default -$0.50
|
|
@@ -100,6 +107,7 @@ export async function debitCredits(
|
|
|
100
107
|
capability: string,
|
|
101
108
|
provider: string,
|
|
102
109
|
attributedUserId?: string,
|
|
110
|
+
attributedTenantId?: string | null,
|
|
103
111
|
): Promise<void> {
|
|
104
112
|
if (!deps.creditLedger) return;
|
|
105
113
|
|
|
@@ -122,6 +130,7 @@ export async function debitCredits(
|
|
|
122
130
|
description: `Gateway ${capability} via ${provider}`,
|
|
123
131
|
allowNegative: true,
|
|
124
132
|
attributedUserId,
|
|
133
|
+
...(attributedTenantId ? { attributed_tenant_id: attributedTenantId } : {}),
|
|
125
134
|
});
|
|
126
135
|
|
|
127
136
|
// Only fire on first zero-crossing (balance was positive before, now ≤ 0)
|
|
@@ -136,6 +136,7 @@ export function createAnthropicRoutes(deps: ProtocolDeps): Hono<GatewayAuthEnv>
|
|
|
136
136
|
function messagesHandler(deps: ProtocolDeps) {
|
|
137
137
|
return async (c: Context<GatewayAuthEnv>) => {
|
|
138
138
|
const tenant = c.get("gatewayTenant");
|
|
139
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
139
140
|
|
|
140
141
|
// Budget check
|
|
141
142
|
const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
|
|
@@ -224,7 +225,16 @@ function messagesHandler(deps: ProtocolDeps) {
|
|
|
224
225
|
provider: "openrouter",
|
|
225
226
|
timestamp: Date.now(),
|
|
226
227
|
});
|
|
227
|
-
debitCredits(
|
|
228
|
+
debitCredits(
|
|
229
|
+
deps,
|
|
230
|
+
tenant.id,
|
|
231
|
+
costUsd,
|
|
232
|
+
deps.defaultMargin,
|
|
233
|
+
"chat-completions",
|
|
234
|
+
"openrouter",
|
|
235
|
+
undefined,
|
|
236
|
+
attributedTenantId,
|
|
237
|
+
);
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
return new Response(res.body, {
|
|
@@ -146,6 +146,7 @@ export function createOpenAIRoutes(deps: ProtocolDeps): Hono<GatewayAuthEnv> {
|
|
|
146
146
|
function chatCompletionsHandler(deps: ProtocolDeps) {
|
|
147
147
|
return async (c: Context<GatewayAuthEnv>) => {
|
|
148
148
|
const tenant = c.get("gatewayTenant");
|
|
149
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
149
150
|
|
|
150
151
|
// Budget check
|
|
151
152
|
const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
|
|
@@ -241,7 +242,16 @@ function chatCompletionsHandler(deps: ProtocolDeps) {
|
|
|
241
242
|
provider: "openrouter",
|
|
242
243
|
timestamp: Date.now(),
|
|
243
244
|
});
|
|
244
|
-
debitCredits(
|
|
245
|
+
debitCredits(
|
|
246
|
+
deps,
|
|
247
|
+
tenant.id,
|
|
248
|
+
costUsd,
|
|
249
|
+
deps.defaultMargin,
|
|
250
|
+
"chat-completions",
|
|
251
|
+
"openrouter",
|
|
252
|
+
undefined,
|
|
253
|
+
attributedTenantId,
|
|
254
|
+
);
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
return new Response(res.body, {
|
|
@@ -277,7 +287,16 @@ function chatCompletionsHandler(deps: ProtocolDeps) {
|
|
|
277
287
|
provider: "openrouter",
|
|
278
288
|
timestamp: Date.now(),
|
|
279
289
|
});
|
|
280
|
-
debitCredits(
|
|
290
|
+
debitCredits(
|
|
291
|
+
deps,
|
|
292
|
+
tenant.id,
|
|
293
|
+
costUsd,
|
|
294
|
+
deps.defaultMargin,
|
|
295
|
+
"chat-completions",
|
|
296
|
+
"openrouter",
|
|
297
|
+
undefined,
|
|
298
|
+
attributedTenantId,
|
|
299
|
+
);
|
|
281
300
|
}
|
|
282
301
|
|
|
283
302
|
return new Response(responseBody, {
|
|
@@ -309,6 +328,7 @@ function chatCompletionsHandler(deps: ProtocolDeps) {
|
|
|
309
328
|
function embeddingsHandler(deps: ProtocolDeps) {
|
|
310
329
|
return async (c: Context<GatewayAuthEnv>) => {
|
|
311
330
|
const tenant = c.get("gatewayTenant");
|
|
331
|
+
const attributedTenantId = c.get("attributedTenantId");
|
|
312
332
|
|
|
313
333
|
// Budget check
|
|
314
334
|
const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
|
|
@@ -391,7 +411,16 @@ function embeddingsHandler(deps: ProtocolDeps) {
|
|
|
391
411
|
provider: "openrouter",
|
|
392
412
|
timestamp: Date.now(),
|
|
393
413
|
});
|
|
394
|
-
debitCredits(
|
|
414
|
+
debitCredits(
|
|
415
|
+
deps,
|
|
416
|
+
tenant.id,
|
|
417
|
+
costUsd,
|
|
418
|
+
deps.defaultMargin,
|
|
419
|
+
"embeddings",
|
|
420
|
+
"openrouter",
|
|
421
|
+
undefined,
|
|
422
|
+
attributedTenantId,
|
|
423
|
+
);
|
|
395
424
|
}
|
|
396
425
|
|
|
397
426
|
return new Response(responseBody, {
|
|
@@ -14,6 +14,7 @@ export interface GatewayAuthEnv {
|
|
|
14
14
|
Variables: {
|
|
15
15
|
gatewayTenant: GatewayTenant;
|
|
16
16
|
webhookBody: Record<string, unknown>;
|
|
17
|
+
attributedTenantId: string | null;
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -90,6 +91,8 @@ export function serviceKeyAuth(
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
c.set("gatewayTenant", tenant);
|
|
94
|
+
const attributedTenantId = c.req.header("x-attribute-to") ?? null;
|
|
95
|
+
c.set("attributedTenantId", attributedTenantId);
|
|
93
96
|
return next();
|
|
94
97
|
};
|
|
95
98
|
}
|
package/src/gateway/types.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface GatewayEndpoint {
|
|
|
47
47
|
export interface GatewayTenant {
|
|
48
48
|
/** Tenant ID */
|
|
49
49
|
id: string;
|
|
50
|
+
/** Tenant type — platform_service accounts bypass the pre-call credit gate */
|
|
51
|
+
type?: "personal" | "org" | "platform_service";
|
|
50
52
|
/** Spend limits for budget checking */
|
|
51
53
|
spendLimits: SpendLimits;
|
|
52
54
|
/** Plan tier for rate limit lookup */
|