@fayegram-ai/decision-bridge 0.1.0 → 0.1.2

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 (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +20 -14
  3. package/docs/README.md +5 -4
  4. package/docs/development/ARCHITECTURE.md +26 -3
  5. package/docs/development/DESIGN.md +11 -0
  6. package/docs/development/TESTING.md +23 -1
  7. package/docs/guides/BROWSER.md +3 -0
  8. package/docs/guides/CLOUDFLARE_DECISIONS.md +2 -2
  9. package/docs/guides/CUSTOM_PROVIDERS.md +4 -2
  10. package/docs/guides/GETTING_STARTED.md +1 -1
  11. package/docs/guides/JEV.md +174 -0
  12. package/docs/guides/OPENAI_DECISIONS.md +46 -4
  13. package/docs/guides/PLATFORMS.md +2 -2
  14. package/docs/guides/SWITCHING_MODELS.md +10 -4
  15. package/docs/guides/TROUBLESHOOTING.md +14 -0
  16. package/docs/reference/CLI.md +26 -4
  17. package/docs/reference/CONFIGURATION.md +12 -5
  18. package/docs/reference/ERRORS.md +5 -2
  19. package/docs/reference/PROVIDERS.md +20 -7
  20. package/docs/reference/SDK.md +22 -8
  21. package/docs/reference/SOURCES.md +4 -0
  22. package/examples/configs/openai-decisions.json +1 -1
  23. package/examples/decisions.native.json +1 -1
  24. package/package.json +1 -1
  25. package/schemas/decision.config.schema.json +15 -1
  26. package/src/capabilities.mjs +11 -8
  27. package/src/cli/config-validation.mjs +9 -1
  28. package/src/cli/config.mjs +17 -3
  29. package/src/cli/decisions.mjs +10 -0
  30. package/src/cli/initialize.mjs +8 -2
  31. package/src/cli/main.mjs +16 -4
  32. package/src/decisions.d.mts +5 -0
  33. package/src/index.d.mts +30 -8
  34. package/src/index.mjs +1 -1
  35. package/src/jev.d.mts +16 -0
  36. package/src/probabilities.mjs +6 -3
  37. package/src/providers/cloudflare-images.mjs +2 -58
  38. package/src/providers/decisions-preflight.mjs +18 -7
  39. package/src/providers/decisions-validation.mjs +3 -2
  40. package/src/providers/image-header.mjs +59 -0
  41. package/src/providers/jev-images.mjs +90 -0
  42. package/src/providers/jev-preflight.mjs +61 -0
  43. package/src/providers/jev-profile.mjs +37 -0
  44. package/src/providers/jev.mjs +32 -13
  45. package/src/providers/openai-decisions.mjs +17 -10
  46. package/src/providers/openai-profile.mjs +24 -0
  47. package/src/questions.mjs +2 -0
  48. package/src/validation.mjs +13 -6
  49. package/src/version.mjs +1 -1
@@ -20,8 +20,9 @@ it with `node bin/decision.mjs` from a source checkout. `decision` and
20
20
  The CLI defaults to Jev. Flags override `DECISION_*` environment settings, which
21
21
  override an explicit config file and then built-in defaults. It constructs an
22
22
  explicit provider for the SDK; `createBridge` itself has no implicit selection.
23
- Use the root README's [model list](../../README.md#supported-models) to select a
24
- supported native model. Custom generative presets require an explicit model ID.
23
+ Choose a supported native model from the
24
+ [model selector inventory](../../README.md#model-selectors).
25
+ Custom generative presets require an explicit model ID.
25
26
 
26
27
  ## Commands
27
28
 
@@ -32,6 +33,12 @@ for native request/response structures on `init`, `evaluate`, `validate`, or
32
33
  or `-`. See [OpenAI Decisions](../guides/OPENAI_DECISIONS.md) and
33
34
  [Cloudflare Clef](../guides/CLOUDFLARE_DECISIONS.md) for examples. Both providers report
34
35
  model listing as unsupported.
36
+ OpenAI model selection defaults to `default`; use `--model luna` for the Luna
37
+ family or an explicit supported upstream ID. Both stable selectors currently
38
+ resolve to `gpt-6-luna` within Decision Bridge. OpenAI `validate` reports the
39
+ selected value in `model` and the outgoing ID in `resolvedModel`, without credentials.
40
+ Successful common and native calls include `meta.requestedModel` and
41
+ `meta.resolvedModel`; response models retain the service-reported identity.
35
42
  Cloudflare CLI execution uses REST. The Workers AI binding is an SDK transport;
36
43
  there is no CLI binding selector. Gateway settings route the selected REST request.
37
44
 
@@ -44,6 +51,12 @@ there is no CLI binding selector. Gateway settings route the selected REST reque
44
51
  | `models` | List the configured provider's models; may be paginated |
45
52
  | `batch FILE\|-` | Stream JSONL inputs and ordered JSONL result/error records |
46
53
 
54
+ Jev supports both common and `--native` requests for evaluation, validation,
55
+ initialization, and ordered batches. Experimental images/boxes require
56
+ `--jev-images`, and longer context requires `--jev-long-context` (or matching
57
+ JSON configuration). Use `--no-jev-images` and `--no-jev-long-context` to
58
+ override enabled settings. See the [Jev guide](../guides/JEV.md).
59
+
47
60
  No input filename on `evaluate`, `validate`, or `schema` selects the bundled sample,
48
61
  not stdin. Use `-` explicitly for stdin. Empty invocation prints help. Configuration
49
62
  and credential errors do not cause fallback to another backend.
@@ -71,6 +84,10 @@ explicit service implementation error until you connect it. An existing
71
84
  `--provider-module` is referenced without execution or overwrite. All profiles
72
85
  include a copied JSON Schema for editor validation. See [custom providers](../guides/CUSTOM_PROVIDERS.md).
73
86
 
87
+ OpenAI initialization writes the selected model into configuration, using `default`
88
+ when omitted. Its generated common and native requests omit `model`, allowing
89
+ later configuration changes to control selection.
90
+
74
91
  ## Flags
75
92
 
76
93
  | Flag | Meaning |
@@ -80,7 +97,9 @@ include a copied JSON Schema for editor validation. See [custom providers](../gu
80
97
  | `--provider NAME` | `jev`, `openai-decisions`, `cloudflare-decisions`, `custom-decision`, `custom-generative` |
81
98
  | `--preset NAME` | custom-generative preset: `openai-compatible`, `openrouter`, or `anthropic`; exclusive with module |
82
99
  | `--native` | Explicit native Decisions input/output; init/evaluate/validate/batch only |
83
- | `--model ID` | Overrides input JSON model and provider default |
100
+ | `--jev-images`, `--no-jev-images` | Enable/disable experimental Jev images and boxes |
101
+ | `--jev-long-context`, `--no-jev-long-context` | Enable/disable experimental Jev 262k context |
102
+ | `--model SELECTOR` | Model selector or supported upstream ID; overrides input JSON model and configured default |
84
103
  | `--base-url URL` | Custom endpoint; requires explicit credential selection |
85
104
  | `--api-key-env NAME` | Environment variable name, never the key itself |
86
105
  | `--account-id ID` | Cloudflare REST account ID |
@@ -161,7 +180,10 @@ delivery needs separate validation.
161
180
 
162
181
  ## I/O and resource limits
163
182
 
164
- Single JSON input: **8 MiB**. JSONL: **8 MiB per line, 64 MiB total**. Config:
183
+ Single JSON input normally uses **8 MiB**. JSONL normally uses **8 MiB per line,
184
+ 64 MiB total**. Jev permits **32 MiB** for a single JSON input or JSONL record,
185
+ then checks serialized requests against its configured ceiling (32 MiB by default).
186
+ Config:
165
187
  **1 MiB**. Environment file: **64 KiB**. Provider request and response default to
166
188
  8 MiB each, configurable to at most 64 MiB. Cloudflare reads single JSON inputs
167
189
  and individual JSONL records within 64 MiB, then applies model-specific serialized
@@ -40,16 +40,17 @@ profile. Gateway configuration is optional for either SDK transport and for CLI
40
40
  | `schemaVersion` | 1; optional, but generated profiles include it |
41
41
  | `provider` | `jev` default, `openai-decisions`, `cloudflare-decisions`, `custom-decision`, `custom-generative` |
42
42
  | `preset` | `custom-generative` only: `openai-compatible`, `openrouter`, `anthropic`; mutually exclusive with module |
43
- | `model` | Nonempty model ID; built-in native providers have defaults; required for generative evaluation unless the request supplies it |
43
+ | `model` | Nonempty model selector or supported upstream ID; built-in native providers have defaults; required for generative evaluation unless the request supplies it |
44
44
  | `baseURL` | HTTPS API base; loopback may use HTTP |
45
45
  | `apiKeyEnv` | Credential environment variable name; inline `apiKey` is rejected |
46
46
  | `noAuth` | Boolean; true only for loopback OpenAI-compatible endpoints |
47
47
  | `timeoutMs` | Positive integer up to 2,147,483,647 milliseconds |
48
48
  | `maxRetries` | Integer 0–10; OpenAI/Cloudflare default 0; Jev and generative presets default 2 |
49
- | `maxRequestBytes`, `maxResponseBytes` | Positive integer through 64 MiB; default 8 MiB. Cloudflare requests: Clef/Clef-flash default/cap 13 MiB; Omni default 36 MiB, configurable through 64 MiB |
49
+ | `maxRequestBytes`, `maxResponseBytes` | Positive integer through 64 MiB; default 8 MiB. Jev requests default/cap 32 MiB. Cloudflare requests: Clef/Clef-flash default/cap 13 MiB; Omni default 36 MiB, configurable through 64 MiB |
50
50
  | `accountId` | Cloudflare REST account ID; otherwise `CLOUDFLARE_ACCOUNT_ID` |
51
51
  | `rejectIfBusy` | Cloudflare boolean; default false, incompatible with retries. Effective true fails REST preflight with `UNSUPPORTED_CAPABILITY`; see the [binding support and REST limitation](../guides/CLOUDFLARE_DECISIONS.md#capacity-rejection-and-ai-gateway) |
52
52
  | `gateway` | Cloudflare Gateway object; see the [Cloudflare guide](../guides/CLOUDFLARE_DECISIONS.md) |
53
+ | `dangerZone` | Jev-only `{ images?: boolean, longContext?: boolean }`; both default false; see the [Jev guide](../guides/JEV.md) |
53
54
  | `maxTokens` | Generative output budget, 1–1,048,576; default 4096 |
54
55
  | `responseFormat` | Compatible generative presets: `json_schema`, `json_object`, `text` |
55
56
  | `tokenParameter` | Compatible generative presets: `max_completion_tokens` or `max_tokens` |
@@ -58,6 +59,10 @@ profile. Gateway configuration is optional for either SDK transport and for CLI
58
59
  | `options` | Custom factory's JSON options, e.g. its own baseURL |
59
60
 
60
61
  Upper numeric limits validate client configuration, not vendor model capabilities.
62
+ Jev requests default to and are capped at 32 MiB; a smaller `maxRequestBytes`
63
+ lowers that ceiling. `DECISION_JEV_IMAGES` and `DECISION_JEV_LONG_CONTEXT`
64
+ accept `true` or `false`; their CLI overrides are `--jev-images`,
65
+ `--no-jev-images`, `--jev-long-context`, and `--no-jev-long-context`.
61
66
  Cloudflare selects its byte budget from the effective request model, including
62
67
  per-call overrides. An explicit smaller `maxRequestBytes` remains the ceiling;
63
68
  selecting Omni cannot enlarge it. Omni's default is a bridge policy, not a
@@ -88,8 +93,8 @@ existing process environment > --env-file values
88
93
  The loader does not mutate the process environment. The SDK independently reads
89
94
  its documented ambient credential variables at provider construction.
90
95
 
91
- The [supported-model list](../../README.md#supported-models) records native
92
- selectors, aliases and defaults. Configuration precedence does not expand a
96
+ The [model selector list](../../README.md#model-selectors) records native
97
+ selectors and defaults. Configuration precedence does not expand a
93
98
  provider's accepted models or grant account access.
94
99
 
95
100
  For evaluation model selection:
@@ -99,7 +104,9 @@ For evaluation model selection:
99
104
  ```
100
105
 
101
106
  Jev defaults to `jev-latest`, with `JEV_MODEL` as a legacy/direct convenience.
102
- Native OpenAI Decisions defaults to `gpt-6-luna`; it accepts transport settings
107
+ Native OpenAI Decisions defaults to `default` and accepts `luna` or explicit
108
+ `gpt-6-luna`; Decision Bridge currently resolves each to upstream `gpt-6-luna`.
109
+ OpenAI accepts transport settings
103
110
  and rejects generative output settings. Its default retry count is zero. Cloudflare Clef defaults to `clef`, uses
104
111
  account/token credentials, rejects generation settings, and also defaults to zero retries.
105
112
  A `DECISION_MODEL` or file model takes precedence over that convenience.
@@ -77,6 +77,9 @@ validation failures can still return HTTP 422/5012 and retain safe diagnostics.
77
77
  Keep the default disabled setting for ordinary REST
78
78
  inference; see the [Cloudflare guide](../guides/CLOUDFLARE_DECISIONS.md#capacity-rejection-and-ai-gateway).
79
79
 
80
- Keep a request ID and available Gateway log ID, bridge version, configured provider, requested/reported model,
81
- and template revision for incident analysis. Log input or results only under your
80
+ Keep a request ID and available Gateway log ID, bridge version, configured provider,
81
+ model selection, reported model, and template revision for incident analysis.
82
+ For successful OpenAI calls, retain
83
+ the requested selection, resolved outgoing ID, and reported model separately;
84
+ see [model identities](../guides/OPENAI_DECISIONS.md#model-selection). Log input or results only under your
82
85
  own data-handling policy. No logging or telemetry is enabled by default.
@@ -2,10 +2,10 @@
2
2
 
3
3
  ## Native routes
4
4
 
5
- | Provider type | Wire API | Credential | Default model | Output strategy |
5
+ | Provider type | Wire API | Credential | Default selector | Output strategy |
6
6
  | --- | --- | --- | --- | --- |
7
7
  | `jev` | `POST /v1/systemone` at api.typesafe.ai | `TYPESAFE_API_KEY` | `jev-latest` | Native typed decisions |
8
- | `openai-decisions` | `POST /v1/decisions` at api.openai.com | `OPENAI_API_KEY` | `gpt-6-luna` | Native Decisions with common and dedicated access |
8
+ | `openai-decisions` | `POST /v1/decisions` at api.openai.com | `OPENAI_API_KEY` | `default` | Native Decisions with common and dedicated access |
9
9
  | `cloudflare-decisions` | Workers AI REST model route or `AI.run` binding | `CLOUDFLARE_API_TOKEN` and account ID for REST | `clef` | Native Clef decisions with common and dedicated access |
10
10
 
11
11
  `createProvider()` and the root SDK export these three native providers only.
@@ -15,9 +15,12 @@ maintained, but are not native decision models. See [custom providers](../guides
15
15
 
16
16
  ## Supported model selection
17
17
 
18
- See the root README's [model list](../../README.md#supported-models) for the
19
- built-in selectors and aliases. Jev accepts `jev-latest` and explicit Jev IDs from
20
- its account catalog. OpenAI Decisions supports only `gpt-6-luna` in this release.
18
+ The [model selector inventory](../../README.md#model-selectors) summarizes the
19
+ built-in providers. Jev accepts `jev-latest` and explicit Jev IDs from
20
+ its account catalog. OpenAI Decisions accepts `default`, `luna`, and explicit
21
+ `gpt-6-luna`. Decision Bridge resolves these to the upstream `gpt-6-luna` model
22
+ in this release. The selected value remains in `meta.requestedModel`, the outgoing
23
+ ID in `meta.resolvedModel`, and the service-reported identity in the response's `model`.
21
24
  Cloudflare supports `clef`, `clef-flash`, and `clef-omni`, including each full
22
25
  `@cf/cloudflare/<model>` alias. Unsupported OpenAI/Cloudflare selectors fail
23
26
  local preflight with `UNSUPPORTED_CAPABILITY`.
@@ -41,7 +44,7 @@ contract. The following additional forms are checked before dispatch:
41
44
 
42
45
  | Provider | Ordered text / embedded images | Typed string/boolean choices | Labelled score levels | Safety identifier | Model listing |
43
46
  | --- | --- | --- | --- | --- | --- |
44
- | Jev | Unsupported | Unsupported | Unsupported | Unsupported | Supported |
47
+ | Jev | Ordered text; images with explicit opt-in | Unsupported | Unsupported | Unsupported | Supported |
45
48
  | OpenAI Decisions | Supported; up to 128 images, preserving order | Supported | Supported | Supported | Unsupported |
46
49
  | Cloudflare Clef / Clef-flash | Supported; up to four images before text | Supported | Supported; 2–10 levels | Unsupported | Unsupported |
47
50
  | Cloudflare Clef-omni | Supported; up to four images before audio/video/text | Supported | Supported; 2–10 levels | Unsupported | Unsupported |
@@ -59,7 +62,11 @@ capability fields `audio`, `videos`, `maxAudioClips`, and `maxVideos` normalize
59
62
  false/zero when omitted, preserving legacy custom profiles. Cloudflare's older
60
63
  models and all other shipped providers reject audio/video before inference.
61
64
 
62
- Dedicated `createDecision()` access is available on OpenAI Decisions and
65
+ Jev additionally supports `box` questions on `jev-preview` and pinned 1.14+ models
66
+ when `dangerZone.images` is enabled. Other built-ins reject box questions.
67
+ See the [Jev guide](../guides/JEV.md) for asset placement and experimental opt-ins.
68
+
69
+ Dedicated `createDecision()` access is available on Jev, OpenAI Decisions and
63
70
  Cloudflare providers. It uses each service's native request/response shape and
64
71
  bypasses bridge preparation and observation hooks.
65
72
 
@@ -71,6 +78,12 @@ intermediate language model, skill, portal prompt, or chat conversion is used.
71
78
  `GET /v1/models` lists native models. A listed catalog can differ from valid aliases;
72
79
  `jev-latest` is the documented stable alias.
73
80
 
81
+ `jev-preview` selects the preview release. Images and longer context require
82
+ explicit `dangerZone` settings. Common image input preserves ordering; native
83
+ `createDecision()` additionally accepts JSON assets referenced in state,
84
+ instructions, and criteria. Box outputs preserve normalized edges and confidence.
85
+ See the [Jev guide](../guides/JEV.md) for limits and absence handling.
86
+
74
87
  Pin a version after evaluating it for your workload rather than assuming a moving
75
88
  alias yields reproducible quality. Store the returned `model` along with the
76
89
  requested model and your question-template revision.
@@ -48,7 +48,7 @@ guides. These are SDK options, not per-evaluation bridge options.
48
48
  | --- | --- |
49
49
  | `timeoutMs` | 30,000 ms; positive integer through 2,147,483,647; one transport budget for dispatch, retries, backoff, and response reading |
50
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) |
51
+ | `maxRequestBytes` | Complete serialized request ceiling; normally 8 MiB, configurable from 1 byte through 64 MiB; Jev defaults to and caps at 32 MiB; Cloudflare applies its [model-specific budgets](../guides/CLOUDFLARE_DECISIONS.md#embedded-images-and-limits) |
52
52
  | `maxResponseBytes` | 8 MiB; configurable from 1 byte through 64 MiB; applies while reading the response body |
53
53
  | `fetchImpl` | Optional `(url: string, init?: RequestInit) => Promise<Response>`; defaults to the runtime's `fetch` |
54
54
 
@@ -72,7 +72,10 @@ they interact; [errors](ERRORS.md#retry-policy) lists retryable statuses.
72
72
  `model` and `safetyIdentifier`. New forms require declared provider capabilities;
73
73
  see [OpenAI Decisions](../guides/OPENAI_DECISIONS.md) and
74
74
  [Cloudflare Clef](../guides/CLOUDFLARE_DECISIONS.md). The request model
75
- wins over the provider's default. No default is assumed for a generative provider.
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.
76
79
  `options` accepts `signal: AbortSignal` and `timeoutMs`, overriding the bridge's
77
80
  60-second evaluation budget for this call. It does not mutate the provider's
78
81
  independent transport timeout, including Workers AI binding calls.
@@ -92,10 +95,12 @@ when client retries are configured.
92
95
  ### `bridge.getCapabilities(model?)` and `bridge.validate(request)`
93
96
 
94
97
  Capability lookup is synchronous and reports implemented features and limits.
95
- `validate` resolves the model, snapshots/freezes the request, and checks capabilities
98
+ `validate` applies the default model selection, snapshots/freezes the request, and checks capabilities
96
99
  and preparation-extension compatibility without inference. Unsupported features
97
100
  fail with `UNSUPPORTED_CAPABILITY` before dispatch. Native Decisions has no model
98
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.
99
104
 
100
105
  Providers can implement optional `validateRequest(resolvedRequest)` for additional
101
106
  static checks. This synchronous hook receives a frozen request, must return no
@@ -141,6 +146,10 @@ const q3 = score('How far did processing progress?', [
141
146
  ]);
142
147
  ```
143
148
 
149
+ `box(instructions)` returns a box question with no criteria; supported providers
150
+ must declare `boxes: true`. Jev supports it with experimental image opt-in and
151
+ a supported model; see [Jev images and boxes](../guides/JEV.md).
152
+
144
153
  Builders return immutable question objects. `choice(instructions, criteria)` accepts
145
154
  a label map containing 1 to 255 options. `noul(instructions, criteria?)` accepts an
146
155
  optional criteria object,
@@ -196,7 +205,8 @@ rubric exactly. Generative providers calculate score and legend locally. Native
196
205
  values are validated and preserved, not normalized or replaced.
197
206
 
198
207
  **Usage:** `input_tokens` and `output_tokens` are nonnegative integers or null.
199
- Jev, OpenAI Decisions, and Cloudflare require reported integer counts. OpenAI and
208
+ Jev accepts explicitly null counts as unknown; OpenAI Decisions and Cloudflare
209
+ require reported integer counts. OpenAI and
200
210
  Anthropic cache creation/read counts are preserved separately when provided;
201
211
  Cloudflare does not fabricate cache or total-token fields. No total price, inferred cache saving, or
202
212
  estimated token count is fabricated.
@@ -207,7 +217,8 @@ estimated token count is fabricated.
207
217
  | --- | --- |
208
218
  | `provider` | Bridge backend name; native Jev reports `typesafe` |
209
219
  | `providerKind` | `decision` or `generative`; authoritative provider classification, never response-selected |
210
- | `requestedModel` | Resolved model requested by the caller |
220
+ | `requestedModel` | Selected model after applying the provider default; OpenAI retains `default`, `luna`, or the explicit ID |
221
+ | `resolvedModel` | Upstream model ID sent by OpenAI Decisions; optional for other providers |
211
222
  | `probabilitySource` | `native`, `elicited`, or custom/`unspecified` provenance |
212
223
  | `confidenceSource` | `native` or `unavailable` for built-in providers |
213
224
  | `modelSource` | `reported` or `requested` fallback for built-in providers |
@@ -225,7 +236,8 @@ unavailable metadata exists. Result objects and nested data returned by the brid
225
236
  are immutable. Standalone validation functions return snapshots, not frozen values.
226
237
 
227
238
  `schemaVersion` remains 1 for legacy requests and 2 for ordered images/text or
228
- richer question forms. Audio/video requests use 3. This result version is separate
239
+ richer question forms. Audio/video requests use 3; requests with boxes use 4.
240
+ This result version is separate
229
241
  from the CLI configuration schema, which remains 1.
230
242
 
231
243
  ## Extensions
@@ -298,6 +310,8 @@ automatic model fallback. The CLI has a separate streaming JSONL implementation.
298
310
  - `createProvider(config)`: factory requiring an explicit `type` equal to `jev`,
299
311
  `openai-decisions`, or `cloudflare-decisions`.
300
312
  - `JevProvider`, `OpenAIDecisionsProvider`, `CloudflareDecisionsProvider`: native provider classes.
313
+ - `box(instructions)` and `JevProvider.createDecision(request, options?)`:
314
+ bounding boxes and native JSON assets; see the [Jev guide](../guides/JEV.md).
301
315
  - `scoreLevels(instructions, levels)` and the array overload of `choice`:
302
316
  labelled rubrics and typed string/boolean options, with provider capability checks.
303
317
  - `OpenAIDecisionsProvider.createDecision(request, options?)`: dedicated native
@@ -308,9 +322,9 @@ automatic model fallback. The CLI has a separate streaming JSONL implementation.
308
322
  an own `input.model` property or an explicitly supplied default; inherited
309
323
  models are ignored, and it never selects a Jev model.
310
324
  Question builders and schema generation validate without selecting an inference model.
311
- - `validateResult(input, questions, { native = false, probabilityDecimalPlaces })`: snapshot supplied
325
+ - `validateResult(input, questions, { native = false, nullableUsage = false, probabilityDecimalPlaces })`: snapshot supplied
312
326
  questions and validate the canonical result; native mode requires confidence
313
- and reported counts.
327
+ and reported counts unless the provider explicitly enables nullable usage.
314
328
  - `decideChoice(answer, { minProbability, minMargin = 0, abstainOptions = [], probabilityDecimalPlaces })`:
315
329
  returns a policy result, never executes an action. `minProbability` is required.
316
330
  - `DecisionError`, `publicError(error)`: public structured failure handling.
@@ -11,6 +11,10 @@ reference does not imply that authenticated inference was run against its servic
11
11
  | TypeSafe request/response API | https://docs.typesafe.ai/api |
12
12
  | TypeSafe JavaScript examples | https://docs.typesafe.ai/sdk/javascript |
13
13
  | Native model listing and aliases | https://docs.typesafe.ai/models |
14
+ | Jev image assets and experimental opt-in | https://docs.typesafe.ai/images |
15
+ | Jev bounding boxes | https://docs.typesafe.ai/primitives/box |
16
+ | Jev 1.14 release | https://typesafe.ai/blog/jev-1-14 |
17
+ | Jev nullable usage compatibility | https://docs.typesafe.ai/sdk/javascript/changelog |
14
18
  | Choice decisions | https://docs.typesafe.ai/primitives/choice |
15
19
  | Noul probabilities | https://docs.typesafe.ai/primitives/noul |
16
20
  | Score rubric and weighted expectation | https://docs.typesafe.ai/primitives/score |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "provider": "openai-decisions",
4
- "model": "gpt-6-luna",
4
+ "model": "default",
5
5
  "apiKeyEnv": "OPENAI_API_KEY",
6
6
  "maxRetries": 0
7
7
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "model": "gpt-6-luna",
2
+ "model": "default",
3
3
  "input": "Export fails in Safari but works in Chrome.",
4
4
  "questions": [
5
5
  { "type": "predicate", "name": "browser_specific", "instructions": "Is the failure browser-specific?" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayegram-ai/decision-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "JavaScript SDK and CLI for structured decisions across native decision APIs and configurable model providers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,13 +29,27 @@
29
29
  "$ref": "#/$defs/retries"
30
30
  },
31
31
  "maxRequestBytes": {
32
- "$ref": "#/$defs/bytes"
32
+ "type": "integer",
33
+ "minimum": 1,
34
+ "maximum": 33554432
33
35
  },
34
36
  "maxResponseBytes": {
35
37
  "$ref": "#/$defs/bytes"
36
38
  },
37
39
  "provider": {
38
40
  "const": "jev"
41
+ },
42
+ "dangerZone": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "images": {
47
+ "type": "boolean"
48
+ },
49
+ "longContext": {
50
+ "type": "boolean"
51
+ }
52
+ }
39
53
  }
40
54
  }
41
55
  },
@@ -1,6 +1,8 @@
1
1
  import { DecisionError, requireCondition as check } from './errors.mjs';
2
2
  import { cloneJson, freezeJson, isRecord } from './validation.mjs';
3
3
  import { CLOUDFLARE_MODELS } from './providers/cloudflare-profile.mjs';
4
+ import { openaiModelProfile } from './providers/openai-profile.mjs';
5
+ import { jevFeatures } from './providers/jev-profile.mjs';
4
6
 
5
7
  /** Conservative defaults preserve the pre-existing custom-provider contract. */
6
8
  export const LEGACY_CAPABILITIES = freezeJson({
@@ -8,11 +10,7 @@ export const LEGACY_CAPABILITIES = freezeJson({
8
10
  labeledScores: false, safetyIdentifier: false,
9
11
  maxImages: 0, maxChoiceOptions: 255, maxScoreLevels: 10,
10
12
  audio: false, videos: false, maxAudioClips: 0, maxVideos: 0,
11
- });
12
- const OPENAI_CAPABILITIES = freezeJson({
13
- supported: true, orderedInput: true, images: true, typedChoices: true,
14
- labeledScores: true, safetyIdentifier: true,
15
- maxImages: 128, maxChoiceOptions: 255, maxScoreLevels: null,
13
+ boxes: false,
16
14
  });
17
15
 
18
16
  const CLOUDFLARE_CAPABILITIES = freezeJson({
@@ -20,7 +18,9 @@ const CLOUDFLARE_CAPABILITIES = freezeJson({
20
18
  labeledScores: true, safetyIdentifier: false, maxImages: 4, maxChoiceOptions: 255, maxScoreLevels: 10,
21
19
  });
22
20
 
23
- export function builtInCapabilities(type, model) {
21
+ export function builtInCapabilities(type, model, options = {}) {
22
+ if (type === 'jev') return freezeJson({ ...LEGACY_CAPABILITIES,
23
+ ...jevFeatures(model, Object.hasOwn(options, 'dangerZone') ? options.dangerZone : undefined) });
24
24
  if (type === 'cloudflare-decisions') {
25
25
  const key = typeof model === 'string' ? model.trim().replace(/^@cf\/cloudflare\//, '') : '';
26
26
  const profile = Object.hasOwn(CLOUDFLARE_MODELS, key) ? CLOUDFLARE_MODELS[key] : undefined;
@@ -29,7 +29,8 @@ export function builtInCapabilities(type, model) {
29
29
  maxAudioClips: profile?.omni ? 4 : 0, maxVideos: profile?.omni ? 2 : 0 });
30
30
  }
31
31
  if (type !== 'openai-decisions') return LEGACY_CAPABILITIES;
32
- return freezeJson({ ...LEGACY_CAPABILITIES, ...OPENAI_CAPABILITIES, supported: model === 'gpt-6-luna' });
32
+ const profile = openaiModelProfile(model);
33
+ return freezeJson({ ...LEGACY_CAPABILITIES, ...profile, supported: !!profile });
33
34
  }
34
35
 
35
36
  export function providerCapabilities(provider, model) {
@@ -48,7 +49,7 @@ export function providerCapabilities(provider, model) {
48
49
  check(isRecord(profile) && Object.keys(profile).every(key => Object.hasOwn(LEGACY_CAPABILITIES, key)),
49
50
  'Invalid capability profile.', 'CONFIGURATION_ERROR');
50
51
  const result = { ...LEGACY_CAPABILITIES, ...profile };
51
- for (const key of ['supported', 'orderedInput', 'images', 'typedChoices', 'labeledScores', 'safetyIdentifier', 'audio', 'videos']) {
52
+ for (const key of ['supported', 'orderedInput', 'images', 'typedChoices', 'labeledScores', 'safetyIdentifier', 'audio', 'videos', 'boxes']) {
52
53
  check(typeof result[key] === 'boolean', 'Capability flags must be boolean.', 'CONFIGURATION_ERROR');
53
54
  }
54
55
  for (const key of ['maxImages', 'maxChoiceOptions', 'maxScoreLevels', 'maxAudioClips', 'maxVideos']) {
@@ -78,6 +79,7 @@ export function assertCapabilities(request, capabilities) {
78
79
  }
79
80
  if (Object.hasOwn(request, 'safetyIdentifier')) requireFeature(capabilities.safetyIdentifier, 'safetyIdentifier');
80
81
  for (const q of Object.values(request.questions)) {
82
+ if (q.type === 'box') requireFeature(capabilities.boxes, 'box questions');
81
83
  if (q.options) requireFeature(capabilities.typedChoices, 'typed choices');
82
84
  if (q.levels) requireFeature(capabilities.labeledScores, 'labelled scores');
83
85
  const choices = q.options?.length ?? (q.type === 'choice' ? Object.keys(q.criteria).length : 0);
@@ -89,6 +91,7 @@ export function assertCapabilities(request, capabilities) {
89
91
  }
90
92
 
91
93
  export function commonSchemaVersion(request) {
94
+ if (Object.values(request.questions).some(q => q.type === 'box')) return 4;
92
95
  if (request.input?.some(part => ['audio', 'video'].includes(part.type))) return 3;
93
96
  return Object.hasOwn(request, 'input') || Object.hasOwn(request, 'safetyIdentifier') ||
94
97
  Object.values(request.questions).some(q => q.options || q.levels) ? 2 : 1;
@@ -2,13 +2,14 @@ import { requireCondition as check } from '../errors.mjs';
2
2
  import { cloneJson, isRecord } from '../validation.mjs';
3
3
  import { cloudflareGateway, validateCloudflareLimits } from '../providers/cloudflare-profile.mjs';
4
4
  import { generationOptions } from '../custom/generative/shared.mjs';
5
+ import { jevDangerZone, jevRequestLimit } from '../providers/jev-profile.mjs';
5
6
 
6
7
  export const PROVIDERS = ['jev', 'openai-decisions', 'cloudflare-decisions', 'custom-decision', 'custom-generative'];
7
8
  export const PRESETS = ['openai-compatible', 'openrouter', 'anthropic'];
8
9
  export const TRANSPORT = ['baseURL', 'maxRetries', 'maxRequestBytes', 'maxResponseBytes'];
9
10
  export const GENERATION = ['maxTokens', 'responseFormat', 'tokenParameter', 'parameters', 'noAuth'];
10
11
  export const PROVIDER_FIELDS = [...TRANSPORT, ...GENERATION, 'apiKeyEnv', 'model', 'preset', 'module',
11
- 'options', 'accountId', 'rejectIfBusy', 'gateway'];
12
+ 'options', 'accountId', 'rejectIfBusy', 'gateway', 'dangerZone'];
12
13
  const FIELDS = new Set(['$schema', 'schemaVersion', 'provider', 'timeoutMs', ...PROVIDER_FIELDS]);
13
14
  export const NUMERIC = new Set(['timeoutMs', 'maxRetries', 'maxRequestBytes', 'maxResponseBytes', 'maxTokens']);
14
15
  export const KEY_ENVS = { jev: 'TYPESAFE_API_KEY', 'openai-decisions': 'OPENAI_API_KEY',
@@ -56,7 +57,14 @@ export function validateConfig(input, { complete = false } = {}) {
56
57
  check(config.module === undefined || ['custom-decision', 'custom-generative'].includes(config.provider),
57
58
  'module requires custom-decision or custom-generative.', 'CONFIGURATION_ERROR');
58
59
  check(config.options === undefined || isModule(config), 'options requires a custom provider module.', 'CONFIGURATION_ERROR');
60
+ // Validate nested shape even when a partial file omits its provider selector.
61
+ if (Object.hasOwn(config, 'dangerZone')) config.dangerZone = jevDangerZone(config.dangerZone);
59
62
  if (config.provider !== undefined) {
63
+ if (config.provider !== 'jev') check(!Object.hasOwn(config, 'dangerZone'),
64
+ 'dangerZone requires Jev.', 'CONFIGURATION_ERROR');
65
+ else {
66
+ jevRequestLimit(config.maxRequestBytes);
67
+ }
60
68
  if (config.provider !== 'cloudflare-decisions') check(['accountId', 'rejectIfBusy', 'gateway'].every(key => config[key] === undefined),
61
69
  'Cloudflare settings require cloudflare-decisions.', 'CONFIGURATION_ERROR');
62
70
  else {
@@ -1,8 +1,10 @@
1
1
  import { readFile, stat } from 'node:fs/promises';
2
2
  import { resolve, dirname } from 'node:path';
3
3
  import { parseEnv } from 'node:util';
4
+ import { OPENAI_DEFAULT_MODEL } from '../providers/openai-profile.mjs';
4
5
  import { DecisionError, requireCondition as check } from '../errors.mjs';
5
6
  import { validateConfig, NUMERIC, PROVIDER_FIELDS } from './config-validation.mjs';
7
+ import { jevDangerZone } from '../providers/jev-profile.mjs';
6
8
  export { validateConfig } from './config-validation.mjs';
7
9
  export { configuredProvider } from './provider-loader.mjs';
8
10
 
@@ -11,6 +13,7 @@ const ENV_FIELDS = {
11
13
  DECISION_API_KEY_ENV: 'apiKeyEnv', DECISION_TIMEOUT_MS: 'timeoutMs',
12
14
  DECISION_MAX_RETRIES: 'maxRetries', DECISION_RESPONSE_FORMAT: 'responseFormat',
13
15
  DECISION_ACCOUNT_ID: 'accountId', DECISION_REJECT_IF_BUSY: 'rejectIfBusy', DECISION_GATEWAY_ID: 'gateway',
16
+ DECISION_JEV_IMAGES: 'jevImages', DECISION_JEV_LONG_CONTEXT: 'jevLongContext',
14
17
  };
15
18
 
16
19
  async function boundedFile(path, maxBytes, label) {
@@ -46,13 +49,17 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
46
49
  fileConfig = validateConfig(fileConfig);
47
50
  moduleBase = dirname(path);
48
51
  }
49
- const environment = {};
52
+ const environment = Object.create(null);
50
53
  for (const [key, field] of Object.entries(ENV_FIELDS)) {
51
54
  if (effectiveEnv[key] === undefined) continue;
52
55
  const value = effectiveEnv[key];
53
56
  if (NUMERIC.has(field)) check(typeof value === 'string' && /^\d+$/.test(value),
54
57
  'Numeric DECISION_* variables must contain decimal integers.', 'CONFIGURATION_ERROR');
55
- if (field === 'rejectIfBusy') {
58
+ if (field === 'jevImages' || field === 'jevLongContext') {
59
+ check(value === 'true' || value === 'false', 'Jev DECISION_* flags must be true or false.', 'CONFIGURATION_ERROR');
60
+ environment.dangerZone = { ...(Object.hasOwn(environment, 'dangerZone') ? environment.dangerZone : {}),
61
+ [field === 'jevImages' ? 'images' : 'longContext']: value === 'true' };
62
+ } else if (field === 'rejectIfBusy') {
56
63
  check(value === 'true' || value === 'false', 'DECISION_REJECT_IF_BUSY must be true or false.', 'CONFIGURATION_ERROR');
57
64
  environment[field] = value === 'true';
58
65
  } else environment[field] = field === 'gateway' ? { id: value } : NUMERIC.has(field) ? Number(value) : value;
@@ -61,6 +68,10 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
61
68
  // environment-selected provider must not inherit that provider's key/endpoint.
62
69
  function mergeProviderLayer(base, layer, layerModuleBase = moduleBase) {
63
70
  const next = { ...base };
71
+ const dangerZone = Object.hasOwn(layer, 'dangerZone') ? layer.dangerZone : undefined;
72
+ // Validate before spreading, without filling defaults that would erase a
73
+ // different opt-in from a lower layer.
74
+ if (dangerZone !== undefined) jevDangerZone(dangerZone);
64
75
  // File and flag paths have different bases. Equal path text can identify
65
76
  // different implementations; compare destinations before changing the base.
66
77
  const moduleChanged = layer.module !== undefined && (typeof layer.module !== 'string' ||
@@ -71,6 +82,9 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
71
82
  if (changed) for (const key of PROVIDER_FIELDS) delete next[key];
72
83
  if (layer.module !== undefined) moduleBase = layerModuleBase;
73
84
  return { ...next, ...layer,
85
+ ...(dangerZone === undefined ? {} : { dangerZone: {
86
+ ...(Object.hasOwn(next, 'dangerZone') ? next.dangerZone : {}), ...dangerZone,
87
+ } }),
74
88
  ...(layer.gateway === undefined ? {} : { gateway: { ...next.gateway, ...layer.gateway } }) };
75
89
  }
76
90
  const config = validateConfig(mergeProviderLayer(mergeProviderLayer(
@@ -78,7 +92,7 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
78
92
  if (config.provider === 'jev' && config.model === undefined && effectiveEnv.JEV_MODEL?.trim()) {
79
93
  config.model = effectiveEnv.JEV_MODEL.trim();
80
94
  }
81
- if (config.provider === 'openai-decisions' && config.model === undefined) config.model = 'gpt-6-luna';
95
+ if (config.provider === 'openai-decisions' && config.model === undefined) config.model = OPENAI_DEFAULT_MODEL;
82
96
  if (config.provider === 'cloudflare-decisions' && config.model === undefined) config.model = 'clef';
83
97
  return { config, env: effectiveEnv, moduleBase };
84
98
  }
@@ -3,11 +3,21 @@ import { encodeCommonDecision } from '../providers/decisions-codec.mjs';
3
3
  import { preflightCloudflareDecision, preflightCommonCloudflareDecision } from '../providers/cloudflare-preflight.mjs';
4
4
  import { encodeCloudflareDecision } from '../providers/cloudflare-codec.mjs';
5
5
  import { CLOUDFLARE_MAX_REQUEST_LIMIT } from '../providers/cloudflare-profile.mjs';
6
+ import { resolveOpenAIModel } from '../providers/openai-profile.mjs';
7
+ import { preflightCommonJevDecision, preflightJevDecision, encodeJevDecision } from '../providers/jev-preflight.mjs';
8
+ import { JEV_REQUEST_LIMIT } from '../providers/jev-profile.mjs';
6
9
 
7
10
  /** Provider-specific native dispatch is shared by validation, initialization and calls. */
8
11
  export function decisionProfile(provider) {
12
+ if (provider === 'jev') return {
13
+ common: preflightCommonJevDecision, native: preflightJevDecision, encode: encodeJevDecision,
14
+ models: true, inputLimit: JEV_REQUEST_LIMIT,
15
+ options: config => ({ model: config.model, maxRequestBytes: config.maxRequestBytes,
16
+ dangerZone: Object.hasOwn(config, 'dangerZone') ? config.dangerZone : undefined }),
17
+ };
9
18
  if (provider === 'openai-decisions') return {
10
19
  common: preflightCommonDecision, native: preflightDecision, encode: encodeCommonDecision,
20
+ resolveModel: resolveOpenAIModel,
11
21
  options: config => ({ model: config.model, maxRequestBytes: config.maxRequestBytes }),
12
22
  };
13
23
  if (provider === 'cloudflare-decisions') return {
@@ -4,6 +4,7 @@ import { DecisionError } from '../errors.mjs';
4
4
  import { validateRequest } from '../validation.mjs';
5
5
  import { decisionProfile } from './decisions.mjs';
6
6
  import { validateConfig, KEY_ENVS } from './config-validation.mjs';
7
+ import { OPENAI_DEFAULT_MODEL } from '../providers/openai-profile.mjs';
7
8
 
8
9
  /** Templates are honest edit points for an unknown protocol, never fake inference. */
9
10
  function providerTemplate(kind) {
@@ -38,6 +39,7 @@ export async function initialize(directory, config, native = false, moduleBase =
38
39
  ...(generateModule ? { module: './provider.mjs' } : {}),
39
40
  ...(config.module ? { module: resolve(moduleBase, config.module) } : {}),
40
41
  ...(config.provider === 'jev' && config.model === undefined ? { model: 'jev-latest' } : {}),
42
+ ...(config.provider === 'openai-decisions' && config.model === undefined ? { model: OPENAI_DEFAULT_MODEL } : {}),
41
43
  ...(custom && config.model === undefined ? { model: 'YOUR_MODEL' } : {}),
42
44
  }, { complete: true });
43
45
  const request = { state: "ModuleNotFoundError: No module named 'pytest'", questions: {
@@ -47,12 +49,16 @@ export async function initialize(directory, config, native = false, moduleBase =
47
49
  missing_dependency: { type: 'noul', instructions: 'Does this indicate a missing Python package?' },
48
50
  } };
49
51
  const profile = decisionProfile(config.provider);
52
+ const sample = native ? profile.native(profile.encode(validateRequest(request, template.model)),
53
+ profile.options(template)) : request;
54
+ // OpenAI request files inherit selection from configuration, including after later edits.
55
+ const generated = { ...sample };
56
+ if (config.provider === 'openai-decisions') delete generated.model;
50
57
  const schema = await readFile(new URL('../../schemas/decision.config.schema.json', import.meta.url), 'utf8');
51
58
  const files = {
52
59
  'decision.config.json': JSON.stringify(template, null, 2) + '\n',
53
60
  'decision.config.schema.json': schema,
54
- 'request.json': JSON.stringify(native ? profile.native(profile.encode(validateRequest(request, template.model)),
55
- profile.options(template)) : request, null, 2) + '\n',
61
+ 'request.json': JSON.stringify(generated, null, 2) + '\n',
56
62
  '.env.example': `${template.apiKeyEnv}=replace_with_your_api_key\n` +
57
63
  (config.provider === 'cloudflare-decisions' ? 'CLOUDFLARE_ACCOUNT_ID=replace_with_your_account_id\n' : ''),
58
64
  '.gitignore': '.env\n.env.*\n!.env.example\nnode_modules/\n*.tmp\n',