@omnicross/subscriptions 0.1.0 → 0.1.3

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/index.d.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import { AccountTokensConfig } from '@omnicross/contracts/account-tokens-types';
2
- import { SubscriptionProviderId, SubscriptionStatusEntry, SubscriptionListEntry, OpenCodeGoTokenConfig, OpenCodeGoScenario, OpenCodeGoModelEntry } from '@omnicross/contracts/subscription-types';
2
+ import { SubscriptionProviderId, SubscriptionStatusEntry, SubscriptionListEntry, OpenCodeGoTokenConfig } from '@omnicross/contracts/subscription-types';
3
+ import { AuthStrategy } from '@omnicross/core/pipeline/SubscriptionAuthStrategy';
4
+ export { AuthApplyHints, AuthStrategy } from '@omnicross/core/pipeline/SubscriptionAuthStrategy';
5
+ import { SubscriptionDispatchProfile } from '@omnicross/core/provider-proxy/types';
6
+ export { SubscriptionDispatchProfile, SubscriptionRequestSummary } from '@omnicross/core/provider-proxy/types';
3
7
  import http from 'node:http';
4
- export { FetchLike, claudeOAuth, codexOAuth, geminiOAuth } from './oauth.js';
8
+ import { TransformerChainExecutor } from '@omnicross/core/transformer/TransformerChainExecutor';
9
+ import { TransformerService } from '@omnicross/core/transformer/TransformerService';
10
+ import { Transformer } from '@omnicross/core/transformer/types';
11
+ export { FetchLike } from '@omnicross/core/auth/GeminiCodeAssistProjectResolver';
12
+ export { claudeOAuth, codexOAuth, geminiOAuth } from './oauth.js';
5
13
 
6
14
  /**
7
15
  * SubscriptionCredentialStore — the credential port the subscription block
@@ -19,7 +27,15 @@ export { FetchLike, claudeOAuth, codexOAuth, geminiOAuth } from './oauth.js';
19
27
  * subscriptions layer).
20
28
  */
21
29
 
