@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.
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +5 -3
- package/README.md +26 -10
- package/SECURITY.md +18 -11
- package/docs/README.md +19 -5
- package/docs/development/ARCHITECTURE.md +36 -7
- package/docs/development/DESIGN.md +21 -3
- package/docs/development/RELEASING.md +7 -3
- package/docs/development/REPOSITORY_LAYOUT.md +2 -0
- package/docs/development/TESTING.md +31 -3
- package/docs/guides/BROWSER.md +8 -0
- package/docs/guides/CLOUDFLARE_DECISIONS.md +24 -46
- package/docs/guides/CUSTOMIZATION.md +8 -2
- package/docs/guides/CUSTOM_GENERATIVE.md +5 -3
- package/docs/guides/CUSTOM_PROVIDERS.md +5 -3
- package/docs/guides/EXTRACTION.md +125 -0
- package/docs/guides/GETTING_STARTED.md +8 -4
- package/docs/guides/JEV.md +167 -0
- package/docs/guides/LIVE_DECISIONS.md +163 -0
- package/docs/guides/OPENAI_DECISIONS.md +52 -24
- package/docs/guides/PLATFORMS.md +3 -3
- package/docs/guides/SWITCHING_MODELS.md +6 -2
- package/docs/guides/TROUBLESHOOTING.md +25 -0
- package/docs/reference/CLI.md +60 -11
- package/docs/reference/CONFIGURATION.md +34 -6
- package/docs/reference/ERRORS.md +41 -12
- package/docs/reference/PROVIDERS.md +31 -8
- package/docs/reference/SDK.md +72 -26
- package/docs/reference/SOURCES.md +11 -0
- package/examples/extraction.mjs +16 -0
- package/examples/live-decisions.mjs +24 -0
- package/package.json +11 -1
- package/schemas/decision.config.schema.json +102 -11
- package/src/bridge.mjs +9 -6
- package/src/capabilities.mjs +34 -4
- package/src/cli/config-validation.mjs +13 -5
- package/src/cli/config.mjs +19 -4
- package/src/cli/decisions.mjs +8 -0
- package/src/cli/initialize.mjs +8 -4
- package/src/cli/live.mjs +63 -0
- package/src/cli/main.mjs +50 -16
- package/src/cli/provider-loader.mjs +3 -2
- package/src/cloudflare-decisions.d.mts +12 -5
- package/src/decisions.d.mts +28 -4
- package/src/errors.mjs +7 -1
- package/src/extraction/index.d.mts +66 -0
- package/src/extraction/index.mjs +78 -0
- package/src/extraction/validation.mjs +105 -0
- package/src/index.d.mts +42 -9
- package/src/index.mjs +1 -1
- package/src/input.mjs +21 -3
- package/src/jev.d.mts +16 -0
- package/src/live/index.d.mts +73 -0
- package/src/live/index.mjs +170 -0
- package/src/live/state.mjs +125 -0
- package/src/live/validation.mjs +122 -0
- package/src/probabilities.mjs +6 -3
- package/src/providers/cloudflare-images.mjs +5 -67
- package/src/providers/cloudflare-media.mjs +5 -10
- package/src/providers/cloudflare-preflight.mjs +4 -3
- package/src/providers/cloudflare-profile.mjs +10 -14
- package/src/providers/cloudflare-transport.mjs +12 -5
- package/src/providers/cloudflare-validation.mjs +3 -1
- package/src/providers/decisions-codec.mjs +10 -5
- package/src/providers/decisions-input.mjs +120 -0
- package/src/providers/decisions-validation.mjs +37 -69
- package/src/providers/image-header.mjs +59 -0
- package/src/providers/jev-images.mjs +84 -0
- package/src/providers/jev-preflight.mjs +62 -0
- package/src/providers/jev-profile.mjs +37 -0
- package/src/providers/jev.mjs +32 -13
- package/src/providers/media-binary.mjs +0 -7
- package/src/providers/openai-decisions.mjs +2 -1
- package/src/providers/openai-errors.mjs +23 -0
- package/src/providers/openai-profile.mjs +2 -1
- package/src/questions.mjs +2 -0
- package/src/response-transport.mjs +84 -17
- package/src/validation.mjs +39 -20
- 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 =
|
|
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 =
|
|
84
|
-
const method =
|
|
86
|
+
const field = inputField(request);
|
|
87
|
+
const method = inputMethod(request);
|
|
85
88
|
if (!extension[method]) continue;
|
|
86
89
|
let prepared;
|
|
87
90
|
try {
|
package/src/capabilities.mjs
CHANGED
|
@@ -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:
|
|
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 ?
|
|
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 !== '
|
|
61
|
-
'
|
|
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;
|
package/src/cli/config.mjs
CHANGED
|
@@ -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 === '
|
|
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:
|
|
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
|
}
|
package/src/cli/decisions.mjs
CHANGED
|
@@ -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,
|
package/src/cli/initialize.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
(
|
|
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
|
};
|
package/src/cli/live.mjs
ADDED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
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
|
|
44
|
+
/** Nonblank printable ASCII Gateway name. */
|
|
45
45
|
readonly id: string;
|
|
46
46
|
readonly cacheKey?: string;
|
|
47
|
-
/**
|
|
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
|
-
/**
|
|
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?:
|
|
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?:
|
|
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. */
|