@fayegram-ai/decision-bridge 0.1.1 → 0.1.3

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 (79) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CONTRIBUTING.md +5 -3
  3. package/README.md +26 -10
  4. package/SECURITY.md +18 -11
  5. package/docs/README.md +19 -5
  6. package/docs/development/ARCHITECTURE.md +36 -7
  7. package/docs/development/DESIGN.md +21 -3
  8. package/docs/development/RELEASING.md +7 -3
  9. package/docs/development/REPOSITORY_LAYOUT.md +2 -0
  10. package/docs/development/TESTING.md +31 -3
  11. package/docs/guides/BROWSER.md +8 -0
  12. package/docs/guides/CLOUDFLARE_DECISIONS.md +24 -46
  13. package/docs/guides/CUSTOMIZATION.md +8 -2
  14. package/docs/guides/CUSTOM_GENERATIVE.md +5 -3
  15. package/docs/guides/CUSTOM_PROVIDERS.md +5 -3
  16. package/docs/guides/EXTRACTION.md +125 -0
  17. package/docs/guides/GETTING_STARTED.md +8 -4
  18. package/docs/guides/JEV.md +167 -0
  19. package/docs/guides/LIVE_DECISIONS.md +163 -0
  20. package/docs/guides/OPENAI_DECISIONS.md +52 -24
  21. package/docs/guides/PLATFORMS.md +3 -3
  22. package/docs/guides/SWITCHING_MODELS.md +6 -2
  23. package/docs/guides/TROUBLESHOOTING.md +25 -0
  24. package/docs/reference/CLI.md +60 -11
  25. package/docs/reference/CONFIGURATION.md +34 -6
  26. package/docs/reference/ERRORS.md +41 -12
  27. package/docs/reference/PROVIDERS.md +31 -8
  28. package/docs/reference/SDK.md +72 -26
  29. package/docs/reference/SOURCES.md +11 -0
  30. package/examples/extraction.mjs +16 -0
  31. package/examples/live-decisions.mjs +24 -0
  32. package/package.json +11 -1
  33. package/schemas/decision.config.schema.json +102 -11
  34. package/src/bridge.mjs +9 -6
  35. package/src/capabilities.mjs +34 -4
  36. package/src/cli/config-validation.mjs +13 -5
  37. package/src/cli/config.mjs +19 -4
  38. package/src/cli/decisions.mjs +8 -0
  39. package/src/cli/initialize.mjs +8 -4
  40. package/src/cli/live.mjs +63 -0
  41. package/src/cli/main.mjs +50 -16
  42. package/src/cli/provider-loader.mjs +3 -2
  43. package/src/cloudflare-decisions.d.mts +12 -5
  44. package/src/decisions.d.mts +28 -4
  45. package/src/errors.mjs +7 -1
  46. package/src/extraction/index.d.mts +66 -0
  47. package/src/extraction/index.mjs +78 -0
  48. package/src/extraction/validation.mjs +105 -0
  49. package/src/index.d.mts +42 -9
  50. package/src/index.mjs +1 -1
  51. package/src/input.mjs +21 -3
  52. package/src/jev.d.mts +16 -0
  53. package/src/live/index.d.mts +73 -0
  54. package/src/live/index.mjs +170 -0
  55. package/src/live/state.mjs +125 -0
  56. package/src/live/validation.mjs +122 -0
  57. package/src/probabilities.mjs +6 -3
  58. package/src/providers/cloudflare-images.mjs +5 -67
  59. package/src/providers/cloudflare-media.mjs +5 -10
  60. package/src/providers/cloudflare-preflight.mjs +4 -3
  61. package/src/providers/cloudflare-profile.mjs +10 -14
  62. package/src/providers/cloudflare-transport.mjs +12 -5
  63. package/src/providers/cloudflare-validation.mjs +3 -1
  64. package/src/providers/decisions-codec.mjs +10 -5
  65. package/src/providers/decisions-input.mjs +120 -0
  66. package/src/providers/decisions-validation.mjs +37 -69
  67. package/src/providers/image-header.mjs +59 -0
  68. package/src/providers/jev-images.mjs +84 -0
  69. package/src/providers/jev-preflight.mjs +62 -0
  70. package/src/providers/jev-profile.mjs +37 -0
  71. package/src/providers/jev.mjs +32 -13
  72. package/src/providers/media-binary.mjs +0 -7
  73. package/src/providers/openai-decisions.mjs +2 -1
  74. package/src/providers/openai-errors.mjs +23 -0
  75. package/src/providers/openai-profile.mjs +2 -1
  76. package/src/questions.mjs +2 -0
  77. package/src/response-transport.mjs +84 -17
  78. package/src/validation.mjs +39 -20
  79. package/src/version.mjs +1 -1
package/src/bridge.mjs CHANGED
@@ -9,6 +9,9 @@ import { assertCapabilities, commonSchemaVersion, providerCapabilities } from '.
9
9
  import { validateResultOptions } from './probabilities.mjs';
10
10
 
11
11
  const elapsed = start => Math.round((performance.now() - start) * 1000) / 1000;
12
+ const inputField = request => Object.hasOwn(request, 'state') ? 'state'
13
+ : Object.hasOwn(request, 'conversation') ? 'conversation' : 'input';
14
+ const inputMethod = request => ({ state: 'prepareState', input: 'prepareInput', conversation: 'prepareConversation' })[inputField(request)];
12
15
 
13
16
  /** Stable interface. Prepare context, evaluate, validate, then observe immutable results. */
