@fayegram-ai/decision-bridge 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +20 -14
- package/docs/README.md +5 -4
- package/docs/development/ARCHITECTURE.md +26 -3
- package/docs/development/DESIGN.md +11 -0
- package/docs/development/TESTING.md +23 -1
- package/docs/guides/BROWSER.md +3 -0
- package/docs/guides/CLOUDFLARE_DECISIONS.md +2 -2
- package/docs/guides/CUSTOM_PROVIDERS.md +4 -2
- package/docs/guides/GETTING_STARTED.md +1 -1
- package/docs/guides/JEV.md +174 -0
- package/docs/guides/OPENAI_DECISIONS.md +46 -4
- package/docs/guides/PLATFORMS.md +2 -2
- package/docs/guides/SWITCHING_MODELS.md +10 -4
- package/docs/guides/TROUBLESHOOTING.md +14 -0
- package/docs/reference/CLI.md +26 -4
- package/docs/reference/CONFIGURATION.md +12 -5
- package/docs/reference/ERRORS.md +5 -2
- package/docs/reference/PROVIDERS.md +20 -7
- package/docs/reference/SDK.md +22 -8
- package/docs/reference/SOURCES.md +4 -0
- package/examples/configs/openai-decisions.json +1 -1
- package/examples/decisions.native.json +1 -1
- package/package.json +1 -1
- package/schemas/decision.config.schema.json +15 -1
- package/src/capabilities.mjs +11 -8
- package/src/cli/config-validation.mjs +9 -1
- package/src/cli/config.mjs +17 -3
- package/src/cli/decisions.mjs +10 -0
- package/src/cli/initialize.mjs +8 -2
- package/src/cli/main.mjs +16 -4
- package/src/decisions.d.mts +5 -0
- package/src/index.d.mts +30 -8
- 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/decisions-preflight.mjs +18 -7
- package/src/providers/decisions-validation.mjs +3 -2
- 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/providers/openai-decisions.mjs +17 -10
- package/src/providers/openai-profile.mjs +24 -0
- package/src/questions.mjs +2 -0
- package/src/validation.mjs +13 -6
- package/src/version.mjs +1 -1
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') {
|
|
@@ -214,6 +225,7 @@ export async function runCli(argv, { stdin = process.stdin, stdout = process.std
|
|
|
214
225
|
? (await import('../custom/generative/schema.mjs')).buildResponseSchema : undefined;
|
|
215
226
|
await writeJson(command === 'schema' ? buildResponseSchema(request.questions)
|
|
216
227
|
: { valid: true, validationScope: isModule(loaded.config) ? 'structure' : 'provider', questionCount: Object.keys(request.questions).length, model: selectedModel,
|
|
228
|
+
...(profile?.resolveModel ? { resolvedModel: profile.resolveModel(request.model) } : {}),
|
|
217
229
|
...(values.native ? { native: true } : {}) });
|
|
218
230
|
} else {
|
|
219
231
|
let request;
|
package/src/decisions.d.mts
CHANGED
|
@@ -39,6 +39,11 @@ export interface OpenAIDecisionResponse {
|
|
|
39
39
|
export interface TransportMetadata {
|
|
40
40
|
readonly attempts: number; readonly durationMs: number; readonly requestId?: string;
|
|
41
41
|
}
|
|
42
|
+
/** Selected model, resolved outgoing ID, and transport diagnostics for native calls. */
|
|
43
|
+
export interface OpenAIDecisionsTransportMetadata extends TransportMetadata {
|
|
44
|
+
readonly requestedModel: string;
|
|
45
|
+
readonly resolvedModel: string;
|
|
46
|
+
}
|
|
42
47
|
export type OpenAIDecisionsMetadata = {
|
|
43
48
|
readonly usage: OpenAIDecisionUsage;
|
|
44
49
|
readonly answers: Readonly<Record<string, { readonly type: 'predicate' | 'choice' | 'score'; readonly confidence?: number }>>;
|
package/src/index.d.mts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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
|
-
OpenAIDecisionsMetadata,
|
|
5
|
+
OpenAIDecisionsMetadata, OpenAIDecisionsTransportMetadata } from './decisions.d.mts';
|
|
4
6
|
export type { NativeDecisionInputPart, NativeDecisionMessage, NativeDecisionQuestion, NativeDecisionRequest,
|
|
5
|
-
NativeDecisionAnswer, OpenAIDecisionResponse, OpenAIDecisionUsage, OpenAIDecisionsMetadata,
|
|
7
|
+
NativeDecisionAnswer, OpenAIDecisionResponse, OpenAIDecisionUsage, OpenAIDecisionsMetadata,
|
|
8
|
+
OpenAIDecisionsTransportMetadata, TransportMetadata } from './decisions.d.mts';
|
|
6
9
|
import type { CloudflareDecisionsOptions, CloudflareDecisionRequest, CloudflareDecisionResponse,
|
|
7
10
|
CloudflareTransportMetadata, CloudflareDecisionsMetadata } from './cloudflare-decisions.d.mts';
|
|
8
11
|
export type { CloudflareDecisionModel, CloudflareDecisionImage, CloudflareDecisionAudio, CloudflareDecisionVideo,
|
|
@@ -24,6 +27,15 @@ export interface NoulQuestion {
|
|
|
24
27
|
readonly type: 'noul'; readonly instructions: Instructions;
|
|
25
28
|
readonly criteria?: { readonly true?: Instructions; readonly false?: Instructions };
|
|
26
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
|
+
}
|
|
27
39
|
export interface ScoreQuestion<C extends ScoreCriteria = ScoreCriteria> {
|
|
28
40
|
readonly type: 'score'; readonly instructions: Instructions; readonly criteria: C;
|
|
29
41
|
readonly levels?: never;
|
|
@@ -44,7 +56,7 @@ export type DecisionInputPart = { readonly type: 'text'; readonly text: string }
|
|
|
44
56
|
| { readonly type: 'audio'; readonly dataUrl: string }
|
|
45
57
|
| { readonly type: 'video'; readonly dataUrl: string };
|
|
46
58
|
export type OrderedInput = readonly DecisionInputPart[];
|
|
47
|
-
export type Question = ChoiceQuestion | TypedChoiceQuestion | NoulQuestion | ScoreQuestion | LabeledScoreQuestion;
|
|
59
|
+
export type Question = ChoiceQuestion | TypedChoiceQuestion | NoulQuestion | ScoreQuestion | LabeledScoreQuestion | BoxQuestion;
|
|
48
60
|
export type Questions = Readonly<Record<string, Question>>;
|
|
49
61
|
export interface ChoiceAnswer<K extends string = string> {
|
|
50
62
|
readonly type: 'choice'; readonly choice: K;
|
|
@@ -70,7 +82,7 @@ type ChoiceLabel<K> = K extends string ? K : K extends number ? `${K}` : never;
|
|
|
70
82
|
export type AnswerFor<Q extends Question> = Q extends TypedChoiceQuestion<infer O> ? TypedChoiceAnswer<O[number]['value']>
|
|
71
83
|
: Q extends LabeledScoreQuestion<infer L> ? LabeledScoreAnswer<L>
|
|
72
84
|
: Q extends ChoiceQuestion<infer C>
|
|
73
|
-
? ChoiceAnswer<ChoiceLabel<keyof C>> : Q extends NoulQuestion ? NoulAnswer : ScoreAnswer;
|
|
85
|
+
? ChoiceAnswer<ChoiceLabel<keyof C>> : Q extends NoulQuestion ? NoulAnswer : Q extends BoxQuestion ? BoxAnswer : ScoreAnswer;
|
|
74
86
|
export type EvaluationRequest<Q extends Questions = Questions> = {
|
|
75
87
|
readonly questions: Q; readonly model?: string; readonly safetyIdentifier?: string | null;
|
|
76
88
|
} & ({ readonly state: State; readonly input?: never } | { readonly input: OrderedInput; readonly state?: never });
|
|
@@ -88,6 +100,8 @@ export interface ProviderResult<Q extends Questions = Questions> {
|
|
|
88
100
|
}
|
|
89
101
|
export interface Metadata {
|
|
90
102
|
readonly provider: string; readonly providerKind: ProviderKind; readonly requestedModel: string;
|
|
103
|
+
/** Upstream model ID sent after provider selection, when supplied by the provider. */
|
|
104
|
+
readonly resolvedModel?: string;
|
|
91
105
|
readonly probabilitySource: 'native' | 'elicited' | 'unspecified' | (string & {});
|
|
92
106
|
readonly confidenceSource?: 'native' | 'unavailable' | (string & {});
|
|
93
107
|
readonly modelSource?: 'reported' | 'requested';
|
|
@@ -107,7 +121,7 @@ export interface ModelList {
|
|
|
107
121
|
readonly release_date?: string; readonly [key: string]: unknown }[];
|
|
108
122
|
readonly has_more?: boolean; readonly last_id?: string;
|
|
109
123
|
}
|
|
110
|
-
export interface ResultValidationOptions { readonly native?: boolean; readonly probabilityDecimalPlaces?: number; }
|
|
124
|
+
export interface ResultValidationOptions { readonly native?: boolean; readonly nullableUsage?: boolean; readonly probabilityDecimalPlaces?: number; }
|
|
111
125
|
export type ProviderKind = 'decision' | 'generative';
|
|
112
126
|
export interface Provider {
|
|
113
127
|
readonly kind: ProviderKind;
|
|
@@ -122,6 +136,7 @@ export interface Provider {
|
|
|
122
136
|
}
|
|
123
137
|
export interface DecisionProvider extends Provider { readonly kind: 'decision'; }
|
|
124
138
|
export interface Capabilities {
|
|
139
|
+
readonly boxes?: boolean;
|
|
125
140
|
readonly audio?: boolean; readonly videos?: boolean;
|
|
126
141
|
readonly maxAudioClips?: number | null; readonly maxVideos?: number | null;
|
|
127
142
|
readonly supported: boolean; readonly orderedInput: boolean; readonly images: boolean;
|
|
@@ -143,7 +158,7 @@ export interface TransportOptions {
|
|
|
143
158
|
timeoutMs?: number; maxRetries?: number; maxRequestBytes?: number; maxResponseBytes?: number;
|
|
144
159
|
fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
145
160
|
}
|
|
146
|
-
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; }
|
|
147
162
|
export interface OpenAIDecisionsOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; }
|
|
148
163
|
export interface Bridge {
|
|
149
164
|
readonly provider: string; readonly providerKind: ProviderKind; readonly defaultModel?: string;
|
|
@@ -163,6 +178,10 @@ export class JevProvider implements DecisionProvider {
|
|
|
163
178
|
evaluate<const Q extends Questions>(request: EvaluationRequest<Q>, options?: ProviderCallOptions): Promise<ProviderResult<Q>>;
|
|
164
179
|
listModels(options?: ProviderCallOptions): Promise<ModelList>;
|
|
165
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 }>;
|
|
166
185
|
}
|
|
167
186
|
export class OpenAIDecisionsProvider implements DecisionProvider {
|
|
168
187
|
readonly kind: 'decision'; readonly name: 'openai-decisions'; readonly probabilitySource: 'native'; readonly defaultModel: string;
|
|
@@ -171,7 +190,7 @@ export class OpenAIDecisionsProvider implements DecisionProvider {
|
|
|
171
190
|
validateRequest(request: ResolvedRequest): void;
|
|
172
191
|
evaluate<const Q extends Questions>(request: EvaluationRequest<Q>, options?: ProviderCallOptions): Promise<ProviderResult<Q>>;
|
|
173
192
|
createDecision(request: NativeDecisionRequest, options?: ProviderCallOptions):
|
|
174
|
-
Promise<{ readonly decision: OpenAIDecisionResponse; readonly meta:
|
|
193
|
+
Promise<{ readonly decision: OpenAIDecisionResponse; readonly meta: OpenAIDecisionsTransportMetadata }>;
|
|
175
194
|
}
|
|
176
195
|
export class CloudflareDecisionsProvider implements DecisionProvider {
|
|
177
196
|
readonly kind: 'decision'; readonly name: 'cloudflare-decisions'; readonly probabilitySource: 'native'; readonly defaultModel: string;
|
|
@@ -184,10 +203,12 @@ export class CloudflareDecisionsProvider implements DecisionProvider {
|
|
|
184
203
|
Promise<{ readonly decision: CloudflareDecisionResponse; readonly meta: CloudflareTransportMetadata }>;
|
|
185
204
|
}
|
|
186
205
|
export type CloudflareDecisionsConfig = { type: 'cloudflare-decisions' } & CloudflareDecisionsOptions;
|
|
187
|
-
export type
|
|
206
|
+
export type JevConfig = { type: 'jev' } & JevOptions;
|
|
207
|
+
export type ProviderConfig = JevConfig
|
|
188
208
|
| ({ type: 'openai-decisions' } & OpenAIDecisionsOptions)
|
|
189
209
|
| CloudflareDecisionsConfig;
|
|
190
210
|
export type OpenAIDecisionsConfig = { type: 'openai-decisions' } & OpenAIDecisionsOptions;
|
|
211
|
+
export function createProvider(options: JevConfig): JevProvider;
|
|
191
212
|
export function createProvider(options: OpenAIDecisionsConfig): OpenAIDecisionsProvider;
|
|
192
213
|
export function createProvider(options: CloudflareDecisionsConfig): CloudflareDecisionsProvider;
|
|
193
214
|
export function createProvider(options: ProviderConfig | OpenAIDecisionsConfig): DecisionProvider;
|
|
@@ -197,6 +218,7 @@ export function choice<const C extends ChoiceCriteria>(instructions: Instruction
|
|
|
197
218
|
export function noul(instructions: Instructions, criteria?: NoulQuestion['criteria']): NoulQuestion;
|
|
198
219
|
export function score<const C extends ScoreCriteria>(instructions: Instructions, criteria: C): ScoreQuestion<C>;
|
|
199
220
|
export function scoreLevels<const L extends ScoreLevels>(instructions: Instructions, levels: L): LabeledScoreQuestion<L>;
|
|
221
|
+
export function box(instructions: Instructions): BoxQuestion;
|
|
200
222
|
export interface ChoicePolicyOptions<K extends string | boolean = string> {
|
|
201
223
|
minProbability: number; minMargin?: number; abstainOptions?: readonly K[];
|
|
202
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
|
}
|
|
@@ -4,17 +4,28 @@ import { assertCapabilities, builtInCapabilities } from '../capabilities.mjs';
|
|
|
4
4
|
import { serializeRequestBody } from '../transport.mjs';
|
|
5
5
|
import { validateDecisionRequest } from './decisions-validation.mjs';
|
|
6
6
|
import { encodeCommonDecision } from './decisions-codec.mjs';
|
|
7
|
+
import { OPENAI_DEFAULT_MODEL, resolveOpenAIModel } from './openai-profile.mjs';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** Keep the caller selection separate from the resolved, size-checked wire request. */
|
|
10
|
+
export function prepareDecision(input, { model = OPENAI_DEFAULT_MODEL, maxRequestBytes } = {}) {
|
|
9
11
|
const request = validateDecisionRequest(input, model);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
const native = freezeJson({ ...request, model: resolveOpenAIModel(request.model) });
|
|
13
|
+
serializeRequestBody(native, maxRequestBytes);
|
|
14
|
+
return { request, native };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function preflightDecision(input, options = {}) {
|
|
18
|
+
return prepareDecision(input, options).request;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
/** Check converted JSON text and wire bytes, not just the common request shape. */
|
|
15
|
-
export function
|
|
16
|
-
const request = validateRequest(input, options.model ??
|
|
22
|
+
export function prepareCommonDecision(input, options = {}) {
|
|
23
|
+
const request = validateRequest(input, options.model ?? OPENAI_DEFAULT_MODEL);
|
|
17
24
|
assertCapabilities(request, builtInCapabilities('openai-decisions', request.model));
|
|
18
|
-
|
|
19
|
-
return freezeJson(request);
|
|
25
|
+
const { native } = prepareDecision(encodeCommonDecision(request), options);
|
|
26
|
+
return { request: freezeJson(request), native };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function preflightCommonDecision(input, options = {}) {
|
|
30
|
+
return prepareCommonDecision(input, options).request;
|
|
20
31
|
}
|
|
@@ -2,6 +2,7 @@ import { DecisionError, requireCondition as check } from '../errors.mjs';
|
|
|
2
2
|
import { cloneJson, freezeJson, isRecord } from '../validation.mjs';
|
|
3
3
|
import { isTextWithinLimit, validateImage } from '../input.mjs';
|
|
4
4
|
import { builtInCapabilities } from '../capabilities.mjs';
|
|
5
|
+
import { OPENAI_DEFAULT_MODEL } from './openai-profile.mjs';
|
|
5
6
|
|
|
6
7
|
const own = (value, key) => Object.hasOwn(value, key);
|
|
7
8
|
function fields(value, allowed, required, label, code = 'VALIDATION_ERROR') {
|
|
@@ -12,8 +13,8 @@ function text(value, label, max = 1048576) {
|
|
|
12
13
|
check(isTextWithinLimit(value, max), `Invalid ${label}.`);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
/** Validate
|
|
16
|
-
export function validateDecisionRequest(input, defaultModel =
|
|
16
|
+
/** Validate native fields while retaining the selected model for preparation and metadata. */
|
|
17
|
+
export function validateDecisionRequest(input, defaultModel = OPENAI_DEFAULT_MODEL) {
|
|
17
18
|
const request = cloneJson(input, 'native decision request');
|
|
18
19
|
fields(request, ['input', 'model', 'questions', 'safety_identifier'], ['input', 'questions'], 'decision request');
|
|
19
20
|
const model = request.model ?? defaultModel;
|
|
@@ -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
|
+
}
|