@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/providers/jev.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { optionsObject, TRANSPORT_FIELDS } from '../options.mjs';
|
|
2
2
|
import { withResponseMeta, requireCondition as check } from '../errors.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { freezeJson, isRecord, validateResult } from '../validation.mjs';
|
|
4
4
|
import { JsonTransport, parseBaseURL, validateApiKey } from '../transport.mjs';
|
|
5
5
|
import { environmentValue } from '../runtime-env.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { builtInCapabilities } from '../capabilities.mjs';
|
|
7
|
+
import { jevDangerZone, jevHeaders, jevRequestLimit, JEV_RESULT_OPTIONS } from './jev-profile.mjs';
|
|
8
|
+
import { prepareCommonJevDecision, prepareJevDecision } from './jev-preflight.mjs';
|
|
7
9
|
|
|
8
10
|
/** Native TypeSafe HTTP provider: one shared-state request, no chat emulation. */
|
|
9
11
|
export class JevProvider {
|
|
@@ -11,12 +13,13 @@ export class JevProvider {
|
|
|
11
13
|
kind = 'decision';
|
|
12
14
|
probabilitySource = 'native';
|
|
13
15
|
#transport;
|
|
16
|
+
#options;
|
|
14
17
|
|
|
15
18
|
constructor(options = {}) {
|
|
16
19
|
const { apiKey,
|
|
17
20
|
model = environmentValue('JEV_MODEL')?.trim() || 'jev-latest',
|
|
18
|
-
baseURL = 'https://api.typesafe.ai', ...transportOptions } = optionsObject(options,
|
|
19
|
-
['apiKey', 'model', 'baseURL', ...TRANSPORT_FIELDS], 'Jev options');
|
|
21
|
+
baseURL = 'https://api.typesafe.ai', dangerZone, ...transportOptions } = Object.assign(Object.create(null), optionsObject(options,
|
|
22
|
+
['apiKey', 'model', 'baseURL', 'dangerZone', ...TRANSPORT_FIELDS], 'Jev options'));
|
|
20
23
|
const endpointInfo = parseBaseURL(baseURL, { originOnly: true });
|
|
21
24
|
const supplied = apiKey === undefined && endpointInfo.origin === 'https://api.typesafe.ai'
|
|
22
25
|
? environmentValue('TYPESAFE_API_KEY') : apiKey;
|
|
@@ -24,20 +27,36 @@ export class JevProvider {
|
|
|
24
27
|
check(typeof model === 'string' && model.trim(), 'model must be nonempty.', 'CONFIGURATION_ERROR');
|
|
25
28
|
const endpoint = endpointInfo.baseURL;
|
|
26
29
|
this.defaultModel = model.trim();
|
|
27
|
-
this.#
|
|
28
|
-
|
|
30
|
+
this.#options = { model: this.defaultModel, dangerZone: jevDangerZone(dangerZone),
|
|
31
|
+
maxRequestBytes: jevRequestLimit(transportOptions.maxRequestBytes) };
|
|
32
|
+
this.#transport = new JsonTransport({ ...transportOptions, maxRequestBytes: this.#options.maxRequestBytes, baseURL: endpoint,
|
|
33
|
+
headers: { authorization: `Bearer ${key}`, ...jevHeaders(this.#options.dangerZone) }, secrets: [key] });
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
async evaluate(input, options = {}) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return { ...result, meta: { ...meta, provider: this.name, requestedModel: request.model,
|
|
37
|
-
probabilitySource: 'native', confidenceSource: 'native', modelSource: 'reported' } };
|
|
37
|
+
const prepared = prepareCommonJevDecision(input, this.#options);
|
|
38
|
+
const { decision, meta } = await this.#perform(prepared, options);
|
|
39
|
+
return freezeJson({ ...decision, meta: { ...meta, provider: this.name,
|
|
40
|
+
probabilitySource: 'native', confidenceSource: 'native', modelSource: 'reported' } });
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
getCapabilities() { return
|
|
43
|
+
getCapabilities(model = this.defaultModel) { return builtInCapabilities('jev', model, this.#options); }
|
|
44
|
+
|
|
45
|
+
getResultValidationOptions() { return JEV_RESULT_OPTIONS; }
|
|
46
|
+
|
|
47
|
+
validateRequest(request) { prepareCommonJevDecision(request, this.#options); }
|
|
48
|
+
|
|
49
|
+
/** Native JSON assets allow images anywhere in state, instructions or criteria. */
|
|
50
|
+
async createDecision(input, options = {}) {
|
|
51
|
+
return this.#perform(prepareJevDecision(input, this.#options), options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async #perform({ request, native }, options) {
|
|
55
|
+
const { data, meta: transport } = await this.#transport.request('POST', '/v1/systemone', native, options);
|
|
56
|
+
const meta = { ...transport, requestedModel: request.model };
|
|
57
|
+
const decision = withResponseMeta(meta, () => validateResult(data, request.questions, JEV_RESULT_OPTIONS));
|
|
58
|
+
return freezeJson({ decision, meta });
|
|
59
|
+
}
|
|
41
60
|
|
|
42
61
|
async listModels(options = {}) {
|
|
43
62
|
const { data, meta } = await this.#transport.request('GET', '/v1/models', undefined, options);
|
|
@@ -5,8 +5,9 @@ import { builtInCapabilities } from '../capabilities.mjs';
|
|
|
5
5
|
import { JsonTransport, parseBaseURL, validateApiKey } from '../transport.mjs';
|
|
6
6
|
import { environmentValue } from '../runtime-env.mjs';
|
|
7
7
|
import { validateDecisionResponse } from './decisions-validation.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import { preflightCommonDecision,
|
|
8
|
+
import { decodeCommonDecision } from './decisions-codec.mjs';
|
|
9
|
+
import { preflightCommonDecision, prepareCommonDecision, prepareDecision } from './decisions-preflight.mjs';
|
|
10
|
+
import { OPENAI_DEFAULT_MODEL } from './openai-profile.mjs';
|
|
10
11
|
|
|
11
12
|
/** Native Decisions transport. Common and dedicated access share a single codec. */
|
|
12
13
|
export class OpenAIDecisionsProvider {
|
|
@@ -17,7 +18,7 @@ export class OpenAIDecisionsProvider {
|
|
|
17
18
|
#maxRequestBytes;
|
|
18
19
|
|
|
19
20
|
constructor(options = {}) {
|
|
20
|
-
const { apiKey, model =
|
|
21
|
+
const { apiKey, model = OPENAI_DEFAULT_MODEL, baseURL = 'https://api.openai.com', maxRetries = 0, ...transportOptions } =
|
|
21
22
|
optionsObject(options, ['apiKey', 'model', 'baseURL', ...TRANSPORT_FIELDS], 'OpenAI Decisions options');
|
|
22
23
|
const endpoint = parseBaseURL(baseURL, { originOnly: true });
|
|
23
24
|
const supplied = apiKey === undefined && endpoint.origin === 'https://api.openai.com'
|
|
@@ -36,16 +37,22 @@ export class OpenAIDecisionsProvider {
|
|
|
36
37
|
preflightCommonDecision(request, { model: this.defaultModel, maxRequestBytes: this.#maxRequestBytes });
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const { data, meta } = await this.#transport.request('POST', '/v1/decisions',
|
|
42
|
-
const
|
|
40
|
+
/** Dispatch only prepared wire input; keep selection and reported identity independent. */
|
|
41
|
+
async #perform({ request, native }, options) {
|
|
42
|
+
const { data, meta: transport } = await this.#transport.request('POST', '/v1/decisions', native, options);
|
|
43
|
+
const meta = { ...transport, requestedModel: request.model, resolvedModel: native.model };
|
|
44
|
+
const decision = withResponseMeta(meta, () => validateDecisionResponse(data, native));
|
|
43
45
|
return freezeJson({ decision, meta });
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
async createDecision(input, options = {}) {
|
|
49
|
+
const prepared = prepareDecision(input, { model: this.defaultModel, maxRequestBytes: this.#maxRequestBytes });
|
|
50
|
+
return this.#perform(prepared, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
async evaluate(input, options = {}) {
|
|
47
|
-
const
|
|
48
|
-
const { decision, meta } = await this
|
|
49
|
-
return freezeJson(withResponseMeta(meta, () => decodeCommonDecision(decision, request, meta)));
|
|
54
|
+
const prepared = prepareCommonDecision(input, { model: this.defaultModel, maxRequestBytes: this.#maxRequestBytes });
|
|
55
|
+
const { decision, meta } = await this.#perform(prepared, options);
|
|
56
|
+
return freezeJson(withResponseMeta(meta, () => decodeCommonDecision(decision, prepared.request, meta)));
|
|
50
57
|
}
|
|
51
58
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requireCondition as check } from '../errors.mjs';
|
|
2
|
+
|
|
3
|
+
/** Release-owned selectors resolve locally; upstream responses never change this mapping. */
|
|
4
|
+
export const OPENAI_DEFAULT_MODEL = 'default';
|
|
5
|
+
const LUNA_MODEL = 'gpt-6-luna';
|
|
6
|
+
const SELECTORS = Object.freeze({ default: LUNA_MODEL, luna: LUNA_MODEL, [LUNA_MODEL]: LUNA_MODEL });
|
|
7
|
+
const PROFILES = Object.freeze({
|
|
8
|
+
[LUNA_MODEL]: Object.freeze({
|
|
9
|
+
supported: true, orderedInput: true, images: true, typedChoices: true,
|
|
10
|
+
labeledScores: true, safetyIdentifier: true,
|
|
11
|
+
maxImages: 128, maxChoiceOptions: 255, maxScoreLevels: null,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Lookup does not throw for unsupported selections, matching capability-query semantics. */
|
|
16
|
+
export function openaiModelProfile(selector) {
|
|
17
|
+
return typeof selector === 'string' && Object.hasOwn(SELECTORS, selector)
|
|
18
|
+
? PROFILES[SELECTORS[selector]] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveOpenAIModel(selector) {
|
|
22
|
+
check(openaiModelProfile(selector), 'Selected provider/model does not support this model.', 'UNSUPPORTED_CAPABILITY');
|
|
23
|
+
return SELECTORS[selector];
|
|
24
|
+
}
|
package/src/questions.mjs
CHANGED
|
@@ -13,3 +13,5 @@ export const noul = (instructions, criteria) => build({
|
|
|
13
13
|
});
|
|
14
14
|
export const score = (instructions, criteria) => build({ type: 'score', instructions, criteria });
|
|
15
15
|
export const scoreLevels = (instructions, levels) => build({ type: 'score', instructions, levels });
|
|
16
|
+
/** One bounding box; use a separate noul to check whether the target is present. */
|
|
17
|
+
export const box = instructions => build({ type: 'box', instructions });
|
package/src/validation.mjs
CHANGED
|
@@ -97,9 +97,9 @@ export function validateRequest(input, defaultModel) {
|
|
|
97
97
|
for (const [id, q] of Object.entries(request.questions)) {
|
|
98
98
|
check(nonempty(id), 'Question IDs must be nonempty.');
|
|
99
99
|
check(isRecord(q), 'Every question must be an object.');
|
|
100
|
-
allowedKeys(q, q.type === 'choice' ? ['type', 'instructions', 'criteria', 'options'] :
|
|
100
|
+
allowedKeys(q, q.type === 'box' ? ['type', 'instructions'] : q.type === 'choice' ? ['type', 'instructions', 'criteria', 'options'] :
|
|
101
101
|
q.type === 'score' ? ['type', 'instructions', 'criteria', 'levels'] : ['type', 'instructions', 'criteria'], 'question');
|
|
102
|
-
check(['choice', 'noul', 'score'].includes(q.type), 'Question type must be choice, noul, or
|
|
102
|
+
check(['choice', 'noul', 'score', 'box'].includes(q.type), 'Question type must be choice, noul, score, or box.');
|
|
103
103
|
check(own(q, 'instructions') && q.instructions !== null && isEntry(q.instructions),
|
|
104
104
|
'instructions are required and must be text, a JSON object, or an array.');
|
|
105
105
|
if (typeof q.instructions === 'string') check(nonempty(q.instructions), 'instructions must not be empty.');
|
|
@@ -158,7 +158,7 @@ function distribution(value, labels, decimalPlaces) {
|
|
|
158
158
|
|
|
159
159
|
/** Validate, but do not invent, round, or normalize model results. */
|
|
160
160
|
export function validateResult(input, questions, options = {}) {
|
|
161
|
-
const { native, probabilityDecimalPlaces } = validateResultOptions(options);
|
|
161
|
+
const { native, nullableUsage, probabilityDecimalPlaces } = validateResultOptions(options);
|
|
162
162
|
const result = cloneJson(input, 'response', 'INVALID_RESPONSE');
|
|
163
163
|
// Snapshot both sides so rubric comparison uses JSON values without executing accessors.
|
|
164
164
|
const questionSnapshot = cloneJson(questions, 'questions');
|
|
@@ -168,7 +168,7 @@ export function validateResult(input, questions, options = {}) {
|
|
|
168
168
|
check(nonempty(result.model), 'Response must include model.', 'INVALID_RESPONSE');
|
|
169
169
|
exactKeys(result.answers, Object.keys(questionSnapshot), 'answers', 'INVALID_RESPONSE');
|
|
170
170
|
for (const [id, q] of Object.entries(questionSnapshot)) {
|
|
171
|
-
check(isRecord(q) && ['noul', 'choice', 'score'].includes(q.type), 'A valid question type is required.');
|
|
171
|
+
check(isRecord(q) && ['noul', 'choice', 'score', 'box'].includes(q.type), 'A valid question type is required.');
|
|
172
172
|
if (q.type === 'choice' && own(q, 'options')) check(Array.isArray(q.options) && q.options.length > 0 &&
|
|
173
173
|
q.options.every(option => isRecord(option)), 'Typed choice requires nonempty option objects.');
|
|
174
174
|
if (q.type === 'score' && own(q, 'levels')) check(Array.isArray(q.levels) && q.levels.length >= 2 &&
|
|
@@ -178,13 +178,20 @@ export function validateResult(input, questions, options = {}) {
|
|
|
178
178
|
if (q.type === 'score' && !own(q, 'levels')) check(Array.isArray(q.criteria) && q.criteria.length >= 2 && q.criteria.length <= 10, 'score requires 2–10 levels.');
|
|
179
179
|
const answer = result.answers[id];
|
|
180
180
|
check(isRecord(answer) && answer.type === q.type, 'Answer type must match its question.', 'INVALID_RESPONSE');
|
|
181
|
-
allowedKeys(answer, q.type === 'noul' ? ['type', 'noul'] : q.type === 'choice'
|
|
181
|
+
allowedKeys(answer, q.type === 'box' ? ['type', 'box', 'confidence'] : q.type === 'noul' ? ['type', 'noul'] : q.type === 'choice'
|
|
182
182
|
? ['type', 'choice', 'probabilities', 'confidence'] : ['type', 'score', 'legend', 'probabilities', 'confidence'], 'answer', 'INVALID_RESPONSE');
|
|
183
183
|
if (q.type === 'noul') {
|
|
184
184
|
probability(answer.noul, 'noul');
|
|
185
185
|
continue;
|
|
186
186
|
}
|
|
187
187
|
if (native || own(answer, 'confidence')) probability(answer.confidence, 'confidence');
|
|
188
|
+
if (q.type === 'box') {
|
|
189
|
+
exactKeys(answer.box, ['top', 'bottom', 'left', 'right'], 'box', 'INVALID_RESPONSE');
|
|
190
|
+
Object.values(answer.box).forEach(value => probability(value, 'box edge'));
|
|
191
|
+
check(answer.box.left < answer.box.right && answer.box.top < answer.box.bottom,
|
|
192
|
+
'Box edges must enclose a positive area.', 'INVALID_RESPONSE');
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
188
195
|
if (q.type === 'choice' && q.options) {
|
|
189
196
|
validateTypedChoice(answer, q.options, probabilityDecimalPlaces);
|
|
190
197
|
continue;
|
|
@@ -215,7 +222,7 @@ export function validateResult(input, questions, options = {}) {
|
|
|
215
222
|
if (own(result.usage, field)) check(Number.isSafeInteger(result.usage[field]) && result.usage[field] >= 0, 'Invalid cache token count.', 'INVALID_RESPONSE');
|
|
216
223
|
}
|
|
217
224
|
for (const field of ['input_tokens', 'output_tokens']) {
|
|
218
|
-
check((!native && result.usage[field] === null) ||
|
|
225
|
+
check(((!native || nullableUsage) && result.usage[field] === null) ||
|
|
219
226
|
(Number.isSafeInteger(result.usage[field]) && result.usage[field] >= 0),
|
|
220
227
|
'Token counts must be nonnegative safe integers.', 'INVALID_RESPONSE');
|
|
221
228
|
}
|
package/src/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Keep in sync with package.json; scripts/check.mjs verifies the release version. */
|
|
2
|
-
export const VERSION = '0.1.
|
|
2
|
+
export const VERSION = '0.1.2';
|