@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,374 @@
1
+ # Cloudflare Clef decisions
2
+
3
+ Use `cloudflare-decisions` for hosted inference with `clef` (the default),
4
+ `clef-flash`, or `clef-omni`. All three support common bridge requests and the
5
+ dedicated native API. Each accepts its full `@cf/cloudflare/<model>` alias.
6
+ The provider normalizes the endpoint identifier and the required input selector together.
7
+ See the [model selector inventory](../../README.md#model-selectors) for supported
8
+ short selectors and full forms.
9
+
10
+ | Model | Hosted context, in tokens | Default client request budget | Media |
11
+ | --- | ---: | ---: | --- |
12
+ | Clef | 65,536 | 13 MiB | Images |
13
+ | Clef-flash | 24,576 | 13 MiB | Images |
14
+ | Clef-omni | 64,000 | 36 MiB | Images, audio, video |
15
+
16
+ Questions and media consume context before text state; the service truncates
17
+ remaining text state to fit. Requests whose questions/media exhaust context can
18
+ fail. Byte checks cannot guarantee token fit or detect remote truncation.
19
+ Clef's serving improvements and Clef-flash's lower pricing apply upstream without
20
+ client configuration. Consult the official [model pages](#official-references)
21
+ for current pricing and availability; the bridge does not discover account access.
22
+
23
+ ## Supported inference paths
24
+
25
+ | Path | Required application configuration |
26
+ | --- | --- |
27
+ | REST | Account ID and Workers AI API token |
28
+ | REST through AI Gateway | REST credentials and an existing Gateway ID in that account |
29
+ | Workers AI binding | An existing Worker's AI binding injected as `ai` |
30
+ | Workers AI binding through Gateway | AI binding and an existing Gateway ID in that account |
31
+
32
+ Common `bridge.evaluate()` and native `provider.createDecision()` access work
33
+ through either transport. REST is the default and is also the CLI transport.
34
+ Gateway routing is optional. The bridge supplies inference integration;
35
+ applications configure their own Worker and Gateway resources.
36
+
37
+ ## REST credentials and common access
38
+
39
+ Create a Cloudflare API token with **Account > Workers AI > Read** permission
40
+ scoped to the intended account. Set these variables in your application's ignored
41
+ environment file:
42
+
43
+ ```dotenv
44
+ CLOUDFLARE_ACCOUNT_ID=replace_with_your_account_id
45
+ CLOUDFLARE_API_TOKEN=replace_with_your_api_token
46
+ ```
47
+
48
+ Load it explicitly with Node's `--env-file` or the CLI's `--env-file` flag.
49
+ The REST constructor reads these variables only for the official Cloudflare origin.
50
+ A custom origin requires explicit `accountId` and `apiKey`.
51
+ REST requests use
52
+ `/client/v4/accounts/{accountId}/ai/run/@cf/cloudflare/{model}`.
53
+ The account ID selects the account in that URL; the token authorizes access.
54
+ Gateway inference uses the same Workers AI permission. Gateway management
55
+ permissions apply to creating or changing Gateway resources, which the SDK does
56
+ not perform.
57
+
58
+ ```javascript
59
+ import { createBridge, CloudflareDecisionsProvider, choice, noul, scoreLevels } from '@fayegram-ai/decision-bridge';
60
+
61
+ const provider = new CloudflareDecisionsProvider({ model: 'clef-flash' });
62
+ const bridge = createBridge({ provider });
63
+ const result = await bridge.evaluate({
64
+ state: { ticket: 'The production checkout is unavailable.' },
65
+ questions: {
66
+ urgent: noul('Does this need immediate attention?'),
67
+ route: choice('Choose a route.', [
68
+ { value: true, description: 'Escalate to the incident team.' },
69
+ { value: 'review', description: 'Send to normal review.' },
70
+ ]),
71
+ severity: scoreLevels('Rate severity.', [
72
+ { label: 'Low', description: 'No production impact.' },
73
+ { label: 'High', description: 'Production functionality is unavailable.' },
74
+ ]),
75
+ },
76
+ });
77
+ console.log(result.answers, result.usage, result.meta.cloudflareDecisions);
78
+ ```
79
+
80
+ Structured state, instructions, criteria, and rubric descriptions remain JSON.
81
+ Typed choices use generated native IDs and map back to the exact original
82
+ string or boolean values; `true` and `"true"` remain distinct.
83
+
84
+ ## Dedicated native access
85
+
86
+ ```javascript
87
+ const { decision, meta } = await provider.createDecision({
88
+ state: { ticket: 'The production checkout is unavailable.' },
89
+ questions: {
90
+ urgent: { type: 'noul', instructions: 'Does this need immediate attention?' },
91
+ route: { type: 'choice', instructions: 'Choose a route.',
92
+ criteria: { incident: 'Immediate incident response.', review: 'Normal review.' } },
93
+ severity: { type: 'score', instructions: 'Rate severity.',
94
+ criteria: ['No production impact.', 'Production functionality is unavailable.'] },
95
+ },
96
+ });
97
+ console.log(decision.answers, meta.requestId);
98
+ ```
99
+
100
+ The SDK supplies an omitted model from the provider default. Explicit `null`
101
+ model or execution options are invalid and fail before dispatch. Native questions
102
+ use `criteria`; common typed `options` and labelled `levels` are available through
103
+ `bridge.evaluate()`. Results preserve native probabilities, confidence, model,
104
+ and input/output token counts. Unreported cache or total-token counts are not invented.
105
+ `createDecision()` accepts an optional second argument containing `signal`.
106
+ Configure its timeout on the provider; direct native access bypasses bridge
107
+ extensions and the bridge's separate total deadline.
108
+
109
+ ## Embedded images and limits
110
+
111
+ Clef accepts up to four embedded PNG, JPEG, or WebP images. Native `images` entries
112
+ can be base64 data URLs or `{ content_type, base64 }` objects. Remote URLs are
113
+ rejected. Common ordered input places all images before all text:
114
+
115
+ ```javascript
116
+ import { readFile } from 'node:fs/promises';
117
+
118
+ const bytes = await readFile('screenshot.png');
119
+ const imageResult = await bridge.evaluate({
120
+ input: [
121
+ { type: 'image', dataUrl: 'data:image/png;base64,' + bytes.toString('base64') },
122
+ { type: 'text', text: 'Assess this screenshot.' },
123
+ ],
124
+ questions: { error: noul('Does the screenshot show an error?') },
125
+ });
126
+ console.log(imageResult.answers.error.noul);
127
+ ```
128
+
129
+ Text parts concatenate exactly, without inserted separators. Image detail can be
130
+ omitted, `null`, or `auto`; explicit `low`, `high`, and `original` are unsupported.
131
+ Safety identifiers are unsupported by this provider.
132
+
133
+ | Boundary | Limit |
134
+ | --- | --- |
135
+ | Questions per request | 1–64 |
136
+ | Question IDs | 1–100 ASCII letters, digits, `_`, `.`, or `-` |
137
+ | Choice options | 2–255 |
138
+ | Score levels | 2–10, ordered lowest first, indexed from zero |
139
+ | Images | At most four |
140
+ | Decoded image size | 4 MiB each; 8 MiB combined |
141
+ | Image dimensions | Positive dimensions; at most 16 million pixels each |
142
+ | Serialized request | Clef/Clef-flash: at most 13 MiB; Omni: 36 MiB by default, configurable through 64 MiB |
143
+
144
+ Preflight checks supported formats, base64 syntax, headers, dimensions, and size.
145
+ Header inspection is not full image decoding; the service can reject corrupt image data.
146
+ Limits include execution options and binding envelope overhead. Omni's 36 MiB
147
+ default is a bridge policy allowing base64 expansion of the permitted decoded
148
+ media; its published schema specifies no whole-request byte ceiling. The effective
149
+ request model selects the budget, including per-call overrides. An explicit
150
+ `maxRequestBytes` remains a caller ceiling and is never enlarged by selecting Omni.
151
+
152
+ Cloudflare's four-decimal probability precision receives a count-aware sum
153
+ allowance: the larger of 0.001 and half a decimal unit per option, plus floating-point
154
+ slack. This additional allowance applies only when every probability aligns to
155
+ that precision. Values are preserved without renormalization; missing options,
156
+ out-of-range probabilities, wrong choices, and inconsistent score rubrics remain errors.
157
+
158
+ When applying `decideChoice` to these answers, supply the same precision from
159
+ the provider contract. The helper keeps the stricter baseline when it is omitted:
160
+
161
+ ```javascript
162
+ import { decideChoice } from '@fayegram-ai/decision-bridge';
163
+
164
+ const policy = decideChoice(result.answers.route, {
165
+ minProbability: 0.8,
166
+ probabilityDecimalPlaces: provider.getResultValidationOptions(result.model).probabilityDecimalPlaces,
167
+ });
168
+ console.log(policy.status, policy.reason);
169
+ ```
170
+
171
+ ## Omni audio and video
172
+
173
+ Common Omni input follows **images → audio → videos → text**, with any group
174
+ optional. Order within each group is preserved, and text concatenates exactly.
175
+ Interleaving groups is rejected because native requests represent them as separate
176
+ arrays. Common requests still require exactly one of `state` or `input`; native
177
+ requests can combine structured `state` with `images`, `audio`, and `videos`.
178
+
179
+ ```javascript
180
+ const omniProvider = new CloudflareDecisionsProvider({ model: 'clef-omni' });
181
+ const omniBridge = createBridge({ provider: omniProvider });
182
+ const audioUrl = 'data:audio/wav;base64,' + (await readFile('clip.wav')).toString('base64');
183
+ const videoUrl = 'data:video/mp4;base64,' + (await readFile('clip.mp4')).toString('base64');
184
+ const mediaResult = await omniBridge.evaluate({
185
+ input: [
186
+ { type: 'audio', dataUrl: audioUrl },
187
+ { type: 'video', dataUrl: videoUrl },
188
+ { type: 'text', text: 'Assess these clips.' },
189
+ ],
190
+ questions: { speech: noul('Is audible speech present?') },
191
+ });
192
+ const nativeMedia = await omniProvider.createDecision({
193
+ state: { purpose: 'Assess these clips.' },
194
+ audio: [audioUrl],
195
+ videos: [videoUrl],
196
+ questions: { speech: { type: 'noul', instructions: 'Is audible speech present?' } },
197
+ });
198
+ ```
199
+
200
+ Native media entries accept data URLs or `{ content_type, base64 }` objects.
201
+ Audio supports WAV (`audio/wav`, alias `audio/x-wav`) and MP3 (`audio/mpeg`, alias
202
+ `audio/mp3`). Video supports MP4 (`video/mp4`) and WebM (`video/webm`). Common
203
+ audio/video parts accept only `type` and `dataUrl`; remote URLs, paths, image
204
+ detail, and caller-provided durations are rejected. Other providers and the older
205
+ Clef models reject audio/video before dispatch.
206
+
207
+ | Media | Count | Decoded size per item | Duration per item |
208
+ | --- | ---: | ---: | ---: |
209
+ | Audio | 4 | 8 MiB | 300 seconds |
210
+ | Video | 2 | 16 MiB | 60 seconds |
211
+
212
+ Audio and video together are limited to 16 MiB decoded. Image limits above remain
213
+ separate. Bounded inspectors validate canonical base64, MIME/header consistency,
214
+ and container structure, and reject excessive duration when PCM/float WAV,
215
+ ordinary MP3 frames, MP4 metadata, or WebM metadata establishes it reliably.
216
+ Full decoding, codec validity, and duration that cannot be established locally
217
+ (including compressed WAV, free-format MP3, or missing container duration) remain
218
+ service validation. The bridge performs no transcription, transcoding, frame
219
+ extraction, or streaming, and adds no runtime media dependency.
220
+
221
+ Cloudflare samples video at two frames per second. Video soundtracks are processed
222
+ only when **every supplied video has a soundtrack**. Common results use
223
+ `meta.schemaVersion: 3` when audio or video is present; older requests retain
224
+ their existing schema version. See the [runnable Omni example](../../examples/cloudflare-omni.mjs).
225
+
226
+ ## Workers AI binding
227
+
228
+ Inject your configured binding to avoid REST credentials:
229
+
230
+ ```javascript
231
+ const workerProvider = new CloudflareDecisionsProvider({
232
+ transport: 'binding',
233
+ ai: env.AI,
234
+ model: 'clef',
235
+ rejectIfBusy: true,
236
+ });
237
+ const workerBridge = createBridge({ provider: workerProvider });
238
+ ```
239
+
240
+ Both transports support cancellation, bounded response reads, and deadlines.
241
+ The binding receives `signal`, `returnRawResponse: true`, execution options, and
242
+ Gateway options through `ai.run()`. Request and Gateway log IDs come from each
243
+ response rather than shared binding fields. Available IDs are also preserved in
244
+ safe public errors, including response validation and body-reading failures. See the complete
245
+ [inference handler example](../../examples/cloudflare-worker.mjs).
246
+
247
+ The example covers inference integration. Route requests through your application's
248
+ authorization and enforce an incoming HTTP body limit appropriate to the selected
249
+ model before parsing JSON: 13 MiB for Clef/Clef-flash, or the configured Omni
250
+ budget (36 MiB by default). SDK preflight validates the parsed decision request; it cannot bound
251
+ the HTTP body already consumed by `request.json()` or authorize callers.
252
+
253
+ REST is the ordinary account-ID/token API path. Workers binding and Gateway are
254
+ optional, supported inference paths for all three models. Use your Worker's configured
255
+ AI binding for binding transport and a Gateway in the same account for Gateway
256
+ routing; the bridge does not create or deploy those resources.
257
+
258
+ ## Capacity rejection and AI Gateway
259
+
260
+ `rejectIfBusy` defaults to false. With binding transport, set it to true to
261
+ reject unavailable capacity instead of waiting in the inference queue. Capacity rejection reports
262
+ `CAPACITY_EXCEEDED`, HTTP 429, and numeric `upstreamCode: 3040`.
263
+ It cannot be combined with client retries or multiple Gateway attempts.
264
+
265
+ REST requests with effective `rejectIfBusy: true` fail local preflight with
266
+ `UNSUPPORTED_CAPABILITY` before dispatch. This applies to common/native SDK calls,
267
+ CLI evaluation/batching, and credential-free offline validation, with or without
268
+ Gateway. An explicit native `options.rejectIfBusy: false` can override a true
269
+ provider default; disabled execution options are omitted from REST requests.
270
+
271
+ The hosted
272
+ Clef, Clef-flash, and Clef-omni REST service rejects the `options` field with HTTP 422 and upstream code
273
+ `5012`, including `options: { rejectIfBusy: true }`, although the general Workers AI
274
+ documentation describes that field. The bridge rejects this known unavailable
275
+ feature instead of dispatching it or silently disabling the requested behavior.
276
+ Binding execution options use the third `ai.run()`
277
+ argument; use the binding transport for these models with `rejectIfBusy: true`.
278
+ Omni option acceptance was checked through binding both directly and through
279
+ Gateway; those checks did not deliberately exhaust capacity. This compatibility
280
+ behavior was checked on 2026-10-09; REST rejection was reconfirmed on 2026-10-10.
281
+ The model-specific documentation does not establish whether this REST limitation
282
+ is intentional or a platform defect. Behavior can change upstream.
283
+
284
+ Gateway routing is disabled unless configured. Use an existing Gateway:
285
+
286
+ ```javascript
287
+ const gatewayProvider = new CloudflareDecisionsProvider({
288
+ gateway: {
289
+ id: 'existing-gateway',
290
+ skipCache: true,
291
+ collectLog: false,
292
+ metadata: { application: 'support-routing' },
293
+ requestTimeoutMs: 10000,
294
+ retries: { maxAttempts: 1, retryDelayMs: 0, backoff: 'constant' },
295
+ },
296
+ });
297
+ ```
298
+
299
+ Use the same `gateway` object alongside `transport: 'binding'` and `ai: env.AI`
300
+ for binding requests through Gateway. REST sends documented `cf-aig-*` headers;
301
+ binding sends Gateway settings in the third `ai.run()` argument.
302
+
303
+ | Setting | Accepted value |
304
+ | --- | --- |
305
+ | `id` | Required nonblank printable ASCII string, 1–64 characters |
306
+ | `cacheKey` | Optional nonblank printable ASCII string, 1–200 characters |
307
+ | `cacheTtl` | Integer seconds, 60–2,592,000 (30 days) |
308
+ | `skipCache`, `collectLog` | Optional booleans |
309
+ | `metadata` | At most five entries with JSON primitive values; caller-supplied `cf.*` keys are reserved |
310
+ | `requestTimeoutMs` | Positive integer milliseconds, up to 2,147,483,647 |
311
+ | `retries.maxAttempts` | 1–5; defaults to 1 when Gateway is configured |
312
+ | `retries.retryDelayMs` | 0–60,000 milliseconds |
313
+ | `retries.backoff` | `constant`, `linear`, or `exponential` |
314
+
315
+ The shared validator rejects invalid Gateway settings with `CONFIGURATION_ERROR`
316
+ before dispatch. Metadata is preserved unchanged: the bridge rejects entries
317
+ that [Cloudflare would ignore or remove](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/)
318
+ instead of silently dropping them. Cloudflare's [cache TTL documentation](https://developers.cloudflare.com/ai-gateway/features/caching/)
319
+ describes the upper limit as one month; its [official numeric reference](https://github.com/cloudflare/skills/blob/0cb6116228ad153126f474dce1dfbd05f7944604/skills/cloudflare/references/ai-gateway/README.md)
320
+ specifies 2,592,000 seconds (30 days). Cache TTL and first-response timeout have
321
+ separate validation bounds.
322
+
323
+ Gateway timeout covers time to the first response; the bridge's total deadline
324
+ continues to bound the call. Gateway logging/caching follows the explicit options
325
+ and your Gateway configuration. Client retries and Gateway attempts are separate
326
+ controls and both may repeat inference. Busy mode requires zero client retries
327
+ and one Gateway attempt.
328
+
329
+ The provider defaults to a 30-second transport timeout, zero client retries,
330
+ the model-specific request budget above, and an 8 MiB response limit. Gateway's
331
+ [25 MB cacheable-request limit](https://developers.cloudflare.com/ai-gateway/reference/limits/)
332
+ is separate from inference and client byte limits; the bridge preserves explicit
333
+ Gateway settings without automatically enabling caching. The bridge defaults to a separate
334
+ 60-second total deadline. Explicit client retries use the existing eligible HTTP
335
+ status policy and the same deadline; ambiguous network failures are not retried.
336
+
337
+ ## CLI
338
+
339
+ ```bash
340
+ decision-bridge init my-clef --provider cloudflare-decisions --native
341
+ decision-bridge validate my-clef/request.json --provider cloudflare-decisions --native
342
+ decision-bridge evaluate my-clef/request.json --provider cloudflare-decisions --native --env-file .env
343
+ decision-bridge evaluate request.json --provider cloudflare-decisions --model clef-flash --env-file .env
344
+ decision-bridge evaluate omni-request.json --provider cloudflare-decisions --model clef-omni --env-file .env
345
+ decision-bridge batch requests.jsonl --provider cloudflare-decisions --env-file .env --concurrency 4
346
+ ```
347
+
348
+ Initialization writes credential placeholders. Offline validation needs no credentials.
349
+ Use `--account-id`, `--gateway-id`, and `--no-reject-if-busy` for explicit overrides;
350
+ richer Gateway settings belong in the JSON config's `gateway` object. The SDK
351
+ Gateway example above shows that object's fields. A native request's
352
+ `options.rejectIfBusy` overrides its configured default; an explicit busy-mode
353
+ CLI flag overrides the native request option.
354
+ `--reject-if-busy` is retained for explicit configuration but fails local preflight
355
+ for Clef REST. The CLI uses REST; enabled busy mode requires the SDK binding path.
356
+
357
+ Cloudflare single JSON input and individual JSONL records are read within a
358
+ 64 MiB cap, then the same model-specific SDK preflight applies. This supports
359
+ request-level model overrides and mixed-model batches. JSONL retains its 64 MiB
360
+ total cap. No separate media-upload flags are needed; use embedded input parts
361
+ or native arrays in JSON. The configuration schema version remains 1. `models` is unsupported.
362
+ SDK array batching prevalidates all requests before inference; CLI JSONL batching
363
+ validates incrementally. Neither uses Cloudflare's asynchronous batch service.
364
+
365
+ ## Official references
366
+
367
+ - [Clef](https://developers.cloudflare.com/workers-ai/models/clef/), [Clef-flash](https://developers.cloudflare.com/workers-ai/models/clef-flash/), and [Clef-omni](https://developers.cloudflare.com/workers-ai/models/clef-omni/)
368
+ - [Omni input schema](https://developers.cloudflare.com/workers-ai/models/clef-omni/schema-input.json) and [output schema](https://developers.cloudflare.com/workers-ai/models/clef-omni/schema-output.json)
369
+ - [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)
370
+ - [REST and token permissions](https://developers.cloudflare.com/ai-gateway/usage/rest-api/)
371
+ - [Reject busy requests](https://developers.cloudflare.com/workers-ai/features/reject-if-busy/)
372
+ - [Gateway binding configuration](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/)
373
+ - [Gateway request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/)
374
+ - [Workers AI errors](https://developers.cloudflare.com/workers-ai/platform/errors/)
@@ -0,0 +1,141 @@
1
+ # Customizing decisions and extending the bridge
2
+
3
+ Customization is **per request**. You supply context in `state`, the judgment in
4
+ `instructions`, and the allowed categories/rubric in `criteria`. The same bridge
5
+ instance can evaluate unrelated tasks on successive calls. This changes the
6
+ question sent to the existing model, not its weights or account configuration.
7
+
8
+ ## Runtime templates
9
+
10
+ In your application, save reusable question definitions as `questions.mjs`:
11
+
12
+ ```javascript
13
+ import { choice } from '@fayegram-ai/decision-bridge';
14
+
15
+ export function failureQuestion(includeNetwork = true) {
16
+ return choice('Classify the failure. Use unknown when evidence is insufficient.', {
17
+ dependency: 'An installed package is absent or incompatible.',
18
+ assertion: 'A test ran and an assertion failed.',
19
+ ...(includeNetwork ? { network: 'A connection or network request failed.' } : {}),
20
+ unknown: 'No supplied category is established.',
21
+ });
22
+ }
23
+ ```
24
+
25
+ Save this as `customize.mjs` beside it. Configure `.env` as described in
26
+ [getting started](GETTING_STARTED.md) and run `node --env-file=.env customize.mjs`:
27
+
28
+ ```javascript
29
+ import { createBridge, publicError } from '@fayegram-ai/decision-bridge';
30
+ import { failureQuestion } from './questions.mjs';
31
+
32
+ try {
33
+ const bridge = createBridge({ jev: {} });
34
+ const result = await bridge.evaluate({
35
+ state: { log: "ModuleNotFoundError: No module named 'pytest'" },
36
+ questions: { category: failureQuestion(true) },
37
+ });
38
+ console.log(result.answers.category.choice);
39
+ } catch (error) {
40
+ console.error(publicError(error));
41
+ process.exitCode = 1;
42
+ }
43
+ ```
44
+
45
+ The remaining snippets show individual changes to this SDK flow. The source
46
+ checkout also has [examples/customize.mjs](../../examples/customize.mjs). A rule stored only
47
+ in an unrelated repository file is not automatically known to the provider. Read
48
+ and include the relevant content explicitly, with appropriate size/redaction checks.
49
+
50
+ ## Separate specification from evidence
51
+
52
+ Question definitions are your trusted decision specification. State can contain
53
+ untrusted logs, documents, or user content. The generative providers separate these
54
+ in their prompts, but this is not proof of prompt-injection resistance. Do not let
55
+ untrusted input choose unrestricted instructions, URLs, credentials, or actions.
56
+ Validation guarantees structure, not semantic correctness or authorization.
57
+
58
+ Use bounded questions and well-defined categories. Add an explicit `unknown` or
59
+ `needs_review` option when abstention is useful. Examples and domain definitions
60
+ belong in relevant instructions/context. Store a template revision alongside your
61
+ request/result externally; adding arbitrary top-level request fields is rejected.
62
+
63
+ ## Add context preparation
64
+
65
+ ```javascript
66
+ import { createBridge } from '@fayegram-ai/decision-bridge';
67
+
68
+ const bridge = createBridge({
69
+ jev: {},
70
+ extensions: [{
71
+ name: 'project-context',
72
+ prepareState(state) {
73
+ return { evidence: state, policy: 'Do not infer test success from missing logs.' };
74
+ },
75
+ }],
76
+ });
77
+ ```
78
+
79
+ Extensions run in registration order. They receive immutable snapshots and must
80
+ return replacements rather than mutate input. Async retrieval can use the supplied
81
+ context signal. No content is persisted unless your extension explicitly saves it.
82
+
83
+ ## Observe without changing decisions
84
+
85
+ ```javascript
86
+ import { createBridge } from '@fayegram-ai/decision-bridge';
87
+
88
+ const bridge = createBridge({
89
+ jev: {},
90
+ extensions: [{
91
+ name: 'metrics',
92
+ onResult(result) {
93
+ // This example logs metadata only, not state, keys, or answer content.
94
+ console.error(JSON.stringify({
95
+ event: 'decision_completed',
96
+ provider: result.meta.provider,
97
+ model: result.model,
98
+ durationMs: result.meta.durationMs,
99
+ }));
100
+ },
101
+ }],
102
+ });
103
+ ```
104
+
105
+ Observer failure is isolated and recorded in the returned metadata. It is not a
106
+ reason to retry inference. Observers cannot rewrite outputs. The CLI does not load
107
+ SDK extensions from ordinary JSON configuration; add them in the application or
108
+ an explicitly trusted provider module instead of executable auto-discovery.
109
+
110
+ ## Application policy is a separate stage
111
+
112
+ ```javascript
113
+ import { decideChoice } from '@fayegram-ai/decision-bridge';
114
+
115
+ const policy = decideChoice(result.answers.category, {
116
+ minProbability: 0.90,
117
+ minMargin: 0.20,
118
+ abstainOptions: ['unknown'],
119
+ });
120
+ ```
121
+
122
+ These are illustrative thresholds, not validated recommendations. `policy.status`
123
+ indicates `selected` or `review`. The original model answer remains unchanged.
124
+ Your code decides whether any downstream action is allowed. A high probability
125
+ cannot replace tests, permission checks, or human approval for consequential work.
126
+
127
+ When a provider declares `probabilityDecimalPlaces` through
128
+ `getResultValidationOptions(model)`, pass that precision to `decideChoice` for
129
+ rounded distributions. Cloudflare declares four-decimal precision; the helper
130
+ otherwise keeps the baseline 0.001 tolerance. See the
131
+ [Cloudflare policy example](CLOUDFLARE_DECISIONS.md#embedded-images-and-limits)
132
+ and [SDK validation contract](../reference/SDK.md#additional-exports).
133
+
134
+ ## Develop additions without changing the public call
135
+
136
+ Add retrieval/redaction through `prepareState` or `prepareInput`, metrics through
137
+ `onResult`, and another protocol through the explicit [custom decision](CUSTOM_DECISION.md)
138
+ or [custom generative](CUSTOM_GENERATIVE.md) contract. Keep routing/threshold policies
139
+ outside the provider and cover additions with relevant regression tests. Persisted templates,
140
+ caching, and routing can be built around the same `evaluate({ state, questions })`
141
+ contract rather than hardcoded into every backend.
@@ -0,0 +1,66 @@
1
+ # Custom decision providers
2
+
3
+ Use this path for a service that exposes a decision model. It is separate from
4
+ eliciting decisions from a general-purpose model. The root SDK already supports
5
+ Jev, OpenAI Decisions, and Cloudflare; a new native decision protocol uses
6
+ `@fayegram-ai/decision-bridge/custom/decision`.
7
+
8
+ ## SDK definition
9
+
10
+ ```javascript
11
+ import { createBridge, JevProvider } from '@fayegram-ai/decision-bridge';
12
+ import { defineDecisionProvider } from '@fayegram-ai/decision-bridge/custom/decision';
13
+
14
+ const backend = new JevProvider(); // TYPESAFE_API_KEY; native delegation example.
15
+ const provider = defineDecisionProvider({
16
+ name: 'my-decision-service',
17
+ defaultModel: backend.defaultModel,
18
+ probabilitySource: backend.probabilitySource,
19
+ evaluate: (request, options) => backend.evaluate(request, options),
20
+ getCapabilities: model => backend.getCapabilities(model),
21
+ });
22
+ const bridge = createBridge({ provider });
23
+ console.log(bridge.providerKind); // decision
24
+ ```
25
+
26
+ For a new service, replace the delegated evaluation with your encoding, transport,
27
+ and canonical response mapping. The helper validates and freezes the contract,
28
+ sets `kind: 'decision'`, and preserves callback receivers. It does not infer
29
+ capabilities or implement a protocol. Omitted capabilities use the legacy
30
+ shared-state contract.
31
+
32
+ Return `{ model, answers, usage, meta? }`. Preserve native values and reported
33
+ usage; use null for unknown token counts. Confidence is optional in the common
34
+ contract and must not be invented. Classification alone does not apply the stricter
35
+ Jev confidence/count validation mode to another service. Declare probability
36
+ provenance explicitly when known; the default is `unspecified`.
37
+
38
+ ## CLI module
39
+
40
+ ```bash
41
+ npm exec -- decision-bridge init ./my-service --provider custom-decision
42
+ ```
43
+
44
+ This writes a configuration, schema, request, environment placeholders, and a
45
+ `provider.mjs` edit point. The module exports `createProvider(options)` and must
46
+ return a decision-kind provider. Connect it to your service before evaluation.
47
+ A runnable Jev delegation example is at
48
+ [examples/custom/decision-provider.mjs](../../examples/custom/decision-provider.mjs).
49
+
50
+ ```json
51
+ {
52
+ "schemaVersion": 1,
53
+ "provider": "custom-decision",
54
+ "module": "./provider.mjs",
55
+ "model": "YOUR_DECISION_MODEL",
56
+ "apiKeyEnv": "CUSTOM_PROVIDER_API_KEY",
57
+ "timeoutMs": 30000,
58
+ "options": {}
59
+ }
60
+ ```
61
+
62
+ Module transport settings belong inside `options`. Offline `validate` checks
63
+ common structure without importing the module; it reports `validationScope:
64
+ 'structure'`. Executed module kinds must match the selected CLI path. See the
65
+ shared [custom contract](CUSTOM_PROVIDERS.md) for capability/preflight hooks,
66
+ module loading, cancellation, and tests.