highflame 0.2.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 +579 -0
- package/dist/index.cjs +705 -0
- package/dist/index.d.cts +644 -0
- package/dist/index.d.ts +644 -0
- package/dist/index.js +666 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var HighflameError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "HighflameError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var APIError = class extends HighflameError {
|
|
9
|
+
constructor(status, title, detail) {
|
|
10
|
+
super(`[${status}] ${title}: ${detail}`);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.title = title;
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
this.name = "APIError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var AuthenticationError = class extends APIError {
|
|
18
|
+
constructor(detail = "Invalid or expired credentials") {
|
|
19
|
+
super(401, "Unauthorized", detail);
|
|
20
|
+
this.name = "AuthenticationError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var RateLimitError = class extends APIError {
|
|
24
|
+
constructor(detail = "Rate limit exceeded") {
|
|
25
|
+
super(429, "Too Many Requests", detail);
|
|
26
|
+
this.name = "RateLimitError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var APIConnectionError = class extends HighflameError {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "APIConnectionError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var BlockedError = class extends HighflameError {
|
|
36
|
+
constructor(response) {
|
|
37
|
+
super(`Blocked by guard: ${response.policy_reason || response.decision}`);
|
|
38
|
+
this.response = response;
|
|
39
|
+
this.name = "BlockedError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/stream.ts
|
|
44
|
+
function* parseSseLines(lines) {
|
|
45
|
+
let event = "detector_result";
|
|
46
|
+
let data = "";
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line === "") {
|
|
49
|
+
if (data) {
|
|
50
|
+
yield { event, data };
|
|
51
|
+
}
|
|
52
|
+
event = "detector_result";
|
|
53
|
+
data = "";
|
|
54
|
+
} else if (line.startsWith("event:")) {
|
|
55
|
+
event = line.slice(6).trim();
|
|
56
|
+
} else if (line.startsWith("data:")) {
|
|
57
|
+
data = line.slice(5).trim();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (data) {
|
|
61
|
+
yield { event, data };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function* streamSseResponse(response) {
|
|
65
|
+
if (!response.body) {
|
|
66
|
+
throw new APIConnectionError("Response body is null");
|
|
67
|
+
}
|
|
68
|
+
const reader = response.body.getReader();
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
let buffer = "";
|
|
71
|
+
try {
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) break;
|
|
75
|
+
buffer += decoder.decode(value, { stream: true });
|
|
76
|
+
const lines = buffer.split("\n");
|
|
77
|
+
buffer = lines.pop() ?? "";
|
|
78
|
+
for (const { event, data } of parseSseLines(lines)) {
|
|
79
|
+
if (!data) continue;
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(data);
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
yield {
|
|
87
|
+
type: event,
|
|
88
|
+
data: parsed
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (buffer) {
|
|
93
|
+
const lines = buffer.split("\n");
|
|
94
|
+
for (const { event, data } of parseSseLines([...lines, ""])) {
|
|
95
|
+
if (!data) continue;
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(data);
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
yield {
|
|
103
|
+
type: event,
|
|
104
|
+
data: parsed
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/client.ts
|
|
114
|
+
var VERSION = "0.2.0";
|
|
115
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
116
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
117
|
+
var RETRY_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
118
|
+
var TOKEN_REFRESH_BUFFER_MS = 6e4;
|
|
119
|
+
var USER_AGENT = `highflame-js/${VERSION}`;
|
|
120
|
+
var SAAS_BASE_URL = "https://shield.api.highflame.ai";
|
|
121
|
+
var SAAS_TOKEN_URL = "https://studio.api.highflame.ai/api/cli-auth/token";
|
|
122
|
+
var LOG_BODY_MAX = 1024;
|
|
123
|
+
var NOOP_LOGGER = {
|
|
124
|
+
debug() {
|
|
125
|
+
},
|
|
126
|
+
info() {
|
|
127
|
+
},
|
|
128
|
+
warn() {
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function truncate(text, max = LOG_BODY_MAX) {
|
|
132
|
+
if (text.length <= max) return text;
|
|
133
|
+
return text.slice(0, max) + `... (${text.length} chars)`;
|
|
134
|
+
}
|
|
135
|
+
function sleep(ms) {
|
|
136
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
|
+
}
|
|
138
|
+
function retryDelay(attempt) {
|
|
139
|
+
const base = 1e3 * Math.pow(2, attempt);
|
|
140
|
+
const jitter = base * 0.25 * (2 * Math.random() - 1);
|
|
141
|
+
return Math.max(0, base + jitter);
|
|
142
|
+
}
|
|
143
|
+
async function parseError(response) {
|
|
144
|
+
let title = response.statusText || "Error";
|
|
145
|
+
let detail = "";
|
|
146
|
+
try {
|
|
147
|
+
const body = await response.json();
|
|
148
|
+
if (typeof body["title"] === "string") title = body["title"];
|
|
149
|
+
if (typeof body["detail"] === "string") detail = body["detail"];
|
|
150
|
+
} catch {
|
|
151
|
+
try {
|
|
152
|
+
detail = await response.text();
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (response.status === 401) return new AuthenticationError(detail);
|
|
157
|
+
if (response.status === 429) return new RateLimitError(detail);
|
|
158
|
+
return new APIError(response.status, title, detail);
|
|
159
|
+
}
|
|
160
|
+
var GuardResource = class {
|
|
161
|
+
constructor(_client) {
|
|
162
|
+
this._client = _client;
|
|
163
|
+
}
|
|
164
|
+
/** Evaluate content against guard policies (POST /v1/guard). */
|
|
165
|
+
async evaluate(request, options) {
|
|
166
|
+
return this._client._postJSON("/v1/guard", request, options?.timeout);
|
|
167
|
+
}
|
|
168
|
+
/** Shorthand: evaluate a user prompt. */
|
|
169
|
+
async evaluatePrompt(content, options) {
|
|
170
|
+
const { timeout, ...rest } = options ?? {};
|
|
171
|
+
return this.evaluate(
|
|
172
|
+
{ content, content_type: "prompt", action: "process_prompt", ...rest },
|
|
173
|
+
{ timeout }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
/** Shorthand: evaluate a tool call. */
|
|
177
|
+
async evaluateToolCall(toolName, args, options) {
|
|
178
|
+
const { timeout, ...rest } = options ?? {};
|
|
179
|
+
return this.evaluate(
|
|
180
|
+
{
|
|
181
|
+
content: `Tool call: ${toolName}`,
|
|
182
|
+
content_type: "tool_call",
|
|
183
|
+
action: "call_tool",
|
|
184
|
+
tool: { name: toolName, is_builtin: false, arguments: args },
|
|
185
|
+
...rest
|
|
186
|
+
},
|
|
187
|
+
{ timeout }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
/** Stream guard evaluation results (POST /v1/guard/stream). */
|
|
191
|
+
async *stream(request, options) {
|
|
192
|
+
yield* this._client._stream("/v1/guard/stream", request, options?.timeout);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var DetectResource = class {
|
|
196
|
+
constructor(_client) {
|
|
197
|
+
this._client = _client;
|
|
198
|
+
}
|
|
199
|
+
/** Run detectors without policy evaluation (POST /v1/detect). */
|
|
200
|
+
async run(request, options) {
|
|
201
|
+
return this._client._postJSON("/v1/detect", request, options?.timeout);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var DetectorsResource = class {
|
|
205
|
+
constructor(_client) {
|
|
206
|
+
this._client = _client;
|
|
207
|
+
}
|
|
208
|
+
/** List available detectors (GET /v1/detectors). */
|
|
209
|
+
async list(options) {
|
|
210
|
+
return this._client._getJSON("/v1/detectors", options?.timeout);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var DebugResource = class {
|
|
214
|
+
constructor(_client) {
|
|
215
|
+
this._client = _client;
|
|
216
|
+
}
|
|
217
|
+
/** Inspect loaded policies (GET /v1/debug/policies). */
|
|
218
|
+
async policies(product = "guardrails", options) {
|
|
219
|
+
return this._client._getJSON(
|
|
220
|
+
`/v1/debug/policies?product=${product}`,
|
|
221
|
+
options?.timeout
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var Highflame = class {
|
|
226
|
+
#baseUrl;
|
|
227
|
+
#tokenUrl;
|
|
228
|
+
#apiKey;
|
|
229
|
+
#timeout;
|
|
230
|
+
#maxRetries;
|
|
231
|
+
#overrideAccountId;
|
|
232
|
+
#overrideProjectId;
|
|
233
|
+
#defaultHeaders;
|
|
234
|
+
#staticHeaders;
|
|
235
|
+
#log;
|
|
236
|
+
#cachedToken = null;
|
|
237
|
+
#tokenPromise = null;
|
|
238
|
+
guard;
|
|
239
|
+
detect;
|
|
240
|
+
detectors;
|
|
241
|
+
debug;
|
|
242
|
+
constructor(options) {
|
|
243
|
+
const baseUrl = options.baseUrl ?? SAAS_BASE_URL;
|
|
244
|
+
const tokenUrl = options.tokenUrl ?? SAAS_TOKEN_URL;
|
|
245
|
+
this.#baseUrl = baseUrl.replace(/\/$/, "");
|
|
246
|
+
this.#tokenUrl = options.apiKey.startsWith("hf_sk") ? tokenUrl : void 0;
|
|
247
|
+
this.#apiKey = options.apiKey;
|
|
248
|
+
this.#timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
249
|
+
this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
250
|
+
this.#overrideAccountId = options.accountId;
|
|
251
|
+
this.#overrideProjectId = options.projectId;
|
|
252
|
+
this.#defaultHeaders = options.defaultHeaders ?? {};
|
|
253
|
+
this.#staticHeaders = {
|
|
254
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
255
|
+
"Content-Type": "application/json",
|
|
256
|
+
Accept: "application/json",
|
|
257
|
+
"User-Agent": USER_AGENT,
|
|
258
|
+
...this.#defaultHeaders
|
|
259
|
+
};
|
|
260
|
+
if (options.accountId) this.#staticHeaders["X-Account-ID"] = options.accountId;
|
|
261
|
+
if (options.projectId) this.#staticHeaders["X-Project-ID"] = options.projectId;
|
|
262
|
+
this.#log = options.logger ?? NOOP_LOGGER;
|
|
263
|
+
this.guard = new GuardResource(this);
|
|
264
|
+
this.detect = new DetectResource(this);
|
|
265
|
+
this.detectors = new DetectorsResource(this);
|
|
266
|
+
this.debug = new DebugResource(this);
|
|
267
|
+
}
|
|
268
|
+
get accountId() {
|
|
269
|
+
return this.#overrideAccountId ?? this.#cachedToken?.accountId ?? "";
|
|
270
|
+
}
|
|
271
|
+
get projectId() {
|
|
272
|
+
return this.#overrideProjectId ?? this.#cachedToken?.projectId ?? "";
|
|
273
|
+
}
|
|
274
|
+
/** Get auth headers (exposed for external use with custom HTTP clients). */
|
|
275
|
+
async getAuthHeaders() {
|
|
276
|
+
if (!this.#tokenUrl) {
|
|
277
|
+
return this.#staticHeaders;
|
|
278
|
+
}
|
|
279
|
+
if (this.#cachedToken && Date.now() < this.#cachedToken.expiresAt) {
|
|
280
|
+
return this.#buildTokenHeaders(this.#cachedToken);
|
|
281
|
+
}
|
|
282
|
+
if (!this.#tokenPromise) {
|
|
283
|
+
this.#tokenPromise = this.#exchangeToken().finally(() => {
|
|
284
|
+
this.#tokenPromise = null;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
this.#cachedToken = await this.#tokenPromise;
|
|
288
|
+
return this.#buildTokenHeaders(this.#cachedToken);
|
|
289
|
+
}
|
|
290
|
+
#buildTokenHeaders(token) {
|
|
291
|
+
const headers = {
|
|
292
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
293
|
+
"Content-Type": "application/json",
|
|
294
|
+
Accept: "application/json",
|
|
295
|
+
"User-Agent": USER_AGENT,
|
|
296
|
+
...this.#defaultHeaders
|
|
297
|
+
};
|
|
298
|
+
const accountId = this.#overrideAccountId ?? (token.accountId || void 0);
|
|
299
|
+
const projectId = this.#overrideProjectId ?? (token.projectId || void 0);
|
|
300
|
+
if (accountId) headers["X-Account-ID"] = accountId;
|
|
301
|
+
if (projectId) headers["X-Project-ID"] = projectId;
|
|
302
|
+
return headers;
|
|
303
|
+
}
|
|
304
|
+
async #exchangeToken() {
|
|
305
|
+
this.#log.debug("Token exchange: POST %s", this.#tokenUrl);
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
const timer = setTimeout(() => controller.abort(), this.#timeout);
|
|
308
|
+
let response;
|
|
309
|
+
try {
|
|
310
|
+
response = await fetch(this.#tokenUrl, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
313
|
+
body: JSON.stringify({ grant_type: "api_key", api_key: this.#apiKey }),
|
|
314
|
+
signal: controller.signal
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
318
|
+
this.#log.warn("Token exchange timed out after %dms", this.#timeout);
|
|
319
|
+
throw new APIConnectionError(`Token exchange timed out after ${this.#timeout}ms`);
|
|
320
|
+
}
|
|
321
|
+
this.#log.warn("Token exchange failed: %s", err instanceof Error ? err.message : String(err));
|
|
322
|
+
throw new APIConnectionError(
|
|
323
|
+
`Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
|
|
324
|
+
);
|
|
325
|
+
} finally {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
}
|
|
328
|
+
if (!response.ok) {
|
|
329
|
+
this.#log.warn("Token exchange error: %d %s", response.status, response.statusText);
|
|
330
|
+
throw await parseError(response);
|
|
331
|
+
}
|
|
332
|
+
const tok = await response.json();
|
|
333
|
+
this.#log.debug("Token exchange: success (expires_in=%ds)", tok.expires_in);
|
|
334
|
+
return {
|
|
335
|
+
accessToken: tok.access_token,
|
|
336
|
+
expiresAt: Date.now() + tok.expires_in * 1e3 - TOKEN_REFRESH_BUFFER_MS,
|
|
337
|
+
accountId: tok.account_id ?? "",
|
|
338
|
+
projectId: tok.project_id ?? "",
|
|
339
|
+
gatewayId: tok.gateway_id ?? ""
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// -----------------------------------------------------------------------
|
|
343
|
+
// Internal HTTP helpers (used by resource classes)
|
|
344
|
+
// -----------------------------------------------------------------------
|
|
345
|
+
/** @internal */
|
|
346
|
+
async _fetchWithRetry(path, init, overrideTimeout) {
|
|
347
|
+
const effectiveTimeout = overrideTimeout ?? this.#timeout;
|
|
348
|
+
let lastError;
|
|
349
|
+
let idempotencyKey;
|
|
350
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
351
|
+
for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
|
|
352
|
+
if (attempt > 0) {
|
|
353
|
+
const delay = retryDelay(attempt - 1);
|
|
354
|
+
this.#log.debug(
|
|
355
|
+
"Retrying %s %s (attempt %d/%d) after %.0fms",
|
|
356
|
+
method,
|
|
357
|
+
path,
|
|
358
|
+
attempt + 1,
|
|
359
|
+
this.#maxRetries + 1,
|
|
360
|
+
delay
|
|
361
|
+
);
|
|
362
|
+
await sleep(delay);
|
|
363
|
+
if (!idempotencyKey) {
|
|
364
|
+
idempotencyKey = crypto.randomUUID();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
this.#log.debug("%s %s", method, path);
|
|
368
|
+
if (init.body && typeof init.body === "string") {
|
|
369
|
+
this.#log.debug("Request body: %s", truncate(init.body));
|
|
370
|
+
}
|
|
371
|
+
const authHeaders = await this.getAuthHeaders();
|
|
372
|
+
if (idempotencyKey) {
|
|
373
|
+
authHeaders["X-Idempotency-Key"] = idempotencyKey;
|
|
374
|
+
}
|
|
375
|
+
const controller = new AbortController();
|
|
376
|
+
const timer = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
377
|
+
let response;
|
|
378
|
+
try {
|
|
379
|
+
response = await fetch(this.#baseUrl + path, {
|
|
380
|
+
...init,
|
|
381
|
+
headers: authHeaders,
|
|
382
|
+
signal: controller.signal
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
386
|
+
this.#log.debug("Request timed out: %s %s", method, path);
|
|
387
|
+
lastError = new APIConnectionError(`Request timed out after ${effectiveTimeout}ms`);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
throw new APIConnectionError(
|
|
391
|
+
`Connection failed: ${err instanceof Error ? err.message : String(err)}`
|
|
392
|
+
);
|
|
393
|
+
} finally {
|
|
394
|
+
clearTimeout(timer);
|
|
395
|
+
}
|
|
396
|
+
this.#log.debug("%s %s \u2192 %d", method, path, response.status);
|
|
397
|
+
if (!RETRY_STATUS_CODES.has(response.status)) {
|
|
398
|
+
return response;
|
|
399
|
+
}
|
|
400
|
+
lastError = await parseError(response);
|
|
401
|
+
}
|
|
402
|
+
throw lastError;
|
|
403
|
+
}
|
|
404
|
+
/** @internal */
|
|
405
|
+
async _postJSON(path, body, overrideTimeout) {
|
|
406
|
+
const response = await this._fetchWithRetry(
|
|
407
|
+
path,
|
|
408
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
409
|
+
overrideTimeout
|
|
410
|
+
);
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
throw await parseError(response);
|
|
413
|
+
}
|
|
414
|
+
return response.json();
|
|
415
|
+
}
|
|
416
|
+
/** @internal */
|
|
417
|
+
async _getJSON(path, overrideTimeout) {
|
|
418
|
+
const response = await this._fetchWithRetry(
|
|
419
|
+
path,
|
|
420
|
+
{ method: "GET" },
|
|
421
|
+
overrideTimeout
|
|
422
|
+
);
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
throw await parseError(response);
|
|
425
|
+
}
|
|
426
|
+
return response.json();
|
|
427
|
+
}
|
|
428
|
+
/** @internal */
|
|
429
|
+
async *_stream(path, body, overrideTimeout) {
|
|
430
|
+
const effectiveTimeout = overrideTimeout ?? this.#timeout;
|
|
431
|
+
this.#log.debug("POST %s (stream)", path);
|
|
432
|
+
const authHeaders = await this.getAuthHeaders();
|
|
433
|
+
const controller = new AbortController();
|
|
434
|
+
const timer = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
435
|
+
let response;
|
|
436
|
+
try {
|
|
437
|
+
response = await fetch(this.#baseUrl + path, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: { ...authHeaders, Accept: "text/event-stream" },
|
|
440
|
+
body: JSON.stringify(body),
|
|
441
|
+
signal: controller.signal
|
|
442
|
+
});
|
|
443
|
+
} catch (err) {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
446
|
+
this.#log.debug("Stream timed out: POST %s", path);
|
|
447
|
+
throw new APIConnectionError(`Stream timed out after ${effectiveTimeout}ms`);
|
|
448
|
+
}
|
|
449
|
+
throw new APIConnectionError(
|
|
450
|
+
`Connection failed: ${err instanceof Error ? err.message : String(err)}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
this.#log.debug("POST %s (stream) \u2192 %d", path, response.status);
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
clearTimeout(timer);
|
|
456
|
+
throw await parseError(response);
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
yield* streamSseResponse(response);
|
|
460
|
+
} finally {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
toString() {
|
|
465
|
+
return `Highflame(baseUrl=${JSON.stringify(this.#baseUrl)})`;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/shield.ts
|
|
470
|
+
function splitOnTopLevelCommas(s) {
|
|
471
|
+
const parts = [];
|
|
472
|
+
let depth = 0;
|
|
473
|
+
let inString = null;
|
|
474
|
+
let cur = "";
|
|
475
|
+
let i = 0;
|
|
476
|
+
while (i < s.length) {
|
|
477
|
+
const ch = s[i];
|
|
478
|
+
if (inString !== null) {
|
|
479
|
+
cur += ch;
|
|
480
|
+
if (ch === "\\") {
|
|
481
|
+
i++;
|
|
482
|
+
if (i < s.length) cur += s[i];
|
|
483
|
+
} else if (ch === inString) {
|
|
484
|
+
inString = null;
|
|
485
|
+
}
|
|
486
|
+
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
487
|
+
inString = ch;
|
|
488
|
+
cur += ch;
|
|
489
|
+
} else if (ch === "(" || ch === "[" || ch === "{") {
|
|
490
|
+
depth++;
|
|
491
|
+
cur += ch;
|
|
492
|
+
} else if (ch === ")" || ch === "]" || ch === "}") {
|
|
493
|
+
depth--;
|
|
494
|
+
cur += ch;
|
|
495
|
+
} else if (ch === "," && depth === 0) {
|
|
496
|
+
parts.push(cur);
|
|
497
|
+
cur = "";
|
|
498
|
+
} else {
|
|
499
|
+
cur += ch;
|
|
500
|
+
}
|
|
501
|
+
i++;
|
|
502
|
+
}
|
|
503
|
+
if (cur) parts.push(cur);
|
|
504
|
+
return parts;
|
|
505
|
+
}
|
|
506
|
+
function getParamNames(fn) {
|
|
507
|
+
try {
|
|
508
|
+
const src = fn.toString().replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
|
509
|
+
const headerMatch = src.match(/^(?:async\s+)?(?:function\s*\w*\s*|\w+\s*)?\(/);
|
|
510
|
+
if (!headerMatch) {
|
|
511
|
+
const singleParam = src.match(/^(?:async\s+)?(\w+)\s*=>/);
|
|
512
|
+
if (singleParam) return [singleParam[1] ?? "arg0"];
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
const openIdx = headerMatch[0].length - 1;
|
|
516
|
+
let depth = 0;
|
|
517
|
+
let closeIdx = -1;
|
|
518
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
519
|
+
if (src[i] === "(") depth++;
|
|
520
|
+
else if (src[i] === ")") {
|
|
521
|
+
if (--depth === 0) {
|
|
522
|
+
closeIdx = i;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (closeIdx === -1) return [];
|
|
528
|
+
const inner = src.slice(openIdx + 1, closeIdx).trim();
|
|
529
|
+
if (!inner) return [];
|
|
530
|
+
return splitOnTopLevelCommas(inner).map(
|
|
531
|
+
(p) => p.trim().replace(/[=:].*/s, "").replace(/^\.\.\./, "").replace(/[{}[\]]/g, "").trim()
|
|
532
|
+
).filter(Boolean);
|
|
533
|
+
} catch {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function buildArgsDict(fn, args) {
|
|
538
|
+
const names = getParamNames(fn);
|
|
539
|
+
const result = {};
|
|
540
|
+
for (let i = 0; i < args.length; i++) {
|
|
541
|
+
result[names[i] ?? `arg${i}`] = args[i];
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
function raiseIfBlocked(response) {
|
|
546
|
+
if (response.decision === "deny") {
|
|
547
|
+
throw new BlockedError(response);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
var Shield = class {
|
|
551
|
+
constructor(client) {
|
|
552
|
+
this.client = client;
|
|
553
|
+
}
|
|
554
|
+
prompt(fn, options) {
|
|
555
|
+
const argIdx = options?.contentArg ?? 0;
|
|
556
|
+
const { mode, sessionId } = options ?? {};
|
|
557
|
+
const wrapper = async (...args) => {
|
|
558
|
+
const content = args[argIdx];
|
|
559
|
+
if (typeof content !== "string") {
|
|
560
|
+
throw new TypeError(
|
|
561
|
+
`shield.prompt: argument at index ${argIdx} must be string, got ${typeof content}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
raiseIfBlocked(
|
|
565
|
+
await this.client.guard.evaluatePrompt(content, { mode, session_id: sessionId })
|
|
566
|
+
);
|
|
567
|
+
return fn(...args);
|
|
568
|
+
};
|
|
569
|
+
Object.defineProperty(wrapper, "name", { value: fn.name, configurable: true });
|
|
570
|
+
return wrapper;
|
|
571
|
+
}
|
|
572
|
+
tool(fn, options) {
|
|
573
|
+
const toolName = options?.toolName ?? fn.name;
|
|
574
|
+
const { mode, sessionId } = options ?? {};
|
|
575
|
+
const wrapper = async (...args) => {
|
|
576
|
+
const argsDict = buildArgsDict(fn, args);
|
|
577
|
+
raiseIfBlocked(
|
|
578
|
+
await this.client.guard.evaluateToolCall(toolName, argsDict, {
|
|
579
|
+
mode,
|
|
580
|
+
session_id: sessionId
|
|
581
|
+
})
|
|
582
|
+
);
|
|
583
|
+
return fn(...args);
|
|
584
|
+
};
|
|
585
|
+
Object.defineProperty(wrapper, "name", { value: fn.name, configurable: true });
|
|
586
|
+
return wrapper;
|
|
587
|
+
}
|
|
588
|
+
toolResponse(fn, options) {
|
|
589
|
+
const toolName = options?.toolName ?? fn.name;
|
|
590
|
+
const { mode, sessionId } = options ?? {};
|
|
591
|
+
const wrapper = async (...args) => {
|
|
592
|
+
const result = await fn(...args);
|
|
593
|
+
raiseIfBlocked(
|
|
594
|
+
await this.client.guard.evaluate({
|
|
595
|
+
content: typeof result === "string" ? result : String(result),
|
|
596
|
+
content_type: "response",
|
|
597
|
+
action: "call_tool",
|
|
598
|
+
mode,
|
|
599
|
+
session_id: sessionId,
|
|
600
|
+
tool: { name: toolName, is_builtin: false }
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
return result;
|
|
604
|
+
};
|
|
605
|
+
Object.defineProperty(wrapper, "name", { value: fn.name, configurable: true });
|
|
606
|
+
return wrapper;
|
|
607
|
+
}
|
|
608
|
+
modelResponse(fn, options) {
|
|
609
|
+
const { mode, sessionId } = options ?? {};
|
|
610
|
+
const wrapper = async (...args) => {
|
|
611
|
+
const result = await fn(...args);
|
|
612
|
+
raiseIfBlocked(
|
|
613
|
+
await this.client.guard.evaluate({
|
|
614
|
+
content: typeof result === "string" ? result : String(result),
|
|
615
|
+
content_type: "response",
|
|
616
|
+
action: "process_prompt",
|
|
617
|
+
mode,
|
|
618
|
+
session_id: sessionId
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
return result;
|
|
622
|
+
};
|
|
623
|
+
Object.defineProperty(wrapper, "name", { value: fn.name, configurable: true });
|
|
624
|
+
return wrapper;
|
|
625
|
+
}
|
|
626
|
+
wrap(options) {
|
|
627
|
+
const { contentType, action, contentArg: argIdx = 0, mode, sessionId } = options;
|
|
628
|
+
return (fn) => {
|
|
629
|
+
const wrapper = async (...args) => {
|
|
630
|
+
const content = args[argIdx];
|
|
631
|
+
if (typeof content !== "string") {
|
|
632
|
+
throw new TypeError(
|
|
633
|
+
`shield.wrap: argument at index ${argIdx} must be string, got ${typeof content}`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
raiseIfBlocked(
|
|
637
|
+
await this.client.guard.evaluate({
|
|
638
|
+
content,
|
|
639
|
+
content_type: contentType,
|
|
640
|
+
action,
|
|
641
|
+
mode,
|
|
642
|
+
session_id: sessionId
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
return fn(...args);
|
|
646
|
+
};
|
|
647
|
+
Object.defineProperty(wrapper, "name", { value: fn.name, configurable: true });
|
|
648
|
+
return wrapper;
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
export {
|
|
653
|
+
APIConnectionError,
|
|
654
|
+
APIError,
|
|
655
|
+
AuthenticationError,
|
|
656
|
+
BlockedError,
|
|
657
|
+
DebugResource,
|
|
658
|
+
DetectResource,
|
|
659
|
+
DetectorsResource,
|
|
660
|
+
GuardResource,
|
|
661
|
+
Highflame,
|
|
662
|
+
HighflameError,
|
|
663
|
+
RateLimitError,
|
|
664
|
+
Shield,
|
|
665
|
+
VERSION
|
|
666
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "highflame",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "JavaScript/TypeScript SDK for Highflame AI guardrails",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"lint": "eslint src tests",
|
|
26
|
+
"lint:fix": "eslint src tests --fix",
|
|
27
|
+
"format": "prettier --write src tests smoke_test.mjs",
|
|
28
|
+
"format:check": "prettier --check src tests smoke_test.mjs",
|
|
29
|
+
"check": "tsc --noEmit && eslint src tests",
|
|
30
|
+
"coverage": "vitest run --coverage"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20",
|
|
34
|
+
"@vitest/coverage-v8": "^2",
|
|
35
|
+
"eslint": "^9",
|
|
36
|
+
"msw": "^2",
|
|
37
|
+
"prettier": "^3",
|
|
38
|
+
"tsup": "^8",
|
|
39
|
+
"typescript": "^5.4",
|
|
40
|
+
"typescript-eslint": "^8",
|
|
41
|
+
"vitest": "^2"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|