@wengine-ai/llms 2.3.2417 → 2.3.2418

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.
@@ -56,6 +56,30 @@ export interface PluginDispatchInput {
56
56
  * spreads) for per-request wrapper state. */
57
57
  req?: any;
58
58
  }
59
+ /**
60
+ * Outcome of a level-1 account-state check.
61
+ *
62
+ * `available: false` parks the provider without a network call; `parkedUntil`
63
+ * lets the plugin tell the core when the account frees up (e.g. a quota reset
64
+ * epoch), so the probe does not re-check before then.
65
+ */
66
+ export interface ManagedProbeVerdict {
67
+ available: boolean;
68
+ /** Epoch ms after which the account may be usable again. */
69
+ parkedUntil?: number;
70
+ /** Stable reason used for logging only — never shown to a client. */
71
+ reason?: string;
72
+ }
73
+ /** Input for a runtime-owned liveness probe. */
74
+ export interface ManagedProbeInput {
75
+ /** One ordinary core dispatch attempt (same seam as `PluginDispatchInput.attempt`). */
76
+ attempt(): Promise<Response>;
77
+ /** Model to probe with (the first circuit-open model of the provider). */
78
+ model: string;
79
+ provider: LLMProvider;
80
+ /** Per-attempt config; the core sets `TIMEOUT` so a probe cannot outlive its budget. */
81
+ config: any;
82
+ }
59
83
  /**
60
84
  * Managed-subscription semantics for one provider family. Everything the data
61
85
  * plane needs to talk to a subscription upstream without the core knowing it:
@@ -109,6 +133,36 @@ export interface ManagedRuntime {
109
133
  * hard attempt bound and must never swallow client aborts.
110
134
  */
111
135
  dispatchRequest?(input: PluginDispatchInput): Promise<Response>;
136
+ /**
137
+ * Cheap account-state check used by the background recovery probe.
138
+ *
139
+ * Two-level probing exists because account state and reachability fail
140
+ * independently: a parked/over-quota account must NOT spend a network call
141
+ * (the Gemini lesson), while a healthy-but-unreachable account must not be
142
+ * restored to the model list on the strength of its state alone. The core
143
+ * calls this FIRST and only issues the (expensive) real request when it
144
+ * resolves.
145
+ *
146
+ * Resolve to use the `available` hint semantics below; throw a classified
147
+ * error (e.g. `*_account_cooling`) to report the account as unusable. When
148
+ * absent, the core falls back to `ensureUsable()` — which is the same check
149
+ * every real request already performs — so plugins that only implement the
150
+ * circuit breaker still work.
151
+ */
152
+ probeAccountState?(): Promise<ManagedProbeVerdict>;
153
+ /**
154
+ * Optional runtime-owned liveness probe (level 2).
155
+ *
156
+ * Called only after `probeAccountState`/`ensureUsable` reported the account as
157
+ * usable. Implementations should make the cheapest request that still proves
158
+ * the upstream answers for THIS account (a quota/status endpoint is ideal,
159
+ * otherwise a 1-token chat). Must honour the supplied signal/timeout and must
160
+ * not persist cooldowns that a probe is not allowed to set.
161
+ *
162
+ * When absent the core performs a generic 1-token request through
163
+ * `dispatchRequest` (see `PluginDispatchInput`).
164
+ */
165
+ probeLiveness?(input: ManagedProbeInput): Promise<Response>;
112
166
  /**
113
167
  * Opt-in 401 handling: when set, the core performs ONE forced refresh and
114
168
  * re-dispatch on 401 before classifying. `refresh` must persist rotated
@@ -57,9 +57,13 @@ export declare class ActiveProbeService {
57
57
  private resolveProxyUrl?;
58
58
  private logger?;
59
59
  private getConfig?;
60
+ /** Resolves the installed plugin runtime that owns a managed provider. */
61
+ private resolveManagedRuntime?;
62
+ /** Periodic timer for managed-subscription recovery probing. */
63
+ private managedProbeTimer?;
60
64
  private running;
61
65
  private probeTelemetry;
62
- constructor(getProviders: () => LLMProvider[], config?: ActiveProbeConfig, resolveProxyUrl?: (provider: LLMProvider) => string | undefined, logger?: any, getConfig?: (key: string) => any);
66
+ constructor(getProviders: () => LLMProvider[], config?: ActiveProbeConfig, resolveProxyUrl?: (provider: LLMProvider) => string | undefined, logger?: any, getConfig?: (key: string) => any, resolveManagedRuntime?: (provider: LLMProvider) => any);
63
67
  /**
64
68
  * Record the latest reachability probe result for a provider.
65
69
  * Only /models-style reachability probes (health, manual, rate-limit header
@@ -102,6 +106,43 @@ export declare class ActiveProbeService {
102
106
  * Rate-limited models are NOT probed by runHealthProbe — they have a separate timer
103
107
  * so they can be checked more frequently (every 2 minutes) without affecting normal probes.
104
108
  */
