octaflow 0.33.0 → 0.34.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
@@ -216,7 +216,9 @@ next to what your steps actually do.
216
216
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
217
217
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
218
218
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
219
- | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, a `usage` sink that books each step into `@octabits-io/metering` (the paid APIs around the model in the same table, priced at read time), 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`) |
219
+ | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, a `usage` sink that books each step into `@octabits-io/metering` (the paid APIs around the model in the same table, priced at read time), 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`), Ollama's native API as language and embedding models (`createOllama` in `octaflow/ai/providers`: `num_ctx`, thinking and `keep_alive` per model or per call, no dependency) |
220
+ | 🗂️ | **Model providers an administrator manages** — `createModelProviders` (`octaflow/ai`): providers as rows, each with its kind, server, region, a key that crosses a cipher seam, and its models with declared capabilities (temperature, JSON mode, vision, video, tools, embedding); `createPgModelProviderStore` + `modelProviderDdl` (`octaflow/postgres`), `modelProviderColumns` (`octaflow/drizzle`); `createModelResolver` (`octaflow/ai/providers`) builds the AI SDK model from a row, shapes each call to what the model declares, hands the host its `destination` for the disclosure guard, and rebuilds after a write |
221
+ | 🏃 | **Model runners** — a model the app cannot reach, served by a process that connects *out*: a provider row of kind `runner` carries tags instead of a URL, a runner registered with a token (`octaflow/ai/runner`: `createModelRunnerHub`, shown once, kept as a hash) subscribes with its tags, claims the calls by long-poll through `@octabits-io/server/fetch/octaflow`'s `createModelRunnerHandler` and posts the answers back; the mailbox is two tables (`createPgModelRunnerStore` + `modelRunnerDdl`, `modelRunnerColumns` + `modelRunnerRequestColumns`) whose rows live only while a call is in flight; nobody online fails the call at once, so the breaker trips and the probe answers in milliseconds |
220
222
  | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
221
223
 
222
224
  ---
@@ -249,9 +251,10 @@ pnpm add ai @ai-sdk/provider
249
251
 
250
252
  ### The Postgres tables
251
253
 
252
- `octaflow/postgres` owns five tables and ships their DDL: `flowStoreDdl()` (`flow_workflow`,
253
- `flow_workflow_step`), `flowEventDdl()` (`flow_step_event`) and `flowGateDdl()`
254
- (`flow_rate_bucket`, `flow_step_lease`). Each is idempotent `CREATE TABLE IF NOT EXISTS`, takes
254
+ `octaflow/postgres` owns eight tables and ships their DDL: `flowStoreDdl()` (`flow_workflow`,
255
+ `flow_workflow_step`), `flowEventDdl()` (`flow_step_event`), `flowGateDdl()`
256
+ (`flow_rate_bucket`, `flow_step_lease`), `modelProviderDdl()` (`model_provider`) and
257
+ `modelRunnerDdl()` (`model_runner`, `model_runner_request`). Each is idempotent `CREATE TABLE IF NOT EXISTS`, takes
255
258
  a `schema`, and is meant to be pasted into your own migration system; `applySchema(pool, ddl)`
256
259
  is the dev and test shortcut.
257
260
 
@@ -269,8 +272,9 @@ column set, so a host on Drizzle declares it in its own schema and drizzle-kit o
269
272
  migration. Where the store's queries are one SQL statement by nature — every one of octaflow's
270
273
  is — `./drizzle` ships the columns only, and the store stays the SQL one. So
271
274
  `octaflow/drizzle` exports `flowWorkflowColumns`, `flowWorkflowStepColumns`,
272
- `flowStepEventColumns`, `flowRateBucketColumns` and `flowStepLeaseColumns` (`drizzle-orm` an
273
- optional peer) and nothing that runs a query. The foreign keys, the composite keys, the unique
275
+ `flowStepEventColumns`, `flowRateBucketColumns`, `flowStepLeaseColumns`, `modelProviderColumns`,
276
+ `modelRunnerColumns` and `modelRunnerRequestColumns` (`drizzle-orm` an optional peer) and
277
+ nothing that runs a query. The foreign keys, the composite keys, the unique
274
278
  constraint and the indexes are yours to declare on the `pgTable`; each column set's doc lists
