cerno-sdk 0.1.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/README.md +103 -0
- package/dist/cjs/answers.d.ts +43 -0
- package/dist/cjs/answers.js +91 -0
- package/dist/cjs/builder.d.ts +31 -0
- package/dist/cjs/builder.js +53 -0
- package/dist/cjs/client.d.ts +39 -0
- package/dist/cjs/client.js +98 -0
- package/dist/cjs/errors.d.ts +37 -0
- package/dist/cjs/errors.js +69 -0
- package/dist/cjs/index.d.ts +12 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types.d.ts +117 -0
- package/dist/cjs/types.js +7 -0
- package/dist/esm/answers.d.ts +43 -0
- package/dist/esm/answers.js +87 -0
- package/dist/esm/builder.d.ts +31 -0
- package/dist/esm/builder.js +49 -0
- package/dist/esm/client.d.ts +39 -0
- package/dist/esm/client.js +94 -0
- package/dist/esm/errors.d.ts +37 -0
- package/dist/esm/errors.js +60 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/types.d.ts +117 -0
- package/dist/esm/types.js +6 -0
- package/package.json +37 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the cerno API.
|
|
3
|
+
*
|
|
4
|
+
* These mirror `spec/openapi.json` exactly. A change here without a change there is a bug.
|
|
5
|
+
*/
|
|
6
|
+
/** Temperature scaling applied to label logits before the softmax. */
|
|
7
|
+
export interface Calibration {
|
|
8
|
+
/** 1 leaves the model's own distribution alone; above 1 flattens, below 1 sharpens. */
|
|
9
|
+
temperature: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ChoiceSpec {
|
|
12
|
+
question?: string;
|
|
13
|
+
options: string[];
|
|
14
|
+
}
|
|
15
|
+
/** A rubric given either as a plain count or as the text of each level, lowest first. */
|
|
16
|
+
export type LevelSpec = number | string[];
|
|
17
|
+
export interface ScoreSpec {
|
|
18
|
+
question?: string;
|
|
19
|
+
levels: LevelSpec;
|
|
20
|
+
}
|
|
21
|
+
export type Question = {
|
|
22
|
+
id: string;
|
|
23
|
+
noul: string;
|
|
24
|
+
} | {
|
|
25
|
+
id: string;
|
|
26
|
+
choice: ChoiceSpec;
|
|
27
|
+
} | {
|
|
28
|
+
id: string;
|
|
29
|
+
score: ScoreSpec;
|
|
30
|
+
};
|
|
31
|
+
export interface SystemOneRequest {
|
|
32
|
+
state: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
calibration?: Calibration;
|
|
35
|
+
questions: Question[];
|
|
36
|
+
}
|
|
37
|
+
export interface OptionProbability {
|
|
38
|
+
option: string;
|
|
39
|
+
probability: number;
|
|
40
|
+
}
|
|
41
|
+
export interface LevelProbability {
|
|
42
|
+
level: number;
|
|
43
|
+
legend: string;
|
|
44
|
+
probability: number;
|
|
45
|
+
}
|
|
46
|
+
interface AnswerBase {
|
|
47
|
+
/**
|
|
48
|
+
* The logprob fed into the softmax, per label. A floor substitution appears here like any
|
|
49
|
+
* other value; `truncated` is what says one happened.
|
|
50
|
+
*/
|
|
51
|
+
raw_logprobs: Record<string, number>;
|
|
52
|
+
/**
|
|
53
|
+
* Whether some label fell outside the host's reporting window, which makes its probability
|
|
54
|
+
* an upper bound rather than an observation.
|
|
55
|
+
*/
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The labels whose `raw_logprobs` entry is an upper bound rather than an observation. Empty
|
|
59
|
+
* unless `truncated`. A server predating this field omits it; `Answers.truncatedLabels` reads
|
|
60
|
+
* that as empty.
|
|
61
|
+
*/
|
|
62
|
+
truncated_labels?: string[];
|
|
63
|
+
}
|
|
64
|
+
export interface NoulAnswer extends AnswerBase {
|
|
65
|
+
type: "noul";
|
|
66
|
+
/** Probability that the answer is yes. */
|
|
67
|
+
noul: number;
|
|
68
|
+
}
|
|
69
|
+
export interface ChoiceAnswer extends AnswerBase {
|
|
70
|
+
type: "choice";
|
|
71
|
+
choice: string;
|
|
72
|
+
/** How peaked the distribution was, in 0..=1. */
|
|
73
|
+
confidence: number;
|
|
74
|
+
/** The winning option's position in the request. */
|
|
75
|
+
index: number;
|
|
76
|
+
probabilities: OptionProbability[];
|
|
77
|
+
}
|
|
78
|
+
export interface ScoreAnswer extends AnswerBase {
|
|
79
|
+
type: "score";
|
|
80
|
+
/** The most likely level, 1-based. */
|
|
81
|
+
score: number;
|
|
82
|
+
/** The probability-weighted mean level — often more useful than the argmax. */
|
|
83
|
+
expected_score: number;
|
|
84
|
+
legend: string;
|
|
85
|
+
/** How peaked the distribution was, in 0..=1. */
|
|
86
|
+
confidence: number;
|
|
87
|
+
probabilities: LevelProbability[];
|
|
88
|
+
}
|
|
89
|
+
export type Answer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
|
|
90
|
+
export interface Usage {
|
|
91
|
+
input_tokens: number;
|
|
92
|
+
questions: number;
|
|
93
|
+
}
|
|
94
|
+
export interface SystemOneResponse {
|
|
95
|
+
answers: Record<string, Answer>;
|
|
96
|
+
model: string;
|
|
97
|
+
usage: Usage;
|
|
98
|
+
timing_ms: {
|
|
99
|
+
total: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export type ErrorCode = "too_many_options" | "too_many_questions" | "invalid_request" | "too_few_options" | "invalid_levels" | "empty_state" | "empty_question" | "duplicate_question_id" | "no_questions" | "unknown_model" | "invalid_calibration" | "no_label_matched" | "host_unavailable" | "host_timeout" | "internal";
|
|
103
|
+
export interface ErrorResponse {
|
|
104
|
+
code: ErrorCode;
|
|
105
|
+
message: string;
|
|
106
|
+
question_id?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface ModelInfo {
|
|
109
|
+
alias: string;
|
|
110
|
+
model: string;
|
|
111
|
+
calibration: Calibration;
|
|
112
|
+
}
|
|
113
|
+
export interface ModelsResponse {
|
|
114
|
+
models: ModelInfo[];
|
|
115
|
+
default: string;
|
|
116
|
+
}
|
|
117
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Answer, SystemOneResponse, Usage } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The answers to one request, with accessors that fail loudly on the wrong id or type.
|
|
4
|
+
*
|
|
5
|
+
* Reaching for `noul("team")` when `team` was a choice is a programming mistake, not a runtime
|
|
6
|
+
* condition, so it throws rather than handing back a default.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Answers {
|
|
9
|
+
readonly model: string;
|
|
10
|
+
readonly usage: Usage;
|
|
11
|
+
readonly timingMs: number;
|
|
12
|
+
private readonly response;
|
|
13
|
+
constructor(response: SystemOneResponse);
|
|
14
|
+
/** The raw answer for `id`. */
|
|
15
|
+
get(id: string): Answer;
|
|
16
|
+
ids(): string[];
|
|
17
|
+
/** Probability that the answer to `id` is yes. */
|
|
18
|
+
noul(id: string): number;
|
|
19
|
+
/** The winning option for `id`. */
|
|
20
|
+
choice(id: string): string;
|
|
21
|
+
/** The winning option's position in the request. */
|
|
22
|
+
index(id: string): number;
|
|
23
|
+
/** The winning level for `id`, 1-based. */
|
|
24
|
+
score(id: string): number;
|
|
25
|
+
/** The probability-weighted mean level for `id`. */
|
|
26
|
+
expectedScore(id: string): number;
|
|
27
|
+
legend(id: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* How peaked the distribution behind `id` was, in 0..=1. A noul has none, as in JEV: its
|
|
30
|
+
* probability is already the whole answer.
|
|
31
|
+
*/
|
|
32
|
+
confidence(id: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Whether some label for `id` fell outside the host's reporting window. When true, that
|
|
35
|
+
* label's probability is an upper bound rather than an observation.
|
|
36
|
+
*/
|
|
37
|
+
truncated(id: string): boolean;
|
|
38
|
+
/** The labels for `id` whose logprob is an upper bound rather than an observation. */
|
|
39
|
+
truncatedLabels(id: string): string[];
|
|
40
|
+
/** The response exactly as the service sent it. */
|
|
41
|
+
raw(): SystemOneResponse;
|
|
42
|
+
private typed;
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { MissingAnswer, WrongAnswerType } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The answers to one request, with accessors that fail loudly on the wrong id or type.
|
|
4
|
+
*
|
|
5
|
+
* Reaching for `noul("team")` when `team` was a choice is a programming mistake, not a runtime
|
|
6
|
+
* condition, so it throws rather than handing back a default.
|
|
7
|
+
*/
|
|
8
|
+
export class Answers {
|
|
9
|
+
model;
|
|
10
|
+
usage;
|
|
11
|
+
timingMs;
|
|
12
|
+
// Declared explicitly rather than as a constructor parameter property: Node's native
|
|
13
|
+
// type-stripping does not support those, and the test suite runs the sources directly.
|
|
14
|
+
response;
|
|
15
|
+
constructor(response) {
|
|
16
|
+
this.response = response;
|
|
17
|
+
this.model = response.model;
|
|
18
|
+
this.usage = response.usage;
|
|
19
|
+
this.timingMs = response.timing_ms.total;
|
|
20
|
+
}
|
|
21
|
+
/** The raw answer for `id`. */
|
|
22
|
+
get(id) {
|
|
23
|
+
const answer = this.response.answers[id];
|
|
24
|
+
if (answer === undefined)
|
|
25
|
+
throw new MissingAnswer(id);
|
|
26
|
+
return answer;
|
|
27
|
+
}
|
|
28
|
+
ids() {
|
|
29
|
+
return Object.keys(this.response.answers);
|
|
30
|
+
}
|
|
31
|
+
/** Probability that the answer to `id` is yes. */
|
|
32
|
+
noul(id) {
|
|
33
|
+
return this.typed(id, "noul").noul;
|
|
34
|
+
}
|
|
35
|
+
/** The winning option for `id`. */
|
|
36
|
+
choice(id) {
|
|
37
|
+
return this.typed(id, "choice").choice;
|
|
38
|
+
}
|
|
39
|
+
/** The winning option's position in the request. */
|
|
40
|
+
index(id) {
|
|
41
|
+
return this.typed(id, "choice").index;
|
|
42
|
+
}
|
|
43
|
+
/** The winning level for `id`, 1-based. */
|
|
44
|
+
score(id) {
|
|
45
|
+
return this.typed(id, "score").score;
|
|
46
|
+
}
|
|
47
|
+
/** The probability-weighted mean level for `id`. */
|
|
48
|
+
expectedScore(id) {
|
|
49
|
+
return this.typed(id, "score").expected_score;
|
|
50
|
+
}
|
|
51
|
+
legend(id) {
|
|
52
|
+
return this.typed(id, "score").legend;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* How peaked the distribution behind `id` was, in 0..=1. A noul has none, as in JEV: its
|
|
56
|
+
* probability is already the whole answer.
|
|
57
|
+
*/
|
|
58
|
+
confidence(id) {
|
|
59
|
+
const answer = this.get(id);
|
|
60
|
+
if (answer.type === "noul") {
|
|
61
|
+
throw new WrongAnswerType(id, "choice or score", answer.type);
|
|
62
|
+
}
|
|
63
|
+
return answer.confidence;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether some label for `id` fell outside the host's reporting window. When true, that
|
|
67
|
+
* label's probability is an upper bound rather than an observation.
|
|
68
|
+
*/
|
|
69
|
+
truncated(id) {
|
|
70
|
+
return this.get(id).truncated;
|
|
71
|
+
}
|
|
72
|
+
/** The labels for `id` whose logprob is an upper bound rather than an observation. */
|
|
73
|
+
truncatedLabels(id) {
|
|
74
|
+
return this.get(id).truncated_labels ?? [];
|
|
75
|
+
}
|
|
76
|
+
/** The response exactly as the service sent it. */
|
|
77
|
+
raw() {
|
|
78
|
+
return this.response;
|
|
79
|
+
}
|
|
80
|
+
typed(id, expected) {
|
|
81
|
+
const answer = this.get(id);
|
|
82
|
+
if (answer.type !== expected) {
|
|
83
|
+
throw new WrongAnswerType(id, expected, answer.type);
|
|
84
|
+
}
|
|
85
|
+
return answer;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Answers } from "./answers.ts";
|
|
2
|
+
import type { LevelSpec, SystemOneRequest } from "./types.ts";
|
|
3
|
+
/** A request under construction. Chain questions onto it, then `send()`. */
|
|
4
|
+
export declare class SystemOneBuilder {
|
|
5
|
+
private readonly request;
|
|
6
|
+
private readonly dispatch;
|
|
7
|
+
constructor(state: string, dispatch: (body: SystemOneRequest) => Promise<Answers>);
|
|
8
|
+
/** Name a model or a configured alias. The service's default applies otherwise. */
|
|
9
|
+
model(model: string): this;
|
|
10
|
+
/** Scale the label logits before normalising. Above 1 flattens, below 1 sharpens. */
|
|
11
|
+
calibration(temperature: number): this;
|
|
12
|
+
/** How likely the answer to `question` is yes. */
|
|
13
|
+
noul(id: string, question: string): this;
|
|
14
|
+
/**
|
|
15
|
+
* One of `options`.
|
|
16
|
+
*
|
|
17
|
+
* Called with two arguments the options come second and no question is sent, which is the
|
|
18
|
+
* right shape when the options speak for themselves.
|
|
19
|
+
*/
|
|
20
|
+
choice(id: string, question: string, options: string[]): this;
|
|
21
|
+
choice(id: string, options: string[]): this;
|
|
22
|
+
/** A position on a rubric: a number for generated levels, or the level texts. */
|
|
23
|
+
score(id: string, question: string, levels: LevelSpec): this;
|
|
24
|
+
/**
|
|
25
|
+
* The request as it will be sent. Useful for logging, and for testing a chain without a
|
|
26
|
+
* server.
|
|
27
|
+
*/
|
|
28
|
+
body(): SystemOneRequest;
|
|
29
|
+
send(): Promise<Answers>;
|
|
30
|
+
private push;
|
|
31
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** A request under construction. Chain questions onto it, then `send()`. */
|
|
2
|
+
export class SystemOneBuilder {
|
|
3
|
+
request;
|
|
4
|
+
dispatch;
|
|
5
|
+
constructor(state, dispatch) {
|
|
6
|
+
this.request = { state, questions: [] };
|
|
7
|
+
this.dispatch = dispatch;
|
|
8
|
+
}
|
|
9
|
+
/** Name a model or a configured alias. The service's default applies otherwise. */
|
|
10
|
+
model(model) {
|
|
11
|
+
this.request.model = model;
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
/** Scale the label logits before normalising. Above 1 flattens, below 1 sharpens. */
|
|
15
|
+
calibration(temperature) {
|
|
16
|
+
this.request.calibration = { temperature };
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
/** How likely the answer to `question` is yes. */
|
|
20
|
+
noul(id, question) {
|
|
21
|
+
return this.push({ id, noul: question });
|
|
22
|
+
}
|
|
23
|
+
choice(id, second, third) {
|
|
24
|
+
const options = third ?? second;
|
|
25
|
+
const question = third === undefined ? undefined : second;
|
|
26
|
+
return this.push({
|
|
27
|
+
id,
|
|
28
|
+
choice: question === undefined ? { options } : { question, options },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** A position on a rubric: a number for generated levels, or the level texts. */
|
|
32
|
+
score(id, question, levels) {
|
|
33
|
+
return this.push({ id, score: { question, levels } });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The request as it will be sent. Useful for logging, and for testing a chain without a
|
|
37
|
+
* server.
|
|
38
|
+
*/
|
|
39
|
+
body() {
|
|
40
|
+
return this.request;
|
|
41
|
+
}
|
|
42
|
+
send() {
|
|
43
|
+
return this.dispatch(this.request);
|
|
44
|
+
}
|
|
45
|
+
push(question) {
|
|
46
|
+
this.request.questions.push(question);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SystemOneBuilder } from "./builder.ts";
|
|
2
|
+
import type { ModelsResponse, SystemOneRequest } from "./types.ts";
|
|
3
|
+
export interface ClientOptions {
|
|
4
|
+
/** Defaults to `http://localhost:3000`. */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** Milliseconds before a request is aborted. Defaults to 60000. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** Sent with every request. */
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/** Injected for testing, or to route through a custom transport. */
|
|
11
|
+
fetch?: typeof globalThis.fetch;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Client for the cerno service.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const client = new Client({ baseUrl: "http://localhost:3000" });
|
|
18
|
+
* const answers = await client
|
|
19
|
+
* .systemone("Ticket: server room at 31C, rising.")
|
|
20
|
+
* .noul("urgent", "Is this urgent?")
|
|
21
|
+
* .choice("team", "Which team?", ["IT", "Facility", "HR"])
|
|
22
|
+
* .send();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class Client {
|
|
26
|
+
private readonly baseUrl;
|
|
27
|
+
private readonly timeoutMs;
|
|
28
|
+
private readonly headers;
|
|
29
|
+
private readonly doFetch;
|
|
30
|
+
constructor(options?: ClientOptions | string);
|
|
31
|
+
/** Start a request about `state`. */
|
|
32
|
+
systemone(state: string): SystemOneBuilder;
|
|
33
|
+
/** The models this service will answer for. */
|
|
34
|
+
models(): Promise<ModelsResponse>;
|
|
35
|
+
/** Whether the service is up. False, not an error, when it cannot be reached in time. */
|
|
36
|
+
health(): Promise<boolean>;
|
|
37
|
+
private request;
|
|
38
|
+
}
|
|
39
|
+
export type { SystemOneRequest };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Answers } from "./answers.js";
|
|
2
|
+
import { SystemOneBuilder } from "./builder.js";
|
|
3
|
+
import { ApiError, TransportError, UnexpectedResponse } from "./errors.js";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
5
|
+
/**
|
|
6
|
+
* Client for the cerno service.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const client = new Client({ baseUrl: "http://localhost:3000" });
|
|
10
|
+
* const answers = await client
|
|
11
|
+
* .systemone("Ticket: server room at 31C, rising.")
|
|
12
|
+
* .noul("urgent", "Is this urgent?")
|
|
13
|
+
* .choice("team", "Which team?", ["IT", "Facility", "HR"])
|
|
14
|
+
* .send();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class Client {
|
|
18
|
+
baseUrl;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
headers;
|
|
21
|
+
doFetch;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
const opts = typeof options === "string" ? { baseUrl: options } : options;
|
|
24
|
+
this.baseUrl = (opts.baseUrl ?? "http://localhost:3000").replace(/\/+$/, "");
|
|
25
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
26
|
+
this.headers = opts.headers ?? {};
|
|
27
|
+
this.doFetch = opts.fetch ?? globalThis.fetch;
|
|
28
|
+
}
|
|
29
|
+
/** Start a request about `state`. */
|
|
30
|
+
systemone(state) {
|
|
31
|
+
return new SystemOneBuilder(state, async (body) => {
|
|
32
|
+
const response = await this.request("POST", "/v1/systemone", body);
|
|
33
|
+
return new Answers(response);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** The models this service will answer for. */
|
|
37
|
+
models() {
|
|
38
|
+
return this.request("GET", "/v1/models");
|
|
39
|
+
}
|
|
40
|
+
/** Whether the service is up. False, not an error, when it cannot be reached in time. */
|
|
41
|
+
async health() {
|
|
42
|
+
try {
|
|
43
|
+
const response = await this.doFetch(`${this.baseUrl}/health`, {
|
|
44
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
45
|
+
});
|
|
46
|
+
return response.ok;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async request(method, path, body) {
|
|
53
|
+
// An explicit abort, so a wedged host surfaces as a timeout rather than hanging forever.
|
|
54
|
+
const abort = AbortSignal.timeout(this.timeoutMs);
|
|
55
|
+
let response;
|
|
56
|
+
let text;
|
|
57
|
+
try {
|
|
58
|
+
response = await this.doFetch(`${this.baseUrl}${path}`, {
|
|
59
|
+
method,
|
|
60
|
+
signal: abort,
|
|
61
|
+
headers: {
|
|
62
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
63
|
+
...this.headers,
|
|
64
|
+
},
|
|
65
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
66
|
+
});
|
|
67
|
+
text = await response.text();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new TransportError(`could not reach cerno: ${String(err)}`, err);
|
|
71
|
+
}
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new UnexpectedResponse(response.status, text);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
const candidate = JSON.parse(text);
|
|
83
|
+
if (candidate && typeof candidate.code === "string")
|
|
84
|
+
parsed = candidate;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Falls through to UnexpectedResponse below.
|
|
88
|
+
}
|
|
89
|
+
// Not our error shape, so do not pretend to know what went wrong.
|
|
90
|
+
if (!parsed)
|
|
91
|
+
throw new UnexpectedResponse(response.status, text);
|
|
92
|
+
throw new ApiError(response.status, parsed);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ErrorCode, ErrorResponse } from "./types.ts";
|
|
2
|
+
export declare class CernoError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* The service answered with a structured failure.
|
|
6
|
+
*
|
|
7
|
+
* Branch on `code`, never on `message` — the codes are the contract, the prose is not.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ApiError extends CernoError {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly code: ErrorCode;
|
|
12
|
+
readonly questionId?: string;
|
|
13
|
+
constructor(status: number, body: ErrorResponse);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The service could not be reached, or did not answer within the timeout. The error `fetch`
|
|
17
|
+
* threw is kept as `cause`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class TransportError extends CernoError {
|
|
20
|
+
constructor(message: string, cause: unknown);
|
|
21
|
+
}
|
|
22
|
+
/** A non-2xx response that was not shaped like a cerno error — a proxy, most likely. */
|
|
23
|
+
export declare class UnexpectedResponse extends CernoError {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly body: string;
|
|
26
|
+
constructor(status: number, body: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class MissingAnswer extends CernoError {
|
|
29
|
+
readonly questionId: string;
|
|
30
|
+
constructor(questionId: string);
|
|
31
|
+
}
|
|
32
|
+
export declare class WrongAnswerType extends CernoError {
|
|
33
|
+
readonly questionId: string;
|
|
34
|
+
readonly expected: string;
|
|
35
|
+
readonly actual: string;
|
|
36
|
+
constructor(questionId: string, expected: string, actual: string);
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export class CernoError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/**
|
|
4
|
+
* The service answered with a structured failure.
|
|
5
|
+
*
|
|
6
|
+
* Branch on `code`, never on `message` — the codes are the contract, the prose is not.
|
|
7
|
+
*/
|
|
8
|
+
export class ApiError extends CernoError {
|
|
9
|
+
status;
|
|
10
|
+
code;
|
|
11
|
+
questionId;
|
|
12
|
+
constructor(status, body) {
|
|
13
|
+
super(`cerno returned ${status} (${body.code}): ${body.message}`);
|
|
14
|
+
this.name = "ApiError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = body.code;
|
|
17
|
+
this.questionId = body.question_id;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The service could not be reached, or did not answer within the timeout. The error `fetch`
|
|
22
|
+
* threw is kept as `cause`.
|
|
23
|
+
*/
|
|
24
|
+
export class TransportError extends CernoError {
|
|
25
|
+
constructor(message, cause) {
|
|
26
|
+
super(message, { cause });
|
|
27
|
+
this.name = "TransportError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** A non-2xx response that was not shaped like a cerno error — a proxy, most likely. */
|
|
31
|
+
export class UnexpectedResponse extends CernoError {
|
|
32
|
+
status;
|
|
33
|
+
body;
|
|
34
|
+
constructor(status, body) {
|
|
35
|
+
super(`cerno returned ${status}: ${body.slice(0, 200)}`);
|
|
36
|
+
this.name = "UnexpectedResponse";
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.body = body;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class MissingAnswer extends CernoError {
|
|
42
|
+
questionId;
|
|
43
|
+
constructor(questionId) {
|
|
44
|
+
super(`no answer for question ${JSON.stringify(questionId)}`);
|
|
45
|
+
this.name = "MissingAnswer";
|
|
46
|
+
this.questionId = questionId;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class WrongAnswerType extends CernoError {
|
|
50
|
+
questionId;
|
|
51
|
+
expected;
|
|
52
|
+
actual;
|
|
53
|
+
constructor(questionId, expected, actual) {
|
|
54
|
+
super(`question ${JSON.stringify(questionId)} answered with a ${actual}, not a ${expected}`);
|
|
55
|
+
this.name = "WrongAnswerType";
|
|
56
|
+
this.questionId = questionId;
|
|
57
|
+
this.expected = expected;
|
|
58
|
+
this.actual = actual;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript client for cerno.
|
|
3
|
+
*
|
|
4
|
+
* cerno answers three kinds of question about a piece of text — is it true (`noul`), which one
|
|
5
|
+
* is it (`choice`), where on a scale does it sit (`score`) — using a locally hosted model. Each
|
|
6
|
+
* question is one forward pass, so answers come back in tens of milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
export { Answers } from "./answers.ts";
|
|
9
|
+
export { SystemOneBuilder } from "./builder.ts";
|
|
10
|
+
export { Client, type ClientOptions } from "./client.ts";
|
|
11
|
+
export { ApiError, CernoError, MissingAnswer, TransportError, UnexpectedResponse, WrongAnswerType, } from "./errors.ts";
|
|
12
|
+
export type * from "./types.ts";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript client for cerno.
|
|
3
|
+
*
|
|
4
|
+
* cerno answers three kinds of question about a piece of text — is it true (`noul`), which one
|
|
5
|
+
* is it (`choice`), where on a scale does it sit (`score`) — using a locally hosted model. Each
|
|
6
|
+
* question is one forward pass, so answers come back in tens of milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
export { Answers } from "./answers.js";
|
|
9
|
+
export { SystemOneBuilder } from "./builder.js";
|
|
10
|
+
export { Client } from "./client.js";
|
|
11
|
+
export { ApiError, CernoError, MissingAnswer, TransportError, UnexpectedResponse, WrongAnswerType, } from "./errors.js";
|