@fayegram-ai/decision-bridge 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +75 -0
  3. package/CONTRIBUTING.md +57 -0
  4. package/LICENSE +21 -0
  5. package/README.md +307 -2
  6. package/SECURITY.md +88 -0
  7. package/bin/decision.mjs +15 -0
  8. package/docs/README.md +58 -0
  9. package/docs/development/ARCHITECTURE.md +118 -0
  10. package/docs/development/DESIGN.md +66 -0
  11. package/docs/development/README.md +12 -0
  12. package/docs/development/RELEASING.md +100 -0
  13. package/docs/development/REPOSITORY_LAYOUT.md +33 -0
  14. package/docs/development/TESTING.md +131 -0
  15. package/docs/guides/BROWSER.md +43 -0
  16. package/docs/guides/CLOUDFLARE_DECISIONS.md +374 -0
  17. package/docs/guides/CUSTOMIZATION.md +141 -0
  18. package/docs/guides/CUSTOM_DECISION.md +66 -0
  19. package/docs/guides/CUSTOM_GENERATIVE.md +164 -0
  20. package/docs/guides/CUSTOM_PROVIDERS.md +81 -0
  21. package/docs/guides/GETTING_STARTED.md +252 -0
  22. package/docs/guides/OPENAI_DECISIONS.md +210 -0
  23. package/docs/guides/PLATFORMS.md +107 -0
  24. package/docs/guides/SWITCHING_MODELS.md +202 -0
  25. package/docs/guides/TROUBLESHOOTING.md +82 -0
  26. package/docs/reference/CLI.md +202 -0
  27. package/docs/reference/CONFIGURATION.md +168 -0
  28. package/docs/reference/ERRORS.md +82 -0
  29. package/docs/reference/PROVIDERS.md +113 -0
  30. package/docs/reference/SDK.md +388 -0
  31. package/docs/reference/SOURCES.md +51 -0
  32. package/examples/basic.mjs +17 -0
  33. package/examples/batch.mjs +13 -0
  34. package/examples/cloudflare-decisions.mjs +22 -0
  35. package/examples/cloudflare-omni.mjs +32 -0
  36. package/examples/cloudflare-worker.mjs +21 -0
  37. package/examples/cloudflare.native.json +28 -0
  38. package/examples/configs/anthropic.json +9 -0
  39. package/examples/configs/cloudflare-decisions.json +8 -0
  40. package/examples/configs/custom-decision.json +9 -0
  41. package/examples/configs/custom-generative.json +14 -0
  42. package/examples/configs/jev.json +7 -0
  43. package/examples/configs/local.json +12 -0
  44. package/examples/configs/openai-decisions.json +7 -0
  45. package/examples/configs/openai.json +10 -0
  46. package/examples/configs/openrouter.json +10 -0
  47. package/examples/custom/decision-provider.mjs +15 -0
  48. package/examples/custom/generative-provider.mjs +13 -0
  49. package/examples/custom/openrouter.mjs +13 -0
  50. package/examples/customize.mjs +23 -0
  51. package/examples/decisions.native.json +19 -0
  52. package/examples/extended.mjs +43 -0
  53. package/examples/openai-decisions.mjs +20 -0
  54. package/examples/request.json +31 -0
  55. package/examples/requests.jsonl +2 -0
  56. package/package.json +87 -4
  57. package/schemas/decision.config.schema.json +553 -0
  58. package/src/async.mjs +63 -0
  59. package/src/batch.mjs +38 -0
  60. package/src/bridge.mjs +184 -0
  61. package/src/capabilities.mjs +95 -0
  62. package/src/cli/config-validation.mjs +89 -0
  63. package/src/cli/config.mjs +84 -0
  64. package/src/cli/decisions.mjs +19 -0
  65. package/src/cli/initialize.mjs +79 -0
  66. package/src/cli/io.mjs +171 -0
  67. package/src/cli/main.mjs +280 -0
  68. package/src/cli/ordered.mjs +43 -0
  69. package/src/cli/provider-loader.mjs +58 -0
  70. package/src/cloudflare-decisions.d.mts +78 -0
  71. package/src/custom/decision.d.mts +5 -0
  72. package/src/custom/decision.mjs +6 -0
  73. package/src/custom/definition.mjs +19 -0
  74. package/src/custom/generative/anthropic.mjs +75 -0
  75. package/src/custom/generative/index.d.mts +42 -0
  76. package/src/custom/generative/index.mjs +29 -0
  77. package/src/custom/generative/openai-compatible.mjs +101 -0
  78. package/src/custom/generative/schema.mjs +95 -0
  79. package/src/custom/generative/shared.mjs +39 -0
  80. package/src/decisions.d.mts +45 -0
  81. package/src/embedded.mjs +24 -0
  82. package/src/errors.mjs +49 -0
  83. package/src/index.d.mts +229 -0
  84. package/src/index.mjs +7 -0
  85. package/src/input.mjs +37 -0
  86. package/src/options.mjs +15 -0
  87. package/src/policy.mjs +44 -0
  88. package/src/probabilities.mjs +27 -0
  89. package/src/provider-contract.mjs +42 -0
  90. package/src/providers/cloudflare-audio.mjs +83 -0
  91. package/src/providers/cloudflare-codec.mjs +39 -0
  92. package/src/providers/cloudflare-decisions.mjs +81 -0
  93. package/src/providers/cloudflare-images.mjs +95 -0
  94. package/src/providers/cloudflare-media.mjs +36 -0
  95. package/src/providers/cloudflare-preflight.mjs +92 -0
  96. package/src/providers/cloudflare-profile.mjs +102 -0
  97. package/src/providers/cloudflare-transport.mjs +63 -0
  98. package/src/providers/cloudflare-validation.mjs +48 -0
  99. package/src/providers/cloudflare-video.mjs +84 -0
  100. package/src/providers/decisions-codec.mjs +61 -0
  101. package/src/providers/decisions-preflight.mjs +20 -0
  102. package/src/providers/decisions-validation.mjs +150 -0
  103. package/src/providers/index.mjs +26 -0
  104. package/src/providers/jev.mjs +51 -0
  105. package/src/providers/media-binary.mjs +51 -0
  106. package/src/providers/openai-decisions.mjs +51 -0
  107. package/src/questions.mjs +15 -0
  108. package/src/response-transport.mjs +176 -0
  109. package/src/runtime-env.mjs +4 -0
  110. package/src/transport.mjs +51 -0
  111. package/src/validation.mjs +245 -0
  112. package/src/version.mjs +2 -0
