octaflow 0.29.2 → 0.30.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.
package/README.md CHANGED
@@ -198,13 +198,14 @@ next to what your steps actually do.
198
198
  | 🧩 | **Typed DAG** — Zod-validated input/output per step; dependency outputs are typed |
199
199
  | ⚡ | **Auto-parallelism** — dependency-free steps run concurrently; a step starts when all its deps complete |
200
200
  | 🔁 | **Retry & timeout** — per-step `maxAttempts`, fixed/exponential backoff, wall-clock timeout |
201
+ | 🩹 | **Best-effort steps** — `onFailure: { output }` completes a step with a stand-in once its budget is spent, so one analysis that could not run costs a section of the result, not the run |
201
202
  | 💤 | **Durable sleep** — hold a step in the queue for N ms (survives restarts) |
202
203
  | 🔀 | **Conditional branching** — `when` guards skip a step and its branch; `join: 'any'` converges |
203
204
  | 🎯 | **Decisions** — typed choices with a confidence from a swappable decider (a function, a model, your own); below a threshold a person settles it |
204
205
  | ⏳ | **Deadlines** — a budget on a suspended step (fail, or continue with a stand-in answer) and on a whole run |
205
206
  | ♻️ | **Retry a failed run** — `retryWorkflow` resumes from the failure point; completed steps keep their output |
206
207
  | 🧹 | **Delete the runs about a record** — `purgeWorkflows({ entityRef })` cancels and deletes them with their sub-workflow runs and steps, for an erasure or a deleted record; refuses only while a step is executing |
207
- | 🚦 | **Concurrency & rate limiting** — per-step-type caps and token buckets via a pluggable gate |
208
+ | 🚦 | **Concurrency & rate limiting** — caps and token buckets per step type or per shared resource (`resources: ['llm:local']`), via a pluggable gate |
208
209
  | ⏰ | **Cron / scheduled starts** — fire workflows on a schedule (pg-boss) |
209
210
  | 🔑 | **Start idempotency** — a dedup key collapses double-clicks / overlapping ticks |
210
211
  | 🗺️ | **Dynamic fan-out / map** — spawn one child step per item of a runtime-sized list |
@@ -214,7 +215,7 @@ next to what your steps actually do.
214
215
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
215
216
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
216
217
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
217
- | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups |
218
+ | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, refusal-aware models, lenient JSON from local models (`withJsonRepair`), a vendor's refusal explained by key and hint (`explainProviderError`), a per-run circuit breaker, a connection test (`testProvider`) |
218
219
  | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
219
220
 
220
221
  ---
@@ -1,9 +1,10 @@
1
- import { A as TypedStep, H as RetryPolicy, M as TypedWorkflow, f as Decider, j as TypedStepContext, vt as FlowErrorShape, yt as Result } from "../index-B8SvtiYn.js";
1
+ import { A as TypedStep, H as RetryPolicy, M as TypedWorkflow, bt as Result, f as Decider, j as TypedStepContext, yt as FlowErrorShape } from "../index-Du05Lnwr.js";
2
2
  import { n as Logger } from "../logger-BCus3AOa.js";
3
- import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-BV-Vany9.js";
3
+ import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-mWaAKHqn.js";
4
4
  import { C as isSubprocessorResidency, S as embeddingDimensionsFor, _ as EmbeddingProviderMeta, a as AiProcessingRegion, b as OpenAiCompatibleProviderId, c as AiProviderModelOption, d as AiVendorResidency, f as DEFAULT_AI_PROVIDER, g as EmbeddingProviderId, h as EMBEDDING_PROVIDER_IDS, i as AiEndpointResidency, l as AiResidencyError, m as EMBEDDING_PROVIDERS, n as AI_PROVIDER_IDS, o as AiProviderId, p as DEFAULT_EMBEDDING_PROVIDER, r as AiEndpointRegion, s as AiProviderMeta, t as AI_PROVIDERS, u as AiResidencyResult, v as OPENAI_COMPATIBLE_PROVIDER, w as resolveEndpointResidency, x as OpenAiCompatibleProviderMeta, y as OPENAI_COMPATIBLE_PROVIDER_ID } from "../provider-registry-C_mOfdHH.js";
5
+ import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, b as PROVIDER_ERROR_REDACTED, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, g as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as ProbeableProvider, p as CircuitBreakerOptions, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as ProviderProbeFailure, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../provider-probe-l2Jmpuhr.js";
5
6
  import { z } from "zod";
6
- import { EmbeddingModelV4, LanguageModelV4, SharedV4ProviderMetadata } from "@ai-sdk/provider";
7
+ import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4Usage, SharedV4ProviderMetadata } from "@ai-sdk/provider";
7
8
  //#region src/ai/instrumented-model.d.ts
