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

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 (113) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +79 -0
  3. package/CONTRIBUTING.md +57 -0
  4. package/LICENSE +21 -0
  5. package/README.md +313 -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 +139 -0
  10. package/docs/development/DESIGN.md +77 -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 +153 -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 +83 -0
  21. package/docs/guides/GETTING_STARTED.md +252 -0
  22. package/docs/guides/OPENAI_DECISIONS.md +252 -0
  23. package/docs/guides/PLATFORMS.md +107 -0
  24. package/docs/guides/SWITCHING_MODELS.md +208 -0
  25. package/docs/guides/TROUBLESHOOTING.md +96 -0
  26. package/docs/reference/CLI.md +213 -0
  27. package/docs/reference/CONFIGURATION.md +170 -0
  28. package/docs/reference/ERRORS.md +85 -0
  29. package/docs/reference/PROVIDERS.md +116 -0
  30. package/docs/reference/SDK.md +394 -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 +92 -0
  62. package/src/cli/config-validation.mjs +89 -0
  63. package/src/cli/config.mjs +85 -0
  64. package/src/cli/decisions.mjs +21 -0
  65. package/src/cli/initialize.mjs +85 -0
  66. package/src/cli/io.mjs +171 -0
  67. package/src/cli/main.mjs +281 -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 +50 -0
  81. package/src/embedded.mjs +24 -0
  82. package/src/errors.mjs +49 -0
  83. package/src/index.d.mts +232 -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 +31 -0
  102. package/src/providers/decisions-validation.mjs +151 -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 +58 -0
  107. package/src/providers/openai-profile.mjs +24 -0
  108. package/src/questions.mjs +15 -0
  109. package/src/response-transport.mjs +176 -0
  110. package/src/runtime-env.mjs +4 -0
  111. package/src/transport.mjs +51 -0
  112. package/src/validation.mjs +245 -0
  113. package/src/version.mjs +2 -0
