octaflow 0.17.3 → 0.18.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.
@@ -1,8 +1,9 @@
1
1
  import { E as RetryPolicy, g as TypedWorkflow, h as TypedStepContext, m as TypedStep, nt as Result, tt as FlowErrorShape } from "../index-BURAYZui.js";
2
2
  import { n as Logger } from "../logger-BCus3AOa.js";
3
3
  import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-BdxIcc92.js";
4
+ import { a as AiProviderMeta, c as DEFAULT_AI_PROVIDER, d as EMBEDDING_PROVIDER_IDS, f as EmbeddingProviderId, i as AiProviderId, l as DEFAULT_EMBEDDING_PROVIDER, m as embeddingDimensionsFor, n as AI_PROVIDER_IDS, o as AiProviderModelOption, p as EmbeddingProviderMeta, r as AiProcessingRegion, s as AiVendorResidency, t as AI_PROVIDERS, u as EMBEDDING_PROVIDERS } from "../provider-registry-bXGOmyeh.js";
4
5
  import { z } from "zod";
5
- import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
6
+ import { EmbeddingModelV4, LanguageModelV4, SharedV4ProviderMetadata } from "@ai-sdk/provider";
6
7
  //#region src/ai/instrumented-model.d.ts
7
8
  export interface AccumulatedUsage {
8
9
  inputTokens: number;
@@ -71,7 +72,15 @@ export interface TokenUsage {
71
72
  cacheReadTokens: number;
72
73
  cacheWriteTokens: number;
73
74
  }
74
- /** Default pricing (USD per million tokens), as of 2026-04. */
75
+ /**
76
+ * Default pricing, USD per million tokens — Anthropic as of 2026-09, Mistral and
77
+ * the OpenAI embeddings as of 2026-08. A best-effort estimate, not a billing
78
+ * record: a BYOK caller can name any model id, and an unknown id prices at the
79
+ * priciest row (safe overcounting — see {@link createCostEstimator}). Retired ids
80
+ * are kept rather than deleted: dropping one silently reprices every historical
81
+ * usage row at the fallback rate. Only the Mistral `-latest` aliases are listed;
82
+ * pinned ids (`mistral-medium-2508`) fall to the fallback.
83
+ */
75
84
  export declare const DEFAULT_MODEL_PRICING: Record<string, ModelPricing>;
76
85
  export interface CostEstimatorOptions {
77
86
  /** Pricing table keyed by model id. Defaults to `DEFAULT_MODEL_PRICING`. */
@@ -393,4 +402,145 @@ export interface AiUsageAggregationService {
393
402
  }
394
403
  /** Build a store-agnostic AI usage-aggregation service. */
395
404
  export declare function createAiUsageAggregationService(deps: CreateAiUsageAggregationServiceDeps): AiUsageAggregationService;
396
- //#endregion
405
+ //#endregion
406
+ //#region src/ai/refusal.d.ts
407
+ /** The unified finish reason the AI SDK reports for a declined generation. */
408
+ export declare const REFUSAL_FINISH_REASON = "content-filter";
409
+ /**
410
+ * What the provider said about a refusal. Every field is optional except the
411
+ * model: providers other than Anthropic report the refusal without the
412
+ * `stop_details` breakdown, and Anthropic itself only sends `category` and
413
+ * `explanation` for some categories.
414
+ */
415
+ export interface AiRefusalDetails {
416
+ /** The model that declined. */
417
+ modelId: string;
418
+ /** Provider's refusal category — Anthropic's `stop_details.category`. */
419
+ category?: string;
420
+ /** Provider's prose reason, when it sends one. */
421
+ explanation?: string;
422
+ /** The model the provider suggests retrying on (`stop_details.recommended_model`). */
423
+ recommendedModel?: string;
424
+ /** The provider's own stop reason (`refusal` on Anthropic), before unification. */
425
+ rawFinishReason?: string;
426
+ }
427
+ /**
428
+ * A generation the model declined to produce.
429
+ *
430
+ * Distinct from a transport failure on purpose: it is deterministic, so
431
+ * retrying the same prompt on the same model produces the same refusal. Callers
432
+ * that classify retryability must treat it as terminal — `defineAiStep` does
433
+ * this for every AI workflow step.
434
+ */
435
+ export declare class AiRefusalError extends Error {
436
+ readonly name = "AiRefusalError";
437
+ readonly details: AiRefusalDetails;
438
+ constructor(details: AiRefusalDetails);
439
+ }
440
+ /**
441
+ * True when `error` is an {@link AiRefusalError}, or wraps one as its `cause`.
442
+ *
443
+ * Walks the chain because the SDK layer between the middleware and the caller
444
+ * may re-wrap: `generateObject`, for instance, reports parse failures as
445
+ * `NoObjectGeneratedError` with the original as `cause`. Depth-capped and
446
+ * cycle-safe.
447
+ */
448
+ export declare function isAiRefusalError(error: unknown): error is AiRefusalError;
449
+ /**
450
+ * Pull the provider's refusal breakdown out of `providerMetadata`.
451
+ *
452
+ * Scans every provider namespace for a `stopDetails` record rather than reading
453
+ * `providerMetadata.anthropic` directly — the shape is Anthropic's today, and
454
+ * the surrounding module is deliberately provider-agnostic. A provider that
455
+ * reports a refusal with no breakdown simply yields no fields.
456
+ */
457
+ export declare function readRefusalDetails(providerMetadata: SharedV4ProviderMetadata | undefined): Omit<AiRefusalDetails, 'modelId'>;
458
+ /**
459
+ * Wrap a model so a declined generation throws {@link AiRefusalError} instead
460
+ * of returning empty content.
461
+ *
462
+ * **`doGenerate` only, on purpose.** One-shot calls — every AI workflow step,
463
+ * every ad-hoc generation service, both classifiers — have no way to present a
464
+ * refusal to anyone: their contract is "return the value or fail", so a refusal
465
+ * that does not throw becomes an empty field or an unparseable object several
466
+ * frames away from its cause. The streaming path is the opposite case: the help
467
+ * chat is a conversation, where a decline is a legitimate turn the operator
468
+ * should *read*, and it already has a refusal surface (the `refusal` data part)
469
+ * for exactly that. `ChatAgentService` branches on the finish reason in
470
+ * `onFinish` instead, so the stream stays a stream.
471
+ */
472
+ export declare function createRefusalAwareModel(model: LanguageModelV4): LanguageModelV4;
473
+ //#endregion
474
+ //#region src/ai/capabilities.d.ts
475
+ /**
476
+ * Per-model API capability switches.
477
+ *
478
+ * Claude 4.6+ models (Opus 4.6/4.7/4.8/5, Sonnet 4.6/5, Fable/Mythos 5) use
479
+ * adaptive thinking; on 4.7+ the legacy `thinking: { type: 'enabled',
480
+ * budget_tokens }` shape and non-default sampling params (`temperature`,
481
+ * `top_p`, `top_k`) are rejected with a 400. Older models (Haiku 4.5,
482
+ * Sonnet 4.5, …) still require `enabled` + budgetTokens for extended
483
+ * thinking and accept `temperature`.
484
+ *
485
+ * A BYOK deployment lets a caller name any model id, so call sites must
486
+ * branch on the resolved id rather than assume one API shape — these
487
+ * functions are that branch, written once. `providerId` is a plain string:
488
+ * only `'anthropic'` has provider-specific shapes today.
489
+ */
490
+ /** True for Claude 4.6+ / 5-family models: adaptive thinking, no budgetTokens, no sampling params. */
491
+ export declare function supportsAdaptiveThinking(modelId: string): boolean;
492
+ export interface AnthropicThinkingSettings {
493
+ temperature?: number;
494
+ headers?: Record<string, string>;
495
+ providerOptions: {
496
+ anthropic: {
497
+ thinking: {
498
+ type: 'adaptive';
499
+ display: 'summarized';
500
+ } | {
501
+ type: 'enabled';
502
+ budgetTokens: number;
503
+ };
504
+ };
505
+ };
506
+ }
507
+ /**
508
+ * Spreadable `streamText`/`generateText` settings for a thinking-enabled
509
+ * agent loop, shaped for the target model's API generation.
510
+ *
511
+ * - Adaptive models: `thinking: { type: 'adaptive' }` with summarized display
512
+ * (the default `omitted` streams empty reasoning parts to the client).
513
+ * Interleaved thinking is implicit — no beta header, no temperature.
514
+ * - Legacy models: extended thinking with a token budget, the interleaved
515
+ * thinking beta header, and the caller's temperature.
516
+ */
517
+ export declare function anthropicThinkingSettings(modelId: string, opts: {
518
+ temperature: number;
519
+ budgetTokens: number;
520
+ }): AnthropicThinkingSettings;
521
+ /**
522
+ * True when the provider+model pair supports Anthropic's tool search tool.
523
+ * Anthropic-only — like the thinking/beta-header shapes, `deferLoading`
524
+ * provider options and the search tool must never be emitted for another
525
+ * vendor. Unknown or pre-4.5 Anthropic model ids return false so callers
526
+ * fall back to shipping the full tool registry undeferred.
527
+ */
528
+ export declare function supportsToolSearch(providerId: string, modelId: string): boolean;
529
+ export interface ThinkingSettings {
530
+ temperature?: number;
531
+ headers?: Record<string, string>;
532
+ providerOptions?: AnthropicThinkingSettings['providerOptions'];
533
+ }
534
+ /**
535
+ * Provider-neutral wrapper around {@link anthropicThinkingSettings}. Other
536
+ * providers have no thinking/beta-header surface — they just get the caller's
537
+ * temperature. Never emit `providerOptions.anthropic` (or the beta header) for
538
+ * another vendor's model, even though it would be ignored on the wire — it
539
+ * pollutes request logs and breaks request-shape tests.
540
+ */
541
+ export declare function thinkingSettingsFor(providerId: string, modelId: string, opts: {
542
+ temperature: number;
543
+ budgetTokens: number;
544
+ }): ThinkingSettings;
545
+ //#endregion
546
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiVendorResidency, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, embeddingDimensionsFor };
package/dist/ai/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { S as ok, b as noopLogger, f as defineStep, l as buildWorkflow, x as err } from "../core-DNWPwFaE.js";
2
+ import { a as EMBEDDING_PROVIDERS, c as AiRefusalError, d as isAiRefusalError, f as readRefusalDetails, i as DEFAULT_EMBEDDING_PROVIDER, l as REFUSAL_FINISH_REASON, n as AI_PROVIDER_IDS, o as EMBEDDING_PROVIDER_IDS, r as DEFAULT_AI_PROVIDER, s as embeddingDimensionsFor, t as AI_PROVIDERS, u as createRefusalAwareModel } from "../provider-registry-DhNXw_CC.js";
2
3
  import { wrapEmbeddingModel, wrapLanguageModel } from "ai";
3
4
  //#region src/ai/instrumented-model.ts
4
5
  function createUsageAccumulator() {
@@ -121,7 +122,15 @@ function createInstrumentedEmbeddingModel(model, accumulator) {
121
122
  }
122
123
  //#endregion
123
124
  //#region src/ai/cost.ts
124
- /** Default pricing (USD per million tokens), as of 2026-04. */
125
+ /**
126
+ * Default pricing, USD per million tokens — Anthropic as of 2026-09, Mistral and
127
+ * the OpenAI embeddings as of 2026-08. A best-effort estimate, not a billing
128
+ * record: a BYOK caller can name any model id, and an unknown id prices at the
129
+ * priciest row (safe overcounting — see {@link createCostEstimator}). Retired ids
130
+ * are kept rather than deleted: dropping one silently reprices every historical
131
+ * usage row at the fallback rate. Only the Mistral `-latest` aliases are listed;
132
+ * pinned ids (`mistral-medium-2508`) fall to the fallback.
133
+ */
125
134
  const DEFAULT_MODEL_PRICING = {
126
135
  "text-embedding-3-small": {
127
136
  inputPerMillion: .02,
@@ -135,6 +144,48 @@ const DEFAULT_MODEL_PRICING = {
135
144
  cacheReadPerMillion: 0,
136
145
  cacheWritePerMillion: 0
137
146
  },
147
+ "claude-fable-5": {
148
+ inputPerMillion: 10,
149
+ outputPerMillion: 50,
150
+ cacheReadPerMillion: 1,
151
+ cacheWritePerMillion: 12.5
152
+ },
153
+ "claude-mythos-5": {
154
+ inputPerMillion: 10,
155
+ outputPerMillion: 50,
156
+ cacheReadPerMillion: 1,
157
+ cacheWritePerMillion: 12.5
158
+ },
159
+ "claude-fable-5-1": {
160
+ inputPerMillion: 10,
161
+ outputPerMillion: 50,
162
+ cacheReadPerMillion: .25,
163
+ cacheWritePerMillion: 12.5
164
+ },
165
+ "claude-mythos-5-1": {
166
+ inputPerMillion: 10,
167
+ outputPerMillion: 50,
168
+ cacheReadPerMillion: .25,
169
+ cacheWritePerMillion: 12.5
170
+ },
171
+ "claude-opus-5": {
172
+ inputPerMillion: 5,
173
+ outputPerMillion: 25,
174
+ cacheReadPerMillion: .5,
175
+ cacheWritePerMillion: 6.25
176
+ },
177
+ "claude-sonnet-5": {
178
+ inputPerMillion: 2,
179
+ outputPerMillion: 10,
180
+ cacheReadPerMillion: .2,
181
+ cacheWritePerMillion: 2.5
182
+ },
183
+ "claude-opus-4-8": {
184
+ inputPerMillion: 5,
185
+ outputPerMillion: 25,
186
+ cacheReadPerMillion: .5,
187
+ cacheWritePerMillion: 6.25
188
+ },
138
189
  "claude-opus-4-7": {
139
190
  inputPerMillion: 5,
140
191
  outputPerMillion: 25,
@@ -142,10 +193,16 @@ const DEFAULT_MODEL_PRICING = {
142
193
  cacheWritePerMillion: 6.25
143
194
  },
144
195
  "claude-opus-4-6": {
145
- inputPerMillion: 15,
146
- outputPerMillion: 75,
147
- cacheReadPerMillion: 1.5,
148
- cacheWritePerMillion: 18.75
196
+ inputPerMillion: 5,
197
+ outputPerMillion: 25,
198
+ cacheReadPerMillion: .5,
199
+ cacheWritePerMillion: 6.25
200
+ },
201
+ "claude-opus-4-5": {
202
+ inputPerMillion: 5,
203
+ outputPerMillion: 25,
204
+ cacheReadPerMillion: .5,
205
+ cacheWritePerMillion: 6.25
149
206
  },
150
207
  "claude-sonnet-4-6": {
151
208
  inputPerMillion: 3,
@@ -153,17 +210,59 @@ const DEFAULT_MODEL_PRICING = {
153
210
  cacheReadPerMillion: .3,
154
211
  cacheWritePerMillion: 3.75
155
212
  },
213
+ "claude-sonnet-4-5": {
214
+ inputPerMillion: 3,
215
+ outputPerMillion: 15,
216
+ cacheReadPerMillion: .3,
217
+ cacheWritePerMillion: 3.75
218
+ },
156
219
  "claude-sonnet-4-5-20250929": {
157
220
  inputPerMillion: 3,
158
221
  outputPerMillion: 15,
159
222
  cacheReadPerMillion: .3,
160
223
  cacheWritePerMillion: 3.75
161
224
  },
225
+ "claude-haiku-4-5": {
226
+ inputPerMillion: 1,
227
+ outputPerMillion: 5,
228
+ cacheReadPerMillion: .1,
229
+ cacheWritePerMillion: 1.25
230
+ },
162
231
  "claude-haiku-4-5-20251001": {
163
- inputPerMillion: .8,
164
- outputPerMillion: 4,
165
- cacheReadPerMillion: .08,
166
- cacheWritePerMillion: 1
232
+ inputPerMillion: 1,
233
+ outputPerMillion: 5,
234
+ cacheReadPerMillion: .1,
235
+ cacheWritePerMillion: 1.25
236
+ },
237
+ "mistral-medium-latest": {
238
+ inputPerMillion: 1.5,
239
+ outputPerMillion: 7.5,
240
+ cacheReadPerMillion: .15,
241
+ cacheWritePerMillion: 1.5
242
+ },
243
+ "mistral-large-latest": {
244
+ inputPerMillion: .5,
245
+ outputPerMillion: 1.5,
246
+ cacheReadPerMillion: .05,
247
+ cacheWritePerMillion: .5
248
+ },
249
+ "mistral-small-latest": {
250
+ inputPerMillion: .15,
251
+ outputPerMillion: .6,
252
+ cacheReadPerMillion: .015,
253
+ cacheWritePerMillion: .15
254
+ },
255
+ "magistral-medium-latest": {
256
+ inputPerMillion: 2,
257
+ outputPerMillion: 5,
258
+ cacheReadPerMillion: .2,
259
+ cacheWritePerMillion: 2
260
+ },
261
+ "mistral-embed": {
262
+ inputPerMillion: .1,
263
+ outputPerMillion: 0,
264
+ cacheReadPerMillion: 0,
265
+ cacheWritePerMillion: 0
167
266
  }
168
267
  };
169
268
  function priciest(pricing) {
@@ -463,4 +562,91 @@ function createAiUsageAggregationService(deps) {
463
562
  };
464
563
  }
465
564
  //#endregion
466
- export { DEFAULT_AI_QUOTA, DEFAULT_MODEL_PRICING, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createUsageAccumulator, defineAiStep, estimateCostMicros };
565
+ //#region src/ai/capabilities.ts
566
+ /**
567
+ * Per-model API capability switches.
568
+ *
569
+ * Claude 4.6+ models (Opus 4.6/4.7/4.8/5, Sonnet 4.6/5, Fable/Mythos 5) use
570
+ * adaptive thinking; on 4.7+ the legacy `thinking: { type: 'enabled',
571
+ * budget_tokens }` shape and non-default sampling params (`temperature`,
572
+ * `top_p`, `top_k`) are rejected with a 400. Older models (Haiku 4.5,
573
+ * Sonnet 4.5, …) still require `enabled` + budgetTokens for extended
574
+ * thinking and accept `temperature`.
575
+ *
576
+ * A BYOK deployment lets a caller name any model id, so call sites must
577
+ * branch on the resolved id rather than assume one API shape — these
578
+ * functions are that branch, written once. `providerId` is a plain string:
579
+ * only `'anthropic'` has provider-specific shapes today.
580
+ */
581
+ const ADAPTIVE_THINKING_MODEL_PREFIXES = [
582
+ "claude-fable-5",
583
+ "claude-mythos-5",
584
+ "claude-opus-5",
585
+ "claude-opus-4-8",
586
+ "claude-opus-4-7",
587
+ "claude-opus-4-6",
588
+ "claude-sonnet-5",
589
+ "claude-sonnet-4-6"
590
+ ];
591
+ /** True for Claude 4.6+ / 5-family models: adaptive thinking, no budgetTokens, no sampling params. */
592
+ function supportsAdaptiveThinking(modelId) {
593
+ return ADAPTIVE_THINKING_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix));
594
+ }
595
+ /**
596
+ * Spreadable `streamText`/`generateText` settings for a thinking-enabled
597
+ * agent loop, shaped for the target model's API generation.
598
+ *
599
+ * - Adaptive models: `thinking: { type: 'adaptive' }` with summarized display
600
+ * (the default `omitted` streams empty reasoning parts to the client).
601
+ * Interleaved thinking is implicit — no beta header, no temperature.
602
+ * - Legacy models: extended thinking with a token budget, the interleaved
603
+ * thinking beta header, and the caller's temperature.
604
+ */
605
+ function anthropicThinkingSettings(modelId, opts) {
606
+ if (supportsAdaptiveThinking(modelId)) return { providerOptions: { anthropic: { thinking: {
607
+ type: "adaptive",
608
+ display: "summarized"
609
+ } } } };
610
+ return {
611
+ temperature: opts.temperature,
612
+ headers: { "anthropic-beta": "interleaved-thinking-2025-05-14" },
613
+ providerOptions: { anthropic: { thinking: {
614
+ type: "enabled",
615
+ budgetTokens: opts.budgetTokens
616
+ } } }
617
+ };
618
+ }
619
+ /**
620
+ * Models supporting Anthropic's server-side tool search
621
+ * (`tool_search_tool_regex_20251119`) with deferred tool loading — every
622
+ * adaptive-thinking model plus the 4.5 generation.
623
+ */
624
+ const TOOL_SEARCH_MODEL_PREFIXES = [
625
+ ...ADAPTIVE_THINKING_MODEL_PREFIXES,
626
+ "claude-opus-4-5",
627
+ "claude-sonnet-4-5",
628
+ "claude-haiku-4-5"
629
+ ];
630
+ /**
631
+ * True when the provider+model pair supports Anthropic's tool search tool.
632
+ * Anthropic-only — like the thinking/beta-header shapes, `deferLoading`
633
+ * provider options and the search tool must never be emitted for another
634
+ * vendor. Unknown or pre-4.5 Anthropic model ids return false so callers
635
+ * fall back to shipping the full tool registry undeferred.
636
+ */
637
+ function supportsToolSearch(providerId, modelId) {
638
+ return providerId === "anthropic" && TOOL_SEARCH_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix));
639
+ }
640
+ /**
641
+ * Provider-neutral wrapper around {@link anthropicThinkingSettings}. Other
642
+ * providers have no thinking/beta-header surface — they just get the caller's
643
+ * temperature. Never emit `providerOptions.anthropic` (or the beta header) for
644
+ * another vendor's model, even though it would be ignored on the wire — it
645
+ * pollutes request logs and breaks request-shape tests.
646
+ */
647
+ function thinkingSettingsFor(providerId, modelId, opts) {
648
+ if (providerId === "anthropic") return anthropicThinkingSettings(modelId, opts);
649
+ return { temperature: opts.temperature };
650
+ }
651
+ //#endregion
652
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_PRICING, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, REFUSAL_FINISH_REASON, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createRefusalAwareModel, createUsageAccumulator, defineAiStep, embeddingDimensionsFor, estimateCostMicros, isAiRefusalError, readRefusalDetails, supportsAdaptiveThinking, supportsToolSearch, thinkingSettingsFor };
@@ -0,0 +1,69 @@
1
+ import { f as EmbeddingProviderId, i as AiProviderId } from "../../provider-registry-bXGOmyeh.js";
2
+ import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
3
+ //#region src/ai/providers/index.d.ts
4
+ export type LanguageModelFactory = (modelId: string) => LanguageModelV4;
5
+ export type EmbeddingModelFactory = (modelId: string) => EmbeddingModelV4;
6
+ /** A language-model factory for a provider, loading its SDK on first use. */
7
+ export declare function createLanguageModelFactory(providerId: AiProviderId, apiKey: string, baseURL?: string): Promise<LanguageModelFactory>;
8
+ /** An embedding-model factory for a provider, loading its SDK on first use. */
9
+ export declare function createEmbeddingModelFactory(providerId: EmbeddingProviderId, apiKey: string, baseURL?: string): Promise<EmbeddingModelFactory>;
10
+ export interface LanguageModelProvider {
11
+ /** Which vendor the key belongs to — gate provider-specific request shapes on this (see `thinkingSettingsFor`). */
12
+ providerId: AiProviderId;
13
+ /** Cheap model for classifier-style passes, from the registry. */
14
+ classifierModelId: string;
15
+ /**
16
+ * A model wrapped for refusal detection: a declined generation throws
17
+ * `AiRefusalError` from `doGenerate` instead of returning empty content.
18
+ * Make every one-shot call site go through this and the behaviour is universal.
19
+ */
20
+ languageModel(modelId?: string): LanguageModelV4;
21
+ }
22
+ export interface LanguageModelProviderConfig {
23
+ provider?: AiProviderId;
24
+ apiKey: string;
25
+ /** Overrides the registry's default model. */
26
+ defaultModel?: string;
27
+ /** Region-pinned endpoint, when the vendor serves one. Undefined = SDK default. */
28
+ baseUrl?: string;
29
+ }
30
+ /**
31
+ * The seam a BYOK deployment resolves per scope: key + provider → models.
32
+ *
33
+ * ```ts
34
+ * const models = await createLanguageModelProvider({ provider: 'mistral', apiKey });
35
+ * const result = await generateText({ model: models.languageModel(), prompt });
36
+ * ```
37
+ */
38
+ export declare function createLanguageModelProvider(config: LanguageModelProviderConfig): Promise<LanguageModelProvider>;
39
+ export interface EmbeddingModelProvider {
40
+ providerId: EmbeddingProviderId;
41
+ /** The resolved model id — what `embeddingDimensionsFor` should be asked about. */
42
+ modelId: string;
43
+ embeddingModel(): EmbeddingModelV4;
44
+ }
45
+ export interface EmbeddingModelProviderConfig {
46
+ provider?: EmbeddingProviderId;
47
+ apiKey: string;
48
+ /** Overrides the registry's default model. */
49
+ model?: string;
50
+ /** Region-pinned endpoint, when the vendor serves one. Undefined = SDK default. */
51
+ baseUrl?: string;
52
+ }
53
+ export declare function createEmbeddingModelProvider(config: EmbeddingModelProviderConfig): Promise<EmbeddingModelProvider>;
54
+ export interface LazyEmbeddingModelMeta {
55
+ /** Logged as the model's `provider`. */
56
+ provider: string;
57
+ modelId: string;
58
+ }
59
+ /**
60
+ * An `EmbeddingModelV4` whose provider is resolved on first use — for
61
+ * synchronous wiring (a DI registration, a registry built at boot) that
62
+ * cannot await {@link createEmbeddingModelProvider}. The V4 contract allows
63
+ * `maxEmbeddingsPerCall` / `supportsParallelCalls` to be promises, so the
64
+ * laziness is expressible without a wrapper the caller has to unwrap: `embed()`
65
+ * awaits them itself. `load` runs at most once; a failure is retried on the
66
+ * next call rather than cached.
67
+ */
68
+ export declare function createLazyEmbeddingModel(meta: LazyEmbeddingModelMeta, load: () => Promise<EmbeddingModelV4>): EmbeddingModelV4;
69
+ //#endregion
@@ -0,0 +1,106 @@
1
+ import { a as EMBEDDING_PROVIDERS, t as AI_PROVIDERS, u as createRefusalAwareModel } from "../../provider-registry-DhNXw_CC.js";
2
+ //#region src/ai/providers/index.ts
3
+ const LLM_LOADERS = {
4
+ anthropic: async (apiKey, baseURL) => {
5
+ const { createAnthropic } = await import("@ai-sdk/anthropic");
6
+ return createAnthropic({
7
+ apiKey,
8
+ baseURL
9
+ });
10
+ },
11
+ mistral: async (apiKey, baseURL) => {
12
+ const { createMistral } = await import("@ai-sdk/mistral");
13
+ return createMistral({
14
+ apiKey,
15
+ baseURL
16
+ });
17
+ }
18
+ };
19
+ const EMBEDDING_LOADERS = {
20
+ openai: async (apiKey, baseURL) => {
21
+ const { createOpenAI } = await import("@ai-sdk/openai");
22
+ const provider = createOpenAI({
23
+ apiKey,
24
+ baseURL
25
+ });
26
+ return (modelId) => provider.embedding(modelId);
27
+ },
28
+ mistral: async (apiKey, baseURL) => {
29
+ const { createMistral } = await import("@ai-sdk/mistral");
30
+ const provider = createMistral({
31
+ apiKey,
32
+ baseURL
33
+ });
34
+ return (modelId) => provider.embedding(modelId);
35
+ }
36
+ };
37
+ /** A language-model factory for a provider, loading its SDK on first use. */
38
+ function createLanguageModelFactory(providerId, apiKey, baseURL) {
39
+ return LLM_LOADERS[providerId](apiKey, baseURL);
40
+ }
41
+ /** An embedding-model factory for a provider, loading its SDK on first use. */
42
+ function createEmbeddingModelFactory(providerId, apiKey, baseURL) {
43
+ return EMBEDDING_LOADERS[providerId](apiKey, baseURL);
44
+ }
45
+ /**
46
+ * The seam a BYOK deployment resolves per scope: key + provider → models.
47
+ *
48
+ * ```ts
49
+ * const models = await createLanguageModelProvider({ provider: 'mistral', apiKey });
50
+ * const result = await generateText({ model: models.languageModel(), prompt });
51
+ * ```
52
+ */
53
+ async function createLanguageModelProvider(config) {
54
+ const providerId = config.provider ?? "anthropic";
55
+ const meta = AI_PROVIDERS[providerId];
56
+ const factory = await createLanguageModelFactory(providerId, config.apiKey, config.baseUrl);
57
+ const defaultModel = config.defaultModel ?? meta.defaultModel;
58
+ return {
59
+ providerId,
60
+ classifierModelId: meta.classifierModel,
61
+ languageModel: (modelId) => createRefusalAwareModel(factory(modelId ?? defaultModel))
62
+ };
63
+ }
64
+ async function createEmbeddingModelProvider(config) {
65
+ const providerId = config.provider ?? "openai";
66
+ const factory = await createEmbeddingModelFactory(providerId, config.apiKey, config.baseUrl);
67
+ const modelId = config.model ?? EMBEDDING_PROVIDERS[providerId].defaultModel;
68
+ return {
69
+ providerId,
70
+ modelId,
71
+ embeddingModel: () => factory(modelId)
72
+ };
73
+ }
74
+ /**
75
+ * An `EmbeddingModelV4` whose provider is resolved on first use — for
76
+ * synchronous wiring (a DI registration, a registry built at boot) that
77
+ * cannot await {@link createEmbeddingModelProvider}. The V4 contract allows
78
+ * `maxEmbeddingsPerCall` / `supportsParallelCalls` to be promises, so the
79
+ * laziness is expressible without a wrapper the caller has to unwrap: `embed()`
80
+ * awaits them itself. `load` runs at most once; a failure is retried on the
81
+ * next call rather than cached.
82
+ */
83
+ function createLazyEmbeddingModel(meta, load) {
84
+ let pending;
85
+ const model = () => {
86
+ pending ??= load().catch((error) => {
87
+ pending = void 0;
88
+ throw error;
89
+ });
90
+ return pending;
91
+ };
92
+ return {
93
+ specificationVersion: "v4",
94
+ provider: meta.provider,
95
+ modelId: meta.modelId,
96
+ get maxEmbeddingsPerCall() {
97
+ return model().then((m) => m.maxEmbeddingsPerCall);
98
+ },
99
+ get supportsParallelCalls() {
100
+ return model().then((m) => m.supportsParallelCalls);
101
+ },
102
+ doEmbed: (options) => model().then((m) => m.doEmbed(options))
103
+ };
104
+ }
105
+ //#endregion
106
+ export { createEmbeddingModelFactory, createEmbeddingModelProvider, createLanguageModelFactory, createLanguageModelProvider, createLazyEmbeddingModel };
@@ -0,0 +1,224 @@
1
+ import { wrapLanguageModel } from "ai";
2
+ //#region src/ai/refusal.ts
3
+ /** The unified finish reason the AI SDK reports for a declined generation. */
4
+ const REFUSAL_FINISH_REASON = "content-filter";
5
+ /**
6
+ * A generation the model declined to produce.
7
+ *
8
+ * Distinct from a transport failure on purpose: it is deterministic, so
9
+ * retrying the same prompt on the same model produces the same refusal. Callers
10
+ * that classify retryability must treat it as terminal — `defineAiStep` does
11
+ * this for every AI workflow step.
12
+ */
13
+ var AiRefusalError = class extends Error {
14
+ name = "AiRefusalError";
15
+ details;
16
+ constructor(details) {
17
+ super(refusalMessage(details));
18
+ this.details = details;
19
+ }
20
+ };
21
+ function refusalMessage(details) {
22
+ const parts = [`${details.modelId} declined to answer`];
23
+ if (details.category) parts.push(`(${details.category})`);
24
+ if (details.explanation) parts.push(`— ${details.explanation}`);
25
+ return parts.join(" ");
26
+ }
27
+ /** How deep to follow `cause` before giving up. Guards against pathological chains. */
28
+ const MAX_CAUSE_DEPTH = 8;
29
+ /**
30
+ * True when `error` is an {@link AiRefusalError}, or wraps one as its `cause`.
31
+ *
32
+ * Walks the chain because the SDK layer between the middleware and the caller
33
+ * may re-wrap: `generateObject`, for instance, reports parse failures as
34
+ * `NoObjectGeneratedError` with the original as `cause`. Depth-capped and
35
+ * cycle-safe.
36
+ */
37
+ function isAiRefusalError(error) {
38
+ const seen = /* @__PURE__ */ new Set();
39
+ let current = error;
40
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {
41
+ if (typeof current !== "object" || current === null || seen.has(current)) return false;
42
+ if (current instanceof AiRefusalError) return true;
43
+ seen.add(current);
44
+ current = current.cause;
45
+ }
46
+ return false;
47
+ }
48
+ /**
49
+ * Pull the provider's refusal breakdown out of `providerMetadata`.
50
+ *
51
+ * Scans every provider namespace for a `stopDetails` record rather than reading
52
+ * `providerMetadata.anthropic` directly — the shape is Anthropic's today, and
53
+ * the surrounding module is deliberately provider-agnostic. A provider that
54
+ * reports a refusal with no breakdown simply yields no fields.
55
+ */
56
+ function readRefusalDetails(providerMetadata) {
57
+ for (const namespace of Object.values(providerMetadata ?? {})) {
58
+ const stopDetails = namespace?.stopDetails;
59
+ if (typeof stopDetails !== "object" || stopDetails === null || Array.isArray(stopDetails)) continue;
60
+ const read = (key) => {
61
+ const value = stopDetails[key];
62
+ return typeof value === "string" && value.length > 0 ? value : void 0;
63
+ };
64
+ return {
65
+ ...read("category") ? { category: read("category") } : {},
66
+ ...read("explanation") ? { explanation: read("explanation") } : {},
67
+ ...read("recommendedModel") ? { recommendedModel: read("recommendedModel") } : {}
68
+ };
69
+ }
70
+ return {};
71
+ }
72
+ /**
73
+ * Wrap a model so a declined generation throws {@link AiRefusalError} instead
74
+ * of returning empty content.
75
+ *
76
+ * **`doGenerate` only, on purpose.** One-shot calls — every AI workflow step,
77
+ * every ad-hoc generation service, both classifiers — have no way to present a
78
+ * refusal to anyone: their contract is "return the value or fail", so a refusal
79
+ * that does not throw becomes an empty field or an unparseable object several
80
+ * frames away from its cause. The streaming path is the opposite case: the help
81
+ * chat is a conversation, where a decline is a legitimate turn the operator
82
+ * should *read*, and it already has a refusal surface (the `refusal` data part)
83
+ * for exactly that. `ChatAgentService` branches on the finish reason in
84
+ * `onFinish` instead, so the stream stays a stream.
85
+ */
86
+ function createRefusalAwareModel(model) {
87
+ return wrapLanguageModel({
88
+ model,
89
+ middleware: {
90
+ specificationVersion: "v4",
91
+ wrapGenerate: async ({ doGenerate }) => {
92
+ const result = await doGenerate();
93
+ if (result.finishReason.unified === "content-filter") throw new AiRefusalError({
94
+ modelId: model.modelId,
95
+ ...readRefusalDetails(result.providerMetadata),
96
+ ...result.finishReason.raw ? { rawFinishReason: result.finishReason.raw } : {}
97
+ });
98
+ return result;
99
+ }
100
+ }
101
+ });
102
+ }
103
+ //#endregion
104
+ //#region src/ai/provider-registry.ts
105
+ /**
106
+ * The provider registry — pure metadata about the vendors a BYOK deployment
107
+ * can point at: ids, default and cheap models, where a key is created, and
108
+ * **where the vendor processes what you send it**. Dependency-free, so a
109
+ * browser bundle (a provider/model picker, a residency badge) can import it;
110
+ * the SDK factories that need `@ai-sdk/*` live in `octaflow/ai/providers`.
111
+ *
112
+ * Adding a provider: an id here with its metadata, a factory in
113
+ * `providers/`, and pricing rows in `cost.ts`.
114
+ */
115
+ const AI_PROVIDER_IDS = ["anthropic", "mistral"];
116
+ const EMBEDDING_PROVIDER_IDS = ["openai", "mistral"];
117
+ /** What an unset provider resolves to. */
118
+ const DEFAULT_AI_PROVIDER = "anthropic";
119
+ const DEFAULT_EMBEDDING_PROVIDER = "openai";
120
+ const AI_PROVIDERS = {
121
+ anthropic: {
122
+ id: "anthropic",
123
+ label: "Anthropic",
124
+ residency: {
125
+ region: "third-country",
126
+ legalName: "Anthropic PBC",
127
+ country: "US"
128
+ },
129
+ defaultModel: "claude-sonnet-5",
130
+ classifierModel: "claude-haiku-4-5",
131
+ testModel: "claude-haiku-4-5",
132
+ keyPrefix: "sk-ant-",
133
+ consoleKeyUrl: "https://console.anthropic.com/settings/keys",
134
+ modelOptions: [
135
+ {
136
+ label: "Claude Opus 5",
137
+ value: "claude-opus-5"
138
+ },
139
+ {
140
+ label: "Claude Opus 4.8",
141
+ value: "claude-opus-4-8"
142
+ },
143
+ {
144
+ label: "Claude Sonnet 5",
145
+ value: "claude-sonnet-5"
146
+ },
147
+ {
148
+ label: "Claude Haiku 4.5",
149
+ value: "claude-haiku-4-5"
150
+ }
151
+ ]
152
+ },
153
+ mistral: {
154
+ id: "mistral",
155
+ label: "Mistral",
156
+ residency: {
157
+ region: "eu",
158
+ legalName: "Mistral AI SAS",
159
+ country: "FR"
160
+ },
161
+ defaultModel: "mistral-medium-latest",
162
+ classifierModel: "mistral-small-latest",
163
+ testModel: "mistral-small-latest",
164
+ keyPrefix: null,
165
+ consoleKeyUrl: "https://console.mistral.ai/api-keys",
166
+ modelOptions: [
167
+ {
168
+ label: "Mistral Medium 3.5",
169
+ value: "mistral-medium-latest"
170
+ },
171
+ {
172
+ label: "Mistral Large 3",
173
+ value: "mistral-large-latest"
174
+ },
175
+ {
176
+ label: "Mistral Small 4",
177
+ value: "mistral-small-latest"
178
+ }
179
+ ]
180
+ }
181
+ };
182
+ const EMBEDDING_PROVIDERS = {
183
+ openai: {
184
+ id: "openai",
185
+ label: "OpenAI",
186
+ residency: {
187
+ region: "third-country",
188
+ legalName: "OpenAI, L.L.C.",
189
+ country: "US"
190
+ },
191
+ defaultModel: "text-embedding-3-small",
192
+ consoleKeyUrl: "https://platform.openai.com/api-keys",
193
+ dimensionsByModel: {
194
+ "text-embedding-3-small": 1536,
195
+ "text-embedding-3-large": 3072
196
+ }
197
+ },
198
+ mistral: {
199
+ id: "mistral",
200
+ label: "Mistral",
201
+ residency: {
202
+ region: "eu",
203
+ legalName: "Mistral AI SAS",
204
+ country: "FR"
205
+ },
206
+ defaultModel: "mistral-embed",
207
+ consoleKeyUrl: "https://console.mistral.ai/api-keys",
208
+ dimensionsByModel: { "mistral-embed": 1024 }
209
+ }
210
+ };
211
+ /**
212
+ * Vector dimensions for an embedding model id. Unknown ids fall back to
213
+ * `fallback` (default 1536, the common OpenAI-compatible dimension) so
214
+ * free-form model ids keep a working column.
215
+ */
216
+ function embeddingDimensionsFor(modelId, fallback = 1536) {
217
+ for (const meta of Object.values(EMBEDDING_PROVIDERS)) {
218
+ const dims = meta.dimensionsByModel[modelId];
219
+ if (dims !== void 0) return dims;
220
+ }
221
+ return fallback;
222
+ }
223
+ //#endregion
224
+ export { EMBEDDING_PROVIDERS as a, AiRefusalError as c, isAiRefusalError as d, readRefusalDetails as f, DEFAULT_EMBEDDING_PROVIDER as i, REFUSAL_FINISH_REASON as l, AI_PROVIDER_IDS as n, EMBEDDING_PROVIDER_IDS as o, DEFAULT_AI_PROVIDER as r, embeddingDimensionsFor as s, AI_PROVIDERS as t, createRefusalAwareModel as u };
@@ -0,0 +1,81 @@
1
+ //#region src/ai/provider-registry.d.ts
2
+ /**
3
+ * The provider registry — pure metadata about the vendors a BYOK deployment
4
+ * can point at: ids, default and cheap models, where a key is created, and
5
+ * **where the vendor processes what you send it**. Dependency-free, so a
6
+ * browser bundle (a provider/model picker, a residency badge) can import it;
7
+ * the SDK factories that need `@ai-sdk/*` live in `octaflow/ai/providers`.
8
+ *
9
+ * Adding a provider: an id here with its metadata, a factory in
10
+ * `providers/`, and pricing rows in `cost.ts`.
11
+ */
12
+ declare const AI_PROVIDER_IDS: readonly ['anthropic', 'mistral'];
13
+ type AiProviderId = (typeof AI_PROVIDER_IDS)[number];
14
+ declare const EMBEDDING_PROVIDER_IDS: readonly ['openai', 'mistral'];
15
+ type EmbeddingProviderId = (typeof EMBEDDING_PROVIDER_IDS)[number];
16
+ /** What an unset provider resolves to. */
17
+ declare const DEFAULT_AI_PROVIDER: AiProviderId;
18
+ declare const DEFAULT_EMBEDDING_PROVIDER: EmbeddingProviderId;
19
+ /**
20
+ * Where a provider processes the payloads you send it.
21
+ *
22
+ * `'eu'` means the vendor is established in the EU/EEA and serves the API from
23
+ * there, so a call carries no third-country transfer under Art. 44 ff. GDPR.
24
+ * `'third-country'` means it does — your data-processing agreement then has
25
+ * to carry a transfer mechanism for that vendor.
26
+ *
27
+ * This is metadata about the *vendor endpoint called*, not a promise about a
28
+ * customer's own BYOK key: a customer who brings their own key contracts with
29
+ * the vendor directly.
30
+ */
31
+ type AiProcessingRegion = 'eu' | 'third-country';
32
+ interface AiVendorResidency {
33
+ region: AiProcessingRegion;
34
+ /** Legal entity, as it appears in a subprocessor annex. */
35
+ legalName: string;
36
+ /** Country of establishment, ISO 3166-1 alpha-2. */
37
+ country: string;
38
+ }
39
+ interface AiProviderModelOption {
40
+ label: string;
41
+ value: string;
42
+ }
43
+ interface AiProviderMeta {
44
+ id: AiProviderId;
45
+ label: string;
46
+ /** Where calls to this provider are processed — drives a residency badge and the subprocessor analysis. */
47
+ residency: AiVendorResidency;
48
+ /** Default chat/workflow model when nothing sets one. */
49
+ defaultModel: string;
50
+ /** Cheap model for classifier-style passes. */
51
+ classifierModel: string;
52
+ /** Cheap model for a "test connection" ping. */
53
+ testModel: string;
54
+ /** Well-known API key prefix for a soft format hint, or null when the provider has none. */
55
+ keyPrefix: string | null;
56
+ /** Where the operator creates an API key. */
57
+ consoleKeyUrl: string;
58
+ /** Curated model choices for a picker (free-form custom ids stay allowed). */
59
+ modelOptions: AiProviderModelOption[];
60
+ }
61
+ declare const AI_PROVIDERS: Record<AiProviderId, AiProviderMeta>;
62
+ interface EmbeddingProviderMeta {
63
+ id: EmbeddingProviderId;
64
+ label: string;
65
+ /** See {@link AiProviderMeta.residency}. */
66
+ residency: AiVendorResidency;
67
+ defaultModel: string;
68
+ /** Where the operator creates an API key. */
69
+ consoleKeyUrl: string;
70
+ /** Vector dimensions per known model id — must match the `vector(N)` column in your schema. */
71
+ dimensionsByModel: Record<string, number>;
72
+ }
73
+ declare const EMBEDDING_PROVIDERS: Record<EmbeddingProviderId, EmbeddingProviderMeta>;
74
+ /**
75
+ * Vector dimensions for an embedding model id. Unknown ids fall back to
76
+ * `fallback` (default 1536, the common OpenAI-compatible dimension) so
77
+ * free-form model ids keep a working column.
78
+ */
79
+ declare function embeddingDimensionsFor(modelId: string, fallback?: number): number;
80
+ //#endregion
81
+ export { AiProviderMeta as a, DEFAULT_AI_PROVIDER as c, EMBEDDING_PROVIDER_IDS as d, EmbeddingProviderId as f, AiProviderId as i, DEFAULT_EMBEDDING_PROVIDER as l, embeddingDimensionsFor as m, AI_PROVIDER_IDS as n, AiProviderModelOption as o, EmbeddingProviderMeta as p, AiProcessingRegion as r, AiVendorResidency as s, AI_PROVIDERS as t, EMBEDDING_PROVIDERS as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octaflow",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss, with an optional AI add-on (token/cost/quota). Layered single package.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,6 +24,11 @@
24
24
  "types": "./dist/dispatcher-pgboss/index.d.ts",
25
25
  "import": "./dist/dispatcher-pgboss/index.js",
26
26
  "default": "./dist/dispatcher-pgboss/index.js"
27
+ },
28
+ "./ai/providers": {
29
+ "types": "./dist/ai/providers/index.d.ts",
30
+ "import": "./dist/ai/providers/index.js",
31
+ "default": "./dist/ai/providers/index.js"
27
32
  }
