@plumbus/core 0.5.0 → 0.6.0

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.
Files changed (82) hide show
  1. package/README.md +1 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/ai/ai/cost-tracker.d.ts +93 -0
  4. package/dist/ai/ai/derive-ledger-usage.d.ts +10 -0
  5. package/dist/ai/ai/model-pricing.d.ts +51 -0
  6. package/dist/ai/ai/provider.d.ts +214 -0
  7. package/dist/ai/ai/refusal.d.ts +27 -0
  8. package/dist/ai/ai/usage-client.d.ts +30 -0
  9. package/dist/ai/ai-service.d.ts +1 -1
  10. package/dist/ai/ai-service.d.ts.map +1 -1
  11. package/dist/ai/ai-service.js +21 -16
  12. package/dist/ai/ai-service.js.map +1 -1
  13. package/dist/ai/cost-tracker.d.ts +17 -1
  14. package/dist/ai/cost-tracker.d.ts.map +1 -1
  15. package/dist/ai/cost-tracker.js +8 -6
  16. package/dist/ai/cost-tracker.js.map +1 -1
  17. package/dist/ai/derive-ledger-usage.d.ts +11 -0
  18. package/dist/ai/derive-ledger-usage.d.ts.map +1 -0
  19. package/dist/ai/derive-ledger-usage.js +36 -0
  20. package/dist/ai/derive-ledger-usage.js.map +1 -0
  21. package/dist/ai/index.d.ts +1 -0
  22. package/dist/ai/index.d.ts.map +1 -1
  23. package/dist/ai/index.js +1 -0
  24. package/dist/ai/index.js.map +1 -1
  25. package/dist/ai/types/config.d.ts +72 -0
  26. package/dist/ai/types/enums.d.ts +73 -0
  27. package/dist/cli/cli.d.ts.map +1 -1
  28. package/dist/cli/cli.js +2 -1
  29. package/dist/cli/cli.js.map +1 -1
  30. package/dist/cli/commands/flow-schedule-list.d.ts +31 -0
  31. package/dist/cli/commands/flow-schedule-list.d.ts.map +1 -0
  32. package/dist/cli/commands/flow-schedule-list.js +93 -0
  33. package/dist/cli/commands/flow-schedule-list.js.map +1 -0
  34. package/dist/cli/commands/flow.d.ts.map +1 -1
  35. package/dist/cli/commands/flow.js +26 -0
  36. package/dist/cli/commands/flow.js.map +1 -1
  37. package/dist/cli/commands/index.d.ts +1 -0
  38. package/dist/cli/commands/index.d.ts.map +1 -1
  39. package/dist/cli/commands/index.js +1 -0
  40. package/dist/cli/commands/index.js.map +1 -1
  41. package/dist/cli/commands/init.d.ts +1 -1
  42. package/dist/cli/commands/init.d.ts.map +1 -1
  43. package/dist/cli/commands/init.js +43 -2
  44. package/dist/cli/commands/init.js.map +1 -1
  45. package/dist/cli/commands/voice.d.ts +12 -0
  46. package/dist/cli/commands/voice.d.ts.map +1 -0
  47. package/dist/cli/commands/voice.js +109 -0
  48. package/dist/cli/commands/voice.js.map +1 -0
  49. package/dist/cli/mcp-serve-context.d.ts.map +1 -1
  50. package/dist/cli/mcp-serve-context.js +133 -126
  51. package/dist/cli/mcp-serve-context.js.map +1 -1
  52. package/dist/cli/voice-agent-bootstrap.d.ts +22 -0
  53. package/dist/cli/voice-agent-bootstrap.d.ts.map +1 -0
  54. package/dist/cli/voice-agent-bootstrap.js +12 -0
  55. package/dist/cli/voice-agent-bootstrap.js.map +1 -0
  56. package/dist/cli/voice-serve-context.d.ts +7 -0
  57. package/dist/cli/voice-serve-context.d.ts.map +1 -0
  58. package/dist/cli/voice-serve-context.js +26 -0
  59. package/dist/cli/voice-serve-context.js.map +1 -0
  60. package/dist/execution/context-factory.d.ts.map +1 -1
  61. package/dist/execution/context-factory.js +6 -0
  62. package/dist/execution/context-factory.js.map +1 -1
  63. package/dist/flows/engine.d.ts.map +1 -1
  64. package/dist/flows/engine.js +2 -0
  65. package/dist/flows/engine.js.map +1 -1
  66. package/dist/index.d.ts +2 -2
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/server/bootstrap.d.ts.map +1 -1
  71. package/dist/server/bootstrap.js +6 -0
  72. package/dist/server/bootstrap.js.map +1 -1
  73. package/dist/testing/context.d.ts.map +1 -1
  74. package/dist/testing/context.js +2 -0
  75. package/dist/testing/context.js.map +1 -1
  76. package/dist/testing/e2e.d.ts +6 -0
  77. package/dist/testing/e2e.d.ts.map +1 -1
  78. package/dist/testing/e2e.js +2 -0
  79. package/dist/testing/e2e.js.map +1 -1
  80. package/dist/types/context.d.ts +15 -0
  81. package/dist/types/context.d.ts.map +1 -1
  82. package/package.json +8 -4