275
279
  the ones its DDL creates, and two of them the store relies on:
276
280
 
@@ -314,6 +318,56 @@ The store stays plain SQL, bound to the `schema` you hand `createPgWorkflowStore
314
318
  declare the tables under the same schema and names. A test in the package holds every column
315
319
  set and its DDL equal, column for column: a column added to one side alone fails the build.
316
320
 
321
+ ### Model runners — a model that connects out
322
+
323
+ A model the app cannot reach: a laptop with the model loaded for a demo, a GPU box behind
324
+ someone's firewall, a machine that is only on when it is needed. The runner never opens a
325
+ port. It connects *out* to the app with a token, subscribes with its tags, claims the model
326
+ calls that carry those tags and posts the answers back — the shape of a self-hosted CI
327
+ runner. The mailbox is Postgres, because the call runs in the worker and the door the runner
328
+ knocks on runs in the API; a request row lives only while the call is in flight.
329
+
330
+ ```ts
331
+ // the app — once, beside the providers
332
+ import { createModelRunnerHub } from 'octaflow/ai/runner';
333
+ import { createPgModelRunnerStore } from 'octaflow/postgres';
334
+
335
+ const runners = createModelRunnerHub({ store: createPgModelRunnerStore({ executor }) });
336
+ const resolver = createModelResolver({ providers, runners, wrap });
337
+
338
+ // an administrator: the provider row carries tags, the runner gets its token once
339
+ await providers.save({ id: 'mac', kind: 'runner', label: 'The Mac', region: 'self-hosted', tags: ['qwen'], models: [{ id: 'qwen3:32b', settings: { numCtx: 16_384 } }] });
340
+ const { token } = (await runners.registerRunner({ id: 'dani-mac', label: "Dani's Mac", tags: ['qwen', 'mac'] })).value;
341
+
342
+ // the API: the door, behind the host's rate limit
343
+ import { createModelRunnerHandler } from '@octabits-io/server/fetch/octaflow';
344
+ export const POST = compose(withErrorMapping())(createModelRunnerHandler({ prefix: '/api/model-runner', hub: runners }));
345
+
346
+ // the runner, on the machine with the model
347
+ import { createHttpRunnerClient, runModelRunner } from 'octaflow/ai/runner';
348
+ import { createOllama } from 'octaflow/ai/providers';
349
+ await runModelRunner({
350
+ client: createHttpRunnerClient({ url: 'https://app.example/api/model-runner', token }),
351
+ backend: createOllama({ baseUrl: 'http://localhost:11434' }),
352
+ });
353
+ ```
354
+
355
+ A runner may claim a request when it has **every tag** of the request — the provider row's
356
+ tags — as a CI job's labels must all be on the runner. A call with nobody online for its
357
+ tags fails at once with `provider_unreachable`, so `withCircuitBreaker` trips and
358
+ `testProvider` reports it in milliseconds instead of a timeout; whether another provider is
359
+ tried is the host's resolver's choice, through the same disclosure gate. A claim lasts 30 s
360
+ without a heartbeat; an expired claim is requeued once and failed the second time. The
361
+ region is the administrator's declaration about where runners with these tags are, as for
362
+ every provider, and the disclosure gate reads it the same way.
363
+
364
+ The wire is the AI SDK's own call and answer as JSON — binary file data as base64, a URL
365
+ restored on the other side — so the runner's backend is any `LanguageModelV4`: `createOllama`
366
+ from this package, or `@ai-sdk/openai-compatible` for mlx_lm.server, vLLM and llama.cpp. The
367
+ first version answers whole: `streamText` over a runner model works, emitted as one text
368
+ block when the answer is there. Files reach the runner as bytes, since the runner model
369
+ declares no URL support and the SDK downloads on the app's side. Embeddings go the same way.
370
+
317
371
  ---
318
372
 
319
373
  ---
@@ -3,6 +3,7 @@ import { n as Logger } from "../logger-BCus3AOa.js";
3
3
  import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-DHomzpiq.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
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-BDg4c1Rk.js";
6
+ import { C as modelProviderInputSchema, S as modelEntrySchema, _ as StoredModelProvider, a as ModelCapabilities, b as maskModelProvider, c as ModelProvider, d as ModelProviderError, f as ModelProviderInput, g as ModelProviders, h as ModelProviderStore, i as MODEL_PROVIDER_REGIONS, l as ModelProviderChange, m as ModelProviderRegion, n as MODEL_JSON_MODES, o as ModelEntry, p as ModelProviderKind, r as MODEL_PROVIDER_KINDS, s as ModelJsonMode, t as CreateModelProvidersOptions, u as ModelProviderCipher, v as createInMemoryModelProviderStore, x as modelCapabilitiesSchema, y as createModelProviders } from "../model-providers-DQU57vAf.js";
6
7
  import { z } from "zod";
7
8
  import { ToolSet } from "ai";
8
9
  import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4Usage, SharedV4ProviderMetadata } from "@ai-sdk/provider";
@@ -1014,4 +1015,4 @@ export interface CaseLoop<TMove extends string, TOutcome extends string, TCase e
1014
1015
  */
1015
1016
  export declare function defineCaseLoop<const TMove extends string, const TOutcome extends string, TCase extends Record<string, unknown>, TResult extends Record<string, unknown>, TDecisions extends AgentExits, TFacts, THost = any>(config: CaseLoopConfig<TMove, TOutcome, TCase, TResult, TDecisions, TFacts, THost>): CaseLoop<TMove, TOutcome, TCase, TResult, TDecisions, TFacts, THost>;
1016
1017
  //#endregion
1017
- 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 };
1018
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, CircuitBreaker, CircuitBreakerOptions, CreateModelProvidersOptions, 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, MODEL_JSON_MODES, MODEL_PROVIDER_KINDS, MODEL_PROVIDER_REGIONS, ModelCapabilities, ModelEntry, ModelJsonMode, ModelProvider, ModelProviderChange, ModelProviderCipher, ModelProviderError, ModelProviderInput, ModelProviderKind, ModelProviderRegion, ModelProviderStore, ModelProviders, 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, StoredModelProvider, createCircuitBreaker, createInMemoryModelProviderStore, createModelProviders, embeddingDimensionsFor, explainProviderError, isFatalProviderError, isProviderError, isSubprocessorResidency, maskKeys, maskModelProvider, modelCapabilitiesSchema, modelEntrySchema, modelProviderInputSchema, resolveEndpointResidency, testOllamaModel, testProvider, withCircuitBreaker };
package/dist/ai/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { n as err, r as ok, t as noopLogger } from "../logger-epkdcXDA.js";
2
2
  import { C as nonRetryableError, _ as defineStep, m as buildWorkflow } from "../core-COMWflJl.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
+ import { a as testProvider, c as AiRefusalError, d as isAiRefusalError, f as readRefusalDetails, i as testOllamaModel, l as REFUSAL_FINISH_REASON, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as createCircuitBreaker, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as withCircuitBreaker, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as createRefusalAwareModel } from "../provider-probe-CwZWwFlo.js";
4
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";
5
+ import { a as PROVIDER_ERROR_STATUS, c as isFatalProviderError, i as PROVIDER_ERROR_REDACTED, l as isProviderError, n as FATAL_PROVIDER_ERROR_KEYS, o as ProviderError, r as PROVIDER_ERROR_PHRASES, s as explainProviderError, t as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, u as maskKeys } from "../provider-errors-DZWjl8di.js";
5
6
  import { z } from "zod";
6
7
  import { Output, generateText, stepCountIs, tool, wrapEmbeddingModel, wrapLanguageModel } from "ai";
7
8
  //#region src/ai/instrumented-model.ts
@@ -1559,4 +1560,269 @@ function defineCaseLoop(config) {
1559
1560
  };
1560
1561
  }
1561
1562
  //#endregion
1562
- export { AGENT_TRACE_SCHEMA, 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, defineAgentStep, defineAiStep, defineCaseLoop, embeddingDimensionsFor, estimateCostMicros, excerptOf, explainProviderError, isAiRefusalError, isFatalProviderError, isModelJsonError, isProviderError, isSubprocessorResidency, maskKeys, mergeModelJsonObjects, modelJsonRepairText, parseModelJson, readRefusalDetails, recoverModelJson, resolveEndpointResidency, runAgent, runAgentToExit, supportsAdaptiveThinking, supportsToolSearch, testOllamaModel, testProvider, thinkingSettingsFor, withCircuitBreaker, withJsonRepair };
1563
+ //#region src/ai/model-providers.ts
1564
+ /** A runner tag — the same rule `model-runner.ts` holds a runner's own tags to. */
1565
+ const MODEL_RUNNER_TAG = /^[a-z0-9][a-z0-9._-]{0,62}$/;
1566
+ /**
1567
+ * The API a provider speaks. `openai-compatible` is any server with OpenAI's
1568
+ * chat API (vLLM, LiteLLM, OpenRouter, a gateway). `runner` is no API at all:
1569
+ * the models are served by a runner that connects out to the app and claims
1570
+ * the calls that carry the row's `tags` (`octaflow/ai/runner`).
1571
+ */
1572
+ const MODEL_PROVIDER_KINDS = [
1573
+ "anthropic",
1574
+ "openai",
1575
+ "mistral",
1576
+ "google",
1577
+ "ollama",
1578
+ "openai-compatible",
1579
+ "runner"
1580
+ ];
1581
+ /** Where the data goes: the disclosure destination's region. */
1582
+ const MODEL_PROVIDER_REGIONS = [
1583
+ "self-hosted",
1584
+ "eu",
1585
+ "third-country"
1586
+ ];
1587
+ /**
1588
+ * How a model is asked for JSON: `schema` — native structured output against
1589
+ * a JSON schema; `object` — a JSON mode that guarantees an object but no
1590
+ * shape; `prompt` — no JSON mode, the instruction is in the prompt and the
1591
+ * answer is parsed.
1592
+ */
1593
+ const MODEL_JSON_MODES = [
1594
+ "schema",
1595
+ "object",
1596
+ "prompt"
1597
+ ];
1598
+ const modelCapabilitiesSchema = z.object({
1599
+ /** Takes a sampling temperature. Reasoning models often refuse one. */
1600
+ temperature: z.boolean().default(true),
1601
+ json: z.enum(MODEL_JSON_MODES).default("object"),
1602
+ /** Takes images. */
1603
+ vision: z.boolean().default(false),
1604
+ /** Takes a whole video (a file or, for Gemini, a YouTube URL). */
1605
+ video: z.boolean().default(false),
1606
+ /** Calls tools — what an agent needs. */
1607
+ tools: z.boolean().default(true),
1608
+ /** Embeds text instead of answering. */
1609
+ embedding: z.boolean().default(false)
1610
+ });
1611
+ const modelEntrySchema = z.object({
1612
+ /** The vendor's model id, e.g. `claude-sonnet-5-5`, `gemini-3.5-flash`, `qwen3:32b`. */
1613
+ id: z.string().min(1),
1614
+ label: z.string().optional(),
1615
+ capabilities: modelCapabilitiesSchema.default(modelCapabilitiesSchema.parse({})),
1616
+ /** The most output tokens a call asks for, when the caller names none. */
1617
+ maxOutputTokens: z.number().int().positive().optional(),
1618
+ /**
1619
+ * Settings the kind understands, passed as given — for `ollama`:
1620
+ * `{ numCtx, think, keepAlive, options }` (see `createOllama`).
1621
+ */
1622
+ settings: z.record(z.string(), z.unknown()).optional()
1623
+ });
1624
+ /** What an administrator saves. `apiKey` absent keeps the stored key, `null` removes it. */
1625
+ const modelProviderInputSchema = z.object({
1626
+ id: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, "lowercase letters, digits, - and _; at most 63"),
1627
+ kind: z.enum(MODEL_PROVIDER_KINDS),
1628
+ label: z.string().min(1),
1629
+ /** The server; for a vendor, a region-pinned endpoint or nothing for the SDK's default. */
1630
+ baseUrl: z.string().url().nullish(),
1631
+ region: z.enum(MODEL_PROVIDER_REGIONS),
1632
+ apiKey: z.string().min(1).nullish(),
1633
+ /** Sent with every request — attribution, a gateway's routing. Stored in plain text: never a secret. */
1634
+ headers: z.record(z.string(), z.string()).default({}),
1635
+ /** For `runner`: the tags a runner must have to serve this provider's calls. Empty for every other kind. */
1636
+ tags: z.array(z.string().regex(MODEL_RUNNER_TAG, "lowercase letters, digits, . _ -; at most 63")).max(32).default([]),
1637
+ models: z.array(modelEntrySchema).default([]),
1638
+ defaultModel: z.string().min(1).nullish(),
1639
+ enabled: z.boolean().default(true)
1640
+ }).superRefine((p, issue) => {
1641
+ const ids = p.models.map((m) => m.id);
1642
+ const repeated = ids.find((id, i) => ids.indexOf(id) !== i);
1643
+ if (repeated) issue.addIssue({
1644
+ code: "custom",
1645
+ path: ["models"],
1646
+ message: `model '${repeated}' is listed twice`
1647
+ });
1648
+ if (p.defaultModel && !ids.includes(p.defaultModel)) issue.addIssue({
1649
+ code: "custom",
1650
+ path: ["defaultModel"],
1651
+ message: `'${p.defaultModel}' is not one of the models`
1652
+ });
1653
+ if ((p.kind === "ollama" || p.kind === "openai-compatible") && !p.baseUrl) issue.addIssue({
1654
+ code: "custom",
1655
+ path: ["baseUrl"],
1656
+ message: `a ${p.kind} provider needs its server's URL`
1657
+ });
1658
+ if (p.kind === "runner" && p.tags.length === 0) issue.addIssue({
1659
+ code: "custom",
1660
+ path: ["tags"],
1661
+ message: "a runner provider needs at least one tag"
1662
+ });
1663
+ if (p.kind !== "runner" && p.tags.length > 0) issue.addIssue({
1664
+ code: "custom",
1665
+ path: ["tags"],
1666
+ message: `tags are for a runner provider, not a ${p.kind} one`
1667
+ });
1668
+ });
1669
+ const hintOf = (key) => key.length > 8 ? key.slice(-4) : null;
1670
+ function maskModelProvider(row) {
1671
+ const { apiKey, ...rest } = row;
1672
+ return {
1673
+ ...rest,
1674
+ hasKey: apiKey !== null
1675
+ };
1676
+ }
1677
+ /**
1678
+ * The providers an administrator manages: validated writes, the key through
1679
+ * the cipher, a masked read for the page and a before/after pair for the audit
1680
+ * log.
1681
+ *
1682
+ * ```ts
1683
+ * const providers = createModelProviders({ store: createPgModelProviderStore({ executor }), cipher });
1684
+ * const saved = await providers.save({ id: 'mistral', kind: 'mistral', label: 'Mistral', region: 'eu', apiKey, models: [{ id: 'mistral-large-latest' }] }, { actor: staffId });
1685
+ * ```
1686
+ */
1687
+ function createModelProviders(options) {
1688
+ const { store, cipher } = options;
1689
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1690
+ let version = 0;
1691
+ const listeners = /* @__PURE__ */ new Set();
1692
+ const changed = (id) => {
1693
+ version++;
1694
+ for (const l of listeners) try {
1695
+ l(id);
1696
+ } catch {}
1697
+ };
1698
+ return {
1699
+ async list(opts) {
1700
+ return (await store.list()).filter((r) => !opts?.enabledOnly || r.enabled).map(maskModelProvider);
1701
+ },
1702
+ async get(id) {
1703
+ const row = await store.get(id);
1704
+ return row && maskModelProvider(row);
1705
+ },
1706
+ async save(input, by) {
1707
+ const parsed = modelProviderInputSchema.safeParse(input);
1708
+ if (!parsed.success) {
1709
+ const issues = parsed.error.issues.map((i) => ({
1710
+ path: i.path.join("."),
1711
+ message: i.message
1712
+ }));
1713
+ return {
1714
+ ok: false,
1715
+ error: {
1716
+ key: "invalid",
1717
+ message: issues.map((i) => `${i.path || "(provider)"}: ${i.message}`).join("; "),
1718
+ issues
1719
+ }
1720
+ };
1721
+ }
1722
+ const p = parsed.data;
1723
+ const before = await store.get(p.id);
1724
+ let apiKey = before?.apiKey ?? null;
1725
+ let keyHint = before?.keyHint ?? null;
1726
+ if (p.apiKey === null) apiKey = keyHint = null;
1727
+ else if (p.apiKey !== void 0) {
1728
+ if (!cipher) return {
1729
+ ok: false,
1730
+ error: {
1731
+ key: "no_cipher",
1732
+ message: "a key can only be saved with a cipher"
1733
+ }
1734
+ };
1735
+ apiKey = await cipher.encrypt(p.apiKey);
1736
+ keyHint = hintOf(p.apiKey);
1737
+ }
1738
+ const row = {
1739
+ id: p.id,
1740
+ kind: p.kind,
1741
+ label: p.label,
1742
+ baseUrl: p.baseUrl ?? null,
1743
+ region: p.region,
1744
+ apiKey,
1745
+ keyHint,
1746
+ headers: p.headers,
1747
+ tags: p.tags,
1748
+ models: p.models,
1749
+ defaultModel: p.defaultModel ?? p.models[0]?.id ?? null,
1750
+ enabled: p.enabled,
1751
+ updatedAt: now(),
1752
+ updatedBy: by?.actor ?? null
1753
+ };
1754
+ await store.put(row);
1755
+ changed(p.id);
1756
+ return {
1757
+ ok: true,
1758
+ value: {
1759
+ id: p.id,
1760
+ before: before && maskModelProvider(before),
1761
+ after: maskModelProvider(row),
1762
+ actor: by?.actor ?? null,
1763
+ reason: by?.reason ?? null
1764
+ }
1765
+ };
1766
+ },
1767
+ async remove(id, by) {
1768
+ const before = await store.get(id);
1769
+ if (!before || !await store.remove(id)) return {
1770
+ ok: false,
1771
+ error: {
1772
+ key: "not_found",
1773
+ message: `no provider '${id}'`
1774
+ }
1775
+ };
1776
+ changed(id);
1777
+ return {
1778
+ ok: true,
1779
+ value: {
1780
+ id,
1781
+ before: maskModelProvider(before),
1782
+ after: null,
1783
+ actor: by?.actor ?? null,
1784
+ reason: by?.reason ?? null
1785
+ }
1786
+ };
1787
+ },
1788
+ async reveal(id) {
1789
+ const row = await store.get(id);
1790
+ if (!row) return null;
1791
+ if (row.apiKey !== null && !cipher) throw new Error(`provider '${id}' has a key, and no cipher was given to read it`);
1792
+ const apiKey = row.apiKey === null ? null : await cipher.decrypt(row.apiKey);
1793
+ return {
1794
+ ...maskModelProvider(row),
1795
+ apiKey
1796
+ };
1797
+ },
1798
+ version: () => version,
1799
+ onChange(listener) {
1800
+ listeners.add(listener);
1801
+ return () => listeners.delete(listener);
1802
+ },
1803
+ touch() {
1804
+ changed("*");
1805
+ }
1806
+ };
1807
+ }
1808
+ /** The reference store, in memory — for tests and a host with nothing to persist. */
1809
+ function createInMemoryModelProviderStore(rows = []) {
1810
+ const byId = new Map(rows.map((r) => [r.id, structuredClone(r)]));
1811
+ return {
1812
+ async list() {
1813
+ return [...byId.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((r) => structuredClone(r));
1814
+ },
1815
+ async get(id) {
1816
+ const r = byId.get(id);
1817
+ return r ? structuredClone(r) : null;
1818
+ },
1819
+ async put(row) {
1820
+ byId.set(row.id, structuredClone(row));
1821
+ },
1822
+ async remove(id) {
1823
+ return byId.delete(id);
1824
+ }
1825
+ };
1826
+ }
1827
+ //#endregion
1828
+ export { AGENT_TRACE_SCHEMA, 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_MODES, MODEL_JSON_PROVIDER_METADATA_KEY, MODEL_JSON_REPAIRED_ATTRIBUTE, MODEL_PROVIDER_KINDS, MODEL_PROVIDER_REGIONS, 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, createInMemoryModelProviderStore, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createModelProviders, createRefusalAwareModel, createUsageAccumulator, defineAgentStep, defineAiStep, defineCaseLoop, embeddingDimensionsFor, estimateCostMicros, excerptOf, explainProviderError, isAiRefusalError, isFatalProviderError, isModelJsonError, isProviderError, isSubprocessorResidency, maskKeys, maskModelProvider, mergeModelJsonObjects, modelCapabilitiesSchema, modelEntrySchema, modelJsonRepairText, modelProviderInputSchema, parseModelJson, readRefusalDetails, recoverModelJson, resolveEndpointResidency, runAgent, runAgentToExit, supportsAdaptiveThinking, supportsToolSearch, testOllamaModel, testProvider, thinkingSettingsFor, withCircuitBreaker, withJsonRepair };
@@ -1,6 +1,122 @@
1
1
  import { b as OpenAiCompatibleProviderId, g as EmbeddingProviderId, i as AiEndpointResidency, o as AiProviderId } from "../../provider-registry-C_mOfdHH.js";
2
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-BDg4c1Rk.js";
3
- import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
3
+ import { a as ModelCapabilities, c as ModelProvider, g as ModelProviders, m as ModelProviderRegion, o as ModelEntry } from "../../model-providers-DQU57vAf.js";
4
+ import { u as ModelRunnerHub } from "../../hub-SXfvoqH3.js";
5
+ import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4CallOptions } from "@ai-sdk/provider";
6
+ //#region src/ai/providers/ollama.d.ts
7
+ /** `false` switches thinking off; a level asks a model that has levels for that much. */
8
+ type OllamaThink = boolean | 'low' | 'medium' | 'high';
9
+ interface OllamaModelSettings {
10
+ /** The context window in tokens (`num_ctx`). Default: the server's. */
11
+ numCtx?: number;
12
+ /** Thinking on, off, or a level. Default: the model's. */
13
+ think?: OllamaThink;
14
+ /** How long the model stays loaded after the call: `'15m'`, seconds, or `-1` for ever. Default: the server's. */
15
+ keepAlive?: string | number;
16
+ /** Further entries of Ollama's `options` (`repeat_penalty`, `min_p`, …), sent as given. */
17
+ options?: Record<string, unknown>;
18
+ }
19
+ interface OllamaProviderOptions extends OllamaModelSettings {
20
+ /** The server, e.g. `http://localhost:11434`. A trailing `/v1` or `/api` is dropped. */
21
+ baseUrl: string;
22
+ /** Sent with every request — a proxy's key, say. */
23
+ headers?: Record<string, string>;
24
+ /** A fetch of your own: a timeout, a guarded outbound fetch, metering. Default: the global one. */
25
+ fetch?: typeof fetch;
26
+ }
27
+ interface OllamaProvider {
28
+ languageModel(modelId: string, settings?: OllamaModelSettings): LanguageModelV4;
29
+ embeddingModel(modelId: string, settings?: Pick<OllamaModelSettings, 'keepAlive' | 'options'> & {
30
+ truncate?: boolean;
31
+ }): EmbeddingModelV4;
32
+ }
33
+ export declare function createOllama(provider: OllamaProviderOptions): OllamaProvider;
34
+ //#endregion
35
+ //#region src/ai/providers/resolve.d.ts
36
+ interface ResolvedModel<M> {
37
+ model: M;
38
+ provider: ModelProvider;
39
+ entry: ModelEntry;
40
+ /** The disclosure destination this model is: the provider's id and region. */
41
+ destination: {
42
+ id: string;
43
+ region: ModelProviderRegion;
44
+ };
45
+ }
46
+ type ModelResolveError = {
47
+ key: 'no_provider';
48
+ message: string;
49
+ } | {
50
+ key: 'unknown_provider';
51
+ message: string;
52
+ } | {
53
+ key: 'disabled';
54
+ message: string;
55
+ } | {
56
+ key: 'unknown_model';
57
+ message: string;
58
+ } | {
59
+ key: 'not_capable';
60
+ message: string;
61
+ missing: Array<keyof ModelCapabilities>;
62
+ } | {
63
+ key: 'no_key';
64
+ message: string;
65
+ } |
66
+ /** A `runner` provider, and the resolver was made without `runners`. */
67
+ {
68
+ key: 'no_runner_hub';
69
+ message: string;
70
+ };
71
+ type Resolved<M> = {
72
+ ok: true;
73
+ value: ResolvedModel<M>;
74
+ } | {
75
+ ok: false;
76
+ error: ModelResolveError;
77
+ };
78
+ interface ModelRef {
79
+ /** The provider's id; the resolver's `defaultProvider` when absent. */
80
+ provider?: string;
81
+ /** The model's id; the provider's default model when absent. */
82
+ model?: string;
83
+ /** What the model must be able to do — a vision step asks for `{ vision: true }`. */
84
+ require?: Partial<Record<keyof ModelCapabilities, true>>;
85
+ }
86
+ interface ModelResolver {
87
+ languageModel(ref?: ModelRef): Promise<Resolved<LanguageModelV4>>;
88
+ embeddingModel(ref?: Omit<ModelRef, 'require'>): Promise<Resolved<EmbeddingModelV4>>;
89
+ /** Drop every cached model — after a write another replica made, when `providers.touch()` is not wired. */
90
+ invalidate(): void;
91
+ }
92
+ interface CreateModelResolverOptions {
93
+ providers: ModelProviders;
94
+ /** The provider a ref without one resolves to — a setting, an env var. */
95
+ defaultProvider?: () => string | undefined | Promise<string | undefined>;
96
+ /** The fetch every vendor SDK uses: a timeout, a guarded outbound fetch, metering. */
97
+ fetch?: typeof fetch;
98
+ /** The hub a `runner` provider's models go through (`createModelRunnerHub`, `octaflow/ai/runner`). */
99
+ runners?: ModelRunnerHub;
100
+ /** What every language model goes through on its way out — the disclosure guard, a breaker. */
101
+ wrap?: (model: LanguageModelV4, resolved: Omit<ResolvedModel<LanguageModelV4>, 'model'>) => LanguageModelV4;
102
+ /** The same for embedding models. */
103
+ wrapEmbedding?: (model: EmbeddingModelV4, resolved: Omit<ResolvedModel<EmbeddingModelV4>, 'model'>) => EmbeddingModelV4;
104
+ }
105
+ /** Shape a call to what the model declares — see the module comment. */
106
+ export declare function shapeCall(params: LanguageModelV4CallOptions, entry: ModelEntry): LanguageModelV4CallOptions;
107
+ /**
108
+ * ```ts
109
+ * const models = createModelResolver({
110
+ * providers,
111
+ * defaultProvider: () => settings.get('MODEL_PROVIDER'),
112
+ * wrap: (model, r) => guardLanguageModel(model, { destination: r.destination, classOf: () => 'public', log }),
113
+ * });
114
+ * const vision = await models.languageModel({ provider: 'gemini', require: { video: true } });
115
+ * if (vision.ok) await generateText({ model: vision.value.model, messages });
116
+ * ```
117
+ */
118
+ export declare function createModelResolver(options: CreateModelResolverOptions): ModelResolver;
119
+ //#endregion
4
120
  //#region src/ai/providers/index.d.ts