22
- /** The six methods the subscription block consumes from the host credential store. */
30
+ /**
31
+ * The credential surface the subscription block consumes from the host store.
32
+ *
33
+ * The six ORIGINAL methods (active-account getters + refreshers) are REQUIRED;
34
+ * the three by-id methods (subscription-account-scheduling, design D6) are
35
+ * OPTIONAL and feature-detected so every existing lightweight test double that
36
+ * implements only the six keeps compiling and the single-account active path is
37
+ * untouched.
38
+ */
23
39
  interface SubscriptionCredentialStore {
24
40
  /** Full decrypted account-tokens config (all subscription providers). */
25
41
  getFullConfig(): Promise<AccountTokensConfig>;
@@ -33,64 +49,124 @@ interface SubscriptionCredentialStore {
33
49
  refreshGeminiToken(): Promise<boolean>;
34
50
  /** Current valid OpenCodeGo static API key; `null` if none. */
35
51
  getValidOpenCodeGoApiKey(): Promise<string | null>;
52
+ /**
53
+ * Resolve a SPECIFIC account's access token by id (refreshing a near-expiry
54
+ * OAuth token for that account, mirroring the active getter's per-provider
55
+ * refresh policy). `null` when the account is unknown/expired/tokenless.
56
+ */
57
+ getAccessTokenForAccount?(providerId: SubscriptionProviderId, accountId: string): Promise<string | null>;
58
+ /**
59
+ * Force a refresh of a SPECIFIC account's OAuth token by id; `true` on success.
60
+ * Static-key providers (opencodego) return `false` — they don't refresh.
61
+ */
62
+ refreshAccountToken?(providerId: SubscriptionProviderId, accountId: string): Promise<boolean>;
63
+ /**
64
+ * Best-effort record of a selection's time onto the account's `lastUsedAt`
65
+ * (throttled by the selector so the hot path does not rewrite the store every
66
+ * request). Durability only — never affects which credential is valid.
67
+ */
68
+ touchAccountLastUsed?(providerId: SubscriptionProviderId, accountId: string, iso: string): Promise<void>;
36
69
  }
37
70
 
38
71
  /**
39
- * SubscriptionAuthStrategy — the pluggable subscription-auth contract, defined
40
- * in the serving core (`pipeline/`).
72
+ * SubscriptionAccountSelector — the pure, in-memory account scheduler
73
+ * (subscription-account-scheduling, design D3).
74
+ *
75
+ * Given a provider's account list (+ an optional session key) it decides WHICH
76
+ * account of the pool serves the outbound request:
41
77
  *
42
- * Each subscription provider in the `SubscriptionProviderRegistry` (the
43
- * subscriptions package) carries an `AuthStrategy` instance. The proxy calls
44
- * `applyHeaders` before issuing the upstream request and `onUnauthorized`
45
- * after a 401 to ask whether to retry.
78
+ * filter `schedulable !== false`
79
+ * session-affinity short-circuit (sticky `sessionKey accountId`, TTL 1h)
80
+ * sort by `priority` asc effective `lastUsedAt` asc (LRU) `createdAt` asc
81
+ * `[0]`.
46
82
  *
47
- * It is a PURE contract (no upstream semantics), defined down here so
48
- * `pipeline/SubscriptionAuthSource.ts` consumes it WITHOUT importing upward
49
- * (correct dependency direction). The subscriptions package's
50
- * `auth/AuthStrategy.ts` RE-EXPORTS this type, and its three concrete
51
- * strategies implement THIS interface so they remain assignable and no
52
- * downstream consumer's import path changes.
83
+ * A faithful port of CRS `sortAccountsByPriority` + `unifiedClaudeScheduler`
84
+ * session mapping. The class holds process-lived state — the affinity map and an
85
+ * in-memory `lastUsedAt` overlay (the authoritative live tie-break value, seeded
86
+ * from the persisted field and advanced to `now` on each selection so repeated
87
+ * selections round-robin fairly even before any durable persist). ONE instance is
88
+ * constructed at bootstrap and SHARED by all three auth strategies (each scopes
89
+ * its calls by `providerId`), mirroring the single long-lived CRS scheduler.
53
90
  *
54
- * NOTE: this `AuthApplyHints` (OPTIONAL fields) is the SUBSCRIPTION-side hint
55
- * shape and is intentionally distinct from `pipeline/AuthSource.ts`'s
56
- * `AuthApplyHints` (REQUIRED fields). They are not interchangeable; the
57
- * `SubscriptionAuthSource` adapter maps between them at the boundary.
91
+ * ZERO-REGRESSION CARRIER: `select()` returns `null` when the schedulable account
92
+ * count is 1. Callers treat `null` (and an `isActive` result) as "use the
93
+ * existing active-mirror path", so a single-account provider exercises NO new
94
+ * code and its token read is byte-identical to before this change.
58
95
  *
59
- * @module pipeline/SubscriptionAuthStrategy
96
+ * @module scheduler/SubscriptionAccountSelector
60
97
  */
61
98
 
62
- /** Hints the strategy may need to vary header formatting per request. */
63
- interface AuthApplyHints {
64
- /** Resolved upstream URL used by some strategies (e.g. OpenCodeGo) to
65
- * choose between Anthropic-shape and OpenAI-shape headers. */
66
- upstreamUrl?: string;
67
- /** Resolved model id — same purpose as `upstreamUrl`. */
68
- resolvedModel?: string;
69
- }
70
- interface AuthStrategy {
71
- /** Discriminator also surfaced through `subscription:list` to renderers. */
72
- readonly kind: 'pass-through' | 'oauth-bearer' | 'static-bearer';
73
- /** Stable id of the bound subscription provider. */
74
- readonly providerId: SubscriptionProviderId;
75
- /**
76
- * Inject any required authentication headers into the outbound request.
77
- * Implementations MAY refresh expiring tokens here (transparent refresh).
78
- *
79
- * Pass-through implementations are a no-op; the proxy's pass-through
80
- * code path preserves the SDK's own Authorization header instead.
81
- */
82
- applyHeaders(headers: Record<string, string>, hints?: AuthApplyHints): Promise<void>;
83
- /**
84
- * Called when the upstream returns 401. Return `true` to ask the proxy to
85
- * retry the request once with freshly-applied headers; return `false` to
86
- * surface the 401 immediately.
87
- *
88
- * Implementations SHOULD use a shared `RefreshMutex` to dedupe concurrent
89
- * refreshes so N parallel 401s collapse into one upstream refresh call.
90
- */
91
- onUnauthorized(): Promise<boolean>;
92
- /** Diagnostic surface for the `subscription:status` IPC. */
93
- describeStatus(): Promise<SubscriptionStatusEntry>;
99
+ /** Sticky session→account affinity TTL (CRS parity: 1 hour). */
100
+ declare const SESSION_AFFINITY_TTL_MS = 3600000;
101
+ /** Min gap between best-effort `lastUsedAt` durable persists, per account. */
102
+ declare const LAST_USED_PERSIST_THROTTLE_MS = 60000;
103
+ /** Precedence assumed for an account with no `priority` (CRS `|| 50`). */
104
+ declare const DEFAULT_ACCOUNT_PRIORITY = 50;
105
+ /** One schedulable candidate — the scheduling projection of an account entry. */
106
+ interface SchedulableAccount {
107
+ id: string;
108
+ /** Default `50` (lower = higher precedence). */
109
+ priority?: number;
110
+ /** ISO LRU tie-break; absent treated as least-recently-used (`0`). */
111
+ lastUsedAt?: string;
112
+ /** ISO — final tie-break; absent ⇒ oldest (`0`). */
113
+ createdAt?: string;
114
+ /** Default `true`; child #2 (account health) sets `false` for an unhealthy
115
+ * account and the selector skips it. */
116
+ schedulable?: boolean;
117
+ }
118
+ interface SelectInput {
119
+ providerId: SubscriptionProviderId;
120
+ accounts: readonly SchedulableAccount[];
121
+ /** The persistent active-account pointer (for the `isActive` discriminant). */
122
+ activeAccountId?: string;
123
+ /** Stable per-conversation key for session affinity; absent ⇒ pure priority/LRU. */
124
+ sessionKey?: string;
125
+ /** Injectable clock for tests (default `Date.now()`). */
126
+ now?: number;
127
+ }
128
+ interface SelectResult {
129
+ accountId: string;
130
+ /** `true` when the chosen account is the active one — the caller then runs the
131
+ * existing active-mirror getter verbatim (no by-id read). */
132
+ isActive: boolean;
133
+ }
134
+ declare class SubscriptionAccountSelector {
135
+ /** `providerId\0accountId → live lastUsedAt ms` (authoritative tie-break). */
136
+ private readonly lastUsedOverlay;
137
+ /** `providerId\0sessionKey → { accountId, expiresAt }`. */
138
+ private readonly affinity;
139
+ /** `providerId\0accountId → last durable-persist ms` (throttle state). */
140
+ private readonly lastPersist;
141
+ /**
142
+ * Choose the account to serve this request, or `null` when there are ≤ 1
143
+ * schedulable accounts (the zero-regression signal — caller uses the active
144
+ * account). Updates the live `lastUsedAt` overlay for the chosen account and,
145
+ * when a `sessionKey` is given, records/extends the affinity mapping.
146
+ */
147
+ select(input: SelectInput): SelectResult | null;
148
+ /**
149
+ * Drop every session-affinity mapping bound to this account (subscription-account-
150
+ * health, task 4.1). Called when a selected account's by-id token turns out
151
+ * null/invalid or an affinity-bound account becomes unhealthy, so the next
152
+ * selection for those sessions picks a fresh account instead of re-sticking to
153
+ * the bad one. O(affinity entries) — the map is tiny (one entry per live
154
+ * conversation).
155
+ */
156
+ evictAffinity(providerId: SubscriptionProviderId, accountId: string): void;
157
+ /**
158
+ * Whether a best-effort `lastUsedAt` durable persist is DUE for this account
159
+ * (≥ `LAST_USED_PERSIST_THROTTLE_MS` since the last one). Records the persist
160
+ * time when it returns `true`, so the strategy calls `touchAccountLastUsed`
161
+ * sparingly and the request hot path does not rewrite the store every request.
162
+ */
163
+ duePersist(providerId: SubscriptionProviderId, accountId: string, now?: number): boolean;
164
+ /** Sort by `priority` asc → effective `lastUsedAt` asc → `createdAt` asc → `[0]`. */
165
+ private pickOrdered;
166
+ /** The live tie-break value: the in-memory overlay when set, else the persisted
167
+ * `lastUsedAt` (0 when absent). */
168
+ private effectiveLastUsed;
169
+ private markUsed;
94
170
  }
95
171
 
96
172
  /**
@@ -107,6 +183,9 @@ interface AuthStrategy {
107
183
 
108
184
  declare class SubscriptionAccountService {
109
185
  private readonly mutex;
186
+ /** ONE account-pool scheduler (subscription-account-scheduling) shared by all
187
+ * four strategies so they share the affinity map + the `lastUsedAt` overlay. */
188
+ private readonly selector;
110
189
  private readonly strategies;
111
190
  constructor(tokens: SubscriptionCredentialStore);
112
191
  /** Returns the strategy bound to a subscription provider, or `null` for unknown ids. */
@@ -122,622 +201,6 @@ declare function setSubscriptionAccountService(svc: SubscriptionAccountService):
122
201
  * may run before main wiring on early-boot diagnostics). */
123
202
  declare function getSubscriptionAccountService(): SubscriptionAccountService | null;
124
203
 
125
- /**
126
- * TransformerService Types
127
- *
128
- * Type definitions for the Transformer system, providing unified
129
- * request/response formats and transformer chain configuration.
130
- *
131
- * @module transformer/types
132
- */
133
- /**
134
- * Thinking effort level for reasoning models
135
- */
136
- type ThinkLevel = 'none' | 'low' | 'medium' | 'high';
137
- /**
138
- * Thinking content with optional signature for extended context
139
- */
140
- interface ThinkingContent {
141
- content: string;
142
- signature?: string;
143
- }
144
- /**
145
- * Reasoning configuration for request
146
- */
147
- interface ReasoningConfig {
148
- /** OpenAI-style effort level */
149
- effort?: ThinkLevel;
150
- /** Anthropic-style max tokens for thinking */
151
- max_tokens?: number;
152
- /** Whether reasoning is enabled */
153
- enabled?: boolean;
154
- }
155
- /**
156
- * Text content block
157
- */
158
- interface TextContent {
159
- type: 'text';
160
- text: string;
161
- cache_control?: {
162
- type?: string;
163
- };
164
- }
165
- /**
166
- * Image content block
167
- */
168
- interface ImageContent {
169
- type: 'image_url';
170
- image_url: {
171
- url: string;
172
- };
173
- media_type?: string;
174
- }
175
- /**
176
- * Union of all content types
177
- */
178
- type MessageContent = TextContent | ImageContent;
179
- /**
180
- * Tool call definition
181
- */
182
- interface ToolCall {
183
- id: string;
184
- type: 'function';
185
- function: {
186
- name: string;
187
- arguments: string;
188
- };
189
- }
190
- /**
191
- * Unified message format that works across all providers
192
- */
193
- interface UnifiedMessage {
194
- role: 'user' | 'assistant' | 'system' | 'tool';
195
- content: string | null | MessageContent[];
196
- tool_calls?: ToolCall[];
197
- tool_call_id?: string;
198
- cache_control?: {
199
- type?: string;
200
- };
201
- thinking?: ThinkingContent;
202
- }
203
- /**
204
- * Tool parameter schema
205
- */
206
- interface ToolParameterSchema {
207
- type: 'object';
208
- properties: Record<string, unknown>;
209
- required?: string[];
210
- additionalProperties?: boolean;
211
- $schema?: string;
212
- }
213
- /**
214
- * Unified tool definition
215
- */
216
- interface UnifiedTool {
217
- type: 'function';
218
- function: {
219
- name: string;
220
- description: string;
221
- parameters: ToolParameterSchema;
222
- };
223
- }
224
- /**
225
- * Tool choice configuration
226
- */
227
- type ToolChoice = 'auto' | 'none' | 'required' | string | {
228
- type: 'function';
229
- function: {
230
- name: string;
231
- };
232
- };
233
- /**
234
- * Unified chat request format
235
- */
236
- interface UnifiedChatRequest {
237
- messages: UnifiedMessage[];
238
- model: string;
239
- max_tokens?: number;
240
- temperature?: number;
241
- stream?: boolean;
242
- tools?: UnifiedTool[];
243
- tool_choice?: ToolChoice;
244
- reasoning?: ReasoningConfig;
245
- /**
246
- * Internal-only routing metadata. Populated by callers (CompletionService /
247
- * the host's engine adapters and proxy servers) so the usage-capture hook can attribute
248
- * the recorded event to a chat message, session, engine origin, and api key.
249
- *
250
- * **Must not be serialised into the outbound HTTP body.** The transformer chain
251
- * passes this object alongside the request internally; transformers that build
252
- * provider request payloads must drop `meta` from their output.
253
- */
254
- meta?: UnifiedChatRequestMeta;
255
- }
256
- /**
257
- * Routing metadata threaded through the transformer chain so the usage-capture
258
- * hook can identify which chat message / session / api key produced the call.
259
- * See `UnifiedChatRequest.meta`.
260
- */
261
- interface UnifiedChatRequestMeta {
262
- /** Host message id of the assistant message being produced, when known. */
263
- messageId?: string | null;
264
- /** Parent assistant message id (for subagent dispatches that have a parent). */
265
- parentMessageId?: string | null;
266
- /** Host session id, when applicable. */
267
- sessionId?: string | null;
268
- /** Which engine path is making the call. See `UsageEngineOrigin`. */
269
- engineOrigin?: 'completion' | 'claude-sdk' | 'claude-sdk-direct' | 'codex-ingress' | (string & {});
270
- /** The api-key-pool entry id selected for this request. */
271
- apiKeyId?: string | null;
272
- }
273
- /**
274
- * LLM Provider configuration (runtime format)
275
- */
276
- interface LLMProvider {
277
- name: string;
278
- baseUrl: string;
279
- apiKey: string;
280
- models: string[];
281
- transformer?: TransformerChainConfig;
282
- /**
283
- * Resolved Google **Code Assist** project id, threaded onto the transformer
284
- * provider object by the subscription dispatch seam (SubscriptionDispatcher /
285
- * openaiResponsesIngress) so `GeminiCodeAssistTransformer.transformRequestIn`
286
- * can embed it in the top-level envelope. `undefined` is the valid fresh
287
- * free-tier value (sending a project on free/legacy-tier → Precondition
288
- * Failed). Only ever set for the gemini subscription profile; inert otherwise.
289
- */
290
- geminiProject?: string;
291
- }
292
- /**
293
- * Transformer options passed during instantiation
294
- */
295
- interface TransformerOptions {
296
- [key: string]: unknown;
297
- }
298
- /**
299
- * Context passed through transformer chain
300
- */
301
- interface TransformerContext {
302
- /** Original request object */
303
- req?: unknown;
304
- /** Logger instance */
305
- logger?: TransformerLogger;
306
- /** Provider name */
307
- providerName?: string;
308
- /** Additional context data */
309
- [key: string]: unknown;
310
- }
311
- /**
312
- * Logger interface for transformers
313
- */
314
- interface TransformerLogger {
315
- debug(message: string, ...args: unknown[]): void;
316
- info(message: string, ...args: unknown[]): void;
317
- warn(message: string, ...args: unknown[]): void;
318
- error(message: string, ...args: unknown[]): void;
319
- }
320
- /**
321
- * Transformer interface - defines the contract for all transformers
322
- */
323
- interface Transformer {
324
- /** Unique name for the transformer */
325
- name?: string;
326
- /** API endpoint path (e.g., '/v1/chat/completions') */
327
- endPoint?: string;
328
- /** Logger instance */
329
- logger?: TransformerLogger;
330
- /**
331
- * Transform incoming request (provider-specific → unified format)
332
- * Called when request enters the transformer chain
333
- */
334
- transformRequestIn?: (request: UnifiedChatRequest, provider: LLMProvider, context: TransformerContext) => Promise<Record<string, unknown>>;
335
- /**
336
- * Transform outgoing request (unified → provider-specific format)
337
- * Called before sending to LLM provider
338
- */
339
- transformRequestOut?: (request: unknown, context: TransformerContext) => Promise<UnifiedChatRequest>;
340
- /**
341
- * Transform incoming response (provider-specific → unified format)
342
- * Called after receiving response from provider
343
- */
344
- transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;
345
- /**
346
- * Transform outgoing response (unified → client format)
347
- * Called before returning response to client
348
- */
349
- transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;
350
- /**
351
- * Handle authentication for the request
352
- */
353
- auth?: (request: unknown, provider: LLMProvider, context: TransformerContext) => Promise<unknown>;
354
- }
355
- /**
356
- * Transformer constructor interface
357
- */
358
- interface TransformerConstructor {
359
- new (options?: TransformerOptions): Transformer;
360
- TransformerName?: string;
361
- }
362
- /**
363
- * Individual transformer reference in chain
364
- * Can be a string name or [name, options] tuple
365
- */
366
- type TransformerReference = string | [string, TransformerOptions];
367
- /**
368
- * Model-specific transformer configuration
369
- */
370
- interface ModelTransformerConfig {
371
- use?: TransformerReference[];
372
- }
373
- /**
374
- * Transformer chain configuration for a provider
375
- */
376
- interface TransformerChainConfig {
377
- /** Provider-level transformers applied to all models */
378
- use?: TransformerReference[];
379
- /** Model-specific transformer overrides */
380
- [modelName: string]: ModelTransformerConfig | TransformerReference[] | undefined;
381
- }
382
- /**
383
- * Resolved transformer chain (instances ready to execute)
384
- */
385
- interface ResolvedTransformerChain {
386
- /** Provider-level transformer instances */
387
- providerTransformers: Transformer[];
388
- /** Model-specific transformer instances */
389
- modelTransformers: Transformer[];
390
- }
391
- /**
392
- * Result of request transformation
393
- */
394
- interface TransformRequestResult {
395
- /** Transformed request body */
396
- requestBody: unknown;
397
- /** Additional request configuration */
398
- config: RequestConfig;
399
- /** Whether to bypass further transformations */
400
- bypass: boolean;
401
- }
402
- /**
403
- * Request configuration for HTTP call
404
- */
405
- interface RequestConfig {
406
- /** Request URL */
407
- url?: URL | string;
408
- /** Request headers */
409
- headers?: Record<string, string>;
410
- /** Additional fetch options */
411
- [key: string]: unknown;
412
- }
413
-
414
- /**
415
- * TransformerChainExecutor - Executes transformer chains for requests and responses
416
- *
417
- * Implements the transformer pipeline:
418
- * Request: transformRequestOut 鈫?Provider transformers 鈫?Model transformers 鈫?HTTP
419
- * Response: Model transformers (reverse) 鈫?Provider transformers (reverse) 鈫?transformResponseIn
420
- *
421
- * @module transformer/TransformerChainExecutor
422
- */
423
-
424
- /**
425
- * Options for chain execution
426
- */
427
- interface ChainExecutionOptions {
428
- /** The primary/endpoint transformer */
429
- endpointTransformer?: Transformer;
430
- /** Request headers */
431
- headers?: Record<string, string> | Headers;
432
- /** Logger instance */
433
- logger?: TransformerLogger;
434
- /**
435
- * 1M-context opt-in. When `enabled` is true and the post-chain request body
436
- * looks Anthropic-shaped (has a `messages` array), the executor injects
437
- * `'context-1m-2025-08-07'` into `body.anthropic_beta`. Capability gating
438
- * (model id allowlist) lives inside `injectExtendedContextBeta` itself.
439
- */
440
- extendedContext?: {
441
- enabled: boolean;
442
- model: string;
443
- };
444
- }
445
- /**
446
- * TransformerChainExecutor handles the execution of transformer chains.
447
- *
448
- * The execution flow follows the llms project pattern:
449
- *
450
- * Request Phase:
451
- * 1. transformRequestOut (endpoint transformer) - Convert to unified format
452
- * 2. Provider transformers [].transformRequestIn (forward order)
453
- * 3. Model transformers [].transformRequestIn (forward order)
454
- *
455
- * Response Phase:
456
- * 4. Model transformers [].transformResponseOut (reverse order)
457
- * 5. Provider transformers [].transformResponseOut (reverse order)
458
- * 6. transformResponseIn (endpoint transformer) - Convert to client format
459
- */
460
- declare class TransformerChainExecutor {
461
- private logger;
462
- constructor(logger?: TransformerLogger);
463
- /**
464
- * Execute the request transformation chain
465
- *
466
- * @param request - Original request body
467
- * @param provider - LLM provider configuration
468
- * @param chain - Resolved transformer chain
469
- * @param options - Execution options
470
- * @returns Transformed request result
471
- */
472
- executeRequestChain(request: unknown, provider: LLMProvider, chain: ResolvedTransformerChain, options?: ChainExecutionOptions): Promise<TransformRequestResult>;
473
- /**
474
- * Execute the response transformation chain
475
- *
476
- * @param request - Original request (for context)
477
- * @param response - Response from provider
478
- * @param provider - LLM provider configuration
479
- * @param chain - Resolved transformer chain
480
- * @param options - Execution options
481
- * @returns Transformed response
482
- */
483
- executeResponseChain(request: UnifiedChatRequest, response: Response, provider: LLMProvider, chain: ResolvedTransformerChain, options?: ChainExecutionOptions): Promise<Response>;
484
- /**
485
- * Execute authentication handler if available
486
- *
487
- * @param request - Request body
488
- * @param provider - LLM provider
489
- * @param endpointTransformer - Endpoint transformer with auth handler
490
- * @param context - Transformer context
491
- * @returns Auth result with potentially modified request and config
492
- */
493
- executeAuth(request: unknown, provider: LLMProvider, endpointTransformer: Transformer | undefined, context: TransformerContext): Promise<{
494
- requestBody: unknown;
495
- config: RequestConfig;
496
- }>;
497
- /**
498
- * Check if transformers should be bypassed (optimization)
499
- *
500
- * Bypass is enabled when:
501
- * - Provider has only one transformer that matches the endpoint transformer
502
- * - Model has no specific transformers or only the same endpoint transformer
503
- */
504
- private shouldBypassTransformers;
505
- /**
506
- * Clean headers for pass-through
507
- */
508
- private cleanHeaders;
509
- /**
510
- * Get error message from unknown error
511
- */
512
- private getErrorMessage;
513
- }
514
-
515
- /**
516
- * TransformerService - Core service for managing transformer lifecycle
517
- *
518
- * Handles transformer registration, retrieval, and initialization.
519
- *
520
- * Adapted from the `musistudio/llms` project (MIT, © 2025 musistudio) — the
521
- * transformer IR upstream — for the Electron environment. See the package
522
- * `NOTICE` for full third-party attribution.
523
- *
524
- * @module transformer/TransformerService
525
- */
526
-
527
- /**
528
- * TransformerService manages the lifecycle of all transformers.
529
- *
530
- * Features:
531
- * - Register transformers by name
532
- * - Get transformer by name
533
- * - Initialize default transformers
534
- * - Resolve transformer chains from configuration
535
- */
536
- declare class TransformerService {
537
- private transformers;
538
- private logger;
539
- private instanceCache;
540
- private endpointTransformersCache;
541
- private noEndpointTransformersCache;
542
- constructor(logger?: TransformerLogger);
543
- /**
544
- * Clear instance caches
545
- */
546
- clearInstanceCache(): void;
547
- /**
548
- * Get cache statistics for diagnostics
549
- */
550
- getCacheStats(): {
551
- instanceCacheSize: number;
552
- hasEndpointCache: boolean;
553
- };
554
- /**
555
- * Register a transformer by name
556
- * @param name - Unique transformer name
557
- * @param transformer - Transformer instance or constructor
558
- */
559
- registerTransformer(name: string, transformer: Transformer | TransformerConstructor): void;
560
- /**
561
- * Get a transformer by name
562
- * @param name - Transformer name
563
- * @returns Transformer instance/constructor or undefined
564
- */
565
- getTransformer(name: string): Transformer | TransformerConstructor | undefined;
566
- /**
567
- * Get all registered transformers
568
- * @returns Map of all transformers
569
- */
570
- getAllTransformers(): Map<string, Transformer | TransformerConstructor>;
571
- /**
572
- * Get transformers that have an endpoint defined
573
- * Uses cache to avoid redundant traversal
574
- * @returns Array of transformers with endpoints
575
- */
576
- getTransformersWithEndpoint(): {
577
- name: string;
578
- transformer: Transformer;
579
- }[];
580
- /**
581
- * Get transformers without endpoints
582
- * Uses cache to avoid redundant traversal
583
- * @returns Array of transformers without endpoints
584
- */
585
- getTransformersWithoutEndpoint(): {
586
- name: string;
587
- transformer: Transformer;
588
- }[];
589
- /**
590
- * Remove a transformer by name
591
- * @param name - Transformer name
592
- * @returns true if removed, false if not found
593
- */
594
- removeTransformer(name: string): boolean;
595
- /**
596
- * Check if a transformer is registered
597
- * @param name - Transformer name
598
- * @returns true if registered
599
- */
600
- hasTransformer(name: string): boolean;
601
- /**
602
- * Initialize the service with default transformers
603
- * @param defaultTransformers - Map or object of default transformers
604
- */
605
- initialize(defaultTransformers?: Record<string, Transformer | TransformerConstructor>): Promise<void>;
606
- /**
607
- * Register default transformers from a map
608
- * @param transformers - Map of transformer name to transformer
609
- */
610
- private registerDefaultTransformers;
611
- /**
612
- * Resolve a transformer chain configuration to actual transformer instances
613
- * @param chainConfig - Transformer chain configuration
614
- * @param modelName - Model name for model-specific transformers
615
- * @returns Resolved transformer chain with instances
616
- */
617
- resolveTransformerChain(chainConfig: TransformerChainConfig | undefined, modelName?: string): ResolvedTransformerChain;
618
- /**
619
- * Resolve an array of transformer references to instances
620
- * @param refs - Array of transformer references
621
- * @returns Array of transformer instances
622
- */
623
- resolveTransformerReferences(refs: TransformerReference[]): Transformer[];
624
- /**
625
- * Instantiate a transformer if it's a constructor
626
- * Uses instance cache to avoid redundant instantiation
627
- *
628
- * @param transformer - Transformer instance or constructor
629
- * @param options - Options to pass to constructor
630
- * @returns Transformer instance
631
- */
632
- private instantiateIfNeeded;
633
- /**
634
- * Check if a value is a transformer constructor
635
- * @param value - Value to check
636
- * @returns true if it's a constructor
637
- */
638
- private isTransformerConstructor;
639
- /**
640
- * Get list of all registered transformer names
641
- * @returns Array of transformer names
642
- */
643
- getTransformerNames(): string[];
644
- /**
645
- * Clear all registered transformers
646
- */
647
- clear(): void;
648
- }
649
-
650
- /**
651
- * Shared contract types for the resident `ProviderProxy`.
652
- *
653
- * The `ProviderProxy` (OpenSpec `engine-provider-decouple`, design D0/D3/D7/D9)
654
- * is the single resident `127.0.0.1` listener that subsumes both of the
655
- * host's per-session proxies (Anthropic Messages ingress and OpenAI Responses
656
- * ingress). Per-run state lives in a `Map<token, RouteContext>`
657
- * minted at run start and reaped at run end / on idle TTL.
658
- *
659
- * These types are kept in their own module so the server, route map, router,
660
- * and the two ingress parsers can share shapes without importing one another
661
- * for type-only purposes.
662
- *
663
- * @module provider-proxy/types
664
- */
665
-
666
- /** Lightweight summary derived from the inbound Anthropic request body —
667
- * consumed by `modelMapper` (scenario routing) without full body access. */
668
- interface SubscriptionRequestSummary {
669
- messageCount: number;
670
- /** cl100k_base-estimated token count of system + messages (no tools). */
671
- estimatedInputTokens: number;
672
- /**
673
- * OPTIONAL bounded per-message text slice (system prompt + the most recent
674
- * user/system messages, each per-message-capped) consumed ONLY by the
675
- * OpenCodeGo keyword matcher in `@omnicross/subscriptions`
676
- * (`resolveOpenCodeGoScenario`). Core only WRITES this `string[]`; it never
677
- * reads it and never names the matcher — keeping the cross-layer litmus at 0
678
- * (no `@omnicross/core` → `@omnicross/subscriptions` edge). Optional so callers
679
- * that omit it (legacy/tests) compile and degrade to the token-threshold +
680
- * `default` routing.
681
- */
682
- matchText?: string[];
683
- }
684
- interface SubscriptionDispatchProfile {
685
- readonly providerId: SubscriptionProviderId;
686
- readonly displayName: string;
687
- readonly authStrategy: AuthStrategy;
688
- /** Pass-through providers (Claude) skip the transformer chain entirely.
689
- * Transformer providers use the chain below + the proxy's existing
690
- * `AnthropicTransformer` endpoint reverse-decoder. */
691
- readonly mode: 'pass-through' | 'transformer';
692
- /** Resolve the upstream URL for a given resolved model id. Required for
693
- * `mode === 'transformer'`; unused for pass-through (proxy hard-codes
694
- * `api.anthropic.com`). The OPTIONAL 2nd `config` arg lets the opencodego
695
- * profile honor a per-account `baseUrl` override (D1) — additive, so existing
696
- * one-arg callers compile unchanged. */
697
- readonly resolveUpstreamUrl?: (resolvedModel: string, config?: OpenCodeGoTokenConfig) => string;
698
- /** Names of transformers (registered in `TransformerService`) to run on the
699
- * provider chain. The proxy adds `AnthropicTransformer` as the endpoint
700
- * reverse-decoder. */
701
- readonly providerTransformerNames?: readonly string[];
702
- readonly modelTransformerNames?: readonly string[];
703
- /**
704
- * OPTIONAL shape-aware provider transformer-name resolver (opencodego zen).
705
- * Parallel to `resolveUpstreamUrl(model, config)`: lets a profile vary its
706
- * provider chain by the RESOLVED model's wire shape (e.g. zen `responses` ⇒
707
- * `['openai-response']`, `gemini` ⇒ `['gemini']`). `config` is `unknown` on the
708
- * core side (opaque-config discipline — core never names
709
- * `OpenCodeGoTokenConfig` from `@omnicross/subscriptions`); the subscriptions
710
- * implementation narrows it. When ABSENT, both ingress paths fall back to the
711
- * static `providerTransformerNames` — BYTE-IDENTICAL for claude / codex / gemini
712
- * (which leave this unset). Purely additive (optional).
713
- */
714
- readonly resolveProviderTransformerNames?: (model: string, config?: unknown) => readonly string[];
715
- /** Optional model placeholder rewriter — only set for OpenCodeGo. */
716
- readonly modelMapper?: (sdkModel: string, summary: SubscriptionRequestSummary, config: OpenCodeGoTokenConfig | undefined) => {
717
- resolvedModel: string;
718
- scenario: OpenCodeGoScenario;
719
- };
720
- /** Optional fallback resolver — for OpenCodeGo, picks the next model after
721
- * an unrecoverable error. Returns `null` when exhausted. Cap = 3. The
722
- * opencodego implementation ALSO consults the circuit breaker (D5): it skips
723
- * models whose circuit is open. */
724
- readonly nextFallback?: (scenario: OpenCodeGoScenario, attempted: readonly string[], config: OpenCodeGoTokenConfig | undefined) => OpenCodeGoModelEntry | null;
725
- /** Optional circuit-breaker admission gate for the PRIMARY (mapped) model
726
- * (D5 primary-gating). Only set for OpenCodeGo. Returns whether `modelId`'s
727
- * circuit currently admits a request (side-effecting: flips an `open` model
728
- * to `half-open` once its window elapses, exactly like `nextFallback`'s
729
- * internal consult). `nextFallback` covers the FALLBACKS; this covers the
730
- * primary the loop already holds. Absent/undefined ⇒ the loop treats the
731
- * primary as always admitted (claude / codex / gemini have no breaker). */
732
- readonly allowModel?: (modelId: string) => boolean;
733
- /** Optional record-outcome callback (D5 record seam). Only set for OpenCodeGo.
734
- * Both fallback loops invoke it after each attempt: `ok: true` on a `2xx`,
735
- * `ok: false` on a thrown/network error / `5xx` / `429`; a non-429 `4xx` is
736
- * NEUTRAL and the loops MUST NOT call it. Drives the per-model breaker.
737
- * Absent/undefined for claude / codex / gemini ⇒ a no-op (no breaker). */
738
- readonly recordModelOutcome?: (modelId: string, ok: boolean) => void;
739
- }
740
-
741
204
  /**
742
205
  * SubscriptionProviderRegistry — fixed catalog of subscription dispatch profiles.
743
206
  *
@@ -802,7 +265,17 @@ interface DispatcherHooks {
802
265
  readonly executor: TransformerChainExecutor;
803
266
  /** Shared transformer service registry — looks up transformer-by-name. */
804
267
  readonly transformerService: TransformerService;
805
- /** Fetch + retry helper from the proxy (semaphore, 429/5xx loop). */
268
+ /**
269
+ * Fetch + retry helper from the proxy (semaphore, 429/5xx loop). On a non-ok
270
+ * upstream it throws a `ProviderApiError`-shaped error carrying `.status`.
271
+ *
272
+ * ACCOUNT-HEALTH CONTRACT (subscription-account-health): for the daemon-path
273
+ * 429-reset cooldown + 403-ban sniff to function, the thrown error SHOULD also
274
+ * carry the upstream response `headers` (a `Headers` or a plain record) and, on
275
+ * a 403, a bounded `bodyText`/`body` string. The dispatcher reads them
276
+ * STRUCTURALLY (`errHeaders`/`errBodyText`) — absent ⇒ the account-health mark
277
+ * gracefully degrades to a bare-429 (unmarked, lazy re-probe), never an error.
278
+ */
806
279
  fetchWithRetry(url: string, headers: Record<string, string>, body: unknown, model: string): Promise<Response>;
807
280
  /** Forward the upstream response to the SDK + tap usage. */
808
281
  writeProxyResponse(res: http.ServerResponse, providerResponse: Response, isStream: boolean, reqId?: number): Promise<void>;
@@ -851,6 +324,17 @@ declare class SubscriptionDispatcher {
851
324
  */
852
325
  private maybeRetryAfterError;
853
326
  private applyHeadersWithRetry;
327
+ /**
328
+ * Mark the served account's health against ONE attempt's outcome
329
+ * (subscription-account-health, task 3.4). No-op when no account was reported
330
+ * (non-pooled / single-account) or on a session-cancel (status 0). On a caught
331
+ * error `err` is passed so the 429-reset / 403-ban drivers are read STRUCTURALLY
332
+ * from the error's upstream `headers` + `bodyText` (the `fetchWithRetry`
333
+ * contract) — so daemon-path 429 cooldown + ban blocking function for
334
+ * multi-account codex/gemini/opencodego pools; absent headers ⇒ a bare-429
335
+ * (unmarked, lazy re-probe). Success (2xx) clears; 401/5xx/thrown → transient.
336
+ */
337
+ private markHealth;
854
338
  /**
855
339
  * Resolve the Code Assist project for the gemini subscription profile. Pulls
856
340
  * the Bearer the bound `AuthStrategy` would inject (so the strategy stays the
@@ -865,4 +349,4 @@ declare class SubscriptionDispatcher {
865
349
  private buildRequestSummary;
866
350
  }
867
351
 
868
- export { type AuthApplyHints, type AuthStrategy, type DispatchRequest, type DispatcherHooks, SubscriptionAccountService, type SubscriptionCredentialStore, type SubscriptionDispatchProfile, SubscriptionDispatcher, SubscriptionProviderRegistry, type SubscriptionRequestSummary, getSubscriptionAccountService, getSubscriptionProviderRegistry, setSubscriptionAccountService, setSubscriptionProviderRegistry };
352
+ export { DEFAULT_ACCOUNT_PRIORITY, type DispatchRequest, type DispatcherHooks, LAST_USED_PERSIST_THROTTLE_MS, SESSION_AFFINITY_TTL_MS, type SchedulableAccount, type SelectInput, type SelectResult, SubscriptionAccountSelector, SubscriptionAccountService, type SubscriptionCredentialStore, SubscriptionDispatcher, SubscriptionProviderRegistry, getSubscriptionAccountService, getSubscriptionProviderRegistry, setSubscriptionAccountService, setSubscriptionProviderRegistry };