14
17
  export function createBridge(options) {
@@ -26,11 +29,11 @@ export function createBridge(options) {
26
29
  'Bridge timeouts must be positive 32-bit integers.', 'CONFIGURATION_ERROR');
27
30
  check(Array.isArray(extensions), 'extensions must be an array.', 'CONFIGURATION_ERROR');
28
31
  const registered = Array.from(extensions, extension => {
29
- optionsObject(extension, ['name', 'prepareState', 'prepareInput', 'onResult'], 'Extension');
32
+ optionsObject(extension, ['name', 'prepareState', 'prepareInput', 'prepareConversation', 'onResult'], 'Extension');
30
33
  check(isRecord(extension) && typeof extension.name === 'string' && extension.name.trim() &&
31
34
  extension.name.length <= 100 && !/[\x00-\x1f\x7f]/.test(extension.name),
32
35
  'Each extension requires a printable name up to 100 characters.', 'CONFIGURATION_ERROR');
33
- for (const method of ['prepareState', 'prepareInput', 'onResult']) check(extension[method] === undefined || typeof extension[method] === 'function',
36
+ for (const method of ['prepareState', 'prepareInput', 'prepareConversation', 'onResult']) check(extension[method] === undefined || typeof extension[method] === 'function',
34
37
  'Extension handlers must be functions.', 'CONFIGURATION_ERROR');
35
38
  return { ...extension };
36
39
  });
@@ -39,9 +42,9 @@ export function createBridge(options) {
39
42
  function validate(input) {
40
43
  const request = validateRequest(input, backend.defaultModel ?? '');
41
44
  assertCapabilities(request, providerCapabilities(backend, request.model));
42
- const method = Object.hasOwn(request, 'state') ? 'prepareState' : 'prepareInput';
45
+ const method = inputMethod(request);
43
46
  for (const extension of registered) {
44
- check(!(extension.prepareState || extension.prepareInput) || typeof extension[method] === 'function',
47
+ check(!(extension.prepareState || extension.prepareInput || extension.prepareConversation) || typeof extension[method] === 'function',
45
48
  `Preparation extension ${extension.name} cannot handle this input form.`, 'UNSUPPORTED_CAPABILITY');
46
49
  }
47
50
  freezeJson(request);
@@ -80,8 +83,8 @@ export function createBridge(options) {
80
83
  questionIds: Object.freeze(Object.keys(request.questions)), signal: budget.signal });
81
84
  for (const extension of registered) {
82
85
  budget.throwIfAborted();
83
- const field = Object.hasOwn(request, 'state') ? 'state' : 'input';
84
- const method = field === 'state' ? 'prepareState' : 'prepareInput';
86
+ const field = inputField(request);
87
+ const method = inputMethod(request);
85
88
  if (!extension[method]) continue;
86
89
  let prepared;
87
90
  try {
@@ -2,6 +2,7 @@ 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
4
  import { openaiModelProfile } from './providers/openai-profile.mjs';
5
+ import { jevFeatures } from './providers/jev-profile.mjs';
5
6
 
6
7
  /** Conservative defaults preserve the pre-existing custom-provider contract. */
7
8
  export const LEGACY_CAPABILITIES = freezeJson({
@@ -9,20 +10,25 @@ export const LEGACY_CAPABILITIES = freezeJson({
9
10
  labeledScores: false, safetyIdentifier: false,
10
11
  maxImages: 0, maxChoiceOptions: 255, maxScoreLevels: 10,
11
12
  audio: false, videos: false, maxAudioClips: 0, maxVideos: 0,
13
+ boxes: false,
14
+ conversation: false, toolHistory: false, remoteImages: false, promptCaching: false,
12
15
  });
13
16
 
14
17
  const CLOUDFLARE_CAPABILITIES = freezeJson({
15
18
  supported: true, orderedInput: true, images: true, typedChoices: true,
16
- labeledScores: true, safetyIdentifier: false, maxImages: 4, maxChoiceOptions: 255, maxScoreLevels: 10,
19
+ labeledScores: true, safetyIdentifier: false, maxImages: null, maxChoiceOptions: null, maxScoreLevels: null,
17
20
  });
18
21
 
19
- export function builtInCapabilities(type, model) {
22
+ export function builtInCapabilities(type, model, options = {}) {
23
+ if (type === 'jev') return freezeJson({ ...LEGACY_CAPABILITIES,
24
+ ...jevFeatures(model, Object.hasOwn(options, 'dangerZone') ? options.dangerZone : undefined),
25
+ maxChoiceOptions: null, maxScoreLevels: null });
20
26
  if (type === 'cloudflare-decisions') {
21
27
  const key = typeof model === 'string' ? model.trim().replace(/^@cf\/cloudflare\//, '') : '';
22
28
  const profile = Object.hasOwn(CLOUDFLARE_MODELS, key) ? CLOUDFLARE_MODELS[key] : undefined;
23
29
  return freezeJson({ ...LEGACY_CAPABILITIES, ...CLOUDFLARE_CAPABILITIES, supported: !!profile,
24
30
  audio: profile?.omni ?? false, videos: profile?.omni ?? false,
25
- maxAudioClips: profile?.omni ? 4 : 0, maxVideos: profile?.omni ? 2 : 0 });
31
+ maxAudioClips: profile?.omni ? null : 0, maxVideos: profile?.omni ? null : 0 });
26
32
  }
27
33
  if (type !== 'openai-decisions') return LEGACY_CAPABILITIES;
28
34
  const profile = openaiModelProfile(model);
@@ -45,7 +51,8 @@ export function providerCapabilities(provider, model) {
45
51
  check(isRecord(profile) && Object.keys(profile).every(key => Object.hasOwn(LEGACY_CAPABILITIES, key)),
46
52
  'Invalid capability profile.', 'CONFIGURATION_ERROR');
47
53
  const result = { ...LEGACY_CAPABILITIES, ...profile };
48
- for (const key of ['supported', 'orderedInput', 'images', 'typedChoices', 'labeledScores', 'safetyIdentifier', 'audio', 'videos']) {
54
+ for (const key of ['supported', 'orderedInput', 'images', 'typedChoices', 'labeledScores', 'safetyIdentifier', 'audio', 'videos', 'boxes',
55
+ 'conversation', 'toolHistory', 'remoteImages', 'promptCaching']) {
49
56
  check(typeof result[key] === 'boolean', 'Capability flags must be boolean.', 'CONFIGURATION_ERROR');
50
57
  }
51
58
  for (const key of ['maxImages', 'maxChoiceOptions', 'maxScoreLevels', 'maxAudioClips', 'maxVideos']) {
@@ -60,9 +67,27 @@ export function assertCapabilities(request, capabilities) {
60
67
  const requireFeature = (condition, feature) => check(condition,
61
68
  `Selected provider/model does not support ${feature}.`, 'UNSUPPORTED_CAPABILITY');
62
69
  requireFeature(capabilities.supported, 'this model');
70
+ if (Object.hasOwn(request, 'conversation')) {
71
+ requireFeature(capabilities.conversation, 'conversation input');
72
+ if (request.conversation.some(item => !Object.hasOwn(item, 'role'))) requireFeature(capabilities.toolHistory, 'tool history');
73
+ const parts = request.conversation.flatMap(item => Array.isArray(item.content) ? item.content
74
+ : Array.isArray(item.output) ? item.output : []);
75
+ const images = parts.filter(part => part.type === 'input_image');
76
+ if (images.length) requireFeature(capabilities.images, 'image input');
77
+ requireFeature(capabilities.maxImages === null || images.length <= capabilities.maxImages, 'this image count');
78
+ if (parts.some(part => part.type === 'input_image' && /^https?:/i.test(part.image_url))) {
79
+ requireFeature(capabilities.remoteImages, 'remote images');
80
+ }
81
+ if (parts.some(part => Object.hasOwn(part, 'prompt_cache_breakpoint'))) requireFeature(capabilities.promptCaching, 'prompt caching');
82
+ }
83
+ if (Object.hasOwn(request, 'promptCacheKey') || Object.hasOwn(request, 'promptCacheOptions') ||
84
+ request.input?.some(part => Object.hasOwn(part, 'promptCacheBreakpoint'))) requireFeature(capabilities.promptCaching, 'prompt caching');
63
85
  if (Object.hasOwn(request, 'input')) {
64
86
  requireFeature(capabilities.orderedInput, 'ordered input');
65
87
  const images = request.input.filter(part => part.type === 'image').length;
88
+ if (request.input.some(part => part.type === 'image' && Object.hasOwn(part, 'url'))) {
89
+ requireFeature(capabilities.remoteImages, 'remote images');
90
+ }
66
91
  if (images) requireFeature(capabilities.images, 'image input');
67
92
  requireFeature(capabilities.maxImages === null || images <= capabilities.maxImages, 'this image count');
68
93
  for (const [kind, flag, limit] of [['audio', 'audio', 'maxAudioClips'], ['video', 'videos', 'maxVideos']]) {
@@ -75,6 +100,7 @@ export function assertCapabilities(request, capabilities) {
75
100
  }
76
101
  if (Object.hasOwn(request, 'safetyIdentifier')) requireFeature(capabilities.safetyIdentifier, 'safetyIdentifier');
77
102
  for (const q of Object.values(request.questions)) {
103
+ if (q.type === 'box') requireFeature(capabilities.boxes, 'box questions');
78
104
  if (q.options) requireFeature(capabilities.typedChoices, 'typed choices');
79
105
  if (q.levels) requireFeature(capabilities.labeledScores, 'labelled scores');
80
106
  const choices = q.options?.length ?? (q.type === 'choice' ? Object.keys(q.criteria).length : 0);
@@ -86,6 +112,10 @@ export function assertCapabilities(request, capabilities) {
86
112
  }
87
113
 
88
114
  export function commonSchemaVersion(request) {
115
+ if (Object.hasOwn(request, 'conversation') || Object.hasOwn(request, 'promptCacheKey') ||
116
+ Object.hasOwn(request, 'promptCacheOptions') || request.input?.some(part =>
117
+ Object.hasOwn(part, 'url') || Object.hasOwn(part, 'promptCacheBreakpoint'))) return 5;
118
+ if (Object.values(request.questions).some(q => q.type === 'box')) return 4;
89
119
  if (request.input?.some(part => ['audio', 'video'].includes(part.type))) return 3;
90
120
  return Object.hasOwn(request, 'input') || Object.hasOwn(request, 'safetyIdentifier') ||
91
121
  Object.values(request.questions).some(q => q.options || q.levels) ? 2 : 1;
@@ -2,17 +2,18 @@ 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
- export const PROVIDERS = ['jev', 'openai-decisions', 'cloudflare-decisions', 'custom-decision', 'custom-generative'];
7
+ export const PROVIDERS = ['jev', 'openai-decisions', 'cloudflare-decisions', 'cloudflare-extraction', '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',
15
- 'cloudflare-decisions': 'CLOUDFLARE_API_TOKEN', 'openai-compatible': 'OPENAI_API_KEY',
16
+ 'cloudflare-decisions': 'CLOUDFLARE_API_TOKEN', 'cloudflare-extraction': 'CLOUDFLARE_API_TOKEN', 'openai-compatible': 'OPENAI_API_KEY',
16
17
  openrouter: 'OPENROUTER_API_KEY', anthropic: 'ANTHROPIC_API_KEY' };
17
18
  export const isModule = config => ['custom-decision', 'custom-generative'].includes(config.provider) && config.module !== undefined;
18
19
 
@@ -56,9 +57,16 @@ 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) {
60
- if (config.provider !== 'cloudflare-decisions') check(['accountId', 'rejectIfBusy', 'gateway'].every(key => config[key] === undefined),
61
- 'Cloudflare settings require cloudflare-decisions.', 'CONFIGURATION_ERROR');
63
+ if (config.provider !== 'jev') check(!Object.hasOwn(config, 'dangerZone'),
64
+ 'dangerZone requires Jev.', 'CONFIGURATION_ERROR');
65
+ else {
66
+ jevRequestLimit(config.maxRequestBytes);
67
+ }
68
+ if (!['cloudflare-decisions', 'cloudflare-extraction'].includes(config.provider)) check(['accountId', 'rejectIfBusy', 'gateway'].every(key => config[key] === undefined),
69
+ 'Cloudflare settings require a Cloudflare provider.', 'CONFIGURATION_ERROR');
62
70
  else {
63
71
  config.gateway = cloudflareGateway(config.gateway);
64
72
  if (config.gateway === undefined) delete config.gateway;
@@ -4,6 +4,7 @@ import { parseEnv } from 'node:util';
4
4
  import { OPENAI_DEFAULT_MODEL } from '../providers/openai-profile.mjs';
5
5
  import { DecisionError, requireCondition as check } from '../errors.mjs';
6
6
  import { validateConfig, NUMERIC, PROVIDER_FIELDS } from './config-validation.mjs';
7
+ import { jevDangerZone } from '../providers/jev-profile.mjs';
7
8
  export { validateConfig } from './config-validation.mjs';
8
9
  export { configuredProvider } from './provider-loader.mjs';
9
10
 
@@ -12,6 +13,7 @@ const ENV_FIELDS = {
12
13
  DECISION_API_KEY_ENV: 'apiKeyEnv', DECISION_TIMEOUT_MS: 'timeoutMs',
13
14
  DECISION_MAX_RETRIES: 'maxRetries', DECISION_RESPONSE_FORMAT: 'responseFormat',
14
15
  DECISION_ACCOUNT_ID: 'accountId', DECISION_REJECT_IF_BUSY: 'rejectIfBusy', DECISION_GATEWAY_ID: 'gateway',
16
+ DECISION_JEV_IMAGES: 'jevImages', DECISION_JEV_LONG_CONTEXT: 'jevLongContext',
15
17
  };
16
18
 
17
19
  async function boundedFile(path, maxBytes, label) {
@@ -28,7 +30,8 @@ async function boundedFile(path, maxBytes, label) {
28
30
  }
29
31
 
30
32
  /** Explicit file only. Flags > DECISION_* environment > file > defaults. */
31
- export async function loadConfig({ configPath, envFile, overrides = {}, cwd = process.cwd(), env = process.env, allowIncomplete = false } = {}) {
33
+ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = process.cwd(), env = process.env, allowIncomplete = false,
34
+ defaultProvider = 'jev' } = {}) {
32
35
  const effectiveEnv = { ...env };
33
36
  if (envFile) {
34
37
  const text = await boundedFile(resolve(cwd, envFile), 65536, 'environment file');
@@ -47,13 +50,17 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
47
50
  fileConfig = validateConfig(fileConfig);
48
51
  moduleBase = dirname(path);
49
52
  }
50
- const environment = {};
53
+ const environment = Object.create(null);
51
54
  for (const [key, field] of Object.entries(ENV_FIELDS)) {
52
55
  if (effectiveEnv[key] === undefined) continue;
53
56
  const value = effectiveEnv[key];
54
57
  if (NUMERIC.has(field)) check(typeof value === 'string' && /^\d+$/.test(value),
55
58
  'Numeric DECISION_* variables must contain decimal integers.', 'CONFIGURATION_ERROR');
56
- if (field === 'rejectIfBusy') {
59
+ if (field === 'jevImages' || field === 'jevLongContext') {
60
+ check(value === 'true' || value === 'false', 'Jev DECISION_* flags must be true or false.', 'CONFIGURATION_ERROR');
61
+ environment.dangerZone = { ...(Object.hasOwn(environment, 'dangerZone') ? environment.dangerZone : {}),
62
+ [field === 'jevImages' ? 'images' : 'longContext']: value === 'true' };
63
+ } else if (field === 'rejectIfBusy') {
57
64
  check(value === 'true' || value === 'false', 'DECISION_REJECT_IF_BUSY must be true or false.', 'CONFIGURATION_ERROR');
58
65
  environment[field] = value === 'true';
59
66
  } else environment[field] = field === 'gateway' ? { id: value } : NUMERIC.has(field) ? Number(value) : value;
@@ -62,6 +69,10 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
62
69
  // environment-selected provider must not inherit that provider's key/endpoint.
63
70
  function mergeProviderLayer(base, layer, layerModuleBase = moduleBase) {
64
71
  const next = { ...base };
72
+ const dangerZone = Object.hasOwn(layer, 'dangerZone') ? layer.dangerZone : undefined;
73
+ // Validate before spreading, without filling defaults that would erase a
74
+ // different opt-in from a lower layer.
75
+ if (dangerZone !== undefined) jevDangerZone(dangerZone);
65
76
  // File and flag paths have different bases. Equal path text can identify
66
77
  // different implementations; compare destinations before changing the base.
67
78
  const moduleChanged = layer.module !== undefined && (typeof layer.module !== 'string' ||
@@ -72,14 +83,18 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
72
83
  if (changed) for (const key of PROVIDER_FIELDS) delete next[key];
73
84
  if (layer.module !== undefined) moduleBase = layerModuleBase;
74
85
  return { ...next, ...layer,
86
+ ...(dangerZone === undefined ? {} : { dangerZone: {
87
+ ...(Object.hasOwn(next, 'dangerZone') ? next.dangerZone : {}), ...dangerZone,
88
+ } }),
75
89
  ...(layer.gateway === undefined ? {} : { gateway: { ...next.gateway, ...layer.gateway } }) };
76
90
  }
77
91
  const config = validateConfig(mergeProviderLayer(mergeProviderLayer(
78
- { schemaVersion: 1, provider: 'jev', ...fileConfig }, environment), overrides, cwd), { complete: !allowIncomplete });
92
+ { schemaVersion: 1, provider: defaultProvider, ...fileConfig }, environment), overrides, cwd), { complete: !allowIncomplete });
79
93
  if (config.provider === 'jev' && config.model === undefined && effectiveEnv.JEV_MODEL?.trim()) {
80
94
  config.model = effectiveEnv.JEV_MODEL.trim();
81
95
  }
82
96
  if (config.provider === 'openai-decisions' && config.model === undefined) config.model = OPENAI_DEFAULT_MODEL;
83
97
  if (config.provider === 'cloudflare-decisions' && config.model === undefined) config.model = 'clef';
98
+ if (config.provider === 'cloudflare-extraction' && config.model === undefined) config.model = 'clairic';
84
99
  return { config, env: effectiveEnv, moduleBase };
85
100
  }
@@ -4,9 +4,17 @@ import { preflightCloudflareDecision, preflightCommonCloudflareDecision } from '
4
4
  import { encodeCloudflareDecision } from '../providers/cloudflare-codec.mjs';
5
5
  import { CLOUDFLARE_MAX_REQUEST_LIMIT } from '../providers/cloudflare-profile.mjs';
6
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';
7
9
 
8
10
  /** Provider-specific native dispatch is shared by validation, initialization and calls. */
9
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
+ };
10
18
  if (provider === 'openai-decisions') return {
11
19
  common: preflightCommonDecision, native: preflightDecision, encode: encodeCommonDecision,
12
20
  resolveModel: resolveOpenAIModel,
@@ -5,6 +5,7 @@ import { validateRequest } from '../validation.mjs';
5
5
  import { decisionProfile } from './decisions.mjs';
6
6
  import { validateConfig, KEY_ENVS } from './config-validation.mjs';
7
7
  import { OPENAI_DEFAULT_MODEL } from '../providers/openai-profile.mjs';
8
+ import { validateExtractionRequest } from '../extraction/validation.mjs';
8
9
 
9
10
  /** Templates are honest edit points for an unknown protocol, never fake inference. */
10
11
  function providerTemplate(kind) {
@@ -42,15 +43,18 @@ export async function initialize(directory, config, native = false, moduleBase =
42
43
  ...(config.provider === 'openai-decisions' && config.model === undefined ? { model: OPENAI_DEFAULT_MODEL } : {}),
43
44
  ...(custom && config.model === undefined ? { model: 'YOUR_MODEL' } : {}),
44
45
  }, { complete: true });
45
- const request = { state: "ModuleNotFoundError: No module named 'pytest'", questions: {
46
+ const request = config.provider === 'cloudflare-extraction' ? {
47
+ state: 'Invoice total: 1080 USD. Due date: 2026-11-01.',
48
+ fields: { total: { type: 'number', unit: 'USD' }, due_date: { type: 'date' } },
49
+ } : { state: "ModuleNotFoundError: No module named 'pytest'", questions: {
46
50
  category: { type: 'choice', instructions: 'Classify the failure using only the supplied evidence.',
47
51
  criteria: { dependency: 'A required package or module is unavailable.',
48
52
  network: 'A network operation failed.', assertion: 'A test assertion failed.', unknown: 'Insufficient evidence.' } },
49
53
  missing_dependency: { type: 'noul', instructions: 'Does this indicate a missing Python package?' },
50
54
  } };
51
55
  const profile = decisionProfile(config.provider);
52
- const sample = native ? profile.native(profile.encode(validateRequest(request, template.model)),
53
- profile.options(template)) : request;
56
+ const sample = config.provider === 'cloudflare-extraction' ? validateExtractionRequest(request, { model: template.model })
57
+ : native ? profile.native(profile.encode(validateRequest(request, template.model)), profile.options(template)) : request;
54
58
  // OpenAI request files inherit selection from configuration, including after later edits.
55
59
  const generated = { ...sample };
56
60
  if (config.provider === 'openai-decisions') delete generated.model;
@@ -60,7 +64,7 @@ export async function initialize(directory, config, native = false, moduleBase =
60
64
  'decision.config.schema.json': schema,
61
65
  'request.json': JSON.stringify(generated, null, 2) + '\n',
62
66
  '.env.example': `${template.apiKeyEnv}=replace_with_your_api_key\n` +
63
- (config.provider === 'cloudflare-decisions' ? 'CLOUDFLARE_ACCOUNT_ID=replace_with_your_account_id\n' : ''),
67
+ (['cloudflare-decisions', 'cloudflare-extraction'].includes(config.provider) ? 'CLOUDFLARE_ACCOUNT_ID=replace_with_your_account_id\n' : ''),
64
68
  '.gitignore': '.env\n.env.*\n!.env.example\nnode_modules/\n*.tmp\n',
65
69
  ...(generateModule ? { 'provider.mjs': providerTemplate(config.provider === 'custom-decision' ? 'decision' : 'generative') } : {}),
66
70
  };
@@ -0,0 +1,63 @@
1
+ import { DecisionError, requireCondition as check, publicError } from '../errors.mjs';
2
+ import { validateLiveCommand, validateLiveConfiguration, validateLiveEvent } from '../live/validation.mjs';
3
+ import { createLiveState } from '../live/state.mjs';
4
+ import { assertDistinctFiles, createWriter, readJson, readJsonLines } from './io.mjs';
5
+
6
+ /** Offline protocol tools deliberately do not load provider configuration or keys. */
7
+ export async function runLiveCli(positionals, values, context) {
8
+ const [command, positional] = positionals;
9
+ check(['validate', 'replay'].includes(command) && positionals.length <= 2, 'Use live validate or live replay.', 'USAGE_ERROR');
10
+ check(Object.keys(values).every(key => ['input', 'output', 'force', 'pretty', 'compact', 'kind'].includes(key)),
11
+ 'Unsupported option for offline Live tools.', 'USAGE_ERROR');
12
+ check(!(values.input && positional), 'Supply one Live input file.', 'USAGE_ERROR');
13
+ check(!(values.pretty && values.compact), 'Choose pretty or compact output.', 'USAGE_ERROR');
14
+ check(!values.force || (values.output && values.output !== '-'), '--force requires file output.', 'USAGE_ERROR');
15
+ const path = values.input ?? positional;
16
+ check(path, 'Live tools require an explicit input file or -.', 'USAGE_ERROR');
17
+ if (values.output && values.output !== '-' && path !== '-') await assertDistinctFiles(path, values.output, context.cwd);
18
+ const validators = { config: validateLiveConfiguration, command: validateLiveCommand, event: validateLiveEvent };
19
+ check(command !== 'validate' || Object.hasOwn(validators, values.kind ?? ''), 'Live validation requires --kind config, command or event.', 'USAGE_ERROR');
20
+ check(command !== 'replay' || values.kind === undefined, '--kind applies only to validation.', 'USAGE_ERROR');
21
+ const writer = await createWriter({ ...context, output: values.output, force: values.force });
22
+ try {
23
+ if (command === 'validate') {
24
+ const input = await readJson(path, context);
25
+ try { validators[values.kind](input); }
26
+ catch (error) {
27
+ // An offline event file is caller input, even though the SDK's
28
+ // event validator correctly describes malformed server responses.
29
+ if (values.kind === 'event' && error instanceof DecisionError && error.code === 'INVALID_RESPONSE') {
30
+ throw new DecisionError(error.message, { ...error.toJSON(), code: 'VALIDATION_ERROR', source: 'client' });
31
+ }
32
+ throw error;
33
+ }
34
+ await writer.write(JSON.stringify({ valid: true, operation: 'live', kind: values.kind,
35
+ validationScope: 'structure' }, null, values.compact ? 0 : 2) + '\n');
36
+ await writer.finish();
37
+ return 0;
38
+ }
39
+ const state = createLiveState();
40
+ let failed = false;
41
+ for await (const row of readJsonLines(path, context)) {
42
+ let record;
43
+ try {
44
+ if (row.error) { failed = true; record = { line: row.line, ok: false, error: row.error }; }
45
+ else {
46
+ const input = row.input;
47
+ check(input && typeof input === 'object' && Object.keys(input).length === 2 &&
48
+ Object.hasOwn(input, 'event') && ['client', 'server'].includes(input.direction), 'Replay requires direction and event.');
49
+ let event;
50
+ if (input.direction === 'client') {
51
+ const command = validateLiveCommand(input.event);
52
+ state.observeClient(command);
53
+ event = { type: 'command', eventType: command.type, ...(command.event_id ? { eventId: command.event_id } : {}) };
54
+ } else event = state.accept(validateLiveEvent(input.event)) ?? { type: 'ignored', eventType: input.event.type };
55
+ record = { line: row.line, ok: true, event };
56
+ }
57
+ } catch (error) { failed = true; record = { line: row.line, ok: false, error: publicError(error) }; }
58
+ await writer.write(JSON.stringify(record) + '\n');
59
+ }
60
+ await writer.finish();
61
+ return failed ? 1 : 0;
62
+ } catch (error) { await writer.abort(); throw error; }
63
+ }
package/src/cli/main.mjs CHANGED
@@ -13,11 +13,14 @@ import { isModule } from './config-validation.mjs';
13
13
  import { loadConfig, configuredProvider } from './config.mjs';
14
14
  import { assertDistinctFiles, createWriter, readJson, readJsonLines } from './io.mjs';
15
15
  import { mapOrdered } from './ordered.mjs';
16
+ import { validateExtractionRequest } from '../extraction/index.mjs';
17
+ import { runLiveCli } from './live.mjs';
16
18
 
17
19
  const SAMPLE_PATH = fileURLToPath(new URL('../../examples/request.json', import.meta.url));
18
- const BOOLS = ['help', 'version', 'pretty', 'compact', 'no-auth', 'force', 'native', 'reject-if-busy', 'no-reject-if-busy'];
20
+ const BOOLS = ['help', 'version', 'pretty', 'compact', 'no-auth', 'force', 'native', 'reject-if-busy', 'no-reject-if-busy',
21
+ 'jev-images', 'no-jev-images', 'jev-long-context', 'no-jev-long-context'];
19
22
  const STRINGS = ['config', 'env-file', 'provider', 'preset', 'model', 'base-url', 'api-key-env', 'timeout-ms',
20
- 'retries', 'max-tokens', 'response-format', 'token-parameter', 'input', 'output', 'concurrency', 'provider-module', 'account-id', 'gateway-id'];
23
+ 'retries', 'max-tokens', 'response-format', 'token-parameter', 'input', 'output', 'concurrency', 'provider-module', 'account-id', 'gateway-id', 'kind'];
21
24
  const FIELD_MAP = { preset: 'preset', provider: 'provider', model: 'model', 'base-url': 'baseURL', 'api-key-env': 'apiKeyEnv',
22
25
  'timeout-ms': 'timeoutMs', retries: 'maxRetries', 'max-tokens': 'maxTokens',
23
26
  'response-format': 'responseFormat', 'token-parameter': 'tokenParameter', 'provider-module': 'module', 'account-id': 'accountId' };
@@ -31,6 +34,10 @@ Usage:
31
34
  decision-bridge schema [request.json | -] [options] (offline generative schema)
32
35
  decision-bridge models [options]
33
36
  decision-bridge batch requests.jsonl [--concurrency 4] [options]
37
+ decision-bridge extraction run|validate|batch request-file [options]
38
+ decision-bridge extraction init [directory] [options]
39
+ decision-bridge live validate FILE --kind config|command|event (offline)
40
+ decision-bridge live replay FILE.jsonl (offline)
34
41
 
35
42
  Command aliases: decision, faye-decision
36
43
 
@@ -48,8 +55,12 @@ Options:
48
55
  --api-key-env NAME Read credential from this environment variable
49
56
  --no-auth Local OpenAI-compatible endpoint only
50
57
  --timeout-ms N End-to-end evaluation/provider deadline
51
- --retries N HTTP retries, 0–10; Decisions default 0, others 2
58
+ --retries N HTTP retries, 0–10; OpenAI/Cloudflare default 0, Jev 2
52
59
  --native Selected decision provider native format (init/evaluate/validate/batch)
60
+ --jev-images Enable experimental Jev images and box questions
61
+ --no-jev-images Explicitly disable Jev images
62
+ --jev-long-context Enable experimental Jev 262k context
63
+ --no-jev-long-context Explicitly disable Jev long context
53
64
  --account-id ID Cloudflare account (otherwise CLOUDFLARE_ACCOUNT_ID)
54
65
  --reject-if-busy Unavailable for Clef REST; rejected before inference
55
66
  --no-reject-if-busy Explicitly disable Cloudflare capacity rejection
@@ -65,7 +76,8 @@ Options:
65
76
  --pretty / --compact JSON layout; default pretty (batch is always JSONL)
66
77
  --help, -h / --version, -v
67
78
 
68
- Without an input file, evaluate/validate/schema use the bundled sample.
79
+ Without an input file, decision evaluate/validate/schema use the bundled sample.
80
+ Extraction and Live operations require an explicit input file or stdin.
69
81
  No credentials, file reads, or network calls are needed for help/version.
70
82
  All API calls are explicit; evaluate/models/batch may access paid services.
71
83
  Exit codes: 0 success; 1 provider/partial-batch failure; 2 input/config/IO;
@@ -95,7 +107,7 @@ function argumentsFor(argv) {
95
107
  }
96
108
 
97
109
  function overridesFor(values) {
98
- const overrides = {};
110
+ const overrides = Object.create(null);
99
111
  for (const [flag, field] of Object.entries(FIELD_MAP)) if (values[flag] !== undefined) {
100
112
  const value = values[flag];
101
113
  if (['timeoutMs', 'maxRetries', 'maxTokens'].includes(field)) {
@@ -107,12 +119,18 @@ function overridesFor(values) {
107
119
  check(!(values['reject-if-busy'] && values['no-reject-if-busy']), 'Choose one reject-if-busy flag.', 'USAGE_ERROR');
108
120
  if (values['reject-if-busy'] || values['no-reject-if-busy']) overrides.rejectIfBusy = !!values['reject-if-busy'];
109
121
  if (values['gateway-id'] !== undefined) overrides.gateway = { id: values['gateway-id'] };
122
+ for (const [flag, field] of [['jev-images', 'images'], ['jev-long-context', 'longContext']]) {
123
+ check(!(values[flag] && values['no-' + flag]), 'Choose one Jev feature flag for each setting.', 'USAGE_ERROR');
124
+ if (values[flag] || values['no-' + flag]) overrides.dangerZone = {
125
+ ...(Object.hasOwn(overrides, 'dangerZone') ? overrides.dangerZone : {}), [field]: !!values[flag],
126
+ };
127
+ }
110
128
  return overrides;
111
129
  }
112
130
 
113
131
  const INPUT_CODES = new Set(['USAGE_ERROR', 'CONFIGURATION_ERROR', 'VALIDATION_ERROR', 'UNSUPPORTED_CAPABILITY', 'IO_ERROR', 'INPUT_TOO_LARGE', 'REQUEST_TOO_LARGE']);
114
132
  export function exitCode(error) {
115
- return error?.code === 'ABORTED' ? 130 : INPUT_CODES.has(error?.code) ? 2 : 1;
133
+ return error?.code === 'ABORTED' ? 130 : error?.source === 'provider' ? 1 : INPUT_CODES.has(error?.code) ? 2 : 1;
116
134
  }
117
135
 
118
136
  function scrub(value, env, explicitSecret) {
@@ -141,14 +159,21 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
141
159
  if (signal?.aborted) cancelled();
142
160
  signal?.addEventListener('abort', cancelled, { once: true });
143
161
  try {
144
- const { values, positionals } = argumentsFor(argv);
162
+ const { values, positionals: originalPositionals } = argumentsFor(argv);
163
+ const namespace = ['extraction', 'live'].includes(originalPositionals[0]) ? originalPositionals[0] : undefined;
164
+ const positionals = namespace ? originalPositionals.slice(1) : originalPositionals;
145
165
  if (values.help || (positionals.length === 0 && !values.version)) { stdout.write(HELP); return 0; }
146
166
  if (values.version) { stdout.write(VERSION + '\n'); return 0; }
147
- const aliases = { eval: 'evaluate', ask: 'evaluate' };
167
+ if (namespace === 'live') return await runLiveCli(positionals, values,
168
+ { stdin, stdout, cwd, signal: internal.signal });
169
+ const extracting = namespace === 'extraction';
170
+ const aliases = { eval: 'evaluate', ask: 'evaluate', ...(extracting ? { run: 'evaluate' } : {}) };
148
171
  const command = aliases[positionals[0]] ?? positionals[0];
149
172
  if (command === 'help') { stdout.write(HELP); return 0; }
150
173
  if (internal.signal.aborted) throw abortError();
151
174
  check(['evaluate', 'validate', 'schema', 'models', 'init', 'batch'].includes(command), 'Unknown command. Run --help.', 'USAGE_ERROR');
175
+ check(!extracting || ['evaluate', 'validate', 'init', 'batch'].includes(command), 'Unsupported extraction command.', 'USAGE_ERROR');
176
+ check(values.kind === undefined, '--kind applies only to live validation.', 'USAGE_ERROR');
152
177
  check(positionals.length <= (command === 'models' ? 1 : 2), 'Too many positional arguments.', 'USAGE_ERROR');
153
178
  check(!(values.input && positionals[1]), 'Supply either --input or a positional file, not both.', 'USAGE_ERROR');
154
179
  check(!(values.pretty && values.compact), 'Choose --pretty or --compact, not both.', 'USAGE_ERROR');
@@ -165,23 +190,30 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
165
190
  'concurrency must be from 1 to 64.', 'USAGE_ERROR');
166
191
  }
167
192
  const loaded = await loadConfig({ configPath: values.config, envFile: values['env-file'],
168
- overrides: overridesFor(values), cwd, env, allowIncomplete: command === 'init' });
169
- const profile = decisionProfile(loaded.config.provider);
193
+ overrides: overridesFor(values), cwd, env, allowIncomplete: command === 'init',
194
+ defaultProvider: extracting ? 'cloudflare-extraction' : 'jev' });
195
+ check(extracting === (loaded.config.provider === 'cloudflare-extraction'), 'Selected provider does not match the command operation.', 'CONFIGURATION_ERROR');
196
+ const profile = extracting ? { common: validateExtractionRequest, inputLimit: 64 * 1024 * 1024,
197
+ options: config => Object.fromEntries(['model', 'maxRequestBytes', 'maxRetries', 'rejectIfBusy', 'gateway']
198
+ .filter(key => config[key] !== undefined).map(key => [key, config[key]])) } : decisionProfile(loaded.config.provider);
199
+ check(!extracting || !values.native, 'Extraction already uses its native field schema.', 'USAGE_ERROR');
170
200
  check(!values.native || (profile && ['init', 'evaluate', 'validate', 'batch'].includes(command)),
171
201
  '--native requires a decision provider and init/evaluate/validate/batch.', 'USAGE_ERROR');
172
202
  check(!values.native || command === 'init' || values.input || positionals[1],
173
203
  '--native requires an explicit input file or -.', 'USAGE_ERROR');
174
- check(command !== 'models' || !profile,
204
+ check(command !== 'models' || !profile || profile.models,
175
205
  'This decision provider does not implement model listing.', 'UNSUPPORTED_OPERATION');
176
206
  activeEnv = loaded.env;
177
207
  explicitSecret = loaded.config.apiKeyEnv ? activeEnv[loaded.config.apiKeyEnv] : undefined;
178
208
  const inputPath = values.input ?? positionals[1] ?? SAMPLE_PATH;
209
+ check(!extracting || command === 'init' || values.input || positionals[1], 'Extraction requires an explicit input file or -.', 'USAGE_ERROR');
179
210
  if (values.output && values.output !== '-' && inputPath !== '-') {
180
211
  await assertDistinctFiles(inputPath, values.output, cwd);
181
212
  }
182
213
  writer = await createWriter({ output: values.output, force: values.force, stdout, cwd, signal: internal.signal });
183
214
  const writeJson = value => writer.write(JSON.stringify(value, null, values.compact ? 0 : 2) + '\n');
184
- const context = { stdin, cwd, signal: internal.signal, maxInputBytes: profile?.inputLimit };
215
+ const context = { stdin, cwd, signal: internal.signal,
216
+ maxInputBytes: Math.max(profile?.inputLimit ?? 8 * 1024 * 1024, loaded.config.maxRequestBytes ?? 0) };
185
217
  const withModel = input => {
186
218
  if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
187
219
  // Apply the configured fallback independently of a custom factory's default.
@@ -189,7 +221,7 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
189
221
  const model = values.model ?? (isModule(loaded.config) && !Object.hasOwn(input, 'model')
190
222
  ? loaded.config.model : undefined);
191
223
  const request = model === undefined ? input : { ...input, model };
192
- if (values.native && (values['reject-if-busy'] || values['no-reject-if-busy'])) {
224
+ if ((values.native || extracting) && (values['reject-if-busy'] || values['no-reject-if-busy'])) {
193
225
  check(request.options === undefined || (request.options !== null && typeof request.options === 'object' &&
194
226
  !Array.isArray(request.options)), 'Native execution options must be an object.');
195
227
  return { ...request, options: { ...request.options, rejectIfBusy: !!values['reject-if-busy'] } };
@@ -201,7 +233,7 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
201
233
  if (profile && command !== 'schema') return profile.common(raw, profile.options(loaded.config));
202
234
  const request = validateRequest(raw, raw?.model ?? loaded.config.model ?? 'validation-only');
203
235
  if (!isModule(loaded.config)) assertCapabilities(request,
204
- builtInCapabilities(loaded.config.provider, request.model));
236
+ builtInCapabilities(loaded.config.provider, request.model, loaded.config));
205
237
  return request;
206
238
  };
207
239
  if (command === 'init') {
@@ -213,7 +245,8 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
213
245
  const buildResponseSchema = command === 'schema'
214
246
  ? (await import('../custom/generative/schema.mjs')).buildResponseSchema : undefined;
215
247
  await writeJson(command === 'schema' ? buildResponseSchema(request.questions)
216
- : { valid: true, validationScope: isModule(loaded.config) ? 'structure' : 'provider', questionCount: Object.keys(request.questions).length, model: selectedModel,
248
+ : { valid: true, operation: extracting ? 'extraction' : 'decision', validationScope: isModule(loaded.config) ? 'structure' : 'provider',
249
+ ...(extracting ? { fieldCount: Object.keys(request.fields).length } : { questionCount: Object.keys(request.questions).length }), model: selectedModel,
217
250
  ...(profile?.resolveModel ? { resolvedModel: profile.resolveModel(request.model) } : {}),
218
251
  ...(values.native ? { native: true } : {}) });
219
252
  } else {
@@ -226,8 +259,9 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
226
259
  validateInput(request);
227
260
  }
228
261
  const provider = await configuredProvider(loaded, { signal: internal.signal });
229
- const bridge = createBridge({ provider, timeoutMs: loaded.config.timeoutMs ?? 60000 });
262
+ const bridge = extracting ? undefined : createBridge({ provider, timeoutMs: loaded.config.timeoutMs ?? 60000 });
230
263
  async function evaluateInput(input) {
264
+ if (extracting) return provider.extract(input, { signal: internal.signal });
231
265
  if (!values.native) return bridge.evaluate(input, { signal: internal.signal });
232
266
  const budget = deadline(loaded.config.timeoutMs ?? 60000, internal.signal);
233
267
  try {
@@ -4,6 +4,7 @@ import { DecisionError, requireCondition as check } from '../errors.mjs';
4
4
  import { deadline, withSignal, abortError } from '../async.mjs';
5
5
  import { providerContract } from '../provider-contract.mjs';
6
6
  import { createProvider } from '../providers/index.mjs';
7
+ import { CloudflareExtractionProvider } from '../extraction/index.mjs';
7
8
  import { validateConfig, isModule, KEY_ENVS } from './config-validation.mjs';
8
9
 
9
10
  /** Executable modules are loaded only by explicit inference/model-list commands. */
@@ -52,7 +53,7 @@ export async function configuredProvider(loaded, { signal } = {}) {
52
53
  const { createGenerativeProvider } = await import('../custom/generative/index.mjs');
53
54
  return createGenerativeProvider({ type: config.preset, ...options });
54
55
  }
55
- if (type === 'cloudflare-decisions') options.accountId = config.accountId ??
56
+ if (['cloudflare-decisions', 'cloudflare-extraction'].includes(type)) options.accountId = config.accountId ??
56
57
  (config.baseURL === undefined ? loaded.env.CLOUDFLARE_ACCOUNT_ID : undefined) ?? '';
57
- return createProvider({ type, ...options });
58
+ return type === 'cloudflare-extraction' ? new CloudflareExtractionProvider(options) : createProvider({ type, ...options });
58
59
  }
@@ -41,25 +41,32 @@ export interface CloudflareDecisionResponse {
41
41
  readonly usage: CloudflareDecisionUsage;
42
42
  }
43
43
  export interface CloudflareGatewayOptions {
44
- /** Nonblank printable ASCII Gateway name, at most 64 characters. */
44
+ /** Nonblank printable ASCII Gateway name. */
45
45
  readonly id: string;
46
46
  readonly cacheKey?: string;
47
- /** Integer seconds from 60 through 2,592,000 (30 days). */
47
+ /** Nonnegative integer seconds; the service validates its cache policy. */
48
48
  readonly cacheTtl?: number;
49
49
  readonly skipCache?: boolean;
50
50
  readonly collectLog?: boolean;
51
- /** At most five JSON primitive entries; caller-supplied cf.* keys are reserved. */
51
+ /** JSON primitive entries; caller-supplied cf.* keys are reserved. */
52
52
  readonly metadata?: Readonly<Record<string, string | number | boolean | null>>;
53
53
  readonly requestTimeoutMs?: number;
54
- readonly retries?: { readonly maxAttempts?: 1 | 2 | 3 | 4 | 5; readonly retryDelayMs?: number;
54
+ readonly retries?: { readonly maxAttempts?: number; readonly retryDelayMs?: number;
55
55
  readonly backoff?: 'constant' | 'linear' | 'exponential' };
56
56
  }
57
57
  export interface CloudflareAIBinding {
58
58
  run(model: '@cf/cloudflare/clef' | '@cf/cloudflare/clef-flash' | '@cf/cloudflare/clef-omni',
59
59
  input: Omit<CloudflareDecisionRequest, 'model' | 'options'> & { readonly model: 'clef' | 'clef-flash' | 'clef-omni' },
60
60
  options?: { readonly signal?: AbortSignal; readonly returnRawResponse?: boolean;
61
- readonly rejectIfBusy?: boolean; readonly gateway?: CloudflareGatewayOptions }): Promise<unknown>;
61
+ readonly rejectIfBusy?: boolean; readonly gateway?: CloudflareBindingGatewayOptions }): Promise<unknown>;
62
62
  }
63
+
64
+ /** Known Workers binding ABI. Provider configuration remains forwardable to the service. */
65
+ export type CloudflareBindingGatewayOptions = Omit<CloudflareGatewayOptions, 'retries'> & {
66
+ readonly retries?: Omit<NonNullable<CloudflareGatewayOptions['retries']>, 'maxAttempts'> & {
67
+ readonly maxAttempts?: 1 | 2 | 3 | 4 | 5;
68
+ };
69
+ };
63
70
  type CommonOptions = {
64
71
  model?: CloudflareDecisionModel;
65
72
  /** Defaults to false. Enabling it requires binding transport and no retries. */