@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.
- package/.env.example +10 -0
- package/CHANGELOG.md +75 -0
- package/CONTRIBUTING.md +57 -0
- package/LICENSE +21 -0
- package/README.md +307 -2
- package/SECURITY.md +88 -0
- package/bin/decision.mjs +15 -0
- package/docs/README.md +58 -0
- package/docs/development/ARCHITECTURE.md +118 -0
- package/docs/development/DESIGN.md +66 -0
- package/docs/development/README.md +12 -0
- package/docs/development/RELEASING.md +100 -0
- package/docs/development/REPOSITORY_LAYOUT.md +33 -0
- package/docs/development/TESTING.md +131 -0
- package/docs/guides/BROWSER.md +43 -0
- package/docs/guides/CLOUDFLARE_DECISIONS.md +374 -0
- package/docs/guides/CUSTOMIZATION.md +141 -0
- package/docs/guides/CUSTOM_DECISION.md +66 -0
- package/docs/guides/CUSTOM_GENERATIVE.md +164 -0
- package/docs/guides/CUSTOM_PROVIDERS.md +81 -0
- package/docs/guides/GETTING_STARTED.md +252 -0
- package/docs/guides/OPENAI_DECISIONS.md +210 -0
- package/docs/guides/PLATFORMS.md +107 -0
- package/docs/guides/SWITCHING_MODELS.md +202 -0
- package/docs/guides/TROUBLESHOOTING.md +82 -0
- package/docs/reference/CLI.md +202 -0
- package/docs/reference/CONFIGURATION.md +168 -0
- package/docs/reference/ERRORS.md +82 -0
- package/docs/reference/PROVIDERS.md +113 -0
- package/docs/reference/SDK.md +388 -0
- package/docs/reference/SOURCES.md +51 -0
- package/examples/basic.mjs +17 -0
- package/examples/batch.mjs +13 -0
- package/examples/cloudflare-decisions.mjs +22 -0
- package/examples/cloudflare-omni.mjs +32 -0
- package/examples/cloudflare-worker.mjs +21 -0
- package/examples/cloudflare.native.json +28 -0
- package/examples/configs/anthropic.json +9 -0
- package/examples/configs/cloudflare-decisions.json +8 -0
- package/examples/configs/custom-decision.json +9 -0
- package/examples/configs/custom-generative.json +14 -0
- package/examples/configs/jev.json +7 -0
- package/examples/configs/local.json +12 -0
- package/examples/configs/openai-decisions.json +7 -0
- package/examples/configs/openai.json +10 -0
- package/examples/configs/openrouter.json +10 -0
- package/examples/custom/decision-provider.mjs +15 -0
- package/examples/custom/generative-provider.mjs +13 -0
- package/examples/custom/openrouter.mjs +13 -0
- package/examples/customize.mjs +23 -0
- package/examples/decisions.native.json +19 -0
- package/examples/extended.mjs +43 -0
- package/examples/openai-decisions.mjs +20 -0
- package/examples/request.json +31 -0
- package/examples/requests.jsonl +2 -0
- package/package.json +87 -4
- package/schemas/decision.config.schema.json +553 -0
- package/src/async.mjs +63 -0
- package/src/batch.mjs +38 -0
- package/src/bridge.mjs +184 -0
- package/src/capabilities.mjs +95 -0
- package/src/cli/config-validation.mjs +89 -0
- package/src/cli/config.mjs +84 -0
- package/src/cli/decisions.mjs +19 -0
- package/src/cli/initialize.mjs +79 -0
- package/src/cli/io.mjs +171 -0
- package/src/cli/main.mjs +280 -0
- package/src/cli/ordered.mjs +43 -0
- package/src/cli/provider-loader.mjs +58 -0
- package/src/cloudflare-decisions.d.mts +78 -0
- package/src/custom/decision.d.mts +5 -0
- package/src/custom/decision.mjs +6 -0
- package/src/custom/definition.mjs +19 -0
- package/src/custom/generative/anthropic.mjs +75 -0
- package/src/custom/generative/index.d.mts +42 -0
- package/src/custom/generative/index.mjs +29 -0
- package/src/custom/generative/openai-compatible.mjs +101 -0
- package/src/custom/generative/schema.mjs +95 -0
- package/src/custom/generative/shared.mjs +39 -0
- package/src/decisions.d.mts +45 -0
- package/src/embedded.mjs +24 -0
- package/src/errors.mjs +49 -0
- package/src/index.d.mts +229 -0
- package/src/index.mjs +7 -0
- package/src/input.mjs +37 -0
- package/src/options.mjs +15 -0
- package/src/policy.mjs +44 -0
- package/src/probabilities.mjs +27 -0
- package/src/provider-contract.mjs +42 -0
- package/src/providers/cloudflare-audio.mjs +83 -0
- package/src/providers/cloudflare-codec.mjs +39 -0
- package/src/providers/cloudflare-decisions.mjs +81 -0
- package/src/providers/cloudflare-images.mjs +95 -0
- package/src/providers/cloudflare-media.mjs +36 -0
- package/src/providers/cloudflare-preflight.mjs +92 -0
- package/src/providers/cloudflare-profile.mjs +102 -0
- package/src/providers/cloudflare-transport.mjs +63 -0
- package/src/providers/cloudflare-validation.mjs +48 -0
- package/src/providers/cloudflare-video.mjs +84 -0
- package/src/providers/decisions-codec.mjs +61 -0
- package/src/providers/decisions-preflight.mjs +20 -0
- package/src/providers/decisions-validation.mjs +150 -0
- package/src/providers/index.mjs +26 -0
- package/src/providers/jev.mjs +51 -0
- package/src/providers/media-binary.mjs +51 -0
- package/src/providers/openai-decisions.mjs +51 -0
- package/src/questions.mjs +15 -0
- package/src/response-transport.mjs +176 -0
- package/src/runtime-env.mjs +4 -0
- package/src/transport.mjs +51 -0
- package/src/validation.mjs +245 -0
- 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 root README's [model list](../../README.md#supported-models) for the exact
|
|
8
|
+
short selectors and full aliases.
|
|
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.
|