5
121
  export type LanguageModelFactory = (modelId: string) => LanguageModelV4;
6
122
  export type EmbeddingModelFactory = (modelId: string) => EmbeddingModelV4;
@@ -207,4 +323,4 @@ export interface OpenAiCompatibleGatewayProvider {
207
323
  */
208
324
  export declare function createOpenAiCompatibleGatewayProvider(config: OpenAiCompatibleGatewayConfig): Promise<OpenAiCompatibleGatewayProvider>;
209
325
  //#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 };
326
+ export { type CircuitBreaker, type CircuitBreakerOptions, type CreateModelResolverOptions, type ExplainProviderErrorOptions, FATAL_PROVIDER_ERROR_KEYS, type ModelRef, type ModelResolveError, type ModelResolver, type OllamaModelSettings, type OllamaProbeOptions, type OllamaProbeReport, type OllamaProvider, type OllamaProviderOptions, type OllamaThink, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_STATUS, type ProbeableProvider, ProviderError, type ProviderErrorExplanation, type ProviderErrorKey, type ProviderErrorPhrase, type ProviderErrorPreset, type ProviderProbeFailure, type ProviderProbeOptions, type ProviderProbeReport, type ResolvedModel, createCircuitBreaker, explainProviderError, isFatalProviderError, isProviderError, maskKeys, testOllamaModel, testProvider, withCircuitBreaker };