@corbits/system-one 0.3.1 → 0.4.0

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/dist/config.js CHANGED
@@ -61,6 +61,8 @@ export const DEFAULT_TIMEOUT_MS = 1500;
61
61
  * distinguishes caller-routed traffic from the official endpoint —
62
62
  * `'gateway'` is reserved for the proxy quirks path. The model always
63
63
  * resolves here: a `custom` target without one sends `typesafe-ai/jev`.
64
+ * `auth` is `"none"` only when the caller set `auth: "none"` on a custom
65
+ * endpoint; every other target still requires a key.
64
66
  */
65
67
  export function resolveEndpoint(endpoint) {
66
68
  if (endpoint?.kind === "custom") {
@@ -71,6 +73,7 @@ export function resolveEndpoint(endpoint) {
71
73
  url: endpoint.url,
72
74
  backend: "custom",
73
75
  model: endpoint.model ?? GATEWAY_QUIRKS.model,
76
+ auth: endpoint.auth === "none" ? "none" : "key",
74
77
  };
75
78
  }
76
79
  const kind = endpoint?.kind ?? "official";
@@ -83,5 +86,6 @@ export function resolveEndpoint(endpoint) {
83
86
  url,
84
87
  backend: kind === "gateway" ? "gateway" : "corbits-system-one",
85
88
  model: endpoint?.model ?? quirks.model,
89
+ auth: "key",
86
90
  };
87
91
  }
