@antseed/node 0.2.156 → 0.2.158
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/discovery/announcer.d.ts +16 -1
- package/dist/discovery/announcer.d.ts.map +1 -1
- package/dist/discovery/announcer.js +69 -9
- package/dist/discovery/announcer.js.map +1 -1
- package/dist/discovery/http-metadata-resolver.d.ts +1 -1
- package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
- package/dist/discovery/http-metadata-resolver.js +7 -1
- package/dist/discovery/http-metadata-resolver.js.map +1 -1
- package/dist/discovery/index.d.ts +3 -2
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +2 -1
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/metadata/define-version.d.ts +40 -0
- package/dist/discovery/metadata/define-version.d.ts.map +1 -0
- package/dist/discovery/metadata/define-version.js +104 -0
- package/dist/discovery/metadata/define-version.js.map +1 -0
- package/dist/discovery/metadata/field.d.ts +26 -0
- package/dist/discovery/metadata/field.d.ts.map +1 -0
- package/dist/discovery/metadata/field.js +2 -0
- package/dist/discovery/metadata/field.js.map +1 -0
- package/dist/discovery/metadata/fields/offerings.d.ts +7 -0
- package/dist/discovery/metadata/fields/offerings.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/offerings.js +52 -0
- package/dist/discovery/metadata/fields/offerings.js.map +1 -0
- package/dist/discovery/metadata/fields/on-chain-stats.d.ts +6 -0
- package/dist/discovery/metadata/fields/on-chain-stats.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/on-chain-stats.js +30 -0
- package/dist/discovery/metadata/fields/on-chain-stats.js.map +1 -0
- package/dist/discovery/metadata/fields/peer-capabilities.d.ts +4 -0
- package/dist/discovery/metadata/fields/peer-capabilities.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/peer-capabilities.js +21 -0
- package/dist/discovery/metadata/fields/peer-capabilities.js.map +1 -0
- package/dist/discovery/metadata/fields/peer-profile.d.ts +11 -0
- package/dist/discovery/metadata/fields/peer-profile.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/peer-profile.js +58 -0
- package/dist/discovery/metadata/fields/peer-profile.js.map +1 -0
- package/dist/discovery/metadata/fields/provider-basics.d.ts +12 -0
- package/dist/discovery/metadata/fields/provider-basics.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/provider-basics.js +87 -0
- package/dist/discovery/metadata/fields/provider-basics.js.map +1 -0
- package/dist/discovery/metadata/fields/service-api-protocols.d.ts +4 -0
- package/dist/discovery/metadata/fields/service-api-protocols.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/service-api-protocols.js +40 -0
- package/dist/discovery/metadata/fields/service-api-protocols.js.map +1 -0
- package/dist/discovery/metadata/fields/service-capabilities.d.ts +7 -0
- package/dist/discovery/metadata/fields/service-capabilities.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/service-capabilities.js +229 -0
- package/dist/discovery/metadata/fields/service-capabilities.js.map +1 -0
- package/dist/discovery/metadata/fields/service-categories.d.ts +4 -0
- package/dist/discovery/metadata/fields/service-categories.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/service-categories.js +37 -0
- package/dist/discovery/metadata/fields/service-categories.js.map +1 -0
- package/dist/discovery/metadata/fields/service-health.d.ts +7 -0
- package/dist/discovery/metadata/fields/service-health.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/service-health.js +90 -0
- package/dist/discovery/metadata/fields/service-health.js.map +1 -0
- package/dist/discovery/metadata/fields/service-performance.d.ts +7 -0
- package/dist/discovery/metadata/fields/service-performance.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/service-performance.js +63 -0
- package/dist/discovery/metadata/fields/service-performance.js.map +1 -0
- package/dist/discovery/metadata/fields/service-unit-billing.d.ts +8 -0
- package/dist/discovery/metadata/fields/service-unit-billing.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/service-unit-billing.js +81 -0
- package/dist/discovery/metadata/fields/service-unit-billing.js.map +1 -0
- package/dist/discovery/metadata/fields/verifications.d.ts +7 -0
- package/dist/discovery/metadata/fields/verifications.d.ts.map +1 -0
- package/dist/discovery/metadata/fields/verifications.js +96 -0
- package/dist/discovery/metadata/fields/verifications.js.map +1 -0
- package/dist/discovery/metadata/index.d.ts +27 -0
- package/dist/discovery/metadata/index.d.ts.map +1 -0
- package/dist/discovery/metadata/index.js +56 -0
- package/dist/discovery/metadata/index.js.map +1 -0
- package/dist/discovery/metadata/versions/v10.d.ts +5 -0
- package/dist/discovery/metadata/versions/v10.d.ts.map +1 -0
- package/dist/discovery/metadata/versions/v10.js +32 -0
- package/dist/discovery/metadata/versions/v10.js.map +1 -0
- package/dist/discovery/metadata/versions/v11.d.ts +5 -0
- package/dist/discovery/metadata/versions/v11.d.ts.map +1 -0
- package/dist/discovery/metadata/versions/v11.js +11 -0
- package/dist/discovery/metadata/versions/v11.js.map +1 -0
- package/dist/discovery/metadata/versions/v12.d.ts +5 -0
- package/dist/discovery/metadata/versions/v12.d.ts.map +1 -0
- package/dist/discovery/metadata/versions/v12.js +66 -0
- package/dist/discovery/metadata/versions/v12.js.map +1 -0
- package/dist/discovery/metadata/versions/v13.d.ts +5 -0
- package/dist/discovery/metadata/versions/v13.d.ts.map +1 -0
- package/dist/discovery/metadata/versions/v13.js +16 -0
- package/dist/discovery/metadata/versions/v13.js.map +1 -0
- package/dist/discovery/metadata/versions/v14.d.ts +5 -0
- package/dist/discovery/metadata/versions/v14.d.ts.map +1 -0
- package/dist/discovery/metadata/versions/v14.js +16 -0
- package/dist/discovery/metadata/versions/v14.js.map +1 -0
- package/dist/discovery/metadata/wire.d.ts +54 -0
- package/dist/discovery/metadata/wire.d.ts.map +1 -0
- package/dist/discovery/metadata/wire.js +149 -0
- package/dist/discovery/metadata/wire.js.map +1 -0
- package/dist/discovery/metadata-codec.d.ts +1 -38
- package/dist/discovery/metadata-codec.d.ts.map +1 -1
- package/dist/discovery/metadata-codec.js +3 -1154
- package/dist/discovery/metadata-codec.js.map +1 -1
- package/dist/discovery/metadata-server.d.ts +3 -2
- package/dist/discovery/metadata-server.d.ts.map +1 -1
- package/dist/discovery/metadata-server.js +4 -1
- package/dist/discovery/metadata-server.js.map +1 -1
- package/dist/discovery/metadata-validator.d.ts.map +1 -1
- package/dist/discovery/metadata-validator.js +90 -1
- package/dist/discovery/metadata-validator.js.map +1 -1
- package/dist/discovery/service-catalog.d.ts +16 -2
- package/dist/discovery/service-catalog.d.ts.map +1 -1
- package/dist/discovery/service-catalog.js +8 -1
- package/dist/discovery/service-catalog.js.map +1 -1
- package/dist/health/gas-auto-refuel.d.ts +138 -0
- package/dist/health/gas-auto-refuel.d.ts.map +1 -0
- package/dist/health/gas-auto-refuel.js +216 -0
- package/dist/health/gas-auto-refuel.js.map +1 -0
- package/dist/health/gas-health-monitor.d.ts +4 -0
- package/dist/health/gas-health-monitor.d.ts.map +1 -1
- package/dist/health/gas-health-monitor.js +13 -0
- package/dist/health/gas-health-monitor.js.map +1 -1
- package/dist/health/model-health-checker.d.ts +67 -33
- package/dist/health/model-health-checker.d.ts.map +1 -1
- package/dist/health/model-health-checker.js +273 -121
- package/dist/health/model-health-checker.js.map +1 -1
- package/dist/health/self-health-error.d.ts +20 -0
- package/dist/health/self-health-error.d.ts.map +1 -0
- package/dist/health/self-health-error.js +60 -0
- package/dist/health/self-health-error.js.map +1 -0
- package/dist/health/seller-events.d.ts +1 -1
- package/dist/health/seller-events.d.ts.map +1 -1
- package/dist/health/seller-events.js.map +1 -1
- package/dist/health/service-performance.d.ts +28 -0
- package/dist/health/service-performance.d.ts.map +1 -0
- package/dist/health/service-performance.js +144 -0
- package/dist/health/service-performance.js.map +1 -0
- package/dist/health/stream-outcome.d.ts +19 -0
- package/dist/health/stream-outcome.d.ts.map +1 -0
- package/dist/health/stream-outcome.js +91 -0
- package/dist/health/stream-outcome.js.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/interfaces/seller-provider.d.ts +11 -5
- package/dist/interfaces/seller-provider.d.ts.map +1 -1
- package/dist/node.d.ts +10 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +43 -89
- package/dist/node.js.map +1 -1
- package/dist/p2p/connection-manager.d.ts +5 -1
- package/dist/p2p/connection-manager.d.ts.map +1 -1
- package/dist/p2p/connection-manager.js +48 -13
- package/dist/p2p/connection-manager.js.map +1 -1
- package/dist/payments/chain-config.d.ts +11 -0
- package/dist/payments/chain-config.d.ts.map +1 -1
- package/dist/payments/chain-config.js +7 -0
- package/dist/payments/chain-config.js.map +1 -1
- package/dist/payments/evm/usdc-eth-swap-client.d.ts +45 -0
- package/dist/payments/evm/usdc-eth-swap-client.d.ts.map +1 -0
- package/dist/payments/evm/usdc-eth-swap-client.js +83 -0
- package/dist/payments/evm/usdc-eth-swap-client.js.map +1 -0
- package/dist/payments/index.d.ts +2 -0
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +2 -0
- package/dist/payments/index.js.map +1 -1
- package/dist/payments/seller-payment-manager.d.ts +50 -0
- package/dist/payments/seller-payment-manager.d.ts.map +1 -1
- package/dist/payments/seller-payment-manager.js +320 -80
- package/dist/payments/seller-payment-manager.js.map +1 -1
- package/dist/routing/seller-health.d.ts +21 -0
- package/dist/routing/seller-health.d.ts.map +1 -1
- package/dist/routing/seller-health.js +19 -3
- package/dist/routing/seller-health.js.map +1 -1
- package/dist/seller-request-handler.d.ts +14 -0
- package/dist/seller-request-handler.d.ts.map +1 -1
- package/dist/seller-request-handler.js +207 -30
- package/dist/seller-request-handler.js.map +1 -1
- package/dist/types/peer.d.ts +6 -2
- package/dist/types/peer.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -1,23 +1,35 @@
|
|
|
1
|
+
import { serviceHealthOf } from '../discovery/peer-metadata.js';
|
|
1
2
|
import { isNativeVideoProtocol, isRoutingProtocol } from '../types/service-api.js';
|
|
2
3
|
import { debugLog, debugWarn } from '../utils/debug.js';
|
|
3
4
|
import { isLiveResponseOutOfCredit, isUpstreamOutOfCredit } from './upstream-credit.js';
|
|
4
|
-
/** Default sweep interval. Each sweep sends one 1-token probe per
|
|
5
|
+
/** Default sweep interval. Each sweep sends one 1-token probe per service without recent buyer traffic. */
|
|
5
6
|
export const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 5 * 60_000;
|
|
6
|
-
/** Consecutive
|
|
7
|
+
/** Consecutive failed checks before a service is announced as failing. */
|
|
7
8
|
export const DEFAULT_HEALTH_CHECK_FAILURE_THRESHOLD = 3;
|
|
8
9
|
/** Per-probe timeout. A model that hangs this long is treated as failing. */
|
|
9
10
|
export const DEFAULT_HEALTH_CHECK_PROBE_TIMEOUT_MS = 60_000;
|
|
11
|
+
/** Shortest gap between probes triggered by failed buyer requests for one service. */
|
|
12
|
+
export const LIVE_FAILURE_PROBE_MIN_GAP_MS = 30_000;
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* A service that keeps failing is probed less often: every interval, then
|
|
15
|
+
* every second, then every third (with the default, 5, 10 and 15 minutes).
|
|
16
|
+
* Its upstream is often rate limited or out of credit, and each probe would
|
|
17
|
+
* add to that. A successful buyer request still marks it healthy at once.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_FAILING_PROBE_BACKOFF = 3;
|
|
20
|
+
class ProbeTimeoutError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The seller's self health check. Every service stays announced; its latest
|
|
24
|
+
* result is published on `provider.serviceHealth`, which goes into signed
|
|
25
|
+
* metadata (v13+) so buyers can hide failing services from catalogs and
|
|
26
|
+
* routing and explain why a pinned request failed.
|
|
15
27
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
28
|
+
* Buyer traffic is the main evidence: a successful billable response marks a
|
|
29
|
+
* service healthy, and a service that had one within the last interval is not
|
|
30
|
+
* probed. Idle services get a minimal (1-token) completion each interval, and
|
|
31
|
+
* a failed buyer request triggers an early probe. A service is failing after
|
|
32
|
+
* `failureThreshold` failed checks in a row and recovers on the first pass.
|
|
21
33
|
*/
|
|
22
34
|
export class ModelHealthChecker {
|
|
23
35
|
_targets;
|
|
@@ -36,8 +48,8 @@ export class ModelHealthChecker {
|
|
|
36
48
|
_onSweepComplete;
|
|
37
49
|
/** Each upstream's credit as last seen; unset until something said. */
|
|
38
50
|
_accounts = new Map();
|
|
39
|
-
/** Services
|
|
40
|
-
|
|
51
|
+
/** Services marked failing before `onChange` could be told; reported on the first start. */
|
|
52
|
+
_failingAtInit = [];
|
|
41
53
|
constructor(config) {
|
|
42
54
|
this._targets = config.targets;
|
|
43
55
|
this._intervalMs = config.intervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS;
|
|
@@ -47,20 +59,23 @@ export class ModelHealthChecker {
|
|
|
47
59
|
this._onUpstreamAccount = config.onUpstreamAccount;
|
|
48
60
|
this._lowBalanceUsd = config.lowBalanceUsd;
|
|
49
61
|
this._onSweepComplete = config.onSweepComplete;
|
|
50
|
-
// Providers retained after an OAuth initialization failure
|
|
51
|
-
//
|
|
52
|
-
|
|
62
|
+
// Providers retained after an OAuth initialization failure stay announced
|
|
63
|
+
// as failing until a real check passes.
|
|
64
|
+
const now = Date.now();
|
|
53
65
|
for (const { provider } of this._targets) {
|
|
54
66
|
if (provider.healthCheckAvailable !== false)
|
|
55
67
|
continue;
|
|
56
|
-
for (const
|
|
68
|
+
for (const service of provider.services) {
|
|
57
69
|
const state = this._stateFor(provider, service);
|
|
58
|
-
state.
|
|
59
|
-
state.
|
|
60
|
-
state.
|
|
61
|
-
|
|
70
|
+
state.status = 'failing';
|
|
71
|
+
state.reason = 'init_failed';
|
|
72
|
+
state.failingSince = now;
|
|
73
|
+
state.checkedAt = now;
|
|
74
|
+
state.source = 'probe';
|
|
75
|
+
state.lastDetail = 'Initialization failed; awaiting a successful check';
|
|
76
|
+
this._publish(state);
|
|
77
|
+
this._failingAtInit.push(state);
|
|
62
78
|
}
|
|
63
|
-
provider.services.splice(0);
|
|
64
79
|
}
|
|
65
80
|
}
|
|
66
81
|
/** Start periodic sweeps. The first sweep runs immediately (async). */
|
|
@@ -68,15 +83,16 @@ export class ModelHealthChecker {
|
|
|
68
83
|
if (this._timer)
|
|
69
84
|
return;
|
|
70
85
|
this._stopped = false;
|
|
71
|
-
for (const state of this.
|
|
72
|
-
if (
|
|
86
|
+
for (const state of this._failingAtInit.splice(0)) {
|
|
87
|
+
if (state.status !== 'failing')
|
|
73
88
|
continue;
|
|
74
89
|
this._emitChange({
|
|
75
90
|
provider: state.providerName,
|
|
76
91
|
providerId: state.providerId,
|
|
77
92
|
service: state.service,
|
|
78
|
-
status: '
|
|
93
|
+
status: 'failing',
|
|
79
94
|
consecutiveFailures: state.consecutiveFailures,
|
|
95
|
+
reason: 'init_failed',
|
|
80
96
|
detail: state.lastDetail ?? 'initialization failed',
|
|
81
97
|
});
|
|
82
98
|
}
|
|
@@ -94,7 +110,7 @@ export class ModelHealthChecker {
|
|
|
94
110
|
this._timer = null;
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
|
-
/**
|
|
113
|
+
/** Check every service once, skipping probes for services with recent buyer traffic. */
|
|
98
114
|
async runSweep() {
|
|
99
115
|
if (this._sweepInFlight)
|
|
100
116
|
return this._sweepInFlight;
|
|
@@ -116,11 +132,16 @@ export class ModelHealthChecker {
|
|
|
116
132
|
provider: state.providerName,
|
|
117
133
|
providerId: state.providerId,
|
|
118
134
|
service: state.service,
|
|
119
|
-
|
|
135
|
+
status: state.status,
|
|
120
136
|
consecutiveFailures: state.consecutiveFailures,
|
|
137
|
+
failingSince: state.failingSince,
|
|
138
|
+
reason: state.reason,
|
|
139
|
+
checkedAt: state.checkedAt,
|
|
140
|
+
source: state.source,
|
|
121
141
|
lastProbeAt: state.lastProbeAt,
|
|
122
142
|
lastStatusCode: state.lastStatusCode,
|
|
123
143
|
lastDetail: state.lastDetail,
|
|
144
|
+
lastTrafficAt: state.lastTrafficAt,
|
|
124
145
|
}));
|
|
125
146
|
}
|
|
126
147
|
async _runSweep() {
|
|
@@ -128,39 +149,33 @@ export class ModelHealthChecker {
|
|
|
128
149
|
if (this._stopped)
|
|
129
150
|
return;
|
|
130
151
|
const provider = target.provider;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (capacity.current >= capacity.max) {
|
|
135
|
-
debugLog(`[ModelHealth] Skipping ${provider.name}: at capacity (${capacity.current}/${capacity.max})`);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// getCapacity should not throw; probe anyway if it does.
|
|
141
|
-
}
|
|
142
|
-
// Probe currently advertised services plus ones we removed earlier
|
|
143
|
-
// (so they can recover). Snapshot before iterating — probes mutate
|
|
144
|
-
// the live array on state transitions.
|
|
145
|
-
const services = new Set(provider.services);
|
|
146
|
-
for (const state of this._states.values()) {
|
|
147
|
-
if (state.provider === provider && state.removed) {
|
|
148
|
-
services.add(state.service);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
152
|
+
if (this._atCapacity(provider))
|
|
153
|
+
continue;
|
|
154
|
+
const services = [...provider.services];
|
|
151
155
|
const hasFreeCheck = (candidate) => Boolean(candidate?.checkServiceHealth || candidate?.checkUpstreamAccount);
|
|
152
156
|
const checkProvider = hasFreeCheck(target.probeProvider) ? target.probeProvider : provider;
|
|
157
|
+
// Free checks also read the account balance, so they run even with traffic.
|
|
153
158
|
const checked = hasFreeCheck(checkProvider)
|
|
154
|
-
?
|
|
159
|
+
? services.filter((service) => !supportsHealthProbe(resolveProbeProtocol(provider, service)))
|
|
155
160
|
: [];
|
|
156
161
|
if (checked.length > 0) {
|
|
157
162
|
await this._checkServices(target, checkProvider, checked);
|
|
158
163
|
}
|
|
164
|
+
const now = Date.now();
|
|
159
165
|
for (const service of services) {
|
|
160
166
|
if (this._stopped)
|
|
161
167
|
return;
|
|
162
168
|
if (checked.includes(service))
|
|
163
169
|
continue;
|
|
170
|
+
const state = this._stateFor(provider, service);
|
|
171
|
+
if (state.lastTrafficAt !== null && now - state.lastTrafficAt < this._intervalMs) {
|
|
172
|
+
debugLog(`[ModelHealth] ${provider.name}/${service}: skipping probe, served a buyer ${Math.round((now - state.lastTrafficAt) / 1000)}s ago`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (this._backingOff(state, now)) {
|
|
176
|
+
debugLog(`[ModelHealth] ${provider.name}/${service}: failing, next probe later (${state.consecutiveFailures} failed checks in a row)`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
164
179
|
await this._probeService(target, service);
|
|
165
180
|
}
|
|
166
181
|
}
|
|
@@ -170,12 +185,13 @@ export class ModelHealthChecker {
|
|
|
170
185
|
* upstream account check (key valid, account funded) once, and the model
|
|
171
186
|
* check (model still offered) for every service. A service is unavailable
|
|
172
187
|
* when either check says so, and available only when no check is unsure.
|
|
173
|
-
* `unavailable` counts as a failed check, `available`
|
|
188
|
+
* `unavailable` counts as a failed check, `available` as a pass,
|
|
174
189
|
* `unknown` changes nothing.
|
|
175
190
|
*/
|
|
176
191
|
async _checkServices(target, checkProvider, services) {
|
|
177
192
|
const provider = target.provider;
|
|
178
193
|
const timed = (label, check) => withTimeout(Promise.resolve().then(check), this._probeTimeoutMs, `${label} for ${provider.name} timed out after ${this._probeTimeoutMs}ms`);
|
|
194
|
+
const checkStartedAt = Date.now();
|
|
179
195
|
const [account, models] = await Promise.all([
|
|
180
196
|
checkProvider.checkUpstreamAccount
|
|
181
197
|
? timed('account check', () => checkProvider.checkUpstreamAccount())
|
|
@@ -186,7 +202,8 @@ export class ModelHealthChecker {
|
|
|
186
202
|
: undefined,
|
|
187
203
|
]);
|
|
188
204
|
const balance = account?.status === 'available' ? account.balanceUsd : undefined;
|
|
189
|
-
|
|
205
|
+
const accountOutOfCredit = account?.status === 'unavailable' && (account.outOfCredit === true || isUpstreamOutOfCredit(null, account.reason));
|
|
206
|
+
if (accountOutOfCredit) {
|
|
190
207
|
this._setAccount(provider, 'out', account.reason, null);
|
|
191
208
|
}
|
|
192
209
|
else if (account?.status === 'available') {
|
|
@@ -204,15 +221,17 @@ export class ModelHealthChecker {
|
|
|
204
221
|
const model = models === undefined
|
|
205
222
|
? undefined
|
|
206
223
|
: models?.[service] ?? { status: 'unknown', reason: 'model check gave no result' };
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
if (account?.status === 'unavailable')
|
|
225
|
+
return { result: account, reason: accountOutOfCredit ? 'out_of_credit' : reasonForAccount(account.reason) };
|
|
226
|
+
if (model?.status === 'unavailable')
|
|
227
|
+
return { result: model, reason: 'model_unavailable' };
|
|
228
|
+
const unsure = [account, model].find((result) => result?.status === 'unknown');
|
|
229
|
+
return { result: unsure ?? { status: 'available', balanceUsd: balance }, reason: 'upstream_error' };
|
|
211
230
|
};
|
|
212
231
|
for (const service of services) {
|
|
213
232
|
if (this._stopped)
|
|
214
233
|
return;
|
|
215
|
-
const result = combine(service);
|
|
234
|
+
const { result, reason } = combine(service);
|
|
216
235
|
const state = this._stateFor(provider, service);
|
|
217
236
|
if (result.status === 'unknown') {
|
|
218
237
|
debugLog(`[ModelHealth] ${provider.name}/${service}: inconclusive (${result.reason ?? 'no result'})`);
|
|
@@ -222,14 +241,10 @@ export class ModelHealthChecker {
|
|
|
222
241
|
state.lastStatusCode = null;
|
|
223
242
|
if (result.status === 'unavailable') {
|
|
224
243
|
state.lastDetail = result.reason;
|
|
225
|
-
this._recordFailure(
|
|
244
|
+
this._recordFailure(state, reason, null, checkStartedAt);
|
|
226
245
|
continue;
|
|
227
246
|
}
|
|
228
|
-
state
|
|
229
|
-
if (!state.removed)
|
|
230
|
-
continue;
|
|
231
|
-
state.lastDetail = balance === undefined ? 'Account check passed' : `Account check passed (balance $${balance.toFixed(2)})`;
|
|
232
|
-
this._restoreService(provider, state);
|
|
247
|
+
this._recordSuccess(state, 'probe', balance === undefined ? 'Account check passed' : `Account check passed (balance $${balance.toFixed(2)})`);
|
|
233
248
|
}
|
|
234
249
|
}
|
|
235
250
|
_stateFor(provider, service) {
|
|
@@ -246,12 +261,18 @@ export class ModelHealthChecker {
|
|
|
246
261
|
providerName: provider.name,
|
|
247
262
|
providerId: this._targets.find((target) => target.provider === provider)?.id ?? provider.name,
|
|
248
263
|
service,
|
|
264
|
+
status: 'unknown',
|
|
249
265
|
consecutiveFailures: 0,
|
|
250
|
-
|
|
251
|
-
|
|
266
|
+
failingSince: null,
|
|
267
|
+
reason: null,
|
|
268
|
+
httpStatus: null,
|
|
269
|
+
checkedAt: null,
|
|
270
|
+
source: null,
|
|
252
271
|
lastProbeAt: null,
|
|
253
272
|
lastStatusCode: null,
|
|
254
273
|
lastDetail: null,
|
|
274
|
+
lastTrafficAt: null,
|
|
275
|
+
probeInFlight: false,
|
|
255
276
|
};
|
|
256
277
|
this._states.set(key, state);
|
|
257
278
|
}
|
|
@@ -261,8 +282,13 @@ export class ModelHealthChecker {
|
|
|
261
282
|
const provider = target.provider;
|
|
262
283
|
const probeProvider = target.probeProvider ?? provider;
|
|
263
284
|
const state = this._stateFor(provider, service);
|
|
264
|
-
state.
|
|
285
|
+
if (state.probeInFlight)
|
|
286
|
+
return;
|
|
287
|
+
state.probeInFlight = true;
|
|
288
|
+
const probeStartedAt = Date.now();
|
|
289
|
+
state.lastProbeAt = probeStartedAt;
|
|
265
290
|
let outcome;
|
|
291
|
+
let reason;
|
|
266
292
|
let errorText = null;
|
|
267
293
|
try {
|
|
268
294
|
const protocol = resolveProbeProtocol(provider, service);
|
|
@@ -276,6 +302,7 @@ export class ModelHealthChecker {
|
|
|
276
302
|
const response = await withTimeout(probeProvider.handleRequest(request), this._probeTimeoutMs, `health probe for ${service} timed out after ${this._probeTimeoutMs}ms`);
|
|
277
303
|
state.lastStatusCode = response.statusCode;
|
|
278
304
|
outcome = classifyProbeStatus(response.statusCode);
|
|
305
|
+
reason = reasonForStatus(response.statusCode);
|
|
279
306
|
// Classified on the body itself, not the 200-byte summary: a billing code can follow a long message.
|
|
280
307
|
if (outcome !== 'healthy')
|
|
281
308
|
errorText = decodeBody(response.body, 16_384);
|
|
@@ -288,17 +315,22 @@ export class ModelHealthChecker {
|
|
|
288
315
|
state.lastDetail = err instanceof Error ? err.message : String(err);
|
|
289
316
|
errorText = state.lastDetail;
|
|
290
317
|
outcome = 'unhealthy';
|
|
318
|
+
reason = err instanceof ProbeTimeoutError ? 'timeout' : 'unreachable';
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
state.probeInFlight = false;
|
|
291
322
|
}
|
|
292
323
|
// A billing error is a failure whatever its status (Anthropic answers one with a 400);
|
|
293
324
|
// a 404 or 422 is about the probe itself, so its text never marks the account.
|
|
294
325
|
if (outcome !== 'healthy' && state.lastStatusCode !== 404 && state.lastStatusCode !== 422 && isUpstreamOutOfCredit(state.lastStatusCode, errorText)) {
|
|
295
326
|
outcome = 'unhealthy';
|
|
327
|
+
reason = 'out_of_credit';
|
|
296
328
|
this._setAccount(provider, 'out', state.lastDetail ?? 'out of credit', null);
|
|
297
329
|
}
|
|
298
330
|
if (outcome === 'inconclusive') {
|
|
299
331
|
// The endpoint answered but the probe result proves nothing either way
|
|
300
|
-
// (for example, 400 for an unsupported probe parameter). Leave
|
|
301
|
-
//
|
|
332
|
+
// (for example, 400 for an unsupported probe parameter). Leave the
|
|
333
|
+
// service's health untouched.
|
|
302
334
|
debugLog(`[ModelHealth] ${provider.name}/${service}: inconclusive (${state.lastDetail})`);
|
|
303
335
|
return;
|
|
304
336
|
}
|
|
@@ -306,22 +338,22 @@ export class ModelHealthChecker {
|
|
|
306
338
|
// An answered completion means the account pays again (its balance is unknown here).
|
|
307
339
|
if (this._accounts.get(provider) === 'out')
|
|
308
340
|
this._setAccount(provider, 'ok', state.lastDetail ?? 'probe succeeded', null);
|
|
309
|
-
state.
|
|
310
|
-
if (state.removed) {
|
|
311
|
-
this._restoreService(provider, state);
|
|
312
|
-
}
|
|
341
|
+
this._recordSuccess(state, 'probe', state.lastDetail ?? 'probe succeeded');
|
|
313
342
|
return;
|
|
314
343
|
}
|
|
315
|
-
this._recordFailure(
|
|
344
|
+
this._recordFailure(state, reason, state.lastStatusCode, probeStartedAt);
|
|
316
345
|
}
|
|
317
346
|
/**
|
|
318
|
-
* A live request's upstream answer (the seller's own traffic)
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
347
|
+
* A live request's upstream answer (the seller's own traffic). A billable
|
|
348
|
+
* success marks `service` healthy and spares it the next probe. A failure
|
|
349
|
+
* the upstream is responsible for (auth, limits, 5xx) triggers an early
|
|
350
|
+
* probe instead of counting directly, so one buyer's odd request cannot mark
|
|
351
|
+
* a service failing. An out-of-credit error is reported at once, and a
|
|
352
|
+
* billable success after one means the account pays again (polling or
|
|
353
|
+
* downloading a video already paid for is not billable and proves nothing).
|
|
322
354
|
*/
|
|
323
|
-
reportUpstreamResponse(provider, statusCode, body, billable = true) {
|
|
324
|
-
const target = this.
|
|
355
|
+
reportUpstreamResponse(provider, statusCode, body, billable = true, service) {
|
|
356
|
+
const target = this._findTarget(provider);
|
|
325
357
|
if (!target || this._stopped)
|
|
326
358
|
return;
|
|
327
359
|
if (statusCode >= 400 && isLiveResponseOutOfCredit(statusCode, body)) {
|
|
@@ -330,6 +362,78 @@ export class ModelHealthChecker {
|
|
|
330
362
|
else if (statusCode < 400 && billable && this._accounts.get(target.provider) === 'out') {
|
|
331
363
|
this._setAccount(target.provider, 'ok', `HTTP ${statusCode} on a request`, null);
|
|
332
364
|
}
|
|
365
|
+
if (!service || !target.provider.services.includes(service))
|
|
366
|
+
return;
|
|
367
|
+
if (statusCode < 400) {
|
|
368
|
+
if (!billable)
|
|
369
|
+
return;
|
|
370
|
+
const state = this._stateFor(target.provider, service);
|
|
371
|
+
state.lastTrafficAt = Date.now();
|
|
372
|
+
state.lastStatusCode = statusCode;
|
|
373
|
+
this._recordSuccess(state, 'traffic', `HTTP ${statusCode} on a buyer request`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (statusCode !== 404 && classifyProbeStatus(statusCode) === 'unhealthy') {
|
|
377
|
+
this._probeSoon(target, service);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** A live request threw before the upstream answered (network error, timeout). */
|
|
381
|
+
reportUpstreamFailure(provider, service) {
|
|
382
|
+
const target = this._findTarget(provider);
|
|
383
|
+
if (!target || this._stopped || !target.provider.services.includes(service))
|
|
384
|
+
return;
|
|
385
|
+
this._probeSoon(target, service);
|
|
386
|
+
}
|
|
387
|
+
/** The announced health of `service`, if it has been checked. */
|
|
388
|
+
getServiceHealth(provider, service) {
|
|
389
|
+
const target = this._findTarget(provider);
|
|
390
|
+
return serviceHealthOf(target?.provider.serviceHealth, service);
|
|
391
|
+
}
|
|
392
|
+
_findTarget(provider) {
|
|
393
|
+
return this._targets.find((entry) => entry.provider === provider || entry.probeProvider === provider);
|
|
394
|
+
}
|
|
395
|
+
/** A failing service skips scheduled probes for longer the more checks it has failed in a row. */
|
|
396
|
+
_backingOff(state, now) {
|
|
397
|
+
if (state.status !== 'failing' || state.lastProbeAt === null)
|
|
398
|
+
return false;
|
|
399
|
+
const backoff = Math.min(MAX_FAILING_PROBE_BACKOFF, 1 + state.consecutiveFailures - this._failureThreshold);
|
|
400
|
+
// Sweeps come one interval apart, so allow one to come a little early.
|
|
401
|
+
return backoff > 1 && now - state.lastProbeAt < backoff * this._intervalMs - LIVE_FAILURE_PROBE_MIN_GAP_MS;
|
|
402
|
+
}
|
|
403
|
+
_probeSoon(target, service) {
|
|
404
|
+
// Image and video services are never probed (a probe would cost a real generation).
|
|
405
|
+
if (!supportsHealthProbe(resolveProbeProtocol(target.provider, service)))
|
|
406
|
+
return;
|
|
407
|
+
const state = this._stateFor(target.provider, service);
|
|
408
|
+
if (state.probeInFlight)
|
|
409
|
+
return;
|
|
410
|
+
// Already announced as failing: a probe could only confirm it. A buyer
|
|
411
|
+
// success or the next scheduled probe notices recovery.
|
|
412
|
+
if (state.status === 'failing')
|
|
413
|
+
return;
|
|
414
|
+
if (state.lastProbeAt !== null && Date.now() - state.lastProbeAt < LIVE_FAILURE_PROBE_MIN_GAP_MS)
|
|
415
|
+
return;
|
|
416
|
+
// A full provider answers its own 429, which says nothing about the upstream.
|
|
417
|
+
if (this._atCapacity(target.provider))
|
|
418
|
+
return;
|
|
419
|
+
debugLog(`[ModelHealth] ${target.provider.name}/${service}: buyer request failed upstream, probing now`);
|
|
420
|
+
void this._probeService(target, service).catch((err) => {
|
|
421
|
+
debugWarn(`[ModelHealth] early probe failed: ${err instanceof Error ? err.message : err}`);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/** Probes never compete with paying buyers for concurrency slots. */
|
|
425
|
+
_atCapacity(provider) {
|
|
426
|
+
try {
|
|
427
|
+
const capacity = provider.getCapacity();
|
|
428
|
+
if (capacity.current < capacity.max)
|
|
429
|
+
return false;
|
|
430
|
+
debugLog(`[ModelHealth] Skipping ${provider.name}: at capacity (${capacity.current}/${capacity.max})`);
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// getCapacity should not throw; probe anyway if it does.
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
333
437
|
}
|
|
334
438
|
_lowLine() {
|
|
335
439
|
const value = typeof this._lowBalanceUsd === 'function' ? this._lowBalanceUsd() : this._lowBalanceUsd;
|
|
@@ -344,14 +448,13 @@ export class ModelHealthChecker {
|
|
|
344
448
|
if (next === 'ok' && previous === undefined)
|
|
345
449
|
return;
|
|
346
450
|
const target = this._targets.find((entry) => entry.provider === provider);
|
|
347
|
-
const services = [...new Set([...provider.services, ...[...this._states.values()].filter((state) => state.provider === provider).map((state) => state.service)])];
|
|
348
451
|
const event = {
|
|
349
452
|
provider: provider.name,
|
|
350
453
|
providerId: target?.id ?? provider.name,
|
|
351
454
|
status: next === 'out' ? 'out_of_credit' : next === 'low' ? 'low_balance' : 'funded',
|
|
352
455
|
detail,
|
|
353
456
|
balanceUsd,
|
|
354
|
-
services,
|
|
457
|
+
services: [...provider.services],
|
|
355
458
|
};
|
|
356
459
|
debugWarn(`[ModelHealth] ${event.providerId}: upstream ${event.status} (${detail})`);
|
|
357
460
|
if (!this._onUpstreamAccount)
|
|
@@ -366,59 +469,88 @@ export class ModelHealthChecker {
|
|
|
366
469
|
debugWarn(`[ModelHealth] onUpstreamAccount failed: ${err instanceof Error ? err.message : err}`);
|
|
367
470
|
}
|
|
368
471
|
}
|
|
369
|
-
|
|
370
|
-
state.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (services.length === 0) {
|
|
384
|
-
provider.healthCheckAvailable = false;
|
|
385
|
-
}
|
|
472
|
+
_recordSuccess(state, source, detail) {
|
|
473
|
+
const wasFailing = state.status === 'failing';
|
|
474
|
+
state.status = 'healthy';
|
|
475
|
+
state.consecutiveFailures = 0;
|
|
476
|
+
state.failingSince = null;
|
|
477
|
+
state.reason = null;
|
|
478
|
+
state.httpStatus = null;
|
|
479
|
+
state.checkedAt = Date.now();
|
|
480
|
+
state.source = source;
|
|
481
|
+
state.lastDetail = detail;
|
|
482
|
+
if (state.provider.healthCheckAvailable === false)
|
|
483
|
+
state.provider.healthCheckAvailable = true;
|
|
484
|
+
this._publish(state);
|
|
485
|
+
if (!wasFailing)
|
|
386
486
|
return;
|
|
387
|
-
}
|
|
388
|
-
services.splice(index, 1);
|
|
389
|
-
state.removed = true;
|
|
390
|
-
state.removedAtIndex = index;
|
|
391
|
-
if (services.length === 0) {
|
|
392
|
-
provider.healthCheckAvailable = false;
|
|
393
|
-
}
|
|
394
487
|
this._emitChange({
|
|
395
|
-
provider:
|
|
488
|
+
provider: state.providerName,
|
|
396
489
|
providerId: state.providerId,
|
|
397
490
|
service: state.service,
|
|
398
|
-
status: '
|
|
399
|
-
consecutiveFailures:
|
|
400
|
-
detail
|
|
491
|
+
status: 'recovered',
|
|
492
|
+
consecutiveFailures: 0,
|
|
493
|
+
detail,
|
|
401
494
|
});
|
|
402
495
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
state.
|
|
406
|
-
|
|
407
|
-
|
|
496
|
+
/** A failed check started at `checkStartedAt`; a buyer success since then outweighs it. */
|
|
497
|
+
_recordFailure(state, reason, httpStatus, checkStartedAt) {
|
|
498
|
+
if (state.lastTrafficAt !== null && state.lastTrafficAt >= checkStartedAt) {
|
|
499
|
+
debugLog(`[ModelHealth] ${state.providerName}/${state.service}: check failed, but a buyer request succeeded meanwhile`);
|
|
500
|
+
return;
|
|
408
501
|
}
|
|
409
|
-
|
|
502
|
+
const now = Date.now();
|
|
503
|
+
state.consecutiveFailures += 1;
|
|
504
|
+
state.reason = reason;
|
|
505
|
+
state.httpStatus = httpStatus;
|
|
506
|
+
state.failingSince ??= now;
|
|
507
|
+
debugWarn(`[ModelHealth] ${state.providerName}/${state.service}: health check failed `
|
|
508
|
+
+ `(${state.consecutiveFailures}/${this._failureThreshold}, ${reason}) — ${state.lastDetail}`);
|
|
509
|
+
const crossed = state.status !== 'failing' && state.consecutiveFailures >= this._failureThreshold;
|
|
510
|
+
if (crossed)
|
|
511
|
+
state.status = 'failing';
|
|
512
|
+
// Below the threshold the announced status and its evidence stand; only
|
|
513
|
+
// a failing status is backed by this check.
|
|
514
|
+
if (state.status !== 'failing')
|
|
515
|
+
return;
|
|
516
|
+
state.checkedAt = now;
|
|
517
|
+
state.source = 'probe';
|
|
518
|
+
this._publish(state);
|
|
519
|
+
if (!crossed)
|
|
520
|
+
return;
|
|
410
521
|
this._emitChange({
|
|
411
|
-
provider:
|
|
522
|
+
provider: state.providerName,
|
|
412
523
|
providerId: state.providerId,
|
|
413
524
|
service: state.service,
|
|
414
|
-
status: '
|
|
415
|
-
consecutiveFailures:
|
|
416
|
-
|
|
525
|
+
status: 'failing',
|
|
526
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
527
|
+
reason,
|
|
528
|
+
detail: state.lastDetail ?? 'check failed',
|
|
417
529
|
});
|
|
418
530
|
}
|
|
531
|
+
/** Writes the announced form of `state` onto the provider. */
|
|
532
|
+
_publish(state) {
|
|
533
|
+
const health = { status: state.status };
|
|
534
|
+
if (state.checkedAt !== null)
|
|
535
|
+
health.checkedAt = state.checkedAt;
|
|
536
|
+
if (state.source !== null)
|
|
537
|
+
health.source = state.source;
|
|
538
|
+
if (state.status === 'failing') {
|
|
539
|
+
if (state.failingSince !== null)
|
|
540
|
+
health.failingSince = state.failingSince;
|
|
541
|
+
if (state.consecutiveFailures > 0)
|
|
542
|
+
health.consecutiveFailures = Math.min(0xffff, state.consecutiveFailures);
|
|
543
|
+
if (state.reason !== null)
|
|
544
|
+
health.reason = state.reason;
|
|
545
|
+
if (state.httpStatus !== null && state.httpStatus >= 100 && state.httpStatus <= 599)
|
|
546
|
+
health.httpStatus = state.httpStatus;
|
|
547
|
+
}
|
|
548
|
+
// An own entry even for a service named like an Object.prototype property ("__proto__").
|
|
549
|
+
state.provider.serviceHealth ??= Object.create(null);
|
|
550
|
+
Object.defineProperty(state.provider.serviceHealth, state.service, { value: health, enumerable: true, writable: true, configurable: true });
|
|
551
|
+
}
|
|
419
552
|
_emitChange(event) {
|
|
420
|
-
|
|
421
|
-
debugLog(`[ModelHealth] ${event.provider}/${event.service} ${action} (${event.detail})`);
|
|
553
|
+
debugLog(`[ModelHealth] ${event.provider}/${event.service} ${event.status === 'failing' ? 'marked failing' : 'recovered'} (${event.detail})`);
|
|
422
554
|
if (!this._onChange)
|
|
423
555
|
return;
|
|
424
556
|
try {
|
|
@@ -434,6 +566,26 @@ export class ModelHealthChecker {
|
|
|
434
566
|
}
|
|
435
567
|
}
|
|
436
568
|
}
|
|
569
|
+
/** The announced reason for an account check that says the upstream cannot serve. */
|
|
570
|
+
function reasonForAccount(reason) {
|
|
571
|
+
if (/balance|credit|fund|top.?up|quota/i.test(reason))
|
|
572
|
+
return 'out_of_credit';
|
|
573
|
+
if (/key|auth|credential|forbidden|permission|token/i.test(reason))
|
|
574
|
+
return 'auth';
|
|
575
|
+
return 'upstream_error';
|
|
576
|
+
}
|
|
577
|
+
/** The announced reason for a failed check's upstream status. */
|
|
578
|
+
function reasonForStatus(statusCode) {
|
|
579
|
+
if (statusCode === 429)
|
|
580
|
+
return 'rate_limited';
|
|
581
|
+
if (statusCode === 402)
|
|
582
|
+
return 'out_of_credit';
|
|
583
|
+
if (statusCode === 401 || statusCode === 403)
|
|
584
|
+
return 'auth';
|
|
585
|
+
if (statusCode === 404)
|
|
586
|
+
return 'model_unavailable';
|
|
587
|
+
return 'upstream_error';
|
|
588
|
+
}
|
|
437
589
|
/** Pick the probe request shape from the service's advertised API protocol. */
|
|
438
590
|
function resolveProbeProtocol(provider, service) {
|
|
439
591
|
return provider.serviceApiProtocols?.[service]?.[0] ?? 'openai-chat-completions';
|
|
@@ -507,7 +659,7 @@ export function buildHealthProbeRequest(service, protocol) {
|
|
|
507
659
|
* collapses upstream network errors/timeouts to 502.)
|
|
508
660
|
* - Everything else (400, 422, ...) — the endpoint is alive but the probe
|
|
509
661
|
* itself was rejected (for example, an unsupported parameter): inconclusive,
|
|
510
|
-
* so a working model is never
|
|
662
|
+
* so a working model is never marked failing over a probe quirk.
|
|
511
663
|
*/
|
|
512
664
|
export function classifyProbeStatus(statusCode) {
|
|
513
665
|
if (statusCode >= 200 && statusCode < 400)
|
|
@@ -538,7 +690,7 @@ function summarizeBody(body) {
|
|
|
538
690
|
async function withTimeout(promise, timeoutMs, message) {
|
|
539
691
|
let timer;
|
|
540
692
|
const timeout = new Promise((_, reject) => {
|
|
541
|
-
timer = setTimeout(() => reject(new
|
|
693
|
+
timer = setTimeout(() => reject(new ProbeTimeoutError(message)), timeoutMs);
|
|
542
694
|
timer.unref?.();
|
|
543
695
|
});
|
|
544
696
|
try {
|