@@ -0,0 +1,388 @@
1
+ # JavaScript SDK reference
2
+
3
+ Import from `@fayegram-ai/decision-bridge` in your application after package installation.
4
+ See [getting started](../guides/GETTING_STARTED.md) for a complete JavaScript call,
5
+ TypeScript setup, and CommonJS dynamic import. The source-tree entry point is
6
+ `src/index.mjs`. The package exposes ESM. Public
7
+ TypeScript declarations are in `src/index.d.mts`; runtime JavaScript is authoritative
8
+ and checked independently of those declarations.
9
+
10
+ ## Create a bridge
11
+
12
+ ```javascript
13
+ import { createBridge, createProvider } from '@fayegram-ai/decision-bridge';
14
+
15
+ const provider = createProvider({
16
+ type: 'jev', model: 'jev-latest', timeoutMs: 30000, maxRetries: 2,
17
+ });
18
+
19
+ const bridge = createBridge({
20
+ provider,
21
+ timeoutMs: 60000,
22
+ observerTimeoutMs: 1000,
23
+ extensions: [],
24
+ });
25
+ ```
26
+
27
+ Select exactly one non-undefined `provider` (a `Provider` with an explicit kind) or `jev`
28
+ (native Jev options). The explicit shortcut `createBridge({ jev: {} })` reads
29
+ Jev's credentials and model defaults. Missing selection, null selectors, and
30
+ simultaneous selections are configuration errors. The exported `BridgeOptions`
31
+ type requires selection, and `createBridge` returns a `Bridge`.
32
+ Selection uses own configuration properties: inherited `provider`, `jev`, or
33
+ factory `type` properties do not select a provider, and their getters are not read.
34
+ The bridge's `provider`, `providerKind`, and `defaultModel` properties are read-only.
35
+ `providerKind` is `decision` or `generative`, selected from the provider contract.
36
+ Unknown options are errors, not ignored settings. Provider credentials are resolved
37
+ at construction time; create a new provider after rotating credentials.
38
+
39
+ ### Provider transport options
40
+
41
+ Native REST providers and maintained custom generative presets accept the public
42
+ `TransportOptions` fields below through their constructors or the appropriate
43
+ provider factory. Model, credentials, endpoint selection, and generation settings
44
+ remain provider-specific; see [configuration](CONFIGURATION.md) and the provider
45
+ guides. These are SDK options, not per-evaluation bridge options.
46
+
47
+ | Option | Default and behavior |
48
+ | --- | --- |
49
+ | `timeoutMs` | 30,000 ms; positive integer through 2,147,483,647; one transport budget for dispatch, retries, backoff, and response reading |
50
+ | `maxRetries` | Additional eligible attempts, 0–10; Jev and generative presets default to 2, native OpenAI and Cloudflare to 0 |
51
+ | `maxRequestBytes` | Complete serialized request ceiling; normally 8 MiB, configurable from 1 byte through 64 MiB; Cloudflare applies its [model-specific budgets](../guides/CLOUDFLARE_DECISIONS.md#embedded-images-and-limits) |
52
+ | `maxResponseBytes` | 8 MiB; configurable from 1 byte through 64 MiB; applies while reading the response body |
53
+ | `fetchImpl` | Optional `(url: string, init?: RequestInit) => Promise<Response>`; defaults to the runtime's `fetch` |
54
+
55
+ Use `fetchImpl` to wrap or inject a fetch implementation in an application or
56
+ local protocol test. The SDK supplies the destination URL, method, headers, body,
57
+ abort signal, and `redirect: 'error'`. A wrapper must preserve those settings and
58
+ return a standard byte-stream response; it owns the behavior of any replacement
59
+ networking. The CLI's JSON configuration cannot contain functions, so this option
60
+ is SDK-only. Cloudflare binding transport accepts the timeout, retry, and byte
61
+ controls, but rejects `fetchImpl` and REST credential/endpoint options.
62
+
63
+ The bridge separately defaults to a 60,000 ms evaluation budget and a 1,000 ms
64
+ timeout per observer. Call-level `timeoutMs` changes the bridge budget, while
65
+ provider constructor `timeoutMs` changes the transport budget. The
66
+ [cancellation and deadlines](#cancellation-and-deadlines) section explains how
67
+ they interact; [errors](ERRORS.md#retry-policy) lists retryable statuses.
68
+
69
+ ### `bridge.evaluate(request, options?)`
70
+
71
+ `request` has exactly one of `state` or ordered `input`, `questions`, and optional
72
+ `model` and `safetyIdentifier`. New forms require declared provider capabilities;
73
+ see [OpenAI Decisions](../guides/OPENAI_DECISIONS.md) and
74
+ [Cloudflare Clef](../guides/CLOUDFLARE_DECISIONS.md). The request model
75
+ wins over the provider's default. No default is assumed for a generative provider.
76
+ `options` accepts `signal: AbortSignal` and `timeoutMs`, overriding the bridge's
77
+ 60-second evaluation budget for this call. It does not mutate the provider's
78
+ independent transport timeout, including Workers AI binding calls.
79
+
80
+ Ordered parts are `{ type: 'text', text }`, `{ type: 'image', dataUrl, detail? }`,
81
+ `{ type: 'audio', dataUrl }`, or `{ type: 'video', dataUrl }`. Audio/video are
82
+ supported by Cloudflare Clef-omni and explicitly capable custom providers; other
83
+ built-ins reject them before dispatch. Cloudflare requires images → audio →
84
+ videos → text. Media use embedded data URLs, with format and size checks described
85
+ in the [Cloudflare guide](../guides/CLOUDFLARE_DECISIONS.md#omni-audio-and-video).
86
+
87
+ The bridge snapshots and freezes its input, runs sequential state or input preparation,
88
+ invokes the provider once, validates the result, then observes an immutable result.
89
+ Built-in transports can retry eligible response statuses within that invocation
90
+ when client retries are configured.
91
+
92
+ ### `bridge.getCapabilities(model?)` and `bridge.validate(request)`
93
+
94
+ Capability lookup is synchronous and reports implemented features and limits.
95
+ `validate` resolves the model, snapshots/freezes the request, and checks capabilities
96
+ and preparation-extension compatibility without inference. Unsupported features
97
+ fail with `UNSUPPORTED_CAPABILITY` before dispatch. Native Decisions has no model
98
+ listing operation; capability lookup is not account model discovery.
99
+
100
+ Providers can implement optional `validateRequest(resolvedRequest)` for additional
101
+ static checks. This synchronous hook receives a frozen request, must return no
102
+ value, and rejects by throwing; it must not perform inference or other side
103
+ effects. OpenAI Decisions and Cloudflare Clef use it to check native fields and UTF-8 request
104
+ bytes against `maxRequestBytes`. The bridge repeats validation after preparation.
105
+
106
+ ### `bridge.listModels(options?)`
107
+
108
+ Calls the provider's listing operation. Returns `{ models: [{ name, ... }], ... }`.
109
+ Pagination markers are preserved; this is not a promise of an exhaustive catalog.
110
+ OpenAI Decisions, Cloudflare Clef, and custom providers without listing support
111
+ throw `UNSUPPORTED_OPERATION`.
112
+
113
+ ## Request data
114
+
115
+ `state` is a string, plain JSON object, or array. It may be empty, but not null,
116
+ a top-level number, or a top-level boolean. Values nested within objects/arrays can
117
+ be any JSON values. JSON snapshots reject `undefined`, cycles, non-finite numbers,
118
+ functions, dates, class instances, accessors, symbols, sparse arrays, and extra or
119
+ non-enumerable array/object properties. Limits: nesting depth 100, 200,000 nodes.
120
+
121
+ `questions` is a nonempty object keyed by application-owned identifiers. Every
122
+ question requires non-null `instructions`: nonempty text, a JSON object, or an
123
+ array. Descriptions can be text or structured JSON. Only choice descriptions permit
124
+ raw null. Question IDs and legacy choice-map keys must be nonempty; typed string
125
+ choice values may be empty. Request and question unknown fields are rejected.
126
+
127
+ ### Question builders
128
+
129
+ ```javascript
130
+ const q1 = choice('Which queue should receive this?', {
131
+ billing: 'Invoices or payment problems.',
132
+ engineering: 'Technical defects.',
133
+ unknown: 'Insufficient evidence.',
134
+ });
135
+ const q2 = noul('Does the evidence show a failed test?', {
136
+ true: 'At least one executed test failed.',
137
+ false: 'No executed test is shown to have failed.',
138
+ });
139
+ const q3 = score('How far did processing progress?', [
140
+ 'Not started.', 'Started but incomplete.', 'Completed.',
141
+ ]);
142
+ ```
143
+
144
+ Builders return immutable question objects. `choice(instructions, criteria)` accepts
145
+ a label map containing 1 to 255 options. `noul(instructions, criteria?)` accepts an
146
+ optional criteria object,
147
+ with only `true` and `false` keys. `score(instructions, criteria)` accepts **2–10**
148
+ ordered levels; numeric level indices are zero-based. There is no boolean alias:
149
+ `noul` means a probability, not a thresholded truth value.
150
+
151
+ `choice(instructions, options)` accepts ordered objects with unique string or
152
+ boolean `value` fields and optional text or structured JSON descriptions. Boolean `true` and string
153
+ `"true"` remain distinct. `scoreLevels(instructions, levels)` accepts at least two
154
+ ordered label/description objects. Provider preflight applies service limits:
155
+ OpenAI Decisions and Cloudflare require 2–255 choices; Cloudflare limits scores
156
+ to 2–10 levels, while OpenAI labelled scores have no additional client count cap.
157
+
158
+ ## Returned data
159
+
160
+ ```text
161
+ EvaluationResult
162
+ model Reported model identity, or explicitly marked fallback
163
+ answers.<question-id> Typed answer, matched to that input question
164
+ usage Token counts; unreported generic counts are null
165
+ meta Provenance, version, timings, diagnostics
166
+ ```
167
+
168
+ **Legacy choice:** `{ type: 'choice', choice, probabilities, confidence? }`.
169
+ The probability keys must exactly match the options, all values must be in [0,1],
170
+ and the sum must be within 0.001 of 1, except for the provider-selected rounding
171
+ allowance described below. The selected option must be maximal within
172
+ rounding tolerance. Jev requires reported choice/score confidence; the other
173
+ native providers validate and preserve their service-specific confidence.
174
+ Generative providers derive the selected option from
175
+ the distribution and omit confidence. Generative ties choose the first label in
176
+ JavaScript property enumeration order; numeric-looking keys
177
+ follow JavaScript's numeric property ordering.
178
+ Native selections are retained after validation.
179
+
180
+ Typed choice questions instead return a typed string/boolean selection and an
181
+ array of `{ value, probability }` entries. See the [Decisions guide](../guides/OPENAI_DECISIONS.md).
182
+
183
+ Cloudflare selects four-decimal probability validation. Distributions aligned to
184
+ that precision allow `max(0.001, optionCount * 0.00005)`, plus a small floating-point
185
+ allowance. Other Cloudflare distributions and existing providers retain the 0.001
186
+ baseline. The bridge selects this policy from the provider contract, never from
187
+ response metadata, and preserves all returned numbers.
188
+
189
+ **Noul:** `{ type: 'noul', noul }`, with a number in [0,1]. The library never
190
+ thresholds this into true/false without application code.
191
+
192
+ **Score:** `{ type: 'score', score, probabilities, legend, confidence? }`.
193
+ For N levels, keys are `'0'` through `String(N-1)`. The score is the weighted mean
194
+ of these indices; validation tolerance is `0.02 * N` (accommodating 2-decimal rounded probability distributions from live services). `legend` must match the input
195
+ rubric exactly. Generative providers calculate score and legend locally. Native
196
+ values are validated and preserved, not normalized or replaced.
197
+
198
+ **Usage:** `input_tokens` and `output_tokens` are nonnegative integers or null.
199
+ Jev, OpenAI Decisions, and Cloudflare require reported integer counts. OpenAI and
200
+ Anthropic cache creation/read counts are preserved separately when provided;
201
+ Cloudflare does not fabricate cache or total-token fields. No total price, inferred cache saving, or
202
+ estimated token count is fabricated.
203
+
204
+ **Metadata:**
205
+
206
+ | Field | Meaning |
207
+ | --- | --- |
208
+ | `provider` | Bridge backend name; native Jev reports `typesafe` |
209
+ | `providerKind` | `decision` or `generative`; authoritative provider classification, never response-selected |
210
+ | `requestedModel` | Resolved model requested by the caller |
211
+ | `probabilitySource` | `native`, `elicited`, or custom/`unspecified` provenance |
212
+ | `confidenceSource` | `native` or `unavailable` for built-in providers |
213
+ | `modelSource` | `reported` or `requested` fallback for built-in providers |
214
+ | `bridgeVersion`, `schemaVersion` | Package version and result contract version |
215
+ | `attempts`, `requestId` | Transport diagnostics when supplied |
216
+ | `gatewayLogId` | Cloudflare Gateway response identifier when reported |
217
+ | `openaiDecisions`, `cloudflareDecisions` | Provider-specific reported usage and native answer/confidence metadata |
218
+ | `durationMs` | Provider transport duration, including retries and body reading |
219
+ | `totalDurationMs` | Bridge duration through observation |
220
+ | `observerDurationMs` | Observation phase duration; initially zero inside observers |
221
+ | `observerFailures` | Names of failed/timed-out observers |
222
+
223
+ Metadata can have additional JSON fields. Application logic should not assume
224
+ unavailable metadata exists. Result objects and nested data returned by the bridge
225
+ are immutable. Standalone validation functions return snapshots, not frozen values.
226
+
227
+ `schemaVersion` remains 1 for legacy requests and 2 for ordered images/text or
228
+ richer question forms. Audio/video requests use 3. This result version is separate
229
+ from the CLI configuration schema, which remains 1.
230
+
231
+ ## Extensions
232
+
233
+ An extension has a unique printable `name` (maximum 100 characters), optional
234
+ `prepareState(state, context)`, `prepareInput(input, context)`, and
235
+ `onResult(result, context)`. Preparation extensions must support the request's
236
+ input form; incompatible hooks fail explicitly before dispatch.
237
+
238
+ Preparation returns replacement state or input, or `undefined` to leave it unchanged.
239
+ Preparation exceptions fail before inference with `EXTENSION_ERROR`; transformed
240
+ content is revalidated, including capabilities and provider preflight. Preparation
241
+ cannot rewrite question definitions or switch the model.
242
+ Construct the request directly for those changes.
243
+
244
+ Observers run after successful inference; their failures are collected in
245
+ `meta.observerFailures`. They do not change answers, fail the successful request,
246
+ or trigger another billed call. Each observer gets its own timeout, default 1,000
247
+ milliseconds. Results shown to observers precede final timing/failure aggregation.
248
+
249
+ `context` contains `provider`, `model`, frozen `questionIds`, and an `AbortSignal`.
250
+ Custom callbacks should honor the signal. The bridge can stop waiting for ignored
251
+ signals, but cannot terminate arbitrary user code or its external side effects.
252
+ See [customization](../guides/CUSTOMIZATION.md) for runnable examples.
253
+
254
+ ## Cancellation and deadlines
255
+
256
+ ```javascript
257
+ const controller = new AbortController();
258
+ const resultPromise = bridge.evaluate(request, {
259
+ signal: controller.signal,
260
+ timeoutMs: 15000,
261
+ });
262
+ // controller.abort() cancels the in-flight request when needed.
263
+ ```
264
+
265
+ The transport deadline covers dispatch, attempts, backoff, and body reading for
266
+ REST and Workers AI binding calls. A bridge deadline
267
+ also covers preparation and the provider. The first applicable deadline wins.
268
+ Observer time is additional and bounded separately. Aborting after a successful
269
+ inference while observing preserves the successful result and records observer
270
+ failure; it does not pretend the billed evaluation failed.
271
+
272
+ Elapsed time is checked before dispatch and before accepting completion, including
273
+ when synchronous work delays timer callbacks. Nested transport calls inherit the
274
+ remaining bridge deadline. Synchronous user code cannot be interrupted; an
275
+ overrun is detected when control returns, without undoing completed side effects.
276
+
277
+ ## SDK batch evaluation
278
+
279
+ ```javascript
280
+ const records = await evaluateBatch(bridge, requests, {
281
+ concurrency: 4,
282
+ stopOnError: false,
283
+ signal: controller.signal,
284
+ });
285
+ ```
286
+
287
+ This array API validates all raw requests, including provider preflight, before
288
+ dispatch. Preparation runs during each evaluation, so errors introduced by hooks
289
+ remain per-request failures. Maximum array length is
290
+ 10,000; concurrency is 1–64. Records retain input order and are either
291
+ `{ index, ok: true, result }` or `{ index, ok: false, error }`.
292
+ With `stopOnError`, already-started work completes and new work is marked
293
+ `BATCH_SKIPPED`. Cancellation marks undispatched records `ABORTED`. There is no
294
+ automatic model fallback. The CLI has a separate streaming JSONL implementation.
295
+
296
+ ## Additional exports
297
+
298
+ - `createProvider(config)`: factory requiring an explicit `type` equal to `jev`,
299
+ `openai-decisions`, or `cloudflare-decisions`.
300
+ - `JevProvider`, `OpenAIDecisionsProvider`, `CloudflareDecisionsProvider`: native provider classes.
301
+ - `scoreLevels(instructions, levels)` and the array overload of `choice`:
302
+ labelled rubrics and typed string/boolean options, with provider capability checks.
303
+ - `OpenAIDecisionsProvider.createDecision(request, options?)`: dedicated native
304
+ access returning `{ decision, meta }`; see the [Decisions guide](../guides/OPENAI_DECISIONS.md).
305
+ - `CloudflareDecisionsProvider.createDecision(request, options?)`: dedicated native
306
+ Clef access returning `{ decision, meta }`; see the [Cloudflare guide](../guides/CLOUDFLARE_DECISIONS.md).
307
+ - `validateRequest(input, defaultModel?)`: snapshot and validate. Requires
308
+ an own `input.model` property or an explicitly supplied default; inherited
309
+ models are ignored, and it never selects a Jev model.
310
+ Question builders and schema generation validate without selecting an inference model.
311
+ - `validateResult(input, questions, { native = false, probabilityDecimalPlaces })`: snapshot supplied
312
+ questions and validate the canonical result; native mode requires confidence
313
+ and reported counts.
314
+ - `decideChoice(answer, { minProbability, minMargin = 0, abstainOptions = [], probabilityDecimalPlaces })`:
315
+ returns a policy result, never executes an action. `minProbability` is required.
316
+ - `DecisionError`, `publicError(error)`: public structured failure handling.
317
+
318
+ `decideChoice` returns `status: 'selected' | 'review'`, the model's choice, selected
319
+ probability, margin to the runner-up, and a reason. Thresholds are your policy;
320
+ no default claims of safety or empirical calibration are implied.
321
+ Its optional `probabilityDecimalPlaces` uses the same sum validation as
322
+ `validateResult`. Supply the precision from the provider's contract when using
323
+ rounded distributions, such as Cloudflare's four-decimal results. Omission keeps
324
+ the existing 0.001 tolerance; the helper never infers precision from an answer.
325
+
326
+ ## Custom entry points
327
+
328
+ The root exports common `Provider`, `ProviderKind`, `DecisionProvider`, request,
329
+ result, and transport types. Every provider requires `kind`. Generative classes
330
+ and types use the explicit `custom/generative` entry point; the root provider
331
+ factory selects native decision providers.
332
+
333
+ | Entry point | Runtime exports | Types |
334
+ | --- | --- | --- |
335
+ | `custom/decision` | `defineDecisionProvider` | `DecisionProviderDefinition`; `DecisionProvider` |
336
+ | `custom/generative` | `defineGenerativeProvider`, `createGenerativeProvider`, `OpenAICompatibleProvider`, `OpenRouterProvider`, `AnthropicProvider` | `GenerativeProvider`, definition/config, and preset options |
337
+ | `custom/generative` | `buildResponseSchema`, `buildPrompt`, `parseGeneratedJson`, `normalizeGenerated` | Typed legacy generative strategy helpers |
338
+
339
+ Import these as package subpaths under `@fayegram-ai/decision-bridge/`. All SDK
340
+ entry points are browser-safe ESM. Definition helpers preserve callback receivers
341
+ and validate/freeze wrappers; they do not implement a new protocol. Both kinds
342
+ work with the same bridge, extensions, cancellation, and batch API.
343
+
344
+ Generative results always use elicited probabilities and unavailable confidence.
345
+ The bridge rejects native provenance and answer confidence from that path.
346
+ Supplied probability/confidence provenance labels must be nonempty strings;
347
+ decision providers may preserve custom string labels. Generative prompt and
348
+ normalization helpers independently validate the shared-state, legacy-question
349
+ profile and explicitly reject unsupported request capabilities.
350
+ Decision-kind classification alone does not require confidence or known counts;
351
+ `validateResult({ native: true })` is a separate strict validation option used for
352
+ Jev's contract. See [custom decision](../guides/CUSTOM_DECISION.md),
353
+ [custom generative](../guides/CUSTOM_GENERATIVE.md), and the shared
354
+ [custom contract](../guides/CUSTOM_PROVIDERS.md).
355
+
356
+ ## Validation guarantees
357
+
358
+ Legacy choice criteria maps allow at most 255 options, matching Jev's contract. Noul
359
+ criteria must be omitted or an object; its present true/false descriptions cannot
360
+ be raw null. Score levels likewise cannot be raw null (nested null JSON values are
361
+ still allowed). These invalid forms now fail before provider dispatch.
362
+
363
+ `decideChoice()` rejects unknown option names and abstention labels not present in
364
+ the answer. `evaluateBatch()` validates array holes as invalid requests before any
365
+ item is dispatched, rather than spending on adjacent valid entries.
366
+
367
+ The optional third argument to `validateResult()` must be a plain options object
368
+ with optional boolean `native` and integer `probabilityDecimalPlaces` (1–15) fields.
369
+ Providers can declare a pure synchronous `getResultValidationOptions(model)` hook;
370
+ its policy is validated before inference and is not read from response metadata.
371
+ Returning a Promise is a configuration error; its rejection is observed without
372
+ calling inference or reading a custom `then` accessor.
373
+ Cloudflare selects four-decimal rounding; other built-ins retain the 0.001 sum tolerance.
374
+ Unknown keys, accessors, symbol keys, non-enumerable fields, a non-boolean `native`,
375
+ or invalid probability precision are rejected as `CONFIGURATION_ERROR`.
376
+ An omitted or explicitly undefined `native` value keeps the default `false`.
377
+ Extension arrays must be dense; a hole is not an omitted extension.
378
+
379
+ Standalone `validateResult()` snapshots both the response and supplied questions.
380
+ Matching structured score rubrics compare by JSON content, including plain
381
+ objects with a null prototype. Invalid question JSON fails with `VALIDATION_ERROR`;
382
+ accessors are rejected without invocation. Question and result inputs are neither
383
+ mutated nor frozen.
384
+
385
+ Native provider cancellation/timeout errors preserve the last observed sanitized
386
+ request ID and attempt count when available. The bridge may settle its own
387
+ cancellation race before the provider returns diagnostics; do not require those
388
+ optional fields on every `ABORTED` or `TIMEOUT` error.
@@ -0,0 +1,51 @@
1
+ # Primary protocol sources
2
+
3
+ These are the upstream protocol and platform references used by the bridge.
4
+ Recheck them before updating versions or claiming new compatibility. Listing a
5
+ reference does not imply that authenticated inference was run against its service.
6
+
7
+ | Subject | Official source |
8
+ | --- | --- |
9
+ | Node 22.16 JSON modules | https://nodejs.org/download/release/v22.16.0/docs/api/esm.html#json-modules |
10
+ | Node filesystem identities and atomic operations | https://nodejs.org/docs/latest-v22.x/api/fs.html |
11
+ | TypeSafe request/response API | https://docs.typesafe.ai/api |
12
+ | TypeSafe JavaScript examples | https://docs.typesafe.ai/sdk/javascript |
13
+ | Native model listing and aliases | https://docs.typesafe.ai/models |
14
+ | Choice decisions | https://docs.typesafe.ai/primitives/choice |
15
+ | Noul probabilities | https://docs.typesafe.ai/primitives/noul |
16
+ | Score rubric and weighted expectation | https://docs.typesafe.ai/primitives/score |
17
+ | Native confidence interpretation | https://docs.typesafe.ai/confidence |
18
+ | OpenAI structured output modes | https://platform.openai.com/docs/guides/structured-outputs |
19
+ | OpenAI native Decisions guide | https://developers.openai.com/api/docs/guides/decisions |
20
+ | OpenAI Decisions request/response contract | https://developers.openai.com/api/reference/resources/decisions/methods/create |
21
+ | OpenRouter structured output and routing | https://openrouter.ai/docs/guides/features/structured-outputs |
22
+ | Anthropic Messages, tools, usage, and stop reasons | https://platform.claude.com/docs/en/api/messages/create |
23
+ | GitHub checkout action | https://github.com/actions/checkout |
24
+ | GitHub Node setup action | https://github.com/actions/setup-node |
25
+
26
+ No model pricing, benchmark score, latency promise, or calibration guarantee is
27
+ baked into this implementation. The repository tests verify its own protocol and
28
+ software invariants using explicitly labeled fixtures. Live model behavior is a
29
+ separate operational check.
30
+
31
+ ## Cloudflare decision inference
32
+
33
+ - [Clef](https://developers.cloudflare.com/workers-ai/models/clef/) and [Clef-flash](https://developers.cloudflare.com/workers-ai/models/clef-flash/)
34
+ - [Clef-omni](https://developers.cloudflare.com/workers-ai/models/clef-omni/) and its [input](https://developers.cloudflare.com/workers-ai/models/clef-omni/schema-input.json) / [output](https://developers.cloudflare.com/workers-ai/models/clef-omni/schema-output.json) schemas
35
+ - [Clef serving, pricing, and multimodal announcement](https://blog.cloudflare.com/clef-faster-cheaper-multimodal/)
36
+ - [Hosted input schema](https://developers.cloudflare.com/workers-ai/models/clef/schema-input.json) and [output schema](https://developers.cloudflare.com/workers-ai/models/clef/schema-output.json)
37
+ - [REST and permissions](https://developers.cloudflare.com/ai-gateway/usage/rest-api/)
38
+ - [Binding implementation and cancellation](https://github.com/cloudflare/workerd/blob/main/src/cloudflare/internal/ai-api.ts)
39
+ - [Gateway binding methods and options](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/)
40
+ - [Capacity rejection](https://developers.cloudflare.com/workers-ai/features/reject-if-busy/)
41
+ - [Gateway request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/)
42
+ - [Gateway limits](https://developers.cloudflare.com/ai-gateway/reference/limits/), including cacheable request size
43
+ - [Workers AI errors](https://developers.cloudflare.com/workers-ai/platform/errors/)
44
+ - [Cloudflare-owned model implementation](https://huggingface.co/Cloudflare/clef-flash/blob/main/joint_schema_model.py), including output precision
45
+ - [Cloudflare-owned Omni implementation](https://huggingface.co/Cloudflare/clef-omni/blob/main/joint_schema_model.py), including grouped media and output precision
46
+
47
+ ## Package and repository release
48
+
49
+ - [Scoped public npm packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/)
50
+ - [npm staged publishing](https://docs.npmjs.com/staged-publishing/) and [stage commands](https://docs.npmjs.com/cli/v12/commands/npm-stage/)
51
+ - [GitHub private vulnerability reporting](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/configure-for-a-repository)
@@ -0,0 +1,17 @@
1
+ import { createBridge, choice, noul } from '../src/index.mjs';
2
+
3
+ const bridge = createBridge({ jev: {} }); // TYPESAFE_API_KEY; direct TypeSafe; jev-latest by default.
4
+ const result = await bridge.evaluate({
5
+ state: { error: "ModuleNotFoundError: No module named 'pytest'" },
6
+ questions: {
7
+ category: choice('Classify this failure from the supplied evidence.', {
8
+ dependency: 'A required package or module is unavailable.',
9
+ network: 'A network operation failed.',
10
+ assertion: 'A test assertion failed.',
11
+ unknown: 'Insufficient evidence for the other categories.',
12
+ }),
13
+ missing_dependency: noul('Does this error indicate a missing Python dependency?'),
14
+ },
15
+ });
16
+ console.log(JSON.stringify(result, null, 2));
17
+ // The caller decides what happens next. This example does not execute any action.
@@ -0,0 +1,13 @@
1
+ import { createBridge, evaluateBatch, noul } from '../src/index.mjs';
2
+
3
+ try {
4
+ const bridge = createBridge({ jev: {} });
5
+ const questions = { failure: noul('Does the evidence explicitly report a failed test?') };
6
+ const requests = ['18 passed, 2 failed.', 'Tests were not run.'].map(state => ({ state, questions }));
7
+ const records = await evaluateBatch(bridge, requests, { concurrency: 2 });
8
+ console.log(JSON.stringify(records, null, 2));
9
+ if (records.some(record => !record.ok)) process.exitCode = 1;
10
+ } catch (error) {
11
+ console.error(error.code ?? 'ERROR', error.message);
12
+ process.exitCode = 1;
13
+ }
@@ -0,0 +1,22 @@
1
+ // Run explicitly with: node --env-file=.env examples/cloudflare-decisions.mjs
2
+ // These two hosted inference calls may incur charges.
3
+ import { createBridge, CloudflareDecisionsProvider, choice, noul, publicError } from '../src/index.mjs';
4
+
5
+ try {
6
+ const provider = new CloudflareDecisionsProvider({ model: process.env.DECISION_MODEL ?? 'clef' });
7
+ const bridge = createBridge({ provider });
8
+ const state = { ticket: 'The production checkout is unavailable.' };
9
+ const common = await bridge.evaluate({ state, questions: {
10
+ urgent: noul('Does this need immediate attention?'),
11
+ route: choice('Choose a route.', [{ value: true, description: 'Immediate incident response.' },
12
+ { value: 'review', description: 'Normal review.' }]),
13
+ } });
14
+ console.log(JSON.stringify(common, null, 2));
15
+ const native = await provider.createDecision({ state, questions: {
16
+ urgent: { type: 'noul', instructions: 'Does this need immediate attention?' },
17
+ } });
18
+ console.log(JSON.stringify(native, null, 2));
19
+ } catch (error) {
20
+ console.error(publicError(error));
21
+ process.exitCode = 1;
22
+ }
@@ -0,0 +1,32 @@
1
+ // Run: node --env-file=.env examples/cloudflare-omni.mjs clip.wav clip.mp4
2
+ // Makes two hosted inference calls. Supply valid local WAV and MP4 fixtures.
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createBridge, CloudflareDecisionsProvider, noul, publicError } from '../src/index.mjs';
5
+
6
+ try {
7
+ const [audioPath, videoPath] = process.argv.slice(2);
8
+ if (!audioPath || !videoPath) throw new Error('Supply a WAV path and an MP4 path.');
9
+ const [audio, video] = await Promise.all([readFile(audioPath), readFile(videoPath)]);
10
+ const audioUrl = 'data:audio/wav;base64,' + audio.toString('base64');
11
+ const videoUrl = 'data:video/mp4;base64,' + video.toString('base64');
12
+ const provider = new CloudflareDecisionsProvider({ model: 'clef-omni' });
13
+ const bridge = createBridge({ provider });
14
+ const common = await bridge.evaluate({
15
+ input: [
16
+ { type: 'audio', dataUrl: audioUrl },
17
+ { type: 'video', dataUrl: videoUrl },
18
+ { type: 'text', text: 'Assess these clips.' },
19
+ ],
20
+ questions: { speech: noul('Is audible speech present?') },
21
+ });
22
+ const native = await provider.createDecision({
23
+ state: { purpose: 'Assess these clips.' },
24
+ audio: [audioUrl],
25
+ videos: [videoUrl],
26
+ questions: { speech: { type: 'noul', instructions: 'Is audible speech present?' } },
27
+ });
28
+ console.log(JSON.stringify({ common, native }, null, 2));
29
+ } catch (error) {
30
+ console.error(publicError(error));
31
+ process.exitCode = 1;
32
+ }
@@ -0,0 +1,21 @@
1
+ // Integrate this handler into an existing Worker with a configured AI binding.
2
+ // Authorize callers and cap incoming HTTP bodies at 13 MiB before this handler.
3
+ // For Omni, select clef-omni and use its configured budget (36 MiB by default).
4
+ // SDK preflight runs after JSON parsing; it does not protect unbounded HTTP intake.
5
+ import { createBridge, CloudflareDecisionsProvider, publicError } from '../src/index.mjs';
6
+
7
+ export default {
8
+ async fetch(request, env) {
9
+ if (request.method !== 'POST') return new Response('Use POST with a common decision request.', { status: 405 });
10
+ try {
11
+ const provider = new CloudflareDecisionsProvider({ transport: 'binding', ai: env.AI, model: 'clef' });
12
+ const bridge = createBridge({ provider });
13
+ const result = await bridge.evaluate(await request.json(), { signal: request.signal });
14
+ return Response.json(result);
15
+ } catch (error) {
16
+ const details = publicError(error);
17
+ const invalid = ['VALIDATION_ERROR', 'UNSUPPORTED_CAPABILITY', 'REQUEST_TOO_LARGE'].includes(details.code);
18
+ return Response.json({ error: details }, { status: invalid ? 400 : 502 });
19
+ }
20
+ },
21
+ };
@@ -0,0 +1,28 @@
1
+ {
2
+ "model": "clef",
3
+ "state": {
4
+ "ticket": "The production checkout is unavailable."
5
+ },
6
+ "questions": {
7
+ "urgent": {
8
+ "type": "noul",
9
+ "instructions": "Does this need immediate attention?"
10
+ },
11
+ "route": {
12
+ "type": "choice",
13
+ "instructions": "Choose a route.",
14
+ "criteria": {
15
+ "incident": "Immediate incident response.",
16
+ "review": "Normal review."
17
+ }
18
+ },
19
+ "severity": {
20
+ "type": "score",
21
+ "instructions": "Rate severity.",
22
+ "criteria": [
23
+ "No production impact.",
24
+ "Production functionality is unavailable."
25
+ ]
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "../../schemas/decision.config.schema.json",
3
+ "schemaVersion": 1,
4
+ "provider": "custom-generative",
5
+ "model": "YOUR_ANTHROPIC_MODEL",
6
+ "apiKeyEnv": "ANTHROPIC_API_KEY",
7
+ "timeoutMs": 30000,
8
+ "preset": "anthropic"
9
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "provider": "cloudflare-decisions",
4
+ "model": "clef",
5
+ "apiKeyEnv": "CLOUDFLARE_API_TOKEN",
6
+ "timeoutMs": 30000,
7
+ "maxRetries": 0
8
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "../../schemas/decision.config.schema.json",
3
+ "schemaVersion": 1,
4
+ "provider": "custom-decision",
5
+ "module": "../custom/decision-provider.mjs",
6
+ "model": "jev-latest",
7
+ "apiKeyEnv": "TYPESAFE_API_KEY",
8
+ "timeoutMs": 30000
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "../../schemas/decision.config.schema.json",
3
+ "schemaVersion": 1,
4
+ "provider": "custom-generative",
5
+ "module": "../custom/generative-provider.mjs",
6
+ "model": "YOUR_SERVED_MODEL",
7
+ "apiKeyEnv": "PROJECT_MODEL_KEY",
8
+ "options": {
9
+ "baseURL": "https://your-model-service.example/v1",
10
+ "responseFormat": "json_schema",
11
+ "tokenParameter": "max_tokens"
12
+ },
13
+ "timeoutMs": 30000
14
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "provider": "jev",
4
+ "model": "jev-latest",
5
+ "apiKeyEnv": "TYPESAFE_API_KEY",
6
+ "timeoutMs": 30000
7
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "../../schemas/decision.config.schema.json",
3
+ "schemaVersion": 1,
4
+ "provider": "custom-generative",
5
+ "model": "YOUR_LOCAL_MODEL",
6
+ "baseURL": "http://127.0.0.1:8000/v1",
7
+ "noAuth": true,
8
+ "responseFormat": "json_schema",
9
+ "tokenParameter": "max_tokens",
10
+ "timeoutMs": 30000,
11
+ "preset": "openai-compatible"
12
+ }