@tokenbuddy/tokenbuddy 1.0.30 → 1.0.33
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/src/buyer-store.d.ts +26 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +61 -8
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/daemon.d.ts +24 -0
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +1259 -9
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +12 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +71 -2
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/prewarm-cache.js +4 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/provider-routing-config.d.ts +91 -0
- package/dist/src/provider-routing-config.d.ts.map +1 -0
- package/dist/src/provider-routing-config.js +292 -0
- package/dist/src/provider-routing-config.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +113 -8
- package/src/daemon.ts +1389 -16
- package/src/doctor-diagnostics.ts +100 -1
- package/src/prewarm-cache.ts +5 -1
- package/src/provider-routing-config.ts +410 -0
- package/static/ui/assets/index-0MVXD7bH.css +1 -0
- package/static/ui/assets/index-Mt3BZFuP.js +266 -0
- package/static/ui/assets/index-Mt3BZFuP.js.map +1 -0
- package/static/ui/icons/apple-touch-icon.png +0 -0
- package/static/ui/icons/tokenbuddy-192.png +0 -0
- package/static/ui/icons/tokenbuddy-512.png +0 -0
- package/static/ui/icons/tokenbuddy.svg +6 -0
- package/static/ui/index.html +3 -2
- package/static/ui/tool-logos/cc-switch.png +0 -0
- package/static/ui/tool-logos/claude.svg +1 -0
- package/static/ui/tool-logos/cline.svg +1 -0
- package/static/ui/tool-logos/codex.svg +1 -0
- package/static/ui/tool-logos/cursor.svg +1 -0
- package/static/ui/tool-logos/dirac.ico +18 -0
- package/static/ui/tool-logos/factory.svg +4 -0
- package/static/ui/tool-logos/fast-agent.svg +6 -0
- package/static/ui/tool-logos/glm.svg +1 -0
- package/static/ui/tool-logos/goose.svg +1 -0
- package/static/ui/tool-logos/hermes.svg +1 -0
- package/static/ui/tool-logos/kilocode.svg +1 -0
- package/static/ui/tool-logos/opencode.svg +1 -0
- package/static/ui/tool-logos/pi.svg +28 -0
- package/static/ui/tool-logos/qwen-code.png +0 -0
- package/tests/cli-routing.test.ts +43 -0
- package/tests/control-plane-ui-endpoints.test.ts +776 -0
- package/tests/daemon-classify.test.ts +5 -1
- package/tests/e2e.test.ts +5 -0
- package/tests/prewarm-cache.test.ts +15 -0
- package/tests/provider-routing-config.test.ts +150 -0
- package/tests/tokenbuddy.test.ts +27 -0
- package/static/ui/assets/index-Bzbrp7Qe.css +0 -1
- package/static/ui/assets/index-DEDEl8o2.js +0 -236
- package/static/ui/assets/index-DEDEl8o2.js.map +0 -1
package/src/daemon.ts
CHANGED
|
@@ -63,6 +63,27 @@ import {
|
|
|
63
63
|
ROUTING_CONFIG_KEY,
|
|
64
64
|
type BuyerSellerRoutingConfig
|
|
65
65
|
} from "./seller-routing-config.js";
|
|
66
|
+
import {
|
|
67
|
+
AUTO_PROVIDER_CONFIG_KEY,
|
|
68
|
+
MANUAL_PROVIDER_CONFIG_KEY,
|
|
69
|
+
MANUAL_PROVIDER_OBSERVATIONS_CONFIG_KEY,
|
|
70
|
+
PROVIDER_MODE_CONFIG_KEY,
|
|
71
|
+
defaultAutoProviderConfig,
|
|
72
|
+
defaultProviderModeConfig,
|
|
73
|
+
normalizeAutoProviderConfig,
|
|
74
|
+
normalizeManualProviderObservationsConfig,
|
|
75
|
+
normalizeManualProviderConfig,
|
|
76
|
+
normalizeManualProvidersConfig,
|
|
77
|
+
normalizeProviderModeConfig,
|
|
78
|
+
publicManualProviderConfig,
|
|
79
|
+
type AutoProviderConfig,
|
|
80
|
+
type ManualProviderConfig,
|
|
81
|
+
type ManualProviderObservation,
|
|
82
|
+
type ManualProviderObservationsConfig,
|
|
83
|
+
type ManualProvidersConfig,
|
|
84
|
+
type ProviderMode,
|
|
85
|
+
type ProviderModeConfig
|
|
86
|
+
} from "./provider-routing-config.js";
|
|
66
87
|
import {
|
|
67
88
|
assertInitSetupSteps,
|
|
68
89
|
buildCompletedInitSetupMarker,
|
|
@@ -86,6 +107,18 @@ const PROXY_JSON_BODY_LIMIT = "10mb";
|
|
|
86
107
|
const SELLER_CAPACITY_BLOCK_MS = 2_000;
|
|
87
108
|
const CLAWTIP_STATIC_ROUTE = "/static/clawtip";
|
|
88
109
|
const CLAWTIP_RECHARGE_QR_FILE = "recharge.png";
|
|
110
|
+
const MANUAL_PROVIDER_SECRET_CONFIG_KEY = "manual-provider-secrets";
|
|
111
|
+
|
|
112
|
+
interface ManualProviderSecretsConfig {
|
|
113
|
+
version: 1;
|
|
114
|
+
secrets: Array<{
|
|
115
|
+
secretRef: string;
|
|
116
|
+
apiKey: string;
|
|
117
|
+
createdAt: string;
|
|
118
|
+
updatedAt: string;
|
|
119
|
+
}>;
|
|
120
|
+
updatedAt: string;
|
|
121
|
+
}
|
|
89
122
|
|
|
90
123
|
function currentModuleDir(): string {
|
|
91
124
|
if (typeof __dirname !== "undefined") {
|
|
@@ -263,6 +296,8 @@ export interface DaemonConfig {
|
|
|
263
296
|
|
|
264
297
|
interface SellerRoute {
|
|
265
298
|
seller: RegistrySeller;
|
|
299
|
+
transport: "tokenbuddy_seller" | "manual_provider";
|
|
300
|
+
manualProvider?: ManualProviderConfig;
|
|
266
301
|
manifest: SellerManifest | null;
|
|
267
302
|
protocol: string;
|
|
268
303
|
modelId: string;
|
|
@@ -276,6 +311,7 @@ interface SellerRoute {
|
|
|
276
311
|
interface UsageSummary {
|
|
277
312
|
promptTokens: number;
|
|
278
313
|
completionTokens: number;
|
|
314
|
+
cacheReadTokens: number;
|
|
279
315
|
billedMicros: number;
|
|
280
316
|
}
|
|
281
317
|
|
|
@@ -325,6 +361,19 @@ interface SellerSettlementSummary {
|
|
|
325
361
|
reservedBalanceMicros?: number;
|
|
326
362
|
spentMicros?: number;
|
|
327
363
|
priceVersion?: string;
|
|
364
|
+
billingBreakdown?: BillingBreakdownSummary;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
interface BillingBreakdownSummary {
|
|
368
|
+
inputPriceMicrosPer1m: number;
|
|
369
|
+
outputPriceMicrosPer1m: number;
|
|
370
|
+
cacheReadPriceMicrosPer1m: number;
|
|
371
|
+
inputCostMicros: number;
|
|
372
|
+
outputCostMicros: number;
|
|
373
|
+
cacheReadCostMicros: number;
|
|
374
|
+
originalUsdMicros: number;
|
|
375
|
+
billingMultiplier: number;
|
|
376
|
+
serviceTier?: string;
|
|
328
377
|
}
|
|
329
378
|
|
|
330
379
|
interface SellerAttemptRequestContext {
|
|
@@ -339,6 +388,12 @@ interface SellerBalanceSnapshot {
|
|
|
339
388
|
availableMicros: number;
|
|
340
389
|
}
|
|
341
390
|
|
|
391
|
+
interface PurchasePaymentSummary {
|
|
392
|
+
paymentAmount?: string;
|
|
393
|
+
paymentAmountMinor?: number;
|
|
394
|
+
paymentCurrency?: string;
|
|
395
|
+
}
|
|
396
|
+
|
|
342
397
|
function numericHeaderField(value: unknown): number | undefined {
|
|
343
398
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
344
399
|
return value;
|
|
@@ -350,6 +405,77 @@ function numericHeaderField(value: unknown): number | undefined {
|
|
|
350
405
|
return undefined;
|
|
351
406
|
}
|
|
352
407
|
|
|
408
|
+
function nonNegativeIntegerField(value: unknown): number | undefined {
|
|
409
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function nonNegativeFiniteField(value: unknown): number | undefined {
|
|
413
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function usageRecord(value: unknown): Record<string, unknown> | undefined {
|
|
417
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function safeBillingServiceTier(value: unknown): string | undefined {
|
|
421
|
+
if (typeof value !== "string") return undefined;
|
|
422
|
+
const trimmed = value.trim();
|
|
423
|
+
if (trimmed.length === 0 || trimmed.length > 64) return undefined;
|
|
424
|
+
return /^[A-Za-z0-9 _.-]+$/.test(trimmed) ? trimmed : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function billingBreakdownSummary(value: unknown): BillingBreakdownSummary | undefined {
|
|
428
|
+
const data = usageRecord(value);
|
|
429
|
+
if (!data) return undefined;
|
|
430
|
+
const inputPriceMicrosPer1m = nonNegativeIntegerField(data.inputPriceMicrosPer1m ?? data.input_price_micros_per_1m);
|
|
431
|
+
const outputPriceMicrosPer1m = nonNegativeIntegerField(data.outputPriceMicrosPer1m ?? data.output_price_micros_per_1m);
|
|
432
|
+
const cacheReadPriceMicrosPer1m = nonNegativeIntegerField(data.cacheReadPriceMicrosPer1m ?? data.cache_read_price_micros_per_1m);
|
|
433
|
+
const inputCostMicros = nonNegativeIntegerField(data.inputCostMicros ?? data.input_cost_micros);
|
|
434
|
+
const outputCostMicros = nonNegativeIntegerField(data.outputCostMicros ?? data.output_cost_micros);
|
|
435
|
+
const cacheReadCostMicros = nonNegativeIntegerField(data.cacheReadCostMicros ?? data.cache_read_cost_micros);
|
|
436
|
+
const originalUsdMicros = nonNegativeIntegerField(data.originalUsdMicros ?? data.original_usd_micros);
|
|
437
|
+
const billingMultiplier = nonNegativeFiniteField(data.billingMultiplier ?? data.billing_multiplier);
|
|
438
|
+
if (
|
|
439
|
+
inputPriceMicrosPer1m === undefined ||
|
|
440
|
+
outputPriceMicrosPer1m === undefined ||
|
|
441
|
+
cacheReadPriceMicrosPer1m === undefined ||
|
|
442
|
+
inputCostMicros === undefined ||
|
|
443
|
+
outputCostMicros === undefined ||
|
|
444
|
+
cacheReadCostMicros === undefined ||
|
|
445
|
+
originalUsdMicros === undefined ||
|
|
446
|
+
billingMultiplier === undefined
|
|
447
|
+
) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
inputPriceMicrosPer1m,
|
|
452
|
+
outputPriceMicrosPer1m,
|
|
453
|
+
cacheReadPriceMicrosPer1m,
|
|
454
|
+
inputCostMicros,
|
|
455
|
+
outputCostMicros,
|
|
456
|
+
cacheReadCostMicros,
|
|
457
|
+
originalUsdMicros,
|
|
458
|
+
billingMultiplier,
|
|
459
|
+
serviceTier: safeBillingServiceTier(data.serviceTier ?? data.service_tier)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function purchasePaymentSummaryFromQuote(value: unknown): PurchasePaymentSummary {
|
|
464
|
+
const quote = usageRecord(value);
|
|
465
|
+
if (!quote) return {};
|
|
466
|
+
const paymentAmount = typeof quote.paymentAmount === "string" && quote.paymentAmount.trim().length > 0
|
|
467
|
+
? quote.paymentAmount.trim()
|
|
468
|
+
: undefined;
|
|
469
|
+
const paymentCurrency = typeof quote.paymentCurrency === "string" && quote.paymentCurrency.trim().length > 0
|
|
470
|
+
? quote.paymentCurrency.trim().toUpperCase()
|
|
471
|
+
: undefined;
|
|
472
|
+
return {
|
|
473
|
+
paymentAmount,
|
|
474
|
+
paymentAmountMinor: nonNegativeIntegerField(quote.paymentAmountMinor),
|
|
475
|
+
paymentCurrency
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
353
479
|
class SellerSettlementStreamExtractor {
|
|
354
480
|
private pending = "";
|
|
355
481
|
private settlement: SellerSettlementSummary | undefined;
|
|
@@ -422,7 +548,8 @@ function parseSellerSettlementObject(raw: string): SellerSettlementSummary | und
|
|
|
422
548
|
? parsed.priceVersion
|
|
423
549
|
: typeof parsed.price_version === "string"
|
|
424
550
|
? parsed.price_version
|
|
425
|
-
: undefined
|
|
551
|
+
: undefined,
|
|
552
|
+
billingBreakdown: billingBreakdownSummary(parsed.billingBreakdown ?? parsed.billing_breakdown)
|
|
426
553
|
};
|
|
427
554
|
} catch {
|
|
428
555
|
return undefined;
|
|
@@ -483,6 +610,11 @@ function finiteNumber(value: unknown): number | undefined {
|
|
|
483
610
|
return undefined;
|
|
484
611
|
}
|
|
485
612
|
|
|
613
|
+
function finitePositiveNumber(value: unknown): number | undefined {
|
|
614
|
+
const number = finiteNumber(value);
|
|
615
|
+
return number !== undefined && number > 0 ? number : undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
486
618
|
function readErrorCode(bodyText: string): string | undefined {
|
|
487
619
|
try {
|
|
488
620
|
const parsed = JSON.parse(bodyText) as { error?: { code?: unknown }; code?: unknown };
|
|
@@ -668,7 +800,7 @@ export class TokenbuddyDaemon {
|
|
|
668
800
|
httpStatus: res.status,
|
|
669
801
|
ttftMs: finiteNumber(latency?.ttftMs ?? latency?.ttft_ms),
|
|
670
802
|
avgInferenceMs: finiteNumber(latency?.avgInferenceMs ?? latency?.avg_inference_ms),
|
|
671
|
-
avgTokensPerSecond:
|
|
803
|
+
avgTokensPerSecond: finitePositiveNumber(latency?.avgTokensPerSecond ?? latency?.avg_tokens_per_second),
|
|
672
804
|
upstreamStatus: typeof upstream?.status === "string"
|
|
673
805
|
? upstream.status as "healthy" | "degraded" | "unhealthy" | "unknown"
|
|
674
806
|
: undefined,
|
|
@@ -1049,6 +1181,187 @@ export class TokenbuddyDaemon {
|
|
|
1049
1181
|
return this.tokenStore.listPayments().map((payment) => withLiveClawtipWalletState(payment, this.config.clawtipHomeDir));
|
|
1050
1182
|
}
|
|
1051
1183
|
|
|
1184
|
+
private providerPaymentState(): { paymentReady: boolean; paymentLabel?: string } {
|
|
1185
|
+
const payment = this.livePayments().find((entry) => entry.enabled && entry.method !== "mock");
|
|
1186
|
+
return {
|
|
1187
|
+
paymentReady: Boolean(payment),
|
|
1188
|
+
paymentLabel: payment ? paymentLabel(payment.method) : undefined
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
private currentProviderMode(): ProviderModeConfig {
|
|
1193
|
+
return normalizeProviderModeConfig(
|
|
1194
|
+
this.tokenStore.getDaemonRuntimeConfig<unknown>(PROVIDER_MODE_CONFIG_KEY)?.config
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private saveProviderMode(mode: ProviderMode): ProviderModeConfig {
|
|
1199
|
+
const config = {
|
|
1200
|
+
...defaultProviderModeConfig(),
|
|
1201
|
+
mode
|
|
1202
|
+
};
|
|
1203
|
+
this.tokenStore.saveDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY, config);
|
|
1204
|
+
return config;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private currentManualProviders(): ManualProvidersConfig {
|
|
1208
|
+
return normalizeManualProvidersConfig(
|
|
1209
|
+
this.tokenStore.getDaemonRuntimeConfig<unknown>(MANUAL_PROVIDER_CONFIG_KEY)?.config
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private saveManualProviders(config: ManualProvidersConfig): void {
|
|
1214
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_CONFIG_KEY, {
|
|
1215
|
+
...config,
|
|
1216
|
+
updatedAt: new Date().toISOString()
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private currentManualProviderSecrets(): ManualProviderSecretsConfig {
|
|
1221
|
+
const value = this.tokenStore.getDaemonRuntimeConfig<unknown>(MANUAL_PROVIDER_SECRET_CONFIG_KEY)?.config;
|
|
1222
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1223
|
+
return { version: 1, secrets: [], updatedAt: new Date().toISOString() };
|
|
1224
|
+
}
|
|
1225
|
+
const record = value as { secrets?: unknown[]; updatedAt?: unknown };
|
|
1226
|
+
const secrets = (Array.isArray(record.secrets) ? record.secrets : [])
|
|
1227
|
+
.filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
|
|
1228
|
+
.flatMap((entry) => {
|
|
1229
|
+
const secretRef = typeof entry.secretRef === "string" ? entry.secretRef.trim() : "";
|
|
1230
|
+
const apiKey = typeof entry.apiKey === "string" ? entry.apiKey : "";
|
|
1231
|
+
if (!secretRef || !apiKey) return [];
|
|
1232
|
+
return [{
|
|
1233
|
+
secretRef,
|
|
1234
|
+
apiKey,
|
|
1235
|
+
createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date().toISOString(),
|
|
1236
|
+
updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : new Date().toISOString()
|
|
1237
|
+
}];
|
|
1238
|
+
});
|
|
1239
|
+
return {
|
|
1240
|
+
version: 1,
|
|
1241
|
+
secrets,
|
|
1242
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date().toISOString()
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
private saveManualProviderSecret(secretRef: string, apiKey: string): void {
|
|
1247
|
+
const now = new Date().toISOString();
|
|
1248
|
+
const current = this.currentManualProviderSecrets();
|
|
1249
|
+
const existing = current.secrets.find((entry) => entry.secretRef === secretRef);
|
|
1250
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_SECRET_CONFIG_KEY, {
|
|
1251
|
+
version: 1,
|
|
1252
|
+
secrets: [
|
|
1253
|
+
...current.secrets.filter((entry) => entry.secretRef !== secretRef),
|
|
1254
|
+
{
|
|
1255
|
+
secretRef,
|
|
1256
|
+
apiKey,
|
|
1257
|
+
createdAt: existing?.createdAt ?? now,
|
|
1258
|
+
updatedAt: now
|
|
1259
|
+
}
|
|
1260
|
+
],
|
|
1261
|
+
updatedAt: now
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
private removeManualProviderSecret(secretRef: string | undefined): void {
|
|
1266
|
+
if (!secretRef) return;
|
|
1267
|
+
const current = this.currentManualProviderSecrets();
|
|
1268
|
+
const secrets = current.secrets.filter((entry) => entry.secretRef !== secretRef);
|
|
1269
|
+
if (secrets.length === current.secrets.length) return;
|
|
1270
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_SECRET_CONFIG_KEY, {
|
|
1271
|
+
version: 1,
|
|
1272
|
+
secrets,
|
|
1273
|
+
updatedAt: new Date().toISOString()
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private currentManualProviderObservations(): ManualProviderObservationsConfig {
|
|
1278
|
+
return normalizeManualProviderObservationsConfig(
|
|
1279
|
+
this.tokenStore.getDaemonRuntimeConfig<unknown>(MANUAL_PROVIDER_OBSERVATIONS_CONFIG_KEY)?.config
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private recordManualProviderObservation(input: Omit<ManualProviderObservation, "lastAccess"> & { lastAccess?: string }): void {
|
|
1284
|
+
const now = input.lastAccess ?? new Date().toISOString();
|
|
1285
|
+
const current = this.currentManualProviderObservations();
|
|
1286
|
+
const observations = current.observations
|
|
1287
|
+
.filter((entry) => entry.providerId !== input.providerId)
|
|
1288
|
+
.map((entry) => ({
|
|
1289
|
+
...entry,
|
|
1290
|
+
current: input.current ? false : entry.current
|
|
1291
|
+
}));
|
|
1292
|
+
observations.push({
|
|
1293
|
+
providerId: input.providerId,
|
|
1294
|
+
current: input.current,
|
|
1295
|
+
lastAccess: now,
|
|
1296
|
+
status: input.status,
|
|
1297
|
+
errorClass: input.errorClass,
|
|
1298
|
+
errorMessage: input.errorMessage,
|
|
1299
|
+
ttftMs: input.ttftMs,
|
|
1300
|
+
avgTokensPerSecond: input.avgTokensPerSecond
|
|
1301
|
+
});
|
|
1302
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_OBSERVATIONS_CONFIG_KEY, {
|
|
1303
|
+
version: 1,
|
|
1304
|
+
observations,
|
|
1305
|
+
updatedAt: now
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private currentAutoProviderConfig(): AutoProviderConfig {
|
|
1310
|
+
return normalizeAutoProviderConfig(
|
|
1311
|
+
this.tokenStore.getDaemonRuntimeConfig<unknown>(AUTO_PROVIDER_CONFIG_KEY)?.config
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
private saveAutoProviderConfig(config: AutoProviderConfig): void {
|
|
1316
|
+
this.tokenStore.saveDaemonRuntimeConfig(AUTO_PROVIDER_CONFIG_KEY, {
|
|
1317
|
+
...config,
|
|
1318
|
+
updatedAt: new Date().toISOString()
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private applyAutoProviderRoutingConfig(config: AutoProviderConfig): BuyerSellerRoutingConfig {
|
|
1323
|
+
const routing: BuyerSellerRoutingConfig = config.range === "custom"
|
|
1324
|
+
? {
|
|
1325
|
+
mode: "fixedSet",
|
|
1326
|
+
scorer: config.scorer,
|
|
1327
|
+
sellerIds: config.sellerIds
|
|
1328
|
+
}
|
|
1329
|
+
: {
|
|
1330
|
+
mode: "fullAuto",
|
|
1331
|
+
scorer: config.scorer
|
|
1332
|
+
};
|
|
1333
|
+
assertSellerRoutingConfig(routing);
|
|
1334
|
+
this.tokenStore.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, routing);
|
|
1335
|
+
const current = this.refreshSellerRoutingConfig();
|
|
1336
|
+
if (config.modelIds.length > 0) {
|
|
1337
|
+
this.applyFocusSet(config.modelIds);
|
|
1338
|
+
}
|
|
1339
|
+
return current;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private autoProviderCanRoute(config: AutoProviderConfig): boolean {
|
|
1343
|
+
return config.enabled && (config.range !== "custom" || config.sellerIds.length > 0);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
private providerModePayload(): Record<string, unknown> {
|
|
1347
|
+
const mode = this.currentProviderMode();
|
|
1348
|
+
const autoProvider = this.currentAutoProviderConfig();
|
|
1349
|
+
const payment = this.providerPaymentState();
|
|
1350
|
+
return {
|
|
1351
|
+
mode: mode.mode,
|
|
1352
|
+
updatedAt: mode.updatedAt,
|
|
1353
|
+
active: mode.mode,
|
|
1354
|
+
manualEnabled: mode.mode === "manual",
|
|
1355
|
+
autoEnabled: mode.mode === "auto" && this.autoProviderCanRoute(autoProvider) && payment.paymentReady,
|
|
1356
|
+
paymentReady: payment.paymentReady,
|
|
1357
|
+
paymentRequired: !payment.paymentReady,
|
|
1358
|
+
paymentLabel: payment.paymentLabel,
|
|
1359
|
+
locked: {
|
|
1360
|
+
auto: !payment.paymentReady
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1052
1365
|
private clientToolsSummary(): { clients: ClientToolStatus[]; summary: { configuredCount: number; detectedCount: number; totalCount: number; installCommand: string } } {
|
|
1053
1366
|
const providerStatuses = detectProviders({ home: this.config.providerHomeDir }).map(clientToolStatusFromProvider);
|
|
1054
1367
|
const clients = [
|
|
@@ -1289,7 +1602,7 @@ export class TokenbuddyDaemon {
|
|
|
1289
1602
|
this.sellerPool.recordRuntimeMetrics(route.seller.id, {
|
|
1290
1603
|
ttftMs: finiteNumber(latency?.ttftMs ?? latency?.ttft_ms),
|
|
1291
1604
|
avgInferenceMs: finiteNumber(latency?.avgInferenceMs ?? latency?.avg_inference_ms),
|
|
1292
|
-
avgTokensPerSecond:
|
|
1605
|
+
avgTokensPerSecond: finitePositiveNumber(latency?.avgTokensPerSecond ?? latency?.avg_tokens_per_second)
|
|
1293
1606
|
});
|
|
1294
1607
|
} catch (error: unknown) {
|
|
1295
1608
|
logger.warn("pool.runtime_metrics.refresh_failed", "seller health refresh failed after inference", {
|
|
@@ -1393,6 +1706,91 @@ export class TokenbuddyDaemon {
|
|
|
1393
1706
|
return payments.find((payment) => payment.isDefault)?.method || payments.find((payment) => payment.method === "mock")?.method;
|
|
1394
1707
|
}
|
|
1395
1708
|
|
|
1709
|
+
private selectManualProviderRoutes(endpoint: string, modelId: string): { routes: SellerRoute[]; plan: SellerRoutePlan; paymentMethod: string } {
|
|
1710
|
+
const protocol = this.endpointProtocol(endpoint);
|
|
1711
|
+
if (!protocol) {
|
|
1712
|
+
throw new Error(`unsupported proxy endpoint: ${endpoint}`);
|
|
1713
|
+
}
|
|
1714
|
+
const config = this.currentManualProviders();
|
|
1715
|
+
let providers: ManualProviderConfig[];
|
|
1716
|
+
let reason: string;
|
|
1717
|
+
if (config.routing.policy === "locked") {
|
|
1718
|
+
const provider = config.providers.find((entry) => entry.id === config.routing.lockedProviderId);
|
|
1719
|
+
if (!provider) {
|
|
1720
|
+
throw new Error(`locked manual provider is not configured: ${config.routing.lockedProviderId}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (!provider.enabled) {
|
|
1723
|
+
throw new Error(`locked manual provider is disabled: ${provider.id}`);
|
|
1724
|
+
}
|
|
1725
|
+
if (!provider.supportedProtocols.includes(protocol as ManualProviderConfig["supportedProtocols"][number])) {
|
|
1726
|
+
throw new Error(`locked manual provider ${provider.id} does not support ${endpoint}`);
|
|
1727
|
+
}
|
|
1728
|
+
if (!provider.models.includes(modelId)) {
|
|
1729
|
+
throw new Error(`locked manual provider ${provider.id} does not support model ${modelId}`);
|
|
1730
|
+
}
|
|
1731
|
+
providers = [provider];
|
|
1732
|
+
reason = `manual:locked:${provider.id}`;
|
|
1733
|
+
} else {
|
|
1734
|
+
providers = config.providers.filter((provider) => (
|
|
1735
|
+
provider.enabled &&
|
|
1736
|
+
provider.models.includes(modelId) &&
|
|
1737
|
+
provider.supportedProtocols.includes(protocol as ManualProviderConfig["supportedProtocols"][number])
|
|
1738
|
+
));
|
|
1739
|
+
reason = `manual:fallback:routes_${providers.length}`;
|
|
1740
|
+
}
|
|
1741
|
+
if (providers.length === 0) {
|
|
1742
|
+
throw new Error(`no compatible manual provider for ${endpoint} model ${modelId}`);
|
|
1743
|
+
}
|
|
1744
|
+
const routes: SellerRoute[] = providers.map((provider) => ({
|
|
1745
|
+
seller: {
|
|
1746
|
+
id: provider.id,
|
|
1747
|
+
name: provider.name,
|
|
1748
|
+
url: provider.baseUrl,
|
|
1749
|
+
status: "active",
|
|
1750
|
+
supportedProtocols: provider.supportedProtocols,
|
|
1751
|
+
paymentMethods: ["provider_key"],
|
|
1752
|
+
models: provider.models
|
|
1753
|
+
},
|
|
1754
|
+
transport: "manual_provider",
|
|
1755
|
+
manualProvider: provider,
|
|
1756
|
+
manifest: null,
|
|
1757
|
+
protocol,
|
|
1758
|
+
modelId,
|
|
1759
|
+
paymentMethod: "provider_key",
|
|
1760
|
+
planSource: "registry_fallback",
|
|
1761
|
+
planReason: reason,
|
|
1762
|
+
planSellerCount: providers.length
|
|
1763
|
+
}));
|
|
1764
|
+
return {
|
|
1765
|
+
routes,
|
|
1766
|
+
paymentMethod: "provider_key",
|
|
1767
|
+
plan: {
|
|
1768
|
+
routes: [],
|
|
1769
|
+
source: "registry_fallback",
|
|
1770
|
+
sourceReason: "manual_provider_config",
|
|
1771
|
+
reason,
|
|
1772
|
+
mode: "fixedSet",
|
|
1773
|
+
scorer: "balanced",
|
|
1774
|
+
candidateCount: providers.length,
|
|
1775
|
+
diagnostics: {
|
|
1776
|
+
registryVisibleCount: providers.length,
|
|
1777
|
+
prewarmCandidateCount: 0,
|
|
1778
|
+
prewarmUsableCount: 0,
|
|
1779
|
+
prewarmMissingSellerIds: [],
|
|
1780
|
+
prewarmBlockedSellerIds: [],
|
|
1781
|
+
prewarmIncompatibleSellerIds: [],
|
|
1782
|
+
sourceCandidateCount: providers.length,
|
|
1783
|
+
blockedOpenCircuitCount: 0,
|
|
1784
|
+
blockedCapacityCount: 0,
|
|
1785
|
+
blockedLocalConcurrencyCount: 0,
|
|
1786
|
+
blockedSellerIds: [],
|
|
1787
|
+
incompatibleCount: 0,
|
|
1788
|
+
incompatibleSellerIds: []
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1396
1794
|
private async selectSellerRoutes(endpoint: string, modelId: string, requestId?: string): Promise<{ routes: SellerRoute[]; plan: SellerRoutePlan; paymentMethod: string }> {
|
|
1397
1795
|
const protocol = this.endpointProtocol(endpoint);
|
|
1398
1796
|
if (!protocol) {
|
|
@@ -1458,6 +1856,7 @@ export class TokenbuddyDaemon {
|
|
|
1458
1856
|
|
|
1459
1857
|
const routes: SellerRoute[] = planned.routes.map((route) => ({
|
|
1460
1858
|
seller: route.seller,
|
|
1859
|
+
transport: "tokenbuddy_seller",
|
|
1461
1860
|
manifest: null,
|
|
1462
1861
|
protocol,
|
|
1463
1862
|
modelId,
|
|
@@ -1713,6 +2112,9 @@ export class TokenbuddyDaemon {
|
|
|
1713
2112
|
status: string;
|
|
1714
2113
|
creditMicros: number;
|
|
1715
2114
|
currency: string;
|
|
2115
|
+
paymentAmount?: string;
|
|
2116
|
+
paymentAmountMinor?: number;
|
|
2117
|
+
paymentCurrency?: string;
|
|
1716
2118
|
durationMs: number;
|
|
1717
2119
|
}): void {
|
|
1718
2120
|
logger.info("purchase.ledger.recorded", "safe purchase ledger recorded", {
|
|
@@ -1724,6 +2126,9 @@ export class TokenbuddyDaemon {
|
|
|
1724
2126
|
ledgerStatus: input.status,
|
|
1725
2127
|
creditMicros: input.creditMicros,
|
|
1726
2128
|
currency: input.currency,
|
|
2129
|
+
paymentAmount: input.paymentAmount,
|
|
2130
|
+
paymentAmountMinor: input.paymentAmountMinor,
|
|
2131
|
+
paymentCurrency: input.paymentCurrency,
|
|
1727
2132
|
durationMs: input.durationMs
|
|
1728
2133
|
});
|
|
1729
2134
|
}
|
|
@@ -1752,6 +2157,32 @@ export class TokenbuddyDaemon {
|
|
|
1752
2157
|
models: ModelCatalogEntry[];
|
|
1753
2158
|
sellers: SellerCatalogEntry[];
|
|
1754
2159
|
}> {
|
|
2160
|
+
const manualConfig = this.currentManualProviders();
|
|
2161
|
+
if (this.currentProviderMode().mode === "manual" && manualConfig.providers.some((provider) => provider.enabled)) {
|
|
2162
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
2163
|
+
const providers = manualConfig.providers.filter((provider) => provider.enabled);
|
|
2164
|
+
return {
|
|
2165
|
+
models: providers.flatMap((provider) => provider.models.map((modelId) => ({
|
|
2166
|
+
id: modelId,
|
|
2167
|
+
sellerId: provider.id,
|
|
2168
|
+
sellerName: provider.name,
|
|
2169
|
+
sellerUrl: provider.baseUrl,
|
|
2170
|
+
supportedProtocols: provider.supportedProtocols,
|
|
2171
|
+
paymentMethods: ["provider_key"]
|
|
2172
|
+
}))),
|
|
2173
|
+
sellers: providers.map((provider) => {
|
|
2174
|
+
const observation = observations.get(provider.id);
|
|
2175
|
+
return {
|
|
2176
|
+
id: provider.id,
|
|
2177
|
+
name: provider.name,
|
|
2178
|
+
url: provider.baseUrl,
|
|
2179
|
+
status: observation?.status ?? "active",
|
|
2180
|
+
ttftMs: observation?.ttftMs,
|
|
2181
|
+
avgTokensPerSecond: observation?.avgTokensPerSecond
|
|
2182
|
+
};
|
|
2183
|
+
})
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
1755
2186
|
try {
|
|
1756
2187
|
const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
|
|
1757
2188
|
this.lastRegistrySnapshot = catalog.registry ?? sellerCatalogResultToRegistrySnapshot(catalog);
|
|
@@ -1786,7 +2217,7 @@ export class TokenbuddyDaemon {
|
|
|
1786
2217
|
return {
|
|
1787
2218
|
...seller,
|
|
1788
2219
|
ttftMs: runtime?.ttftMs ?? seller.ttftMs,
|
|
1789
|
-
avgTokensPerSecond: runtime?.avgTokensPerSecond ?? seller.avgTokensPerSecond
|
|
2220
|
+
avgTokensPerSecond: runtime?.avgTokensPerSecond ?? seller.avgTokensPerSecond
|
|
1790
2221
|
};
|
|
1791
2222
|
});
|
|
1792
2223
|
}
|
|
@@ -1824,25 +2255,28 @@ export class TokenbuddyDaemon {
|
|
|
1824
2255
|
const fallback: UsageSummary = {
|
|
1825
2256
|
promptTokens: 0,
|
|
1826
2257
|
completionTokens: 0,
|
|
2258
|
+
cacheReadTokens: 0,
|
|
1827
2259
|
billedMicros: 0
|
|
1828
2260
|
};
|
|
1829
2261
|
if (!bodyText.trim()) {
|
|
1830
2262
|
return fallback;
|
|
1831
2263
|
}
|
|
1832
2264
|
try {
|
|
1833
|
-
const data = JSON.parse(bodyText)
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2265
|
+
const data = usageRecord(JSON.parse(bodyText));
|
|
2266
|
+
const usage = usageRecord(data?.usage) ?? usageRecord(usageRecord(data?.response)?.usage);
|
|
2267
|
+
const promptDetails = usageRecord(usage?.prompt_tokens_details);
|
|
2268
|
+
const inputDetails = usageRecord(usage?.input_tokens_details);
|
|
2269
|
+
const promptTokens = nonNegativeIntegerField(usage?.prompt_tokens) ?? nonNegativeIntegerField(usage?.input_tokens) ?? 0;
|
|
2270
|
+
const completionTokens = nonNegativeIntegerField(usage?.completion_tokens) ?? nonNegativeIntegerField(usage?.output_tokens) ?? 0;
|
|
2271
|
+
const cacheReadTokens = nonNegativeIntegerField(promptDetails?.cached_tokens)
|
|
2272
|
+
?? nonNegativeIntegerField(inputDetails?.cached_tokens)
|
|
2273
|
+
?? nonNegativeIntegerField(usage?.cache_read_input_tokens)
|
|
2274
|
+
?? nonNegativeIntegerField(usage?.cache_read_tokens)
|
|
2275
|
+
?? 0;
|
|
1843
2276
|
return {
|
|
1844
2277
|
promptTokens,
|
|
1845
2278
|
completionTokens,
|
|
2279
|
+
cacheReadTokens,
|
|
1846
2280
|
billedMicros: (promptTokens + completionTokens) * 4
|
|
1847
2281
|
};
|
|
1848
2282
|
} catch {
|
|
@@ -1891,6 +2325,7 @@ export class TokenbuddyDaemon {
|
|
|
1891
2325
|
const sellerRequestId = settlement?.requestId && settlement.requestId !== requestId
|
|
1892
2326
|
? settlement.requestId
|
|
1893
2327
|
: undefined;
|
|
2328
|
+
const billingBreakdown = settlement?.billingBreakdown;
|
|
1894
2329
|
this.tokenStore.recordInferenceLedger({
|
|
1895
2330
|
requestId,
|
|
1896
2331
|
sellerKey: route.seller.id,
|
|
@@ -1899,11 +2334,21 @@ export class TokenbuddyDaemon {
|
|
|
1899
2334
|
status: settlement ? "settled" : "estimated",
|
|
1900
2335
|
promptTokens: usage.promptTokens,
|
|
1901
2336
|
completionTokens: usage.completionTokens,
|
|
2337
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
1902
2338
|
billedMicros: settledMicros ?? usage.billedMicros,
|
|
1903
2339
|
estimatedMicros: usage.billedMicros,
|
|
1904
2340
|
settledMicros,
|
|
1905
2341
|
settledUsdMicros: settlement?.settledUsdMicros,
|
|
1906
2342
|
priceVersion: settlement?.priceVersion,
|
|
2343
|
+
inputPriceMicrosPer1m: billingBreakdown?.inputPriceMicrosPer1m,
|
|
2344
|
+
outputPriceMicrosPer1m: billingBreakdown?.outputPriceMicrosPer1m,
|
|
2345
|
+
cacheReadPriceMicrosPer1m: billingBreakdown?.cacheReadPriceMicrosPer1m,
|
|
2346
|
+
inputCostMicros: billingBreakdown?.inputCostMicros,
|
|
2347
|
+
outputCostMicros: billingBreakdown?.outputCostMicros,
|
|
2348
|
+
cacheReadCostMicros: billingBreakdown?.cacheReadCostMicros,
|
|
2349
|
+
originalUsdMicros: billingBreakdown?.originalUsdMicros,
|
|
2350
|
+
billingMultiplier: billingBreakdown?.billingMultiplier,
|
|
2351
|
+
serviceTier: billingBreakdown?.serviceTier,
|
|
1907
2352
|
balanceSnapshotMicros: settlement?.remainingCreditMicros,
|
|
1908
2353
|
balanceSource: settlement ? "seller_authoritative" : "estimated",
|
|
1909
2354
|
prompt,
|
|
@@ -1928,6 +2373,7 @@ export class TokenbuddyDaemon {
|
|
|
1928
2373
|
billedMicros: settledMicros ?? usage.billedMicros,
|
|
1929
2374
|
promptTokens: usage.promptTokens,
|
|
1930
2375
|
completionTokens: usage.completionTokens,
|
|
2376
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
1931
2377
|
balanceSnapshotMicros: settlement?.remainingCreditMicros,
|
|
1932
2378
|
balanceSource: settlement ? "seller_authoritative" : "estimated",
|
|
1933
2379
|
sellerRequestId,
|
|
@@ -2240,6 +2686,7 @@ export class TokenbuddyDaemon {
|
|
|
2240
2686
|
const currency = completeData.currency || createData.currency || "USD";
|
|
2241
2687
|
const expiresAt = new Date(Date.now() + 86400 * 1000).toISOString();
|
|
2242
2688
|
const ledgerStatus = completeData.status || "funded";
|
|
2689
|
+
const paymentSummary = purchasePaymentSummaryFromQuote(completeData.quote ?? createData.quote);
|
|
2243
2690
|
|
|
2244
2691
|
this.tokenStore.saveToken(sellerKey, token, tokenClass, creditMicros, expiresAt);
|
|
2245
2692
|
this.tokenStore.recordPurchaseLedger({
|
|
@@ -2250,6 +2697,7 @@ export class TokenbuddyDaemon {
|
|
|
2250
2697
|
status: ledgerStatus,
|
|
2251
2698
|
creditMicros,
|
|
2252
2699
|
currency,
|
|
2700
|
+
...paymentSummary,
|
|
2253
2701
|
paymentReference: completeData.paymentReference || completeData.payment_reference,
|
|
2254
2702
|
completedAt: new Date().toISOString()
|
|
2255
2703
|
});
|
|
@@ -2262,6 +2710,7 @@ export class TokenbuddyDaemon {
|
|
|
2262
2710
|
status: ledgerStatus,
|
|
2263
2711
|
creditMicros,
|
|
2264
2712
|
currency,
|
|
2713
|
+
...paymentSummary,
|
|
2265
2714
|
durationMs: Date.now() - startedAt
|
|
2266
2715
|
});
|
|
2267
2716
|
// v1.1: feed the credit tracker so the route-failover controller
|
|
@@ -2276,6 +2725,7 @@ export class TokenbuddyDaemon {
|
|
|
2276
2725
|
tokenClass,
|
|
2277
2726
|
creditMicros,
|
|
2278
2727
|
currency,
|
|
2728
|
+
...paymentSummary,
|
|
2279
2729
|
ledgerStatus,
|
|
2280
2730
|
completeStatus: completeRes.status,
|
|
2281
2731
|
durationMs: Date.now() - startedAt
|
|
@@ -2436,6 +2886,366 @@ export class TokenbuddyDaemon {
|
|
|
2436
2886
|
});
|
|
2437
2887
|
}
|
|
2438
2888
|
|
|
2889
|
+
private manualProviderApiKey(provider: ManualProviderConfig): string {
|
|
2890
|
+
if (provider.apiKeyEnv) {
|
|
2891
|
+
const value = process.env[provider.apiKeyEnv];
|
|
2892
|
+
if (!value) {
|
|
2893
|
+
throw new Error(`manual provider key env is not configured: ${provider.apiKeyEnv}`);
|
|
2894
|
+
}
|
|
2895
|
+
return value;
|
|
2896
|
+
}
|
|
2897
|
+
if (provider.secretRef) {
|
|
2898
|
+
const secret = this.currentManualProviderSecrets().secrets.find((entry) => entry.secretRef === provider.secretRef);
|
|
2899
|
+
if (!secret) {
|
|
2900
|
+
throw new Error(`manual provider secret is not configured: ${provider.secretRef}`);
|
|
2901
|
+
}
|
|
2902
|
+
return secret.apiKey;
|
|
2903
|
+
}
|
|
2904
|
+
throw new Error(`manual provider key reference is not configured: ${provider.id}`);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
private manualProviderEndpointUrl(provider: ManualProviderConfig, endpoint: string): string {
|
|
2908
|
+
const baseUrl = provider.baseUrl.replace(/\/+$/, "");
|
|
2909
|
+
if (baseUrl.endsWith("/v1") && endpoint.startsWith("/v1/")) {
|
|
2910
|
+
return `${baseUrl}${endpoint.slice(3)}`;
|
|
2911
|
+
}
|
|
2912
|
+
return `${baseUrl}${endpoint}`;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
private manualProviderEndpointUrlFromBase(baseUrl: string, endpoint: string): string {
|
|
2916
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
2917
|
+
if (normalized.endsWith("/v1") && endpoint.startsWith("/v1/")) {
|
|
2918
|
+
return `${normalized}${endpoint.slice(3)}`;
|
|
2919
|
+
}
|
|
2920
|
+
return `${normalized}${endpoint}`;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
private async probeManualProviderModels(input: {
|
|
2924
|
+
baseUrl: string;
|
|
2925
|
+
apiKey: string;
|
|
2926
|
+
}): Promise<{ modelIds: string[]; elapsedMs: number }> {
|
|
2927
|
+
let parsed: URL;
|
|
2928
|
+
try {
|
|
2929
|
+
parsed = new URL(input.baseUrl);
|
|
2930
|
+
} catch {
|
|
2931
|
+
throw new Error("manual provider baseUrl must be a valid URL");
|
|
2932
|
+
}
|
|
2933
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2934
|
+
throw new Error("manual provider baseUrl must use http or https");
|
|
2935
|
+
}
|
|
2936
|
+
if (input.apiKey.trim().length < 6) {
|
|
2937
|
+
throw new Error("manual provider api key is too short");
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
const startedAt = Date.now();
|
|
2941
|
+
const ac = new AbortController();
|
|
2942
|
+
const timer = setTimeout(() => ac.abort(new Error("manual provider model probe timeout")), this.config.warmupProbeTimeoutMs ?? 3000);
|
|
2943
|
+
try {
|
|
2944
|
+
const response = await fetch(this.manualProviderEndpointUrlFromBase(parsed.toString(), "/v1/models"), {
|
|
2945
|
+
method: "GET",
|
|
2946
|
+
headers: {
|
|
2947
|
+
"Authorization": `Bearer ${input.apiKey.trim()}`
|
|
2948
|
+
},
|
|
2949
|
+
signal: ac.signal
|
|
2950
|
+
});
|
|
2951
|
+
if (!response.ok) {
|
|
2952
|
+
if (response.status === 401 || response.status === 403) {
|
|
2953
|
+
throw new Error("manual provider authentication failed");
|
|
2954
|
+
}
|
|
2955
|
+
if (response.status === 404) {
|
|
2956
|
+
throw new Error("manual provider /v1/models endpoint was not found");
|
|
2957
|
+
}
|
|
2958
|
+
throw new Error(`manual provider model probe returned HTTP ${response.status}`);
|
|
2959
|
+
}
|
|
2960
|
+
const body = await response.json().catch(() => undefined) as unknown;
|
|
2961
|
+
const modelIds = parseOpenAiModelIds(body);
|
|
2962
|
+
if (modelIds.length === 0) {
|
|
2963
|
+
throw new Error("manual provider model list is empty");
|
|
2964
|
+
}
|
|
2965
|
+
return {
|
|
2966
|
+
modelIds,
|
|
2967
|
+
elapsedMs: Date.now() - startedAt
|
|
2968
|
+
};
|
|
2969
|
+
} finally {
|
|
2970
|
+
clearTimeout(timer);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
private manualProviderErrorClass(status: number): string {
|
|
2975
|
+
if (status === 401 || status === 403) {
|
|
2976
|
+
return "auth_failed";
|
|
2977
|
+
}
|
|
2978
|
+
if (status === 429) {
|
|
2979
|
+
return "rate_limited";
|
|
2980
|
+
}
|
|
2981
|
+
if (status >= 500) {
|
|
2982
|
+
return "upstream_5xx";
|
|
2983
|
+
}
|
|
2984
|
+
if (status === 404) {
|
|
2985
|
+
return "model_or_endpoint_not_found";
|
|
2986
|
+
}
|
|
2987
|
+
return `http_${status}`;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
private shouldFailoverManualProvider(status: number): boolean {
|
|
2991
|
+
return status === 408 || status === 429 || status >= 500;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
private async fetchManualProviderRoute(
|
|
2995
|
+
route: SellerRoute,
|
|
2996
|
+
endpoint: string,
|
|
2997
|
+
requestId: string,
|
|
2998
|
+
idempotencyKey: string,
|
|
2999
|
+
routeIndex: number,
|
|
3000
|
+
attempt: number,
|
|
3001
|
+
body: Record<string, unknown>
|
|
3002
|
+
): Promise<globalThis.Response> {
|
|
3003
|
+
const provider = route.manualProvider;
|
|
3004
|
+
if (!provider) {
|
|
3005
|
+
throw new Error("manual provider route missing provider config");
|
|
3006
|
+
}
|
|
3007
|
+
const deadlineMs = this.requestDeadlineMs();
|
|
3008
|
+
const requestAc = new AbortController();
|
|
3009
|
+
const requestTimer = setTimeout(() => requestAc.abort(new Error("buyer deadline exceeded")), deadlineMs);
|
|
3010
|
+
const attemptContext = sellerAttemptRequestContext(
|
|
3011
|
+
requestId,
|
|
3012
|
+
idempotencyKey,
|
|
3013
|
+
routeIndex,
|
|
3014
|
+
attempt,
|
|
3015
|
+
0
|
|
3016
|
+
);
|
|
3017
|
+
try {
|
|
3018
|
+
return await fetch(this.manualProviderEndpointUrl(provider, endpoint), {
|
|
3019
|
+
method: "POST",
|
|
3020
|
+
headers: {
|
|
3021
|
+
"Content-Type": "application/json",
|
|
3022
|
+
"Authorization": `Bearer ${this.manualProviderApiKey(provider)}`,
|
|
3023
|
+
"X-Request-Id": attemptContext.requestId,
|
|
3024
|
+
"Idempotency-Key": attemptContext.idempotencyKey,
|
|
3025
|
+
"X-TokenBuddy-Deadline-Ms": String(deadlineMs)
|
|
3026
|
+
},
|
|
3027
|
+
body: JSON.stringify({
|
|
3028
|
+
...body,
|
|
3029
|
+
requestId: attemptContext.requestId
|
|
3030
|
+
}),
|
|
3031
|
+
signal: requestAc.signal
|
|
3032
|
+
});
|
|
3033
|
+
} finally {
|
|
3034
|
+
clearTimeout(requestTimer);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
private async forwardManualProviderRoute(input: {
|
|
3039
|
+
route: SellerRoute;
|
|
3040
|
+
endpoint: string;
|
|
3041
|
+
reqBody: Record<string, unknown>;
|
|
3042
|
+
res: Response;
|
|
3043
|
+
requestId: string;
|
|
3044
|
+
idempotencyKey: string;
|
|
3045
|
+
modelId: string;
|
|
3046
|
+
requestedModelId?: string;
|
|
3047
|
+
routeIndex: number;
|
|
3048
|
+
routes: SellerRoute[];
|
|
3049
|
+
plan: SellerRoutePlan;
|
|
3050
|
+
paymentMethod: string;
|
|
3051
|
+
startedAt: number;
|
|
3052
|
+
markFirstByte: () => void;
|
|
3053
|
+
}): Promise<boolean> {
|
|
3054
|
+
const { route, endpoint, reqBody, res, requestId, idempotencyKey, modelId, requestedModelId, routeIndex, routes, plan, paymentMethod, startedAt, markFirstByte } = input;
|
|
3055
|
+
const provider = route.manualProvider;
|
|
3056
|
+
if (!provider) {
|
|
3057
|
+
throw new Error("manual provider route missing provider config");
|
|
3058
|
+
}
|
|
3059
|
+
const sellerKey = route.seller.id;
|
|
3060
|
+
const attemptStartedAt = Date.now();
|
|
3061
|
+
const upstreamBody = this.applyResolvedModelToBody(endpoint, {
|
|
3062
|
+
...reqBody,
|
|
3063
|
+
requestId
|
|
3064
|
+
}, modelId);
|
|
3065
|
+
|
|
3066
|
+
logger.info("manual_provider.request.started", "manual provider request started", {
|
|
3067
|
+
requestId,
|
|
3068
|
+
providerId: provider.id,
|
|
3069
|
+
sellerKey,
|
|
3070
|
+
model: modelId,
|
|
3071
|
+
requestedModel: requestedModelId,
|
|
3072
|
+
endpoint,
|
|
3073
|
+
stream: Boolean(reqBody.stream),
|
|
3074
|
+
routeIndex,
|
|
3075
|
+
backup: routeIndex > 0,
|
|
3076
|
+
routePlanReason: route.planReason,
|
|
3077
|
+
bodySummary: summarizeProxyBody(upstreamBody)
|
|
3078
|
+
});
|
|
3079
|
+
|
|
3080
|
+
let response: globalThis.Response;
|
|
3081
|
+
try {
|
|
3082
|
+
response = await this.fetchManualProviderRoute(route, endpoint, requestId, idempotencyKey, routeIndex, 0, upstreamBody);
|
|
3083
|
+
} catch (error: unknown) {
|
|
3084
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3085
|
+
this.recordManualProviderObservation({
|
|
3086
|
+
providerId: provider.id,
|
|
3087
|
+
current: false,
|
|
3088
|
+
status: "unhealthy",
|
|
3089
|
+
errorClass: "deadline",
|
|
3090
|
+
errorMessage
|
|
3091
|
+
});
|
|
3092
|
+
logger.warn("manual_provider.request.failed", "manual provider request failed before response", {
|
|
3093
|
+
requestId,
|
|
3094
|
+
providerId: provider.id,
|
|
3095
|
+
model: modelId,
|
|
3096
|
+
endpoint,
|
|
3097
|
+
errorMessage,
|
|
3098
|
+
durationMs: Date.now() - attemptStartedAt
|
|
3099
|
+
});
|
|
3100
|
+
if (routeIndex + 1 < routes.length) {
|
|
3101
|
+
return false;
|
|
3102
|
+
}
|
|
3103
|
+
throw error;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (!response.ok) {
|
|
3107
|
+
const errorBody = await response.text();
|
|
3108
|
+
const errorClass = this.manualProviderErrorClass(response.status);
|
|
3109
|
+
const canFailover = this.shouldFailoverManualProvider(response.status) && routeIndex + 1 < routes.length;
|
|
3110
|
+
this.recordManualProviderObservation({
|
|
3111
|
+
providerId: provider.id,
|
|
3112
|
+
current: false,
|
|
3113
|
+
status: this.shouldFailoverManualProvider(response.status) ? "degraded" : "unhealthy",
|
|
3114
|
+
errorClass,
|
|
3115
|
+
errorMessage: errorBody.slice(0, 512)
|
|
3116
|
+
});
|
|
3117
|
+
logger.warn("manual_provider.upstream_fetch.failed", "manual provider upstream fetch returned non-ok status", {
|
|
3118
|
+
requestId,
|
|
3119
|
+
providerId: provider.id,
|
|
3120
|
+
model: modelId,
|
|
3121
|
+
endpoint,
|
|
3122
|
+
status: response.status,
|
|
3123
|
+
errorClass,
|
|
3124
|
+
failover: canFailover,
|
|
3125
|
+
durationMs: Date.now() - attemptStartedAt
|
|
3126
|
+
});
|
|
3127
|
+
if (canFailover) {
|
|
3128
|
+
return false;
|
|
3129
|
+
}
|
|
3130
|
+
this.copyUpstreamHeaders(response, res);
|
|
3131
|
+
res.status(response.status);
|
|
3132
|
+
res.send(errorBody);
|
|
3133
|
+
return true;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
this.copyUpstreamHeaders(response, res);
|
|
3137
|
+
res.status(response.status);
|
|
3138
|
+
logger.info("manual_provider.upstream_fetch.succeeded", "manual provider upstream fetch succeeded", {
|
|
3139
|
+
requestId,
|
|
3140
|
+
providerId: provider.id,
|
|
3141
|
+
model: modelId,
|
|
3142
|
+
endpoint,
|
|
3143
|
+
status: response.status,
|
|
3144
|
+
stream: Boolean(reqBody.stream)
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
const contentType = response.headers.get("content-type") || "";
|
|
3148
|
+
if (contentType.includes("text/event-stream") || Boolean(reqBody.stream)) {
|
|
3149
|
+
const reader = response.body?.getReader();
|
|
3150
|
+
if (!reader) {
|
|
3151
|
+
res.end();
|
|
3152
|
+
return true;
|
|
3153
|
+
}
|
|
3154
|
+
let bytes = 0;
|
|
3155
|
+
const decoder = new TextDecoder();
|
|
3156
|
+
while (true) {
|
|
3157
|
+
const { done, value } = await reader.read();
|
|
3158
|
+
if (done) {
|
|
3159
|
+
break;
|
|
3160
|
+
}
|
|
3161
|
+
bytes += value.byteLength;
|
|
3162
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
3163
|
+
if (chunk.length > 0) {
|
|
3164
|
+
markFirstByte();
|
|
3165
|
+
res.write(chunk);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
const decoderTail = decoder.decode();
|
|
3169
|
+
if (decoderTail.length > 0) {
|
|
3170
|
+
markFirstByte();
|
|
3171
|
+
res.write(decoderTail);
|
|
3172
|
+
}
|
|
3173
|
+
res.end();
|
|
3174
|
+
const durationMs = Date.now() - startedAt;
|
|
3175
|
+
const ttftMs = durationMs > 0 ? Date.now() - attemptStartedAt : undefined;
|
|
3176
|
+
this.recordManualProviderObservation({
|
|
3177
|
+
providerId: provider.id,
|
|
3178
|
+
current: true,
|
|
3179
|
+
status: "healthy",
|
|
3180
|
+
ttftMs,
|
|
3181
|
+
avgTokensPerSecond: undefined
|
|
3182
|
+
});
|
|
3183
|
+
this.tokenStore.recordInferenceLedger({
|
|
3184
|
+
requestId,
|
|
3185
|
+
sellerKey,
|
|
3186
|
+
modelId,
|
|
3187
|
+
endpoint,
|
|
3188
|
+
status: "ok",
|
|
3189
|
+
promptTokens: 0,
|
|
3190
|
+
completionTokens: 0,
|
|
3191
|
+
cacheReadTokens: 0,
|
|
3192
|
+
billedMicros: Math.max(1, bytes),
|
|
3193
|
+
estimatedMicros: Math.max(1, bytes),
|
|
3194
|
+
priceVersion: `local-provider:${provider.id}`,
|
|
3195
|
+
balanceSource: "self_funded_provider",
|
|
3196
|
+
prompt: this.inferPromptForHash(reqBody),
|
|
3197
|
+
ttftMs,
|
|
3198
|
+
fallbackCount: routeIndex,
|
|
3199
|
+
routeReason: plan.reason,
|
|
3200
|
+
falloverChain: routes.slice(0, routeIndex + 1).map((entry) => entry.seller.id),
|
|
3201
|
+
upstreamStatus: "healthy",
|
|
3202
|
+
durationMs,
|
|
3203
|
+
paymentMethod
|
|
3204
|
+
});
|
|
3205
|
+
return true;
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
const responseBody = await response.text();
|
|
3209
|
+
markFirstByte();
|
|
3210
|
+
res.send(responseBody);
|
|
3211
|
+
const usage = this.readUsage(responseBody);
|
|
3212
|
+
const durationMs = Date.now() - startedAt;
|
|
3213
|
+
const ttftMs = Date.now() - attemptStartedAt;
|
|
3214
|
+
const completionTokens = usage.completionTokens;
|
|
3215
|
+
const avgTokensPerSecond = durationMs > 0 && completionTokens > 0 ? completionTokens / (durationMs / 1000) : undefined;
|
|
3216
|
+
this.recordManualProviderObservation({
|
|
3217
|
+
providerId: provider.id,
|
|
3218
|
+
current: true,
|
|
3219
|
+
status: "healthy",
|
|
3220
|
+
ttftMs,
|
|
3221
|
+
avgTokensPerSecond
|
|
3222
|
+
});
|
|
3223
|
+
this.tokenStore.recordInferenceLedger({
|
|
3224
|
+
requestId,
|
|
3225
|
+
sellerKey,
|
|
3226
|
+
modelId,
|
|
3227
|
+
endpoint,
|
|
3228
|
+
status: "ok",
|
|
3229
|
+
promptTokens: usage.promptTokens,
|
|
3230
|
+
completionTokens: usage.completionTokens,
|
|
3231
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
3232
|
+
billedMicros: usage.billedMicros,
|
|
3233
|
+
estimatedMicros: usage.billedMicros,
|
|
3234
|
+
priceVersion: `local-provider:${provider.id}`,
|
|
3235
|
+
balanceSource: "self_funded_provider",
|
|
3236
|
+
prompt: this.inferPromptForHash(reqBody),
|
|
3237
|
+
response: responseBody,
|
|
3238
|
+
ttftMs,
|
|
3239
|
+
fallbackCount: routeIndex,
|
|
3240
|
+
routeReason: plan.reason,
|
|
3241
|
+
falloverChain: routes.slice(0, routeIndex + 1).map((entry) => entry.seller.id),
|
|
3242
|
+
upstreamStatus: "healthy",
|
|
3243
|
+
durationMs,
|
|
3244
|
+
paymentMethod
|
|
3245
|
+
});
|
|
3246
|
+
return true;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
2439
3249
|
private async forwardProxyRequest(endpoint: string, req: Request, res: Response): Promise<void> {
|
|
2440
3250
|
const startedAt = Date.now();
|
|
2441
3251
|
let firstByteAt: number | null = null;
|
|
@@ -2454,7 +3264,10 @@ export class TokenbuddyDaemon {
|
|
|
2454
3264
|
}
|
|
2455
3265
|
|
|
2456
3266
|
try {
|
|
2457
|
-
const
|
|
3267
|
+
const routeSelection = this.currentProviderMode().mode === "manual"
|
|
3268
|
+
? this.selectManualProviderRoutes(endpoint, modelId)
|
|
3269
|
+
: await this.selectSellerRoutes(endpoint, modelId, requestId);
|
|
3270
|
+
const { routes, plan, paymentMethod } = routeSelection;
|
|
2458
3271
|
const upstreamStatusFromHeaders = (h: Headers): string | undefined => {
|
|
2459
3272
|
const raw = h.get("x-tokenbuddy-upstream-status") || h.get("x-upstream-status");
|
|
2460
3273
|
if (!raw) return undefined;
|
|
@@ -2465,6 +3278,39 @@ export class TokenbuddyDaemon {
|
|
|
2465
3278
|
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
2466
3279
|
const route = routes[routeIndex];
|
|
2467
3280
|
const sellerKey = route.seller.id;
|
|
3281
|
+
if (route.transport === "manual_provider") {
|
|
3282
|
+
const completed = await this.forwardManualProviderRoute({
|
|
3283
|
+
route,
|
|
3284
|
+
endpoint,
|
|
3285
|
+
reqBody: body as Record<string, unknown>,
|
|
3286
|
+
res,
|
|
3287
|
+
requestId,
|
|
3288
|
+
idempotencyKey,
|
|
3289
|
+
modelId,
|
|
3290
|
+
requestedModelId,
|
|
3291
|
+
routeIndex,
|
|
3292
|
+
routes,
|
|
3293
|
+
plan,
|
|
3294
|
+
paymentMethod,
|
|
3295
|
+
startedAt,
|
|
3296
|
+
markFirstByte
|
|
3297
|
+
});
|
|
3298
|
+
if (completed) {
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
lastError = new Error(`manual provider ${sellerKey} failed over`);
|
|
3302
|
+
logger.warn("manual_provider.route.failover", "manual provider route failed over", {
|
|
3303
|
+
requestId,
|
|
3304
|
+
providerId: sellerKey,
|
|
3305
|
+
model: modelId,
|
|
3306
|
+
endpoint,
|
|
3307
|
+
routeIndex,
|
|
3308
|
+
nextRouteIndex: routeIndex + 1,
|
|
3309
|
+
routesRemaining: routes.length - routeIndex,
|
|
3310
|
+
routesRemainingAfterCurrent: Math.max(0, routes.length - routeIndex - 1)
|
|
3311
|
+
});
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
2468
3314
|
const lease = this.sellerConcurrencyLimiter.tryAcquire(sellerKey, { requestId, modelId, endpoint });
|
|
2469
3315
|
if (!lease) {
|
|
2470
3316
|
const routesRemaining = routes.length - routeIndex;
|
|
@@ -2777,7 +3623,7 @@ export class TokenbuddyDaemon {
|
|
|
2777
3623
|
route,
|
|
2778
3624
|
endpoint,
|
|
2779
3625
|
requestId,
|
|
2780
|
-
{ promptTokens: 0, completionTokens: 0, billedMicros: Math.max(1, bytes) },
|
|
3626
|
+
{ promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, billedMicros: Math.max(1, bytes) },
|
|
2781
3627
|
this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(),
|
|
2782
3628
|
this.inferPromptForHash(body),
|
|
2783
3629
|
undefined,
|
|
@@ -2925,6 +3771,56 @@ export class TokenbuddyDaemon {
|
|
|
2925
3771
|
});
|
|
2926
3772
|
});
|
|
2927
3773
|
|
|
3774
|
+
controlApp.put("/payments/default", (req, res) => {
|
|
3775
|
+
try {
|
|
3776
|
+
const method = typeof req.body?.method === "string" ? req.body.method.trim() : "";
|
|
3777
|
+
const normalizedMethod = normalizePaymentMethodName(method);
|
|
3778
|
+
const payments = this.livePayments();
|
|
3779
|
+
const target = payments.find((payment) => normalizePaymentMethodName(payment.method) === normalizedMethod);
|
|
3780
|
+
if (!normalizedMethod || normalizedMethod === "mock" || !target) {
|
|
3781
|
+
res.status(400).json({
|
|
3782
|
+
error: {
|
|
3783
|
+
code: "payment_default_invalid",
|
|
3784
|
+
message: "payment method is not configured"
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
if (!target.enabled && !readConfigBoolean(target.config, "walletConfigPresent")) {
|
|
3790
|
+
res.status(400).json({
|
|
3791
|
+
error: {
|
|
3792
|
+
code: "payment_default_not_ready",
|
|
3793
|
+
message: "payment method must be bound before it can be selected"
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
for (const payment of payments) {
|
|
3799
|
+
this.tokenStore.savePayment({
|
|
3800
|
+
method: payment.method,
|
|
3801
|
+
enabled: payment.enabled,
|
|
3802
|
+
isDefault: normalizePaymentMethodName(payment.method) === normalizedMethod,
|
|
3803
|
+
config: payment.config
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
logger.info("control.payment.default_selected", "default payment selected", {
|
|
3807
|
+
method: target.method
|
|
3808
|
+
});
|
|
3809
|
+
res.status(200).json({
|
|
3810
|
+
payments: this.livePayments()
|
|
3811
|
+
});
|
|
3812
|
+
} catch (error: unknown) {
|
|
3813
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3814
|
+
logger.warn("control.payment.default_select_failed", "default payment select failed", { errorMessage });
|
|
3815
|
+
res.status(400).json({
|
|
3816
|
+
error: {
|
|
3817
|
+
code: "payment_default_select_failed",
|
|
3818
|
+
message: errorMessage
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
});
|
|
3823
|
+
|
|
2928
3824
|
controlApp.get("/init/state", (req, res) => {
|
|
2929
3825
|
try {
|
|
2930
3826
|
const state = this.initStateSnapshot();
|
|
@@ -3315,6 +4211,453 @@ export class TokenbuddyDaemon {
|
|
|
3315
4211
|
// 所有端点都热生效,不需重启 daemon。改完 store 即下次推理生效。
|
|
3316
4212
|
// ─────────────────────────────────────────────────────────────────
|
|
3317
4213
|
|
|
4214
|
+
controlApp.get("/routing/provider-mode", (req, res) => {
|
|
4215
|
+
try {
|
|
4216
|
+
res.status(200).json(this.providerModePayload());
|
|
4217
|
+
} catch (error: unknown) {
|
|
4218
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4219
|
+
logger.warn("routing.provider_mode.read_failed", "provider mode read failed", { errorMessage });
|
|
4220
|
+
res.status(500).json({ error: { code: "provider_mode_read_failed", message: errorMessage } });
|
|
4221
|
+
}
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
controlApp.put("/routing/provider-mode", (req, res) => {
|
|
4225
|
+
try {
|
|
4226
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
4227
|
+
const requested = normalizeProviderModeConfig({
|
|
4228
|
+
mode: body.mode,
|
|
4229
|
+
updatedAt: new Date().toISOString()
|
|
4230
|
+
});
|
|
4231
|
+
const payment = this.providerPaymentState();
|
|
4232
|
+
if (requested.mode === "auto" && !payment.paymentReady) {
|
|
4233
|
+
res.status(409).json({
|
|
4234
|
+
error: {
|
|
4235
|
+
code: "payment_required",
|
|
4236
|
+
message: "bind a payment method to enable auto provider"
|
|
4237
|
+
},
|
|
4238
|
+
paymentReady: false,
|
|
4239
|
+
paymentRequired: true,
|
|
4240
|
+
bindTarget: "/overview?bind=clawtip"
|
|
4241
|
+
});
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
const saved = this.saveProviderMode(requested.mode);
|
|
4245
|
+
let strategy: BuyerSellerRoutingConfig | undefined;
|
|
4246
|
+
if (saved.mode === "auto") {
|
|
4247
|
+
const nextAuto = {
|
|
4248
|
+
...this.currentAutoProviderConfig(),
|
|
4249
|
+
enabled: true
|
|
4250
|
+
};
|
|
4251
|
+
const shouldRoute = this.autoProviderCanRoute(nextAuto);
|
|
4252
|
+
this.saveAutoProviderConfig(shouldRoute ? nextAuto : { ...nextAuto, enabled: false });
|
|
4253
|
+
if (shouldRoute) {
|
|
4254
|
+
strategy = this.applyAutoProviderRoutingConfig(nextAuto);
|
|
4255
|
+
}
|
|
4256
|
+
} else {
|
|
4257
|
+
this.saveAutoProviderConfig({
|
|
4258
|
+
...this.currentAutoProviderConfig(),
|
|
4259
|
+
enabled: false
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
logger.info("routing.provider_mode.applied", "provider mode applied", {
|
|
4263
|
+
mode: saved.mode,
|
|
4264
|
+
paymentReady: payment.paymentReady
|
|
4265
|
+
});
|
|
4266
|
+
res.status(200).json({
|
|
4267
|
+
applied: true,
|
|
4268
|
+
strategy,
|
|
4269
|
+
...this.providerModePayload()
|
|
4270
|
+
});
|
|
4271
|
+
} catch (error: unknown) {
|
|
4272
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4273
|
+
logger.warn("routing.provider_mode.apply_failed", "provider mode apply failed", { errorMessage });
|
|
4274
|
+
res.status(400).json({ error: { code: "provider_mode_apply_failed", message: errorMessage } });
|
|
4275
|
+
}
|
|
4276
|
+
});
|
|
4277
|
+
|
|
4278
|
+
controlApp.get("/routing/manual-providers", (req, res) => {
|
|
4279
|
+
try {
|
|
4280
|
+
const config = this.currentManualProviders();
|
|
4281
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
4282
|
+
res.status(200).json({
|
|
4283
|
+
version: config.version,
|
|
4284
|
+
updatedAt: config.updatedAt,
|
|
4285
|
+
routing: config.routing,
|
|
4286
|
+
providers: config.providers.map((provider) => publicManualProviderConfig(provider, observations.get(provider.id)))
|
|
4287
|
+
});
|
|
4288
|
+
} catch (error: unknown) {
|
|
4289
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4290
|
+
logger.warn("routing.manual_providers.read_failed", "manual providers read failed", { errorMessage });
|
|
4291
|
+
res.status(500).json({ error: { code: "manual_providers_read_failed", message: errorMessage } });
|
|
4292
|
+
}
|
|
4293
|
+
});
|
|
4294
|
+
|
|
4295
|
+
controlApp.post("/routing/manual-providers/probe", async (req, res) => {
|
|
4296
|
+
try {
|
|
4297
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
4298
|
+
const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
4299
|
+
const apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
4300
|
+
const result = await this.probeManualProviderModels({ baseUrl, apiKey });
|
|
4301
|
+
logger.info("routing.manual_provider.probed", "manual provider model probe succeeded", {
|
|
4302
|
+
modelCount: result.modelIds.length,
|
|
4303
|
+
elapsedMs: result.elapsedMs
|
|
4304
|
+
});
|
|
4305
|
+
res.status(200).json({
|
|
4306
|
+
ok: true,
|
|
4307
|
+
modelIds: result.modelIds,
|
|
4308
|
+
elapsedMs: result.elapsedMs
|
|
4309
|
+
});
|
|
4310
|
+
} catch (error: unknown) {
|
|
4311
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4312
|
+
logger.warn("routing.manual_provider.probe_failed", "manual provider model probe failed", { errorMessage });
|
|
4313
|
+
res.status(400).json({ error: { code: "manual_provider_probe_failed", message: errorMessage } });
|
|
4314
|
+
}
|
|
4315
|
+
});
|
|
4316
|
+
|
|
4317
|
+
controlApp.post("/routing/manual-providers/local", async (req, res) => {
|
|
4318
|
+
try {
|
|
4319
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
4320
|
+
const rawApiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
4321
|
+
if (!rawApiKey) {
|
|
4322
|
+
res.status(400).json({ error: { code: "manual_provider_local_create_failed", message: "manual provider apiKey is required" } });
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
4326
|
+
const probe = await this.probeManualProviderModels({ baseUrl, apiKey: rawApiKey });
|
|
4327
|
+
const config = this.currentManualProviders();
|
|
4328
|
+
const existingIds = new Set(config.providers.map((provider) => provider.id));
|
|
4329
|
+
const rawId = typeof body.id === "string" && body.id.trim().length > 0 ? body.id : undefined;
|
|
4330
|
+
const generatedId = `manual-${crypto.randomUUID().slice(0, 8)}`;
|
|
4331
|
+
const providerId = rawId ?? generatedId;
|
|
4332
|
+
const secretRef = `local:${providerId}`;
|
|
4333
|
+
const providerInput = {
|
|
4334
|
+
...body,
|
|
4335
|
+
apiKey: undefined,
|
|
4336
|
+
apiKeyEnv: undefined,
|
|
4337
|
+
secretRef,
|
|
4338
|
+
models: probe.modelIds,
|
|
4339
|
+
supportedProtocols: Array.isArray(body.supportedProtocols) ? body.supportedProtocols : ["chat_completions"],
|
|
4340
|
+
enabled: body.enabled === undefined ? true : body.enabled
|
|
4341
|
+
};
|
|
4342
|
+
delete (providerInput as Record<string, unknown>).apiKey;
|
|
4343
|
+
const provider = normalizeManualProviderConfig(providerInput, {
|
|
4344
|
+
id: providerId,
|
|
4345
|
+
existingIds
|
|
4346
|
+
});
|
|
4347
|
+
const nextConfig: ManualProvidersConfig = {
|
|
4348
|
+
version: 1,
|
|
4349
|
+
providers: [...config.providers, provider],
|
|
4350
|
+
routing: config.routing,
|
|
4351
|
+
updatedAt: new Date().toISOString()
|
|
4352
|
+
};
|
|
4353
|
+
this.saveManualProviderSecret(secretRef, rawApiKey);
|
|
4354
|
+
this.saveManualProviders(nextConfig);
|
|
4355
|
+
logger.info("routing.manual_provider.local_created", "local manual provider created", {
|
|
4356
|
+
providerId: provider.id,
|
|
4357
|
+
modelCount: provider.models.length,
|
|
4358
|
+
enabled: provider.enabled,
|
|
4359
|
+
keyRefKind: "secret"
|
|
4360
|
+
});
|
|
4361
|
+
res.status(201).json({
|
|
4362
|
+
provider: publicManualProviderConfig(provider),
|
|
4363
|
+
routing: nextConfig.routing,
|
|
4364
|
+
providers: nextConfig.providers.map((entry) => publicManualProviderConfig(entry))
|
|
4365
|
+
});
|
|
4366
|
+
} catch (error: unknown) {
|
|
4367
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4368
|
+
logger.warn("routing.manual_provider.local_create_failed", "local manual provider create failed", { errorMessage });
|
|
4369
|
+
res.status(400).json({ error: { code: "manual_provider_local_create_failed", message: errorMessage } });
|
|
4370
|
+
}
|
|
4371
|
+
});
|
|
4372
|
+
|
|
4373
|
+
controlApp.post("/routing/manual-providers", (req, res) => {
|
|
4374
|
+
try {
|
|
4375
|
+
const config = this.currentManualProviders();
|
|
4376
|
+
const existingIds = new Set(config.providers.map((provider) => provider.id));
|
|
4377
|
+
const rawId = (req.body as Record<string, unknown> | undefined)?.id;
|
|
4378
|
+
const generatedId = `manual-${crypto.randomUUID().slice(0, 8)}`;
|
|
4379
|
+
const provider = normalizeManualProviderConfig(req.body, {
|
|
4380
|
+
id: typeof rawId === "string" && rawId.trim().length > 0 ? rawId : generatedId,
|
|
4381
|
+
existingIds
|
|
4382
|
+
});
|
|
4383
|
+
const nextConfig: ManualProvidersConfig = {
|
|
4384
|
+
version: 1,
|
|
4385
|
+
providers: [...config.providers, provider],
|
|
4386
|
+
routing: config.routing,
|
|
4387
|
+
updatedAt: new Date().toISOString()
|
|
4388
|
+
};
|
|
4389
|
+
this.saveManualProviders(nextConfig);
|
|
4390
|
+
logger.info("routing.manual_provider.created", "manual provider created", {
|
|
4391
|
+
providerId: provider.id,
|
|
4392
|
+
modelCount: provider.models.length,
|
|
4393
|
+
enabled: provider.enabled,
|
|
4394
|
+
keyRefKind: provider.apiKeyEnv ? "env" : "secret"
|
|
4395
|
+
});
|
|
4396
|
+
res.status(201).json({
|
|
4397
|
+
provider: publicManualProviderConfig(provider),
|
|
4398
|
+
routing: nextConfig.routing,
|
|
4399
|
+
providers: nextConfig.providers.map((entry) => publicManualProviderConfig(entry))
|
|
4400
|
+
});
|
|
4401
|
+
} catch (error: unknown) {
|
|
4402
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4403
|
+
logger.warn("routing.manual_provider.create_failed", "manual provider create failed", { errorMessage });
|
|
4404
|
+
res.status(400).json({ error: { code: "manual_provider_create_failed", message: errorMessage } });
|
|
4405
|
+
}
|
|
4406
|
+
});
|
|
4407
|
+
|
|
4408
|
+
controlApp.put("/routing/manual-providers/local/:id", async (req, res) => {
|
|
4409
|
+
try {
|
|
4410
|
+
const providerId = req.params.id;
|
|
4411
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
4412
|
+
const config = this.currentManualProviders();
|
|
4413
|
+
const provider = config.providers.find((entry) => entry.id === providerId);
|
|
4414
|
+
if (!provider) {
|
|
4415
|
+
res.status(404).json({ error: { code: "manual_provider_not_found", message: `manual provider not found: ${providerId}` } });
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
const name = typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : provider.name;
|
|
4419
|
+
const baseUrl = typeof body.baseUrl === "string" && body.baseUrl.trim().length > 0 ? body.baseUrl.trim() : provider.baseUrl;
|
|
4420
|
+
const rawApiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
4421
|
+
const apiKey = rawApiKey || this.manualProviderApiKey(provider);
|
|
4422
|
+
const probe = await this.probeManualProviderModels({ baseUrl, apiKey });
|
|
4423
|
+
const secretRef = provider.secretRef ?? `local:${provider.id}`;
|
|
4424
|
+
const existingIds = new Set(config.providers.map((entry) => entry.id).filter((id) => id !== provider.id));
|
|
4425
|
+
const updatedProvider = normalizeManualProviderConfig({
|
|
4426
|
+
...provider,
|
|
4427
|
+
name,
|
|
4428
|
+
baseUrl,
|
|
4429
|
+
secretRef,
|
|
4430
|
+
models: probe.modelIds,
|
|
4431
|
+
supportedProtocols: provider.supportedProtocols.length > 0 ? provider.supportedProtocols : ["chat_completions"],
|
|
4432
|
+
enabled: body.enabled === undefined ? provider.enabled : body.enabled,
|
|
4433
|
+
updatedAt: new Date().toISOString()
|
|
4434
|
+
}, {
|
|
4435
|
+
id: provider.id,
|
|
4436
|
+
existingIds
|
|
4437
|
+
});
|
|
4438
|
+
const nextConfig: ManualProvidersConfig = {
|
|
4439
|
+
version: 1,
|
|
4440
|
+
providers: config.providers.map((entry) => entry.id === provider.id ? updatedProvider : entry),
|
|
4441
|
+
routing: config.routing,
|
|
4442
|
+
updatedAt: new Date().toISOString()
|
|
4443
|
+
};
|
|
4444
|
+
if (rawApiKey || !provider.secretRef) {
|
|
4445
|
+
this.saveManualProviderSecret(secretRef, apiKey);
|
|
4446
|
+
}
|
|
4447
|
+
this.saveManualProviders(nextConfig);
|
|
4448
|
+
logger.info("routing.manual_provider.local_updated", "local manual provider updated", {
|
|
4449
|
+
providerId: updatedProvider.id,
|
|
4450
|
+
modelCount: updatedProvider.models.length,
|
|
4451
|
+
enabled: updatedProvider.enabled,
|
|
4452
|
+
keyRefKind: "secret"
|
|
4453
|
+
});
|
|
4454
|
+
res.status(200).json({
|
|
4455
|
+
provider: publicManualProviderConfig(updatedProvider),
|
|
4456
|
+
routing: nextConfig.routing,
|
|
4457
|
+
providers: nextConfig.providers.map((entry) => publicManualProviderConfig(entry))
|
|
4458
|
+
});
|
|
4459
|
+
} catch (error: unknown) {
|
|
4460
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4461
|
+
logger.warn("routing.manual_provider.local_update_failed", "local manual provider update failed", { errorMessage });
|
|
4462
|
+
res.status(400).json({ error: { code: "manual_provider_local_update_failed", message: errorMessage } });
|
|
4463
|
+
}
|
|
4464
|
+
});
|
|
4465
|
+
|
|
4466
|
+
controlApp.delete("/routing/manual-providers/:id", (req, res) => {
|
|
4467
|
+
try {
|
|
4468
|
+
const providerId = req.params.id;
|
|
4469
|
+
const config = this.currentManualProviders();
|
|
4470
|
+
const nextProviders = config.providers.filter((provider) => provider.id !== providerId);
|
|
4471
|
+
if (nextProviders.length === config.providers.length) {
|
|
4472
|
+
res.status(404).json({ error: { code: "manual_provider_not_found", message: `manual provider not found: ${providerId}` } });
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4475
|
+
const nextConfig: ManualProvidersConfig = {
|
|
4476
|
+
version: 1,
|
|
4477
|
+
providers: nextProviders,
|
|
4478
|
+
routing: config.routing.policy === "locked" && config.routing.lockedProviderId === providerId
|
|
4479
|
+
? { policy: "fallback" }
|
|
4480
|
+
: config.routing,
|
|
4481
|
+
updatedAt: new Date().toISOString()
|
|
4482
|
+
};
|
|
4483
|
+
const removed = config.providers.find((provider) => provider.id === providerId);
|
|
4484
|
+
this.removeManualProviderSecret(removed?.secretRef);
|
|
4485
|
+
this.saveManualProviders(nextConfig);
|
|
4486
|
+
logger.info("routing.manual_provider.deleted", "manual provider deleted", { providerId });
|
|
4487
|
+
res.status(200).json({
|
|
4488
|
+
deleted: true,
|
|
4489
|
+
providerId,
|
|
4490
|
+
routing: nextConfig.routing,
|
|
4491
|
+
providers: nextProviders.map((provider) => publicManualProviderConfig(provider))
|
|
4492
|
+
});
|
|
4493
|
+
} catch (error: unknown) {
|
|
4494
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4495
|
+
logger.warn("routing.manual_provider.delete_failed", "manual provider delete failed", { errorMessage });
|
|
4496
|
+
res.status(400).json({ error: { code: "manual_provider_delete_failed", message: errorMessage } });
|
|
4497
|
+
}
|
|
4498
|
+
});
|
|
4499
|
+
|
|
4500
|
+
controlApp.put("/routing/manual-providers/order", (req, res) => {
|
|
4501
|
+
try {
|
|
4502
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
4503
|
+
const providerIds = Array.isArray(body.providerIds)
|
|
4504
|
+
? body.providerIds.map((entry) => typeof entry === "string" ? entry.trim() : "")
|
|
4505
|
+
: [];
|
|
4506
|
+
const config = this.currentManualProviders();
|
|
4507
|
+
const currentIds = new Set(config.providers.map((provider) => provider.id));
|
|
4508
|
+
const requestedIds = new Set(providerIds);
|
|
4509
|
+
if (
|
|
4510
|
+
providerIds.length !== config.providers.length ||
|
|
4511
|
+
requestedIds.size !== providerIds.length ||
|
|
4512
|
+
requestedIds.size !== currentIds.size ||
|
|
4513
|
+
providerIds.some((providerId) => !currentIds.has(providerId))
|
|
4514
|
+
) {
|
|
4515
|
+
res.status(400).json({
|
|
4516
|
+
error: {
|
|
4517
|
+
code: "manual_provider_order_invalid",
|
|
4518
|
+
message: "manual provider order must include each configured provider exactly once"
|
|
4519
|
+
}
|
|
4520
|
+
});
|
|
4521
|
+
return;
|
|
4522
|
+
}
|
|
4523
|
+
const providersById = new Map(config.providers.map((provider) => [provider.id, provider]));
|
|
4524
|
+
const nextConfig: ManualProvidersConfig = {
|
|
4525
|
+
version: 1,
|
|
4526
|
+
providers: providerIds.map((providerId) => providersById.get(providerId)).filter((provider): provider is ManualProviderConfig => Boolean(provider)),
|
|
4527
|
+
routing: config.routing,
|
|
4528
|
+
updatedAt: new Date().toISOString()
|
|
4529
|
+
};
|
|
4530
|
+
this.saveManualProviders(nextConfig);
|
|
4531
|
+
logger.info("routing.manual_provider.order_applied", "manual provider order applied", {
|
|
4532
|
+
providerIds
|
|
4533
|
+
});
|
|
4534
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
4535
|
+
res.status(200).json({
|
|
4536
|
+
version: nextConfig.version,
|
|
4537
|
+
updatedAt: nextConfig.updatedAt,
|
|
4538
|
+
routing: nextConfig.routing,
|
|
4539
|
+
providers: nextConfig.providers.map((provider) => publicManualProviderConfig(provider, observations.get(provider.id)))
|
|
4540
|
+
});
|
|
4541
|
+
} catch (error: unknown) {
|
|
4542
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4543
|
+
logger.warn("routing.manual_provider.order_apply_failed", "manual provider order apply failed", { errorMessage });
|
|
4544
|
+
res.status(400).json({ error: { code: "manual_provider_order_apply_failed", message: errorMessage } });
|
|
4545
|
+
}
|
|
4546
|
+
});
|
|
4547
|
+
|
|
4548
|
+
controlApp.put("/routing/manual-providers/routing", (req, res) => {
|
|
4549
|
+
try {
|
|
4550
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
4551
|
+
const policy = body.policy;
|
|
4552
|
+
const lockedProviderId = typeof body.lockedProviderId === "string" ? body.lockedProviderId.trim() : undefined;
|
|
4553
|
+
const config = this.currentManualProviders();
|
|
4554
|
+
const routing = policy === "locked"
|
|
4555
|
+
? { policy: "locked" as const, lockedProviderId }
|
|
4556
|
+
: { policy: "fallback" as const };
|
|
4557
|
+
const normalized = normalizeManualProvidersConfig({
|
|
4558
|
+
...config,
|
|
4559
|
+
routing
|
|
4560
|
+
}).routing;
|
|
4561
|
+
if (normalized.policy === "locked") {
|
|
4562
|
+
const provider = config.providers.find((entry) => entry.id === normalized.lockedProviderId);
|
|
4563
|
+
if (!provider) {
|
|
4564
|
+
res.status(404).json({ error: { code: "manual_provider_not_found", message: `manual provider not found: ${normalized.lockedProviderId}` } });
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
if (!provider.enabled) {
|
|
4568
|
+
res.status(400).json({ error: { code: "manual_provider_locked_disabled", message: `manual provider is disabled: ${provider.id}` } });
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
const nextConfig: ManualProvidersConfig = {
|
|
4573
|
+
version: 1,
|
|
4574
|
+
providers: config.providers,
|
|
4575
|
+
routing: normalized,
|
|
4576
|
+
updatedAt: new Date().toISOString()
|
|
4577
|
+
};
|
|
4578
|
+
this.saveManualProviders(nextConfig);
|
|
4579
|
+
logger.info("routing.manual_provider.routing_applied", "manual provider routing applied", {
|
|
4580
|
+
policy: normalized.policy,
|
|
4581
|
+
lockedProviderId: normalized.lockedProviderId
|
|
4582
|
+
});
|
|
4583
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
4584
|
+
res.status(200).json({
|
|
4585
|
+
version: nextConfig.version,
|
|
4586
|
+
updatedAt: nextConfig.updatedAt,
|
|
4587
|
+
routing: nextConfig.routing,
|
|
4588
|
+
providers: nextConfig.providers.map((provider) => publicManualProviderConfig(provider, observations.get(provider.id)))
|
|
4589
|
+
});
|
|
4590
|
+
} catch (error: unknown) {
|
|
4591
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4592
|
+
logger.warn("routing.manual_provider.routing_apply_failed", "manual provider routing apply failed", { errorMessage });
|
|
4593
|
+
res.status(400).json({ error: { code: "manual_provider_routing_apply_failed", message: errorMessage } });
|
|
4594
|
+
}
|
|
4595
|
+
});
|
|
4596
|
+
|
|
4597
|
+
controlApp.get("/routing/auto-provider", (req, res) => {
|
|
4598
|
+
try {
|
|
4599
|
+
const config = this.currentAutoProviderConfig();
|
|
4600
|
+
const payment = this.providerPaymentState();
|
|
4601
|
+
const mode = this.currentProviderMode();
|
|
4602
|
+
res.status(200).json({
|
|
4603
|
+
config,
|
|
4604
|
+
active: mode.mode === "auto" && this.autoProviderCanRoute(config) && payment.paymentReady,
|
|
4605
|
+
locked: !payment.paymentReady,
|
|
4606
|
+
paymentReady: payment.paymentReady,
|
|
4607
|
+
paymentRequired: !payment.paymentReady,
|
|
4608
|
+
paymentLabel: payment.paymentLabel
|
|
4609
|
+
});
|
|
4610
|
+
} catch (error: unknown) {
|
|
4611
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4612
|
+
logger.warn("routing.auto_provider.read_failed", "auto provider read failed", { errorMessage });
|
|
4613
|
+
res.status(500).json({ error: { code: "auto_provider_read_failed", message: errorMessage } });
|
|
4614
|
+
}
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4617
|
+
controlApp.put("/routing/auto-provider", (req, res) => {
|
|
4618
|
+
try {
|
|
4619
|
+
const next = normalizeAutoProviderConfig(req.body);
|
|
4620
|
+
const payment = this.providerPaymentState();
|
|
4621
|
+
if (next.enabled && !payment.paymentReady) {
|
|
4622
|
+
res.status(409).json({
|
|
4623
|
+
error: {
|
|
4624
|
+
code: "payment_required",
|
|
4625
|
+
message: "bind a payment method to enable auto provider"
|
|
4626
|
+
},
|
|
4627
|
+
paymentReady: false,
|
|
4628
|
+
paymentRequired: true,
|
|
4629
|
+
bindTarget: "/overview?bind=clawtip"
|
|
4630
|
+
});
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
const shouldRoute = this.autoProviderCanRoute(next);
|
|
4634
|
+
this.saveAutoProviderConfig(shouldRoute ? next : { ...next, enabled: false });
|
|
4635
|
+
let strategy: BuyerSellerRoutingConfig | undefined;
|
|
4636
|
+
if (shouldRoute) {
|
|
4637
|
+
this.saveProviderMode("auto");
|
|
4638
|
+
strategy = this.applyAutoProviderRoutingConfig(next);
|
|
4639
|
+
}
|
|
4640
|
+
logger.info("routing.auto_provider.applied", "auto provider applied", {
|
|
4641
|
+
enabled: shouldRoute,
|
|
4642
|
+
range: next.range,
|
|
4643
|
+
scorer: next.scorer,
|
|
4644
|
+
modelCount: next.modelIds.length,
|
|
4645
|
+
sellerCount: next.sellerIds.length,
|
|
4646
|
+
paymentReady: payment.paymentReady
|
|
4647
|
+
});
|
|
4648
|
+
res.status(200).json({
|
|
4649
|
+
applied: true,
|
|
4650
|
+
config: this.currentAutoProviderConfig(),
|
|
4651
|
+
strategy,
|
|
4652
|
+
...this.providerModePayload()
|
|
4653
|
+
});
|
|
4654
|
+
} catch (error: unknown) {
|
|
4655
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4656
|
+
logger.warn("routing.auto_provider.apply_failed", "auto provider apply failed", { errorMessage });
|
|
4657
|
+
res.status(400).json({ error: { code: "auto_provider_apply_failed", message: errorMessage } });
|
|
4658
|
+
}
|
|
4659
|
+
});
|
|
4660
|
+
|
|
3318
4661
|
// 1) GET /routing/strategy — 读当前路由策略 + 来源
|
|
3319
4662
|
controlApp.get("/routing/strategy", (req, res) => {
|
|
3320
4663
|
try {
|
|
@@ -3741,6 +5084,13 @@ function withLiveClawtipWalletState(payment: PaymentConfig, home?: string): Paym
|
|
|
3741
5084
|
};
|
|
3742
5085
|
}
|
|
3743
5086
|
|
|
5087
|
+
function paymentLabel(method: string): string {
|
|
5088
|
+
if (method === "clawtip") {
|
|
5089
|
+
return "ClawTip";
|
|
5090
|
+
}
|
|
5091
|
+
return method;
|
|
5092
|
+
}
|
|
5093
|
+
|
|
3744
5094
|
function normalizeClawtipActivationPayment(bootstrap: ClawtipBootstrapResponse): ClawtipBootstrapPayment {
|
|
3745
5095
|
if (!bootstrap.payment?.orderNo || !bootstrap.payment.indicator || !bootstrap.payment.resourceUrl) {
|
|
3746
5096
|
throw new Error("ClawTip bootstrap response missing orderNo, indicator, or resourceUrl.");
|
|
@@ -3779,6 +5129,10 @@ function readConfigBoolean(config: Record<string, unknown> | undefined, key: str
|
|
|
3779
5129
|
return config?.[key] === true;
|
|
3780
5130
|
}
|
|
3781
5131
|
|
|
5132
|
+
function normalizePaymentMethodName(method: string): string {
|
|
5133
|
+
return method.trim().toLowerCase();
|
|
5134
|
+
}
|
|
5135
|
+
|
|
3782
5136
|
function selectedSellerIdForRouting(routing: BuyerSellerRoutingConfig): string | undefined {
|
|
3783
5137
|
return routing.mode === "fixed" ? routing.sellerId : undefined;
|
|
3784
5138
|
}
|
|
@@ -3848,6 +5202,25 @@ function catalogSnapshotFromRegistry(registry: SellerRegistryDocument): InitDoct
|
|
|
3848
5202
|
};
|
|
3849
5203
|
}
|
|
3850
5204
|
|
|
5205
|
+
function parseOpenAiModelIds(value: unknown): string[] {
|
|
5206
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5207
|
+
return [];
|
|
5208
|
+
}
|
|
5209
|
+
const data = (value as { data?: unknown }).data;
|
|
5210
|
+
if (!Array.isArray(data)) {
|
|
5211
|
+
return [];
|
|
5212
|
+
}
|
|
5213
|
+
return data
|
|
5214
|
+
.flatMap((entry) => {
|
|
5215
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
5216
|
+
return [];
|
|
5217
|
+
}
|
|
5218
|
+
const id = (entry as { id?: unknown }).id;
|
|
5219
|
+
return typeof id === "string" && id.trim().length > 0 ? [id.trim()] : [];
|
|
5220
|
+
})
|
|
5221
|
+
.filter((id, index, all) => all.indexOf(id) === index);
|
|
5222
|
+
}
|
|
5223
|
+
|
|
3851
5224
|
function normalizeTrustedRegistryCache(value: unknown): TrustedRegistryCacheRecord | undefined {
|
|
3852
5225
|
if (!value || typeof value !== "object") {
|
|
3853
5226
|
return undefined;
|