@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/decisions.d.mts
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
/** Native OpenAI Decisions types, separate from the common bridge contract. */
|
|
2
2
|
export type NativeDecisionInputPart =
|
|
3
|
-
| { readonly type: 'input_text'; readonly text: string }
|
|
3
|
+
| { readonly type: 'input_text'; readonly text: string; readonly prompt_cache_breakpoint?: DecisionCacheBreakpoint | null }
|
|
4
4
|
| { readonly type: 'input_image'; readonly image_url: string;
|
|
5
|
-
readonly detail?: 'low' | 'high' | 'auto' | 'original' | null
|
|
6
|
-
|
|
5
|
+
readonly detail?: 'low' | 'high' | 'auto' | 'original' | null;
|
|
6
|
+
readonly prompt_cache_breakpoint?: DecisionCacheBreakpoint | null };
|
|
7
|
+
export interface DecisionCacheBreakpoint { readonly mode: 'explicit'; }
|
|
8
|
+
export interface DecisionCacheOptions { readonly mode?: 'implicit' | 'explicit' | null; readonly ttl?: '30m'; }
|
|
9
|
+
export type NativeDecisionMessage = {
|
|
7
10
|
readonly role: 'user'; readonly type?: 'message';
|
|
8
11
|
readonly content: string | readonly NativeDecisionInputPart[];
|
|
12
|
+
readonly phase?: 'commentary' | 'final_answer' | null;
|
|
13
|
+
} | {
|
|
14
|
+
readonly role: 'developer' | 'system'; readonly type?: 'message';
|
|
15
|
+
readonly content: string | readonly Extract<NativeDecisionInputPart, { readonly type: 'input_text' }>[];
|
|
16
|
+
readonly phase?: 'commentary' | 'final_answer' | null;
|
|
17
|
+
} | {
|
|
18
|
+
readonly role: 'assistant'; readonly type?: 'message';
|
|
19
|
+
readonly content: string | readonly ({ readonly type: 'output_text'; readonly text: string }
|
|
20
|
+
| { readonly type: 'refusal'; readonly refusal: string })[];
|
|
21
|
+
readonly phase?: 'commentary' | 'final_answer' | null;
|
|
22
|
+
};
|
|
23
|
+
interface ToolItem {
|
|
24
|
+
readonly call_id: string; readonly id?: string | null;
|
|
25
|
+
readonly status?: 'in_progress' | 'completed' | 'incomplete' | null;
|
|
9
26
|
}
|
|
27
|
+
export type NativeDecisionInputItem = NativeDecisionMessage
|
|
28
|
+
| (ToolItem & { readonly type: 'function_call'; readonly name: string; readonly arguments: string; readonly namespace?: string | null })
|
|
29
|
+
| (ToolItem & { readonly type: 'custom_tool_call'; readonly name: string; readonly input: string; readonly namespace?: string | null })
|
|
30
|
+
| (ToolItem & { readonly type: 'function_call_output' | 'custom_tool_call_output';
|
|
31
|
+
readonly output: string | readonly NativeDecisionInputPart[] });
|
|
10
32
|
export type NativeDecisionQuestion =
|
|
11
33
|
| { readonly type: 'predicate'; readonly name?: string; readonly instructions: string }
|
|
12
34
|
| { readonly type: 'choice'; readonly name?: string; readonly instructions: string;
|
|
@@ -15,9 +37,11 @@ export type NativeDecisionQuestion =
|
|
|
15
37
|
readonly levels: readonly { readonly label: string; readonly description?: string }[] };
|
|
16
38
|
export interface NativeDecisionRequest {
|
|
17
39
|
readonly model?: string;
|
|
18
|
-
readonly input: string | readonly
|
|
40
|
+
readonly input: string | readonly NativeDecisionInputItem[];
|
|
19
41
|
readonly questions: readonly NativeDecisionQuestion[];
|
|
20
42
|
readonly safety_identifier?: string | null;
|
|
43
|
+
readonly prompt_cache_key?: string | null;
|
|
44
|
+
readonly prompt_cache_options?: DecisionCacheOptions | null;
|
|
21
45
|
}
|
|
22
46
|
export type NativeDecisionAnswer =
|
|
23
47
|
| { readonly type: 'predicate'; readonly name: string | null; readonly probability: number }
|
package/src/errors.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Public errors never retain raw upstream bodies, headers or original exceptions. */
|
|
2
2
|
export class DecisionError extends Error {
|
|
3
|
-
constructor(message, { code = 'DECISION_ERROR', status, requestId, gatewayLogId, attempts, refusedQuestions, usage, upstreamCode
|
|
3
|
+
constructor(message, { code = 'DECISION_ERROR', status, requestId, gatewayLogId, attempts, refusedQuestions, usage, upstreamCode,
|
|
4
|
+
source, upstreamErrorCode, upstreamErrorType, retryAfterMs, retryable } = {}) {
|
|
4
5
|
super(message);
|
|
5
6
|
this.name = 'DecisionError';
|
|
6
7
|
this.code = code;
|
|
@@ -11,6 +12,9 @@ export class DecisionError extends Error {
|
|
|
11
12
|
if (refusedQuestions !== undefined) this.refusedQuestions = refusedQuestions;
|
|
12
13
|
if (usage !== undefined) this.usage = usage;
|
|
13
14
|
if (upstreamCode !== undefined) this.upstreamCode = upstreamCode;
|
|
15
|
+
for (const [key, value] of Object.entries({ source, upstreamErrorCode, upstreamErrorType, retryAfterMs, retryable })) {
|
|
16
|
+
if (value !== undefined) this[key] = value;
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
toJSON() {
|
|
@@ -24,6 +28,8 @@ export class DecisionError extends Error {
|
|
|
24
28
|
...(this.refusedQuestions === undefined ? {} : { refusedQuestions: this.refusedQuestions }),
|
|
25
29
|
...(this.usage === undefined ? {} : { usage: this.usage }),
|
|
26
30
|
...(this.upstreamCode === undefined ? {} : { upstreamCode: this.upstreamCode }),
|
|
31
|
+
...Object.fromEntries(['source', 'upstreamErrorCode', 'upstreamErrorType', 'retryAfterMs', 'retryable']
|
|
32
|
+
.filter(key => this[key] !== undefined).map(key => [key, this[key]])),
|
|
27
33
|
};
|
|
28
34
|
}
|
|
29
35
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JsonValue, TransportOptions, ProviderCallOptions, BatchOptions, ErrorDetails } from '../index.mjs';
|
|
2
|
+
import type { CloudflareGatewayOptions, CloudflareBindingGatewayOptions, CloudflareTransportMetadata } from '../cloudflare-decisions.mjs';
|
|
3
|
+
|
|
4
|
+
export type ExtractionFieldType = 'text' | 'number' | 'date' | 'time' | 'datetime' | 'duration' | 'list' | 'estimate' | 'forecast';
|
|
5
|
+
export interface ExtractionField {
|
|
6
|
+
readonly type: ExtractionFieldType; readonly instructions?: string; readonly unit?: string;
|
|
7
|
+
readonly integer?: boolean; readonly decimals?: number; readonly items?: 'text' | 'number' | 'date';
|
|
8
|
+
readonly min?: number | string; readonly max?: number | string; readonly step?: number | 'month' | 'year';
|
|
9
|
+
readonly scale?: 'linear' | 'log'; readonly null_threshold?: number; readonly horizon?: number;
|
|
10
|
+
readonly interval?: 'step' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
11
|
+
}
|
|
12
|
+
export type ExtractionFields = Readonly<Record<string, ExtractionField>>;
|
|
13
|
+
export interface ExtractionRequest<F extends ExtractionFields = ExtractionFields> {
|
|
14
|
+
readonly model?: 'clairic' | '@cf/cloudflare/clairic'; readonly state?: string; readonly fields: F;
|
|
15
|
+
readonly images?: readonly (string | { readonly content_type: string; readonly base64: string })[];
|
|
16
|
+
readonly reference_date?: string;
|
|
17
|
+
readonly options?: { readonly include_quantiles?: boolean; readonly return_state_text?: boolean; readonly rejectIfBusy?: boolean };
|
|
18
|
+
}
|
|
19
|
+
export interface ExtractionAnswer {
|
|
20
|
+
readonly type: ExtractionFieldType; readonly value: JsonValue; readonly guess?: JsonValue;
|
|
21
|
+
readonly confidence?: number; readonly p_null?: number; readonly span?: readonly [number, number];
|
|
22
|
+
readonly spans?: readonly (readonly [number, number])[]; readonly mean?: JsonValue; readonly median?: JsonValue;
|
|
23
|
+
readonly p10?: JsonValue; readonly p90?: JsonValue; readonly p_below?: number; readonly p_above?: number;
|
|
24
|
+
readonly quantiles?: readonly JsonValue[]; readonly steps?: readonly JsonValue[];
|
|
25
|
+
readonly [key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
// Distribute over field/item unions so unspecialized and batch values stay honest.
|
|
28
|
+
type ListItemValue<I> = I extends 'number' ? number : I extends 'text' | 'date' ? string : number | string;
|
|
29
|
+
type FieldValue<T extends ExtractionFieldType, I> = T extends 'number' | 'duration' ? number | null
|
|
30
|
+
: T extends 'list' ? readonly ListItemValue<I>[] | null
|
|
31
|
+
: T extends 'estimate' ? number | string | null
|
|
32
|
+
: T extends 'forecast' ? JsonValue : string | null;
|
|
33
|
+
export type ExtractionValue<S extends ExtractionField> = FieldValue<S['type'], S['items']>;
|
|
34
|
+
export interface ExtractionResponse<F extends ExtractionFields = ExtractionFields> {
|
|
35
|
+
readonly model: string;
|
|
36
|
+
readonly answers: { readonly [K in keyof F]: ExtractionAnswer & { readonly type: F[K]['type']; readonly value: ExtractionValue<F[K]> } };
|
|
37
|
+
readonly truncated: boolean; readonly state_text?: string;
|
|
38
|
+
readonly usage: { readonly input_tokens: number; readonly output_tokens: number; readonly state_tokens?: number;
|
|
39
|
+
readonly image_tokens?: number; readonly schema_tokens?: number; readonly passes?: number; readonly [key: string]: unknown };
|
|
40
|
+
readonly [key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface ExtractionResult<F extends ExtractionFields = ExtractionFields> {
|
|
43
|
+
readonly extraction: ExtractionResponse<F>;
|
|
44
|
+
readonly meta: CloudflareTransportMetadata & { readonly provider: 'cloudflare-extraction'; readonly requestedModel: string; readonly resolvedModel: string };
|
|
45
|
+
}
|
|
46
|
+
export interface CloudflareExtractionBinding {
|
|
47
|
+
run(model: '@cf/cloudflare/clairic', input: ExtractionRequest,
|
|
48
|
+
options?: { readonly signal?: AbortSignal; readonly returnRawResponse?: boolean; readonly rejectIfBusy?: boolean;
|
|
49
|
+
readonly gateway?: CloudflareBindingGatewayOptions }): Promise<unknown>;
|
|
50
|
+
}
|
|
51
|
+
type Settings = { model?: 'clairic' | '@cf/cloudflare/clairic'; rejectIfBusy?: boolean; gateway?: CloudflareGatewayOptions };
|
|
52
|
+
export type CloudflareExtractionOptions = Settings & (
|
|
53
|
+
| (TransportOptions & { transport?: 'rest'; accountId?: string; apiKey?: string; baseURL?: string; ai?: never })
|
|
54
|
+
| (Omit<TransportOptions, 'fetchImpl'> & { transport: 'binding'; ai: CloudflareExtractionBinding;
|
|
55
|
+
accountId?: never; apiKey?: never; baseURL?: never; fetchImpl?: never }));
|
|
56
|
+
export type ExtractionValidationOptions = Settings & { transport?: 'rest' | 'binding'; maxRequestBytes?: number; maxRetries?: number };
|
|
57
|
+
export function validateExtractionRequest<const F extends ExtractionFields>(request: ExtractionRequest<F>, options?: ExtractionValidationOptions): ExtractionRequest<F> & { readonly model: 'clairic' };
|
|
58
|
+
export class CloudflareExtractionProvider {
|
|
59
|
+
readonly name: 'cloudflare-extraction'; readonly kind: 'extraction'; readonly defaultModel: 'clairic';
|
|
60
|
+
constructor(options?: CloudflareExtractionOptions);
|
|
61
|
+
validate<const F extends ExtractionFields>(request: ExtractionRequest<F>): ExtractionRequest<F> & { readonly model: 'clairic' };
|
|
62
|
+
extract<const F extends ExtractionFields>(request: ExtractionRequest<F>, options?: ProviderCallOptions): Promise<ExtractionResult<F>>;
|
|
63
|
+
}
|
|
64
|
+
export type ExtractionBatchRecord = { readonly index: number; readonly ok: true; readonly result: ExtractionResult }
|
|
65
|
+
| { readonly index: number; readonly ok: false; readonly error: ErrorDetails };
|
|
66
|
+
export function extractBatch(provider: CloudflareExtractionProvider, inputs: readonly ExtractionRequest[], options?: BatchOptions): Promise<readonly ExtractionBatchRecord[]>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { optionsObject, TRANSPORT_FIELDS } from '../options.mjs';
|
|
2
|
+
import { DecisionError, requireCondition as check, withResponseMeta } from '../errors.mjs';
|
|
3
|
+
import { deadline } from '../async.mjs';
|
|
4
|
+
import { cloneJson, freezeJson } from '../validation.mjs';
|
|
5
|
+
import { CloudflareTransport } from '../providers/cloudflare-transport.mjs';
|
|
6
|
+
import { cloudflareGateway, validateCloudflareLimits } from '../providers/cloudflare-profile.mjs';
|
|
7
|
+
import { unwrapCloudflareResponse } from '../providers/cloudflare-validation.mjs';
|
|
8
|
+
import { validateExtractionRequest, validateExtractionResponse, extractionModel } from './validation.mjs';
|
|
9
|
+
import { evaluateBatch } from '../batch.mjs';
|
|
10
|
+
|
|
11
|
+
export { validateExtractionRequest } from './validation.mjs';
|
|
12
|
+
|
|
13
|
+
/** Typed extraction remains independent of the decision bridge's answer contract. */
|
|
14
|
+
export class CloudflareExtractionProvider {
|
|
15
|
+
name = 'cloudflare-extraction';
|
|
16
|
+
kind = 'extraction';
|
|
17
|
+
#settings;
|
|
18
|
+
#transport;
|
|
19
|
+
#timeoutMs;
|
|
20
|
+
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
const supplied = optionsObject(options, ['transport', 'accountId', 'apiKey', 'baseURL', 'ai', 'model',
|
|
23
|
+
'rejectIfBusy', 'gateway', ...TRANSPORT_FIELDS], 'Cloudflare extraction options');
|
|
24
|
+
const transport = supplied.transport === undefined ? 'rest' : supplied.transport;
|
|
25
|
+
check(['rest', 'binding'].includes(transport), 'Unknown Cloudflare transport.', 'CONFIGURATION_ERROR');
|
|
26
|
+
check(transport !== 'binding' || ['accountId', 'apiKey', 'baseURL', 'fetchImpl'].every(key => supplied[key] === undefined),
|
|
27
|
+
'Binding transport does not accept REST credentials or HTTP options.', 'CONFIGURATION_ERROR');
|
|
28
|
+
check(transport !== 'rest' || supplied.ai === undefined, 'REST transport does not accept an ai binding.', 'CONFIGURATION_ERROR');
|
|
29
|
+
this.defaultModel = extractionModel(supplied.model);
|
|
30
|
+
this.#timeoutMs = supplied.timeoutMs ?? 30000;
|
|
31
|
+
const gateway = cloudflareGateway(supplied.gateway);
|
|
32
|
+
this.#settings = { model: this.defaultModel, transport, gateway, ...validateCloudflareLimits({ ...supplied, gateway }) };
|
|
33
|
+
this.#transport = new CloudflareTransport({ ...supplied, ...this.#settings }, {
|
|
34
|
+
models: ['clairic'], requestLimit: (_model, ceiling) => ceiling ?? 8 * 1024 * 1024,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validate(request) { return validateExtractionRequest(request, this.#settings); }
|
|
39
|
+
|
|
40
|
+
async extract(input, options = {}) {
|
|
41
|
+
const { signal } = optionsObject(options, ['signal'], 'Extraction call options');
|
|
42
|
+
const budget = deadline(this.#timeoutMs, signal);
|
|
43
|
+
try {
|
|
44
|
+
budget.throwIfAborted();
|
|
45
|
+
const snapshot = cloneJson(input, 'extraction request');
|
|
46
|
+
const request = this.validate(snapshot);
|
|
47
|
+
const requestedModel = snapshot.model ?? this.#settings.model;
|
|
48
|
+
budget.throwIfAborted();
|
|
49
|
+
const { data, meta } = await this.#transport.request(request, { signal: budget.signal });
|
|
50
|
+
const extraction = withResponseMeta(meta, () => validateExtractionResponse(
|
|
51
|
+
unwrapCloudflareResponse(data, this.#settings.transport, meta), request));
|
|
52
|
+
budget.throwIfAborted();
|
|
53
|
+
return freezeJson({ extraction, meta: { ...meta, provider: 'cloudflare-extraction',
|
|
54
|
+
requestedModel, resolvedModel: request.model } });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const details = error instanceof DecisionError ? error.toJSON() : {};
|
|
57
|
+
if (signal?.aborted) throw new DecisionError('Extraction cancelled.', { ...details, code: 'ABORTED', source: 'client' });
|
|
58
|
+
if (budget.timedOut) throw new DecisionError('Extraction deadline exceeded.', { ...details, code: 'TIMEOUT', source: 'client' });
|
|
59
|
+
throw error;
|
|
60
|
+
} finally { budget.close(); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reuse the existing ordered, preflighted, caller-controlled batch implementation. */
|
|
65
|
+
export function extractBatch(provider, inputs, options = {}) {
|
|
66
|
+
check(provider && typeof provider.validate === 'function' && typeof provider.extract === 'function',
|
|
67
|
+
'An extraction provider is required.', 'CONFIGURATION_ERROR');
|
|
68
|
+
return evaluateBatch({
|
|
69
|
+
validate(input) {
|
|
70
|
+
// Preflight resolves the wire model, but dispatch must retain the caller's
|
|
71
|
+
// selection for diagnostics. Snapshot before validation and async work.
|
|
72
|
+
const request = cloneJson(input, 'extraction request');
|
|
73
|
+
provider.validate(request);
|
|
74
|
+
return freezeJson(request);
|
|
75
|
+
},
|
|
76
|
+
evaluate: (input, call) => provider.extract(input, call),
|
|
77
|
+
}, inputs, options);
|
|
78
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { requireCondition as check } from '../errors.mjs';
|
|
2
|
+
import { cloneJson, freezeJson, isRecord } from '../validation.mjs';
|
|
3
|
+
import { serializeRequestBody } from '../transport.mjs';
|
|
4
|
+
import { optionsObject } from '../options.mjs';
|
|
5
|
+
import { cloudflareBindingBody } from '../providers/cloudflare-preflight.mjs';
|
|
6
|
+
import { cloudflareGateway, validateCloudflareLimits } from '../providers/cloudflare-profile.mjs';
|
|
7
|
+
import { validateCloudflareImages } from '../providers/cloudflare-images.mjs';
|
|
8
|
+
|
|
9
|
+
const own = (value, key) => Object.hasOwn(value, key);
|
|
10
|
+
const TYPES = ['text', 'number', 'date', 'time', 'datetime', 'duration', 'list', 'estimate', 'forecast'];
|
|
11
|
+
const FIELD_KEYS = ['type', 'instructions', 'unit', 'integer', 'decimals', 'items', 'min', 'max',
|
|
12
|
+
'step', 'scale', 'null_threshold', 'horizon', 'interval'];
|
|
13
|
+
const isDate = value => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
14
|
+
|
|
15
|
+
export function extractionModel(value = 'clairic') {
|
|
16
|
+
check(value === 'clairic' || value === '@cf/cloudflare/clairic', 'Unsupported extraction model.', 'UNSUPPORTED_CAPABILITY');
|
|
17
|
+
return 'clairic';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function field(spec) {
|
|
21
|
+
check(isRecord(spec) && Object.keys(spec).every(key => FIELD_KEYS.includes(key)) && TYPES.includes(spec.type), 'Invalid extraction field.');
|
|
22
|
+
for (const key of ['instructions', 'unit']) check(spec[key] === undefined || typeof spec[key] === 'string', `${key} must be text.`);
|
|
23
|
+
check(spec.integer === undefined || (['number', 'duration'].includes(spec.type) && typeof spec.integer === 'boolean'), 'Invalid integer option.');
|
|
24
|
+
check(spec.decimals === undefined || ((['number', 'duration'].includes(spec.type) || spec.type === 'list' && spec.items === 'number') &&
|
|
25
|
+
Number.isSafeInteger(spec.decimals) && spec.decimals >= 0), 'Invalid decimals option.');
|
|
26
|
+
if (spec.type === 'duration' && spec.unit !== undefined) check(['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'].includes(spec.unit), 'Invalid duration unit.');
|
|
27
|
+
if (spec.type === 'list') check(['text', 'number', 'date'].includes(spec.items), 'Lists require an item type.');
|
|
28
|
+
else check(spec.items === undefined, 'items applies only to lists.');
|
|
29
|
+
if (['estimate', 'forecast'].includes(spec.type)) {
|
|
30
|
+
check((typeof spec.min === 'number' && typeof spec.max === 'number' && spec.min <= spec.max) ||
|
|
31
|
+
(isDate(spec.min) && isDate(spec.max) && spec.min <= spec.max), 'Prediction fields require ordered numeric or ISO date bounds.');
|
|
32
|
+
} else check(spec.min === undefined && spec.max === undefined && spec.step === undefined && spec.scale === undefined,
|
|
33
|
+
'Prediction bounds apply only to estimates and forecasts.');
|
|
34
|
+
check(spec.step === undefined || (typeof spec.step === 'number' && spec.step > 0) || ['month', 'year'].includes(spec.step), 'Invalid prediction grid step.');
|
|
35
|
+
check(spec.scale === undefined || ['linear', 'log'].includes(spec.scale), 'Invalid prediction scale.');
|
|
36
|
+
if (spec.scale === 'log') check(typeof spec.min === 'number' && spec.min > 0, 'Log grids require positive numeric bounds.');
|
|
37
|
+
check(spec.null_threshold === undefined || (typeof spec.null_threshold === 'number' && spec.null_threshold > 0 && spec.null_threshold <= 1), 'Invalid null threshold.');
|
|
38
|
+
if (spec.type === 'forecast') {
|
|
39
|
+
check(spec.horizon === undefined || (Number.isSafeInteger(spec.horizon) && spec.horizon > 0), 'Invalid forecast horizon.');
|
|
40
|
+
check(spec.interval === undefined || ['step', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'].includes(spec.interval), 'Invalid forecast interval.');
|
|
41
|
+
} else check(spec.horizon === undefined && spec.interval === undefined, 'Forecast options require a forecast field.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Credential-free preflight shares the exact wire budget with actual dispatch. */
|
|
45
|
+
export function validateExtractionRequest(input, options = {}) {
|
|
46
|
+
const settings = optionsObject(options, ['model', 'transport', 'gateway', 'maxRequestBytes', 'maxRetries', 'rejectIfBusy'], 'Extraction validation options');
|
|
47
|
+
const request = cloneJson(input, 'extraction request');
|
|
48
|
+
check(isRecord(request) && Object.keys(request).every(key => ['model', 'state', 'fields', 'images', 'reference_date', 'options'].includes(key)), 'Invalid extraction request fields.');
|
|
49
|
+
const model = extractionModel(own(request, 'model') ? request.model : settings.model);
|
|
50
|
+
check(isRecord(request.fields) && Object.keys(request.fields).length > 0, 'Extraction requires named fields.');
|
|
51
|
+
for (const [name, spec] of Object.entries(request.fields)) {
|
|
52
|
+
check(/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name), 'Invalid extraction field name.');
|
|
53
|
+
field(spec);
|
|
54
|
+
}
|
|
55
|
+
check(request.state === undefined || typeof request.state === 'string', 'Extraction state must be text.');
|
|
56
|
+
check((request.state?.length ?? 0) > 0 || (Array.isArray(request.images) && request.images.length > 0), 'Provide text or embedded images.');
|
|
57
|
+
check(request.reference_date === undefined || isDate(request.reference_date), 'reference_date must be an ISO date.');
|
|
58
|
+
const inference = own(request, 'options') ? request.options : {};
|
|
59
|
+
check(isRecord(inference) && Object.keys(inference).every(key => ['include_quantiles', 'return_state_text', 'rejectIfBusy'].includes(key)) &&
|
|
60
|
+
Object.values(inference).every(value => typeof value === 'boolean'), 'Invalid extraction options.');
|
|
61
|
+
const transport = settings.transport === undefined ? 'rest' : settings.transport;
|
|
62
|
+
check(['rest', 'binding'].includes(transport), 'Unknown Cloudflare transport.', 'CONFIGURATION_ERROR');
|
|
63
|
+
const gateway = cloudflareGateway(settings.gateway);
|
|
64
|
+
const rejectIfBusy = inference.rejectIfBusy ?? settings.rejectIfBusy ?? false;
|
|
65
|
+
validateCloudflareLimits({ ...settings, gateway, rejectIfBusy });
|
|
66
|
+
const result = { ...request, model, ...(rejectIfBusy ? { options: { ...inference, rejectIfBusy } } : {}) };
|
|
67
|
+
serializeRequestBody(transport === 'binding' ? cloudflareBindingBody(result, gateway) : result, settings.maxRequestBytes);
|
|
68
|
+
if (own(request, 'images')) validateCloudflareImages(request.images);
|
|
69
|
+
return freezeJson(result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Validate established fields without guessing predictive structures or span units. */
|
|
73
|
+
export function validateExtractionResponse(input, request) {
|
|
74
|
+
const data = cloneJson(input, 'extraction response', 'INVALID_RESPONSE');
|
|
75
|
+
const invalid = (condition, message) => check(condition, message, 'INVALID_RESPONSE');
|
|
76
|
+
invalid(isRecord(data) && typeof data.model === 'string' && data.model.trim() && isRecord(data.answers) &&
|
|
77
|
+
typeof data.truncated === 'boolean' && isRecord(data.usage), 'Invalid extraction response.');
|
|
78
|
+
invalid(Object.keys(data.answers).length === Object.keys(request.fields).length, 'Extraction answer count mismatch.');
|
|
79
|
+
invalid(data.state_text === undefined || typeof data.state_text === 'string', 'Invalid returned state text.');
|
|
80
|
+
for (const key of ['input_tokens', 'output_tokens', 'state_tokens', 'image_tokens', 'schema_tokens', 'passes']) {
|
|
81
|
+
if (['input_tokens', 'output_tokens'].includes(key) || own(data.usage, key)) {
|
|
82
|
+
invalid(Number.isSafeInteger(data.usage[key]) && data.usage[key] >= 0, 'Invalid extraction usage.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const [name, spec] of Object.entries(request.fields)) {
|
|
86
|
+
invalid(own(data.answers, name), 'Missing extraction answer.');
|
|
87
|
+
const answer = data.answers[name];
|
|
88
|
+
invalid(isRecord(answer) && answer.type === spec.type && own(answer, 'value'), 'Extraction answer type mismatch.');
|
|
89
|
+
const value = answer.value;
|
|
90
|
+
if (value !== null && spec.type !== 'forecast') {
|
|
91
|
+
if (spec.type === 'list') invalid(Array.isArray(value) && value.every(item =>
|
|
92
|
+
typeof item === (spec.items === 'number' ? 'number' : 'string')), 'Invalid extraction list.');
|
|
93
|
+
else invalid(typeof value === (['number', 'duration'].includes(spec.type) ? 'number'
|
|
94
|
+
: spec.type === 'estimate' ? typeof spec.min : 'string'), 'Invalid extraction value.');
|
|
95
|
+
}
|
|
96
|
+
for (const key of ['confidence', 'p_null', 'p_below', 'p_above']) if (own(answer, key)) {
|
|
97
|
+
invalid(typeof answer[key] === 'number' && answer[key] >= 0 && answer[key] <= 1, 'Invalid extraction probability.');
|
|
98
|
+
}
|
|
99
|
+
const span = item => Array.isArray(item) && item.length === 2 && item.every(n => Number.isSafeInteger(n) && n >= 0) && item[1] >= item[0];
|
|
100
|
+
if (own(answer, 'span')) invalid(span(answer.span), 'Invalid extraction span.');
|
|
101
|
+
if (own(answer, 'spans')) invalid(Array.isArray(answer.spans) && answer.spans.every(span), 'Invalid extraction spans.');
|
|
102
|
+
for (const key of ['quantiles', 'steps']) if (own(answer, key)) invalid(Array.isArray(answer[key]), 'Invalid predictive array.');
|
|
103
|
+
}
|
|
104
|
+
return freezeJson(data);
|
|
105
|
+
}
|
package/src/index.d.mts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
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
|
-
export type { NativeDecisionInputPart,
|
|
6
|
+
export type { NativeDecisionInputPart, NativeDecisionInputItem, DecisionCacheBreakpoint, DecisionCacheOptions,
|
|
7
|
+
NativeDecisionMessage, NativeDecisionQuestion, NativeDecisionRequest,
|
|
5
8
|
NativeDecisionAnswer, OpenAIDecisionResponse, OpenAIDecisionUsage, OpenAIDecisionsMetadata,
|
|
6
9
|
OpenAIDecisionsTransportMetadata, TransportMetadata } from './decisions.d.mts';
|
|
7
10
|
import type { CloudflareDecisionsOptions, CloudflareDecisionRequest, CloudflareDecisionResponse,
|
|
@@ -25,6 +28,15 @@ export interface NoulQuestion {
|
|
|
25
28
|
readonly type: 'noul'; readonly instructions: Instructions;
|
|
26
29
|
readonly criteria?: { readonly true?: Instructions; readonly false?: Instructions };
|
|
27
30
|
}
|
|
31
|
+
export interface BoxQuestion {
|
|
32
|
+
readonly type: 'box'; readonly instructions: Instructions; readonly criteria?: never;
|
|
33
|
+
}
|
|
34
|
+
export interface BoxCoordinates {
|
|
35
|
+
readonly top: number; readonly bottom: number; readonly left: number; readonly right: number;
|
|
36
|
+
}
|
|
37
|
+
export interface BoxAnswer {
|
|
38
|
+
readonly type: 'box'; readonly box: BoxCoordinates; readonly confidence?: number;
|
|
39
|
+
}
|
|
28
40
|
export interface ScoreQuestion<C extends ScoreCriteria = ScoreCriteria> {
|
|
29
41
|
readonly type: 'score'; readonly instructions: Instructions; readonly criteria: C;
|
|
30
42
|
readonly levels?: never;
|
|
@@ -40,12 +52,16 @@ export type ScoreLevels = readonly [ScoreLevel, ScoreLevel, ...ScoreLevel[]];
|
|
|
40
52
|
export interface LabeledScoreQuestion<L extends ScoreLevels = ScoreLevels> {
|
|
41
53
|
readonly type: 'score'; readonly instructions: Instructions; readonly levels: L; readonly criteria?: never;
|
|
42
54
|
}
|
|
43
|
-
export type
|
|
44
|
-
|
|
55
|
+
export type DecisionConversationItem = import('./decisions.mjs').NativeDecisionInputItem;
|
|
56
|
+
export type DecisionInputPart = { readonly type: 'text'; readonly text: string;
|
|
57
|
+
readonly promptCacheBreakpoint?: import('./decisions.mjs').DecisionCacheBreakpoint | null }
|
|
58
|
+
| ({ readonly type: 'image'; readonly detail?: 'low' | 'high' | 'auto' | 'original' | null;
|
|
59
|
+
readonly promptCacheBreakpoint?: import('./decisions.mjs').DecisionCacheBreakpoint | null }
|
|
60
|
+
& ({ readonly dataUrl: string; readonly url?: never } | { readonly url: string; readonly dataUrl?: never }))
|
|
45
61
|
| { readonly type: 'audio'; readonly dataUrl: string }
|
|
46
62
|
| { readonly type: 'video'; readonly dataUrl: string };
|
|
47
63
|
export type OrderedInput = readonly DecisionInputPart[];
|
|
48
|
-
export type Question = ChoiceQuestion | TypedChoiceQuestion | NoulQuestion | ScoreQuestion | LabeledScoreQuestion;
|
|
64
|
+
export type Question = ChoiceQuestion | TypedChoiceQuestion | NoulQuestion | ScoreQuestion | LabeledScoreQuestion | BoxQuestion;
|
|
49
65
|
export type Questions = Readonly<Record<string, Question>>;
|
|
50
66
|
export interface ChoiceAnswer<K extends string = string> {
|
|
51
67
|
readonly type: 'choice'; readonly choice: K;
|
|
@@ -71,10 +87,14 @@ type ChoiceLabel<K> = K extends string ? K : K extends number ? `${K}` : never;
|
|
|
71
87
|
export type AnswerFor<Q extends Question> = Q extends TypedChoiceQuestion<infer O> ? TypedChoiceAnswer<O[number]['value']>
|
|
72
88
|
: Q extends LabeledScoreQuestion<infer L> ? LabeledScoreAnswer<L>
|
|
73
89
|
: Q extends ChoiceQuestion<infer C>
|
|
74
|
-
? ChoiceAnswer<ChoiceLabel<keyof C>> : Q extends NoulQuestion ? NoulAnswer : ScoreAnswer;
|
|
90
|
+
? ChoiceAnswer<ChoiceLabel<keyof C>> : Q extends NoulQuestion ? NoulAnswer : Q extends BoxQuestion ? BoxAnswer : ScoreAnswer;
|
|
75
91
|
export type EvaluationRequest<Q extends Questions = Questions> = {
|
|
76
92
|
readonly questions: Q; readonly model?: string; readonly safetyIdentifier?: string | null;
|
|
77
|
-
|
|
93
|
+
readonly promptCacheKey?: string | null;
|
|
94
|
+
readonly promptCacheOptions?: import('./decisions.mjs').DecisionCacheOptions | null;
|
|
95
|
+
} & ({ readonly state: State; readonly input?: never; readonly conversation?: never }
|
|
96
|
+
| { readonly input: OrderedInput; readonly state?: never; readonly conversation?: never }
|
|
97
|
+
| { readonly conversation: readonly DecisionConversationItem[]; readonly state?: never; readonly input?: never });
|
|
78
98
|
export type ResolvedRequest<Q extends Questions = Questions> = EvaluationRequest<Q> & { readonly model: string };
|
|
79
99
|
export interface Usage {
|
|
80
100
|
/** Null means not reported, not zero tokens. */
|
|
@@ -110,7 +130,7 @@ export interface ModelList {
|
|
|
110
130
|
readonly release_date?: string; readonly [key: string]: unknown }[];
|
|
111
131
|
readonly has_more?: boolean; readonly last_id?: string;
|
|
112
132
|
}
|
|
113
|
-
export interface ResultValidationOptions { readonly native?: boolean; readonly probabilityDecimalPlaces?: number; }
|
|
133
|
+
export interface ResultValidationOptions { readonly native?: boolean; readonly nullableUsage?: boolean; readonly probabilityDecimalPlaces?: number; }
|
|
114
134
|
export type ProviderKind = 'decision' | 'generative';
|
|
115
135
|
export interface Provider {
|
|
116
136
|
readonly kind: ProviderKind;
|
|
@@ -125,6 +145,8 @@ export interface Provider {
|
|
|
125
145
|
}
|
|
126
146
|
export interface DecisionProvider extends Provider { readonly kind: 'decision'; }
|
|
127
147
|
export interface Capabilities {
|
|
148
|
+
readonly conversation?: boolean; readonly toolHistory?: boolean; readonly remoteImages?: boolean; readonly promptCaching?: boolean;
|
|
149
|
+
readonly boxes?: boolean;
|
|
128
150
|
readonly audio?: boolean; readonly videos?: boolean;
|
|
129
151
|
readonly maxAudioClips?: number | null; readonly maxVideos?: number | null;
|
|
130
152
|
readonly supported: boolean; readonly orderedInput: boolean; readonly images: boolean;
|
|
@@ -139,6 +161,8 @@ export interface Extension {
|
|
|
139
161
|
name: string;
|
|
140
162
|
prepareState?(state: State, context: ExtensionContext): State | undefined | Promise<State | undefined>;
|
|
141
163
|
prepareInput?(input: OrderedInput, context: ExtensionContext): OrderedInput | undefined | Promise<OrderedInput | undefined>;
|
|
164
|
+
prepareConversation?(conversation: readonly DecisionConversationItem[], context: ExtensionContext):
|
|
165
|
+
readonly DecisionConversationItem[] | undefined | Promise<readonly DecisionConversationItem[] | undefined>;
|
|
142
166
|
/** Observer failure is isolated in meta.observerFailures; no inference is retried. */
|
|
143
167
|
onResult?(result: EvaluationResult, context: ExtensionContext): void | Promise<void>;
|
|
144
168
|
}
|
|
@@ -146,7 +170,7 @@ export interface TransportOptions {
|
|
|
146
170
|
timeoutMs?: number; maxRetries?: number; maxRequestBytes?: number; maxResponseBytes?: number;
|
|
147
171
|
fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
148
172
|
}
|
|
149
|
-
export interface JevOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; }
|
|
173
|
+
export interface JevOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; dangerZone?: JevDangerZone; }
|
|
150
174
|
export interface OpenAIDecisionsOptions extends TransportOptions { apiKey?: string; model?: string; baseURL?: string; }
|
|
151
175
|
export interface Bridge {
|
|
152
176
|
readonly provider: string; readonly providerKind: ProviderKind; readonly defaultModel?: string;
|
|
@@ -166,6 +190,10 @@ export class JevProvider implements DecisionProvider {
|
|
|
166
190
|
evaluate<const Q extends Questions>(request: EvaluationRequest<Q>, options?: ProviderCallOptions): Promise<ProviderResult<Q>>;
|
|
167
191
|
listModels(options?: ProviderCallOptions): Promise<ModelList>;
|
|
168
192
|
getCapabilities(model?: string): Capabilities;
|
|
193
|
+
getResultValidationOptions(model?: string): ResultValidationOptions;
|
|
194
|
+
validateRequest(request: ResolvedRequest): void;
|
|
195
|
+
createDecision<const Q extends JevQuestions>(request: JevDecisionRequest<Q>, options?: ProviderCallOptions):
|
|
196
|
+
Promise<{ readonly decision: JevDecisionResponse<Q>; readonly meta: JevTransportMetadata }>;
|
|
169
197
|
}
|
|
170
198
|
export class OpenAIDecisionsProvider implements DecisionProvider {
|
|
171
199
|
readonly kind: 'decision'; readonly name: 'openai-decisions'; readonly probabilitySource: 'native'; readonly defaultModel: string;
|
|
@@ -187,10 +215,12 @@ export class CloudflareDecisionsProvider implements DecisionProvider {
|
|
|
187
215
|
Promise<{ readonly decision: CloudflareDecisionResponse; readonly meta: CloudflareTransportMetadata }>;
|
|
188
216
|
}
|
|
189
217
|
export type CloudflareDecisionsConfig = { type: 'cloudflare-decisions' } & CloudflareDecisionsOptions;
|
|
190
|
-
export type
|
|
218
|
+
export type JevConfig = { type: 'jev' } & JevOptions;
|
|
219
|
+
export type ProviderConfig = JevConfig
|
|
191
220
|
| ({ type: 'openai-decisions' } & OpenAIDecisionsOptions)
|
|
192
221
|
| CloudflareDecisionsConfig;
|
|
193
222
|
export type OpenAIDecisionsConfig = { type: 'openai-decisions' } & OpenAIDecisionsOptions;
|
|
223
|
+
export function createProvider(options: JevConfig): JevProvider;
|
|
194
224
|
export function createProvider(options: OpenAIDecisionsConfig): OpenAIDecisionsProvider;
|
|
195
225
|
export function createProvider(options: CloudflareDecisionsConfig): CloudflareDecisionsProvider;
|
|
196
226
|
export function createProvider(options: ProviderConfig | OpenAIDecisionsConfig): DecisionProvider;
|
|
@@ -200,6 +230,7 @@ export function choice<const C extends ChoiceCriteria>(instructions: Instruction
|
|
|
200
230
|
export function noul(instructions: Instructions, criteria?: NoulQuestion['criteria']): NoulQuestion;
|
|
201
231
|
export function score<const C extends ScoreCriteria>(instructions: Instructions, criteria: C): ScoreQuestion<C>;
|
|
202
232
|
export function scoreLevels<const L extends ScoreLevels>(instructions: Instructions, levels: L): LabeledScoreQuestion<L>;
|
|
233
|
+
export function box(instructions: Instructions): BoxQuestion;
|
|
203
234
|
export interface ChoicePolicyOptions<K extends string | boolean = string> {
|
|
204
235
|
minProbability: number; minMargin?: number; abstainOptions?: readonly K[];
|
|
205
236
|
/** Provider-declared probability precision; omission retains the baseline tolerance. */
|
|
@@ -214,8 +245,10 @@ export function decideChoice<K extends string>(answer: ChoiceAnswer<K>, options:
|
|
|
214
245
|
export function decideChoice<V extends string | boolean>(answer: TypedChoiceAnswer<V>, options: ChoicePolicyOptions<V>): ChoiceDecision<V>;
|
|
215
246
|
export interface RefusedQuestion { readonly index: number; readonly name: string | null; }
|
|
216
247
|
export interface ErrorDetails { code: string; message: string; status?: number; requestId?: string; gatewayLogId?: string; attempts?: number; upstreamCode?: number;
|
|
248
|
+
source?: 'client' | 'provider'; upstreamErrorCode?: string; upstreamErrorType?: string; retryAfterMs?: number; retryable?: boolean;
|
|
217
249
|
refusedQuestions?: readonly RefusedQuestion[]; usage?: OpenAIDecisionUsage; }
|
|
218
250
|
export class DecisionError extends Error {
|
|
251
|
+
source?: 'client' | 'provider'; upstreamErrorCode?: string; upstreamErrorType?: string; retryAfterMs?: number; retryable?: boolean;
|
|
219
252
|
code: string; status?: number; requestId?: string; gatewayLogId?: string; attempts?: number; upstreamCode?: number;
|
|
220
253
|
refusedQuestions?: readonly RefusedQuestion[]; usage?: OpenAIDecisionUsage;
|
|
221
254
|
constructor(message: string, details?: Omit<ErrorDetails, 'code' | 'message'> & { code?: string });
|
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/input.mjs
CHANGED
|
@@ -20,17 +20,35 @@ export function validateImage(dataUrl, detail, code = 'VALIDATION_ERROR') {
|
|
|
20
20
|
'Unsupported image detail.', code);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** A public reference is forwarded to the provider; the SDK never fetches it. */
|
|
24
|
+
export function validateImageSource(value, detail, code = 'VALIDATION_ERROR') {
|
|
25
|
+
if (typeof value === 'string' && /^https?:/i.test(value)) {
|
|
26
|
+
let url;
|
|
27
|
+
try { url = new URL(value); } catch { /* Validated below. */ }
|
|
28
|
+
check(url && ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password,
|
|
29
|
+
'Images require a public HTTP(S) URL.', code);
|
|
30
|
+
check(detail === undefined || detail === null || ['low', 'high', 'auto', 'original'].includes(detail),
|
|
31
|
+
'Unsupported image detail.', code);
|
|
32
|
+
} else validateImage(value, detail, code);
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
export function validateOrderedInput(input) {
|
|
24
36
|
check(Array.isArray(input) && input.length > 0, 'input must be a nonempty ordered array.');
|
|
25
37
|
for (const part of input) {
|
|
26
38
|
check(part !== null && typeof part === 'object' && !Array.isArray(part), 'Input parts must be objects.');
|
|
27
|
-
const fields = part.type === 'text' ? ['type', 'text'] : part.type === 'image'
|
|
28
|
-
? ['type', 'dataUrl', 'detail'] : ['type', 'dataUrl'];
|
|
39
|
+
const fields = part.type === 'text' ? ['type', 'text', 'promptCacheBreakpoint'] : part.type === 'image'
|
|
40
|
+
? ['type', 'dataUrl', 'url', 'detail', 'promptCacheBreakpoint'] : ['type', 'dataUrl'];
|
|
29
41
|
check(Object.keys(part).every(key => fields.includes(key)), 'Unknown input part field.');
|
|
30
42
|
if (part.type === 'text') check(typeof part.text === 'string', 'Text parts require text.');
|
|
31
43
|
else {
|
|
32
44
|
check(['image', 'audio', 'video'].includes(part.type), 'Input parts must be text, image, audio, or video.');
|
|
33
|
-
if (part.type === 'image')
|
|
45
|
+
if (part.type === 'image') {
|
|
46
|
+
check(Object.hasOwn(part, 'dataUrl') !== Object.hasOwn(part, 'url'), 'Image requires exactly one of dataUrl or url.');
|
|
47
|
+
if (Object.hasOwn(part, 'url')) {
|
|
48
|
+
check(typeof part.url === 'string' && /^https?:/i.test(part.url), 'url must use HTTP(S).');
|
|
49
|
+
validateImageSource(part.url, part.detail);
|
|
50
|
+
} else validateImage(part.dataUrl, part.detail);
|
|
51
|
+
}
|
|
34
52
|
else embeddedDataUrl(part.dataUrl, part.type);
|
|
35
53
|
}
|
|
36
54
|
}
|
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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CallOptions, ErrorDetails, JsonValue } from '../index.mjs';
|
|
2
|
+
import type { NativeDecisionQuestion, NativeDecisionAnswer, OpenAIDecisionResponse } from '../decisions.mjs';
|
|
3
|
+
|
|
4
|
+
export type LiveTranscriptType = 'session.input_transcript.delta' | 'session.output_transcript.delta';
|
|
5
|
+
export interface LiveDecisionsConfiguration {
|
|
6
|
+
readonly delegation?: { readonly type: 'decisions'; readonly decisions: {
|
|
7
|
+
readonly model?: 'gpt-6-luna'; readonly questions?: readonly NativeDecisionQuestion[] } };
|
|
8
|
+
readonly triggers?: readonly { readonly type: 'decisions'; readonly event_type: LiveTranscriptType;
|
|
9
|
+
readonly decisions: { readonly questions: readonly NativeDecisionQuestion[] } }[];
|
|
10
|
+
}
|
|
11
|
+
export interface LiveDecisionItem {
|
|
12
|
+
readonly id?: string | null; readonly role: 'user'; readonly type?: 'message';
|
|
13
|
+
readonly content: string | readonly ({ readonly type: 'input_text'; readonly text: string }
|
|
14
|
+
| { readonly type: 'input_image'; readonly image_url: string; readonly detail?: 'low' | 'high' | 'auto' | 'original' | null })[];
|
|
15
|
+
}
|
|
16
|
+
export type LiveCommand = (
|
|
17
|
+
| { readonly type: 'session.update'; readonly session: LiveDecisionsConfiguration }
|
|
18
|
+
| { readonly type: 'decision.item.create'; readonly item: LiveDecisionItem }
|
|
19
|
+
| { readonly type: 'decision.item.delete'; readonly item_id: string }
|
|
20
|
+
| { readonly type: 'session.commentary.append'; readonly delegation_id: string | null; readonly content: string }
|
|
21
|
+
) & { readonly event_id?: string | null }
|
|
22
|
+
| { readonly type: 'decision.create'; readonly event_id: string };
|
|
23
|
+
export type LiveNativeDecision = Omit<OpenAIDecisionResponse, 'answers'> & {
|
|
24
|
+
readonly answers: readonly (NativeDecisionAnswer | { readonly type: 'refusal'; readonly name: string | null })[];
|
|
25
|
+
};
|
|
26
|
+
export interface LiveDecisionEvent {
|
|
27
|
+
readonly type: 'decision.event'; readonly event_id: string; readonly delegation_id: string | null;
|
|
28
|
+
readonly trigger_id: string | null; readonly source_event_id: string | null; readonly source_event_type: LiveTranscriptType | null;
|
|
29
|
+
readonly event: { readonly type: 'decision.completed'; readonly decision: LiveNativeDecision };
|
|
30
|
+
}
|
|
31
|
+
export type LiveServerEvent = LiveDecisionEvent | { readonly type: string; readonly event_id: string; readonly [key: string]: unknown };
|
|
32
|
+
export type LiveTransportNotification = { readonly kind: 'event'; readonly event: unknown }
|
|
33
|
+
| { readonly kind: 'close' | 'error' };
|
|
34
|
+
export interface LiveTransport {
|
|
35
|
+
send(event: LiveCommand): void | Promise<void>;
|
|
36
|
+
subscribe(listener: (notification: LiveTransportNotification) => void): () => void;
|
|
37
|
+
}
|
|
38
|
+
interface EventMetadata { readonly eventId: string; readonly eventType: string; }
|
|
39
|
+
export interface LiveCorrelation {
|
|
40
|
+
readonly matched: boolean; readonly stale: boolean; readonly sourceEventId: string | null;
|
|
41
|
+
readonly sourceEventType: LiveTranscriptType | null; readonly delegationId: string | null; readonly triggerId: string | null;
|
|
42
|
+
readonly generation?: number; readonly reason?: 'source_or_configuration_unavailable' | 'configuration_mismatch';
|
|
43
|
+
}
|
|
44
|
+
export type LiveConfigurationEvent = EventMetadata & { readonly type: 'configuration';
|
|
45
|
+
readonly configuration: LiveDecisionsConfiguration; readonly generation: number };
|
|
46
|
+
export type LiveItemEvent = EventMetadata & { readonly type: 'item'; readonly item?: LiveDecisionItem; readonly itemId?: string };
|
|
47
|
+
export type LiveNotification =
|
|
48
|
+
| LiveConfigurationEvent | LiveItemEvent
|
|
49
|
+
| (EventMetadata & { readonly type: 'decision'; readonly decision: LiveNativeDecision; readonly correlation: LiveCorrelation })
|
|
50
|
+
| (EventMetadata & { readonly type: 'transcript'; readonly direction: 'input' | 'output'; readonly delta: string; readonly sequence: number })
|
|
51
|
+
| (EventMetadata & { readonly type: 'delegation'; readonly delegationId: string })
|
|
52
|
+
| (EventMetadata & { readonly type: 'duplicate' })
|
|
53
|
+
| { readonly type: 'error' | 'observer_error' | 'closed'; readonly error?: ErrorDetails;
|
|
54
|
+
readonly eventId?: string; readonly eventType?: string; readonly clientEventId?: string };
|
|
55
|
+
export interface LiveSubmissionReceipt { readonly eventId: string; readonly type: string; readonly status: 'submitted'; }
|
|
56
|
+
export interface LiveDecisionsOptions {
|
|
57
|
+
readonly transport: LiveTransport; readonly configuration?: LiveDecisionsConfiguration;
|
|
58
|
+
readonly timeoutMs?: number; readonly maxPending?: number; readonly maxHistory?: number;
|
|
59
|
+
readonly maxRequestBytes?: number; readonly maxResponseBytes?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface LiveDecisionsController {
|
|
62
|
+
configure(configuration: LiveDecisionsConfiguration, options?: CallOptions): Promise<LiveConfigurationEvent>;
|
|
63
|
+
createItem(item: LiveDecisionItem, options?: CallOptions): Promise<LiveItemEvent>;
|
|
64
|
+
deleteItem(itemId: string, options?: CallOptions): Promise<LiveItemEvent>;
|
|
65
|
+
requestDecision(options?: CallOptions): Promise<LiveSubmissionReceipt>;
|
|
66
|
+
appendCommentary(content: string, options?: CallOptions & { delegationId?: string | null }): Promise<LiveSubmissionReceipt>;
|
|
67
|
+
subscribe(observer: (event: LiveNotification) => void | Promise<void>): () => void;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
}
|
|
70
|
+
export function createLiveDecisions(options: LiveDecisionsOptions): LiveDecisionsController;
|
|
71
|
+
export function validateLiveConfiguration(input: unknown): LiveDecisionsConfiguration;
|
|
72
|
+
export function validateLiveCommand(input: unknown): LiveCommand;
|
|
73
|
+
export function validateLiveEvent(input: unknown): LiveServerEvent;
|