8
9
  export interface AccumulatedUsage {
9
10
  inputTokens: number;
@@ -134,6 +135,16 @@ interface DefineAiStepConfig<TInput extends Record<string, unknown>, TOutput ext
134
135
  timeoutMs?: number;
135
136
  /** Optional durable start delay in ms — held in the queue once the step is ready. */
136
137
  delayMs?: number;
138
+ /**
139
+ * What a terminal failure does — see `defineStep`'s `onFailure`. `{ output }`
140
+ * completes the step with a stand-in and the DAG carries on; the model's
141
+ * refusal or outage then costs one section of the result, not the run.
142
+ */
143
+ onFailure?: 'fail' | {
144
+ output: TOutput;
145
+ };
146
+ /** Resources the step draws on (the model, a vendor), for the admission gate — see `defineStep`. */
147
+ resources?: readonly string[];
137
148
  }
138
149
  /**
139
150
  * Define a type-safe AI workflow step. Identical to flow-core's `defineStep`
@@ -576,4 +587,157 @@ export declare function thinkingSettingsFor(providerId: string, modelId: string,
576
587
  budgetTokens: number;
577
588
  }): ThinkingSettings;
578
589
  //#endregion
579
- export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, OpenAiCompatibleProviderId, OpenAiCompatibleProviderMeta, embeddingDimensionsFor, isSubprocessorResidency, resolveEndpointResidency };
590
+ //#region src/ai/model-json.d.ts
591
+ /** Which step produced the JSON. `retry` and `convert` are `withJsonRepair`'s. */
592
+ export type ModelJsonRepair = 'fence' | 'prose' | 'array_merge' | 'retry' | 'convert';
593
+ /**
594
+ * The span or metric attribute that names a repair, in the GenAI convention's
595
+ * namespace so `@octabits-io/telemetry`'s redaction leaves it alone: its last
596
+ * segment is `repaired`, not a credential or a person's data. Set it on the
597
+ * step's span from `withJsonRepair`'s `onRepair`, and a dashboard counts
598
+ * repairs per model.
599
+ */
600
+ export declare const MODEL_JSON_REPAIRED_ATTRIBUTE = "gen_ai.response.repaired";
601
+ /** Where a repaired answer is also reported: `result.providerMetadata.octaflow.repaired`. */
602
+ export declare const MODEL_JSON_PROVIDER_METADATA_KEY = "octaflow";
603
+ export type ModelJsonErrorKey =
604
+ /** Nothing but whitespace, or an empty fence. */
605
+ 'model_json_empty' |
606
+ /** No step produced JSON: not valid as it is, and no outermost object or array parses. */
607
+ 'model_json_invalid' |
608
+ /** The JSON is not an object or an array of objects (a scalar, an array of scalars, a mixed array). */
609
+ 'model_json_not_object' |
610
+ /** The JSON parsed but the schema refused it; `issues` carries why. */
611
+ 'model_json_schema';
612
+ /** The default length of the excerpt a {@link ModelJsonError} keeps. */
613
+ export declare const DEFAULT_MODEL_JSON_EXCERPT_LENGTH = 500;
614
+ /**
615
+ * The answer could not be read as the JSON that was asked for.
616
+ *
617
+ * `message` names the reason and never quotes the answer: it is what lands in
618
+ * a step's `error` column and in a log line. `excerpt` is the first
619
+ * `excerptLength` characters (500 by default) for a debug trace that wants to
620
+ * see what the model said.
621
+ */
622
+ export declare class ModelJsonError extends Error {
623
+ readonly name = "ModelJsonError";
624
+ readonly key: ModelJsonErrorKey;
625
+ /** The first characters of the answer, cut at the configured length. */
626
+ readonly excerpt: string;
627
+ /** The schema's issues, for `model_json_schema`. */
628
+ readonly issues?: readonly unknown[];
629
+ /** The step that produced the JSON the schema then refused. */
630
+ readonly repair?: ModelJsonRepair;
631
+ constructor(args: {
632
+ key: ModelJsonErrorKey;
633
+ excerpt: string;
634
+ issues?: readonly unknown[];
635
+ repair?: ModelJsonRepair;
636
+ });
637
+ }
638
+ /** True when `error` is a {@link ModelJsonError}, or wraps one as its `cause` (`generateObject` re-wraps). */
639
+ export declare function isModelJsonError(error: unknown): error is ModelJsonError;
640
+ export interface ParseModelJsonOptions<T> {
641
+ /** Validate the value; the issues go on the error. */
642
+ schema?: z.ZodType<T>;
643
+ /**
644
+ * Merge an array of objects into one object: array fields concatenated in
645
+ * order, the first non-empty scalar kept, `null` never overwriting a value,
646
+ * nested objects merged the same way. For a model that answered one object
647
+ * per image when one object was asked for. Off by default: an array answer
648
+ * is then returned as the array.
649
+ */
650
+ merge?: boolean;
651
+ /** How many characters of the answer the error keeps. Default 500. */
652
+ excerptLength?: number;
653
+ }
654
+ export interface ModelJsonParsed<T> {
655
+ value: T;
656
+ /** The step that produced the JSON; absent when the answer parsed as it was. */
657
+ repair?: ModelJsonRepair;
658
+ }
659
+ /**
660
+ * Read a model's answer as JSON, leniently — see the module header for the
661
+ * steps. Returns the value, and which repair it took, or a {@link ModelJsonError}.
662
+ */
663
+ export declare function parseModelJson<T = unknown>(text: string, options?: ParseModelJsonOptions<T>): Result<ModelJsonParsed<T>, ModelJsonError>;
664
+ /**
665
+ * The same recovery as a `repairText` function for the AI SDK's
666
+ * `generateObject` / `Output.object`: given the raw text the SDK could not
667
+ * parse, the repaired JSON as a string, or `null` when no step helps (the SDK
668
+ * then reports its own `NoObjectGeneratedError`).
669
+ *
670
+ * ```ts
671
+ * await generateObject({ model, schema, prompt, repairText: modelJsonRepairText({ merge: true }) });
672
+ * ```
673
+ */
674
+ export declare function modelJsonRepairText(options?: {
675
+ merge?: boolean;
676
+ }): (args: {
677
+ text: string;
678
+ error: unknown;
679
+ }) => Promise<string | null>;
680
+ /** Internal: the recovery without the schema, shared with `withJsonRepair`. */
681
+ export type RecoveredModelJson = {
682
+ ok: true;
683
+ value: Record<string, unknown> | Record<string, unknown>[];
684
+ repair?: ModelJsonRepair;
685
+ } | {
686
+ ok: false;
687
+ key: Exclude<ModelJsonErrorKey, 'model_json_schema'>;
688
+ };
689
+ /** @internal */
690
+ export declare function recoverModelJson(text: string, options: {
691
+ merge: boolean;
692
+ }): RecoveredModelJson;
693
+ /**
694
+ * Merge objects a model answered "per item" into one: array fields are
695
+ * concatenated in order, nested objects merged the same way, and for a scalar
696
+ * the first non-empty value is kept — `null` and `''` never overwrite a value.
697
+ */
698
+ export declare function mergeModelJsonObjects(objects: readonly Record<string, unknown>[]): Record<string, unknown>;
699
+ /** @internal */
700
+ export declare function excerptOf(text: string, length?: number): string;
701
+ //#endregion
702
+ //#region src/ai/json-repair.d.ts
703
+ /** What `onRepair` is told: once per repaired answer, after the answer is final. */
704
+ export interface ModelJsonRepairEvent {
705
+ /** The wrapped model's id. */
706
+ modelId: string;
707
+ /** The step that produced the JSON — the value for `gen_ai.response.repaired`. */
708
+ repair: ModelJsonRepair;
709
+ /** Requests made beyond the first: 0 for a parse-level repair, 1 for a retry or a conversion, 2 for both. */
710
+ extraRequests: number;
711
+ }
712
+ export interface JsonRepairOptions {
713
+ /** Merge an array of objects into one — see `parseModelJson`. Skipped when the requested schema is itself an array. */
714
+ merge?: boolean;
715
+ /** On a parse failure, one more request with the provider's JSON mode off. Default `false`. */
716
+ retryWithoutJsonMode?: boolean;
717
+ /**
718
+ * On a parse failure (after the retry, when both are on), one call that is
719
+ * handed the schema and the prose and asked for the JSON. `true` uses the
720
+ * wrapped model; a `LanguageModelV4` uses that one, for a cheaper converter.
721
+ * Default off.
722
+ */
723
+ convertWith?: LanguageModelV4 | true;
724
+ /** How many characters of the answer the error keeps. Default 500. */
725
+ excerptLength?: number;
726
+ /** Told of every repaired answer: set `MODEL_JSON_REPAIRED_ATTRIBUTE` on the step's span, count it on a meter. Must not throw. */
727
+ onRepair?: (event: ModelJsonRepairEvent) => void;
728
+ }
729
+ /**
730
+ * Wrap a model so a JSON answer that is nearly JSON is read as the JSON it
731
+ * meant — see the module header. The result's text becomes the canonical
732
+ * JSON, and `providerMetadata.octaflow.repaired` names the step taken.
733
+ */
734
+ export declare function withJsonRepair(model: LanguageModelV4, options?: JsonRepairOptions): LanguageModelV4;
735
+ /**
736
+ * Two usages added field by field — `inputTokens`, `outputTokens` and
737
+ * whatever a provider adds — so the instrumented model outside the wrapper
738
+ * counts every request the repair made. An undefined on one side keeps the
739
+ * other; `raw` is the last call's.
740
+ */
741
+ export declare function addUsage(a: LanguageModelV4Usage, b: LanguageModelV4Usage): LanguageModelV4Usage;
742
+ //#endregion
743
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, CircuitBreaker, CircuitBreakerOptions, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, ExplainProviderErrorOptions, FATAL_PROVIDER_ERROR_KEYS, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, OllamaProbeOptions, OllamaProbeReport, OpenAiCompatibleProviderId, OpenAiCompatibleProviderMeta, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProbeableProvider, ProviderError, ProviderErrorExplanation, ProviderErrorKey, ProviderErrorPhrase, ProviderErrorPreset, ProviderProbeFailure, ProviderProbeOptions, ProviderProbeReport, createCircuitBreaker, embeddingDimensionsFor, explainProviderError, isFatalProviderError, isProviderError, isSubprocessorResidency, maskKeys, resolveEndpointResidency, testOllamaModel, testProvider, withCircuitBreaker };
package/dist/ai/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { D as ok, E as err, T as noopLogger, _ as defineStep, m as buildWorkflow } from "../core-C9h3-KFr.js";
2
- import { a as readRefusalDetails, i as isAiRefusalError, n as REFUSAL_FINISH_REASON, r as createRefusalAwareModel, t as AiRefusalError } from "../refusal-BsL2HPDb.js";
1
+ import { n as err, r as ok, t as noopLogger } from "../logger-epkdcXDA.js";
2
+ import { _ as defineStep, m as buildWorkflow } from "../core-DBqxBk3R.js";
3
+ import { S as readRefusalDetails, _ as maskKeys, a as testProvider, b as createRefusalAwareModel, c as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, d as PROVIDER_ERROR_REDACTED, f as PROVIDER_ERROR_STATUS, g as isProviderError, h as isFatalProviderError, i as testOllamaModel, l as FATAL_PROVIDER_ERROR_KEYS, m as explainProviderError, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as createCircuitBreaker, p as ProviderError, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as withCircuitBreaker, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as PROVIDER_ERROR_PHRASES, v as AiRefusalError, x as isAiRefusalError, y as REFUSAL_FINISH_REASON } from "../provider-probe-CGN2MpaZ.js";
3
4
  import { a as EMBEDDING_PROVIDERS, c as OPENAI_COMPATIBLE_PROVIDER_ID, d as resolveEndpointResidency, i as DEFAULT_EMBEDDING_PROVIDER, l as embeddingDimensionsFor, n as AI_PROVIDER_IDS, o as EMBEDDING_PROVIDER_IDS, r as DEFAULT_AI_PROVIDER, s as OPENAI_COMPATIBLE_PROVIDER, t as AI_PROVIDERS, u as isSubprocessorResidency } from "../provider-registry-B91lgOmu.js";
