@fayegram-ai/decision-bridge 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/docs/README.md +2 -1
- package/docs/development/ARCHITECTURE.md +4 -2
- package/docs/guides/BROWSER.md +3 -0
- package/docs/guides/GETTING_STARTED.md +1 -1
- package/docs/guides/JEV.md +174 -0
- package/docs/guides/PLATFORMS.md +2 -2
- package/docs/reference/CLI.md +12 -1
- package/docs/reference/CONFIGURATION.md +6 -1
- package/docs/reference/PROVIDERS.md +12 -2
- package/docs/reference/SDK.md +13 -5
- package/docs/reference/SOURCES.md +4 -0
- package/package.json +1 -1
- package/schemas/decision.config.schema.json +15 -1
- package/src/capabilities.mjs +8 -2
- package/src/cli/config-validation.mjs +9 -1
- package/src/cli/config.mjs +15 -2
- package/src/cli/decisions.mjs +8 -0
- package/src/cli/main.mjs +15 -4
- package/src/index.d.mts +24 -5
- package/src/index.mjs +1 -1
- package/src/jev.d.mts +16 -0
- package/src/probabilities.mjs +6 -3
- package/src/providers/cloudflare-images.mjs +2 -58
- package/src/providers/image-header.mjs +59 -0
- package/src/providers/jev-images.mjs +90 -0
- package/src/providers/jev-preflight.mjs +61 -0
- package/src/providers/jev-profile.mjs +37 -0
- package/src/providers/jev.mjs +32 -13
- package/src/questions.mjs +2 -0
- package/src/validation.mjs +13 -6
- package/src/version.mjs +1 -1
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) {
|
|
@@ -47,13 +49,17 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
|
|
|
47
49
|
fileConfig = validateConfig(fileConfig);
|
|
48
50
|
moduleBase = dirname(path);
|
|
49
51
|
}
|
|
50
|
-
const environment =
|
|
52
|
+
const environment = Object.create(null);
|
|
51
53
|
for (const [key, field] of Object.entries(ENV_FIELDS)) {
|
|
52
54
|
if (effectiveEnv[key] === undefined) continue;
|
|
53
55
|
const value = effectiveEnv[key];
|
|
54
56
|
if (NUMERIC.has(field)) check(typeof value === 'string' && /^\d+$/.test(value),
|
|
55
57
|
'Numeric DECISION_* variables must contain decimal integers.', 'CONFIGURATION_ERROR');
|
|
56
|
-
if (field === '
|
|
58
|
+
if (field === 'jevImages' || field === 'jevLongContext') {
|
|
59
|
+
check(value === 'true' || value === 'false', 'Jev DECISION_* flags must be true or false.', 'CONFIGURATION_ERROR');
|
|
60
|
+
environment.dangerZone = { ...(Object.hasOwn(environment, 'dangerZone') ? environment.dangerZone : {}),
|
|
61
|
+
[field === 'jevImages' ? 'images' : 'longContext']: value === 'true' };
|
|
62
|
+
} else if (field === 'rejectIfBusy') {
|
|
57
63
|
check(value === 'true' || value === 'false', 'DECISION_REJECT_IF_BUSY must be true or false.', 'CONFIGURATION_ERROR');
|
|
58
64
|
environment[field] = value === 'true';
|
|
59
65
|
} else environment[field] = field === 'gateway' ? { id: value } : NUMERIC.has(field) ? Number(value) : value;
|
|
@@ -62,6 +68,10 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
|
|
|
62
68
|
// environment-selected provider must not inherit that provider's key/endpoint.
|
|
63
69
|
function mergeProviderLayer(base, layer, layerModuleBase = moduleBase) {
|
|
64
70
|
const next = { ...base };
|
|
71
|
+
const dangerZone = Object.hasOwn(layer, 'dangerZone') ? layer.dangerZone : undefined;
|
|
72
|
+
// Validate before spreading, without filling defaults that would erase a
|
|
73
|
+
// different opt-in from a lower layer.
|
|
74
|
+
if (dangerZone !== undefined) jevDangerZone(dangerZone);
|
|
65
75
|
// File and flag paths have different bases. Equal path text can identify
|
|
66
76
|
// different implementations; compare destinations before changing the base.
|
|
67
77
|
const moduleChanged = layer.module !== undefined && (typeof layer.module !== 'string' ||
|
|
@@ -72,6 +82,9 @@ export async function loadConfig({ configPath, envFile, overrides = {}, cwd = pr
|
|
|
72
82
|
if (changed) for (const key of PROVIDER_FIELDS) delete next[key];
|
|
73
83
|
if (layer.module !== undefined) moduleBase = layerModuleBase;
|
|
74
84
|
return { ...next, ...layer,
|
|
85
|
+
...(dangerZone === undefined ? {} : { dangerZone: {
|
|
86
|
+
...(Object.hasOwn(next, 'dangerZone') ? next.dangerZone : {}), ...dangerZone,
|
|
87
|
+
} }),
|
|
75
88
|
...(layer.gateway === undefined ? {} : { gateway: { ...next.gateway, ...layer.gateway } }) };
|
|
76
89
|
}
|
|
77
90
|
const config = validateConfig(mergeProviderLayer(mergeProviderLayer(
|
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/main.mjs
CHANGED
|
@@ -15,7 +15,8 @@ import { assertDistinctFiles, createWriter, readJson, readJsonLines } from './io
|
|
|
15
15
|
import { mapOrdered } from './ordered.mjs';
|
|
16
16
|
|
|
17
17
|
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'
|
|
18
|
+
const BOOLS = ['help', 'version', 'pretty', 'compact', 'no-auth', 'force', 'native', 'reject-if-busy', 'no-reject-if-busy',
|
|
19
|
+
'jev-images', 'no-jev-images', 'jev-long-context', 'no-jev-long-context'];
|
|
19
20
|
const STRINGS = ['config', 'env-file', 'provider', 'preset', 'model', 'base-url', 'api-key-env', 'timeout-ms',
|
|
20
21
|
'retries', 'max-tokens', 'response-format', 'token-parameter', 'input', 'output', 'concurrency', 'provider-module', 'account-id', 'gateway-id'];
|
|
21
22
|
const FIELD_MAP = { preset: 'preset', provider: 'provider', model: 'model', 'base-url': 'baseURL', 'api-key-env': 'apiKeyEnv',
|
|
@@ -50,6 +51,10 @@ Options:
|
|
|
50
51
|
--timeout-ms N End-to-end evaluation/provider deadline
|
|
51
52
|
--retries N HTTP retries, 0–10; Decisions default 0, others 2
|
|
52
53
|
--native Selected decision provider native format (init/evaluate/validate/batch)
|
|
54
|
+
--jev-images Enable experimental Jev images and box questions
|
|
55
|
+
--no-jev-images Explicitly disable Jev images
|
|
56
|
+
--jev-long-context Enable experimental Jev 262k context
|
|
57
|
+
--no-jev-long-context Explicitly disable Jev long context
|
|
53
58
|
--account-id ID Cloudflare account (otherwise CLOUDFLARE_ACCOUNT_ID)
|
|
54
59
|
--reject-if-busy Unavailable for Clef REST; rejected before inference
|
|
55
60
|
--no-reject-if-busy Explicitly disable Cloudflare capacity rejection
|
|
@@ -95,7 +100,7 @@ function argumentsFor(argv) {
|
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
function overridesFor(values) {
|
|
98
|
-
const overrides =
|
|
103
|
+
const overrides = Object.create(null);
|
|
99
104
|
for (const [flag, field] of Object.entries(FIELD_MAP)) if (values[flag] !== undefined) {
|
|
100
105
|
const value = values[flag];
|
|
101
106
|
if (['timeoutMs', 'maxRetries', 'maxTokens'].includes(field)) {
|
|
@@ -107,6 +112,12 @@ function overridesFor(values) {
|
|
|
107
112
|
check(!(values['reject-if-busy'] && values['no-reject-if-busy']), 'Choose one reject-if-busy flag.', 'USAGE_ERROR');
|
|
108
113
|
if (values['reject-if-busy'] || values['no-reject-if-busy']) overrides.rejectIfBusy = !!values['reject-if-busy'];
|
|
109
114
|
if (values['gateway-id'] !== undefined) overrides.gateway = { id: values['gateway-id'] };
|
|
115
|
+
for (const [flag, field] of [['jev-images', 'images'], ['jev-long-context', 'longContext']]) {
|
|
116
|
+
check(!(values[flag] && values['no-' + flag]), 'Choose one Jev feature flag for each setting.', 'USAGE_ERROR');
|
|
117
|
+
if (values[flag] || values['no-' + flag]) overrides.dangerZone = {
|
|
118
|
+
...(Object.hasOwn(overrides, 'dangerZone') ? overrides.dangerZone : {}), [field]: !!values[flag],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
110
121
|
return overrides;
|
|
111
122
|
}
|
|
112
123
|
|
|
@@ -171,7 +182,7 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
|
|
|
171
182
|
'--native requires a decision provider and init/evaluate/validate/batch.', 'USAGE_ERROR');
|
|
172
183
|
check(!values.native || command === 'init' || values.input || positionals[1],
|
|
173
184
|
'--native requires an explicit input file or -.', 'USAGE_ERROR');
|
|
174
|
-
check(command !== 'models' || !profile,
|
|
185
|
+
check(command !== 'models' || !profile || profile.models,
|
|
175
186
|
'This decision provider does not implement model listing.', 'UNSUPPORTED_OPERATION');
|
|
176
187
|
activeEnv = loaded.env;
|
|
177
188
|
explicitSecret = loaded.config.apiKeyEnv ? activeEnv[loaded.config.apiKeyEnv] : undefined;
|
|
@@ -201,7 +212,7 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
|
|
|
201
212
|
if (profile && command !== 'schema') return profile.common(raw, profile.options(loaded.config));
|
|
202
213
|
const request = validateRequest(raw, raw?.model ?? loaded.config.model ?? 'validation-only');
|
|
203
214
|
if (!isModule(loaded.config)) assertCapabilities(request,
|
|
204
|
-
builtInCapabilities(loaded.config.provider, request.model));
|
|
215
|
+
builtInCapabilities(loaded.config.provider, request.model, loaded.config));
|
|
205
216
|
return request;
|
|
206
217
|
};
|
|
207
218
|
if (command === 'init') {
|
package/src/index.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** JSON values are read-only because the bridge snapshots/freezes its boundaries. */
|
|
2
|
+
import type { JevDangerZone, JevQuestions, JevDecisionRequest, JevDecisionResponse, JevTransportMetadata } from './jev.d.mts';
|
|
3
|
+
export type { JevDangerZone, JevImageAsset, JevQuestions, JevDecisionRequest, JevDecisionResponse, JevTransportMetadata } from './jev.d.mts';
|
|
2
4
|
import type { NativeDecisionRequest, OpenAIDecisionResponse, OpenAIDecisionUsage,
|
|
3
5
|
OpenAIDecisionsMetadata, OpenAIDecisionsTransportMetadata } from './decisions.d.mts';
|
|
4
6
|
export type { NativeDecisionInputPart, NativeDecisionMessage, NativeDecisionQuestion, NativeDecisionRequest,
|
|
@@ -25,6 +27,15 @@ export interface NoulQuestion {
|
|
|
25
27
|
readonly type: 'noul'; readonly instructions: Instructions;
|
|
26
28
|
readonly criteria?: { readonly true?: Instructions; readonly false?: Instructions };
|
|
27
29
|
}
|
|
30
|
+
export interface BoxQuestion {
|
|
31
|
+
readonly type: 'box'; readonly instructions: Instructions; readonly criteria?: never;
|
|
32
|
+
}
|
|
33
|
+
export interface BoxCoordinates {
|
|
34
|
+
readonly top: number; readonly bottom: number; readonly left: number; readonly right: number;
|
|
35
|
+
}
|
|
36
|
+
export interface BoxAnswer {
|
|
37
|
+
readonly type: 'box'; readonly box: BoxCoordinates; readonly confidence?: number;
|
|
38
|
+
}
|
|
28
39
|
export interface ScoreQuestion<C extends ScoreCriteria = ScoreCriteria> {
|
|
29
40
|
readonly type: 'score'; readonly instructions: Instructions; readonly criteria: C;
|
|
30
41
|
readonly levels?: never;
|
|
@@ -45,7 +56,7 @@ export type DecisionInputPart = { readonly type: 'text'; readonly text: string }
|
|
|
45
56
|
| { readonly type: 'audio'; readonly dataUrl: string }
|
|
46
57
|
| { readonly type: 'video'; readonly dataUrl: string };
|
|
47
58
|
export type OrderedInput = readonly DecisionInputPart[];
|
|
48
|
-
export type Question = ChoiceQuestion | TypedChoiceQuestion | NoulQuestion | ScoreQuestion | LabeledScoreQuestion;
|
|
59
|
+
export type Question = ChoiceQuestion | TypedChoiceQuestion | NoulQuestion | ScoreQuestion | LabeledScoreQuestion | BoxQuestion;
|
|
49
60
|
export type Questions = Readonly<Record<string, Question>>;
|
|
50
61
|
export interface ChoiceAnswer<K extends string = string> {
|
|
51
62
|
readonly type: 'choice'; readonly choice: K;
|
|
@@ -71,7 +82,7 @@ type ChoiceLabel<K> = K extends string ? K : K extends number ? `${K}` : never;
|
|
|
71
82
|
export type AnswerFor<Q extends Question> = Q extends TypedChoiceQuestion<infer O> ? TypedChoiceAnswer<O[number]['value']>
|
|
72
83
|
: Q extends LabeledScoreQuestion<infer L> ? LabeledScoreAnswer<L>
|
|
73
84
|
: Q extends ChoiceQuestion<infer C>
|
|
74
|
-
? ChoiceAnswer<ChoiceLabel<keyof C>> : Q extends NoulQuestion ? NoulAnswer : ScoreAnswer;
|
|
85
|
+
? ChoiceAnswer<ChoiceLabel<keyof C>> : Q extends NoulQuestion ? NoulAnswer : Q extends BoxQuestion ? BoxAnswer : ScoreAnswer;
|
|
75
86
|
export type EvaluationRequest<Q extends Questions = Questions> = {
|
|
76
87
|
readonly questions: Q; readonly model?: string; readonly safetyIdentifier?: string | null;
|
|
77
88
|
} & ({ readonly state: State; readonly input?: never } | { readonly input: OrderedInput; readonly state?: never });
|
|
@@ -110,7 +121,7 @@ export interface ModelList {
|
|
|
110
121
|
readonly release_date?: string; readonly [key: string]: unknown }[];
|
|
111
122
|
readonly has_more?: boolean; readonly last_id?: string;
|
|
112
123
|
}
|
|
113
|
-
export interface ResultValidationOptions { readonly native?: boolean; readonly probabilityDecimalPlaces?: number; }
|
|
124
|
+
export interface ResultValidationOptions { readonly native?: boolean; readonly nullableUsage?: boolean; readonly probabilityDecimalPlaces?: number; }
|
|
114
125
|
export type ProviderKind = 'decision' | 'generative';
|
|
115
126
|
export interface Provider {
|
|
116
127
|
readonly kind: ProviderKind;
|
|
@@ -125,6 +136,7 @@ export interface Provider {
|
|
|
125
136
|
}
|
|
126
137
|
export interface DecisionProvider extends Provider { readonly kind: 'decision'; }
|
|
127
138
|
export interface Capabilities {
|
|
139
|
+
readonly boxes?: boolean;
|
|
128
140
|
readonly audio?: boolean; readonly videos?: boolean;
|
|
129
141
|
readonly maxAudioClips?: number | null; readonly maxVideos?: number | null;
|
|
130
142
|
readonly supported: boolean; readonly orderedInput: boolean; readonly images: boolean;
|
|
@@ -146,7 +158,7 @@ export interface TransportOptions {
|
|
|
146
158
|
timeoutMs?: number; maxRetries?: number; maxRequestBytes?: number; maxResponseBytes?: number;
|
|
147
159
|
fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
148
160
|
}
|
|
149
|
-
export interface JevOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; }
|
|
161
|
+
export interface JevOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; dangerZone?: JevDangerZone; }
|
|
150
162
|
export interface OpenAIDecisionsOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; }
|
|
151
163
|
export interface Bridge {
|
|
152
164
|
readonly provider: string; readonly providerKind: ProviderKind; readonly defaultModel?: string;
|
|
@@ -166,6 +178,10 @@ export class JevProvider implements DecisionProvider {
|
|
|
166
178
|
evaluate<const Q extends Questions>(request: EvaluationRequest<Q>, options?: ProviderCallOptions): Promise<ProviderResult<Q>>;
|
|
167
179
|
listModels(options?: ProviderCallOptions): Promise<ModelList>;
|
|
168
180
|
getCapabilities(model?: string): Capabilities;
|
|
181
|
+
getResultValidationOptions(model?: string): ResultValidationOptions;
|
|
182
|
+
validateRequest(request: ResolvedRequest): void;
|
|
183
|
+
createDecision<const Q extends JevQuestions>(request: JevDecisionRequest<Q>, options?: ProviderCallOptions):
|
|
184
|
+
Promise<{ readonly decision: JevDecisionResponse<Q>; readonly meta: JevTransportMetadata }>;
|
|
169
185
|
}
|
|
170
186
|
export class OpenAIDecisionsProvider implements DecisionProvider {
|
|
171
187
|
readonly kind: 'decision'; readonly name: 'openai-decisions'; readonly probabilitySource: 'native'; readonly defaultModel: string;
|
|
@@ -187,10 +203,12 @@ export class CloudflareDecisionsProvider implements DecisionProvider {
|
|
|
187
203
|
Promise<{ readonly decision: CloudflareDecisionResponse; readonly meta: CloudflareTransportMetadata }>;
|
|
188
204
|
}
|
|
189
205
|
export type CloudflareDecisionsConfig = { type: 'cloudflare-decisions' } & CloudflareDecisionsOptions;
|
|
190
|
-
export type
|
|
206
|
+
export type JevConfig = { type: 'jev' } & JevOptions;
|
|
207
|
+
export type ProviderConfig = JevConfig
|
|
191
208
|
| ({ type: 'openai-decisions' } & OpenAIDecisionsOptions)
|
|
192
209
|
| CloudflareDecisionsConfig;
|
|
193
210
|
export type OpenAIDecisionsConfig = { type: 'openai-decisions' } & OpenAIDecisionsOptions;
|
|
211
|
+
export function createProvider(options: JevConfig): JevProvider;
|
|
194
212
|
export function createProvider(options: OpenAIDecisionsConfig): OpenAIDecisionsProvider;
|
|
195
213
|
export function createProvider(options: CloudflareDecisionsConfig): CloudflareDecisionsProvider;
|
|
196
214
|
export function createProvider(options: ProviderConfig | OpenAIDecisionsConfig): DecisionProvider;
|
|
@@ -200,6 +218,7 @@ export function choice<const C extends ChoiceCriteria>(instructions: Instruction
|
|
|
200
218
|
export function noul(instructions: Instructions, criteria?: NoulQuestion['criteria']): NoulQuestion;
|
|
201
219
|
export function score<const C extends ScoreCriteria>(instructions: Instructions, criteria: C): ScoreQuestion<C>;
|
|
202
220
|
export function scoreLevels<const L extends ScoreLevels>(instructions: Instructions, levels: L): LabeledScoreQuestion<L>;
|
|
221
|
+
export function box(instructions: Instructions): BoxQuestion;
|
|
203
222
|
export interface ChoicePolicyOptions<K extends string | boolean = string> {
|
|
204
223
|
minProbability: number; minMargin?: number; abstainOptions?: readonly K[];
|
|
205
224
|
/** Provider-declared probability precision; omission retains the baseline tolerance. */
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createBridge } from './bridge.mjs';
|
|
2
2
|
export { JevProvider, OpenAIDecisionsProvider, CloudflareDecisionsProvider, createProvider } from './providers/index.mjs';
|
|
3
|
-
export { choice, noul, score, scoreLevels } from './questions.mjs';
|
|
3
|
+
export { choice, noul, score, scoreLevels, box } from './questions.mjs';
|
|
4
4
|
export { decideChoice } from './policy.mjs';
|
|
5
5
|
export { evaluateBatch } from './batch.mjs';
|
|
6
6
|
export { DecisionError, publicError } from './errors.mjs';
|
package/src/jev.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Native Jev JSON assets; file reading and image preparation remain caller-owned. */
|
|
2
|
+
import type { State, ChoiceQuestion, NoulQuestion, ScoreQuestion, BoxQuestion, ProviderResult } from './index.d.mts';
|
|
3
|
+
|
|
4
|
+
export interface JevDangerZone { readonly images?: boolean; readonly longContext?: boolean; }
|
|
5
|
+
export interface JevImageAsset {
|
|
6
|
+
readonly type?: 'image'; readonly format: 'png' | 'jpg' | 'jpeg'; readonly data_base64: string;
|
|
7
|
+
}
|
|
8
|
+
export type JevQuestions = Readonly<Record<string, ChoiceQuestion | NoulQuestion | ScoreQuestion | BoxQuestion>>;
|
|
9
|
+
export interface JevDecisionRequest<Q extends JevQuestions = JevQuestions> {
|
|
10
|
+
readonly state: State; readonly questions: Q; readonly model?: string;
|
|
11
|
+
readonly assets?: Readonly<Record<string, JevImageAsset>>;
|
|
12
|
+
}
|
|
13
|
+
export type JevDecisionResponse<Q extends JevQuestions = JevQuestions> = Omit<ProviderResult<Q>, 'meta'>;
|
|
14
|
+
export interface JevTransportMetadata {
|
|
15
|
+
readonly requestedModel: string; readonly durationMs: number; readonly attempts: number; readonly requestId?: string;
|
|
16
|
+
}
|
package/src/probabilities.mjs
CHANGED
|
@@ -6,14 +6,17 @@ export function validateResultOptions(options) {
|
|
|
6
6
|
[Object.prototype, null].includes(Object.getPrototypeOf(options)) &&
|
|
7
7
|
Object.getOwnPropertySymbols(options).length === 0 &&
|
|
8
8
|
Object.entries(Object.getOwnPropertyDescriptors(options)).every(([key, descriptor]) =>
|
|
9
|
-
['native', 'probabilityDecimalPlaces'].includes(key) && descriptor.enumerable && 'value' in descriptor),
|
|
9
|
+
['native', 'nullableUsage', 'probabilityDecimalPlaces'].includes(key) && descriptor.enumerable && 'value' in descriptor),
|
|
10
10
|
'Result-validation options contain an unknown field or accessor.', 'CONFIGURATION_ERROR');
|
|
11
|
-
|
|
11
|
+
// Only own data fields select policy; inherited values and getters are not settings.
|
|
12
|
+
const { native = false, nullableUsage = false, probabilityDecimalPlaces } = Object.assign(Object.create(null), options);
|
|
12
13
|
check(typeof native === 'boolean', 'native must be boolean.', 'CONFIGURATION_ERROR');
|
|
14
|
+
check(typeof nullableUsage === 'boolean', 'nullableUsage must be boolean.', 'CONFIGURATION_ERROR');
|
|
13
15
|
check(probabilityDecimalPlaces === undefined || (Number.isSafeInteger(probabilityDecimalPlaces) &&
|
|
14
16
|
probabilityDecimalPlaces >= 1 && probabilityDecimalPlaces <= 15),
|
|
15
17
|
'probabilityDecimalPlaces must be an integer from 1 to 15.', 'CONFIGURATION_ERROR');
|
|
16
|
-
|
|
18
|
+
// Keep even an omitted precision as an own value when callers read this policy again.
|
|
19
|
+
return { native, nullableUsage, probabilityDecimalPlaces };
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/** Preserve native numbers. Only grid-aligned values receive the rounding budget. */
|
|
@@ -1,65 +1,9 @@
|
|
|
1
1
|
import { requireCondition as check } from '../errors.mjs';
|
|
2
2
|
import { validateImage } from '../input.mjs';
|
|
3
3
|
import { isRecord } from '../validation.mjs';
|
|
4
|
+
import { imageDimensions } from './image-header.mjs';
|
|
4
5
|
|
|
5
6
|
const FORMATS = ['image/png', 'image/jpeg', 'image/webp'];
|
|
6
|
-
const checkImage = condition => check(condition, 'Invalid embedded image header or dimensions.', 'VALIDATION_ERROR');
|
|
7
|
-
const ascii = (bytes, start, length) => String.fromCharCode(...bytes.subarray(start, start + length));
|
|
8
|
-
|
|
9
|
-
function dimensions(bytes, format) {
|
|
10
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
11
|
-
if (format === 'image/png') {
|
|
12
|
-
checkImage(bytes.length >= 33 && [137, 80, 78, 71, 13, 10, 26, 10].every((value, i) => bytes[i] === value) &&
|
|
13
|
-
view.getUint32(8) === 13 && ascii(bytes, 12, 4) === 'IHDR');
|
|
14
|
-
return [view.getUint32(16), view.getUint32(20)];
|
|
15
|
-
}
|
|
16
|
-
if (format === 'image/jpeg') {
|
|
17
|
-
checkImage(bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216);
|
|
18
|
-
let offset = 2;
|
|
19
|
-
while (offset < bytes.length) {
|
|
20
|
-
checkImage(bytes[offset++] === 255);
|
|
21
|
-
while (offset < bytes.length && bytes[offset] === 255) offset++;
|
|
22
|
-
checkImage(offset < bytes.length);
|
|
23
|
-
const marker = bytes[offset++];
|
|
24
|
-
if (marker === 217 || marker === 218) break;
|
|
25
|
-
if (marker === 1 || (marker >= 208 && marker <= 215)) continue;
|
|
26
|
-
checkImage(offset + 2 <= bytes.length);
|
|
27
|
-
const length = view.getUint16(offset);
|
|
28
|
-
checkImage(length >= 2 && offset + length <= bytes.length);
|
|
29
|
-
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
|
|
30
|
-
checkImage(length >= 8 && bytes[offset + 7] > 0 && length >= 8 + 3 * bytes[offset + 7]);
|
|
31
|
-
return [view.getUint16(offset + 5), view.getUint16(offset + 3)];
|
|
32
|
-
}
|
|
33
|
-
offset += length;
|
|
34
|
-
}
|
|
35
|
-
checkImage(false);
|
|
36
|
-
}
|
|
37
|
-
checkImage(bytes.length >= 20 && ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WEBP' &&
|
|
38
|
-
view.getUint32(4, true) + 8 === bytes.length);
|
|
39
|
-
const little24 = offset => bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16;
|
|
40
|
-
for (let offset = 12; offset + 8 <= bytes.length;) {
|
|
41
|
-
const kind = ascii(bytes, offset, 4);
|
|
42
|
-
const length = view.getUint32(offset + 4, true);
|
|
43
|
-
const start = offset + 8;
|
|
44
|
-
checkImage(start + length <= bytes.length);
|
|
45
|
-
if (kind === 'VP8X') {
|
|
46
|
-
checkImage(length === 10);
|
|
47
|
-
return [little24(start + 4) + 1, little24(start + 7) + 1];
|
|
48
|
-
}
|
|
49
|
-
if (kind === 'VP8L') {
|
|
50
|
-
checkImage(length >= 5 && bytes[start] === 47 && (bytes[start + 4] >> 5) === 0);
|
|
51
|
-
return [1 + (bytes[start + 1] | (bytes[start + 2] & 63) << 8),
|
|
52
|
-
1 + (bytes[start + 2] >> 6 | bytes[start + 3] << 2 | (bytes[start + 4] & 15) << 10)];
|
|
53
|
-
}
|
|
54
|
-
if (kind === 'VP8 ') {
|
|
55
|
-
checkImage(length >= 10 && ascii(bytes, start + 3, 3) === '\x9d\x01\x2a');
|
|
56
|
-
return [view.getUint16(start + 6, true) & 16383, view.getUint16(start + 8, true) & 16383];
|
|
57
|
-
}
|
|
58
|
-
offset = start + length + length % 2;
|
|
59
|
-
}
|
|
60
|
-
checkImage(false);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
7
|
/** Inspect bounded embedded bytes; this is header validation, not full image decoding. */
|
|
64
8
|
export function validateCloudflareImages(images) {
|
|
65
9
|
check(Array.isArray(images) && images.length <= 4, 'Cloudflare accepts at most four images.');
|
|
@@ -88,7 +32,7 @@ export function validateCloudflareImages(images) {
|
|
|
88
32
|
const decoded = atob(payload);
|
|
89
33
|
const bytes = new Uint8Array(decoded.length);
|
|
90
34
|
for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
|
|
91
|
-
const [width, height] =
|
|
35
|
+
const [width, height] = imageDimensions(bytes, format);
|
|
92
36
|
check(width > 0 && height > 0 && width * height <= 16000000,
|
|
93
37
|
'Cloudflare images must have positive dimensions within 16 million pixels.');
|
|
94
38
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Shared bounded image-header inspection; full decoding remains the service boundary.
|
|
2
|
+
import { requireCondition as check } from '../errors.mjs';
|
|
3
|
+
|
|
4
|
+
const checkImage = condition => check(condition, 'Invalid embedded image header or dimensions.', 'VALIDATION_ERROR');
|
|
5
|
+
const ascii = (bytes, start, length) => String.fromCharCode(...bytes.subarray(start, start + length));
|
|
6
|
+
|
|
7
|
+
export function imageDimensions(bytes, format) {
|
|
8
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
9
|
+
if (format === 'image/png') {
|
|
10
|
+
checkImage(bytes.length >= 33 && [137, 80, 78, 71, 13, 10, 26, 10].every((value, i) => bytes[i] === value) &&
|
|
11
|
+
view.getUint32(8) === 13 && ascii(bytes, 12, 4) === 'IHDR');
|
|
12
|
+
return [view.getUint32(16), view.getUint32(20)];
|
|
13
|
+
}
|
|
14
|
+
if (format === 'image/jpeg') {
|
|
15
|
+
checkImage(bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216);
|
|
16
|
+
let offset = 2;
|
|
17
|
+
while (offset < bytes.length) {
|
|
18
|
+
checkImage(bytes[offset++] === 255);
|
|
19
|
+
while (offset < bytes.length && bytes[offset] === 255) offset++;
|
|
20
|
+
checkImage(offset < bytes.length);
|
|
21
|
+
const marker = bytes[offset++];
|
|
22
|
+
if (marker === 217 || marker === 218) break;
|
|
23
|
+
if (marker === 1 || (marker >= 208 && marker <= 215)) continue;
|
|
24
|
+
checkImage(offset + 2 <= bytes.length);
|
|
25
|
+
const length = view.getUint16(offset);
|
|
26
|
+
checkImage(length >= 2 && offset + length <= bytes.length);
|
|
27
|
+
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
|
|
28
|
+
checkImage(length >= 8 && bytes[offset + 7] > 0 && length >= 8 + 3 * bytes[offset + 7]);
|
|
29
|
+
return [view.getUint16(offset + 5), view.getUint16(offset + 3)];
|
|
30
|
+
}
|
|
31
|
+
offset += length;
|
|
32
|
+
}
|
|
33
|
+
checkImage(false);
|
|
34
|
+
}
|
|
35
|
+
checkImage(bytes.length >= 20 && ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WEBP' &&
|
|
36
|
+
view.getUint32(4, true) + 8 === bytes.length);
|
|
37
|
+
const little24 = offset => bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16;
|
|
38
|
+
for (let offset = 12; offset + 8 <= bytes.length;) {
|
|
39
|
+
const kind = ascii(bytes, offset, 4);
|
|
40
|
+
const length = view.getUint32(offset + 4, true);
|
|
41
|
+
const start = offset + 8;
|
|
42
|
+
checkImage(start + length <= bytes.length);
|
|
43
|
+
if (kind === 'VP8X') {
|
|
44
|
+
checkImage(length === 10);
|
|
45
|
+
return [little24(start + 4) + 1, little24(start + 7) + 1];
|
|
46
|
+
}
|
|
47
|
+
if (kind === 'VP8L') {
|
|
48
|
+
checkImage(length >= 5 && bytes[start] === 47 && (bytes[start + 4] >> 5) === 0);
|
|
49
|
+
return [1 + (bytes[start + 1] | (bytes[start + 2] & 63) << 8),
|
|
50
|
+
1 + (bytes[start + 2] >> 6 | bytes[start + 3] << 2 | (bytes[start + 4] & 15) << 10)];
|
|
51
|
+
}
|
|
52
|
+
if (kind === 'VP8 ') {
|
|
53
|
+
checkImage(length >= 10 && ascii(bytes, start + 3, 3) === '\x9d\x01\x2a');
|
|
54
|
+
return [view.getUint16(start + 6, true) & 16383, view.getUint16(start + 8, true) & 16383];
|
|
55
|
+
}
|
|
56
|
+
offset = start + length + length % 2;
|
|
57
|
+
}
|
|
58
|
+
checkImage(false);
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Jev JSON assets use reference substitution in string values, never object keys.
|
|
2
|
+
import { requireCondition as check } from '../errors.mjs';
|
|
3
|
+
import { base64ByteLength } from '../embedded.mjs';
|
|
4
|
+
import { isRecord } from '../validation.mjs';
|
|
5
|
+
import { imageDimensions } from './image-header.mjs';
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
|
|
9
|
+
function inspectImage(asset) {
|
|
10
|
+
check(isRecord(asset) && Object.keys(asset).every(key => ['type', 'format', 'data_base64'].includes(key)) &&
|
|
11
|
+
(asset.type === undefined || asset.type === 'image') && ['png', 'jpg', 'jpeg'].includes(asset.format),
|
|
12
|
+
'Jev JSON image assets require PNG/JPEG format and data_base64.');
|
|
13
|
+
const size = base64ByteLength(asset.data_base64);
|
|
14
|
+
check(size <= 16 * 1024 * 1024, 'Jev images must be within 16 MiB each.', 'REQUEST_TOO_LARGE');
|
|
15
|
+
const decoded = atob(asset.data_base64);
|
|
16
|
+
// A string iterator makes Uint8Array.from allocate an intermediate element
|
|
17
|
+
// list. Copy directly into one bounded byte buffer for multi-MiB images.
|
|
18
|
+
const bytes = new Uint8Array(decoded.length);
|
|
19
|
+
for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
|
|
20
|
+
const [width, height] = imageDimensions(bytes, asset.format === 'png' ? 'image/png' : 'image/jpeg');
|
|
21
|
+
check(width >= 256 && width <= 1568 && height >= 256 && height <= 1568 &&
|
|
22
|
+
width * height >= 78400 && width * height <= 2458624,
|
|
23
|
+
'Jev image dimensions must be 256–1568 pixels with an area of 78400–2458624 pixels.');
|
|
24
|
+
if (asset.format === 'png') {
|
|
25
|
+
const view = new DataView(bytes.buffer);
|
|
26
|
+
for (let offset = 8; offset < bytes.length;) {
|
|
27
|
+
check(offset + 12 <= bytes.length, 'Invalid PNG chunk.');
|
|
28
|
+
const length = view.getUint32(offset);
|
|
29
|
+
check(offset + length + 12 <= bytes.length, 'Invalid PNG chunk length.');
|
|
30
|
+
check(String.fromCharCode(...bytes.subarray(offset + 4, offset + 8)) !== 'acTL',
|
|
31
|
+
'Jev does not accept animated PNG images.');
|
|
32
|
+
offset += length + 12;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return size;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Apply TypeSafe's JSON nesting boundary and collect only replaceable values. */
|
|
39
|
+
function stringValues(value, output, depth = 0) {
|
|
40
|
+
check(depth <= 64, 'Jev state, instructions, and criteria must not exceed 64 nested levels.');
|
|
41
|
+
if (typeof value === 'string') output.push(value);
|
|
42
|
+
else if (value !== null && typeof value === 'object') {
|
|
43
|
+
for (const child of Object.values(value)) stringValues(child, output, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whole values win first; other matches are consumed left to right. Alternatives
|
|
48
|
+
* at the same position retain asset-map order, as on the native TypeSafe API.
|
|
49
|
+
*/
|
|
50
|
+
function usedReferences(strings, references) {
|
|
51
|
+
const known = new Set(references);
|
|
52
|
+
const used = new Set();
|
|
53
|
+
if (!references.length) return used;
|
|
54
|
+
const pattern = new RegExp(references.map(reference => reference.replace(/[.*+?^$(){}|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
55
|
+
for (const value of strings) {
|
|
56
|
+
if (known.has(value)) used.add(value);
|
|
57
|
+
else for (const match of value.matchAll(pattern)) used.add(match[0]);
|
|
58
|
+
}
|
|
59
|
+
return used;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateJevAssets(request) {
|
|
63
|
+
const strings = [];
|
|
64
|
+
stringValues(request.state, strings);
|
|
65
|
+
for (const question of Object.values(request.questions)) {
|
|
66
|
+
stringValues(question.instructions, strings);
|
|
67
|
+
if (question.criteria !== undefined) stringValues(question.criteria, strings);
|
|
68
|
+
}
|
|
69
|
+
const assets = request.assets === undefined ? {} : request.assets;
|
|
70
|
+
check(isRecord(assets), 'Jev assets must be an object.');
|
|
71
|
+
const references = Object.keys(assets);
|
|
72
|
+
let referenceBytes = 0;
|
|
73
|
+
// Bound the literal pattern before compiling it or decoding any images.
|
|
74
|
+
for (const reference of references) {
|
|
75
|
+
const size = encoder.encode(reference).length;
|
|
76
|
+
check(reference.trim() && size <= 1024, 'Jev asset references must be nonempty and within 1024 UTF-8 bytes.');
|
|
77
|
+
referenceBytes += size;
|
|
78
|
+
check(referenceBytes <= 65536, 'Jev asset references exceed 65536 UTF-8 bytes.', 'REQUEST_TOO_LARGE');
|
|
79
|
+
}
|
|
80
|
+
const used = usedReferences(strings, references);
|
|
81
|
+
let imageBytes = 0;
|
|
82
|
+
for (const reference of references) {
|
|
83
|
+
check(used.has(reference), 'Every Jev asset must be used by reference substitution in a string value.');
|
|
84
|
+
imageBytes += inspectImage(assets[reference]);
|
|
85
|
+
check(imageBytes <= 32 * 1024 * 1024, 'Jev images exceed 32 MiB total.', 'REQUEST_TOO_LARGE');
|
|
86
|
+
}
|
|
87
|
+
// Server re-encoding, expanded references, full image decoding and token limits
|
|
88
|
+
// remain authoritative; original file sizes cannot predict those exactly.
|
|
89
|
+
return references.length;
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Shared pure preflight keeps offline validation and inference on the same path.
|
|
2
|
+
import { requireCondition as check } from '../errors.mjs';
|
|
3
|
+
import { cloneJson, freezeJson, isRecord, validateRequest } from '../validation.mjs';
|
|
4
|
+
import { assertCapabilities, builtInCapabilities } from '../capabilities.mjs';
|
|
5
|
+
import { embeddedDataUrl } from '../embedded.mjs';
|
|
6
|
+
import { serializeRequestBody } from '../transport.mjs';
|
|
7
|
+
import { jevDangerZone, jevRequestLimit } from './jev-profile.mjs';
|
|
8
|
+
import { validateJevAssets } from './jev-images.mjs';
|
|
9
|
+
|
|
10
|
+
/** Convert ordered parts to an ordered state array, without altering question text. */
|
|
11
|
+
export function encodeJevDecision(request) {
|
|
12
|
+
if (!Object.hasOwn(request, 'input')) return request;
|
|
13
|
+
const text = JSON.stringify(request);
|
|
14
|
+
// Look ahead without consuming a token: two user tokens can share their
|
|
15
|
+
// boundary underscores, and both must be reserved before choosing a key.
|
|
16
|
+
const references = new Set(Array.from(text.matchAll(/(?=(__decision_bridge_image_\d+__))/g), match => match[1]));
|
|
17
|
+
const assets = {};
|
|
18
|
+
const state = request.input.map((part, index) => {
|
|
19
|
+
if (part.type === 'text') return part.text;
|
|
20
|
+
const image = embeddedDataUrl(part.dataUrl, 'image');
|
|
21
|
+
check(['image/png', 'image/jpeg'].includes(image.format), 'Jev accepts only embedded PNG or JPEG images.');
|
|
22
|
+
check(part.detail === undefined || part.detail === null || part.detail === 'auto',
|
|
23
|
+
'Jev does not support image detail controls.', 'UNSUPPORTED_CAPABILITY');
|
|
24
|
+
let suffix = index;
|
|
25
|
+
let reference = `__decision_bridge_image_${suffix}__`;
|
|
26
|
+
while (references.has(reference)) reference = `__decision_bridge_image_${++suffix}__`;
|
|
27
|
+
references.add(reference);
|
|
28
|
+
assets[reference] = { type: 'image', format: image.format === 'image/png' ? 'png' : 'jpg', data_base64: image.payload };
|
|
29
|
+
return reference;
|
|
30
|
+
});
|
|
31
|
+
return { model: request.model, state, questions: request.questions,
|
|
32
|
+
...(Object.keys(assets).length ? { assets } : {}) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function prepareJevDecision(input, options = {}) {
|
|
36
|
+
const snapshot = cloneJson(input, 'Jev request');
|
|
37
|
+
check(isRecord(snapshot) && Object.keys(snapshot).every(key => ['model', 'state', 'questions', 'assets'].includes(key)),
|
|
38
|
+
'Jev native requests require state, questions, optional model and assets.');
|
|
39
|
+
const { assets, ...common } = snapshot;
|
|
40
|
+
const request = validateRequest(common, options.model ?? 'jev-latest');
|
|
41
|
+
const dangerZone = jevDangerZone(options.dangerZone);
|
|
42
|
+
assertCapabilities(request, builtInCapabilities('jev', request.model, { dangerZone }));
|
|
43
|
+
const native = { ...request, ...(assets === undefined ? {} : { assets }) };
|
|
44
|
+
const imageCount = validateJevAssets(native);
|
|
45
|
+
if (imageCount) check(dangerZone.images, 'Enable Jev dangerZone.images to send images.', 'UNSUPPORTED_CAPABILITY');
|
|
46
|
+
if (Object.values(request.questions).some(question => question.type === 'box')) {
|
|
47
|
+
check(imageCount > 0, 'Jev box questions require at least one image asset.');
|
|
48
|
+
}
|
|
49
|
+
serializeRequestBody(native, jevRequestLimit(options.maxRequestBytes));
|
|
50
|
+
return { request: freezeJson(request), native: freezeJson(native) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function prepareCommonJevDecision(input, options = {}) {
|
|
54
|
+
const request = validateRequest(input, options.model ?? 'jev-latest');
|
|
55
|
+
assertCapabilities(request, builtInCapabilities('jev', request.model, { dangerZone: jevDangerZone(options.dangerZone) }));
|
|
56
|
+
const { native } = prepareJevDecision(encodeJevDecision(request), options);
|
|
57
|
+
return { request: freezeJson(request), native };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const preflightJevDecision = (input, options) => prepareJevDecision(input, options).native;
|
|
61
|
+
export const preflightCommonJevDecision = (input, options) => prepareCommonJevDecision(input, options).request;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { optionsObject } from '../options.mjs';
|
|
2
|
+
import { requireCondition as check } from '../errors.mjs';
|
|
3
|
+
|
|
4
|
+
export const JEV_REQUEST_LIMIT = 32 * 1024 * 1024;
|
|
5
|
+
export const JEV_RESULT_OPTIONS = Object.freeze({ native: true, nullableUsage: true });
|
|
6
|
+
|
|
7
|
+
/** Experimental features are explicit provider policy, never inferred from input. */
|
|
8
|
+
export function jevDangerZone(value = {}) {
|
|
9
|
+
const options = optionsObject(value, ['images', 'longContext'], 'Jev dangerZone');
|
|
10
|
+
const images = Object.hasOwn(options, 'images') ? options.images : false;
|
|
11
|
+
const longContext = Object.hasOwn(options, 'longContext') ? options.longContext : false;
|
|
12
|
+
check([images, longContext].every(value => value === undefined || typeof value === 'boolean'),
|
|
13
|
+
'Jev dangerZone flags must be boolean.', 'CONFIGURATION_ERROR');
|
|
14
|
+
return Object.freeze({ images: images ?? false, longContext: longContext ?? false });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Moving stable aliases stay conservative; pin a supported version for boxes. */
|
|
18
|
+
export function jevFeatures(model, dangerZone = {}) {
|
|
19
|
+
const version = /^jev-(\d+)\.(\d+)\.(\d+)$/.exec(model ?? '');
|
|
20
|
+
const boxes = model === 'jev-preview' || (version !== null &&
|
|
21
|
+
(Number(version[1]) > 1 || (Number(version[1]) === 1 && Number(version[2]) >= 14)));
|
|
22
|
+
const images = Object.hasOwn(dangerZone, 'images') && dangerZone.images === true;
|
|
23
|
+
return { orderedInput: true, images, boxes: images && boxes, maxImages: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function jevHeaders(dangerZone) {
|
|
27
|
+
return {
|
|
28
|
+
...(dangerZone.images ? { 'TypeSafe-Danger-Zone-Images': 'true' } : {}),
|
|
29
|
+
...(dangerZone.longContext ? { 'TypeSafe-Danger-Zone-Long-Context': 'true' } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function jevRequestLimit(value = JEV_REQUEST_LIMIT) {
|
|
34
|
+
check(Number.isSafeInteger(value) && value > 0 && value <= JEV_REQUEST_LIMIT,
|
|
35
|
+
'Jev maxRequestBytes must be within 1 to 32 MiB.', 'CONFIGURATION_ERROR');
|
|
36
|
+
return value;
|
|
37
|
+
}
|