@@ -0,0 +1,93 @@
1
+ import type { TokenUsage } from './provider.js';
2
+ import type { UsageAPIClient } from './usage-client.js';
3
+ export interface AICostRecord {
4
+ id: string;
5
+ timestamp: Date;
6
+ model: string;
7
+ provider: string;
8
+ promptName?: string;
9
+ operation: 'generate' | 'extract' | 'classify' | 'embed' | 'transcribe' | 'synthesize' | 'transport';
10
+ /**
11
+ * True when the call originally streamed but fell back to a non-streaming
12
+ * retry after the streamed text failed JSON/schema validation. Both the
13
+ * original streamed attempt and the fallback attempt are billed, so this
14
+ * flag is the signal consumers use to detect duplicate billing for a
15
+ * single logical generation.
16
+ */
17
+ fallbackUsed?: boolean;
18
+ usage: TokenUsage;
19
+ /**
20
+ * Optional provider-native media billing units for non-token workloads such
21
+ * as speech-to-text, text-to-speech, or realtime transport.
22
+ */
23
+ mediaUsage?: {
24
+ audioInputSeconds?: number;
25
+ audioOutputSeconds?: number;
26
+ characters?: number;
27
+ connectionMinutes?: number;
28
+ participantMinutes?: number;
29
+ };
30
+ /** Actual cost from provider API, or null if API unavailable */
31
+ cost: number | null;
32
+ latencyMs: number;
33
+ tenantId?: string;
34
+ actor?: string;
35
+ /**
36
+ * Whether the underlying provider call completed successfully. Failed rows
37
+ * still represent real provider-side spend and count toward budget caps.
38
+ * Defaults to 'success' inside `createCostTracker` when the caller does
39
+ * not set it, preserving pre-0.3.0 behavior for existing consumers.
40
+ */
41
+ status: 'success' | 'failed' | 'refused' | 'incomplete';
42
+ /** Short description of the failure when `status !== 'success'`. */
43
+ errorMessage?: string;
44
+ }
45
+ export interface BudgetConfig {
46
+ /** Max tokens per single request */
47
+ maxTokensPerRequest?: number;
48
+ /** Daily cost limit in USD */
49
+ dailyCostLimit?: number;
50
+ /** Per-tenant daily cost limit in USD */
51
+ perTenantDailyLimit?: number;
52
+ }
53
+ /**
54
+ * Input type for {@link CostTracker.record}. `status` and `errorMessage` are
55
+ * optional so pre-0.3.0 call sites keep compiling; the tracker defaults
56
+ * `status` to `'success'` internally.
57
+ */
58
+ export type AICostRecordInput = Omit<AICostRecord, 'id' | 'timestamp' | 'status'> & {
59
+ status?: AICostRecord['status'];
60
+ };
61
+ export interface CostTracker {
62
+ record(entry: AICostRecordInput): void;
63
+ checkBudget(config: {
64
+ tenantId?: string;
65
+ estimatedTokens?: number;
66
+ /**
67
+ * Optional normalized USD estimate for non-token workloads. Voice/media
68
+ * layers can compute this ahead of time and pre-check shared daily caps.
69
+ */
70
+ estimatedCostUsd?: number;
71
+ }): BudgetCheckResult;
72
+ getDailyUsage(tenantId?: string): DailyUsage;
73
+ getRecords(): AICostRecord[];
74
+ /** Fetch actual costs from provider usage APIs and update records */
75
+ syncCosts?(): Promise<UsageSyncResult>;
76
+ }
77
+ export interface BudgetCheckResult {
78
+ allowed: boolean;
79
+ reason?: string;
80
+ }
81
+ export interface DailyUsage {
82
+ totalTokens: number;
83
+ totalCost: number;
84
+ /** Whether cost data is available from provider APIs */
85
+ costAvailable: boolean;
86
+ requestCount: number;
87
+ }
88
+ export interface UsageSyncResult {
89
+ synced: boolean;
90
+ totalCost?: number;
91
+ error?: string;
92
+ }
93
+ export declare function createCostTracker(budget?: BudgetConfig, usageClients?: UsageAPIClient[]): CostTracker;
@@ -0,0 +1,10 @@
1
+ import type { AICostRecord } from './cost-tracker.js';
2
+ export type LedgerUsageKind = 'llm_tokens' | 'audio_seconds' | 'audio_output_seconds' | 'characters' | 'participant_minutes' | 'connection_minutes';
3
+ export interface DerivedLedgerUsage {
4
+ usageKind: LedgerUsageKind;
5
+ usageQuantity: number;
6
+ usageQuantitySecondary?: number;
7
+ }
8
+ type DeriveLedgerUsageInput = Pick<AICostRecord, 'usage' | 'mediaUsage' | 'operation'>;
9
+ export declare function deriveLedgerUsage(record: DeriveLedgerUsageInput): DerivedLedgerUsage;
10
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Capability classification for a model. Drives the filter on
3
+ * `AIProviderAdapter.listModels({ kind })`.
4
+ *
5
+ * - `text`: chat / completion / reasoning (called via `complete` / `stream`)
6
+ * - `embedding`: text-embedding models (called via `embed`)
7
+ * - `moderation`: content-moderation classifiers
8
+ * - `image`: image generation
9
+ * - `audio`: speech-to-text / text-to-speech / realtime audio
10
+ */
11
+ export type Kind = 'text' | 'embedding' | 'moderation' | 'image' | 'audio';
12
+ export interface ModelRate {
13
+ /** USD per 1M input tokens */
14
+ inputPerMTok: number;
15
+ /** USD per 1M output tokens (0 for embedding / moderation / free models) */
16
+ outputPerMTok: number;
17
+ /**
18
+ * Capability classification. Optional on the type for backward compat —
19
+ * existing consumers that destructure only the rate fields keep working —
20
+ * but every entry in `MODEL_PRICING` populates it.
21
+ */
22
+ kind?: Kind;
23
+ }
24
+ /**
25
+ * Finds the per-token rate for a model.
26
+ * Tries exact match first, then strips trailing date suffixes
27
+ * (e.g. "claude-sonnet-4-20250514" → "claude-sonnet-4").
28
+ * Returns null for unknown/unsupported models (Ollama, custom).
29
+ */
30
+ export declare function findModelRate(model: string): ModelRate | null;
31
+ /**
32
+ * Return all known model rates as an array of `[id, rate]` tuples. Used by
33
+ * `AIProviderAdapter.listModels()` to join live model ids against the pricing
34
+ * catalog. Order is not guaranteed.
35
+ */
36
+ export declare function allKnownModels(): ReadonlyArray<readonly [string, ModelRate]>;
37
+ /**
38
+ * Calculate the USD cost for a single AI request based on published per-token rates.
39
+ *
40
+ * Pricing adjustments applied:
41
+ * - **Cached input tokens**: charged at 0.1× the base input rate (both providers).
42
+ * - **Cache write tokens**: charged at 1.25× the base input rate (Anthropic 5-min cache).
43
+ * - **Long context premium**: for Claude Sonnet 4 / 4.5, when total input exceeds
44
+ * 200K tokens the entire request is charged at 2× input / 1.5× output.
45
+ *
46
+ * Returns 0 for unknown/unsupported models (Ollama, custom endpoints).
47
+ */
48
+ export declare function calculateModelCost(inputTokens: number, outputTokens: number, model: string, options?: {
49
+ cachedInputTokens?: number;
50
+ cacheWriteTokens?: number;
51
+ }): number;
@@ -0,0 +1,214 @@
1
+ import type { AIProviderConfig } from '../types/config.js';
2
+ import { type Kind } from './model-pricing.js';
3
+ /**
4
+ * A single chat-history turn. Used for native multi-turn requests where the
5
+ * model should see real conversation flow (assistant + user alternation).
6
+ * Optional — when omitted, providers fall back to single-user-message mode
7
+ * built from `ProviderRequest.prompt`.
8
+ */
9
+ export interface ChatMessage {
10
+ role: 'user' | 'assistant';
11
+ content: string;
12
+ }
13
+ export interface ProviderRequest {
14
+ /** System prompt / instructions */
15
+ system?: string;
16
+ /**
17
+ * User prompt content. Used as the SOLE user message when `messages` is
18
+ * absent. When `messages` is provided, this field is IGNORED — the messages
19
+ * array is the conversation. Callers passing `messages` may still set
20
+ * `prompt: ''` for type-compat.
21
+ */
22
+ prompt: string;
23
+ /**
24
+ * Optional native multi-turn conversation history. When present, providers
25
+ * send `[system?, ...messages]` to the API instead of `[system?, {role:'user', content: prompt}]`.
26
+ * The LAST entry should be a `user` message (the latest turn). Order is
27
+ * preserved exactly as supplied. Empty array is treated as "no messages"
28
+ * (falls back to single-user mode using `prompt`).
29
+ */
30
+ messages?: ChatMessage[];
31
+ /** Model name override */
32
+ model?: string;
33
+ /** Temperature (0-2) */
34
+ temperature?: number;
35
+ /** Max tokens for response */
36
+ maxTokens?: number;
37
+ /** Response format hint */
38
+ responseFormat?: 'text' | 'json';
39
+ /** Provider-compatible JSON Schema for strict structured outputs. */
40
+ responseSchema?: Record<string, unknown>;
41
+ /** Transport used for provider-side structured outputs. Defaults to response_format. */
42
+ structuredOutputTransport?: 'response_format' | 'tool';
43
+ /** Request timeout in milliseconds (default: 120_000) */
44
+ timeout?: number;
45
+ /**
46
+ * External AbortSignal (e.g. `ctx.signal`). When fired, the in-flight
47
+ * fetch(es) for this request abort immediately. Combined with the internal
48
+ * timeout signal via `AbortSignal.any([...])`, so whichever triggers first
49
+ * wins.
50
+ */
51
+ signal?: AbortSignal;
52
+ /**
53
+ * Deterministic sampling seed. OpenAI-compatible providers (including xAI
54
+ * Grok) honor this parameter: identical `{seed, temperature, model, prompt}`
55
+ * tuples produce the same tokens. Combined with `temperature: 0`, lets
56
+ * callers pin reproducible output for structural-planning prompts. Ignored
57
+ * by providers that do not support it.
58
+ */
59
+ seed?: number;
60
+ }
61
+ /** Compose a request's optional user signal with a timeout, returning a single AbortSignal. */
62
+ export declare function combineRequestSignals(userSignal: AbortSignal | undefined, timeoutMs: number): AbortSignal;
63
+ export interface ProviderResponse {
64
+ content: string;
65
+ model: string;
66
+ usage: TokenUsage;
67
+ finishReason: string;
68
+ }
69
+ export interface TokenUsage {
70
+ inputTokens: number;
71
+ outputTokens: number;
72
+ totalTokens: number;
73
+ /** Tokens served from provider cache (charged at reduced rate). */
74
+ cachedInputTokens?: number;
75
+ /** Tokens written to provider cache (charged at elevated rate — Anthropic only). */
76
+ cacheWriteTokens?: number;
77
+ }
78
+ export interface ProviderStreamEvent {
79
+ type: 'content_delta' | 'usage' | 'done' | 'error';
80
+ /** Incremental text chunk (for content_delta) */
81
+ delta?: string;
82
+ /** Token usage (for usage / done events) */
83
+ usage?: TokenUsage;
84
+ /** Finish reason (for done events) */
85
+ finishReason?: string;
86
+ /** Error message (for error events) */
87
+ error?: string;
88
+ }
89
+ export interface EmbeddingRequest {
90
+ texts: string[];
91
+ model?: string;
92
+ }
93
+ export interface EmbeddingResponse {
94
+ embeddings: number[][];
95
+ model: string;
96
+ usage: {
97
+ totalTokens: number;
98
+ };
99
+ /**
100
+ * USD cost computed from the pricing catalog using `usage.totalTokens` against
101
+ * the model's input rate (embeddings are input-only). `0` for unknown models
102
+ * — same convention as completion-side cost. Optional so that external
103
+ * `AIProviderAdapter` implementations written before this field existed still
104
+ * satisfy the interface; consumers reading `cost` should treat `undefined` as
105
+ * "unknown" (typically by coalescing to `0`).
106
+ */
107
+ cost?: number;
108
+ }
109
+ /**
110
+ * A single model entry returned by `AIProviderAdapter.listModels()`.
111
+ * The id is whatever the provider's `/v1/models` returns; pricing and `kind`
112
+ * are joined from the framework's pricing catalog. Models not in the catalog
113
+ * surface as `kind: 'unknown'` with `null` prices.
114
+ */
115
+ export interface ProviderModel {
116
+ /** API identifier (the value to pass as `model` in requests). */
117
+ id: string;
118
+ /** Adapter name (e.g. "openai", "anthropic", or a custom adapter's name). */
119
+ provider: string;
120
+ /** Capability classification, from the pricing catalog. `'unknown'` if not in the catalog. */
121
+ kind: Kind | 'unknown';
122
+ /** USD per 1M input tokens, or `null` if the model is not in the catalog. */
123
+ inputPerMTok: number | null;
124
+ /** USD per 1M output tokens, or `null` if the model is not in the catalog. */
125
+ outputPerMTok: number | null;
126
+ /** Provider-supplied human-readable label (Anthropic exposes this; OpenAI doesn't). */
127
+ displayName?: string;
128
+ /** ISO-8601 creation timestamp when the provider exposes one. */
129
+ createdAt?: string;
130
+ /** Owner/team (OpenAI exposes this; Anthropic doesn't). */
131
+ ownedBy?: string;
132
+ }
133
+ /**
134
+ * Optional filter passed to `AIProviderAdapter.listModels()`. `kind` accepts a
135
+ * single Kind or an array. When unset, all models are returned (including
136
+ * unknown ones). When set against an official endpoint, unknown-kind models
137
+ * are excluded; against a custom endpoint they are included alongside the
138
+ * filtered matches (see adapter docs).
139
+ */
140
+ export interface ListModelsFilter {
141
+ kind?: Kind | Kind[];
142
+ }
143
+ export interface AIProviderAdapter {
144
+ readonly name: string;
145
+ /** Send a completion request */
146
+ complete(request: ProviderRequest): Promise<ProviderResponse>;
147
+ /** Stream a completion request, yielding incremental events */
148
+ stream(request: ProviderRequest): AsyncIterable<ProviderStreamEvent>;
149
+ /** Generate embeddings for texts */
150
+ embed(request: EmbeddingRequest): Promise<EmbeddingResponse>;
151
+ /**
152
+ * List models the provider exposes for this account, joined against the
153
+ * framework's pricing catalog. Optional — custom adapters that don't
154
+ * implement it still satisfy the interface.
155
+ *
156
+ * Filtering rule (see docs for full details):
157
+ * - No filter → all models, including unknown-kind entries.
158
+ * - Filter + official endpoint → only matching kinds; unknowns excluded.
159
+ * - Filter + custom endpoint → matching kinds **plus** all unknowns.
160
+ *
161
+ * On network error / 404 / unsupported endpoint, returns `[]` and emits
162
+ * one `console.warn`; does not throw.
163
+ */
164
+ listModels?(filter?: ListModelsFilter): Promise<ProviderModel[]>;
165
+ }
166
+ /**
167
+ * Shared logic used by adapter `listModels` implementations: join a list of
168
+ * provider-supplied model entries against the pricing catalog, then apply
169
+ * the kind filter respecting the official-endpoint rule.
170
+ */
171
+ export declare function joinAndFilterModels(args: {
172
+ provider: string;
173
+ entries: Array<{
174
+ id: string;
175
+ displayName?: string;
176
+ createdAt?: string;
177
+ ownedBy?: string;
178
+ }>;
179
+ filter: ListModelsFilter | undefined;
180
+ isOfficial: boolean;
181
+ }): ProviderModel[];
182
+ export declare class ProviderAPIError extends Error {
183
+ readonly providerName: string;
184
+ readonly statusCode?: number;
185
+ readonly retryable: boolean;
186
+ readonly attempts: number;
187
+ constructor(args: {
188
+ providerName: string;
189
+ message: string;
190
+ retryable: boolean;
191
+ attempts: number;
192
+ statusCode?: number;
193
+ });
194
+ }
195
+ export declare function isProviderAPIError(value: unknown): value is ProviderAPIError;
196
+ export interface OpenAIAdapterConfig {
197
+ apiKey: string;
198
+ model?: string;
199
+ embeddingModel?: string;
200
+ baseUrl?: string;
201
+ /** Request timeout in milliseconds (default: 120_000) */
202
+ requestTimeout?: number;
203
+ }
204
+ export declare function createOpenAIAdapter(config: OpenAIAdapterConfig): AIProviderAdapter;
205
+ export interface AnthropicAdapterConfig {
206
+ apiKey: string;
207
+ model?: string;
208
+ baseUrl?: string;
209
+ /** Request timeout in milliseconds (default: 120_000) */
210
+ requestTimeout?: number;
211
+ }
212
+ export declare function createAnthropicAdapter(config: AnthropicAdapterConfig): AIProviderAdapter;
213
+ /** Create a provider adapter by name from a provider config entry */
214
+ export declare function createProviderAdapter(name: string, providerConfig: AIProviderConfig): AIProviderAdapter;
@@ -0,0 +1,27 @@
1
+ import type { TokenUsage } from './provider.js';
2
+ export declare class AIRefusalError extends Error {
3
+ readonly provider: string;
4
+ readonly model: string;
5
+ readonly refusalText: string;
6
+ readonly usage: TokenUsage;
7
+ constructor(args: {
8
+ provider: string;
9
+ model: string;
10
+ refusalText: string;
11
+ usage?: TokenUsage;
12
+ });
13
+ }
14
+ export declare class AIIncompleteOutputError extends Error {
15
+ readonly provider: string;
16
+ readonly model: string;
17
+ readonly partialText: string;
18
+ readonly usage: TokenUsage;
19
+ readonly finishReason: string;
20
+ constructor(args: {
21
+ provider: string;
22
+ model: string;
23
+ partialText: string;
24
+ usage?: TokenUsage;
25
+ finishReason: string;
26
+ });
27
+ }
@@ -0,0 +1,30 @@
1
+ export interface UsageEntry {
2
+ model: string;
3
+ inputTokens: number;
4
+ outputTokens: number;
5
+ cost: number;
6
+ requestCount: number;
7
+ }
8
+ export interface UsageData {
9
+ totalCost: number;
10
+ currency: string;
11
+ entries: UsageEntry[];
12
+ }
13
+ export interface UsageAPIClient {
14
+ provider: string;
15
+ fetchUsage(params: {
16
+ startDate: Date;
17
+ endDate: Date;
18
+ }): Promise<UsageData>;
19
+ }
20
+ export interface UsageClientConfig {
21
+ provider: 'openai' | 'anthropic';
22
+ apiKey: string;
23
+ baseUrl?: string;
24
+ organizationId?: string;
25
+ }
26
+ export declare function createUsageAPIClient(config: UsageClientConfig): UsageAPIClient;
27
+ export declare class UsageAPIError extends Error {
28
+ readonly provider: string;
29
+ constructor(provider: string, message: string);
30
+ }
@@ -11,7 +11,7 @@ export type { AICostContext };
11
11
  * Framework-level hook fired after every AI call completes (success or
12
12
  * failure) and the in-memory cost tracker has been updated. Consumers wire