4
5
  import { z } from "zod";
5
6
  import { Output, generateText, wrapEmbeddingModel, wrapLanguageModel } from "ai";
@@ -754,4 +755,383 @@ function thinkingSettingsFor(providerId, modelId, opts) {
754
755
  return { temperature: opts.temperature };
755
756
  }
756
757
  //#endregion
757
- export { AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_PRICING, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createRefusalAwareModel, createUsageAccumulator, defineAiStep, embeddingDimensionsFor, estimateCostMicros, isAiRefusalError, isSubprocessorResidency, readRefusalDetails, resolveEndpointResidency, supportsAdaptiveThinking, supportsToolSearch, thinkingSettingsFor };
758
+ //#region src/ai/model-json.ts
759
+ /**
760
+ * The span or metric attribute that names a repair, in the GenAI convention's
761
+ * namespace so `@octabits-io/telemetry`'s redaction leaves it alone: its last
762
+ * segment is `repaired`, not a credential or a person's data. Set it on the
763
+ * step's span from `withJsonRepair`'s `onRepair`, and a dashboard counts
764
+ * repairs per model.
765
+ */
766
+ const MODEL_JSON_REPAIRED_ATTRIBUTE = "gen_ai.response.repaired";
767
+ /** Where a repaired answer is also reported: `result.providerMetadata.octaflow.repaired`. */
768
+ const MODEL_JSON_PROVIDER_METADATA_KEY = "octaflow";
769
+ /** The default length of the excerpt a {@link ModelJsonError} keeps. */
770
+ const DEFAULT_MODEL_JSON_EXCERPT_LENGTH = 500;
771
+ /**
772
+ * The answer could not be read as the JSON that was asked for.
773
+ *
774
+ * `message` names the reason and never quotes the answer: it is what lands in
775
+ * a step's `error` column and in a log line. `excerpt` is the first
776
+ * `excerptLength` characters (500 by default) for a debug trace that wants to
777
+ * see what the model said.
778
+ */
779
+ var ModelJsonError = class extends Error {
780
+ name = "ModelJsonError";
781
+ key;
782
+ /** The first characters of the answer, cut at the configured length. */
783
+ excerpt;
784
+ /** The schema's issues, for `model_json_schema`. */
785
+ issues;
786
+ /** The step that produced the JSON the schema then refused. */
787
+ repair;
788
+ constructor(args) {
789
+ super(modelJsonMessage(args.key, args.issues));
790
+ this.key = args.key;
791
+ this.excerpt = args.excerpt;
792
+ if (args.issues) this.issues = args.issues;
793
+ if (args.repair) this.repair = args.repair;
794
+ }
795
+ };
796
+ function modelJsonMessage(key, issues) {
797
+ switch (key) {
798
+ case "model_json_empty": return "The model answered with no JSON.";
799
+ case "model_json_invalid": return "The model answer is not JSON: no fenced block, outermost object or outermost array parses.";
800
+ case "model_json_not_object": return "The model answer is JSON but not an object or an array of objects.";
801
+ case "model_json_schema": {
802
+ const count = issues?.length ?? 0;
803
+ return `The model answer is JSON but not the shape asked for (${count} ${count === 1 ? "issue" : "issues"}).`;
804
+ }
805
+ }
806
+ }
807
+ /** True when `error` is a {@link ModelJsonError}, or wraps one as its `cause` (`generateObject` re-wraps). */
808
+ function isModelJsonError(error) {
809
+ const seen = /* @__PURE__ */ new Set();
810
+ let current = error;
811
+ for (let depth = 0; depth < 8; depth++) {
812
+ if (typeof current !== "object" || current === null || seen.has(current)) return false;
813
+ if (current instanceof ModelJsonError) return true;
814
+ seen.add(current);
815
+ current = current.cause;
816
+ }
817
+ return false;
818
+ }
819
+ /**
820
+ * Read a model's answer as JSON, leniently — see the module header for the
821
+ * steps. Returns the value, and which repair it took, or a {@link ModelJsonError}.
822
+ */
823
+ function parseModelJson(text, options = {}) {
824
+ const excerpt = excerptOf(text, options.excerptLength);
825
+ const recovered = recoverModelJson(text, { merge: options.merge ?? false });
826
+ if (!recovered.ok) return {
827
+ ok: false,
828
+ error: new ModelJsonError({
829
+ key: recovered.key,
830
+ excerpt
831
+ })
832
+ };
833
+ const repair = recovered.repair;
834
+ if (!options.schema) return {
835
+ ok: true,
836
+ value: repair ? {
837
+ value: recovered.value,
838
+ repair
839
+ } : { value: recovered.value }
840
+ };
841
+ const checked = options.schema.safeParse(recovered.value);
842
+ if (!checked.success) return {
843
+ ok: false,
844
+ error: new ModelJsonError({
845
+ key: "model_json_schema",
846
+ excerpt,
847
+ issues: checked.error.issues,
848
+ ...repair ? { repair } : {}
849
+ })
850
+ };
851
+ return {
852
+ ok: true,
853
+ value: repair ? {
854
+ value: checked.data,
855
+ repair
856
+ } : { value: checked.data }
857
+ };
858
+ }
859
+ /**
860
+ * The same recovery as a `repairText` function for the AI SDK's
861
+ * `generateObject` / `Output.object`: given the raw text the SDK could not
862
+ * parse, the repaired JSON as a string, or `null` when no step helps (the SDK
863
+ * then reports its own `NoObjectGeneratedError`).
864
+ *
865
+ * ```ts
866
+ * await generateObject({ model, schema, prompt, repairText: modelJsonRepairText({ merge: true }) });
867
+ * ```
868
+ */
869
+ function modelJsonRepairText(options = {}) {
870
+ return async ({ text }) => {
871
+ const recovered = recoverModelJson(text, { merge: options.merge ?? false });
872
+ return recovered.ok && recovered.repair ? JSON.stringify(recovered.value) : null;
873
+ };
874
+ }
875
+ /** @internal */
876
+ function recoverModelJson(text, options) {
877
+ const trimmed = text.trim();
878
+ if (!trimmed) return {
879
+ ok: false,
880
+ key: "model_json_empty"
881
+ };
882
+ let body = trimmed;
883
+ let repair;
884
+ const fenced = unfence(trimmed);
885
+ if (fenced !== void 0) {
886
+ body = fenced.trim();
887
+ repair = "fence";
888
+ if (!body) return {
889
+ ok: false,
890
+ key: "model_json_empty"
891
+ };
892
+ }
893
+ let value = tryParse(body);
894
+ if (value === INVALID) {
895
+ const cut = outermost(body);
896
+ if (cut === void 0) return {
897
+ ok: false,
898
+ key: "model_json_invalid"
899
+ };
900
+ value = cut;
901
+ repair = "prose";
902
+ }
903
+ if (isPlainObject(value)) return repair ? {
904
+ ok: true,
905
+ value,
906
+ repair
907
+ } : {
908
+ ok: true,
909
+ value
910
+ };
911
+ if (Array.isArray(value) && value.every(isPlainObject)) {
912
+ const objects = value;
913
+ if (!options.merge) return repair ? {
914
+ ok: true,
915
+ value: objects,
916
+ repair
917
+ } : {
918
+ ok: true,
919
+ value: objects
920
+ };
921
+ return {
922
+ ok: true,
923
+ value: mergeModelJsonObjects(objects),
924
+ repair: "array_merge"
925
+ };
926
+ }
927
+ return {
928
+ ok: false,
929
+ key: "model_json_not_object"
930
+ };
931
+ }
932
+ const INVALID = Symbol("invalid");
933
+ function tryParse(text) {
934
+ try {
935
+ return JSON.parse(text);
936
+ } catch {
937
+ return INVALID;
938
+ }
939
+ }
940
+ const FENCE_RE = /```[A-Za-z0-9_-]*[ \t]*\r?\n?([\s\S]*?)(?:```|$)/;
941
+ /** The content of the first fenced block, or `undefined` when there is none. An unterminated fence runs to the end. */
942
+ function unfence(text) {
943
+ const match = FENCE_RE.exec(text);
944
+ return match ? match[1] : void 0;
945
+ }
946
+ /**
947
+ * The outermost `{ … }` or `[ … ]` in the text, by first opener and last
948
+ * closer; whichever opener comes first is tried first, the other after it.
949
+ * Only a candidate that parses *and* has an accepted shape wins, so a stray
950
+ * `[1]` in the prose before the object does not shadow it; a cut that is JSON
951
+ * of another shape (`["paid"]` inside an object the prose broke) is not the
952
+ * answer either, and the whole is refused as invalid.
953
+ */
954
+ function outermost(text) {
955
+ const candidates = [["{", "}"], ["[", "]"]];
956
+ const objectAt = text.indexOf("{");
957
+ const arrayAt = text.indexOf("[");
958
+ if (arrayAt !== -1 && (objectAt === -1 || arrayAt < objectAt)) candidates.reverse();
959
+ for (const [open, close] of candidates) {
960
+ const start = text.indexOf(open);
961
+ const end = text.lastIndexOf(close);
962
+ if (start === -1 || end <= start) continue;
963
+ const parsed = tryParse(text.slice(start, end + 1));
964
+ if (parsed === INVALID) continue;
965
+ if (isPlainObject(parsed) || Array.isArray(parsed) && parsed.every(isPlainObject)) return parsed;
966
+ }
967
+ }
968
+ function isPlainObject(value) {
969
+ return typeof value === "object" && value !== null && !Array.isArray(value);
970
+ }
971
+ function isEmpty(value) {
972
+ return value === void 0 || value === null || value === "";
973
+ }
974
+ /**
975
+ * Merge objects a model answered "per item" into one: array fields are
976
+ * concatenated in order, nested objects merged the same way, and for a scalar
977
+ * the first non-empty value is kept — `null` and `''` never overwrite a value.
978
+ */
979
+ function mergeModelJsonObjects(objects) {
980
+ const out = {};
981
+ for (const object of objects) for (const [key, incoming] of Object.entries(object)) {
982
+ const current = out[key];
983
+ if (Array.isArray(current) && Array.isArray(incoming)) out[key] = [...current, ...incoming];
984
+ else if (isPlainObject(current) && isPlainObject(incoming)) out[key] = mergeModelJsonObjects([current, incoming]);
985
+ else if (!(key in out) || isEmpty(current)) out[key] = incoming;
986
+ }
987
+ return out;
988
+ }
989
+ /** @internal */
990
+ function excerptOf(text, length = 500) {
991
+ return text.length > length ? text.slice(0, length) : text;
992
+ }
993
+ //#endregion
994
+ //#region src/ai/json-repair.ts
995
+ /**
996
+ * Wrap a model so a JSON answer that is nearly JSON is read as the JSON it
997
+ * meant — see the module header. The result's text becomes the canonical
998
+ * JSON, and `providerMetadata.octaflow.repaired` names the step taken.
999
+ */
1000
+ function withJsonRepair(model, options = {}) {
1001
+ return wrapLanguageModel({
1002
+ model,
1003
+ middleware: {
1004
+ specificationVersion: "v4",
1005
+ wrapGenerate: async ({ doGenerate, params, model: inner }) => {
1006
+ const format = params.responseFormat;
1007
+ if (format?.type !== "json") return doGenerate();
1008
+ const merge = (options.merge ?? false) && format.schema?.type !== "array";
1009
+ const first = await doGenerate();
1010
+ const text = textOf(first);
1011
+ if (text === void 0) return first;
1012
+ const read = recoverModelJson(text, { merge });
1013
+ if (read.ok) {
1014
+ if (!read.repair) return first;
1015
+ return repaired(first, read.value, read.repair, 0, inner, options);
1016
+ }
1017
+ let usage = first.usage;
1018
+ let prose = text;
1019
+ let lastKey = read.key;
1020
+ let extraRequests = 0;
1021
+ if (options.retryWithoutJsonMode) {
1022
+ const second = await inner.doGenerate({
1023
+ ...params,
1024
+ responseFormat: { type: "text" }
1025
+ });
1026
+ extraRequests += 1;
1027
+ usage = addUsage(usage, second.usage);
1028
+ const retried = textOf(second) ?? "";
1029
+ const again = recoverModelJson(retried, { merge });
1030
+ if (again.ok) return repaired({
1031
+ ...second,
1032
+ usage
1033
+ }, again.value, "retry", extraRequests, inner, options);
1034
+ prose = retried;
1035
+ lastKey = again.key;
1036
+ }
1037
+ if (options.convertWith) {
1038
+ const third = await (options.convertWith === true ? inner : options.convertWith).doGenerate(conversionCall(params, prose, format.schema));
1039
+ extraRequests += 1;
1040
+ usage = addUsage(usage, third.usage);
1041
+ const converted = textOf(third) ?? "";
1042
+ const again = recoverModelJson(converted, { merge });
1043
+ if (again.ok) return repaired({
1044
+ ...third,
1045
+ usage
1046
+ }, again.value, "convert", extraRequests, inner, options);
1047
+ prose = converted;
1048
+ lastKey = again.key;
1049
+ }
1050
+ throw new ModelJsonError({
1051
+ key: lastKey,
1052
+ excerpt: excerptOf(prose, options.excerptLength)
1053
+ });
1054
+ }
1055
+ }
1056
+ });
1057
+ }
1058
+ /** The answer's text, all text parts joined; `undefined` when the answer carries no text (tool calls alone). */
1059
+ function textOf(result) {
1060
+ const parts = result.content.filter((part) => part.type === "text");
1061
+ if (parts.length === 0) return void 0;
1062
+ return parts.map((part) => part.text).join("");
1063
+ }
1064
+ function repaired(result, value, repair, extraRequests, inner, options) {
1065
+ options.onRepair?.({
1066
+ modelId: inner.modelId,
1067
+ repair,
1068
+ extraRequests
1069
+ });
1070
+ return {
1071
+ ...result,
1072
+ content: [...result.content.filter((part) => part.type !== "text"), {
1073
+ type: "text",
1074
+ text: JSON.stringify(value)
1075
+ }],
1076
+ providerMetadata: {
1077
+ ...result.providerMetadata,
1078
+ [MODEL_JSON_PROVIDER_METADATA_KEY]: {
1079
+ ...result.providerMetadata?.[MODEL_JSON_PROVIDER_METADATA_KEY],
1080
+ repaired: repair
1081
+ }
1082
+ }
1083
+ };
1084
+ }
1085
+ /** The conversion request: the schema and the prose, JSON asked for, nothing of the original prompt. */
1086
+ function conversionCall(params, prose, schema) {
1087
+ return {
1088
+ prompt: [{
1089
+ role: "user",
1090
+ content: [{
1091
+ type: "text",
1092
+ text: [
1093
+ "Convert the answer below into a single JSON value that matches the schema. Output the JSON only: no fence, no prose.",
1094
+ schema ? `Schema:\n${JSON.stringify(schema)}` : "Shape: one JSON object.",
1095
+ `Answer:\n${prose}`
1096
+ ].join("\n\n")
1097
+ }]
1098
+ }],
1099
+ responseFormat: params.responseFormat,
1100
+ temperature: 0,
1101
+ ...params.maxOutputTokens !== void 0 ? { maxOutputTokens: params.maxOutputTokens } : {},
1102
+ ...params.abortSignal ? { abortSignal: params.abortSignal } : {},
1103
+ ...params.headers ? { headers: params.headers } : {}
1104
+ };
1105
+ }
1106
+ /**
1107
+ * Two usages added field by field — `inputTokens`, `outputTokens` and
1108
+ * whatever a provider adds — so the instrumented model outside the wrapper
1109
+ * counts every request the repair made. An undefined on one side keeps the
1110
+ * other; `raw` is the last call's.
1111
+ */
1112
+ function addUsage(a, b) {
1113
+ const { raw: _rawA, ...restA } = a;
1114
+ const { raw: rawB, ...restB } = b;
1115
+ const sum = addNumbers(restA, restB);
1116
+ return rawB ? {
1117
+ ...sum,
1118
+ raw: rawB
1119
+ } : sum;
1120
+ }
1121
+ function addNumbers(a, b) {
1122
+ const out = {};
1123
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
1124
+ const x = a[key];
1125
+ const y = b[key];
1126
+ if (typeof x === "number" && typeof y === "number") out[key] = x + y;
1127
+ else if (typeof x === "number" || typeof y === "number") out[key] = typeof x === "number" ? x : y;
1128
+ else if (isRecord(x) && isRecord(y)) out[key] = addNumbers(x, y);
1129
+ else out[key] = x ?? y;
1130
+ }
1131
+ return out;
1132
+ }
1133
+ function isRecord(value) {
1134
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1135
+ }
1136
+ //#endregion
1137
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_JSON_EXCERPT_LENGTH, DEFAULT_MODEL_PRICING, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, FATAL_PROVIDER_ERROR_KEYS, MODEL_JSON_PROVIDER_METADATA_KEY, MODEL_JSON_REPAIRED_ATTRIBUTE, ModelJsonError, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProviderError, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, addUsage, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCircuitBreaker, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createRefusalAwareModel, createUsageAccumulator, defineAiStep, embeddingDimensionsFor, estimateCostMicros, excerptOf, explainProviderError, isAiRefusalError, isFatalProviderError, isModelJsonError, isProviderError, isSubprocessorResidency, maskKeys, mergeModelJsonObjects, modelJsonRepairText, parseModelJson, readRefusalDetails, recoverModelJson, resolveEndpointResidency, supportsAdaptiveThinking, supportsToolSearch, testOllamaModel, testProvider, thinkingSettingsFor, withCircuitBreaker, withJsonRepair };
@@ -1,4 +1,5 @@
1
1
  import { b as OpenAiCompatibleProviderId, g as EmbeddingProviderId, i as AiEndpointResidency, o as AiProviderId } from "../../provider-registry-C_mOfdHH.js";