@@ -0,0 +1,394 @@
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 configured default, which an omitted request model inherits.
76
+ OpenAI Decisions uses the `default` selector when its provider configuration omits
77
+ `model`; `luna` and explicit `gpt-6-luna` are also supported. Its selectors
78
+ resolve to upstream IDs within the provider. No default is assumed for a generative provider.
79
+ `options` accepts `signal: AbortSignal` and `timeoutMs`, overriding the bridge's
80
+ 60-second evaluation budget for this call. It does not mutate the provider's
81
+ independent transport timeout, including Workers AI binding calls.
82
+
83
+ Ordered parts are `{ type: 'text', text }`, `{ type: 'image', dataUrl, detail? }`,
84
+ `{ type: 'audio', dataUrl }`, or `{ type: 'video', dataUrl }`. Audio/video are
85
+ supported by Cloudflare Clef-omni and explicitly capable custom providers; other
86
+ built-ins reject them before dispatch. Cloudflare requires images → audio →
87
+ videos → text. Media use embedded data URLs, with format and size checks described
88
+ in the [Cloudflare guide](../guides/CLOUDFLARE_DECISIONS.md#omni-audio-and-video).
89
+
90
+ The bridge snapshots and freezes its input, runs sequential state or input preparation,
91
+ invokes the provider once, validates the result, then observes an immutable result.
92
+ Built-in transports can retry eligible response statuses within that invocation
93
+ when client retries are configured.
94
+
95
+ ### `bridge.getCapabilities(model?)` and `bridge.validate(request)`
96
+
97
+ Capability lookup is synchronous and reports implemented features and limits.
98
+ `validate` applies the default model selection, snapshots/freezes the request, and checks capabilities
99
+ and preparation-extension compatibility without inference. Unsupported features
100
+ fail with `UNSUPPORTED_CAPABILITY` before dispatch. Native Decisions has no model
101
+ listing operation; capability lookup is not account model discovery.
102
+ OpenAI validated requests retain the selected value, while preflight checks the
103
+ resolved outgoing request, including its model ID, against the byte limit.
104
+
105
+ Providers can implement optional `validateRequest(resolvedRequest)` for additional
106
+ static checks. This synchronous hook receives a frozen request, must return no
107
+ value, and rejects by throwing; it must not perform inference or other side
108
+ effects. OpenAI Decisions and Cloudflare Clef use it to check native fields and UTF-8 request
109
+ bytes against `maxRequestBytes`. The bridge repeats validation after preparation.
110
+
111
+ ### `bridge.listModels(options?)`
112
+
113
+ Calls the provider's listing operation. Returns `{ models: [{ name, ... }], ... }`.
114
+ Pagination markers are preserved; this is not a promise of an exhaustive catalog.
115
+ OpenAI Decisions, Cloudflare Clef, and custom providers without listing support
116
+ throw `UNSUPPORTED_OPERATION`.
117
+
118
+ ## Request data
119
+
120
+ `state` is a string, plain JSON object, or array. It may be empty, but not null,
121
+ a top-level number, or a top-level boolean. Values nested within objects/arrays can
122
+ be any JSON values. JSON snapshots reject `undefined`, cycles, non-finite numbers,
123
+ functions, dates, class instances, accessors, symbols, sparse arrays, and extra or
124
+ non-enumerable array/object properties. Limits: nesting depth 100, 200,000 nodes.
125
+
126
+ `questions` is a nonempty object keyed by application-owned identifiers. Every
127
+ question requires non-null `instructions`: nonempty text, a JSON object, or an
128
+ array. Descriptions can be text or structured JSON. Only choice descriptions permit
129
+ raw null. Question IDs and legacy choice-map keys must be nonempty; typed string
130
+ choice values may be empty. Request and question unknown fields are rejected.
131
+
132
+ ### Question builders
133
+
134
+ ```javascript
135
+ const q1 = choice('Which queue should receive this?', {
136
+ billing: 'Invoices or payment problems.',
137
+ engineering: 'Technical defects.',
138
+ unknown: 'Insufficient evidence.',
139
+ });
140
+ const q2 = noul('Does the evidence show a failed test?', {
141
+ true: 'At least one executed test failed.',
142
+ false: 'No executed test is shown to have failed.',
143
+ });
144
+ const q3 = score('How far did processing progress?', [
145
+ 'Not started.', 'Started but incomplete.', 'Completed.',
146
+ ]);
147
+ ```
148
+
149
+ Builders return immutable question objects. `choice(instructions, criteria)` accepts
150
+ a label map containing 1 to 255 options. `noul(instructions, criteria?)` accepts an
151
+ optional criteria object,
152
+ with only `true` and `false` keys. `score(instructions, criteria)` accepts **2–10**
153
+ ordered levels; numeric level indices are zero-based. There is no boolean alias:
154
+ `noul` means a probability, not a thresholded truth value.
155
+
156
+ `choice(instructions, options)` accepts ordered objects with unique string or
157
+ boolean `value` fields and optional text or structured JSON descriptions. Boolean `true` and string
158
+ `"true"` remain distinct. `scoreLevels(instructions, levels)` accepts at least two
159
+ ordered label/description objects. Provider preflight applies service limits:
160
+ OpenAI Decisions and Cloudflare require 2–255 choices; Cloudflare limits scores
161
+ to 2–10 levels, while OpenAI labelled scores have no additional client count cap.
162
+
163
+ ## Returned data
164
+
165
+ ```text
166
+ EvaluationResult
167
+ model Reported model identity, or explicitly marked fallback
168
+ answers.<question-id> Typed answer, matched to that input question
169
+ usage Token counts; unreported generic counts are null
170
+ meta Provenance, version, timings, diagnostics
171
+ ```
172
+
173
+ **Legacy choice:** `{ type: 'choice', choice, probabilities, confidence? }`.
174
+ The probability keys must exactly match the options, all values must be in [0,1],
175
+ and the sum must be within 0.001 of 1, except for the provider-selected rounding
176
+ allowance described below. The selected option must be maximal within
177
+ rounding tolerance. Jev requires reported choice/score confidence; the other
178
+ native providers validate and preserve their service-specific confidence.
179
+ Generative providers derive the selected option from
180
+ the distribution and omit confidence. Generative ties choose the first label in
181
+ JavaScript property enumeration order; numeric-looking keys
182
+ follow JavaScript's numeric property ordering.
183
+ Native selections are retained after validation.
184
+
185
+ Typed choice questions instead return a typed string/boolean selection and an
186
+ array of `{ value, probability }` entries. See the [Decisions guide](../guides/OPENAI_DECISIONS.md).
187
+
188
+ Cloudflare selects four-decimal probability validation. Distributions aligned to
189
+ that precision allow `max(0.001, optionCount * 0.00005)`, plus a small floating-point
190
+ allowance. Other Cloudflare distributions and existing providers retain the 0.001
191
+ baseline. The bridge selects this policy from the provider contract, never from
192
+ response metadata, and preserves all returned numbers.
193
+
194
+ **Noul:** `{ type: 'noul', noul }`, with a number in [0,1]. The library never
195
+ thresholds this into true/false without application code.
196
+
197
+ **Score:** `{ type: 'score', score, probabilities, legend, confidence? }`.
198
+ For N levels, keys are `'0'` through `String(N-1)`. The score is the weighted mean
199
+ of these indices; validation tolerance is `0.02 * N` (accommodating 2-decimal rounded probability distributions from live services). `legend` must match the input
200
+ rubric exactly. Generative providers calculate score and legend locally. Native
201
+ values are validated and preserved, not normalized or replaced.
202
+
203
+ **Usage:** `input_tokens` and `output_tokens` are nonnegative integers or null.
204
+ Jev, OpenAI Decisions, and Cloudflare require reported integer counts. OpenAI and
205
+ Anthropic cache creation/read counts are preserved separately when provided;
206
+ Cloudflare does not fabricate cache or total-token fields. No total price, inferred cache saving, or
207
+ estimated token count is fabricated.
208
+
209
+ **Metadata:**
210
+
211
+ | Field | Meaning |
212
+ | --- | --- |
213
+ | `provider` | Bridge backend name; native Jev reports `typesafe` |
214
+ | `providerKind` | `decision` or `generative`; authoritative provider classification, never response-selected |
215
+ | `requestedModel` | Selected model after applying the provider default; OpenAI retains `default`, `luna`, or the explicit ID |
216
+ | `resolvedModel` | Upstream model ID sent by OpenAI Decisions; optional for other providers |
217
+ | `probabilitySource` | `native`, `elicited`, or custom/`unspecified` provenance |
218
+ | `confidenceSource` | `native` or `unavailable` for built-in providers |
219
+ | `modelSource` | `reported` or `requested` fallback for built-in providers |
220
+ | `bridgeVersion`, `schemaVersion` | Package version and result contract version |
221
+ | `attempts`, `requestId` | Transport diagnostics when supplied |
222
+ | `gatewayLogId` | Cloudflare Gateway response identifier when reported |
223
+ | `openaiDecisions`, `cloudflareDecisions` | Provider-specific reported usage and native answer/confidence metadata |
224
+ | `durationMs` | Provider transport duration, including retries and body reading |
225
+ | `totalDurationMs` | Bridge duration through observation |
226
+ | `observerDurationMs` | Observation phase duration; initially zero inside observers |
227
+ | `observerFailures` | Names of failed/timed-out observers |
228
+
229
+ Metadata can have additional JSON fields. Application logic should not assume
230
+ unavailable metadata exists. Result objects and nested data returned by the bridge
231
+ are immutable. Standalone validation functions return snapshots, not frozen values.
232
+
233
+ `schemaVersion` remains 1 for legacy requests and 2 for ordered images/text or
234
+ richer question forms. Audio/video requests use 3. This result version is separate
235
+ from the CLI configuration schema, which remains 1.
236
+
237
+ ## Extensions
238
+
239
+ An extension has a unique printable `name` (maximum 100 characters), optional
240
+ `prepareState(state, context)`, `prepareInput(input, context)`, and
241
+ `onResult(result, context)`. Preparation extensions must support the request's
242
+ input form; incompatible hooks fail explicitly before dispatch.
243
+
244
+ Preparation returns replacement state or input, or `undefined` to leave it unchanged.
245
+ Preparation exceptions fail before inference with `EXTENSION_ERROR`; transformed
246
+ content is revalidated, including capabilities and provider preflight. Preparation
247
+ cannot rewrite question definitions or switch the model.
248
+ Construct the request directly for those changes.
249
+
250
+ Observers run after successful inference; their failures are collected in
251
+ `meta.observerFailures`. They do not change answers, fail the successful request,
252
+ or trigger another billed call. Each observer gets its own timeout, default 1,000
253
+ milliseconds. Results shown to observers precede final timing/failure aggregation.
254
+
255
+ `context` contains `provider`, `model`, frozen `questionIds`, and an `AbortSignal`.
256
+ Custom callbacks should honor the signal. The bridge can stop waiting for ignored
257
+ signals, but cannot terminate arbitrary user code or its external side effects.
258
+ See [customization](../guides/CUSTOMIZATION.md) for runnable examples.
259
+
260
+ ## Cancellation and deadlines
261
+
262
+ ```javascript
263
+ const controller = new AbortController();
264
+ const resultPromise = bridge.evaluate(request, {
265
+ signal: controller.signal,
266
+ timeoutMs: 15000,
267
+ });
268
+ // controller.abort() cancels the in-flight request when needed.
269
+ ```
270
+
271
+ The transport deadline covers dispatch, attempts, backoff, and body reading for
272
+ REST and Workers AI binding calls. A bridge deadline
273
+ also covers preparation and the provider. The first applicable deadline wins.
274
+ Observer time is additional and bounded separately. Aborting after a successful
275
+ inference while observing preserves the successful result and records observer
276
+ failure; it does not pretend the billed evaluation failed.
277
+
278
+ Elapsed time is checked before dispatch and before accepting completion, including
279
+ when synchronous work delays timer callbacks. Nested transport calls inherit the
280
+ remaining bridge deadline. Synchronous user code cannot be interrupted; an
281
+ overrun is detected when control returns, without undoing completed side effects.
282
+
283
+ ## SDK batch evaluation
284
+
285
+ ```javascript
286
+ const records = await evaluateBatch(bridge, requests, {
287
+ concurrency: 4,
288
+ stopOnError: false,
289
+ signal: controller.signal,
290
+ });
291
+ ```
292
+
293
+ This array API validates all raw requests, including provider preflight, before
294
+ dispatch. Preparation runs during each evaluation, so errors introduced by hooks
295
+ remain per-request failures. Maximum array length is
296
+ 10,000; concurrency is 1–64. Records retain input order and are either
297
+ `{ index, ok: true, result }` or `{ index, ok: false, error }`.
298
+ With `stopOnError`, already-started work completes and new work is marked
299
+ `BATCH_SKIPPED`. Cancellation marks undispatched records `ABORTED`. There is no
300
+ automatic model fallback. The CLI has a separate streaming JSONL implementation.
301
+
302
+ ## Additional exports
303
+
304
+ - `createProvider(config)`: factory requiring an explicit `type` equal to `jev`,
305
+ `openai-decisions`, or `cloudflare-decisions`.
306
+ - `JevProvider`, `OpenAIDecisionsProvider`, `CloudflareDecisionsProvider`: native provider classes.
307
+ - `scoreLevels(instructions, levels)` and the array overload of `choice`:
308
+ labelled rubrics and typed string/boolean options, with provider capability checks.
309
+ - `OpenAIDecisionsProvider.createDecision(request, options?)`: dedicated native
310
+ access returning `{ decision, meta }`; see the [Decisions guide](../guides/OPENAI_DECISIONS.md).
311
+ - `CloudflareDecisionsProvider.createDecision(request, options?)`: dedicated native
312
+ Clef access returning `{ decision, meta }`; see the [Cloudflare guide](../guides/CLOUDFLARE_DECISIONS.md).
313
+ - `validateRequest(input, defaultModel?)`: snapshot and validate. Requires
314
+ an own `input.model` property or an explicitly supplied default; inherited
315
+ models are ignored, and it never selects a Jev model.
316
+ Question builders and schema generation validate without selecting an inference model.
317
+ - `validateResult(input, questions, { native = false, probabilityDecimalPlaces })`: snapshot supplied
318
+ questions and validate the canonical result; native mode requires confidence
319
+ and reported counts.
320
+ - `decideChoice(answer, { minProbability, minMargin = 0, abstainOptions = [], probabilityDecimalPlaces })`:
321
+ returns a policy result, never executes an action. `minProbability` is required.
322
+ - `DecisionError`, `publicError(error)`: public structured failure handling.
323
+
324
+ `decideChoice` returns `status: 'selected' | 'review'`, the model's choice, selected
325
+ probability, margin to the runner-up, and a reason. Thresholds are your policy;
326
+ no default claims of safety or empirical calibration are implied.
327
+ Its optional `probabilityDecimalPlaces` uses the same sum validation as
328
+ `validateResult`. Supply the precision from the provider's contract when using
329
+ rounded distributions, such as Cloudflare's four-decimal results. Omission keeps
330
+ the existing 0.001 tolerance; the helper never infers precision from an answer.
331
+
332
+ ## Custom entry points
333
+
334
+ The root exports common `Provider`, `ProviderKind`, `DecisionProvider`, request,
335
+ result, and transport types. Every provider requires `kind`. Generative classes
336
+ and types use the explicit `custom/generative` entry point; the root provider
337
+ factory selects native decision providers.
338
+
339
+ | Entry point | Runtime exports | Types |
340
+ | --- | --- | --- |
341
+ | `custom/decision` | `defineDecisionProvider` | `DecisionProviderDefinition`; `DecisionProvider` |
342
+ | `custom/generative` | `defineGenerativeProvider`, `createGenerativeProvider`, `OpenAICompatibleProvider`, `OpenRouterProvider`, `AnthropicProvider` | `GenerativeProvider`, definition/config, and preset options |
343
+ | `custom/generative` | `buildResponseSchema`, `buildPrompt`, `parseGeneratedJson`, `normalizeGenerated` | Typed legacy generative strategy helpers |
344
+
345
+ Import these as package subpaths under `@fayegram-ai/decision-bridge/`. All SDK
346
+ entry points are browser-safe ESM. Definition helpers preserve callback receivers
347
+ and validate/freeze wrappers; they do not implement a new protocol. Both kinds
348
+ work with the same bridge, extensions, cancellation, and batch API.
349
+
350
+ Generative results always use elicited probabilities and unavailable confidence.
351
+ The bridge rejects native provenance and answer confidence from that path.
352
+ Supplied probability/confidence provenance labels must be nonempty strings;
353
+ decision providers may preserve custom string labels. Generative prompt and
354
+ normalization helpers independently validate the shared-state, legacy-question
355
+ profile and explicitly reject unsupported request capabilities.
356
+ Decision-kind classification alone does not require confidence or known counts;
357
+ `validateResult({ native: true })` is a separate strict validation option used for
358
+ Jev's contract. See [custom decision](../guides/CUSTOM_DECISION.md),
359
+ [custom generative](../guides/CUSTOM_GENERATIVE.md), and the shared
360
+ [custom contract](../guides/CUSTOM_PROVIDERS.md).
361
+
362
+ ## Validation guarantees
363
+
364
+ Legacy choice criteria maps allow at most 255 options, matching Jev's contract. Noul
365
+ criteria must be omitted or an object; its present true/false descriptions cannot
366
+ be raw null. Score levels likewise cannot be raw null (nested null JSON values are
367
+ still allowed). These invalid forms now fail before provider dispatch.
368
+
369
+ `decideChoice()` rejects unknown option names and abstention labels not present in
370
+ the answer. `evaluateBatch()` validates array holes as invalid requests before any
371
+ item is dispatched, rather than spending on adjacent valid entries.
372
+
373
+ The optional third argument to `validateResult()` must be a plain options object
374
+ with optional boolean `native` and integer `probabilityDecimalPlaces` (1–15) fields.
375
+ Providers can declare a pure synchronous `getResultValidationOptions(model)` hook;
376
+ its policy is validated before inference and is not read from response metadata.
377
+ Returning a Promise is a configuration error; its rejection is observed without
378
+ calling inference or reading a custom `then` accessor.
379
+ Cloudflare selects four-decimal rounding; other built-ins retain the 0.001 sum tolerance.
380
+ Unknown keys, accessors, symbol keys, non-enumerable fields, a non-boolean `native`,
381
+ or invalid probability precision are rejected as `CONFIGURATION_ERROR`.
382
+ An omitted or explicitly undefined `native` value keeps the default `false`.
383
+ Extension arrays must be dense; a hole is not an omitted extension.
384
+
385
+ Standalone `validateResult()` snapshots both the response and supplied questions.
386
+ Matching structured score rubrics compare by JSON content, including plain
387
+ objects with a null prototype. Invalid question JSON fails with `VALIDATION_ERROR`;
388
+ accessors are rejected without invocation. Question and result inputs are neither
389
+ mutated nor frozen.
390
+
391
+ Native provider cancellation/timeout errors preserve the last observed sanitized
392
+ request ID and attempt count when available. The bridge may settle its own
393
+ cancellation race before the provider returns diagnostics; do not require those
394
+ 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
+ }