13
13
  * this in through `ServerConfig.onAICostRecorded` to persist per-call spend
14
- * to their own ledger (see MemoirAi's `project_cost_ledger`).
14
+ * to their own ledger (e.g. an app-owned per-project cost ledger).
15
15
  */
16
16
  export type OnAICostRecorded = (record: AICostRecord, costContext?: AICostContext) => Promise<void> | void;
17
17
  export interface AIServiceConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"ai-service.d.ts","sourceRoot":"","sources":["../../src/ai/ai-service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,aAAa,EAGb,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,YAAY,EAAqB,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAA4C,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,YAAY,EACpB,WAAW,CAAC,EAAE,aAAa,KACxB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAG1B,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,0DAA0D,CAAC;IACjF,gFAAgF;IAChF,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAChF,CAAC,kCAAkC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,iBAAiB,CAAC,GAC5D,eAAe,CAMjB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAijClE"}
1
+ {"version":3,"file":"ai-service.d.ts","sourceRoot":"","sources":["../../src/ai/ai-service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,aAAa,EAGb,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,YAAY,EAAqB,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAA4C,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,YAAY,EACpB,WAAW,CAAC,EAAE,aAAa,KACxB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAG1B,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,0DAA0D,CAAC;IACjF,gFAAgF;IAChF,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAChF,CAAC,kCAAkC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,iBAAiB,CAAC,GAC5D,eAAe,CAMjB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAwjClE"}
@@ -86,7 +86,7 @@ export function createAIService(config) {
86
86
  * id/timestamp generation mirror what `createCostTracker` does internally
87
87
  * so the hook gets a fully-formed `AICostRecord`.
88
88
  */
89
- async function recordCost(entry, costContext) {
89
+ async function recordProviderCost(entry, costContext) {
90
90
  if (costTracker) {
91
91
  costTracker.record(entry);
92
92
  }
@@ -117,12 +117,13 @@ export function createAIService(config) {
117
117
  cacheWriteTokens: (left.cacheWriteTokens ?? 0) + (right.cacheWriteTokens ?? 0),
118
118
  };
119
119
  }
120
- function checkBudget(estimatedTokens) {
120
+ function checkBudget(estimatedTokens, estimatedCostUsd) {
121
121
  if (!costTracker)
122
122
  return;
123
123
  const result = costTracker.checkBudget({
124
124
  tenantId: config.budget?.tenantId,
125
125
  estimatedTokens,
126
+ estimatedCostUsd,
126
127
  });
127
128
  if (!result.allowed) {
128
129
  throw new Error(`AI budget exceeded: ${result.reason}`);
@@ -267,7 +268,7 @@ export function createAIService(config) {
267
268
  cacheWriteTokens: failureUsage.cacheWriteTokens,
268
269
  })
269
270
  : null;
270
- await recordCost({
271
+ await recordProviderCost({
271
272
  model: resolvedModel,
272
273
  provider: activeProvider.name,
273
274
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -284,7 +285,7 @@ export function createAIService(config) {
284
285
  }
285
286
  const latencyMs = performance.now() - start;
286
287
  // Track cost (success path)
287
- await recordCost({
288
+ await recordProviderCost({
288
289
  model: resolvedModel,
289
290
  provider: activeProvider.name,
290
291
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -329,6 +330,10 @@ export function createAIService(config) {
329
330
  };
330
331
  }
331
332
  return {
333
+ recordProviderCost,
334
+ checkProviderCostBudget(config = {}) {
335
+ checkBudget(config.estimatedTokens, config.estimatedCostUsd);
336
+ },
332
337
  async generate(params) {
333
338
  const { data } = await _generateCore(params);
334
339
  return data;
@@ -432,7 +437,7 @@ export function createAIService(config) {
432
437
  cacheWriteTokens: streamUsage.cacheWriteTokens,
433
438
  })
434
439
  : null;
435
- await recordCost({
440
+ await recordProviderCost({
436
441
  model: resolvedModel,
437
442
  provider: activeProvider.name,
438
443
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -460,7 +465,7 @@ export function createAIService(config) {
460
465
  cacheWriteTokens: errUsage.cacheWriteTokens,
461
466
  })
462
467
  : null;
463
- await recordCost({
468
+ await recordProviderCost({
464
469
  model: resolvedModel,
465
470
  provider: activeProvider.name,
466
471
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -492,7 +497,7 @@ export function createAIService(config) {
492
497
  if (request.responseSchema &&
493
498
  (providerFinishReason === 'length' || providerFinishReason === 'max_tokens')) {
494
499
  const failureUsage = streamUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
495
- await recordCost({
500
+ await recordProviderCost({
496
501
  model: resolvedModel,
497
502
  provider: activeProvider.name,
498
503
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -516,7 +521,7 @@ export function createAIService(config) {
516
521
  // Build the final validated result
517
522
  if (singleTextField) {
518
523
  // Plain text mode — wrap the accumulated text in the schema field
519
- await recordCost({
524
+ await recordProviderCost({
520
525
  model: resolvedModel,
521
526
  provider: activeProvider.name,
522
527
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -554,7 +559,7 @@ export function createAIService(config) {
554
559
  cacheWriteTokens: failureUsage.cacheWriteTokens,
555
560
  })
556
561
  : null;
557
- await recordCost({
562
+ await recordProviderCost({
558
563
  model: resolvedModel,
559
564
  provider: activeProvider.name,
560
565
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -598,7 +603,7 @@ export function createAIService(config) {
598
603
  cacheWriteTokens: combinedUsage.cacheWriteTokens,
599
604
  })
600
605
  : null;
601
- await recordCost({
606
+ await recordProviderCost({
602
607
  model: resolvedModel,
603
608
  provider: activeProvider.name,
604
609
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -617,7 +622,7 @@ export function createAIService(config) {
617
622
  const successUsage = validationFellBackToNonStreaming
618
623
  ? sumUsage(streamUsage, fallbackUsage)
619
624
  : (streamUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 });
620
- await recordCost({
625
+ await recordProviderCost({
621
626
  model: resolvedModel,
622
627
  provider: activeProvider.name,
623
628
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -638,7 +643,7 @@ export function createAIService(config) {
638
643
  };
639
644
  }
640
645
  else {
641
- await recordCost({
646
+ await recordProviderCost({
642
647
  model: resolvedModel,
643
648
  provider: activeProvider.name,
644
649
  promptName: hasPromptDef ? params.prompt : undefined,
@@ -683,7 +688,7 @@ export function createAIService(config) {
683
688
  const failureUsage = err instanceof AIValidationError
684
689
  ? err.usage
685
690
  : (getTypedAIErrorUsage(err) ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 });
686
- await recordCost({
691
+ await recordProviderCost({
687
692
  model: resolvedModel,
688
693
  provider: activeProvider.name,
689
694
  operation: 'extract',
@@ -698,7 +703,7 @@ export function createAIService(config) {
698
703
  throw err;
699
704
  }
700
705
  const latencyMs = performance.now() - start;
701
- await recordCost({
706
+ await recordProviderCost({
702
707
  model: resolvedModel,
703
708
  provider: activeProvider.name,
704
709
  operation: 'extract',
@@ -756,7 +761,7 @@ export function createAIService(config) {
756
761
  const failureUsage = err instanceof AIValidationError
757
762
  ? err.usage
758
763
  : (getTypedAIErrorUsage(err) ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 });
759
- await recordCost({
764
+ await recordProviderCost({
760
765
  model: resolvedModel,
761
766
  provider: activeProvider.name,
762
767
  operation: 'classify',
@@ -773,7 +778,7 @@ export function createAIService(config) {
773
778
  const latencyMs = performance.now() - start;
774
779
  // Filter to only valid labels
775
780
  const result = validated.data.filter((l) => params.labels.includes(l));
776
- await recordCost({
781
+ await recordProviderCost({
777
782
  model: resolvedModel,
778
783
  provider: activeProvider.name,
779
784
  operation: 'classify',