109
+ /**
110
+ * Recover circuit-open MANAGED (subscription) providers with one real request.
111
+ *
112
+ * runHealthProbe deliberately skips managed providers: their credentials are
113
+ * plugin-owned, so an unauthenticated GET /v1/models would fail for reasons
114
+ * unrelated to health. The consequence was that a subscription provider whose
115
+ * circuit opened (exhausted quota, revoked token, upstream outage) had NO
116
+ * background path back to healthy — it stayed circuit-open and the only way it
117
+ * ever recovered was a real user request hitting it again, which is exactly the
118
+ * "keeps failing and keeps being called" behaviour.
119
+ *
120
+ * This closes that loop: for each managed provider with a circuit-open model
121
+ * whose probe interval has elapsed, issue one minimal request through the
122
+ * plugin's own dispatcher (so token refresh, account rotation and the plugin's
123
+ * error taxonomy all apply) and restore its models only when the upstream
124
+ * actually answered.
125
+ */
126
+ runManagedRecoveryProbe(): Promise<void>;
127
+ /**
128
+ * Level 1 of the managed recovery probe: is the ACCOUNT usable at all?
129
+ *
130
+ * Returns false when the account is parked (quota exhausted / verification
131
+ * needed / risk-blocked), in which case the caller must skip the liveness call
132
+ * entirely — testing the network for an account that cannot answer is pure
133
+ * waste and, for subscription providers, a rate-limit risk.
134
+ */
135
+ private probeManagedAccountState;
136
+ /**
137
+ * Level 2 of the managed recovery probe: does the upstream actually answer?
138
+ *
139
+ * Uses a runtime-owned `probeLiveness` when the plugin provides one (a quota or
140
+ * status endpoint costs no chat quota), otherwise performs a generic 1-token
141
+ * request through the runtime's dispatcher. The dispatcher input carries a real
142
+ * `attempt()` and a per-attempt `config` — both are REQUIRED by
143
+ * `PluginDispatchInput`, and omitting them made wrappers fail unconditionally.
144
+ */
145
+ private probeManagedLiveness;
105
146
  private runRateLimitProbe;
106
147
  /**
107
148
  * Run scheduled wake-up for configured providers
@@ -122,11 +163,11 @@ export declare class ActiveProbeService {
122
163
  /**
123
164
  * Initialize and get the active probe service
124
165
  */
125
- export declare function getActiveProbeService(getProviders: () => LLMProvider[], config?: ActiveProbeConfig, resolveProxyUrl?: (provider: LLMProvider) => string | undefined, logger?: any, getConfig?: (key: string) => any): ActiveProbeService;
166
+ export declare function getActiveProbeService(getProviders: () => LLMProvider[], config?: ActiveProbeConfig, resolveProxyUrl?: (provider: LLMProvider) => string | undefined, logger?: any, getConfig?: (key: string) => any, resolveManagedRuntime?: (provider: LLMProvider) => any): ActiveProbeService;
126
167
  /**
127
168
  * Start the active probe service (convenience function)
128
169
  */
129
- export declare function startActiveProbe(getProviders: () => LLMProvider[], config?: ActiveProbeConfig, resolveProxyUrl?: (provider: LLMProvider) => string | undefined, logger?: any, getConfig?: (key: string) => any): ActiveProbeService;
170
+ export declare function startActiveProbe(getProviders: () => LLMProvider[], config?: ActiveProbeConfig, resolveProxyUrl?: (provider: LLMProvider) => string | undefined, logger?: any, getConfig?: (key: string) => any, resolveManagedRuntime?: (provider: LLMProvider) => any): ActiveProbeService;
130
171
  /**
131
172
  * Stop the active probe service
132
173
  */
@@ -76,6 +76,23 @@ export declare class ProviderHealthStore {
76
76
  * Get all models in fail pool (open status)
77
77
  */
78
78
  getFailPoolModels(): string[];
79
+ /**
80
+ * Stamp a background probe attempt without changing circuit state.
81
+ *
82
+ * Recovery probes need this: a failed probe must keep the model circuit-open
83
+ * but move `lastProbeTime` forward, otherwise the interval gate in
84
+ * isAvailable()/the probe loop would re-probe on every tick.
85
+ */
86
+ recordProbeAttempt(provider: string, model: string): void;
87
+ /**
88
+ * Park a model until the account's own reset epoch.
89
+ *
90
+ * A plugin that reports its quota window (e.g. `resets_at`) knows better than
91
+ * a fixed probe interval when the account can answer again. Setting
92
+ * `rateLimitUntil` keeps the model hidden from the list AND stops the recovery
93
+ * probe from spending network calls on a known-parked account.
94
+ */
95
+ markParkedUntil(provider: string, model: string, until: number): void;
79
96
  /**
80
97
  * Get all models in half-open state
81
98
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wengine-ai/llms",
3
- "version": "2.3.2417",
3
+ "version": "2.3.2418",
4
4
  "description": "A universal LLM API transformation server",
5
5
  "main": "dist/cjs/server.cjs",
6
6
  "module": "dist/esm/server.mjs",
@@ -64,7 +64,7 @@
64
64
  "uuid": "^11.1.0",
65
65
  "@fastify/multipart": "^9.0.0",
66
66
  "@fastify/static": "^8.2.0",
67
- "@wengine-ai/claude-code-router-shared": "^2.3.2417",
67
+ "@wengine-ai/claude-code-router-shared": "^2.3.2418",
68
68
  "adm-zip": "^0.5.16",
69
69
  "better-sqlite3": "^12.10.0",
70
70
  "rotating-file-stream": "^3.2.7"