2
+ import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, o as ProbeableProvider, p as CircuitBreakerOptions, s as ProviderProbeFailure, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../../provider-probe-l2Jmpuhr.js";
2
3
  import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
3
4
  //#region src/ai/providers/index.d.ts
4
5
  export type LanguageModelFactory = (modelId: string) => LanguageModelV4;
@@ -205,4 +206,5 @@ export interface OpenAiCompatibleGatewayProvider {
205
206
  * in the same review.
206
207
  */
207
208
  export declare function createOpenAiCompatibleGatewayProvider(config: OpenAiCompatibleGatewayConfig): Promise<OpenAiCompatibleGatewayProvider>;
208
- //#endregion
209
+ //#endregion
210
+ export { type CircuitBreaker, type CircuitBreakerOptions, type ExplainProviderErrorOptions, FATAL_PROVIDER_ERROR_KEYS, type OllamaProbeOptions, type OllamaProbeReport, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_STATUS, type ProbeableProvider, ProviderError, type ProviderErrorExplanation, type ProviderErrorKey, type ProviderErrorPhrase, type ProviderErrorPreset, type ProviderProbeFailure, type ProviderProbeOptions, type ProviderProbeReport, createCircuitBreaker, explainProviderError, isFatalProviderError, isProviderError, maskKeys, testOllamaModel, testProvider, withCircuitBreaker };
@@ -1,4 +1,4 @@
1
- import { r as createRefusalAwareModel } from "../../refusal-BsL2HPDb.js";
1
+ import { _ as maskKeys, a as testProvider, b as createRefusalAwareModel, f as PROVIDER_ERROR_STATUS, g as isProviderError, h as isFatalProviderError, i as testOllamaModel, l as FATAL_PROVIDER_ERROR_KEYS, m as explainProviderError, o as createCircuitBreaker, p as ProviderError, s as withCircuitBreaker, u as PROVIDER_ERROR_PHRASES } from "../../provider-probe-CGN2MpaZ.js";
2
2
  import { a as EMBEDDING_PROVIDERS, c as OPENAI_COMPATIBLE_PROVIDER_ID, d as resolveEndpointResidency, t as AI_PROVIDERS } from "../../provider-registry-B91lgOmu.js";
3
3
  import { wrapLanguageModel } from "ai";
4
4
  //#region src/ai/providers/index.ts
@@ -280,4 +280,4 @@ async function createOpenAiCompatibleGatewayProvider(config) {
280
280
  };
281
281
  }
282
282
  //#endregion
283
- export { AiProviderConfigError, createEmbeddingModelFactory, createEmbeddingModelProvider, createLanguageModelFactory, createLanguageModelProvider, createLazyEmbeddingModel, createOpenAiCompatibleEmbeddingProvider, createOpenAiCompatibleGatewayProvider, createOpenAiCompatibleModelProvider };
283
+ export { AiProviderConfigError, FATAL_PROVIDER_ERROR_KEYS, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_STATUS, ProviderError, createCircuitBreaker, createEmbeddingModelFactory, createEmbeddingModelProvider, createLanguageModelFactory, createLanguageModelProvider, createLazyEmbeddingModel, createOpenAiCompatibleEmbeddingProvider, createOpenAiCompatibleGatewayProvider, createOpenAiCompatibleModelProvider, explainProviderError, isFatalProviderError, isProviderError, maskKeys, testOllamaModel, testProvider, withCircuitBreaker };