@@ -0,0 +1,99 @@
1
+ import { Image } from "./assets.js";
2
+ import type { EvaluateDeps } from "./client.js";
3
+ import { type EvaluateOptions } from "./evaluate.js";
4
+ import { type DecisionQuestion, type Description, type EvaluateConfig, type FallbackResult } from "./schemas.js";
5
+ type ChoiceCriteria = Record<string, Description | null>;
6
+ type NoulCriteria = {
7
+ true?: Description;
8
+ false?: Description;
9
+ };
10
+ export declare function choice<const Criteria extends ChoiceCriteria>(instructions: unknown, criteria: Criteria): {
11
+ type: "choice";
12
+ instructions: unknown;
13
+ criteria: Criteria;
14
+ };
15
+ export declare function noul(instructions: unknown, criteria?: NoulCriteria): {
16
+ type: "noul";
17
+ instructions: unknown;
18
+ criteria?: NoulCriteria;
19
+ };
20
+ export declare function score<const Criteria extends readonly Description[]>(instructions: unknown, criteria: Criteria): {
21
+ type: "score";
22
+ instructions: unknown;
23
+ criteria: Criteria;
24
+ };
25
+ /**
26
+ * Locate what `instructions` describe. The answer's `box` edges are fractions
27
+ * of the image. Box questions need the `jev-preview` model; pass it as
28
+ * `model` (or on the endpoint). This client does not reject other models.
29
+ */
30
+ export declare function box(instructions: unknown): {
31
+ type: "box";
32
+ instructions: unknown;
33
+ };
34
+ type ChoiceAnswer<Criteria extends ChoiceCriteria> = {
35
+ type: "choice";
36
+ choice: keyof Criteria & string;
37
+ probabilities: Record<keyof Criteria & string, number>;
38
+ confidence: number;
39
+ };
40
+ type NoulAnswer = {
41
+ type: "noul";
42
+ noul: number;
43
+ };
44
+ type ScoreAnswer = {
45
+ type: "score";
46
+ score: number;
47
+ legend: Record<string, string>;
48
+ probabilities: Record<string, number>;
49
+ confidence: number;
50
+ };
51
+ type BoxAnswer = {
52
+ type: "box";
53
+ box: {
54
+ top: number;
55
+ bottom: number;
56
+ left: number;
57
+ right: number;
58
+ };
59
+ confidence: number | null;
60
+ };
61
+ type AnswerOf<QuestionSpec> = QuestionSpec extends {
62
+ type: "choice";
63
+ criteria: infer Criteria extends ChoiceCriteria;
64
+ } ? ChoiceAnswer<Criteria> : QuestionSpec extends {
65
+ type: "noul";
66
+ } ? NoulAnswer : QuestionSpec extends {
67
+ type: "score";
68
+ } ? ScoreAnswer : QuestionSpec extends {
69
+ type: "box";
70
+ } ? BoxAnswer : never;
71
+ export type SystemOneAnswers<Questions extends Record<string, unknown>> = {
72
+ [Key in keyof Questions]: AnswerOf<Questions[Key]>;
73
+ };
74
+ export type SystemOneSuccess<Questions extends Record<string, unknown>> = {
75
+ fallback: false;
76
+ answers: SystemOneAnswers<Questions>;
77
+ modelId: string;
78
+ backend: "corbits-system-one" | "gateway" | "custom";
79
+ latencyMs: number;
80
+ usage?: {
81
+ inputTokens: number;
82
+ outputTokens: number;
83
+ };
84
+ };
85
+ export type SystemOneResult<Questions extends Record<string, unknown>> = SystemOneSuccess<Questions> | FallbackResult;
86
+ export type SystemOneInput<Questions extends Record<string, DecisionQuestion>> = {
87
+ state: unknown;
88
+ questions: Questions;
89
+ assets?: Record<string, Image>;
90
+ model?: string;
91
+ config?: EvaluateConfig;
92
+ };
93
+ /**
94
+ * Keyed decisions call. `choice`, `noul`, `score`, and `box` compile to the
95
+ * question list `evaluate` already sends. Transport failures return
96
+ * `fallback` and `reason` and do not throw.
97
+ */
98
+ export declare function systemOne<const Questions extends Record<string, DecisionQuestion>>(input: SystemOneInput<Questions>, options?: Pick<EvaluateOptions, "deps" | "onTelemetry">): Promise<SystemOneResult<Questions>>;
99
+ export type { EvaluateDeps };
@@ -0,0 +1,166 @@
1
+ import { type } from "arktype";
2
+ import { contentTypeOf, Image, serializeRequest } from "./assets.js";
3
+ import { evaluate } from "./evaluate.js";
4
+ import { SystemOneError } from "./errors.js";
5
+ import { DecisionQuestionRecord, QuestionList, } from "./schemas.js";
6
+ export function choice(instructions, criteria) {
7
+ if (Array.isArray(criteria)) {
8
+ throw new SystemOneError("parse-error", "Choice criteria must be a map of labels to descriptions, not a list.");
9
+ }
10
+ return { type: "choice", instructions, criteria };
11
+ }
12
+ export function noul(instructions, criteria) {
13
+ const question = { type: "noul", instructions };
14
+ if (criteria !== undefined)
15
+ question.criteria = criteria;
16
+ return question;
17
+ }
18
+ export function score(instructions, criteria) {
19
+ if (!Array.isArray(criteria)) {
20
+ throw new SystemOneError("parse-error", "Score criteria must be a list of descriptions indexed by score from zero, not a map.");
21
+ }
22
+ return { type: "score", instructions, criteria };
23
+ }
24
+ /**
25
+ * Locate what `instructions` describe. The answer's `box` edges are fractions
26
+ * of the image. Box questions need the `jev-preview` model; pass it as
27
+ * `model` (or on the endpoint). This client does not reject other models.
28
+ */
29
+ export function box(instructions) {
30
+ return { type: "box", instructions };
31
+ }
32
+ function withModel(config, model) {
33
+ const next = {};
34
+ if (config?.apiKey !== undefined)
35
+ next.apiKey = config.apiKey;
36
+ if (config?.timeoutMs !== undefined)
37
+ next.timeoutMs = config.timeoutMs;
38
+ const endpoint = config?.endpoint;
39
+ if (model !== undefined) {
40
+ next.endpoint = { ...(endpoint ?? { kind: "official" }), model };
41
+ }
42
+ else if (endpoint !== undefined) {
43
+ next.endpoint = endpoint;
44
+ }
45
+ return next;
46
+ }
47
+ function answersFrom(decisions) {
48
+ const answers = {};
49
+ for (const decision of decisions) {
50
+ if (decision.type === "choice") {
51
+ answers[decision.id] = {
52
+ type: "choice",
53
+ choice: decision.choice,
54
+ probabilities: decision.probabilities,
55
+ confidence: decision.confidence,
56
+ };
57
+ }
58
+ else if (decision.type === "noul") {
59
+ answers[decision.id] = { type: "noul", noul: decision.noul };
60
+ }
61
+ else if (decision.type === "score") {
62
+ answers[decision.id] = {
63
+ type: "score",
64
+ score: decision.score,
65
+ legend: decision.legend,
66
+ probabilities: decision.probabilities,
67
+ confidence: decision.confidence,
68
+ };
69
+ }
70
+ else {
71
+ answers[decision.id] = {
72
+ type: "box",
73
+ box: decision.box,
74
+ confidence: decision.confidence,
75
+ };
76
+ }
77
+ }
78
+ return answers;
79
+ }
80
+ async function loadImages(assets) {
81
+ const parts = [];
82
+ const wire = {};
83
+ let index = 0;
84
+ for (const [alias, asset] of Object.entries(assets)) {
85
+ let bytes;
86
+ if (asset.data instanceof Blob) {
87
+ bytes = new Uint8Array(await asset.data.arrayBuffer());
88
+ }
89
+ else {
90
+ bytes = asset.data;
91
+ }
92
+ const contentType = contentTypeOf(bytes);
93
+ if (contentType !== "image/png" && contentType !== "image/jpeg") {
94
+ throw new SystemOneError("parse-error", `Image "${alias}" must be PNG or JPEG.`);
95
+ }
96
+ const name = `asset-${index}`;
97
+ parts.push({
98
+ name,
99
+ bytes,
100
+ contentType,
101
+ });
102
+ wire[alias] = { type: "image", part: name };
103
+ index += 1;
104
+ }
105
+ return { assets: wire, parts };
106
+ }
107
+ /**
108
+ * Keyed decisions call. `choice`, `noul`, `score`, and `box` compile to the
109
+ * question list `evaluate` already sends. Transport failures return
110
+ * `fallback` and `reason` and do not throw.
111
+ */
112
+ export async function systemOne(input, options) {
113
+ const parsed = DecisionQuestionRecord(input.questions);
114
+ if (parsed instanceof type.errors) {
115
+ throw new SystemOneError("parse-error", `systemOne: invalid questions: ${parsed.summary}`);
116
+ }
117
+ const serialized = serializeRequest({ state: input.state, questions: input.questions }, input.assets);
118
+ let payload;
119
+ try {
120
+ payload = JSON.parse(serialized.json);
121
+ }
122
+ catch (cause) {
123
+ throw new SystemOneError("parse-error", `systemOne: request is not JSON: ${cause instanceof Error ? cause.message : String(cause)}`);
124
+ }
125
+ const questions = QuestionList(Object.entries(payload.questions !== null &&
126
+ typeof payload.questions === "object" &&
127
+ !Array.isArray(payload.questions)
128
+ ? payload.questions
129
+ : {}).map(([id, question]) => typeof question === "object" && question !== null
130
+ ? { id, ...question }
131
+ : { id }));
132
+ if (questions instanceof type.errors) {
133
+ throw new SystemOneError("parse-error", `systemOne: invalid questions: ${questions.summary}`);
134
+ }
135
+ const state = payload.state;
136
+ if (state === undefined) {
137
+ throw new SystemOneError("parse-error", "systemOne: state is required");
138
+ }
139
+ const images = serialized.assets === undefined
140
+ ? undefined
141
+ : await loadImages(serialized.assets);
142
+ const config = withModel(input.config, input.model);
143
+ const result = await evaluate({
144
+ state: state,
145
+ questions,
146
+ ...(input.config !== undefined || input.model !== undefined
147
+ ? { config }
148
+ : {}),
149
+ }, {
150
+ ...(options?.deps !== undefined ? { deps: options.deps } : {}),
151
+ ...(options?.onTelemetry !== undefined
152
+ ? { onTelemetry: options.onTelemetry }
153
+ : {}),
154
+ ...(images !== undefined ? { images } : {}),
155
+ });
156
+ if (result.fallback)
157
+ return result;
158
+ return {
159
+ fallback: false,
160
+ answers: answersFrom(result.decisions),
161
+ modelId: result.modelId,
162
+ backend: result.backend,
163
+ latencyMs: result.latencyMs,
164
+ ...(result.usage !== undefined ? { usage: result.usage } : {}),
165
+ };
166
+ }
@@ -10,6 +10,18 @@ export declare function checkAnswers(questions: Question[], answers: Record<stri
10
10
  export type EvaluateOptions = {
11
11
  onTelemetry?: (event: SystemOneTelemetryEvent) => void;
12
12
  deps?: EvaluateDeps;
13
+ /** Image parts already checked as PNG or JPEG. Omitted for JSON requests. */
14
+ images?: {
15
+ assets: Record<string, {
16
+ type: "image";
17
+ part: string;
18
+ }>;
19
+ parts: {
20
+ name: string;
21
+ bytes: Uint8Array;
22
+ contentType: "image/png" | "image/jpeg";
23
+ }[];
24
+ };
13
25
  };
14
26
  /**
15
27
  * Runs one typed-decision evaluation against System One (or its gateway)
@@ -20,6 +32,9 @@ export type EvaluateOptions = {
20
32
  * `TYPESAFE_API_KEY` for official, `AI_GATEWAY_API_KEY` then
21
33
  * `VERCEL_OIDC_TOKEN` for gateway — then the `SYSTEM_ONE_API_KEY` alias,
22
34
  * then a fail-closed `'no-key'` fallback with zero fetch calls.
35
+ * A custom endpoint with `auth: "none"` skips that check and sends no
36
+ * `Authorization` header, even if a key is present. Official and gateway
37
+ * always require a key; a custom endpoint without `auth: "none"` does too.
23
38
  * A `timeoutMs` that is not a finite number >= 0 falls back to
24
39
  * `DEFAULT_TIMEOUT_MS`. Transport failures map to matching fallback reasons
25
40
  * (`'timeout'`, `'network'`, `'http-error'`, or `'parse-error'` for a 2xx
package/dist/evaluate.js CHANGED
@@ -3,7 +3,7 @@ import { createDefaultScheduler } from "@intx/inference";
3
3
  import { postEvaluate } from "./client.js";
4
4
  import { DEFAULT_TIMEOUT_MS, GATEWAY_API_KEY_ENV, GATEWAY_OIDC_ENV, resolveEndpoint, SYSTEM_ONE_API_KEY_ENV, TYPESAFE_API_KEY_ENV, } from "./config.js";
5
5
  import { SystemOneError, TransportError, transportFallbackReason, } from "./errors.js";
6
- import { APPROXIMATE_SUM_TOLERANCE as SUM_TOLERANCE, EvaluateInput, toDecision, toWireQuestions, WireRequest, WireResponseBody, } from "./schemas.js";
6
+ import { APPROXIMATE_SUM_TOLERANCE as SUM_TOLERANCE, EvaluateInput, toDecision, toWireQuestions, WireRequest, WireResponseBody, boxEdgesOutOfRange, } from "./schemas.js";
7
7
  // ---------------------------------------------------------------------------
8
8
  // evaluate
9
9
  // ---------------------------------------------------------------------------
@@ -94,7 +94,12 @@ function checkAnswer(question, id, answer) {
94
94
  return `answer "${id}" is missing noul`;
95
95
  return undefined;
96
96
  }
97
- if (answer.confidence === undefined) {
97
+ if (question.type === "box" && answer.type === "box") {
98
+ if (answer.box === undefined)
99
+ return `answer "${id}" is missing box`;
100
+ return boxEdgesOutOfRange(id, answer.box);
101
+ }
102
+ if (answer.confidence == null) {
98
103
  return `answer "${id}" is missing confidence`;
99
104
  }
100
105
  if (question.type === "choice" && answer.type === "choice") {
@@ -183,6 +188,9 @@ function envKey(name) {
183
188
  * `TYPESAFE_API_KEY` for official, `AI_GATEWAY_API_KEY` then
184
189
  * `VERCEL_OIDC_TOKEN` for gateway — then the `SYSTEM_ONE_API_KEY` alias,
185
190
  * then a fail-closed `'no-key'` fallback with zero fetch calls.
191
+ * A custom endpoint with `auth: "none"` skips that check and sends no
192
+ * `Authorization` header, even if a key is present. Official and gateway
193
+ * always require a key; a custom endpoint without `auth: "none"` does too.
186
194
  * A `timeoutMs` that is not a finite number >= 0 falls back to
187
195
  * `DEFAULT_TIMEOUT_MS`. Transport failures map to matching fallback reasons
188
196
  * (`'timeout'`, `'network'`, `'http-error'`, or `'parse-error'` for a 2xx
@@ -195,7 +203,8 @@ function envKey(name) {
195
203
  */
196
204
  export async function evaluate(input, options) {
197
205
  const deps = options?.deps ?? {
198
- fetch: globalThis.fetch,
206
+ // Browsers reject fetch called as a method of another object.
207
+ fetch: (input, init) => fetch(input, init),
199
208
  scheduler: createDefaultScheduler(),
200
209
  };
201
210
  const start = deps.scheduler.now();
@@ -221,7 +230,12 @@ export async function evaluate(input, options) {
221
230
  const rawApiKey = parsed.config?.apiKey;
222
231
  let apiKey;
223
232
  let keySources;
224
- if (rawApiKey !== undefined && rawApiKey !== "") {
233
+ const keyless = endpoint.auth === "none";
234
+ if (keyless) {
235
+ apiKey = undefined;
236
+ keySources = "";
237
+ }
238
+ else if (rawApiKey !== undefined && rawApiKey !== "") {
225
239
  apiKey = rawApiKey;
226
240
  keySources = "config.apiKey";
227
241
  }
@@ -271,15 +285,19 @@ export async function evaluate(input, options) {
271
285
  modelId: model,
272
286
  };
273
287
  emit(startEvent);
274
- if (apiKey === undefined || apiKey === "") {
288
+ if (!keyless && (apiKey === undefined || apiKey === "")) {
275
289
  return fallback("no-key", {
276
290
  detail: `no API key supplied; set config.apiKey or ${keySources}`,
277
291
  });
278
292
  }
293
+ const images = options?.images;
279
294
  const wire = WireRequest({
280
295
  model,
281
296
  state: parsed.state,
282
297
  questions: toWireQuestions(parsed.questions),
298
+ ...(images !== undefined && images.parts.length > 0
299
+ ? { assets: images.assets }
300
+ : {}),
283
301
  });
284
302
  if (wire instanceof type.errors) {
285
303
  throw new SystemOneError("parse-error", `evaluate: internal wire serialization failed: ${wire.summary}`);
@@ -287,7 +305,13 @@ export async function evaluate(input, options) {
287
305
  let data;
288
306
  let transportLatencyMs;
289
307
  try {
290
- const response = await postEvaluate(endpoint.url, wire, { timeoutMs, apiKey }, deps);
308
+ const response = await postEvaluate(endpoint.url, wire, {
309
+ timeoutMs,
310
+ ...(apiKey === undefined ? {} : { apiKey }),
311
+ ...(images !== undefined && images.parts.length > 0
312
+ ? { imageParts: images.parts }
313
+ : {}),
314
+ }, deps);
291
315
  data = response.data;
292
316
  transportLatencyMs = response.latencyMs;
293
317
  }
@@ -0,0 +1,104 @@
1
+ import type { FallbackResult } from "./schemas.js";
2
+ export type TextField = {
3
+ type: "text";
4
+ instructions?: string;
5
+ };
6
+ export type DateField = {
7
+ type: "date";
8
+ instructions?: string;
9
+ };
10
+ export type NumberField = {
11
+ type: "number";
12
+ decimals?: number;
13
+ unit?: string;
14
+ instructions?: string;
15
+ };
16
+ export type ListItemKind = "text" | "number" | "date";
17
+ export type ListField<Items extends ListItemKind = ListItemKind> = {
18
+ type: "list";
19
+ items: Items;
20
+ };
21
+ export type EstimateField = {
22
+ type: "estimate";
23
+ min?: number;
24
+ max?: number;
25
+ step?: number;
26
+ instructions?: string;
27
+ };
28
+ export type ExtractionField = TextField | DateField | NumberField | ListField | EstimateField;
29
+ export type TextAnswer = {
30
+ value: string | null;
31
+ start?: number;
32
+ end?: number;
33
+ };
34
+ export type DateAnswer = {
35
+ value: string | null;
36
+ };
37
+ export type NumberAnswer = {
38
+ value: number | null;
39
+ unit?: string;
40
+ };
41
+ type ListValue<Items extends string> = Items extends "number" ? number : string;
42
+ export type ListAnswer<Items extends string = "text"> = {
43
+ value: ListValue<Items>[] | null;
44
+ };
45
+ export type EstimateAnswer = {
46
+ median: number | null;
47
+ interval?: {
48
+ p10: number;
49
+ p90: number;
50
+ };
51
+ };
52
+ type AnswerOf<Field> = Field extends {
53
+ type: "text";
54
+ } ? TextAnswer : Field extends {
55
+ type: "date";
56
+ } ? DateAnswer : Field extends {
57
+ type: "number";
58
+ } ? NumberAnswer : Field extends {
59
+ type: "list";
60
+ items: infer Items extends string;
61
+ } ? ListAnswer<Items> : Field extends {
62
+ type: "estimate";
63
+ } ? EstimateAnswer : never;
64
+ export type ExtractionAnswers<Fields extends Record<string, ExtractionField>> = {
65
+ [Key in keyof Fields]: AnswerOf<Fields[Key]>;
66
+ };
67
+ export type ExtractionSuccess<Fields extends Record<string, ExtractionField>> = {
68
+ fallback: false;
69
+ answers: ExtractionAnswers<Fields>;
70
+ modelId: string;
71
+ latencyMs: number;
72
+ };
73
+ export type ExtractionResult<Fields extends Record<string, ExtractionField>> = ExtractionSuccess<Fields> | FallbackResult;
74
+ export type ExtractInput<Fields extends Record<string, ExtractionField>> = {
75
+ state: unknown;
76
+ fields: Fields;
77
+ };
78
+ export type ExtractFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
79
+ export type ExtractOptions = {
80
+ accountId?: string;
81
+ token?: string;
82
+ fetch?: ExtractFetch;
83
+ };
84
+ declare function text(instructions?: string): TextField;
85
+ declare function date(instructions?: string): DateField;
86
+ declare function number(options?: {
87
+ decimals?: number;
88
+ unit?: string;
89
+ instructions?: string;
90
+ }): NumberField;
91
+ declare function list<const Items extends ListItemKind>(items: Items): ListField<Items>;
92
+ declare function estimate(options?: {
93
+ min?: number;
94
+ max?: number;
95
+ step?: number;
96
+ instructions?: string;
97
+ }): EstimateField;
98
+ export { date, estimate, list, number, text };
99
+ /**
100
+ * Keyed extraction against Cloudflare Workers AI Clairic. Missing credentials,
101
+ * non-2xx responses, and bodies that are not JSON return `fallback` and do
102
+ * not throw.
103
+ */
104
+ export declare function extract<const Fields extends Record<string, ExtractionField>>(input: ExtractInput<Fields>, options?: ExtractOptions): Promise<ExtractionResult<Fields>>;