@secondlayer/shared 11.20.0 → 11.21.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/src/archive/range-digest.d.ts +39 -0
- package/dist/src/archive/semantic-digest-builder.d.ts +39 -0
- package/dist/src/classify/index.d.ts +83 -0
- package/dist/src/classify/index.js +464 -0
- package/dist/src/classify/index.js.map +18 -0
- package/dist/src/coverage/index.d.ts +39 -0
- package/dist/src/db/index.d.ts +46 -1
- package/dist/src/db/index.js +3 -1
- package/dist/src/db/index.js.map +3 -3
- package/dist/src/db/queries/chain-reorgs.d.ts +39 -0
- package/dist/src/db/queries/chain-reorgs.js +3 -1
- package/dist/src/db/queries/chain-reorgs.js.map +3 -3
- package/dist/src/db/queries/contracts.d.ts +39 -0
- package/dist/src/db/queries/instance.d.ts +39 -0
- package/dist/src/db/queries/integrity.d.ts +39 -0
- package/dist/src/db/queries/subgraph-gaps.d.ts +39 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +39 -0
- package/dist/src/db/queries/subgraphs.d.ts +39 -0
- package/dist/src/db/queries/sync-scope.d.ts +39 -0
- package/dist/src/db/queries/webhooks.d.ts +39 -0
- package/dist/src/db/schema.d.ts +44 -1
- package/dist/src/error-envelope.d.ts +48 -0
- package/dist/src/error-envelope.js +76 -0
- package/dist/src/error-envelope.js.map +10 -0
- package/dist/src/index.d.ts +46 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +3 -3
- package/dist/src/node/local-client.d.ts +39 -0
- package/dist/src/queue/listener.js +3 -1
- package/dist/src/queue/listener.js.map +3 -3
- package/migrations/0155_api_failed_requests.ts +37 -0
- package/migrations/0156_feedback_tickets.ts +47 -0
- package/package.json +17 -1
|
@@ -847,6 +847,8 @@ interface Database {
|
|
|
847
847
|
sentinel_accounts: SentinelAccountsTable;
|
|
848
848
|
waitlists: WaitlistsTable;
|
|
849
849
|
waitlist_signups: WaitlistSignupsTable;
|
|
850
|
+
api_failed_requests: ApiFailedRequestsTable;
|
|
851
|
+
feedback_tickets: FeedbackTicketsTable;
|
|
850
852
|
}
|
|
851
853
|
interface ServiceHeartbeatsTable {
|
|
852
854
|
name: string;
|
|
@@ -1055,6 +1057,43 @@ interface WaitlistSignupsTable {
|
|
|
1055
1057
|
created_at: Generated<Date>;
|
|
1056
1058
|
}
|
|
1057
1059
|
/**
|
|
1060
|
+
* 24h hosted record of a failed API request, keyed by `request_id` and
|
|
1061
|
+
* scoped to the account that made it. `query` is redacted before insert.
|
|
1062
|
+
*/
|
|
1063
|
+
interface ApiFailedRequestsTable {
|
|
1064
|
+
request_id: string;
|
|
1065
|
+
account_id: string;
|
|
1066
|
+
method: string;
|
|
1067
|
+
path: string;
|
|
1068
|
+
status: number;
|
|
1069
|
+
code: string;
|
|
1070
|
+
message: Generated<string>;
|
|
1071
|
+
query: Record<string, unknown>;
|
|
1072
|
+
origin: string | null;
|
|
1073
|
+
created_at: Generated<Date>;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Hosted problem report. `attempted` is the server's own copy of the failed
|
|
1077
|
+
* request (never caller-supplied); `status` starts `new` until classified.
|
|
1078
|
+
*/
|
|
1079
|
+
interface FeedbackTicketsTable {
|
|
1080
|
+
id: Generated<string>;
|
|
1081
|
+
account_id: string;
|
|
1082
|
+
idempotency_key: string | null;
|
|
1083
|
+
request_id: string | null;
|
|
1084
|
+
intent: string;
|
|
1085
|
+
expected: Record<string, unknown> | null;
|
|
1086
|
+
kind_hint: string | null;
|
|
1087
|
+
evidence: Record<string, unknown> | null;
|
|
1088
|
+
attempted: Record<string, unknown> | null;
|
|
1089
|
+
origin: string;
|
|
1090
|
+
status: Generated<"new" | "classified">;
|
|
1091
|
+
route: string | null;
|
|
1092
|
+
classification: Record<string, unknown> | null;
|
|
1093
|
+
classified_at: Date | null;
|
|
1094
|
+
created_at: Generated<Date>;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1058
1097
|
* Cheap, SQL-computed digests over a height range — the basis of a verify that
|
|
1059
1098
|
* runs in seconds instead of hours.
|
|
1060
1099
|
*
|
|
@@ -847,6 +847,8 @@ interface Database {
|
|
|
847
847
|
sentinel_accounts: SentinelAccountsTable;
|
|
848
848
|
waitlists: WaitlistsTable;
|
|
849
849
|
waitlist_signups: WaitlistSignupsTable;
|
|
850
|
+
api_failed_requests: ApiFailedRequestsTable;
|
|
851
|
+
feedback_tickets: FeedbackTicketsTable;
|
|
850
852
|
}
|
|
851
853
|
interface ServiceHeartbeatsTable {
|
|
852
854
|
name: string;
|
|
@@ -1055,6 +1057,43 @@ interface WaitlistSignupsTable {
|
|
|
1055
1057
|
created_at: Generated<Date>;
|
|
1056
1058
|
}
|
|
1057
1059
|
/**
|
|
1060
|
+
* 24h hosted record of a failed API request, keyed by `request_id` and
|
|
1061
|
+
* scoped to the account that made it. `query` is redacted before insert.
|
|
1062
|
+
*/
|
|
1063
|
+
interface ApiFailedRequestsTable {
|
|
1064
|
+
request_id: string;
|
|
1065
|
+
account_id: string;
|
|
1066
|
+
method: string;
|
|
1067
|
+
path: string;
|
|
1068
|
+
status: number;
|
|
1069
|
+
code: string;
|
|
1070
|
+
message: Generated<string>;
|
|
1071
|
+
query: Record<string, unknown>;
|
|
1072
|
+
origin: string | null;
|
|
1073
|
+
created_at: Generated<Date>;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Hosted problem report. `attempted` is the server's own copy of the failed
|
|
1077
|
+
* request (never caller-supplied); `status` starts `new` until classified.
|
|
1078
|
+
*/
|
|
1079
|
+
interface FeedbackTicketsTable {
|
|
1080
|
+
id: Generated<string>;
|
|
1081
|
+
account_id: string;
|
|
1082
|
+
idempotency_key: string | null;
|
|
1083
|
+
request_id: string | null;
|
|
1084
|
+
intent: string;
|
|
1085
|
+
expected: Record<string, unknown> | null;
|
|
1086
|
+
kind_hint: string | null;
|
|
1087
|
+
evidence: Record<string, unknown> | null;
|
|
1088
|
+
attempted: Record<string, unknown> | null;
|
|
1089
|
+
origin: string;
|
|
1090
|
+
status: Generated<"new" | "classified">;
|
|
1091
|
+
route: string | null;
|
|
1092
|
+
classification: Record<string, unknown> | null;
|
|
1093
|
+
classified_at: Date | null;
|
|
1094
|
+
created_at: Generated<Date>;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1058
1097
|
* v1 canonical byte encoding for chain identity — the shape that lets
|
|
1059
1098
|
* independently-run runtimes agree on the same block.
|
|
1060
1099
|
*
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type ClassifierName = "jev" | "kev" | "clef" | "rules";
|
|
2
|
+
type Question = {
|
|
3
|
+
type: "choice"
|
|
4
|
+
instructions: string
|
|
5
|
+
criteria: Record<string, string>
|
|
6
|
+
} | {
|
|
7
|
+
type: "score"
|
|
8
|
+
instructions: string
|
|
9
|
+
criteria: readonly string[]
|
|
10
|
+
} | {
|
|
11
|
+
type: "boolean"
|
|
12
|
+
instructions: string
|
|
13
|
+
criteria: {
|
|
14
|
+
true: string
|
|
15
|
+
false: string
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
type Answer = {
|
|
19
|
+
type: "choice"
|
|
20
|
+
choice: string
|
|
21
|
+
probabilities: Record<string, number>
|
|
22
|
+
confidence: number
|
|
23
|
+
} | {
|
|
24
|
+
type: "score"
|
|
25
|
+
score: number
|
|
26
|
+
probabilities: Record<string, number>
|
|
27
|
+
confidence: number
|
|
28
|
+
} | {
|
|
29
|
+
type: "boolean"
|
|
30
|
+
probability: number
|
|
31
|
+
confidence: number
|
|
32
|
+
} | {
|
|
33
|
+
type: "refusal"
|
|
34
|
+
};
|
|
35
|
+
type Answers<Q extends Record<string, Question>> = { [K in keyof Q] : Answer };
|
|
36
|
+
type ClassifyResult<Q extends Record<string, Question>> = {
|
|
37
|
+
provider: ClassifierName
|
|
38
|
+
/** response.modelId when the backend reports one. */
|
|
39
|
+
modelId: string
|
|
40
|
+
answers: Answers<Q>
|
|
41
|
+
};
|
|
42
|
+
/** Deterministic answers for the offline provider. Return a value per question
|
|
43
|
+
* it can decide; omitted questions come back as `{ type: "refusal" }`. */
|
|
44
|
+
type RuleValue = string | number | boolean;
|
|
45
|
+
type RulesFn<Q extends Record<string, Question>> = (state: string | Record<string, unknown>) => { [K in keyof Q]? : RuleValue } | null;
|
|
46
|
+
declare class ClassifierInputError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One classifier call over jev, kev, clef or offline rules.
|
|
51
|
+
* Returns null on any provider failure (fail-open: callers decide what null
|
|
52
|
+
* means). Only ClassifierInputError, a bad question definition, escapes.
|
|
53
|
+
* Never logs state or response bodies.
|
|
54
|
+
*/
|
|
55
|
+
declare function classify<const Q extends Record<string, Question>>(input: {
|
|
56
|
+
state: string | Record<string, unknown>
|
|
57
|
+
questions: Q
|
|
58
|
+
rules?: RulesFn<Q>
|
|
59
|
+
/** Overrides resolveProvider(); used by classifier-compare. */
|
|
60
|
+
provider?: ClassifierName
|
|
61
|
+
/** Overrides CLASSIFIER_MODEL / the provider default. */
|
|
62
|
+
modelId?: string
|
|
63
|
+
/** Default 5000. */
|
|
64
|
+
timeoutMs?: number
|
|
65
|
+
env?: Record<string, string | undefined>
|
|
66
|
+
/** Tests only (kev/clef). */
|
|
67
|
+
fetchImpl?: typeof fetch
|
|
68
|
+
}): Promise<ClassifyResult<Q> | null>;
|
|
69
|
+
type Env = Record<string, string | undefined>;
|
|
70
|
+
/** CLASSIFIER picks the backend; unset means jev when the gateway key is set,
|
|
71
|
+
* else offline rules. A backend missing credentials falls back to rules. */
|
|
72
|
+
declare function resolveProvider(env?: Env): {
|
|
73
|
+
name: ClassifierName
|
|
74
|
+
modelId: string
|
|
75
|
+
};
|
|
76
|
+
/** (p_max - 1/K) / (1 - 1/K). One option gives 1. */
|
|
77
|
+
declare function choiceConfidence(probabilities: Record<string, number>): number;
|
|
78
|
+
/** max(0, 1 - E|level - mode| / D); D = mean distance of a uniform
|
|
79
|
+
* distribution over the levels from the middle level. */
|
|
80
|
+
declare function scoreConfidence(probabilities: Record<string, number>, levels: number): number;
|
|
81
|
+
/** Host-defined: |2p - 1|. Not comparable with choice/score confidence. */
|
|
82
|
+
declare function booleanConfidence(p: number): number;
|
|
83
|
+
export { Answer, Answers, ClassifierInputError, ClassifierName, ClassifyResult, Question, RuleValue, RulesFn, booleanConfidence, choiceConfidence, classify, resolveProvider, scoreConfidence };
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
// src/env.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var networksSchema = z.string().transform((val) => {
|
|
4
|
+
const networks = val.split(",").map((n) => n.trim()).filter(Boolean);
|
|
5
|
+
const valid = ["mainnet", "testnet"];
|
|
6
|
+
for (const n of networks) {
|
|
7
|
+
if (!valid.includes(n)) {
|
|
8
|
+
throw new Error(`Invalid network: ${n}. Must be one of: ${valid.join(", ")}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return networks;
|
|
12
|
+
});
|
|
13
|
+
var envSchema = z.object({
|
|
14
|
+
DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
15
|
+
SOURCE_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
16
|
+
TARGET_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
17
|
+
NETWORK: z.enum(["mainnet", "testnet"]).optional(),
|
|
18
|
+
NETWORKS: networksSchema.optional(),
|
|
19
|
+
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
20
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development")
|
|
21
|
+
});
|
|
22
|
+
var cachedEnv = null;
|
|
23
|
+
function getEnv() {
|
|
24
|
+
if (cachedEnv) {
|
|
25
|
+
return cachedEnv;
|
|
26
|
+
}
|
|
27
|
+
const result = envSchema.safeParse(process.env);
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
console.error("❌ Invalid environment configuration:");
|
|
30
|
+
console.error(z.treeifyError(result.error));
|
|
31
|
+
throw new Error("Invalid environment configuration");
|
|
32
|
+
}
|
|
33
|
+
let enabledNetworks;
|
|
34
|
+
if (result.data.NETWORKS && result.data.NETWORKS.length > 0) {
|
|
35
|
+
enabledNetworks = result.data.NETWORKS;
|
|
36
|
+
} else if (result.data.NETWORK) {
|
|
37
|
+
enabledNetworks = [result.data.NETWORK];
|
|
38
|
+
} else {
|
|
39
|
+
enabledNetworks = ["mainnet"];
|
|
40
|
+
}
|
|
41
|
+
cachedEnv = { ...result.data, enabledNetworks };
|
|
42
|
+
return cachedEnv;
|
|
43
|
+
}
|
|
44
|
+
function isProductionEnv() {
|
|
45
|
+
return process.env["NODE_ENV"] === "production";
|
|
46
|
+
}
|
|
47
|
+
function isPox4DecoderEnabled(env = process.env) {
|
|
48
|
+
return env.POX4_DECODER_ENABLED !== "false";
|
|
49
|
+
}
|
|
50
|
+
function isPox5DecoderEnabled(env = process.env) {
|
|
51
|
+
return env.POX5_DECODER_ENABLED !== "false";
|
|
52
|
+
}
|
|
53
|
+
function isSbtcDecoderEnabled(env = process.env) {
|
|
54
|
+
return env.SBTC_DECODER_ENABLED !== "false";
|
|
55
|
+
}
|
|
56
|
+
function isBnsDecoderEnabled(env = process.env) {
|
|
57
|
+
return env.BNS_DECODER_ENABLED === "true";
|
|
58
|
+
}
|
|
59
|
+
// src/logger.ts
|
|
60
|
+
var LOG_LEVELS = {
|
|
61
|
+
debug: 0,
|
|
62
|
+
info: 1,
|
|
63
|
+
warn: 2,
|
|
64
|
+
error: 3
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
class Logger {
|
|
68
|
+
_level;
|
|
69
|
+
_isProduction;
|
|
70
|
+
_initialized = false;
|
|
71
|
+
init() {
|
|
72
|
+
if (this._initialized)
|
|
73
|
+
return;
|
|
74
|
+
this._initialized = true;
|
|
75
|
+
try {
|
|
76
|
+
const env = getEnv();
|
|
77
|
+
this._level = env.LOG_LEVEL;
|
|
78
|
+
this._isProduction = env.NODE_ENV === "production";
|
|
79
|
+
} catch {
|
|
80
|
+
this._level = "info";
|
|
81
|
+
this._isProduction = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get level() {
|
|
85
|
+
this.init();
|
|
86
|
+
return this._level;
|
|
87
|
+
}
|
|
88
|
+
get isProduction() {
|
|
89
|
+
this.init();
|
|
90
|
+
return this._isProduction;
|
|
91
|
+
}
|
|
92
|
+
shouldLog(level) {
|
|
93
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
|
|
94
|
+
}
|
|
95
|
+
formatMessage(level, message, meta) {
|
|
96
|
+
const timestamp = new Date().toISOString();
|
|
97
|
+
if (this.isProduction) {
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
timestamp,
|
|
100
|
+
level,
|
|
101
|
+
message,
|
|
102
|
+
...meta
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
106
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;
|
|
107
|
+
}
|
|
108
|
+
debug(message, meta) {
|
|
109
|
+
if (this.shouldLog("debug")) {
|
|
110
|
+
console.debug(this.formatMessage("debug", message, meta));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
info(message, meta) {
|
|
114
|
+
if (this.shouldLog("info")) {
|
|
115
|
+
console.info(this.formatMessage("info", message, meta));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
warn(message, meta) {
|
|
119
|
+
if (this.shouldLog("warn")) {
|
|
120
|
+
console.warn(this.formatMessage("warn", message, meta));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
error(message, meta) {
|
|
124
|
+
if (this.shouldLog("error")) {
|
|
125
|
+
console.error(this.formatMessage("error", message, meta));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
var logger = new Logger;
|
|
130
|
+
|
|
131
|
+
// src/classify/confidence.ts
|
|
132
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
133
|
+
function choiceConfidence(probabilities) {
|
|
134
|
+
const ps = Object.values(probabilities);
|
|
135
|
+
const k = ps.length;
|
|
136
|
+
if (k <= 1)
|
|
137
|
+
return 1;
|
|
138
|
+
const pmax = Math.max(...ps);
|
|
139
|
+
return clamp01((pmax - 1 / k) / (1 - 1 / k));
|
|
140
|
+
}
|
|
141
|
+
function scoreConfidence(probabilities, levels) {
|
|
142
|
+
if (levels <= 1)
|
|
143
|
+
return 1;
|
|
144
|
+
let mode = 0;
|
|
145
|
+
let best = -1;
|
|
146
|
+
for (let i = 0;i < levels; i++) {
|
|
147
|
+
const p = probabilities[String(i)] ?? 0;
|
|
148
|
+
if (p > best) {
|
|
149
|
+
best = p;
|
|
150
|
+
mode = i;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let e = 0;
|
|
154
|
+
let d = 0;
|
|
155
|
+
for (let i = 0;i < levels; i++) {
|
|
156
|
+
e += (probabilities[String(i)] ?? 0) * Math.abs(i - mode);
|
|
157
|
+
d += Math.abs(i - (levels - 1) / 2);
|
|
158
|
+
}
|
|
159
|
+
d /= levels;
|
|
160
|
+
return clamp01(1 - e / d);
|
|
161
|
+
}
|
|
162
|
+
function booleanConfidence(p) {
|
|
163
|
+
return Math.abs(2 * p - 1);
|
|
164
|
+
}
|
|
165
|
+
function normalizeAnswer(question, raw) {
|
|
166
|
+
if (raw.type === "refusal")
|
|
167
|
+
return { type: "refusal" };
|
|
168
|
+
if (raw.type === "boolean" && question.type === "boolean") {
|
|
169
|
+
return {
|
|
170
|
+
type: "boolean",
|
|
171
|
+
probability: raw.probability,
|
|
172
|
+
confidence: booleanConfidence(raw.probability)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (raw.type === "choice" && question.type === "choice") {
|
|
176
|
+
const probabilities = raw.probabilities ?? {};
|
|
177
|
+
return {
|
|
178
|
+
type: "choice",
|
|
179
|
+
choice: raw.choice,
|
|
180
|
+
probabilities,
|
|
181
|
+
confidence: raw.probabilities ? choiceConfidence(probabilities) : 0
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (raw.type === "score" && question.type === "score") {
|
|
185
|
+
const probabilities = raw.probabilities ?? {};
|
|
186
|
+
return {
|
|
187
|
+
type: "score",
|
|
188
|
+
score: raw.score,
|
|
189
|
+
probabilities,
|
|
190
|
+
confidence: raw.probabilities ? scoreConfidence(probabilities, question.criteria.length) : 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { type: "refusal" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/classify/types.ts
|
|
197
|
+
class ClassifierInputError extends Error {
|
|
198
|
+
constructor(message) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = "ClassifierInputError";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/classify/limits.ts
|
|
205
|
+
var MAX_QUESTIONS = 64;
|
|
206
|
+
var MIN_CHOICE = 2;
|
|
207
|
+
var MAX_CHOICE = 255;
|
|
208
|
+
var MIN_SCORE = 2;
|
|
209
|
+
var MAX_SCORE = 10;
|
|
210
|
+
var QUESTION_ID = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
211
|
+
var MAX_STATE_CHARS = 24000;
|
|
212
|
+
function validateQuestions(questions) {
|
|
213
|
+
const ids = Object.keys(questions);
|
|
214
|
+
if (ids.length === 0)
|
|
215
|
+
throw new ClassifierInputError("no questions");
|
|
216
|
+
if (ids.length > MAX_QUESTIONS) {
|
|
217
|
+
throw new ClassifierInputError(`more than ${MAX_QUESTIONS} questions`);
|
|
218
|
+
}
|
|
219
|
+
for (const id of ids) {
|
|
220
|
+
if (!QUESTION_ID.test(id)) {
|
|
221
|
+
throw new ClassifierInputError(`invalid question id: ${id}`);
|
|
222
|
+
}
|
|
223
|
+
const q = questions[id];
|
|
224
|
+
if (q.type === "choice") {
|
|
225
|
+
const n = Object.keys(q.criteria).length;
|
|
226
|
+
if (n < MIN_CHOICE || n > MAX_CHOICE) {
|
|
227
|
+
throw new ClassifierInputError(`question ${id}: choice needs ${MIN_CHOICE}-${MAX_CHOICE} options`);
|
|
228
|
+
}
|
|
229
|
+
} else if (q.type === "score") {
|
|
230
|
+
const n = q.criteria.length;
|
|
231
|
+
if (n < MIN_SCORE || n > MAX_SCORE) {
|
|
232
|
+
throw new ClassifierInputError(`question ${id}: score needs ${MIN_SCORE}-${MAX_SCORE} levels`);
|
|
233
|
+
}
|
|
234
|
+
} else if (q.type !== "boolean") {
|
|
235
|
+
throw new ClassifierInputError(`question ${id}: unknown type`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function stateSize(state) {
|
|
240
|
+
return typeof state === "string" ? state.length : JSON.stringify(state).length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/classify/system-one.ts
|
|
244
|
+
function toWireState(state) {
|
|
245
|
+
const parts = state.map((p) => {
|
|
246
|
+
if (p.type === "text")
|
|
247
|
+
return p.text;
|
|
248
|
+
if (p.type === "json")
|
|
249
|
+
return p.value;
|
|
250
|
+
throw new Error("file state unsupported");
|
|
251
|
+
});
|
|
252
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
253
|
+
}
|
|
254
|
+
function renormalize(p) {
|
|
255
|
+
const sum = Object.values(p).reduce((a, b) => a + b, 0);
|
|
256
|
+
if (!(sum > 0))
|
|
257
|
+
return p;
|
|
258
|
+
return Object.fromEntries(Object.entries(p).map(([k, v]) => [k, v / sum]));
|
|
259
|
+
}
|
|
260
|
+
function mapAnswer(a) {
|
|
261
|
+
if (a.type === "noul" && typeof a.noul === "number") {
|
|
262
|
+
return { type: "boolean", probability: a.noul };
|
|
263
|
+
}
|
|
264
|
+
if (a.type === "choice" && typeof a.choice === "string") {
|
|
265
|
+
return {
|
|
266
|
+
type: "choice",
|
|
267
|
+
choice: a.choice,
|
|
268
|
+
...a.probabilities && { probabilities: renormalize(a.probabilities) }
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (a.type === "score" && typeof a.score === "number") {
|
|
272
|
+
if (!a.probabilities)
|
|
273
|
+
return { type: "score", score: a.score };
|
|
274
|
+
const probabilities = renormalize(a.probabilities);
|
|
275
|
+
const score = Object.entries(probabilities).reduce((s, [level, p]) => s + Number(level) * p, 0);
|
|
276
|
+
return { type: "score", score, probabilities };
|
|
277
|
+
}
|
|
278
|
+
return { type: "refusal" };
|
|
279
|
+
}
|
|
280
|
+
function systemOneModel(opts) {
|
|
281
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
282
|
+
return {
|
|
283
|
+
specificationVersion: "v4",
|
|
284
|
+
provider: `secondlayer.${opts.provider}`,
|
|
285
|
+
modelId: opts.modelId,
|
|
286
|
+
supportedQuestionTypes: ["choice", "score", "boolean"],
|
|
287
|
+
async doDecide({ state, questions, abortSignal }) {
|
|
288
|
+
const wireQuestions = Object.fromEntries(Object.entries(questions).map(([id, q]) => [
|
|
289
|
+
id,
|
|
290
|
+
q.type === "boolean" ? { ...q, type: "noul" } : q
|
|
291
|
+
]));
|
|
292
|
+
const res = await doFetch(opts.url, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: {
|
|
295
|
+
"content-type": "application/json",
|
|
296
|
+
...opts.apiKey && { authorization: `Bearer ${opts.apiKey}` }
|
|
297
|
+
},
|
|
298
|
+
body: JSON.stringify({
|
|
299
|
+
model: opts.modelId,
|
|
300
|
+
state: toWireState(state),
|
|
301
|
+
questions: wireQuestions
|
|
302
|
+
}),
|
|
303
|
+
signal: abortSignal
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok)
|
|
306
|
+
throw new Error(`${opts.provider} ${res.status}`);
|
|
307
|
+
const json = await res.json();
|
|
308
|
+
const body = json.result ?? json;
|
|
309
|
+
const answers = Object.fromEntries(Object.entries(body.answers ?? {}).map(([id, a]) => [id, mapAnswer(a)]));
|
|
310
|
+
return {
|
|
311
|
+
answers,
|
|
312
|
+
warnings: [],
|
|
313
|
+
usage: {
|
|
314
|
+
inputTokens: body.usage?.input_tokens,
|
|
315
|
+
outputTokens: body.usage?.output_tokens
|
|
316
|
+
},
|
|
317
|
+
response: { modelId: body.model ?? opts.modelId }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/classify/providers.ts
|
|
324
|
+
var DEFAULT_MODEL = {
|
|
325
|
+
jev: "typesafe-ai/jev",
|
|
326
|
+
kev: "kev-latest",
|
|
327
|
+
clef: "clef"
|
|
328
|
+
};
|
|
329
|
+
var REQUIRED_ENV = {
|
|
330
|
+
jev: ["AI_GATEWAY_API_KEY"],
|
|
331
|
+
kev: ["KEV_URL"],
|
|
332
|
+
clef: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"]
|
|
333
|
+
};
|
|
334
|
+
var envModel = (env) => env.CLASSIFIER_MODEL?.trim() || undefined;
|
|
335
|
+
function resolveProvider(env = process.env) {
|
|
336
|
+
const rules = { name: "rules", modelId: "rules" };
|
|
337
|
+
const requested = env.CLASSIFIER?.trim();
|
|
338
|
+
let name;
|
|
339
|
+
if (!requested) {
|
|
340
|
+
name = env.AI_GATEWAY_API_KEY ? "jev" : "rules";
|
|
341
|
+
} else if (requested === "jev" || requested === "kev" || requested === "clef" || requested === "rules") {
|
|
342
|
+
name = requested;
|
|
343
|
+
} else {
|
|
344
|
+
logger.warn(`classifier: unknown CLASSIFIER=${requested}; using rules`);
|
|
345
|
+
return rules;
|
|
346
|
+
}
|
|
347
|
+
if (name === "rules")
|
|
348
|
+
return rules;
|
|
349
|
+
const missing = REQUIRED_ENV[name].find((v) => !env[v]);
|
|
350
|
+
if (missing) {
|
|
351
|
+
logger.warn(`classifier: CLASSIFIER=${name} missing ${missing}; using rules`);
|
|
352
|
+
return rules;
|
|
353
|
+
}
|
|
354
|
+
return { name, modelId: envModel(env) ?? DEFAULT_MODEL[name] };
|
|
355
|
+
}
|
|
356
|
+
function buildModel(name, modelId, env = process.env, fetchImpl) {
|
|
357
|
+
if (name === "jev")
|
|
358
|
+
return modelId;
|
|
359
|
+
if (name === "kev") {
|
|
360
|
+
return systemOneModel({
|
|
361
|
+
provider: "kev",
|
|
362
|
+
url: `${(env.KEV_URL ?? "").replace(/\/$/, "")}/v1/systemone`,
|
|
363
|
+
modelId,
|
|
364
|
+
apiKey: env.KEV_API_KEY,
|
|
365
|
+
fetchImpl
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return systemOneModel({
|
|
369
|
+
provider: "clef",
|
|
370
|
+
url: `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/ai/run/@cf/cloudflare/${modelId}`,
|
|
371
|
+
modelId,
|
|
372
|
+
apiKey: env.CLOUDFLARE_API_TOKEN,
|
|
373
|
+
fetchImpl
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/classify/rules.ts
|
|
378
|
+
function answerFor(question, value) {
|
|
379
|
+
if (value === undefined)
|
|
380
|
+
return { type: "refusal" };
|
|
381
|
+
if (question.type === "choice") {
|
|
382
|
+
if (typeof value !== "string" || !(value in question.criteria)) {
|
|
383
|
+
return { type: "refusal" };
|
|
384
|
+
}
|
|
385
|
+
const probabilities = Object.fromEntries(Object.keys(question.criteria).map((k) => [k, k === value ? 1 : 0]));
|
|
386
|
+
return { type: "choice", choice: value, probabilities, confidence: 1 };
|
|
387
|
+
}
|
|
388
|
+
if (question.type === "score") {
|
|
389
|
+
const n = question.criteria.length;
|
|
390
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
391
|
+
return { type: "refusal" };
|
|
392
|
+
}
|
|
393
|
+
if (value < 0 || value >= n)
|
|
394
|
+
return { type: "refusal" };
|
|
395
|
+
const probabilities = Object.fromEntries(Array.from({ length: n }, (_, i) => [String(i), i === value ? 1 : 0]));
|
|
396
|
+
return { type: "score", score: value, probabilities, confidence: 1 };
|
|
397
|
+
}
|
|
398
|
+
if (typeof value !== "boolean")
|
|
399
|
+
return { type: "refusal" };
|
|
400
|
+
return { type: "boolean", probability: value ? 1 : 0, confidence: 1 };
|
|
401
|
+
}
|
|
402
|
+
function runRules(questions, fn, state) {
|
|
403
|
+
const values = fn(state) ?? {};
|
|
404
|
+
const out = {};
|
|
405
|
+
for (const id of Object.keys(questions)) {
|
|
406
|
+
out[id] = answerFor(questions[id], values[id]);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/classify/classify.ts
|
|
412
|
+
async function classify(input) {
|
|
413
|
+
validateQuestions(input.questions);
|
|
414
|
+
const env = input.env ?? process.env;
|
|
415
|
+
const resolved = resolveProvider(env);
|
|
416
|
+
const name = input.provider ?? resolved.name;
|
|
417
|
+
if (name === "rules") {
|
|
418
|
+
if (!input.rules)
|
|
419
|
+
return null;
|
|
420
|
+
return {
|
|
421
|
+
provider: "rules",
|
|
422
|
+
modelId: "rules",
|
|
423
|
+
answers: runRules(input.questions, input.rules, input.state)
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const modelId = input.modelId ?? (input.provider ? undefined : envModel(env)) ?? DEFAULT_MODEL[name];
|
|
427
|
+
if (stateSize(input.state) > MAX_STATE_CHARS) {
|
|
428
|
+
logger.warn(`classifier: state over ${MAX_STATE_CHARS} chars; skipped`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const { experimental_decide: decide } = await import("ai");
|
|
433
|
+
const r = await decide({
|
|
434
|
+
model: buildModel(name, modelId, env, input.fetchImpl),
|
|
435
|
+
state: input.state,
|
|
436
|
+
questions: input.questions,
|
|
437
|
+
maxRetries: 1,
|
|
438
|
+
abortSignal: AbortSignal.timeout(input.timeoutMs ?? 5000)
|
|
439
|
+
});
|
|
440
|
+
const answers = {};
|
|
441
|
+
for (const id of Object.keys(input.questions)) {
|
|
442
|
+
answers[id] = normalizeAnswer(input.questions[id], r.answers[id] ?? { type: "refusal" });
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
provider: name,
|
|
446
|
+
modelId: r.response.modelId ?? modelId,
|
|
447
|
+
answers
|
|
448
|
+
};
|
|
449
|
+
} catch (err) {
|
|
450
|
+
logger.warn(`classifier: ${name} failed: ${err instanceof Error ? err.name : "error"}`);
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export {
|
|
455
|
+
ClassifierInputError,
|
|
456
|
+
booleanConfidence,
|
|
457
|
+
choiceConfidence,
|
|
458
|
+
classify,
|
|
459
|
+
resolveProvider,
|
|
460
|
+
scoreConfidence
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
//# debugId=6AA8B48DE6A7F1DB64756E2164756E21
|
|
464
|
+
//# sourceMappingURL=index.js.map
|