@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/dist/src/daemon.js
CHANGED
|
@@ -22,6 +22,7 @@ import { SellerConcurrencyLimiter } from "./seller-concurrency-limiter.js";
|
|
|
22
22
|
import { SellerMetadataCache } from "./seller-metadata-cache.js";
|
|
23
23
|
import { planSellerRouteSet } from "./seller-route-planner.js";
|
|
24
24
|
import { assertSellerRoutingConfig, mergeSellerRoutingConfig, normalizeSellerRoutingConfig, parseSellerIdList, ROUTING_CONFIG_KEY } from "./seller-routing-config.js";
|
|
25
|
+
import { AUTO_PROVIDER_CONFIG_KEY, MANUAL_PROVIDER_CONFIG_KEY, MANUAL_PROVIDER_OBSERVATIONS_CONFIG_KEY, PROVIDER_MODE_CONFIG_KEY, defaultProviderModeConfig, normalizeAutoProviderConfig, normalizeManualProviderObservationsConfig, normalizeManualProviderConfig, normalizeManualProvidersConfig, normalizeProviderModeConfig, publicManualProviderConfig } from "./provider-routing-config.js";
|
|
25
26
|
import { assertInitSetupSteps, buildCompletedInitSetupMarker, INIT_SETUP_CONFIG_KEY, INIT_SETUP_STEPS, isFreshInitMachine, normalizeInitSetupMarker, resolveInitRecommendedModels, } from "./init-setup.js";
|
|
26
27
|
import { checkPackageUpdate, runPackageUpdate, scheduleLaunchAgentRestart, } from "./package-update.js";
|
|
27
28
|
const logger = createModuleLogger("tb-proxyd");
|
|
@@ -31,6 +32,7 @@ const PROXY_JSON_BODY_LIMIT = "10mb";
|
|
|
31
32
|
const SELLER_CAPACITY_BLOCK_MS = 2_000;
|
|
32
33
|
const CLAWTIP_STATIC_ROUTE = "/static/clawtip";
|
|
33
34
|
const CLAWTIP_RECHARGE_QR_FILE = "recharge.png";
|
|
35
|
+
const MANUAL_PROVIDER_SECRET_CONFIG_KEY = "manual-provider-secrets";
|
|
34
36
|
function currentModuleDir() {
|
|
35
37
|
if (typeof __dirname !== "undefined") {
|
|
36
38
|
return __dirname;
|
|
@@ -113,6 +115,73 @@ function numericHeaderField(value) {
|
|
|
113
115
|
}
|
|
114
116
|
return undefined;
|
|
115
117
|
}
|
|
118
|
+
function nonNegativeIntegerField(value) {
|
|
119
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
120
|
+
}
|
|
121
|
+
function nonNegativeFiniteField(value) {
|
|
122
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
123
|
+
}
|
|
124
|
+
function usageRecord(value) {
|
|
125
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
126
|
+
}
|
|
127
|
+
function safeBillingServiceTier(value) {
|
|
128
|
+
if (typeof value !== "string")
|
|
129
|
+
return undefined;
|
|
130
|
+
const trimmed = value.trim();
|
|
131
|
+
if (trimmed.length === 0 || trimmed.length > 64)
|
|
132
|
+
return undefined;
|
|
133
|
+
return /^[A-Za-z0-9 _.-]+$/.test(trimmed) ? trimmed : undefined;
|
|
134
|
+
}
|
|
135
|
+
function billingBreakdownSummary(value) {
|
|
136
|
+
const data = usageRecord(value);
|
|
137
|
+
if (!data)
|
|
138
|
+
return undefined;
|
|
139
|
+
const inputPriceMicrosPer1m = nonNegativeIntegerField(data.inputPriceMicrosPer1m ?? data.input_price_micros_per_1m);
|
|
140
|
+
const outputPriceMicrosPer1m = nonNegativeIntegerField(data.outputPriceMicrosPer1m ?? data.output_price_micros_per_1m);
|
|
141
|
+
const cacheReadPriceMicrosPer1m = nonNegativeIntegerField(data.cacheReadPriceMicrosPer1m ?? data.cache_read_price_micros_per_1m);
|
|
142
|
+
const inputCostMicros = nonNegativeIntegerField(data.inputCostMicros ?? data.input_cost_micros);
|
|
143
|
+
const outputCostMicros = nonNegativeIntegerField(data.outputCostMicros ?? data.output_cost_micros);
|
|
144
|
+
const cacheReadCostMicros = nonNegativeIntegerField(data.cacheReadCostMicros ?? data.cache_read_cost_micros);
|
|
145
|
+
const originalUsdMicros = nonNegativeIntegerField(data.originalUsdMicros ?? data.original_usd_micros);
|
|
146
|
+
const billingMultiplier = nonNegativeFiniteField(data.billingMultiplier ?? data.billing_multiplier);
|
|
147
|
+
if (inputPriceMicrosPer1m === undefined ||
|
|
148
|
+
outputPriceMicrosPer1m === undefined ||
|
|
149
|
+
cacheReadPriceMicrosPer1m === undefined ||
|
|
150
|
+
inputCostMicros === undefined ||
|
|
151
|
+
outputCostMicros === undefined ||
|
|
152
|
+
cacheReadCostMicros === undefined ||
|
|
153
|
+
originalUsdMicros === undefined ||
|
|
154
|
+
billingMultiplier === undefined) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
inputPriceMicrosPer1m,
|
|
159
|
+
outputPriceMicrosPer1m,
|
|
160
|
+
cacheReadPriceMicrosPer1m,
|
|
161
|
+
inputCostMicros,
|
|
162
|
+
outputCostMicros,
|
|
163
|
+
cacheReadCostMicros,
|
|
164
|
+
originalUsdMicros,
|
|
165
|
+
billingMultiplier,
|
|
166
|
+
serviceTier: safeBillingServiceTier(data.serviceTier ?? data.service_tier)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function purchasePaymentSummaryFromQuote(value) {
|
|
170
|
+
const quote = usageRecord(value);
|
|
171
|
+
if (!quote)
|
|
172
|
+
return {};
|
|
173
|
+
const paymentAmount = typeof quote.paymentAmount === "string" && quote.paymentAmount.trim().length > 0
|
|
174
|
+
? quote.paymentAmount.trim()
|
|
175
|
+
: undefined;
|
|
176
|
+
const paymentCurrency = typeof quote.paymentCurrency === "string" && quote.paymentCurrency.trim().length > 0
|
|
177
|
+
? quote.paymentCurrency.trim().toUpperCase()
|
|
178
|
+
: undefined;
|
|
179
|
+
return {
|
|
180
|
+
paymentAmount,
|
|
181
|
+
paymentAmountMinor: nonNegativeIntegerField(quote.paymentAmountMinor),
|
|
182
|
+
paymentCurrency
|
|
183
|
+
};
|
|
184
|
+
}
|
|
116
185
|
class SellerSettlementStreamExtractor {
|
|
117
186
|
pending = "";
|
|
118
187
|
settlement;
|
|
@@ -180,7 +249,8 @@ function parseSellerSettlementObject(raw) {
|
|
|
180
249
|
? parsed.priceVersion
|
|
181
250
|
: typeof parsed.price_version === "string"
|
|
182
251
|
? parsed.price_version
|
|
183
|
-
: undefined
|
|
252
|
+
: undefined,
|
|
253
|
+
billingBreakdown: billingBreakdownSummary(parsed.billingBreakdown ?? parsed.billing_breakdown)
|
|
184
254
|
};
|
|
185
255
|
}
|
|
186
256
|
catch {
|
|
@@ -230,6 +300,10 @@ function finiteNumber(value) {
|
|
|
230
300
|
}
|
|
231
301
|
return undefined;
|
|
232
302
|
}
|
|
303
|
+
function finitePositiveNumber(value) {
|
|
304
|
+
const number = finiteNumber(value);
|
|
305
|
+
return number !== undefined && number > 0 ? number : undefined;
|
|
306
|
+
}
|
|
233
307
|
function readErrorCode(bodyText) {
|
|
234
308
|
try {
|
|
235
309
|
const parsed = JSON.parse(bodyText);
|
|
@@ -380,7 +454,7 @@ export class TokenbuddyDaemon {
|
|
|
380
454
|
httpStatus: res.status,
|
|
381
455
|
ttftMs: finiteNumber(latency?.ttftMs ?? latency?.ttft_ms),
|
|
382
456
|
avgInferenceMs: finiteNumber(latency?.avgInferenceMs ?? latency?.avg_inference_ms),
|
|
383
|
-
avgTokensPerSecond:
|
|
457
|
+
avgTokensPerSecond: finitePositiveNumber(latency?.avgTokensPerSecond ?? latency?.avg_tokens_per_second),
|
|
384
458
|
upstreamStatus: typeof upstream?.status === "string"
|
|
385
459
|
? upstream.status
|
|
386
460
|
: undefined,
|
|
@@ -747,6 +821,167 @@ export class TokenbuddyDaemon {
|
|
|
747
821
|
livePayments() {
|
|
748
822
|
return this.tokenStore.listPayments().map((payment) => withLiveClawtipWalletState(payment, this.config.clawtipHomeDir));
|
|
749
823
|
}
|
|
824
|
+
providerPaymentState() {
|
|
825
|
+
const payment = this.livePayments().find((entry) => entry.enabled && entry.method !== "mock");
|
|
826
|
+
return {
|
|
827
|
+
paymentReady: Boolean(payment),
|
|
828
|
+
paymentLabel: payment ? paymentLabel(payment.method) : undefined
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
currentProviderMode() {
|
|
832
|
+
return normalizeProviderModeConfig(this.tokenStore.getDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY)?.config);
|
|
833
|
+
}
|
|
834
|
+
saveProviderMode(mode) {
|
|
835
|
+
const config = {
|
|
836
|
+
...defaultProviderModeConfig(),
|
|
837
|
+
mode
|
|
838
|
+
};
|
|
839
|
+
this.tokenStore.saveDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY, config);
|
|
840
|
+
return config;
|
|
841
|
+
}
|
|
842
|
+
currentManualProviders() {
|
|
843
|
+
return normalizeManualProvidersConfig(this.tokenStore.getDaemonRuntimeConfig(MANUAL_PROVIDER_CONFIG_KEY)?.config);
|
|
844
|
+
}
|
|
845
|
+
saveManualProviders(config) {
|
|
846
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_CONFIG_KEY, {
|
|
847
|
+
...config,
|
|
848
|
+
updatedAt: new Date().toISOString()
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
currentManualProviderSecrets() {
|
|
852
|
+
const value = this.tokenStore.getDaemonRuntimeConfig(MANUAL_PROVIDER_SECRET_CONFIG_KEY)?.config;
|
|
853
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
854
|
+
return { version: 1, secrets: [], updatedAt: new Date().toISOString() };
|
|
855
|
+
}
|
|
856
|
+
const record = value;
|
|
857
|
+
const secrets = (Array.isArray(record.secrets) ? record.secrets : [])
|
|
858
|
+
.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
|
|
859
|
+
.flatMap((entry) => {
|
|
860
|
+
const secretRef = typeof entry.secretRef === "string" ? entry.secretRef.trim() : "";
|
|
861
|
+
const apiKey = typeof entry.apiKey === "string" ? entry.apiKey : "";
|
|
862
|
+
if (!secretRef || !apiKey)
|
|
863
|
+
return [];
|
|
864
|
+
return [{
|
|
865
|
+
secretRef,
|
|
866
|
+
apiKey,
|
|
867
|
+
createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date().toISOString(),
|
|
868
|
+
updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : new Date().toISOString()
|
|
869
|
+
}];
|
|
870
|
+
});
|
|
871
|
+
return {
|
|
872
|
+
version: 1,
|
|
873
|
+
secrets,
|
|
874
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date().toISOString()
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
saveManualProviderSecret(secretRef, apiKey) {
|
|
878
|
+
const now = new Date().toISOString();
|
|
879
|
+
const current = this.currentManualProviderSecrets();
|
|
880
|
+
const existing = current.secrets.find((entry) => entry.secretRef === secretRef);
|
|
881
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_SECRET_CONFIG_KEY, {
|
|
882
|
+
version: 1,
|
|
883
|
+
secrets: [
|
|
884
|
+
...current.secrets.filter((entry) => entry.secretRef !== secretRef),
|
|
885
|
+
{
|
|
886
|
+
secretRef,
|
|
887
|
+
apiKey,
|
|
888
|
+
createdAt: existing?.createdAt ?? now,
|
|
889
|
+
updatedAt: now
|
|
890
|
+
}
|
|
891
|
+
],
|
|
892
|
+
updatedAt: now
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
removeManualProviderSecret(secretRef) {
|
|
896
|
+
if (!secretRef)
|
|
897
|
+
return;
|
|
898
|
+
const current = this.currentManualProviderSecrets();
|
|
899
|
+
const secrets = current.secrets.filter((entry) => entry.secretRef !== secretRef);
|
|
900
|
+
if (secrets.length === current.secrets.length)
|
|
901
|
+
return;
|
|
902
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_SECRET_CONFIG_KEY, {
|
|
903
|
+
version: 1,
|
|
904
|
+
secrets,
|
|
905
|
+
updatedAt: new Date().toISOString()
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
currentManualProviderObservations() {
|
|
909
|
+
return normalizeManualProviderObservationsConfig(this.tokenStore.getDaemonRuntimeConfig(MANUAL_PROVIDER_OBSERVATIONS_CONFIG_KEY)?.config);
|
|
910
|
+
}
|
|
911
|
+
recordManualProviderObservation(input) {
|
|
912
|
+
const now = input.lastAccess ?? new Date().toISOString();
|
|
913
|
+
const current = this.currentManualProviderObservations();
|
|
914
|
+
const observations = current.observations
|
|
915
|
+
.filter((entry) => entry.providerId !== input.providerId)
|
|
916
|
+
.map((entry) => ({
|
|
917
|
+
...entry,
|
|
918
|
+
current: input.current ? false : entry.current
|
|
919
|
+
}));
|
|
920
|
+
observations.push({
|
|
921
|
+
providerId: input.providerId,
|
|
922
|
+
current: input.current,
|
|
923
|
+
lastAccess: now,
|
|
924
|
+
status: input.status,
|
|
925
|
+
errorClass: input.errorClass,
|
|
926
|
+
errorMessage: input.errorMessage,
|
|
927
|
+
ttftMs: input.ttftMs,
|
|
928
|
+
avgTokensPerSecond: input.avgTokensPerSecond
|
|
929
|
+
});
|
|
930
|
+
this.tokenStore.saveDaemonRuntimeConfig(MANUAL_PROVIDER_OBSERVATIONS_CONFIG_KEY, {
|
|
931
|
+
version: 1,
|
|
932
|
+
observations,
|
|
933
|
+
updatedAt: now
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
currentAutoProviderConfig() {
|
|
937
|
+
return normalizeAutoProviderConfig(this.tokenStore.getDaemonRuntimeConfig(AUTO_PROVIDER_CONFIG_KEY)?.config);
|
|
938
|
+
}
|
|
939
|
+
saveAutoProviderConfig(config) {
|
|
940
|
+
this.tokenStore.saveDaemonRuntimeConfig(AUTO_PROVIDER_CONFIG_KEY, {
|
|
941
|
+
...config,
|
|
942
|
+
updatedAt: new Date().toISOString()
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
applyAutoProviderRoutingConfig(config) {
|
|
946
|
+
const routing = config.range === "custom"
|
|
947
|
+
? {
|
|
948
|
+
mode: "fixedSet",
|
|
949
|
+
scorer: config.scorer,
|
|
950
|
+
sellerIds: config.sellerIds
|
|
951
|
+
}
|
|
952
|
+
: {
|
|
953
|
+
mode: "fullAuto",
|
|
954
|
+
scorer: config.scorer
|
|
955
|
+
};
|
|
956
|
+
assertSellerRoutingConfig(routing);
|
|
957
|
+
this.tokenStore.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, routing);
|
|
958
|
+
const current = this.refreshSellerRoutingConfig();
|
|
959
|
+
if (config.modelIds.length > 0) {
|
|
960
|
+
this.applyFocusSet(config.modelIds);
|
|
961
|
+
}
|
|
962
|
+
return current;
|
|
963
|
+
}
|
|
964
|
+
autoProviderCanRoute(config) {
|
|
965
|
+
return config.enabled && (config.range !== "custom" || config.sellerIds.length > 0);
|
|
966
|
+
}
|
|
967
|
+
providerModePayload() {
|
|
968
|
+
const mode = this.currentProviderMode();
|
|
969
|
+
const autoProvider = this.currentAutoProviderConfig();
|
|
970
|
+
const payment = this.providerPaymentState();
|
|
971
|
+
return {
|
|
972
|
+
mode: mode.mode,
|
|
973
|
+
updatedAt: mode.updatedAt,
|
|
974
|
+
active: mode.mode,
|
|
975
|
+
manualEnabled: mode.mode === "manual",
|
|
976
|
+
autoEnabled: mode.mode === "auto" && this.autoProviderCanRoute(autoProvider) && payment.paymentReady,
|
|
977
|
+
paymentReady: payment.paymentReady,
|
|
978
|
+
paymentRequired: !payment.paymentReady,
|
|
979
|
+
paymentLabel: payment.paymentLabel,
|
|
980
|
+
locked: {
|
|
981
|
+
auto: !payment.paymentReady
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
750
985
|
clientToolsSummary() {
|
|
751
986
|
const providerStatuses = detectProviders({ home: this.config.providerHomeDir }).map(clientToolStatusFromProvider);
|
|
752
987
|
const clients = [
|
|
@@ -977,7 +1212,7 @@ export class TokenbuddyDaemon {
|
|
|
977
1212
|
this.sellerPool.recordRuntimeMetrics(route.seller.id, {
|
|
978
1213
|
ttftMs: finiteNumber(latency?.ttftMs ?? latency?.ttft_ms),
|
|
979
1214
|
avgInferenceMs: finiteNumber(latency?.avgInferenceMs ?? latency?.avg_inference_ms),
|
|
980
|
-
avgTokensPerSecond:
|
|
1215
|
+
avgTokensPerSecond: finitePositiveNumber(latency?.avgTokensPerSecond ?? latency?.avg_tokens_per_second)
|
|
981
1216
|
});
|
|
982
1217
|
}
|
|
983
1218
|
catch (error) {
|
|
@@ -1070,6 +1305,89 @@ export class TokenbuddyDaemon {
|
|
|
1070
1305
|
const payments = this.tokenStore.listPayments().filter((payment) => payment.enabled);
|
|
1071
1306
|
return payments.find((payment) => payment.isDefault)?.method || payments.find((payment) => payment.method === "mock")?.method;
|
|
1072
1307
|
}
|
|
1308
|
+
selectManualProviderRoutes(endpoint, modelId) {
|
|
1309
|
+
const protocol = this.endpointProtocol(endpoint);
|
|
1310
|
+
if (!protocol) {
|
|
1311
|
+
throw new Error(`unsupported proxy endpoint: ${endpoint}`);
|
|
1312
|
+
}
|
|
1313
|
+
const config = this.currentManualProviders();
|
|
1314
|
+
let providers;
|
|
1315
|
+
let reason;
|
|
1316
|
+
if (config.routing.policy === "locked") {
|
|
1317
|
+
const provider = config.providers.find((entry) => entry.id === config.routing.lockedProviderId);
|
|
1318
|
+
if (!provider) {
|
|
1319
|
+
throw new Error(`locked manual provider is not configured: ${config.routing.lockedProviderId}`);
|
|
1320
|
+
}
|
|
1321
|
+
if (!provider.enabled) {
|
|
1322
|
+
throw new Error(`locked manual provider is disabled: ${provider.id}`);
|
|
1323
|
+
}
|
|
1324
|
+
if (!provider.supportedProtocols.includes(protocol)) {
|
|
1325
|
+
throw new Error(`locked manual provider ${provider.id} does not support ${endpoint}`);
|
|
1326
|
+
}
|
|
1327
|
+
if (!provider.models.includes(modelId)) {
|
|
1328
|
+
throw new Error(`locked manual provider ${provider.id} does not support model ${modelId}`);
|
|
1329
|
+
}
|
|
1330
|
+
providers = [provider];
|
|
1331
|
+
reason = `manual:locked:${provider.id}`;
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
providers = config.providers.filter((provider) => (provider.enabled &&
|
|
1335
|
+
provider.models.includes(modelId) &&
|
|
1336
|
+
provider.supportedProtocols.includes(protocol)));
|
|
1337
|
+
reason = `manual:fallback:routes_${providers.length}`;
|
|
1338
|
+
}
|
|
1339
|
+
if (providers.length === 0) {
|
|
1340
|
+
throw new Error(`no compatible manual provider for ${endpoint} model ${modelId}`);
|
|
1341
|
+
}
|
|
1342
|
+
const routes = providers.map((provider) => ({
|
|
1343
|
+
seller: {
|
|
1344
|
+
id: provider.id,
|
|
1345
|
+
name: provider.name,
|
|
1346
|
+
url: provider.baseUrl,
|
|
1347
|
+
status: "active",
|
|
1348
|
+
supportedProtocols: provider.supportedProtocols,
|
|
1349
|
+
paymentMethods: ["provider_key"],
|
|
1350
|
+
models: provider.models
|
|
1351
|
+
},
|
|
1352
|
+
transport: "manual_provider",
|
|
1353
|
+
manualProvider: provider,
|
|
1354
|
+
manifest: null,
|
|
1355
|
+
protocol,
|
|
1356
|
+
modelId,
|
|
1357
|
+
paymentMethod: "provider_key",
|
|
1358
|
+
planSource: "registry_fallback",
|
|
1359
|
+
planReason: reason,
|
|
1360
|
+
planSellerCount: providers.length
|
|
1361
|
+
}));
|
|
1362
|
+
return {
|
|
1363
|
+
routes,
|
|
1364
|
+
paymentMethod: "provider_key",
|
|
1365
|
+
plan: {
|
|
1366
|
+
routes: [],
|
|
1367
|
+
source: "registry_fallback",
|
|
1368
|
+
sourceReason: "manual_provider_config",
|
|
1369
|
+
reason,
|
|
1370
|
+
mode: "fixedSet",
|
|
1371
|
+
scorer: "balanced",
|
|
1372
|
+
candidateCount: providers.length,
|
|
1373
|
+
diagnostics: {
|
|
1374
|
+
registryVisibleCount: providers.length,
|
|
1375
|
+
prewarmCandidateCount: 0,
|
|
1376
|
+
prewarmUsableCount: 0,
|
|
1377
|
+
prewarmMissingSellerIds: [],
|
|
1378
|
+
prewarmBlockedSellerIds: [],
|
|
1379
|
+
prewarmIncompatibleSellerIds: [],
|
|
1380
|
+
sourceCandidateCount: providers.length,
|
|
1381
|
+
blockedOpenCircuitCount: 0,
|
|
1382
|
+
blockedCapacityCount: 0,
|
|
1383
|
+
blockedLocalConcurrencyCount: 0,
|
|
1384
|
+
blockedSellerIds: [],
|
|
1385
|
+
incompatibleCount: 0,
|
|
1386
|
+
incompatibleSellerIds: []
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1073
1391
|
async selectSellerRoutes(endpoint, modelId, requestId) {
|
|
1074
1392
|
const protocol = this.endpointProtocol(endpoint);
|
|
1075
1393
|
if (!protocol) {
|
|
@@ -1130,6 +1448,7 @@ export class TokenbuddyDaemon {
|
|
|
1130
1448
|
}
|
|
1131
1449
|
const routes = planned.routes.map((route) => ({
|
|
1132
1450
|
seller: route.seller,
|
|
1451
|
+
transport: "tokenbuddy_seller",
|
|
1133
1452
|
manifest: null,
|
|
1134
1453
|
protocol,
|
|
1135
1454
|
modelId,
|
|
@@ -1355,6 +1674,9 @@ export class TokenbuddyDaemon {
|
|
|
1355
1674
|
ledgerStatus: input.status,
|
|
1356
1675
|
creditMicros: input.creditMicros,
|
|
1357
1676
|
currency: input.currency,
|
|
1677
|
+
paymentAmount: input.paymentAmount,
|
|
1678
|
+
paymentAmountMinor: input.paymentAmountMinor,
|
|
1679
|
+
paymentCurrency: input.paymentCurrency,
|
|
1358
1680
|
durationMs: input.durationMs
|
|
1359
1681
|
});
|
|
1360
1682
|
}
|
|
@@ -1374,6 +1696,32 @@ export class TokenbuddyDaemon {
|
|
|
1374
1696
|
});
|
|
1375
1697
|
}
|
|
1376
1698
|
async listSellerBackedModels() {
|
|
1699
|
+
const manualConfig = this.currentManualProviders();
|
|
1700
|
+
if (this.currentProviderMode().mode === "manual" && manualConfig.providers.some((provider) => provider.enabled)) {
|
|
1701
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
1702
|
+
const providers = manualConfig.providers.filter((provider) => provider.enabled);
|
|
1703
|
+
return {
|
|
1704
|
+
models: providers.flatMap((provider) => provider.models.map((modelId) => ({
|
|
1705
|
+
id: modelId,
|
|
1706
|
+
sellerId: provider.id,
|
|
1707
|
+
sellerName: provider.name,
|
|
1708
|
+
sellerUrl: provider.baseUrl,
|
|
1709
|
+
supportedProtocols: provider.supportedProtocols,
|
|
1710
|
+
paymentMethods: ["provider_key"]
|
|
1711
|
+
}))),
|
|
1712
|
+
sellers: providers.map((provider) => {
|
|
1713
|
+
const observation = observations.get(provider.id);
|
|
1714
|
+
return {
|
|
1715
|
+
id: provider.id,
|
|
1716
|
+
name: provider.name,
|
|
1717
|
+
url: provider.baseUrl,
|
|
1718
|
+
status: observation?.status ?? "active",
|
|
1719
|
+
ttftMs: observation?.ttftMs,
|
|
1720
|
+
avgTokensPerSecond: observation?.avgTokensPerSecond
|
|
1721
|
+
};
|
|
1722
|
+
})
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1377
1725
|
try {
|
|
1378
1726
|
const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
|
|
1379
1727
|
this.lastRegistrySnapshot = catalog.registry ?? sellerCatalogResultToRegistrySnapshot(catalog);
|
|
@@ -1408,7 +1756,7 @@ export class TokenbuddyDaemon {
|
|
|
1408
1756
|
return {
|
|
1409
1757
|
...seller,
|
|
1410
1758
|
ttftMs: runtime?.ttftMs ?? seller.ttftMs,
|
|
1411
|
-
avgTokensPerSecond: runtime?.avgTokensPerSecond ?? seller.avgTokensPerSecond
|
|
1759
|
+
avgTokensPerSecond: runtime?.avgTokensPerSecond ?? seller.avgTokensPerSecond
|
|
1412
1760
|
};
|
|
1413
1761
|
});
|
|
1414
1762
|
}
|
|
@@ -1441,18 +1789,28 @@ export class TokenbuddyDaemon {
|
|
|
1441
1789
|
const fallback = {
|
|
1442
1790
|
promptTokens: 0,
|
|
1443
1791
|
completionTokens: 0,
|
|
1792
|
+
cacheReadTokens: 0,
|
|
1444
1793
|
billedMicros: 0
|
|
1445
1794
|
};
|
|
1446
1795
|
if (!bodyText.trim()) {
|
|
1447
1796
|
return fallback;
|
|
1448
1797
|
}
|
|
1449
1798
|
try {
|
|
1450
|
-
const data = JSON.parse(bodyText);
|
|
1451
|
-
const
|
|
1452
|
-
const
|
|
1799
|
+
const data = usageRecord(JSON.parse(bodyText));
|
|
1800
|
+
const usage = usageRecord(data?.usage) ?? usageRecord(usageRecord(data?.response)?.usage);
|
|
1801
|
+
const promptDetails = usageRecord(usage?.prompt_tokens_details);
|
|
1802
|
+
const inputDetails = usageRecord(usage?.input_tokens_details);
|
|
1803
|
+
const promptTokens = nonNegativeIntegerField(usage?.prompt_tokens) ?? nonNegativeIntegerField(usage?.input_tokens) ?? 0;
|
|
1804
|
+
const completionTokens = nonNegativeIntegerField(usage?.completion_tokens) ?? nonNegativeIntegerField(usage?.output_tokens) ?? 0;
|
|
1805
|
+
const cacheReadTokens = nonNegativeIntegerField(promptDetails?.cached_tokens)
|
|
1806
|
+
?? nonNegativeIntegerField(inputDetails?.cached_tokens)
|
|
1807
|
+
?? nonNegativeIntegerField(usage?.cache_read_input_tokens)
|
|
1808
|
+
?? nonNegativeIntegerField(usage?.cache_read_tokens)
|
|
1809
|
+
?? 0;
|
|
1453
1810
|
return {
|
|
1454
1811
|
promptTokens,
|
|
1455
1812
|
completionTokens,
|
|
1813
|
+
cacheReadTokens,
|
|
1456
1814
|
billedMicros: (promptTokens + completionTokens) * 4
|
|
1457
1815
|
};
|
|
1458
1816
|
}
|
|
@@ -1482,6 +1840,7 @@ export class TokenbuddyDaemon {
|
|
|
1482
1840
|
const sellerRequestId = settlement?.requestId && settlement.requestId !== requestId
|
|
1483
1841
|
? settlement.requestId
|
|
1484
1842
|
: undefined;
|
|
1843
|
+
const billingBreakdown = settlement?.billingBreakdown;
|
|
1485
1844
|
this.tokenStore.recordInferenceLedger({
|
|
1486
1845
|
requestId,
|
|
1487
1846
|
sellerKey: route.seller.id,
|
|
@@ -1490,11 +1849,21 @@ export class TokenbuddyDaemon {
|
|
|
1490
1849
|
status: settlement ? "settled" : "estimated",
|
|
1491
1850
|
promptTokens: usage.promptTokens,
|
|
1492
1851
|
completionTokens: usage.completionTokens,
|
|
1852
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
1493
1853
|
billedMicros: settledMicros ?? usage.billedMicros,
|
|
1494
1854
|
estimatedMicros: usage.billedMicros,
|
|
1495
1855
|
settledMicros,
|
|
1496
1856
|
settledUsdMicros: settlement?.settledUsdMicros,
|
|
1497
1857
|
priceVersion: settlement?.priceVersion,
|
|
1858
|
+
inputPriceMicrosPer1m: billingBreakdown?.inputPriceMicrosPer1m,
|
|
1859
|
+
outputPriceMicrosPer1m: billingBreakdown?.outputPriceMicrosPer1m,
|
|
1860
|
+
cacheReadPriceMicrosPer1m: billingBreakdown?.cacheReadPriceMicrosPer1m,
|
|
1861
|
+
inputCostMicros: billingBreakdown?.inputCostMicros,
|
|
1862
|
+
outputCostMicros: billingBreakdown?.outputCostMicros,
|
|
1863
|
+
cacheReadCostMicros: billingBreakdown?.cacheReadCostMicros,
|
|
1864
|
+
originalUsdMicros: billingBreakdown?.originalUsdMicros,
|
|
1865
|
+
billingMultiplier: billingBreakdown?.billingMultiplier,
|
|
1866
|
+
serviceTier: billingBreakdown?.serviceTier,
|
|
1498
1867
|
balanceSnapshotMicros: settlement?.remainingCreditMicros,
|
|
1499
1868
|
balanceSource: settlement ? "seller_authoritative" : "estimated",
|
|
1500
1869
|
prompt,
|
|
@@ -1519,6 +1888,7 @@ export class TokenbuddyDaemon {
|
|
|
1519
1888
|
billedMicros: settledMicros ?? usage.billedMicros,
|
|
1520
1889
|
promptTokens: usage.promptTokens,
|
|
1521
1890
|
completionTokens: usage.completionTokens,
|
|
1891
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
1522
1892
|
balanceSnapshotMicros: settlement?.remainingCreditMicros,
|
|
1523
1893
|
balanceSource: settlement ? "seller_authoritative" : "estimated",
|
|
1524
1894
|
sellerRequestId,
|
|
@@ -1819,6 +2189,7 @@ export class TokenbuddyDaemon {
|
|
|
1819
2189
|
const currency = completeData.currency || createData.currency || "USD";
|
|
1820
2190
|
const expiresAt = new Date(Date.now() + 86400 * 1000).toISOString();
|
|
1821
2191
|
const ledgerStatus = completeData.status || "funded";
|
|
2192
|
+
const paymentSummary = purchasePaymentSummaryFromQuote(completeData.quote ?? createData.quote);
|
|
1822
2193
|
this.tokenStore.saveToken(sellerKey, token, tokenClass, creditMicros, expiresAt);
|
|
1823
2194
|
this.tokenStore.recordPurchaseLedger({
|
|
1824
2195
|
purchaseId,
|
|
@@ -1828,6 +2199,7 @@ export class TokenbuddyDaemon {
|
|
|
1828
2199
|
status: ledgerStatus,
|
|
1829
2200
|
creditMicros,
|
|
1830
2201
|
currency,
|
|
2202
|
+
...paymentSummary,
|
|
1831
2203
|
paymentReference: completeData.paymentReference || completeData.payment_reference,
|
|
1832
2204
|
completedAt: new Date().toISOString()
|
|
1833
2205
|
});
|
|
@@ -1840,6 +2212,7 @@ export class TokenbuddyDaemon {
|
|
|
1840
2212
|
status: ledgerStatus,
|
|
1841
2213
|
creditMicros,
|
|
1842
2214
|
currency,
|
|
2215
|
+
...paymentSummary,
|
|
1843
2216
|
durationMs: Date.now() - startedAt
|
|
1844
2217
|
});
|
|
1845
2218
|
// v1.1: feed the credit tracker so the route-failover controller
|
|
@@ -1854,6 +2227,7 @@ export class TokenbuddyDaemon {
|
|
|
1854
2227
|
tokenClass,
|
|
1855
2228
|
creditMicros,
|
|
1856
2229
|
currency,
|
|
2230
|
+
...paymentSummary,
|
|
1857
2231
|
ledgerStatus,
|
|
1858
2232
|
completeStatus: completeRes.status,
|
|
1859
2233
|
durationMs: Date.now() - startedAt
|
|
@@ -1996,6 +2370,323 @@ export class TokenbuddyDaemon {
|
|
|
1996
2370
|
res.setHeader(key, value);
|
|
1997
2371
|
});
|
|
1998
2372
|
}
|
|
2373
|
+
manualProviderApiKey(provider) {
|
|
2374
|
+
if (provider.apiKeyEnv) {
|
|
2375
|
+
const value = process.env[provider.apiKeyEnv];
|
|
2376
|
+
if (!value) {
|
|
2377
|
+
throw new Error(`manual provider key env is not configured: ${provider.apiKeyEnv}`);
|
|
2378
|
+
}
|
|
2379
|
+
return value;
|
|
2380
|
+
}
|
|
2381
|
+
if (provider.secretRef) {
|
|
2382
|
+
const secret = this.currentManualProviderSecrets().secrets.find((entry) => entry.secretRef === provider.secretRef);
|
|
2383
|
+
if (!secret) {
|
|
2384
|
+
throw new Error(`manual provider secret is not configured: ${provider.secretRef}`);
|
|
2385
|
+
}
|
|
2386
|
+
return secret.apiKey;
|
|
2387
|
+
}
|
|
2388
|
+
throw new Error(`manual provider key reference is not configured: ${provider.id}`);
|
|
2389
|
+
}
|
|
2390
|
+
manualProviderEndpointUrl(provider, endpoint) {
|
|
2391
|
+
const baseUrl = provider.baseUrl.replace(/\/+$/, "");
|
|
2392
|
+
if (baseUrl.endsWith("/v1") && endpoint.startsWith("/v1/")) {
|
|
2393
|
+
return `${baseUrl}${endpoint.slice(3)}`;
|
|
2394
|
+
}
|
|
2395
|
+
return `${baseUrl}${endpoint}`;
|
|
2396
|
+
}
|
|
2397
|
+
manualProviderEndpointUrlFromBase(baseUrl, endpoint) {
|
|
2398
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
2399
|
+
if (normalized.endsWith("/v1") && endpoint.startsWith("/v1/")) {
|
|
2400
|
+
return `${normalized}${endpoint.slice(3)}`;
|
|
2401
|
+
}
|
|
2402
|
+
return `${normalized}${endpoint}`;
|
|
2403
|
+
}
|
|
2404
|
+
async probeManualProviderModels(input) {
|
|
2405
|
+
let parsed;
|
|
2406
|
+
try {
|
|
2407
|
+
parsed = new URL(input.baseUrl);
|
|
2408
|
+
}
|
|
2409
|
+
catch {
|
|
2410
|
+
throw new Error("manual provider baseUrl must be a valid URL");
|
|
2411
|
+
}
|
|
2412
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2413
|
+
throw new Error("manual provider baseUrl must use http or https");
|
|
2414
|
+
}
|
|
2415
|
+
if (input.apiKey.trim().length < 6) {
|
|
2416
|
+
throw new Error("manual provider api key is too short");
|
|
2417
|
+
}
|
|
2418
|
+
const startedAt = Date.now();
|
|
2419
|
+
const ac = new AbortController();
|
|
2420
|
+
const timer = setTimeout(() => ac.abort(new Error("manual provider model probe timeout")), this.config.warmupProbeTimeoutMs ?? 3000);
|
|
2421
|
+
try {
|
|
2422
|
+
const response = await fetch(this.manualProviderEndpointUrlFromBase(parsed.toString(), "/v1/models"), {
|
|
2423
|
+
method: "GET",
|
|
2424
|
+
headers: {
|
|
2425
|
+
"Authorization": `Bearer ${input.apiKey.trim()}`
|
|
2426
|
+
},
|
|
2427
|
+
signal: ac.signal
|
|
2428
|
+
});
|
|
2429
|
+
if (!response.ok) {
|
|
2430
|
+
if (response.status === 401 || response.status === 403) {
|
|
2431
|
+
throw new Error("manual provider authentication failed");
|
|
2432
|
+
}
|
|
2433
|
+
if (response.status === 404) {
|
|
2434
|
+
throw new Error("manual provider /v1/models endpoint was not found");
|
|
2435
|
+
}
|
|
2436
|
+
throw new Error(`manual provider model probe returned HTTP ${response.status}`);
|
|
2437
|
+
}
|
|
2438
|
+
const body = await response.json().catch(() => undefined);
|
|
2439
|
+
const modelIds = parseOpenAiModelIds(body);
|
|
2440
|
+
if (modelIds.length === 0) {
|
|
2441
|
+
throw new Error("manual provider model list is empty");
|
|
2442
|
+
}
|
|
2443
|
+
return {
|
|
2444
|
+
modelIds,
|
|
2445
|
+
elapsedMs: Date.now() - startedAt
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
finally {
|
|
2449
|
+
clearTimeout(timer);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
manualProviderErrorClass(status) {
|
|
2453
|
+
if (status === 401 || status === 403) {
|
|
2454
|
+
return "auth_failed";
|
|
2455
|
+
}
|
|
2456
|
+
if (status === 429) {
|
|
2457
|
+
return "rate_limited";
|
|
2458
|
+
}
|
|
2459
|
+
if (status >= 500) {
|
|
2460
|
+
return "upstream_5xx";
|
|
2461
|
+
}
|
|
2462
|
+
if (status === 404) {
|
|
2463
|
+
return "model_or_endpoint_not_found";
|
|
2464
|
+
}
|
|
2465
|
+
return `http_${status}`;
|
|
2466
|
+
}
|
|
2467
|
+
shouldFailoverManualProvider(status) {
|
|
2468
|
+
return status === 408 || status === 429 || status >= 500;
|
|
2469
|
+
}
|
|
2470
|
+
async fetchManualProviderRoute(route, endpoint, requestId, idempotencyKey, routeIndex, attempt, body) {
|
|
2471
|
+
const provider = route.manualProvider;
|
|
2472
|
+
if (!provider) {
|
|
2473
|
+
throw new Error("manual provider route missing provider config");
|
|
2474
|
+
}
|
|
2475
|
+
const deadlineMs = this.requestDeadlineMs();
|
|
2476
|
+
const requestAc = new AbortController();
|
|
2477
|
+
const requestTimer = setTimeout(() => requestAc.abort(new Error("buyer deadline exceeded")), deadlineMs);
|
|
2478
|
+
const attemptContext = sellerAttemptRequestContext(requestId, idempotencyKey, routeIndex, attempt, 0);
|
|
2479
|
+
try {
|
|
2480
|
+
return await fetch(this.manualProviderEndpointUrl(provider, endpoint), {
|
|
2481
|
+
method: "POST",
|
|
2482
|
+
headers: {
|
|
2483
|
+
"Content-Type": "application/json",
|
|
2484
|
+
"Authorization": `Bearer ${this.manualProviderApiKey(provider)}`,
|
|
2485
|
+
"X-Request-Id": attemptContext.requestId,
|
|
2486
|
+
"Idempotency-Key": attemptContext.idempotencyKey,
|
|
2487
|
+
"X-TokenBuddy-Deadline-Ms": String(deadlineMs)
|
|
2488
|
+
},
|
|
2489
|
+
body: JSON.stringify({
|
|
2490
|
+
...body,
|
|
2491
|
+
requestId: attemptContext.requestId
|
|
2492
|
+
}),
|
|
2493
|
+
signal: requestAc.signal
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
finally {
|
|
2497
|
+
clearTimeout(requestTimer);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
async forwardManualProviderRoute(input) {
|
|
2501
|
+
const { route, endpoint, reqBody, res, requestId, idempotencyKey, modelId, requestedModelId, routeIndex, routes, plan, paymentMethod, startedAt, markFirstByte } = input;
|
|
2502
|
+
const provider = route.manualProvider;
|
|
2503
|
+
if (!provider) {
|
|
2504
|
+
throw new Error("manual provider route missing provider config");
|
|
2505
|
+
}
|
|
2506
|
+
const sellerKey = route.seller.id;
|
|
2507
|
+
const attemptStartedAt = Date.now();
|
|
2508
|
+
const upstreamBody = this.applyResolvedModelToBody(endpoint, {
|
|
2509
|
+
...reqBody,
|
|
2510
|
+
requestId
|
|
2511
|
+
}, modelId);
|
|
2512
|
+
logger.info("manual_provider.request.started", "manual provider request started", {
|
|
2513
|
+
requestId,
|
|
2514
|
+
providerId: provider.id,
|
|
2515
|
+
sellerKey,
|
|
2516
|
+
model: modelId,
|
|
2517
|
+
requestedModel: requestedModelId,
|
|
2518
|
+
endpoint,
|
|
2519
|
+
stream: Boolean(reqBody.stream),
|
|
2520
|
+
routeIndex,
|
|
2521
|
+
backup: routeIndex > 0,
|
|
2522
|
+
routePlanReason: route.planReason,
|
|
2523
|
+
bodySummary: summarizeProxyBody(upstreamBody)
|
|
2524
|
+
});
|
|
2525
|
+
let response;
|
|
2526
|
+
try {
|
|
2527
|
+
response = await this.fetchManualProviderRoute(route, endpoint, requestId, idempotencyKey, routeIndex, 0, upstreamBody);
|
|
2528
|
+
}
|
|
2529
|
+
catch (error) {
|
|
2530
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2531
|
+
this.recordManualProviderObservation({
|
|
2532
|
+
providerId: provider.id,
|
|
2533
|
+
current: false,
|
|
2534
|
+
status: "unhealthy",
|
|
2535
|
+
errorClass: "deadline",
|
|
2536
|
+
errorMessage
|
|
2537
|
+
});
|
|
2538
|
+
logger.warn("manual_provider.request.failed", "manual provider request failed before response", {
|
|
2539
|
+
requestId,
|
|
2540
|
+
providerId: provider.id,
|
|
2541
|
+
model: modelId,
|
|
2542
|
+
endpoint,
|
|
2543
|
+
errorMessage,
|
|
2544
|
+
durationMs: Date.now() - attemptStartedAt
|
|
2545
|
+
});
|
|
2546
|
+
if (routeIndex + 1 < routes.length) {
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
throw error;
|
|
2550
|
+
}
|
|
2551
|
+
if (!response.ok) {
|
|
2552
|
+
const errorBody = await response.text();
|
|
2553
|
+
const errorClass = this.manualProviderErrorClass(response.status);
|
|
2554
|
+
const canFailover = this.shouldFailoverManualProvider(response.status) && routeIndex + 1 < routes.length;
|
|
2555
|
+
this.recordManualProviderObservation({
|
|
2556
|
+
providerId: provider.id,
|
|
2557
|
+
current: false,
|
|
2558
|
+
status: this.shouldFailoverManualProvider(response.status) ? "degraded" : "unhealthy",
|
|
2559
|
+
errorClass,
|
|
2560
|
+
errorMessage: errorBody.slice(0, 512)
|
|
2561
|
+
});
|
|
2562
|
+
logger.warn("manual_provider.upstream_fetch.failed", "manual provider upstream fetch returned non-ok status", {
|
|
2563
|
+
requestId,
|
|
2564
|
+
providerId: provider.id,
|
|
2565
|
+
model: modelId,
|
|
2566
|
+
endpoint,
|
|
2567
|
+
status: response.status,
|
|
2568
|
+
errorClass,
|
|
2569
|
+
failover: canFailover,
|
|
2570
|
+
durationMs: Date.now() - attemptStartedAt
|
|
2571
|
+
});
|
|
2572
|
+
if (canFailover) {
|
|
2573
|
+
return false;
|
|
2574
|
+
}
|
|
2575
|
+
this.copyUpstreamHeaders(response, res);
|
|
2576
|
+
res.status(response.status);
|
|
2577
|
+
res.send(errorBody);
|
|
2578
|
+
return true;
|
|
2579
|
+
}
|
|
2580
|
+
this.copyUpstreamHeaders(response, res);
|
|
2581
|
+
res.status(response.status);
|
|
2582
|
+
logger.info("manual_provider.upstream_fetch.succeeded", "manual provider upstream fetch succeeded", {
|
|
2583
|
+
requestId,
|
|
2584
|
+
providerId: provider.id,
|
|
2585
|
+
model: modelId,
|
|
2586
|
+
endpoint,
|
|
2587
|
+
status: response.status,
|
|
2588
|
+
stream: Boolean(reqBody.stream)
|
|
2589
|
+
});
|
|
2590
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2591
|
+
if (contentType.includes("text/event-stream") || Boolean(reqBody.stream)) {
|
|
2592
|
+
const reader = response.body?.getReader();
|
|
2593
|
+
if (!reader) {
|
|
2594
|
+
res.end();
|
|
2595
|
+
return true;
|
|
2596
|
+
}
|
|
2597
|
+
let bytes = 0;
|
|
2598
|
+
const decoder = new TextDecoder();
|
|
2599
|
+
while (true) {
|
|
2600
|
+
const { done, value } = await reader.read();
|
|
2601
|
+
if (done) {
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
bytes += value.byteLength;
|
|
2605
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2606
|
+
if (chunk.length > 0) {
|
|
2607
|
+
markFirstByte();
|
|
2608
|
+
res.write(chunk);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
const decoderTail = decoder.decode();
|
|
2612
|
+
if (decoderTail.length > 0) {
|
|
2613
|
+
markFirstByte();
|
|
2614
|
+
res.write(decoderTail);
|
|
2615
|
+
}
|
|
2616
|
+
res.end();
|
|
2617
|
+
const durationMs = Date.now() - startedAt;
|
|
2618
|
+
const ttftMs = durationMs > 0 ? Date.now() - attemptStartedAt : undefined;
|
|
2619
|
+
this.recordManualProviderObservation({
|
|
2620
|
+
providerId: provider.id,
|
|
2621
|
+
current: true,
|
|
2622
|
+
status: "healthy",
|
|
2623
|
+
ttftMs,
|
|
2624
|
+
avgTokensPerSecond: undefined
|
|
2625
|
+
});
|
|
2626
|
+
this.tokenStore.recordInferenceLedger({
|
|
2627
|
+
requestId,
|
|
2628
|
+
sellerKey,
|
|
2629
|
+
modelId,
|
|
2630
|
+
endpoint,
|
|
2631
|
+
status: "ok",
|
|
2632
|
+
promptTokens: 0,
|
|
2633
|
+
completionTokens: 0,
|
|
2634
|
+
cacheReadTokens: 0,
|
|
2635
|
+
billedMicros: Math.max(1, bytes),
|
|
2636
|
+
estimatedMicros: Math.max(1, bytes),
|
|
2637
|
+
priceVersion: `local-provider:${provider.id}`,
|
|
2638
|
+
balanceSource: "self_funded_provider",
|
|
2639
|
+
prompt: this.inferPromptForHash(reqBody),
|
|
2640
|
+
ttftMs,
|
|
2641
|
+
fallbackCount: routeIndex,
|
|
2642
|
+
routeReason: plan.reason,
|
|
2643
|
+
falloverChain: routes.slice(0, routeIndex + 1).map((entry) => entry.seller.id),
|
|
2644
|
+
upstreamStatus: "healthy",
|
|
2645
|
+
durationMs,
|
|
2646
|
+
paymentMethod
|
|
2647
|
+
});
|
|
2648
|
+
return true;
|
|
2649
|
+
}
|
|
2650
|
+
const responseBody = await response.text();
|
|
2651
|
+
markFirstByte();
|
|
2652
|
+
res.send(responseBody);
|
|
2653
|
+
const usage = this.readUsage(responseBody);
|
|
2654
|
+
const durationMs = Date.now() - startedAt;
|
|
2655
|
+
const ttftMs = Date.now() - attemptStartedAt;
|
|
2656
|
+
const completionTokens = usage.completionTokens;
|
|
2657
|
+
const avgTokensPerSecond = durationMs > 0 && completionTokens > 0 ? completionTokens / (durationMs / 1000) : undefined;
|
|
2658
|
+
this.recordManualProviderObservation({
|
|
2659
|
+
providerId: provider.id,
|
|
2660
|
+
current: true,
|
|
2661
|
+
status: "healthy",
|
|
2662
|
+
ttftMs,
|
|
2663
|
+
avgTokensPerSecond
|
|
2664
|
+
});
|
|
2665
|
+
this.tokenStore.recordInferenceLedger({
|
|
2666
|
+
requestId,
|
|
2667
|
+
sellerKey,
|
|
2668
|
+
modelId,
|
|
2669
|
+
endpoint,
|
|
2670
|
+
status: "ok",
|
|
2671
|
+
promptTokens: usage.promptTokens,
|
|
2672
|
+
completionTokens: usage.completionTokens,
|
|
2673
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
2674
|
+
billedMicros: usage.billedMicros,
|
|
2675
|
+
estimatedMicros: usage.billedMicros,
|
|
2676
|
+
priceVersion: `local-provider:${provider.id}`,
|
|
2677
|
+
balanceSource: "self_funded_provider",
|
|
2678
|
+
prompt: this.inferPromptForHash(reqBody),
|
|
2679
|
+
response: responseBody,
|
|
2680
|
+
ttftMs,
|
|
2681
|
+
fallbackCount: routeIndex,
|
|
2682
|
+
routeReason: plan.reason,
|
|
2683
|
+
falloverChain: routes.slice(0, routeIndex + 1).map((entry) => entry.seller.id),
|
|
2684
|
+
upstreamStatus: "healthy",
|
|
2685
|
+
durationMs,
|
|
2686
|
+
paymentMethod
|
|
2687
|
+
});
|
|
2688
|
+
return true;
|
|
2689
|
+
}
|
|
1999
2690
|
async forwardProxyRequest(endpoint, req, res) {
|
|
2000
2691
|
const startedAt = Date.now();
|
|
2001
2692
|
let firstByteAt = null;
|
|
@@ -2013,7 +2704,10 @@ export class TokenbuddyDaemon {
|
|
|
2013
2704
|
return;
|
|
2014
2705
|
}
|
|
2015
2706
|
try {
|
|
2016
|
-
const
|
|
2707
|
+
const routeSelection = this.currentProviderMode().mode === "manual"
|
|
2708
|
+
? this.selectManualProviderRoutes(endpoint, modelId)
|
|
2709
|
+
: await this.selectSellerRoutes(endpoint, modelId, requestId);
|
|
2710
|
+
const { routes, plan, paymentMethod } = routeSelection;
|
|
2017
2711
|
const upstreamStatusFromHeaders = (h) => {
|
|
2018
2712
|
const raw = h.get("x-tokenbuddy-upstream-status") || h.get("x-upstream-status");
|
|
2019
2713
|
if (!raw)
|
|
@@ -2024,6 +2718,39 @@ export class TokenbuddyDaemon {
|
|
|
2024
2718
|
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
2025
2719
|
const route = routes[routeIndex];
|
|
2026
2720
|
const sellerKey = route.seller.id;
|
|
2721
|
+
if (route.transport === "manual_provider") {
|
|
2722
|
+
const completed = await this.forwardManualProviderRoute({
|
|
2723
|
+
route,
|
|
2724
|
+
endpoint,
|
|
2725
|
+
reqBody: body,
|
|
2726
|
+
res,
|
|
2727
|
+
requestId,
|
|
2728
|
+
idempotencyKey,
|
|
2729
|
+
modelId,
|
|
2730
|
+
requestedModelId,
|
|
2731
|
+
routeIndex,
|
|
2732
|
+
routes,
|
|
2733
|
+
plan,
|
|
2734
|
+
paymentMethod,
|
|
2735
|
+
startedAt,
|
|
2736
|
+
markFirstByte
|
|
2737
|
+
});
|
|
2738
|
+
if (completed) {
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
lastError = new Error(`manual provider ${sellerKey} failed over`);
|
|
2742
|
+
logger.warn("manual_provider.route.failover", "manual provider route failed over", {
|
|
2743
|
+
requestId,
|
|
2744
|
+
providerId: sellerKey,
|
|
2745
|
+
model: modelId,
|
|
2746
|
+
endpoint,
|
|
2747
|
+
routeIndex,
|
|
2748
|
+
nextRouteIndex: routeIndex + 1,
|
|
2749
|
+
routesRemaining: routes.length - routeIndex,
|
|
2750
|
+
routesRemainingAfterCurrent: Math.max(0, routes.length - routeIndex - 1)
|
|
2751
|
+
});
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2027
2754
|
const lease = this.sellerConcurrencyLimiter.tryAcquire(sellerKey, { requestId, modelId, endpoint });
|
|
2028
2755
|
if (!lease) {
|
|
2029
2756
|
const routesRemaining = routes.length - routeIndex;
|
|
@@ -2319,7 +3046,7 @@ export class TokenbuddyDaemon {
|
|
|
2319
3046
|
}
|
|
2320
3047
|
res.end();
|
|
2321
3048
|
void this.refreshSellerRuntimeMetrics(route, requestId);
|
|
2322
|
-
this.recordReconciledInference(route, endpoint, requestId, { promptTokens: 0, completionTokens: 0, billedMicros: Math.max(1, bytes) }, this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(), this.inferPromptForHash(body), undefined, {
|
|
3049
|
+
this.recordReconciledInference(route, endpoint, requestId, { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, billedMicros: Math.max(1, bytes) }, this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(), this.inferPromptForHash(body), undefined, {
|
|
2323
3050
|
ttftMs: firstByteAt ? firstByteAt - startedAt : undefined,
|
|
2324
3051
|
fallbackCount: routeIndex,
|
|
2325
3052
|
routeReason: plan.reason,
|
|
@@ -2446,6 +3173,56 @@ export class TokenbuddyDaemon {
|
|
|
2446
3173
|
payments: this.livePayments()
|
|
2447
3174
|
});
|
|
2448
3175
|
});
|
|
3176
|
+
controlApp.put("/payments/default", (req, res) => {
|
|
3177
|
+
try {
|
|
3178
|
+
const method = typeof req.body?.method === "string" ? req.body.method.trim() : "";
|
|
3179
|
+
const normalizedMethod = normalizePaymentMethodName(method);
|
|
3180
|
+
const payments = this.livePayments();
|
|
3181
|
+
const target = payments.find((payment) => normalizePaymentMethodName(payment.method) === normalizedMethod);
|
|
3182
|
+
if (!normalizedMethod || normalizedMethod === "mock" || !target) {
|
|
3183
|
+
res.status(400).json({
|
|
3184
|
+
error: {
|
|
3185
|
+
code: "payment_default_invalid",
|
|
3186
|
+
message: "payment method is not configured"
|
|
3187
|
+
}
|
|
3188
|
+
});
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
if (!target.enabled && !readConfigBoolean(target.config, "walletConfigPresent")) {
|
|
3192
|
+
res.status(400).json({
|
|
3193
|
+
error: {
|
|
3194
|
+
code: "payment_default_not_ready",
|
|
3195
|
+
message: "payment method must be bound before it can be selected"
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
for (const payment of payments) {
|
|
3201
|
+
this.tokenStore.savePayment({
|
|
3202
|
+
method: payment.method,
|
|
3203
|
+
enabled: payment.enabled,
|
|
3204
|
+
isDefault: normalizePaymentMethodName(payment.method) === normalizedMethod,
|
|
3205
|
+
config: payment.config
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
logger.info("control.payment.default_selected", "default payment selected", {
|
|
3209
|
+
method: target.method
|
|
3210
|
+
});
|
|
3211
|
+
res.status(200).json({
|
|
3212
|
+
payments: this.livePayments()
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
catch (error) {
|
|
3216
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3217
|
+
logger.warn("control.payment.default_select_failed", "default payment select failed", { errorMessage });
|
|
3218
|
+
res.status(400).json({
|
|
3219
|
+
error: {
|
|
3220
|
+
code: "payment_default_select_failed",
|
|
3221
|
+
message: errorMessage
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
2449
3226
|
controlApp.get("/init/state", (req, res) => {
|
|
2450
3227
|
try {
|
|
2451
3228
|
const state = this.initStateSnapshot();
|
|
@@ -2829,6 +3606,452 @@ export class TokenbuddyDaemon {
|
|
|
2829
3606
|
// tb-ui v1: 控制平面写端点(PR-0)
|
|
2830
3607
|
// 所有端点都热生效,不需重启 daemon。改完 store 即下次推理生效。
|
|
2831
3608
|
// ─────────────────────────────────────────────────────────────────
|
|
3609
|
+
controlApp.get("/routing/provider-mode", (req, res) => {
|
|
3610
|
+
try {
|
|
3611
|
+
res.status(200).json(this.providerModePayload());
|
|
3612
|
+
}
|
|
3613
|
+
catch (error) {
|
|
3614
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3615
|
+
logger.warn("routing.provider_mode.read_failed", "provider mode read failed", { errorMessage });
|
|
3616
|
+
res.status(500).json({ error: { code: "provider_mode_read_failed", message: errorMessage } });
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
controlApp.put("/routing/provider-mode", (req, res) => {
|
|
3620
|
+
try {
|
|
3621
|
+
const body = (req.body ?? {});
|
|
3622
|
+
const requested = normalizeProviderModeConfig({
|
|
3623
|
+
mode: body.mode,
|
|
3624
|
+
updatedAt: new Date().toISOString()
|
|
3625
|
+
});
|
|
3626
|
+
const payment = this.providerPaymentState();
|
|
3627
|
+
if (requested.mode === "auto" && !payment.paymentReady) {
|
|
3628
|
+
res.status(409).json({
|
|
3629
|
+
error: {
|
|
3630
|
+
code: "payment_required",
|
|
3631
|
+
message: "bind a payment method to enable auto provider"
|
|
3632
|
+
},
|
|
3633
|
+
paymentReady: false,
|
|
3634
|
+
paymentRequired: true,
|
|
3635
|
+
bindTarget: "/overview?bind=clawtip"
|
|
3636
|
+
});
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const saved = this.saveProviderMode(requested.mode);
|
|
3640
|
+
let strategy;
|
|
3641
|
+
if (saved.mode === "auto") {
|
|
3642
|
+
const nextAuto = {
|
|
3643
|
+
...this.currentAutoProviderConfig(),
|
|
3644
|
+
enabled: true
|
|
3645
|
+
};
|
|
3646
|
+
const shouldRoute = this.autoProviderCanRoute(nextAuto);
|
|
3647
|
+
this.saveAutoProviderConfig(shouldRoute ? nextAuto : { ...nextAuto, enabled: false });
|
|
3648
|
+
if (shouldRoute) {
|
|
3649
|
+
strategy = this.applyAutoProviderRoutingConfig(nextAuto);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
else {
|
|
3653
|
+
this.saveAutoProviderConfig({
|
|
3654
|
+
...this.currentAutoProviderConfig(),
|
|
3655
|
+
enabled: false
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
logger.info("routing.provider_mode.applied", "provider mode applied", {
|
|
3659
|
+
mode: saved.mode,
|
|
3660
|
+
paymentReady: payment.paymentReady
|
|
3661
|
+
});
|
|
3662
|
+
res.status(200).json({
|
|
3663
|
+
applied: true,
|
|
3664
|
+
strategy,
|
|
3665
|
+
...this.providerModePayload()
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
catch (error) {
|
|
3669
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3670
|
+
logger.warn("routing.provider_mode.apply_failed", "provider mode apply failed", { errorMessage });
|
|
3671
|
+
res.status(400).json({ error: { code: "provider_mode_apply_failed", message: errorMessage } });
|
|
3672
|
+
}
|
|
3673
|
+
});
|
|
3674
|
+
controlApp.get("/routing/manual-providers", (req, res) => {
|
|
3675
|
+
try {
|
|
3676
|
+
const config = this.currentManualProviders();
|
|
3677
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
3678
|
+
res.status(200).json({
|
|
3679
|
+
version: config.version,
|
|
3680
|
+
updatedAt: config.updatedAt,
|
|
3681
|
+
routing: config.routing,
|
|
3682
|
+
providers: config.providers.map((provider) => publicManualProviderConfig(provider, observations.get(provider.id)))
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
catch (error) {
|
|
3686
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3687
|
+
logger.warn("routing.manual_providers.read_failed", "manual providers read failed", { errorMessage });
|
|
3688
|
+
res.status(500).json({ error: { code: "manual_providers_read_failed", message: errorMessage } });
|
|
3689
|
+
}
|
|
3690
|
+
});
|
|
3691
|
+
controlApp.post("/routing/manual-providers/probe", async (req, res) => {
|
|
3692
|
+
try {
|
|
3693
|
+
const body = (req.body ?? {});
|
|
3694
|
+
const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
3695
|
+
const apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
3696
|
+
const result = await this.probeManualProviderModels({ baseUrl, apiKey });
|
|
3697
|
+
logger.info("routing.manual_provider.probed", "manual provider model probe succeeded", {
|
|
3698
|
+
modelCount: result.modelIds.length,
|
|
3699
|
+
elapsedMs: result.elapsedMs
|
|
3700
|
+
});
|
|
3701
|
+
res.status(200).json({
|
|
3702
|
+
ok: true,
|
|
3703
|
+
modelIds: result.modelIds,
|
|
3704
|
+
elapsedMs: result.elapsedMs
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
catch (error) {
|
|
3708
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3709
|
+
logger.warn("routing.manual_provider.probe_failed", "manual provider model probe failed", { errorMessage });
|
|
3710
|
+
res.status(400).json({ error: { code: "manual_provider_probe_failed", message: errorMessage } });
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3713
|
+
controlApp.post("/routing/manual-providers/local", async (req, res) => {
|
|
3714
|
+
try {
|
|
3715
|
+
const body = (req.body ?? {});
|
|
3716
|
+
const rawApiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
3717
|
+
if (!rawApiKey) {
|
|
3718
|
+
res.status(400).json({ error: { code: "manual_provider_local_create_failed", message: "manual provider apiKey is required" } });
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
3722
|
+
const probe = await this.probeManualProviderModels({ baseUrl, apiKey: rawApiKey });
|
|
3723
|
+
const config = this.currentManualProviders();
|
|
3724
|
+
const existingIds = new Set(config.providers.map((provider) => provider.id));
|
|
3725
|
+
const rawId = typeof body.id === "string" && body.id.trim().length > 0 ? body.id : undefined;
|
|
3726
|
+
const generatedId = `manual-${crypto.randomUUID().slice(0, 8)}`;
|
|
3727
|
+
const providerId = rawId ?? generatedId;
|
|
3728
|
+
const secretRef = `local:${providerId}`;
|
|
3729
|
+
const providerInput = {
|
|
3730
|
+
...body,
|
|
3731
|
+
apiKey: undefined,
|
|
3732
|
+
apiKeyEnv: undefined,
|
|
3733
|
+
secretRef,
|
|
3734
|
+
models: probe.modelIds,
|
|
3735
|
+
supportedProtocols: Array.isArray(body.supportedProtocols) ? body.supportedProtocols : ["chat_completions"],
|
|
3736
|
+
enabled: body.enabled === undefined ? true : body.enabled
|
|
3737
|
+
};
|
|
3738
|
+
delete providerInput.apiKey;
|
|
3739
|
+
const provider = normalizeManualProviderConfig(providerInput, {
|
|
3740
|
+
id: providerId,
|
|
3741
|
+
existingIds
|
|
3742
|
+
});
|
|
3743
|
+
const nextConfig = {
|
|
3744
|
+
version: 1,
|
|
3745
|
+
providers: [...config.providers, provider],
|
|
3746
|
+
routing: config.routing,
|
|
3747
|
+
updatedAt: new Date().toISOString()
|
|
3748
|
+
};
|
|
3749
|
+
this.saveManualProviderSecret(secretRef, rawApiKey);
|
|
3750
|
+
this.saveManualProviders(nextConfig);
|
|
3751
|
+
logger.info("routing.manual_provider.local_created", "local manual provider created", {
|
|
3752
|
+
providerId: provider.id,
|
|
3753
|
+
modelCount: provider.models.length,
|
|
3754
|
+
enabled: provider.enabled,
|
|
3755
|
+
keyRefKind: "secret"
|
|
3756
|
+
});
|
|
3757
|
+
res.status(201).json({
|
|
3758
|
+
provider: publicManualProviderConfig(provider),
|
|
3759
|
+
routing: nextConfig.routing,
|
|
3760
|
+
providers: nextConfig.providers.map((entry) => publicManualProviderConfig(entry))
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
catch (error) {
|
|
3764
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3765
|
+
logger.warn("routing.manual_provider.local_create_failed", "local manual provider create failed", { errorMessage });
|
|
3766
|
+
res.status(400).json({ error: { code: "manual_provider_local_create_failed", message: errorMessage } });
|
|
3767
|
+
}
|
|
3768
|
+
});
|
|
3769
|
+
controlApp.post("/routing/manual-providers", (req, res) => {
|
|
3770
|
+
try {
|
|
3771
|
+
const config = this.currentManualProviders();
|
|
3772
|
+
const existingIds = new Set(config.providers.map((provider) => provider.id));
|
|
3773
|
+
const rawId = req.body?.id;
|
|
3774
|
+
const generatedId = `manual-${crypto.randomUUID().slice(0, 8)}`;
|
|
3775
|
+
const provider = normalizeManualProviderConfig(req.body, {
|
|
3776
|
+
id: typeof rawId === "string" && rawId.trim().length > 0 ? rawId : generatedId,
|
|
3777
|
+
existingIds
|
|
3778
|
+
});
|
|
3779
|
+
const nextConfig = {
|
|
3780
|
+
version: 1,
|
|
3781
|
+
providers: [...config.providers, provider],
|
|
3782
|
+
routing: config.routing,
|
|
3783
|
+
updatedAt: new Date().toISOString()
|
|
3784
|
+
};
|
|
3785
|
+
this.saveManualProviders(nextConfig);
|
|
3786
|
+
logger.info("routing.manual_provider.created", "manual provider created", {
|
|
3787
|
+
providerId: provider.id,
|
|
3788
|
+
modelCount: provider.models.length,
|
|
3789
|
+
enabled: provider.enabled,
|
|
3790
|
+
keyRefKind: provider.apiKeyEnv ? "env" : "secret"
|
|
3791
|
+
});
|
|
3792
|
+
res.status(201).json({
|
|
3793
|
+
provider: publicManualProviderConfig(provider),
|
|
3794
|
+
routing: nextConfig.routing,
|
|
3795
|
+
providers: nextConfig.providers.map((entry) => publicManualProviderConfig(entry))
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
3798
|
+
catch (error) {
|
|
3799
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3800
|
+
logger.warn("routing.manual_provider.create_failed", "manual provider create failed", { errorMessage });
|
|
3801
|
+
res.status(400).json({ error: { code: "manual_provider_create_failed", message: errorMessage } });
|
|
3802
|
+
}
|
|
3803
|
+
});
|
|
3804
|
+
controlApp.put("/routing/manual-providers/local/:id", async (req, res) => {
|
|
3805
|
+
try {
|
|
3806
|
+
const providerId = req.params.id;
|
|
3807
|
+
const body = (req.body ?? {});
|
|
3808
|
+
const config = this.currentManualProviders();
|
|
3809
|
+
const provider = config.providers.find((entry) => entry.id === providerId);
|
|
3810
|
+
if (!provider) {
|
|
3811
|
+
res.status(404).json({ error: { code: "manual_provider_not_found", message: `manual provider not found: ${providerId}` } });
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
const name = typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : provider.name;
|
|
3815
|
+
const baseUrl = typeof body.baseUrl === "string" && body.baseUrl.trim().length > 0 ? body.baseUrl.trim() : provider.baseUrl;
|
|
3816
|
+
const rawApiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
3817
|
+
const apiKey = rawApiKey || this.manualProviderApiKey(provider);
|
|
3818
|
+
const probe = await this.probeManualProviderModels({ baseUrl, apiKey });
|
|
3819
|
+
const secretRef = provider.secretRef ?? `local:${provider.id}`;
|
|
3820
|
+
const existingIds = new Set(config.providers.map((entry) => entry.id).filter((id) => id !== provider.id));
|
|
3821
|
+
const updatedProvider = normalizeManualProviderConfig({
|
|
3822
|
+
...provider,
|
|
3823
|
+
name,
|
|
3824
|
+
baseUrl,
|
|
3825
|
+
secretRef,
|
|
3826
|
+
models: probe.modelIds,
|
|
3827
|
+
supportedProtocols: provider.supportedProtocols.length > 0 ? provider.supportedProtocols : ["chat_completions"],
|
|
3828
|
+
enabled: body.enabled === undefined ? provider.enabled : body.enabled,
|
|
3829
|
+
updatedAt: new Date().toISOString()
|
|
3830
|
+
}, {
|
|
3831
|
+
id: provider.id,
|
|
3832
|
+
existingIds
|
|
3833
|
+
});
|
|
3834
|
+
const nextConfig = {
|
|
3835
|
+
version: 1,
|
|
3836
|
+
providers: config.providers.map((entry) => entry.id === provider.id ? updatedProvider : entry),
|
|
3837
|
+
routing: config.routing,
|
|
3838
|
+
updatedAt: new Date().toISOString()
|
|
3839
|
+
};
|
|
3840
|
+
if (rawApiKey || !provider.secretRef) {
|
|
3841
|
+
this.saveManualProviderSecret(secretRef, apiKey);
|
|
3842
|
+
}
|
|
3843
|
+
this.saveManualProviders(nextConfig);
|
|
3844
|
+
logger.info("routing.manual_provider.local_updated", "local manual provider updated", {
|
|
3845
|
+
providerId: updatedProvider.id,
|
|
3846
|
+
modelCount: updatedProvider.models.length,
|
|
3847
|
+
enabled: updatedProvider.enabled,
|
|
3848
|
+
keyRefKind: "secret"
|
|
3849
|
+
});
|
|
3850
|
+
res.status(200).json({
|
|
3851
|
+
provider: publicManualProviderConfig(updatedProvider),
|
|
3852
|
+
routing: nextConfig.routing,
|
|
3853
|
+
providers: nextConfig.providers.map((entry) => publicManualProviderConfig(entry))
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
catch (error) {
|
|
3857
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3858
|
+
logger.warn("routing.manual_provider.local_update_failed", "local manual provider update failed", { errorMessage });
|
|
3859
|
+
res.status(400).json({ error: { code: "manual_provider_local_update_failed", message: errorMessage } });
|
|
3860
|
+
}
|
|
3861
|
+
});
|
|
3862
|
+
controlApp.delete("/routing/manual-providers/:id", (req, res) => {
|
|
3863
|
+
try {
|
|
3864
|
+
const providerId = req.params.id;
|
|
3865
|
+
const config = this.currentManualProviders();
|
|
3866
|
+
const nextProviders = config.providers.filter((provider) => provider.id !== providerId);
|
|
3867
|
+
if (nextProviders.length === config.providers.length) {
|
|
3868
|
+
res.status(404).json({ error: { code: "manual_provider_not_found", message: `manual provider not found: ${providerId}` } });
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
const nextConfig = {
|
|
3872
|
+
version: 1,
|
|
3873
|
+
providers: nextProviders,
|
|
3874
|
+
routing: config.routing.policy === "locked" && config.routing.lockedProviderId === providerId
|
|
3875
|
+
? { policy: "fallback" }
|
|
3876
|
+
: config.routing,
|
|
3877
|
+
updatedAt: new Date().toISOString()
|
|
3878
|
+
};
|
|
3879
|
+
const removed = config.providers.find((provider) => provider.id === providerId);
|
|
3880
|
+
this.removeManualProviderSecret(removed?.secretRef);
|
|
3881
|
+
this.saveManualProviders(nextConfig);
|
|
3882
|
+
logger.info("routing.manual_provider.deleted", "manual provider deleted", { providerId });
|
|
3883
|
+
res.status(200).json({
|
|
3884
|
+
deleted: true,
|
|
3885
|
+
providerId,
|
|
3886
|
+
routing: nextConfig.routing,
|
|
3887
|
+
providers: nextProviders.map((provider) => publicManualProviderConfig(provider))
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
catch (error) {
|
|
3891
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3892
|
+
logger.warn("routing.manual_provider.delete_failed", "manual provider delete failed", { errorMessage });
|
|
3893
|
+
res.status(400).json({ error: { code: "manual_provider_delete_failed", message: errorMessage } });
|
|
3894
|
+
}
|
|
3895
|
+
});
|
|
3896
|
+
controlApp.put("/routing/manual-providers/order", (req, res) => {
|
|
3897
|
+
try {
|
|
3898
|
+
const body = (req.body ?? {});
|
|
3899
|
+
const providerIds = Array.isArray(body.providerIds)
|
|
3900
|
+
? body.providerIds.map((entry) => typeof entry === "string" ? entry.trim() : "")
|
|
3901
|
+
: [];
|
|
3902
|
+
const config = this.currentManualProviders();
|
|
3903
|
+
const currentIds = new Set(config.providers.map((provider) => provider.id));
|
|
3904
|
+
const requestedIds = new Set(providerIds);
|
|
3905
|
+
if (providerIds.length !== config.providers.length ||
|
|
3906
|
+
requestedIds.size !== providerIds.length ||
|
|
3907
|
+
requestedIds.size !== currentIds.size ||
|
|
3908
|
+
providerIds.some((providerId) => !currentIds.has(providerId))) {
|
|
3909
|
+
res.status(400).json({
|
|
3910
|
+
error: {
|
|
3911
|
+
code: "manual_provider_order_invalid",
|
|
3912
|
+
message: "manual provider order must include each configured provider exactly once"
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
const providersById = new Map(config.providers.map((provider) => [provider.id, provider]));
|
|
3918
|
+
const nextConfig = {
|
|
3919
|
+
version: 1,
|
|
3920
|
+
providers: providerIds.map((providerId) => providersById.get(providerId)).filter((provider) => Boolean(provider)),
|
|
3921
|
+
routing: config.routing,
|
|
3922
|
+
updatedAt: new Date().toISOString()
|
|
3923
|
+
};
|
|
3924
|
+
this.saveManualProviders(nextConfig);
|
|
3925
|
+
logger.info("routing.manual_provider.order_applied", "manual provider order applied", {
|
|
3926
|
+
providerIds
|
|
3927
|
+
});
|
|
3928
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
3929
|
+
res.status(200).json({
|
|
3930
|
+
version: nextConfig.version,
|
|
3931
|
+
updatedAt: nextConfig.updatedAt,
|
|
3932
|
+
routing: nextConfig.routing,
|
|
3933
|
+
providers: nextConfig.providers.map((provider) => publicManualProviderConfig(provider, observations.get(provider.id)))
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
catch (error) {
|
|
3937
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3938
|
+
logger.warn("routing.manual_provider.order_apply_failed", "manual provider order apply failed", { errorMessage });
|
|
3939
|
+
res.status(400).json({ error: { code: "manual_provider_order_apply_failed", message: errorMessage } });
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
controlApp.put("/routing/manual-providers/routing", (req, res) => {
|
|
3943
|
+
try {
|
|
3944
|
+
const body = (req.body ?? {});
|
|
3945
|
+
const policy = body.policy;
|
|
3946
|
+
const lockedProviderId = typeof body.lockedProviderId === "string" ? body.lockedProviderId.trim() : undefined;
|
|
3947
|
+
const config = this.currentManualProviders();
|
|
3948
|
+
const routing = policy === "locked"
|
|
3949
|
+
? { policy: "locked", lockedProviderId }
|
|
3950
|
+
: { policy: "fallback" };
|
|
3951
|
+
const normalized = normalizeManualProvidersConfig({
|
|
3952
|
+
...config,
|
|
3953
|
+
routing
|
|
3954
|
+
}).routing;
|
|
3955
|
+
if (normalized.policy === "locked") {
|
|
3956
|
+
const provider = config.providers.find((entry) => entry.id === normalized.lockedProviderId);
|
|
3957
|
+
if (!provider) {
|
|
3958
|
+
res.status(404).json({ error: { code: "manual_provider_not_found", message: `manual provider not found: ${normalized.lockedProviderId}` } });
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
if (!provider.enabled) {
|
|
3962
|
+
res.status(400).json({ error: { code: "manual_provider_locked_disabled", message: `manual provider is disabled: ${provider.id}` } });
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
const nextConfig = {
|
|
3967
|
+
version: 1,
|
|
3968
|
+
providers: config.providers,
|
|
3969
|
+
routing: normalized,
|
|
3970
|
+
updatedAt: new Date().toISOString()
|
|
3971
|
+
};
|
|
3972
|
+
this.saveManualProviders(nextConfig);
|
|
3973
|
+
logger.info("routing.manual_provider.routing_applied", "manual provider routing applied", {
|
|
3974
|
+
policy: normalized.policy,
|
|
3975
|
+
lockedProviderId: normalized.lockedProviderId
|
|
3976
|
+
});
|
|
3977
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
3978
|
+
res.status(200).json({
|
|
3979
|
+
version: nextConfig.version,
|
|
3980
|
+
updatedAt: nextConfig.updatedAt,
|
|
3981
|
+
routing: nextConfig.routing,
|
|
3982
|
+
providers: nextConfig.providers.map((provider) => publicManualProviderConfig(provider, observations.get(provider.id)))
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
catch (error) {
|
|
3986
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3987
|
+
logger.warn("routing.manual_provider.routing_apply_failed", "manual provider routing apply failed", { errorMessage });
|
|
3988
|
+
res.status(400).json({ error: { code: "manual_provider_routing_apply_failed", message: errorMessage } });
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
controlApp.get("/routing/auto-provider", (req, res) => {
|
|
3992
|
+
try {
|
|
3993
|
+
const config = this.currentAutoProviderConfig();
|
|
3994
|
+
const payment = this.providerPaymentState();
|
|
3995
|
+
const mode = this.currentProviderMode();
|
|
3996
|
+
res.status(200).json({
|
|
3997
|
+
config,
|
|
3998
|
+
active: mode.mode === "auto" && this.autoProviderCanRoute(config) && payment.paymentReady,
|
|
3999
|
+
locked: !payment.paymentReady,
|
|
4000
|
+
paymentReady: payment.paymentReady,
|
|
4001
|
+
paymentRequired: !payment.paymentReady,
|
|
4002
|
+
paymentLabel: payment.paymentLabel
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
catch (error) {
|
|
4006
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4007
|
+
logger.warn("routing.auto_provider.read_failed", "auto provider read failed", { errorMessage });
|
|
4008
|
+
res.status(500).json({ error: { code: "auto_provider_read_failed", message: errorMessage } });
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
controlApp.put("/routing/auto-provider", (req, res) => {
|
|
4012
|
+
try {
|
|
4013
|
+
const next = normalizeAutoProviderConfig(req.body);
|
|
4014
|
+
const payment = this.providerPaymentState();
|
|
4015
|
+
if (next.enabled && !payment.paymentReady) {
|
|
4016
|
+
res.status(409).json({
|
|
4017
|
+
error: {
|
|
4018
|
+
code: "payment_required",
|
|
4019
|
+
message: "bind a payment method to enable auto provider"
|
|
4020
|
+
},
|
|
4021
|
+
paymentReady: false,
|
|
4022
|
+
paymentRequired: true,
|
|
4023
|
+
bindTarget: "/overview?bind=clawtip"
|
|
4024
|
+
});
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const shouldRoute = this.autoProviderCanRoute(next);
|
|
4028
|
+
this.saveAutoProviderConfig(shouldRoute ? next : { ...next, enabled: false });
|
|
4029
|
+
let strategy;
|
|
4030
|
+
if (shouldRoute) {
|
|
4031
|
+
this.saveProviderMode("auto");
|
|
4032
|
+
strategy = this.applyAutoProviderRoutingConfig(next);
|
|
4033
|
+
}
|
|
4034
|
+
logger.info("routing.auto_provider.applied", "auto provider applied", {
|
|
4035
|
+
enabled: shouldRoute,
|
|
4036
|
+
range: next.range,
|
|
4037
|
+
scorer: next.scorer,
|
|
4038
|
+
modelCount: next.modelIds.length,
|
|
4039
|
+
sellerCount: next.sellerIds.length,
|
|
4040
|
+
paymentReady: payment.paymentReady
|
|
4041
|
+
});
|
|
4042
|
+
res.status(200).json({
|
|
4043
|
+
applied: true,
|
|
4044
|
+
config: this.currentAutoProviderConfig(),
|
|
4045
|
+
strategy,
|
|
4046
|
+
...this.providerModePayload()
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
catch (error) {
|
|
4050
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4051
|
+
logger.warn("routing.auto_provider.apply_failed", "auto provider apply failed", { errorMessage });
|
|
4052
|
+
res.status(400).json({ error: { code: "auto_provider_apply_failed", message: errorMessage } });
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
2832
4055
|
// 1) GET /routing/strategy — 读当前路由策略 + 来源
|
|
2833
4056
|
controlApp.get("/routing/strategy", (req, res) => {
|
|
2834
4057
|
try {
|
|
@@ -3214,6 +4437,12 @@ function withLiveClawtipWalletState(payment, home) {
|
|
|
3214
4437
|
}
|
|
3215
4438
|
};
|
|
3216
4439
|
}
|
|
4440
|
+
function paymentLabel(method) {
|
|
4441
|
+
if (method === "clawtip") {
|
|
4442
|
+
return "ClawTip";
|
|
4443
|
+
}
|
|
4444
|
+
return method;
|
|
4445
|
+
}
|
|
3217
4446
|
function normalizeClawtipActivationPayment(bootstrap) {
|
|
3218
4447
|
if (!bootstrap.payment?.orderNo || !bootstrap.payment.indicator || !bootstrap.payment.resourceUrl) {
|
|
3219
4448
|
throw new Error("ClawTip bootstrap response missing orderNo, indicator, or resourceUrl.");
|
|
@@ -3247,6 +4476,9 @@ function readConfigString(config, key) {
|
|
|
3247
4476
|
function readConfigBoolean(config, key) {
|
|
3248
4477
|
return config?.[key] === true;
|
|
3249
4478
|
}
|
|
4479
|
+
function normalizePaymentMethodName(method) {
|
|
4480
|
+
return method.trim().toLowerCase();
|
|
4481
|
+
}
|
|
3250
4482
|
function selectedSellerIdForRouting(routing) {
|
|
3251
4483
|
return routing.mode === "fixed" ? routing.sellerId : undefined;
|
|
3252
4484
|
}
|
|
@@ -3311,6 +4543,24 @@ function catalogSnapshotFromRegistry(registry) {
|
|
|
3311
4543
|
}))
|
|
3312
4544
|
};
|
|
3313
4545
|
}
|
|
4546
|
+
function parseOpenAiModelIds(value) {
|
|
4547
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4548
|
+
return [];
|
|
4549
|
+
}
|
|
4550
|
+
const data = value.data;
|
|
4551
|
+
if (!Array.isArray(data)) {
|
|
4552
|
+
return [];
|
|
4553
|
+
}
|
|
4554
|
+
return data
|
|
4555
|
+
.flatMap((entry) => {
|
|
4556
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4557
|
+
return [];
|
|
4558
|
+
}
|
|
4559
|
+
const id = entry.id;
|
|
4560
|
+
return typeof id === "string" && id.trim().length > 0 ? [id.trim()] : [];
|
|
4561
|
+
})
|
|
4562
|
+
.filter((id, index, all) => all.indexOf(id) === index);
|
|
4563
|
+
}
|
|
3314
4564
|
function normalizeTrustedRegistryCache(value) {
|
|
3315
4565
|
if (!value || typeof value !== "object") {
|
|
3316
4566
|
return undefined;
|