28
33
  },
29
34
  "engines": {
@@ -58,6 +63,9 @@
58
63
  "zod"
59
64
  ],
60
65
  "peerDependencies": {
66
+ "@ai-sdk/anthropic": "^4.0.0",
67
+ "@ai-sdk/mistral": "^4.0.0",
68
+ "@ai-sdk/openai": "^4.0.0",
61
69
  "@ai-sdk/provider": "^4.0.0",
62
70
  "ai": "^7.0.0",
63
71
  "pg": "^8.0.0",
@@ -66,6 +74,15 @@
66
74
  "zod": "^4.0.0"
67
75
  },
68
76
  "peerDependenciesMeta": {
77
+ "@ai-sdk/anthropic": {
78
+ "optional": true
79
+ },
80
+ "@ai-sdk/mistral": {
81
+ "optional": true
82
+ },
83
+ "@ai-sdk/openai": {
84
+ "optional": true
85
+ },
69
86
  "@ai-sdk/provider": {
70
87
  "optional": true
71
88
  },
@@ -80,6 +97,9 @@
80
97
  }
81
98
  },
82
99
  "devDependencies": {
100
+ "@ai-sdk/anthropic": "^4.0.49",
101
+ "@ai-sdk/mistral": "^4.0.39",
102
+ "@ai-sdk/openai": "^4.0.58",
83
103
  "@ai-sdk/provider": "^4.0.10",
84
104
  "@testcontainers/postgresql": "^12.1.0",
85
105
  "@types/node": "^26.4.1",