@bike4mind/cli 1.0.0 → 1.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 +1 -0
- package/bin/bike4mind-cli.mjs +0 -5
- package/dist/AgentHistoryStore-B2NEOvSW.mjs +18014 -0
- package/dist/{ApiClient-EUTyn5yu.mjs → ApiClient-D0fQ2FT6.mjs} +2 -2
- package/dist/{BubblewrapRuntime-CkL9-gnG.mjs → BubblewrapRuntime-5bLPTwEC.mjs} +2 -2
- package/dist/{ConfigStore-DHNgFdOu.mjs → ConfigStore-qV7NrCgZ.mjs} +2259 -1283
- package/dist/{ProxyManager-C1-lgzEU.mjs → ProxyManager-B0-RuR2w.mjs} +21 -4
- package/dist/{SandboxOrchestrator-C8uleDn2.mjs → SandboxOrchestrator-BcUv9fQ3.mjs} +47 -13
- package/dist/{SandboxRuntimeAdapter-ChGlxSGQ.mjs → SandboxRuntimeAdapter-BgLUVTJL.mjs} +2 -2
- package/dist/{SandboxRuntimeAdapter-CKelGICD.mjs → SandboxRuntimeAdapter-BmHELLuM.mjs} +1 -1
- package/dist/{SeatbeltRuntime-Qqt19cAN.mjs → SeatbeltRuntime-C_Y8q8Mr.mjs} +9 -1
- package/dist/buildAgent-C-C-VGff.mjs +2001 -0
- package/dist/commands/acpCommand.mjs +36 -13
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +92 -42
- package/dist/commands/mcpCommand.mjs +8 -13
- package/dist/commands/pluginCommand.mjs +9 -15
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/{createFile-DPv180yF-BnWFIxey.mjs → createFile-B8bur5Rb-CVzCarEA.mjs} +2 -2
- package/dist/{deleteFile-BdjUwUQF-B3XOJmg3.mjs → deleteFile-9B3gW_Nb-DG2sovIl.mjs} +2 -2
- package/dist/{globFiles-DjfDGaUK-CNR8pMRC.mjs → globFiles-CwJ8qmYo-BR5b2KvO.mjs} +3 -2
- package/dist/{grepSearch-BaYUfIYs-n0XKoGnL.mjs → grepSearch-BgoOOwGe-DtlV8Gn-.mjs} +3 -3
- package/dist/index.mjs +638 -129
- package/dist/{package-7a45-Svr.mjs → package-CGZIoxcs.mjs} +1 -1
- package/dist/{pathValidation-D8tjkQXE-1HwvsuYT.mjs → pathValidation-BRqf4HFX-CHwtwp3O.mjs} +7 -3
- package/dist/{serve-CavAHPdQ.mjs → serve-CPXqcEZr.mjs} +2 -2
- package/dist/types-CdIKgWWe.mjs +3 -0
- package/dist/{types-LyRNHOiS.mjs → types-F61_hxmG.mjs} +2 -0
- package/package.json +28 -29
- package/dist/AgentHistoryStore-CrRb8cMt.mjs +0 -38586
- package/dist/ProxyManager-B1jFWL7b.mjs +0 -3
- package/dist/SandboxOrchestrator-BFPVpmB5.mjs +0 -3
- package/dist/buildAgent-CJrkEG0M.mjs +0 -824
- package/dist/types-CqscS34o.mjs +0 -3
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as DEFAULT_SANDBOX_CONFIG } from "./types-
|
|
3
|
-
import "crypto";
|
|
2
|
+
import { t as DEFAULT_SANDBOX_CONFIG } from "./types-F61_hxmG.mjs";
|
|
3
|
+
import { createHmac } from "crypto";
|
|
4
4
|
import { existsSync, promises } from "fs";
|
|
5
5
|
import os, { homedir } from "os";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { v4 } from "uuid";
|
|
8
|
+
import * as path$1 from "node:path";
|
|
9
|
+
import * as fs$1 from "node:fs";
|
|
8
10
|
import * as z$2 from "zod";
|
|
9
11
|
import z, { ZodError, z as z$1 } from "zod";
|
|
10
12
|
import dayjs from "dayjs";
|
|
@@ -13,124 +15,10 @@ import utc from "dayjs/plugin/utc.js";
|
|
|
13
15
|
import relativeTime from "dayjs/plugin/relativeTime.js";
|
|
14
16
|
import localizedFormat from "dayjs/plugin/localizedFormat.js";
|
|
15
17
|
import { isAxiosError } from "axios";
|
|
16
|
-
import
|
|
18
|
+
import { homedir as homedir$1 } from "node:os";
|
|
19
|
+
import fs from "fs/promises";
|
|
17
20
|
process.env.APP_NAME;
|
|
18
21
|
process.env.WEBSITE_URL;
|
|
19
|
-
const extractSnippetMeta = (content) => {
|
|
20
|
-
const snippetRegex = /<!--snippet-meta\s*(\{[\s\S]*?\})\s*-->[\n\s]*([\s\S]*?)(?=<!--snippet-meta|$)/g;
|
|
21
|
-
const sections = [];
|
|
22
|
-
let lastIndex = 0;
|
|
23
|
-
let match;
|
|
24
|
-
while ((match = snippetRegex.exec(content)) !== null) {
|
|
25
|
-
if (match.index > lastIndex) {
|
|
26
|
-
const textBefore = content.slice(lastIndex, match.index).trim();
|
|
27
|
-
if (textBefore) sections.push({
|
|
28
|
-
type: "text",
|
|
29
|
-
content: textBefore
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const meta = JSON.parse(match[1]);
|
|
34
|
-
const snippetContent = match[2].trim();
|
|
35
|
-
if (meta && snippetContent) sections.push({
|
|
36
|
-
type: "snippet",
|
|
37
|
-
meta,
|
|
38
|
-
content: snippetContent
|
|
39
|
-
});
|
|
40
|
-
} catch (e) {
|
|
41
|
-
console.error("Error parsing snippet meta:", e);
|
|
42
|
-
}
|
|
43
|
-
lastIndex = match.index + match[0].length;
|
|
44
|
-
}
|
|
45
|
-
if (lastIndex < content.length) {
|
|
46
|
-
const remainingText = content.slice(lastIndex).trim();
|
|
47
|
-
if (remainingText) sections.push({
|
|
48
|
-
type: "text",
|
|
49
|
-
content: remainingText
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
return { sections };
|
|
53
|
-
};
|
|
54
|
-
function getHeader$1(headers, name) {
|
|
55
|
-
if (!headers || typeof headers !== "object") return null;
|
|
56
|
-
if (typeof headers.get === "function") {
|
|
57
|
-
const val = headers.get(name);
|
|
58
|
-
return typeof val === "string" ? val : null;
|
|
59
|
-
}
|
|
60
|
-
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
61
|
-
return typeof value === "string" ? value : null;
|
|
62
|
-
}
|
|
63
|
-
function parseNumber(value) {
|
|
64
|
-
if (value === null || value === void 0) return null;
|
|
65
|
-
const num = Number(value);
|
|
66
|
-
return Number.isFinite(num) ? num : null;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Parse rate limit headers from an HTTP response.
|
|
70
|
-
*
|
|
71
|
-
* Reads standard headers:
|
|
72
|
-
* - `X-RateLimit-Limit` - max requests per window
|
|
73
|
-
* - `X-RateLimit-Remaining` - requests left
|
|
74
|
-
* - `X-RateLimit-Reset` - Unix epoch seconds when the window resets
|
|
75
|
-
* - `Retry-After` - seconds to wait (on 429 responses), or an HTTP-date
|
|
76
|
-
*/
|
|
77
|
-
function parseRateLimitHeaders(headers) {
|
|
78
|
-
const limitStr = getHeader$1(headers, "X-RateLimit-Limit") ?? getHeader$1(headers, "x-ratelimit-limit");
|
|
79
|
-
const remainingStr = getHeader$1(headers, "X-RateLimit-Remaining") ?? getHeader$1(headers, "x-ratelimit-remaining");
|
|
80
|
-
const resetStr = getHeader$1(headers, "X-RateLimit-Reset") ?? getHeader$1(headers, "x-ratelimit-reset");
|
|
81
|
-
const retryAfterStr = getHeader$1(headers, "Retry-After") ?? getHeader$1(headers, "retry-after");
|
|
82
|
-
const limit = parseNumber(limitStr);
|
|
83
|
-
const remaining = parseNumber(remainingStr);
|
|
84
|
-
let resetAt = null;
|
|
85
|
-
if (resetStr !== null) {
|
|
86
|
-
const resetNum = Number(resetStr);
|
|
87
|
-
if (Number.isFinite(resetNum)) resetAt = /* @__PURE__ */ new Date(resetNum * 1e3);
|
|
88
|
-
else {
|
|
89
|
-
const parsed = new Date(resetStr);
|
|
90
|
-
if (!isNaN(parsed.getTime())) resetAt = parsed;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
let retryAfterMs = null;
|
|
94
|
-
if (retryAfterStr !== null) {
|
|
95
|
-
const retryNum = Number(retryAfterStr);
|
|
96
|
-
if (Number.isFinite(retryNum)) retryAfterMs = retryNum * 1e3;
|
|
97
|
-
else {
|
|
98
|
-
const parsed = new Date(retryAfterStr);
|
|
99
|
-
if (!isNaN(parsed.getTime())) retryAfterMs = Math.max(0, parsed.getTime() - Date.now());
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
let usagePercent = null;
|
|
103
|
-
if (limit !== null && limit > 0 && remaining !== null && remaining >= 0) usagePercent = Math.round((limit - remaining) / limit * 100);
|
|
104
|
-
return {
|
|
105
|
-
limit,
|
|
106
|
-
remaining,
|
|
107
|
-
resetAt,
|
|
108
|
-
retryAfterMs,
|
|
109
|
-
usagePercent
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function isNearLimit(info, thresholdPercent = 80) {
|
|
113
|
-
if (info.usagePercent === null) return false;
|
|
114
|
-
return info.usagePercent >= thresholdPercent;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Build a structured log object for rate limit events.
|
|
118
|
-
* Used by all integration clients to emit consistent log lines.
|
|
119
|
-
*/
|
|
120
|
-
function buildRateLimitLogEntry(integration, endpoint, info, wasThrottled = false) {
|
|
121
|
-
return {
|
|
122
|
-
type: wasThrottled ? "RATE_LIMIT_ERROR" : "RATE_LIMIT",
|
|
123
|
-
integration,
|
|
124
|
-
endpoint,
|
|
125
|
-
limit: info.limit,
|
|
126
|
-
remaining: info.remaining,
|
|
127
|
-
resetAt: info.resetAt?.toISOString() ?? null,
|
|
128
|
-
retryAfterMs: info.retryAfterMs,
|
|
129
|
-
usagePercent: info.usagePercent,
|
|
130
|
-
wasThrottled,
|
|
131
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
22
|
//#endregion
|
|
135
23
|
//#region ../../b4m-core/hearth/dist/index.mjs
|
|
136
24
|
/**
|
|
@@ -278,23 +166,14 @@ z$1.object({
|
|
|
278
166
|
});
|
|
279
167
|
//#endregion
|
|
280
168
|
//#region ../../b4m-core/common/dist/index.mjs
|
|
281
|
-
let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
|
|
282
|
-
HttpStatus[HttpStatus["Ok"] = 200] = "Ok";
|
|
283
|
-
HttpStatus[HttpStatus["Created"] = 201] = "Created";
|
|
284
|
-
HttpStatus[HttpStatus["BadRequest"] = 400] = "BadRequest";
|
|
285
|
-
HttpStatus[HttpStatus["Unauthorized"] = 401] = "Unauthorized";
|
|
286
|
-
HttpStatus[HttpStatus["Forbidden"] = 403] = "Forbidden";
|
|
287
|
-
HttpStatus[HttpStatus["NotFound"] = 404] = "NotFound";
|
|
288
|
-
HttpStatus[HttpStatus["Conflict"] = 409] = "Conflict";
|
|
289
|
-
HttpStatus[HttpStatus["UnprocessableEntity"] = 422] = "UnprocessableEntity";
|
|
290
|
-
HttpStatus[HttpStatus["TooManyRequests"] = 429] = "TooManyRequests";
|
|
291
|
-
HttpStatus[HttpStatus["InternalServerError"] = 500] = "InternalServerError";
|
|
292
|
-
HttpStatus[HttpStatus["BadGateway"] = 502] = "BadGateway";
|
|
293
|
-
return HttpStatus;
|
|
294
|
-
}({});
|
|
295
169
|
var HTTPError = class extends Error {
|
|
296
170
|
statusCode;
|
|
297
171
|
additionalInfo;
|
|
172
|
+
/**
|
|
173
|
+
* Set on a 5xx that reports a third party's failure the server handled correctly (a source site
|
|
174
|
+
* timing out), so `errorHandler` logs it at warn rather than paging as a server fault.
|
|
175
|
+
*/
|
|
176
|
+
expected;
|
|
298
177
|
constructor(statusCode, message, additionalInfo) {
|
|
299
178
|
super(message);
|
|
300
179
|
this.statusCode = statusCode;
|
|
@@ -326,47 +205,6 @@ var UnprocessableEntityError = class extends HTTPError {
|
|
|
326
205
|
this.name = "UnprocessableEntityError";
|
|
327
206
|
}
|
|
328
207
|
};
|
|
329
|
-
var BadRequestError = class extends HTTPError {
|
|
330
|
-
additionalInfo;
|
|
331
|
-
constructor(message, additionalInfo) {
|
|
332
|
-
super(400, message, additionalInfo);
|
|
333
|
-
this.additionalInfo = additionalInfo;
|
|
334
|
-
this.name = "BadRequestError";
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
var UnauthorizedError = class extends HTTPError {
|
|
338
|
-
additionalInfo;
|
|
339
|
-
constructor(message, additionalInfo) {
|
|
340
|
-
super(401, message, additionalInfo);
|
|
341
|
-
this.additionalInfo = additionalInfo;
|
|
342
|
-
this.name = "UnauthorizedError";
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
var ForbiddenError = class extends HTTPError {
|
|
346
|
-
additionalInfo;
|
|
347
|
-
constructor(message, additionalInfo) {
|
|
348
|
-
super(403, message, additionalInfo);
|
|
349
|
-
this.additionalInfo = additionalInfo;
|
|
350
|
-
this.name = "ForbiddenError";
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
var TooManyRequestsError = class extends HTTPError {
|
|
354
|
-
additionalInfo;
|
|
355
|
-
constructor(message, additionalInfo) {
|
|
356
|
-
super(429, message, additionalInfo);
|
|
357
|
-
this.additionalInfo = additionalInfo;
|
|
358
|
-
this.name = "TooManyRequestsError";
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
var CorruptedFileError = class extends HTTPError {
|
|
362
|
-
additionalInfo;
|
|
363
|
-
constructor(fileName, fileType, corruptionDetails, additionalInfo) {
|
|
364
|
-
const message = `File '${fileName}' (${fileType}) appears to be corrupted${corruptionDetails ? `: ${corruptionDetails}` : ""}. Please try uploading the file again.`;
|
|
365
|
-
super(422, message, additionalInfo);
|
|
366
|
-
this.additionalInfo = additionalInfo;
|
|
367
|
-
this.name = "CorruptedFileError";
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
208
|
function isZodError(err) {
|
|
371
209
|
return Boolean(err && (err instanceof ZodError || err.name === "ZodError"));
|
|
372
210
|
}
|
|
@@ -759,19 +597,7 @@ z$1.object({
|
|
|
759
597
|
content: z$1.string(),
|
|
760
598
|
metadata: ArtifactMetadataSchema.optional()
|
|
761
599
|
});
|
|
762
|
-
|
|
763
|
-
* Regex sub-pattern (as a string) that matches the attribute portion of an
|
|
764
|
-
* `<artifact ...>` opening tag. It handles:
|
|
765
|
-
* - newlines inside the attribute list (AI sometimes wraps long tags),
|
|
766
|
-
* - `>` characters inside double- or single-quoted attribute values.
|
|
767
|
-
*
|
|
768
|
-
* Exported as a string (not a compiled RegExp) so each consumer can
|
|
769
|
-
* compose it into their own regex with the flags they need, avoiding
|
|
770
|
-
* shared mutable `lastIndex` state.
|
|
771
|
-
*
|
|
772
|
-
* Usage: `new RegExp('<artifact\\s+(' + ARTIFACT_ATTRS_PATTERN + ')>...')`
|
|
773
|
-
*/
|
|
774
|
-
const ARTIFACT_ATTRS_PATTERN = String.raw`(?:[^>"']|"[^"]*"|'[^']*')*`;
|
|
600
|
+
String.raw`(?:[^>"']|"[^"]*"|'[^']*')*`;
|
|
775
601
|
const ClaudeArtifactMimeTypes = {
|
|
776
602
|
REACT: "application/vnd.ant.react",
|
|
777
603
|
HTML: "text/html",
|
|
@@ -785,45 +611,6 @@ const ClaudeArtifactMimeTypes = {
|
|
|
785
611
|
PYTHON: "application/vnd.ant.python",
|
|
786
612
|
BLOG_DRAFT: "application/vnd.b4m.blog-draft"
|
|
787
613
|
};
|
|
788
|
-
/**
|
|
789
|
-
* Map a MIME type (or AI-provider artifact-type string) to an internal {@link ArtifactType}.
|
|
790
|
-
*
|
|
791
|
-
* Single source of truth - consumed by the artifact parsers (b4m-core/utils + client) and the
|
|
792
|
-
* tool_result dedup in ChatCompletionProcess. Exact blessed-type matches first (case-insensitive,
|
|
793
|
-
* since MIME types are), then language/format inference; returns null if unrecognized.
|
|
794
|
-
*
|
|
795
|
-
* This previously lived as three hand-maintained copies that drifted - e.g. the lattice
|
|
796
|
-
* tool emits `application/vnd.b4m.lattice` but a copy matched `application/vnd.ant.lattice`,
|
|
797
|
-
* letting lattice tool_result artifacts dodge the dedup set.
|
|
798
|
-
*/
|
|
799
|
-
function mapMimeTypeToArtifactType(mimeType) {
|
|
800
|
-
if (!mimeType) return null;
|
|
801
|
-
const normalized = mimeType.toLowerCase().trim();
|
|
802
|
-
switch (normalized) {
|
|
803
|
-
case ClaudeArtifactMimeTypes.REACT.toLowerCase(): return "react";
|
|
804
|
-
case ClaudeArtifactMimeTypes.HTML.toLowerCase(): return "html";
|
|
805
|
-
case ClaudeArtifactMimeTypes.SVG.toLowerCase(): return "svg";
|
|
806
|
-
case ClaudeArtifactMimeTypes.MERMAID.toLowerCase(): return "mermaid";
|
|
807
|
-
case ClaudeArtifactMimeTypes.RECHARTS.toLowerCase(): return "recharts";
|
|
808
|
-
case ClaudeArtifactMimeTypes.CHESS.toLowerCase(): return "chess";
|
|
809
|
-
case ClaudeArtifactMimeTypes.CODE.toLowerCase(): return "code";
|
|
810
|
-
case ClaudeArtifactMimeTypes.MARKDOWN.toLowerCase(): return "code";
|
|
811
|
-
case ClaudeArtifactMimeTypes.LATTICE.toLowerCase(): return "lattice";
|
|
812
|
-
case ClaudeArtifactMimeTypes.PYTHON.toLowerCase(): return "python";
|
|
813
|
-
case ClaudeArtifactMimeTypes.BLOG_DRAFT.toLowerCase(): return "blog-draft";
|
|
814
|
-
}
|
|
815
|
-
if (normalized.includes("jsx") || normalized.includes("react")) return "react";
|
|
816
|
-
if (normalized.includes("javascript") || normalized.includes("typescript")) return "code";
|
|
817
|
-
if (normalized.includes("python") || normalized === "text/x-python") return "python";
|
|
818
|
-
if (normalized.includes("java") || normalized.includes("c++") || normalized.includes("rust") || normalized.includes("go") || normalized.includes("ruby") || normalized.includes("php") || normalized.includes("swift") || normalized.includes("kotlin") || normalized.includes("csharp") || normalized.includes("c#")) return "code";
|
|
819
|
-
if (normalized.includes("html") || normalized.includes("xhtml")) return "html";
|
|
820
|
-
if (normalized.includes("svg")) return "svg";
|
|
821
|
-
if (normalized.includes("markdown") || normalized.includes("md")) return "code";
|
|
822
|
-
if (normalized.includes("mermaid")) return "mermaid";
|
|
823
|
-
if (normalized.includes("recharts") || normalized.includes("chart")) return "recharts";
|
|
824
|
-
if (normalized.includes("chess")) return "chess";
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
827
614
|
let KnowledgeType = /* @__PURE__ */ function(KnowledgeType) {
|
|
828
615
|
/**
|
|
829
616
|
* A knowledge that is from a URL.
|
|
@@ -858,6 +645,19 @@ let KnowledgeType = /* @__PURE__ */ function(KnowledgeType) {
|
|
|
858
645
|
* fails the build.
|
|
859
646
|
*/
|
|
860
647
|
const QUEST_ERROR_CODES = ["insufficient_credits", "spend_cap_exceeded"];
|
|
648
|
+
/**
|
|
649
|
+
* Single source of truth for `IChatHistoryItem.type` / quest `type`. Restated by hand in
|
|
650
|
+
* `schemas/actions.ts`, `schemas/chat.ts`, and `notebookExportService/types.ts` before this
|
|
651
|
+
* const existed, which is exactly how `voice_transcript` went missing from the export contract
|
|
652
|
+
* once already - derive from this tuple instead of retyping the union.
|
|
653
|
+
*/
|
|
654
|
+
const CHAT_HISTORY_ITEM_TYPES = [
|
|
655
|
+
"message",
|
|
656
|
+
"oob",
|
|
657
|
+
"error",
|
|
658
|
+
"system",
|
|
659
|
+
"voice_transcript"
|
|
660
|
+
];
|
|
861
661
|
z$1.union([
|
|
862
662
|
z$1.literal("string"),
|
|
863
663
|
z$1.literal("number"),
|
|
@@ -982,6 +782,12 @@ const IMAGE_SIZE_CONSTRAINTS = {
|
|
|
982
782
|
"2160x3840"
|
|
983
783
|
],
|
|
984
784
|
defaultSize: "1024x1024",
|
|
785
|
+
/**
|
|
786
|
+
* Accepted by the API, and what generate sends when no size is asked for, but it is not a
|
|
787
|
+
* resolution - so it is deliberately out of `sizes`, the preset list the size picker renders.
|
|
788
|
+
* The one spelling: schemas/openai.ts and utils/imageSizes.ts both read it from here.
|
|
789
|
+
*/
|
|
790
|
+
autoSize: "auto",
|
|
985
791
|
/** Constraints for custom/flexible sizes */
|
|
986
792
|
constraints: {
|
|
987
793
|
maxEdge: 3840,
|
|
@@ -990,6 +796,28 @@ const IMAGE_SIZE_CONSTRAINTS = {
|
|
|
990
796
|
edgeMultiple: 16,
|
|
991
797
|
maxAspectRatio: 3
|
|
992
798
|
}
|
|
799
|
+
},
|
|
800
|
+
/** Also the only sizes the variation endpoint accepts - variations are dall-e-2 only. */
|
|
801
|
+
DALL_E_2: {
|
|
802
|
+
sizes: [
|
|
803
|
+
"256x256",
|
|
804
|
+
"512x512",
|
|
805
|
+
"1024x1024"
|
|
806
|
+
],
|
|
807
|
+
defaultSize: "1024x1024"
|
|
808
|
+
},
|
|
809
|
+
/**
|
|
810
|
+
* dall-e-3 is no longer in ImageModels, but the generate path still accepts its sizes
|
|
811
|
+
* for callers holding a persisted one. Reached by LEGACY_DALL_E_3_MODEL_ID rather than an
|
|
812
|
+
* enum member; isSupportedImageSize measures each dall-e tier against its own list.
|
|
813
|
+
*/
|
|
814
|
+
DALL_E_3: {
|
|
815
|
+
sizes: [
|
|
816
|
+
"1024x1024",
|
|
817
|
+
"1792x1024",
|
|
818
|
+
"1024x1792"
|
|
819
|
+
],
|
|
820
|
+
defaultSize: "1024x1024"
|
|
993
821
|
}
|
|
994
822
|
};
|
|
995
823
|
/**
|
|
@@ -1071,6 +899,7 @@ let ChatModels = /* @__PURE__ */ function(ChatModels) {
|
|
|
1071
899
|
ChatModels["CLAUDE_4_8_OPUS"] = "claude-opus-4-8";
|
|
1072
900
|
ChatModels["CLAUDE_FABLE_5"] = "claude-fable-5";
|
|
1073
901
|
ChatModels["CLAUDE_5_OPUS"] = "claude-opus-5";
|
|
902
|
+
ChatModels["CLAUDE_5_5_OPUS"] = "claude-opus-5-5";
|
|
1074
903
|
ChatModels["JURASSIC2_ULTRA"] = "ai21.j2-ultra-v1";
|
|
1075
904
|
ChatModels["JURASSIC2_MID"] = "ai21.j2-mid-v1";
|
|
1076
905
|
ChatModels["GEMINI_3_5_FLASH"] = "gemini-3.5-flash";
|
|
@@ -1112,12 +941,6 @@ let ChatModels = /* @__PURE__ */ function(ChatModels) {
|
|
|
1112
941
|
const CHAT_MODELS = Object.values(ChatModels);
|
|
1113
942
|
const supportedChatModels = z$1.enum(ChatModels);
|
|
1114
943
|
/**
|
|
1115
|
-
* Every `ChatModels` Gemini entry is named `gemini...` (see the GEMINI block above) - a prefix
|
|
1116
|
-
* check tracks that naming convention automatically as new Gemini models are added, unlike an
|
|
1117
|
-
* explicit id list that would need updating in lockstep and could silently miss one.
|
|
1118
|
-
*/
|
|
1119
|
-
const isGeminiModelId = (model) => model.startsWith("gemini");
|
|
1120
|
-
/**
|
|
1121
944
|
* Models that support the reasoning_effort parameter.
|
|
1122
945
|
* o1-preview and o1-mini do NOT support reasoning_effort.
|
|
1123
946
|
*/
|
|
@@ -1138,137 +961,7 @@ const REASONING_SUPPORTED_MODELS = /* @__PURE__ */ new Set([
|
|
|
1138
961
|
"gpt-5.6-luna",
|
|
1139
962
|
"gpt-5.6-terra"
|
|
1140
963
|
]);
|
|
1141
|
-
|
|
1142
|
-
* GPT-5-family reasoning models whose tool calling breaks on
|
|
1143
|
-
* `/v1/chat/completions` when `reasoning_effort` is also sent. OpenAI requires
|
|
1144
|
-
* this combination to go through `/v1/responses` instead. The failure mode
|
|
1145
|
-
* differs by model:
|
|
1146
|
-
* - GPT-5.4 (and -mini/-nano) hard-reject with a 400:
|
|
1147
|
-
* "Function tools with reasoning_effort are not supported for <model> in
|
|
1148
|
-
* /v1/chat/completions. Please use /v1/responses instead."
|
|
1149
|
-
* - GPT-5 / -mini / -nano / 5.1 / 5.2 return 200 but silently degrade: the
|
|
1150
|
-
* model *narrates* the tool call in its text ("Calling the tool now...")
|
|
1151
|
-
* instead of emitting a real `tool_calls` entry, so no tool ever executes.
|
|
1152
|
-
* This surfaced as the /opti optimizer's "Draft with AI" doing nothing on
|
|
1153
|
-
* GPT-5: the same request on a model with `reasoning_effort`
|
|
1154
|
-
* dropped (or on Claude) fires the tool correctly.
|
|
1155
|
-
*
|
|
1156
|
-
* We drop `reasoning_effort` when tools are sent for these models so tool
|
|
1157
|
-
* calling continues to work on `/v1/chat/completions`. Dropping it only forgoes
|
|
1158
|
-
* explicit effort control - the model still reasons at its default.
|
|
1159
|
-
*
|
|
1160
|
-
* NOTE: for the base GPT-5 narrator family (`RESPONSES_API_TOOL_MODELS`), the
|
|
1161
|
-
* adapter now routes tool turns to `/v1/responses` instead - where reasoning +
|
|
1162
|
-
* tools work together, so `reasoning_effort` is kept. This drop remains as
|
|
1163
|
-
* defense-in-depth for the (now-unreached) chat path and covers the GPT-5.4
|
|
1164
|
-
* family, which is NOT routed to Responses (its drop-path already works).
|
|
1165
|
-
*
|
|
1166
|
-
* O-series reasoning models (o1/o3/o4) are intentionally excluded: they call
|
|
1167
|
-
* tools correctly with `reasoning_effort` on `/v1/chat/completions`.
|
|
1168
|
-
*
|
|
1169
|
-
* Invariant: every member here MUST also be in `REASONING_SUPPORTED_MODELS`.
|
|
1170
|
-
* The gate in `openaiBackend.ts` short-circuits when a model doesn't support
|
|
1171
|
-
* reasoning at all, so adding a non-reasoning model here would make the gate
|
|
1172
|
-
* a no-op and silently leak `reasoning_effort` to the request.
|
|
1173
|
-
*/
|
|
1174
|
-
const REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS = /* @__PURE__ */ new Set([
|
|
1175
|
-
"gpt-5",
|
|
1176
|
-
"gpt-5-mini",
|
|
1177
|
-
"gpt-5-nano",
|
|
1178
|
-
"gpt-5.1",
|
|
1179
|
-
"gpt-5.2",
|
|
1180
|
-
"gpt-5.4",
|
|
1181
|
-
"gpt-5.4-mini",
|
|
1182
|
-
"gpt-5.4-nano",
|
|
1183
|
-
"gpt-5.6-sol",
|
|
1184
|
-
"gpt-5.6-luna",
|
|
1185
|
-
"gpt-5.6-terra"
|
|
1186
|
-
]);
|
|
1187
|
-
/**
|
|
1188
|
-
* GPT-5 reasoning models that silently *narrate* tool calls on
|
|
1189
|
-
* `/v1/chat/completions` (return 200 with the call written as text instead of a
|
|
1190
|
-
* real `tool_calls` entry, so nothing executes). The adapter
|
|
1191
|
-
* routes these to OpenAI's `/v1/responses` API when function tools are present,
|
|
1192
|
-
* where reasoning + tools work together and `reasoning_effort` can be kept.
|
|
1193
|
-
*
|
|
1194
|
-
* Deliberately excludes the GPT-5.4 family: it *hard-errors* (400) on that
|
|
1195
|
-
* combination and is already handled by dropping `reasoning_effort` on the chat
|
|
1196
|
-
* path (see `REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS`), so it stays on
|
|
1197
|
-
* the working chat path to keep this routing's blast radius small. Also excludes
|
|
1198
|
-
* `*-chat-latest` (non-reasoning) and O-series (tools work there already).
|
|
1199
|
-
*
|
|
1200
|
-
* Invariant: every member MUST also be in `REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS`
|
|
1201
|
-
* so the chat path still drops `reasoning_effort` as a fallback if routing is bypassed.
|
|
1202
|
-
*/
|
|
1203
|
-
const RESPONSES_API_TOOL_MODELS = /* @__PURE__ */ new Set([
|
|
1204
|
-
"gpt-5",
|
|
1205
|
-
"gpt-5-mini",
|
|
1206
|
-
"gpt-5-nano",
|
|
1207
|
-
"gpt-5.1",
|
|
1208
|
-
"gpt-5.2",
|
|
1209
|
-
"gpt-5.6-sol",
|
|
1210
|
-
"gpt-5.6-luna",
|
|
1211
|
-
"gpt-5.6-terra"
|
|
1212
|
-
]);
|
|
1213
|
-
/**
|
|
1214
|
-
* Models that only support temperature=1 (no custom temperature).
|
|
1215
|
-
* Includes:
|
|
1216
|
-
* - All reasoning models (OpenAI requires temp=1 when reasoning is active)
|
|
1217
|
-
* - chat-latest variants that enforce this constraint
|
|
1218
|
-
* - GPT-5.5, which rejects custom temperature even though it does not expose
|
|
1219
|
-
* reasoning controls
|
|
1220
|
-
*/
|
|
1221
|
-
const FIXED_TEMPERATURE_MODELS = /* @__PURE__ */ new Set([
|
|
1222
|
-
...Array.from(REASONING_SUPPORTED_MODELS),
|
|
1223
|
-
"gpt-5.1-chat-latest",
|
|
1224
|
-
"gpt-5.2-chat-latest",
|
|
1225
|
-
"gpt-5.5"
|
|
1226
|
-
]);
|
|
1227
|
-
/**
|
|
1228
|
-
* Models that do not accept the temperature parameter at all.
|
|
1229
|
-
* The API will reject requests that include temperature for these models.
|
|
1230
|
-
*/
|
|
1231
|
-
const NO_TEMPERATURE_MODELS = /* @__PURE__ */ new Set([
|
|
1232
|
-
"claude-opus-4-7",
|
|
1233
|
-
"global.anthropic.claude-opus-4-7",
|
|
1234
|
-
"claude-opus-4-8",
|
|
1235
|
-
"global.anthropic.claude-opus-4-8",
|
|
1236
|
-
"claude-sonnet-5",
|
|
1237
|
-
"global.anthropic.claude-sonnet-5",
|
|
1238
|
-
"claude-fable-5",
|
|
1239
|
-
"claude-opus-5",
|
|
1240
|
-
"kimi-k3",
|
|
1241
|
-
"kimi-k2.7-code",
|
|
1242
|
-
"kimi-k2.7-code-highspeed",
|
|
1243
|
-
"kimi-k2.6",
|
|
1244
|
-
"kimi-k2.5",
|
|
1245
|
-
"deepseek-flash",
|
|
1246
|
-
"deepseek-v4-pro"
|
|
1247
|
-
]);
|
|
1248
|
-
/**
|
|
1249
|
-
* Models whose safety classifiers can decline a request with `stop_reason: 'refusal'`
|
|
1250
|
-
* (HTTP 200, empty or partial content) - Claude Fable 5's GA classifiers target research
|
|
1251
|
-
* biology and most cybersecurity content and occasionally false-positive on benign adjacent
|
|
1252
|
-
* work. Per Anthropic's GA guidance a refusal from these is opt-in recoverable: rather than
|
|
1253
|
-
* surfacing a hard refusal, the backend throws so the completion loop's existing fallback
|
|
1254
|
-
* machinery continues the request on Opus 5 (whose classifiers intervene far less often).
|
|
1255
|
-
* A refusal from any *other* model is a genuine decline and surfaces unchanged. Keep in
|
|
1256
|
-
* sync with the `claude-fable-5` fallback preference chain in `adminSettings/fallback.ts`.
|
|
1257
|
-
*/
|
|
1258
|
-
const REFUSAL_FALLBACK_MODELS = /* @__PURE__ */ new Set(["claude-fable-5"]);
|
|
1259
|
-
/**
|
|
1260
|
-
* Bedrock-hosted Claude models that do NOT support prompt caching (`cache_control`).
|
|
1261
|
-
* Sending `cache_control` to these models triggers a Bedrock deserialization error:
|
|
1262
|
-
* `tools.N.cache_control: Extra inputs are not permitted`
|
|
1263
|
-
*
|
|
1264
|
-
* AWS Bedrock added prompt caching for Claude 3.5 Haiku and Claude 3.7 Sonnet (and later);
|
|
1265
|
-
* the OG Claude 3 Haiku and the v1 Claude 3.5 Sonnet were not retrofitted.
|
|
1266
|
-
*
|
|
1267
|
-
* Keep this set narrow - default behavior is to apply caching when `cacheStrategy.enableCaching`
|
|
1268
|
-
* is true. Add a model here only when we have concrete evidence (a Bedrock validation error)
|
|
1269
|
-
* that it rejects `cache_control`.
|
|
1270
|
-
*/
|
|
1271
|
-
const BEDROCK_NO_PROMPT_CACHING_MODELS = /* @__PURE__ */ new Set(["anthropic.claude-3-haiku-20240307-v1:0", "anthropic.claude-3-5-sonnet-20240620-v1:0"]);
|
|
964
|
+
[...Array.from(REASONING_SUPPORTED_MODELS)];
|
|
1272
965
|
/**
|
|
1273
966
|
* Speech to Text Models
|
|
1274
967
|
*
|
|
@@ -1314,13 +1007,6 @@ z$1.enum({
|
|
|
1314
1007
|
...SpeechToTextModels,
|
|
1315
1008
|
...VideoModels
|
|
1316
1009
|
});
|
|
1317
|
-
/** Returns true if the model is deprecated on or before the provided date (default: now). */
|
|
1318
|
-
const isModelDeprecated = (model, now = /* @__PURE__ */ new Date()) => {
|
|
1319
|
-
if (!model.deprecationDate) return false;
|
|
1320
|
-
const todayYMD = new Date(now.toISOString().slice(0, 10));
|
|
1321
|
-
const cutoff = /* @__PURE__ */ new Date(model.deprecationDate + "T00:00:00Z");
|
|
1322
|
-
return todayYMD.getTime() >= cutoff.getTime();
|
|
1323
|
-
};
|
|
1324
1010
|
/**
|
|
1325
1011
|
* Valid status values for sub-quests.
|
|
1326
1012
|
* Canonical vocabulary - the mongoose schema, zod schemas, and client all
|
|
@@ -1516,7 +1202,14 @@ const RealtimeVoiceUsageTransaction = BaseCreditTransaction.extend({
|
|
|
1516
1202
|
});
|
|
1517
1203
|
const ToolUsageTransaction = BaseCreditTransaction.extend({
|
|
1518
1204
|
type: z$1.literal("tool_usage"),
|
|
1519
|
-
|
|
1205
|
+
/**
|
|
1206
|
+
* The model that actually incurred the tool cost (e.g. 'gpt-image-2'), NOT the chat
|
|
1207
|
+
* model of the quest that ran the tool. The row is one aggregate over every charging
|
|
1208
|
+
* tool call in the quest, so this is set only when exactly one model charged; a quest
|
|
1209
|
+
* that charged on two or more models leaves it unset rather than naming one of them.
|
|
1210
|
+
* Per-call attribution always lives on the `feature: 'tool'` UsageEventModel rows.
|
|
1211
|
+
*/
|
|
1212
|
+
model: z$1.string().optional(),
|
|
1520
1213
|
questId: z$1.string(),
|
|
1521
1214
|
sessionId: z$1.string()
|
|
1522
1215
|
});
|
|
@@ -1602,8 +1295,8 @@ z$1.object({
|
|
|
1602
1295
|
ownerType: z$1.enum(CreditHolderType),
|
|
1603
1296
|
sessionId: z$1.string().optional(),
|
|
1604
1297
|
/**
|
|
1605
|
-
* Data lake this call is 1:1 attributable to (ingestion embeds
|
|
1606
|
-
* embedding can span multiple lakes and is never attributed here). Unset for
|
|
1298
|
+
* Data lake this call is 1:1 attributable to (ingestion embeds and research-run judge calls -
|
|
1299
|
+
* a query embedding can span multiple lakes and is never attributed here). Unset for
|
|
1607
1300
|
* every other feature/call.
|
|
1608
1301
|
*/
|
|
1609
1302
|
dataLakeId: z$1.string().optional(),
|
|
@@ -1650,7 +1343,8 @@ z$1.object({
|
|
|
1650
1343
|
"ok",
|
|
1651
1344
|
"error",
|
|
1652
1345
|
"timeout",
|
|
1653
|
-
"refusal"
|
|
1346
|
+
"refusal",
|
|
1347
|
+
"degenerate"
|
|
1654
1348
|
]).default("ok"),
|
|
1655
1349
|
latencyMs: z$1.number().optional(),
|
|
1656
1350
|
/** Originating surface (web/cli/api/agent/system); unset => unclassified. */
|
|
@@ -1757,7 +1451,6 @@ const FIELD_GROUPS = [
|
|
|
1757
1451
|
"dispatch",
|
|
1758
1452
|
"availability"
|
|
1759
1453
|
];
|
|
1760
|
-
const isFieldGroup = (value) => FIELD_GROUPS.includes(value);
|
|
1761
1454
|
/** YYYY-MM-DD, the format every date-ish ModelInfo field already uses. */
|
|
1762
1455
|
const CALENDAR_DATE = z$1.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected a YYYY-MM-DD calendar date");
|
|
1763
1456
|
const ReasoningWrite = z$1.strictObject({
|
|
@@ -1975,6 +1668,7 @@ const MODEL_INFO_FIELD_GROUP_OF = {
|
|
|
1975
1668
|
backend: "identity",
|
|
1976
1669
|
contextWindow: "limits",
|
|
1977
1670
|
max_tokens: "limits",
|
|
1671
|
+
maxOutputTokensDerived: "limits",
|
|
1978
1672
|
can_think: "reasoning",
|
|
1979
1673
|
thinkingStyle: "reasoning",
|
|
1980
1674
|
can_stream: "modalities",
|
|
@@ -2247,6 +1941,16 @@ const DiscoveryPriceSkip = z$1.object({
|
|
|
2247
1941
|
modelId: z$1.string().min(1),
|
|
2248
1942
|
reason: z$1.string()
|
|
2249
1943
|
});
|
|
1944
|
+
/**
|
|
1945
|
+
* A configured source the run never attempted, and why. Produced by
|
|
1946
|
+
* `skipReasonFor` in runModelDiscovery (services/modelDiscoveryService); the
|
|
1947
|
+
* reason stays a plain string rather than that module's SourceSkipReason union
|
|
1948
|
+
* so a new reason needs no schema or UI change.
|
|
1949
|
+
*/
|
|
1950
|
+
const DiscoverySkippedSource = z$1.object({
|
|
1951
|
+
name: z$1.string().min(1),
|
|
1952
|
+
reason: z$1.string()
|
|
1953
|
+
});
|
|
2250
1954
|
const DiscoveryLifecycleTransition = z$1.object({
|
|
2251
1955
|
modelId: z$1.string().min(1),
|
|
2252
1956
|
/** Absent when no row in force carried a lifecycle for this model. */
|
|
@@ -2303,6 +2007,8 @@ z$1.object({
|
|
|
2303
2007
|
*/
|
|
2304
2008
|
mode: z$1.enum(DISCOVERY_RUN_MODES).optional(),
|
|
2305
2009
|
sources: z$1.array(DiscoverySourceReport).optional(),
|
|
2010
|
+
/** Configured sources this run never attempted; disjoint from `sources`. */
|
|
2011
|
+
skippedSources: z$1.array(DiscoverySkippedSource).optional(),
|
|
2306
2012
|
joinCoverage: z$1.array(DiscoveryJoinCoverage).optional(),
|
|
2307
2013
|
/** Ids no aggregator matched: a work item, not a log line. */
|
|
2308
2014
|
unmatchedIds: z$1.array(z$1.string()).optional(),
|
|
@@ -2419,6 +2125,59 @@ let McpServerName = /* @__PURE__ */ function(McpServerName) {
|
|
|
2419
2125
|
McpServerName["Notion"] = "notion";
|
|
2420
2126
|
return McpServerName;
|
|
2421
2127
|
}({});
|
|
2128
|
+
const ROLLUP_DAY_MS = 864e5;
|
|
2129
|
+
/**
|
|
2130
|
+
* Reads a rollup bound as a UTC instant. The schema below accepts an offset-less value (what a
|
|
2131
|
+
* date picker emits), and a bare `new Date` would read that in the host's local zone - so the
|
|
2132
|
+
* same query would cover a different window depending on where it ran.
|
|
2133
|
+
*/
|
|
2134
|
+
function parseFeedbackRollupBound(value) {
|
|
2135
|
+
return new Date(/([Zz]|[+-]\d{2}:?\d{2})$/.test(value) ? value : `${value}Z`);
|
|
2136
|
+
}
|
|
2137
|
+
z$1.object({
|
|
2138
|
+
from: z$1.iso.datetime({
|
|
2139
|
+
offset: true,
|
|
2140
|
+
local: true
|
|
2141
|
+
}),
|
|
2142
|
+
to: z$1.iso.datetime({
|
|
2143
|
+
offset: true,
|
|
2144
|
+
local: true
|
|
2145
|
+
})
|
|
2146
|
+
}).refine((query) => parseFeedbackRollupBound(query.from) < parseFeedbackRollupBound(query.to), {
|
|
2147
|
+
message: "from must be strictly before to",
|
|
2148
|
+
path: ["from"]
|
|
2149
|
+
}).refine((query) => parseFeedbackRollupBound(query.to).getTime() - parseFeedbackRollupBound(query.from).getTime() < 366 * ROLLUP_DAY_MS, {
|
|
2150
|
+
message: `window must not exceed 366 days`,
|
|
2151
|
+
path: ["to"]
|
|
2152
|
+
});
|
|
2153
|
+
const feedbackCountBucketSchema = z$1.object({
|
|
2154
|
+
key: z$1.string(),
|
|
2155
|
+
count: z$1.number()
|
|
2156
|
+
});
|
|
2157
|
+
const orgFeedbackSummaryCountsSchema = z$1.object({
|
|
2158
|
+
totals: z$1.object({ count: z$1.number() }),
|
|
2159
|
+
byDay: z$1.array(z$1.object({
|
|
2160
|
+
day: z$1.string(),
|
|
2161
|
+
count: z$1.number()
|
|
2162
|
+
})),
|
|
2163
|
+
bySubject: z$1.array(feedbackCountBucketSchema),
|
|
2164
|
+
byType: z$1.array(feedbackCountBucketSchema),
|
|
2165
|
+
byStatus: z$1.array(feedbackCountBucketSchema),
|
|
2166
|
+
byTag: z$1.array(feedbackCountBucketSchema),
|
|
2167
|
+
byTagTruncated: z$1.boolean().optional()
|
|
2168
|
+
});
|
|
2169
|
+
z$1.object({
|
|
2170
|
+
summaryJobId: z$1.string(),
|
|
2171
|
+
organizationId: z$1.string(),
|
|
2172
|
+
range: z$1.object({
|
|
2173
|
+
from: z$1.string(),
|
|
2174
|
+
to: z$1.string()
|
|
2175
|
+
}),
|
|
2176
|
+
generatedAt: z$1.string(),
|
|
2177
|
+
model: z$1.string(),
|
|
2178
|
+
summary: z$1.string(),
|
|
2179
|
+
counts: orgFeedbackSummaryCountsSchema
|
|
2180
|
+
});
|
|
2422
2181
|
/**
|
|
2423
2182
|
* Check if a value is a placeholder (not configured or SST default).
|
|
2424
2183
|
* Uses case-insensitive comparison and trims whitespace to prevent bypass attempts.
|
|
@@ -2467,10 +2226,16 @@ function isPlaceholderApiKey(value) {
|
|
|
2467
2226
|
/**
|
|
2468
2227
|
* Lake lifecycle. Stable states (draft/active/archived/deleted) plus transitional
|
|
2469
2228
|
* states (archiving/unarchiving/restoring/deleting/purging) that exist to drive UI and make a crashed
|
|
2470
|
-
* mid-operation observable.
|
|
2471
|
-
* holds its first member file (see `activateIfDraft` below), and unconditionally when an
|
|
2472
|
-
* archived or deleted lake is restored, which is how an empty lake can end up active.
|
|
2229
|
+
* mid-operation observable.
|
|
2473
2230
|
*
|
|
2231
|
+
* draft <-> active is a DELIBERATE, two-way move: `promoteDataLake` publishes a draft (the only
|
|
2232
|
+
* door onto `activateIfDraft` below) and `demoteDataLake` reverses it. Neither fires as a side
|
|
2233
|
+
* effect of adding content - a draft lake that fills up with files stays a draft, excluded from
|
|
2234
|
+
* grounding, until an owner or admin explicitly promotes it. Restoring an archived or
|
|
2235
|
+
* deleted lake still lands unconditionally on `active`, which is how an empty lake can end up
|
|
2236
|
+
* active without ever passing through an explicit promote.
|
|
2237
|
+
*
|
|
2238
|
+
|
|
2474
2239
|
* `purging` is the one transitional state that is NOT recoverable by retrying the same action:
|
|
2475
2240
|
* it is claimed the moment a phase-2 hard delete is ACCEPTED (#1744), before the background
|
|
2476
2241
|
* sweep runs, so that `listDeletedDataLakes` stops offering Restore on a lake whose
|
|
@@ -2500,6 +2265,7 @@ const DATA_LAKE_STABLE_STATUSES = [
|
|
|
2500
2265
|
"deleted"
|
|
2501
2266
|
];
|
|
2502
2267
|
DATA_LAKE_STATUSES.filter((s) => !DATA_LAKE_STABLE_STATUSES.includes(s));
|
|
2268
|
+
const DATA_LAKE_ORIGINS = ["curated", "connector-fed"];
|
|
2503
2269
|
z$1.object({
|
|
2504
2270
|
/**
|
|
2505
2271
|
* The granting lake's Mongo `_id`. ALWAYS a persisted DB lake: a hardcoded/fallback lake has no
|
|
@@ -2547,6 +2313,14 @@ z$1.object({
|
|
|
2547
2313
|
removedAt: z$1.date(),
|
|
2548
2314
|
expiresAt: z$1.date()
|
|
2549
2315
|
});
|
|
2316
|
+
/** Mirrors the read model's vocabulary deliberately (aliased, not re-declared, so the two can
|
|
2317
|
+
* never drift) - an audit reader learns one principal shape for both halves of the trail. */
|
|
2318
|
+
const LAKE_CONFIG_CHANGE_PRINCIPAL_KINDS = [
|
|
2319
|
+
"user",
|
|
2320
|
+
"agent",
|
|
2321
|
+
"apiKey",
|
|
2322
|
+
"system"
|
|
2323
|
+
];
|
|
2550
2324
|
/**
|
|
2551
2325
|
* Every `IDataLake` field, classified as audited or not. A TOTAL map keyed by `keyof IDataLake`,
|
|
2552
2326
|
* exactly like `LAKE_FIELD_VISIBILITY` in redactLakeForActor.ts and for the same reason: a list of
|
|
@@ -2575,6 +2349,7 @@ const LAKE_CONFIG_FIELD_AUDIT = {
|
|
|
2575
2349
|
auditQueryTextEnabled: "audited",
|
|
2576
2350
|
lakeMemoryEnabled: "audited",
|
|
2577
2351
|
status: "audited",
|
|
2352
|
+
origin: "audited",
|
|
2578
2353
|
createdByUserId: "audited",
|
|
2579
2354
|
lastUpdatedByUserId: "excluded",
|
|
2580
2355
|
fileCount: "excluded",
|
|
@@ -2582,15 +2357,59 @@ const LAKE_CONFIG_FIELD_AUDIT = {
|
|
|
2582
2357
|
totalChunkedChars: "excluded",
|
|
2583
2358
|
embeddingSpendMicroUsd: "excluded",
|
|
2584
2359
|
lastSyncAt: "excluded",
|
|
2360
|
+
lastHealthCheckedAt: "excluded",
|
|
2361
|
+
lastInconsistencyScanAt: "excluded",
|
|
2585
2362
|
filesDeletedAt: "excluded",
|
|
2586
2363
|
filesArchivedAt: "excluded",
|
|
2587
2364
|
lakeMemoryExtractionAt: "excluded",
|
|
2588
2365
|
lakeMemoryCursor: "excluded",
|
|
2589
2366
|
lakeMemoryPurgedAt: "excluded",
|
|
2590
2367
|
inconsistencyReport: "excluded",
|
|
2591
|
-
inconsistencyComputedAt: "excluded"
|
|
2368
|
+
inconsistencyComputedAt: "excluded",
|
|
2369
|
+
modelInconsistencyRunAt: "excluded"
|
|
2592
2370
|
};
|
|
2593
2371
|
[...Object.keys(LAKE_CONFIG_FIELD_AUDIT).filter((field) => LAKE_CONFIG_FIELD_AUDIT[field] === "audited")];
|
|
2372
|
+
z$1.object({
|
|
2373
|
+
dataLakeId: z$1.string(),
|
|
2374
|
+
/** Denormalized from the lake at offer time: a recipient route can filter by it without a join. */
|
|
2375
|
+
organizationId: z$1.string().nullish(),
|
|
2376
|
+
/** The actor who made the offer - always the eventual `grantedByUserId` of an accepted transfer. */
|
|
2377
|
+
offeredByUserId: z$1.string(),
|
|
2378
|
+
recipientUserId: z$1.string(),
|
|
2379
|
+
status: z$1.enum([
|
|
2380
|
+
"pending",
|
|
2381
|
+
"accepted",
|
|
2382
|
+
"declined",
|
|
2383
|
+
"cancelled",
|
|
2384
|
+
"expired"
|
|
2385
|
+
]),
|
|
2386
|
+
expiresAt: z$1.date(),
|
|
2387
|
+
/** Set by the atomic `resolve` when the offer leaves `pending`; null while it is still open. */
|
|
2388
|
+
resolvedAt: z$1.date().nullish(),
|
|
2389
|
+
/**
|
|
2390
|
+
* The lake's effective owner ids AT OFFER TIME. Accept re-resolves them and refuses if they moved,
|
|
2391
|
+
* so an offer can never apply a transfer over another transfer (or a departure succession) that
|
|
2392
|
+
* happened in between. Snapshotted rather than recomputed because "stale" is exactly the case
|
|
2393
|
+
* where today's answer differs from the one the offer was made under.
|
|
2394
|
+
*/
|
|
2395
|
+
priorOwnerUserIds: z$1.array(z$1.string()),
|
|
2396
|
+
offeredVia: z$1.enum([
|
|
2397
|
+
"grant-owner",
|
|
2398
|
+
"creator",
|
|
2399
|
+
"platform-admin",
|
|
2400
|
+
"org-admin"
|
|
2401
|
+
]),
|
|
2402
|
+
/**
|
|
2403
|
+
* The principal to attribute the applied transfer to, resolved by the route at OFFER time (only a
|
|
2404
|
+
* route can tell an API key from a session). Carried so the eventual audit row keeps naming the
|
|
2405
|
+
* same principal the offer was made under, rather than whichever principal happens to accept.
|
|
2406
|
+
*/
|
|
2407
|
+
auditPrincipal: z$1.object({
|
|
2408
|
+
principalKind: z$1.enum(LAKE_CONFIG_CHANGE_PRINCIPAL_KINDS),
|
|
2409
|
+
principalId: z$1.string(),
|
|
2410
|
+
onBehalfOfUserId: z$1.string().optional()
|
|
2411
|
+
}).optional()
|
|
2412
|
+
});
|
|
2594
2413
|
/**
|
|
2595
2414
|
* SRE Agent Trio - Shared Types
|
|
2596
2415
|
*
|
|
@@ -2942,32 +2761,7 @@ let SupportedFabFileMimeTypes = /* @__PURE__ */ function(SupportedFabFileMimeTyp
|
|
|
2942
2761
|
SupportedFabFileMimeTypes["CONF"] = "text/plain";
|
|
2943
2762
|
return SupportedFabFileMimeTypes;
|
|
2944
2763
|
}({});
|
|
2945
|
-
|
|
2946
|
-
* The canonical set of MIME types the ingest pipeline can actually chunk +
|
|
2947
|
-
* vectorize. Kept in lockstep with the `SmartChunker` switch in
|
|
2948
|
-
* `@bike4mind/fab-pipeline`.
|
|
2949
|
-
*/
|
|
2950
|
-
const SUPPORTED_FAB_FILE_MIME_TYPES = new Set(Object.values(SupportedFabFileMimeTypes));
|
|
2951
|
-
/**
|
|
2952
|
-
* Type guard: is a claimed MIME type one we actually support ingesting?
|
|
2953
|
-
*
|
|
2954
|
-
* Used to gate uploads so unsupported/binary files (e.g. `.exe`) are rejected
|
|
2955
|
-
* with a clear error instead of being stored and silently "vectorized" into 0
|
|
2956
|
-
* chunks. Node-free so it can run on both the client (upload UI) and server
|
|
2957
|
-
* (ingest endpoints).
|
|
2958
|
-
*/
|
|
2959
|
-
function isSupportedFabFileMimeType(mimeType) {
|
|
2960
|
-
return !!mimeType && SUPPORTED_FAB_FILE_MIME_TYPES.has(mimeType);
|
|
2961
|
-
}
|
|
2962
|
-
/**
|
|
2963
|
-
* Is this an audio MIME type? Matches any `audio/*` (not just the known set)
|
|
2964
|
-
* so the LLM-exclusion and vectorization-skip guards fail safe for any audio
|
|
2965
|
-
* subtype a provider might emit.
|
|
2966
|
-
*/
|
|
2967
|
-
function isAudioMimeType(mimeType) {
|
|
2968
|
-
if (!mimeType) return false;
|
|
2969
|
-
return mimeType.split(";")[0].trim().toLowerCase().startsWith("audio/");
|
|
2970
|
-
}
|
|
2764
|
+
new Set(Object.values(SupportedFabFileMimeTypes));
|
|
2971
2765
|
/** Reads back the classifier set by the tagged-error helpers above; `undefined` for untagged errors. */
|
|
2972
2766
|
function getQuestErrorCode(error) {
|
|
2973
2767
|
const code = error?.additionalInfo?.errorCode;
|
|
@@ -3046,231 +2840,23 @@ const AGENT_QUEST_MANIFEST = {
|
|
|
3046
2840
|
}
|
|
3047
2841
|
};
|
|
3048
2842
|
/**
|
|
3049
|
-
* The catalog row in force for a model and unit at a given time: newest
|
|
3050
|
-
* effectiveFrom <= at AMONG ROWS OF THAT UNIT. Units are independent price
|
|
3051
|
-
* streams - a newer per_minute row must never shadow the in-force per_token
|
|
3052
|
-
* row (that would silently revert token billing to the adapter literal).
|
|
3053
|
-
* Rows are append-only, so this is the whole time-travel story (see
|
|
3054
|
-
* ModelPriceTypes).
|
|
3055
|
-
*/
|
|
3056
|
-
function resolveModelPriceRow(rows, modelId, unit, at) {
|
|
3057
|
-
let inForce;
|
|
3058
|
-
for (const row of rows) {
|
|
3059
|
-
if (row.modelId !== modelId || row.unit !== unit) continue;
|
|
3060
|
-
if (row.effectiveFrom.getTime() > at.getTime()) continue;
|
|
3061
|
-
if (!inForce || row.effectiveFrom.getTime() > inForce.effectiveFrom.getTime()) inForce = row;
|
|
3062
|
-
}
|
|
3063
|
-
return inForce;
|
|
3064
|
-
}
|
|
3065
|
-
/**
|
|
3066
|
-
* Overlay catalog prices onto assembled ModelInfo. Only per_token rows apply
|
|
3067
|
-
* here (they feed getTextModelCost via ModelInfo.pricing); per_minute and
|
|
3068
|
-
* per_image rows are consumed by their own settlement paths. A model with no
|
|
3069
|
-
* per_token row in force keeps its adapter literal - the fallback that keeps
|
|
3070
|
-
* zero-config self-host deployments working.
|
|
3071
|
-
*/
|
|
3072
|
-
function applyModelPriceCatalog(models, rows, at = /* @__PURE__ */ new Date()) {
|
|
3073
|
-
if (rows.length === 0) return models;
|
|
3074
|
-
return models.map((model) => {
|
|
3075
|
-
if (model.type !== "text") return model;
|
|
3076
|
-
const row = resolveModelPriceRow(rows, model.id, "per_token", at);
|
|
3077
|
-
if (!row) return model;
|
|
3078
|
-
const pricing = {};
|
|
3079
|
-
for (const [threshold, tier] of Object.entries(row.pricing)) pricing[Number(threshold)] = tier;
|
|
3080
|
-
return {
|
|
3081
|
-
...model,
|
|
3082
|
-
pricing
|
|
3083
|
-
};
|
|
3084
|
-
});
|
|
3085
|
-
}
|
|
3086
|
-
/**
|
|
3087
2843
|
* Input headroom the chat path holds back on top of a request's reserved output.
|
|
3088
2844
|
* safeInputWindow (ChatCompletionProcess) subtracts it, so a text entry whose output
|
|
3089
2845
|
* reserve leaves less than this has no room for a prompt at all.
|
|
3090
2846
|
*/
|
|
3091
2847
|
const CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS = 1e3;
|
|
3092
2848
|
/**
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3095
|
-
*
|
|
3096
|
-
*
|
|
3097
|
-
|
|
3098
|
-
const DEFAULT_UNKNOWN_CONTEXT_WINDOW = 2e5;
|
|
3099
|
-
/**
|
|
3100
|
-
* The model types this build's ModelInfo consumers narrow on. ModelRecord.type is
|
|
3101
|
-
* wider (embedding / tts / realtime-voice), so the read path drops and counts any
|
|
3102
|
-
* record outside this set: an old build must degrade to "I do not see the new video
|
|
3103
|
-
* models", never to a runtime narrowing failure.
|
|
2849
|
+
* The modalities promptMeta.model.type is allowed to record. Deliberately NARROWER than
|
|
2850
|
+
* MODEL_INFO_TYPES: 'speech-to-text' is served by its own transcription route, never by a
|
|
2851
|
+
* completion, so recording it would put a value in the field that no reader narrows on.
|
|
2852
|
+
* PromptMetaModelSchema.type (schemas/promptMeta) builds its z.enum from this const, and
|
|
2853
|
+
* QuestModelType (apps/client admin reporting) is an alias of the type - one source, three uses.
|
|
3104
2854
|
*/
|
|
3105
|
-
const
|
|
2855
|
+
const PROMPT_META_MODEL_TYPES = [
|
|
3106
2856
|
"text",
|
|
3107
2857
|
"image",
|
|
3108
|
-
"speech-to-text",
|
|
3109
2858
|
"video"
|
|
3110
2859
|
];
|
|
3111
|
-
const isRenderableModelType = (type) => MODEL_INFO_TYPES.includes(type);
|
|
3112
|
-
/**
|
|
3113
|
-
* Whether a ModelInfo type returns media (image/video) rather than tokens. Shared by
|
|
3114
|
-
* safeInputWindow/effectiveContextWindow (@bike4mind/utils, server) and useTokenLimits
|
|
3115
|
-
* (apps/client, browser) so "what counts as media" cannot drift between the two halves of the
|
|
3116
|
-
* same guard - both need it, and common is the one package already safe to import from either.
|
|
3117
|
-
*/
|
|
3118
|
-
const isMediaModelType = (type) => type === "image" || type === "video";
|
|
3119
|
-
/**
|
|
3120
|
-
* The record -> ModelInfo adapter: one place where every ModelInfo field a
|
|
3121
|
-
* catalog record does not carry gets its default. Each default degrades to the
|
|
3122
|
-
* visible, recoverable behavior rather than the silent or expensive one.
|
|
3123
|
-
*
|
|
3124
|
-
* Pricing is never sourced here: applyModelPriceCatalog overlays the ModelPrice
|
|
3125
|
-
* rows afterwards, and an empty map trips the [UNPRICED_MODEL] alarm on first
|
|
3126
|
-
* billed use, which is the intended fail-loud path.
|
|
3127
|
-
*/
|
|
3128
|
-
function toModelInfo(record) {
|
|
3129
|
-
const retired = record.lifecycle?.status === "retired";
|
|
3130
|
-
const disabled = record.disabled === true || record.autoDisabled === true || retired;
|
|
3131
|
-
return {
|
|
3132
|
-
id: record.id,
|
|
3133
|
-
type: record.type,
|
|
3134
|
-
name: record.name,
|
|
3135
|
-
backend: record.backend,
|
|
3136
|
-
contextWindow: record.contextWindow,
|
|
3137
|
-
max_tokens: record.maxOutputTokens ?? Math.min(record.contextWindow, 4096),
|
|
3138
|
-
pricing: {},
|
|
3139
|
-
can_stream: record.canStream,
|
|
3140
|
-
can_think: record.reasoning?.supported ?? false,
|
|
3141
|
-
thinkingStyle: toThinkingStyle(record),
|
|
3142
|
-
adapterFamily: record.adapterFamily,
|
|
3143
|
-
dispatchProfile: record.dispatchProfile,
|
|
3144
|
-
supportsVision: record.supportsVision,
|
|
3145
|
-
supportsTools: record.supportsTools,
|
|
3146
|
-
supportsImageVariation: record.supportsImageVariation ?? false,
|
|
3147
|
-
supportsSafetyTolerance: record.supportsSafetyTolerance,
|
|
3148
|
-
freeToRun: record.freeToRun,
|
|
3149
|
-
private: record.private ?? false,
|
|
3150
|
-
disabled,
|
|
3151
|
-
disabledReason: record.disabledReason ?? record.autoDisabledReason ?? (retired ? "retired by the provider" : void 0),
|
|
3152
|
-
deprecationDate: record.lifecycle?.deprecationDate,
|
|
3153
|
-
replacedBy: record.lifecycle?.replacedBy,
|
|
3154
|
-
trainingCutoff: record.trainingCutoff,
|
|
3155
|
-
releaseDate: record.releaseDate,
|
|
3156
|
-
logoFile: record.logoFile,
|
|
3157
|
-
rank: record.rank,
|
|
3158
|
-
description: record.description,
|
|
3159
|
-
isSlowModel: record.isSlowModel
|
|
3160
|
-
};
|
|
3161
|
-
}
|
|
3162
|
-
/**
|
|
3163
|
-
* ModelInfo.thinkingStyle only describes the two Anthropic request shapes. Other
|
|
3164
|
-
* reasoning styles map to undefined rather than to a wrong shape; the backends
|
|
3165
|
-
* treat unset as their own default.
|
|
3166
|
-
*/
|
|
3167
|
-
function toThinkingStyle(record) {
|
|
3168
|
-
switch (record.reasoning?.style) {
|
|
3169
|
-
case "anthropic-adaptive": return "adaptive";
|
|
3170
|
-
case "anthropic-legacy": return "legacy";
|
|
3171
|
-
default: return;
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
function fromThinkingStyle(style) {
|
|
3175
|
-
if (style === "adaptive") return "anthropic-adaptive";
|
|
3176
|
-
if (style === "legacy") return "anthropic-legacy";
|
|
3177
|
-
}
|
|
3178
|
-
/** Who makes the model, when the id namespace says so: [region.]<vendor>.<model>. */
|
|
3179
|
-
const BEDROCK_REGION_PREFIX = /^(us|eu|apac|global)\./;
|
|
3180
|
-
const VENDOR_BY_BACKEND = {
|
|
3181
|
-
["openai"]: "openai",
|
|
3182
|
-
["anthropic"]: "anthropic",
|
|
3183
|
-
["gemini"]: "google",
|
|
3184
|
-
["xai"]: "xai",
|
|
3185
|
-
["kimi"]: "moonshotai",
|
|
3186
|
-
["deepseek"]: "deepseek",
|
|
3187
|
-
["bfl"]: "black-forest-labs",
|
|
3188
|
-
["aws"]: "amazon",
|
|
3189
|
-
["voyageai"]: "voyageai",
|
|
3190
|
-
["ollama"]: "ollama",
|
|
3191
|
-
["local-image"]: "local",
|
|
3192
|
-
["bedrock"]: "amazon"
|
|
3193
|
-
};
|
|
3194
|
-
/**
|
|
3195
|
-
* ModelInfo carries no vendor (that is one of the four disagreeing taxonomies
|
|
3196
|
-
* this catalog replaces), so the inverse adapter derives it: the backend answers
|
|
3197
|
-
* it for direct providers, and for Bedrock the id namespace does.
|
|
3198
|
-
*/
|
|
3199
|
-
/**
|
|
3200
|
-
* Bedrock id prefixes that name the same maker as a different string. AWS spells
|
|
3201
|
-
* Kimi K2.5 `moonshotai.` and K2 Thinking `moonshot.`, so the raw prefix would
|
|
3202
|
-
* file one vendor's two models under two vendors and split them in the admin
|
|
3203
|
-
* dashboard. Canonicalized to the spelling the direct backend and the models.dev
|
|
3204
|
-
* provider both use.
|
|
3205
|
-
*/
|
|
3206
|
-
const BEDROCK_VENDOR_ALIASES = { moonshot: "moonshotai" };
|
|
3207
|
-
function inferVendor(info) {
|
|
3208
|
-
if (info.backend === "bedrock") {
|
|
3209
|
-
const withoutRegion = String(info.id).replace(BEDROCK_REGION_PREFIX, "");
|
|
3210
|
-
const dot = withoutRegion.indexOf(".");
|
|
3211
|
-
if (dot > 0) {
|
|
3212
|
-
const prefix = withoutRegion.slice(0, dot);
|
|
3213
|
-
return BEDROCK_VENDOR_ALIASES[prefix] ?? prefix;
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
return VENDOR_BY_BACKEND[info.backend] ?? String(info.backend);
|
|
3217
|
-
}
|
|
3218
|
-
/**
|
|
3219
|
-
* ModelInfo -> ModelRecord, the inverse of toModelInfo. Two callers: the
|
|
3220
|
-
* fallback seed generator (adapter literals become seed rows) and the merge's
|
|
3221
|
-
* base tier (a seeded model becomes a record that a catalog row can then claim
|
|
3222
|
-
* groups of).
|
|
3223
|
-
*
|
|
3224
|
-
* `pricing` has no ModelInfo spelling here and is deliberately absent rather
|
|
3225
|
-
* than guessed: catalog rows never carry it. The dispatch group round-trips
|
|
3226
|
-
* (ModelInfo carries it since dispatch consumes it), but no feed may author it -
|
|
3227
|
-
* a wrong value there mis-routes a request, so it stays seed- or
|
|
3228
|
-
* operator-sourced, or comes from the seed-side DispatchResolver.
|
|
3229
|
-
*
|
|
3230
|
-
* Round-tripping normalizes the optional booleans toModelInfo defaults
|
|
3231
|
-
* (can_think, private, disabled, supportsImageVariation): undefined becomes an
|
|
3232
|
-
* explicit false. That is only observable for a model whose merged record a
|
|
3233
|
-
* catalog row actually owns a group of.
|
|
3234
|
-
*/
|
|
3235
|
-
function toModelRecord(info) {
|
|
3236
|
-
return {
|
|
3237
|
-
id: info.id,
|
|
3238
|
-
vendor: inferVendor(info),
|
|
3239
|
-
backend: info.backend,
|
|
3240
|
-
type: info.type,
|
|
3241
|
-
name: info.name,
|
|
3242
|
-
contextWindow: info.contextWindow,
|
|
3243
|
-
maxOutputTokens: info.max_tokens,
|
|
3244
|
-
canStream: info.can_stream,
|
|
3245
|
-
reasoning: info.can_think === void 0 && info.thinkingStyle === void 0 ? void 0 : {
|
|
3246
|
-
supported: info.can_think === true,
|
|
3247
|
-
style: fromThinkingStyle(info.thinkingStyle)
|
|
3248
|
-
},
|
|
3249
|
-
adapterFamily: info.adapterFamily,
|
|
3250
|
-
dispatchProfile: info.dispatchProfile,
|
|
3251
|
-
supportsVision: info.supportsVision,
|
|
3252
|
-
supportsTools: info.supportsTools,
|
|
3253
|
-
supportsImageVariation: info.supportsImageVariation,
|
|
3254
|
-
supportsSafetyTolerance: info.supportsSafetyTolerance,
|
|
3255
|
-
lifecycle: {
|
|
3256
|
-
...info.deprecationDate ? {
|
|
3257
|
-
status: "deprecated",
|
|
3258
|
-
deprecationDate: info.deprecationDate
|
|
3259
|
-
} : { status: "active" },
|
|
3260
|
-
...info.replacedBy ? { replacedBy: info.replacedBy } : {}
|
|
3261
|
-
},
|
|
3262
|
-
description: info.description,
|
|
3263
|
-
logoFile: info.logoFile,
|
|
3264
|
-
rank: info.rank,
|
|
3265
|
-
isSlowModel: info.isSlowModel,
|
|
3266
|
-
trainingCutoff: info.trainingCutoff,
|
|
3267
|
-
releaseDate: info.releaseDate,
|
|
3268
|
-
private: info.private,
|
|
3269
|
-
freeToRun: info.freeToRun,
|
|
3270
|
-
disabled: info.disabled,
|
|
3271
|
-
disabledReason: info.disabledReason
|
|
3272
|
-
};
|
|
3273
|
-
}
|
|
3274
2860
|
const DEFAULT_PRICE_MARGIN = 1.2;
|
|
3275
2861
|
const DEFAULT_USD_TO_CREDITS_RATE = 6e-4;
|
|
3276
2862
|
/**
|
|
@@ -3318,140 +2904,6 @@ const CREDITS_PER_USD_COST = (() => {
|
|
|
3318
2904
|
console.warn(`[pricing] Env-configured margin/rate derive ${derived} credits per USD cost; using defaults`);
|
|
3319
2905
|
return Math.round(DEFAULT_PRICE_MARGIN / DEFAULT_USD_TO_CREDITS_RATE);
|
|
3320
2906
|
})();
|
|
3321
|
-
/**
|
|
3322
|
-
* Converts a USD cost to credits, including markup, rounding UP (minimum 1).
|
|
3323
|
-
*
|
|
3324
|
-
* Use for reservations, eligibility checks, estimates, and display - anywhere
|
|
3325
|
-
* a deterministic, conservative number is needed. Final settlement of variable
|
|
3326
|
-
* usage should use usdToCreditsStochastic so users pay the exact fraction in
|
|
3327
|
-
* expectation instead of the round-up.
|
|
3328
|
-
*
|
|
3329
|
-
* Examples (defaults):
|
|
3330
|
-
* $1 USD = 2000 credits (1.2x markup at $0.0006/credit)
|
|
3331
|
-
* $0.001 USD = 2 credits
|
|
3332
|
-
* $0.0001 USD = 1 credit (minimum)
|
|
3333
|
-
*
|
|
3334
|
-
* @param rate - Credits per $1, defaults to the platform-wide CREDITS_PER_USD_COST.
|
|
3335
|
-
* Pass an override for a provider-specific admin-tunable rate (e.g. a
|
|
3336
|
-
* billing-sensitive external compute path priced independently of the
|
|
3337
|
-
* platform default).
|
|
3338
|
-
*/
|
|
3339
|
-
const usdToCredits = (usd, rate = CREDITS_PER_USD_COST) => {
|
|
3340
|
-
return Math.max(1, Math.ceil(usd * rate));
|
|
3341
|
-
};
|
|
3342
|
-
/**
|
|
3343
|
-
* Uniform draw in [0, 1) from the platform CSPRNG. Billing draws MUST be
|
|
3344
|
-
* unpredictable: a caller who can foresee the stream could time requests to
|
|
3345
|
-
* land on the no-charge side of every draw. Node >= 19 and all browsers
|
|
3346
|
-
* expose globalThis.crypto; the Math.random fallback exists only so tests
|
|
3347
|
-
* and exotic runtimes do not crash, and it warns once.
|
|
3348
|
-
*/
|
|
3349
|
-
let warnedNonCryptoRng = false;
|
|
3350
|
-
const cryptoUniform = () => {
|
|
3351
|
-
const cryptoObj = globalThis.crypto;
|
|
3352
|
-
if (cryptoObj?.getRandomValues) {
|
|
3353
|
-
const buf = /* @__PURE__ */ new Uint32Array(1);
|
|
3354
|
-
cryptoObj.getRandomValues(buf);
|
|
3355
|
-
return buf[0] / 4294967296;
|
|
3356
|
-
}
|
|
3357
|
-
if (!warnedNonCryptoRng) {
|
|
3358
|
-
warnedNonCryptoRng = true;
|
|
3359
|
-
console.warn("[pricing] globalThis.crypto unavailable; billing rounding falls back to Math.random");
|
|
3360
|
-
}
|
|
3361
|
-
return Math.random();
|
|
3362
|
-
};
|
|
3363
|
-
/**
|
|
3364
|
-
* Converts a USD cost to credits with markup using UNBIASED stochastic
|
|
3365
|
-
* rounding: the integer part is always charged, and the fractional part
|
|
3366
|
-
* charges one extra credit with probability equal to the fraction.
|
|
3367
|
-
* E[charge] equals the exact fractional cost at every call size - no
|
|
3368
|
-
* round-up overcharge, no 1-credit minimum, and no free-below-threshold
|
|
3369
|
-
* leak. Because draws are independent and priced at exact cost, no calling
|
|
3370
|
-
* pattern (splitting, retrying, aborting) changes expected cost.
|
|
3371
|
-
*
|
|
3372
|
-
* Server-side settlement only. Do NOT use for display, estimates, or
|
|
3373
|
-
* reservations (it is non-deterministic; use usdToCredits). Zero or
|
|
3374
|
-
* non-finite cost charges 0.
|
|
3375
|
-
*
|
|
3376
|
-
* @param usd - The raw provider cost in USD to convert
|
|
3377
|
-
* @param rng - Uniform [0,1) source, injectable for tests; defaults to CSPRNG
|
|
3378
|
-
* @param rate - Credits per $1, defaults to the platform-wide CREDITS_PER_USD_COST.
|
|
3379
|
-
* Callers settling a provider-specific admin-tunable rate must pass the SAME
|
|
3380
|
-
* rate value that was used at reservation time (snapshot it, don't re-read
|
|
3381
|
-
* a live admin setting), or the settlement math mixes scales.
|
|
3382
|
-
*/
|
|
3383
|
-
const usdToCreditsStochastic = (usd, rng = cryptoUniform, rate = CREDITS_PER_USD_COST) => {
|
|
3384
|
-
const raw = usd * rate;
|
|
3385
|
-
if (!Number.isFinite(raw) || raw <= 0) return 0;
|
|
3386
|
-
const base = Math.floor(raw);
|
|
3387
|
-
const fraction = raw - base;
|
|
3388
|
-
return base + (rng() < fraction ? 1 : 0);
|
|
3389
|
-
};
|
|
3390
|
-
/**
|
|
3391
|
-
* Output tokens the pre-flight credit hold prices when a request's max_tokens
|
|
3392
|
-
* ceiling exceeds it.
|
|
3393
|
-
*
|
|
3394
|
-
* max_tokens is a *ceiling*, not a prediction: adaptive reasoning models stop at
|
|
3395
|
-
* end_turn well short of it, so pricing the hold at the full window (128K on the
|
|
3396
|
-
* flagship models) reserved several thousand credits per turn regardless of answer
|
|
3397
|
-
* length. The excess was always refunded at settlement, but the hold IS the
|
|
3398
|
-
* insufficient-funds gate, so users whose balance sat between their real cost and
|
|
3399
|
-
* the worst case were falsely blocked.
|
|
3400
|
-
*
|
|
3401
|
-
* 16K covers the realistic long answer with headroom - the largest replies seen in
|
|
3402
|
-
* practice are HTML-artifact turns at roughly 10-11K output tokens (see
|
|
3403
|
-
* buildThinkingParams in llm-adapters/thinkingParams.ts, whose max_tokens floor was
|
|
3404
|
-
* sized off the same measurement).
|
|
3405
|
-
*
|
|
3406
|
-
* UNDER-RESERVATION IS ACCEPTED, NOT PREVENTED. A turn that emits more than this
|
|
3407
|
-
* settles as a shortfall debit at reconciliation, which is already a supported path
|
|
3408
|
-
* (provider-basis settlement could always exceed a hold priced on the local
|
|
3409
|
-
* estimate). The two sites clamp the shortfall differently: chat's
|
|
3410
|
-
* computeSettlementDelta (services/llm/ChatCompletionProcess.ts) floors the debit at
|
|
3411
|
-
* the balance snapshot taken at its OWN admission and reports the remainder as
|
|
3412
|
-
* writtenOffCredits; cliCompletions.ts does an unclamped $inc on success and only
|
|
3413
|
-
* logs an ALERT if it lands negative. Neither is a non-negativity guarantee: the
|
|
3414
|
-
* chat snapshot predates any sibling turn's spend (see that function's own "best
|
|
3415
|
-
* effort" note), and when the shortfall still fits the stale snapshot the debit
|
|
3416
|
-
* applies in full - writtenOffCredits 0, and no BILLING_SHORTFALL_CLAMP log - so a
|
|
3417
|
-
* concurrent holder can land negative there too. Either way the resulting balance
|
|
3418
|
-
* fails the *next* turn's own admission gate - but that bound is per turn, not per
|
|
3419
|
-
* holder: turns admitted concurrently are each checked against the balance at their
|
|
3420
|
-
* own admission and settle against a snapshot that predates their siblings' spend,
|
|
3421
|
-
* so a holder running turns in parallel can be shorted once per in-flight turn, not
|
|
3422
|
-
* once total.
|
|
3423
|
-
*/
|
|
3424
|
-
const PREFLIGHT_RESERVATION_OUTPUT_TOKENS = 16384;
|
|
3425
|
-
/**
|
|
3426
|
-
* Reservation ceiling for models that spend reasoning tokens inside their output
|
|
3427
|
-
* budget (see reasonsWithinOutputBudget in llm-adapters/thinkingParams.ts). Those
|
|
3428
|
-
* tokens bill as output on top of the visible answer, so the 16K figure above -
|
|
3429
|
-
* which was measured on visible artifact size alone - under-reserves them badly.
|
|
3430
|
-
*
|
|
3431
|
-
* Deliberately below ADAPTIVE_THINKING_MAX_TOKENS_FLOOR (64K), which sizes the
|
|
3432
|
-
* request's real ceiling and therefore has to cover the worst case: exceeding it
|
|
3433
|
-
* truncates a reply mid-tag, an unrecoverable failure. A hold has no such duty -
|
|
3434
|
-
* exceeding it settles as a shortfall debit - so it is sized for the long turn
|
|
3435
|
-
* (roughly 3x the largest observed visible answer, leaving the rest for the trace)
|
|
3436
|
-
* rather than the worst one, which is what keeps the gate off affordable requests.
|
|
3437
|
-
*/
|
|
3438
|
-
const PREFLIGHT_RESERVATION_REASONING_OUTPUT_TOKENS = 32768;
|
|
3439
|
-
/**
|
|
3440
|
-
* Output-token figure to price a pre-flight credit hold at, given the max_tokens
|
|
3441
|
-
* this request will actually send. Never raises the caller's ceiling: a request
|
|
3442
|
-
* that asks for less than the cap holds only what it can possibly spend.
|
|
3443
|
-
*
|
|
3444
|
-
* Reserving only, never gating: the per-member org credit cap is still priced on
|
|
3445
|
-
* the unshrunk ceiling at both call sites, since that check has no settlement
|
|
3446
|
-
* counterpart to correct an under-estimate. That is a strictly larger figure than
|
|
3447
|
-
* the hold, not a true upper bound on the turn - it prices one model round trip,
|
|
3448
|
-
* at the uncached input rate, on the primary model - so it still under-counts a
|
|
3449
|
-
* multi-round tool loop, a cache-write turn, or a fallback hop onto pricier pricing.
|
|
3450
|
-
*
|
|
3451
|
-
* @param reasonsWithinOutputBudget - reasonsWithinOutputBudget(modelInfo); passed as
|
|
3452
|
-
* a boolean because common cannot import llm-adapters.
|
|
3453
|
-
*/
|
|
3454
|
-
const reservationOutputTokens = (requestedMaxTokens, reasonsWithinOutputBudget = false) => Math.min(requestedMaxTokens, reasonsWithinOutputBudget ? PREFLIGHT_RESERVATION_REASONING_OUTPUT_TOKENS : PREFLIGHT_RESERVATION_OUTPUT_TOKENS);
|
|
3455
2907
|
z.enum([
|
|
3456
2908
|
"openai",
|
|
3457
2909
|
"test",
|
|
@@ -3511,7 +2963,22 @@ const PromptBatchQuerySchema = z$1.object({
|
|
|
3511
2963
|
personal: z$1.boolean().optional()
|
|
3512
2964
|
});
|
|
3513
2965
|
z$1.object({ queries: z$1.array(PromptBatchQuerySchema).min(1).max(32).refine((qs) => new Set(qs.map((q) => q.key)).size === qs.length, { message: "Batch query keys must be unique" }) });
|
|
3514
|
-
|
|
2966
|
+
/**
|
|
2967
|
+
* Request schema for POST /api/chat - the simplified external chat surface.
|
|
2968
|
+
*
|
|
2969
|
+
* Shared between the Next.js API handler (apps/client/pages/api/chat.ts, which
|
|
2970
|
+
* validates req.body with this exact object) and the OpenAPI registry
|
|
2971
|
+
* (b4m-core/common/src/openapi), so the published contract cannot drift from
|
|
2972
|
+
* what the handler actually accepts. The model is optional here and resolved
|
|
2973
|
+
* server-side from admin settings when omitted.
|
|
2974
|
+
*
|
|
2975
|
+
* Public-API rule: no `.catch()` / top-level `.transform()`. Both silently mutate
|
|
2976
|
+
* caller input (fail-quiet) and are opaque to zod-to-openapi. `historyCount` uses
|
|
2977
|
+
* `.default()` (fail loud on a bad value); unknown tool ids are filtered in the
|
|
2978
|
+
* handler (see filterKnownTools) instead of by a schema transform. This keeps the
|
|
2979
|
+
* schema fully OpenAPI-representable with no doc projection needed.
|
|
2980
|
+
*/
|
|
2981
|
+
const SimplifiedChatRequestSchema = z$1.object({
|
|
3515
2982
|
sessionId: z$1.string().nullish(),
|
|
3516
2983
|
message: z$1.string(),
|
|
3517
2984
|
organizationId: z$1.string().optional(),
|
|
@@ -3535,18 +3002,28 @@ z$1.object({
|
|
|
3535
3002
|
"grounded",
|
|
3536
3003
|
"surface"
|
|
3537
3004
|
]).optional(),
|
|
3538
|
-
skip_auto_offers: z$1.boolean().optional().describe("Suppress tools the server would otherwise attach on its own for this session (the knowledge-base search offer, in-app view navigation, blog drafting/editing/publishing, and
|
|
3005
|
+
skip_auto_offers: z$1.boolean().optional().describe("Suppress tools the server would otherwise attach on its own for this session (the knowledge-base search offer, in-app view navigation, blog drafting/editing/publishing, skill invocation, and every tool from a connected MCP server). Native tools you request explicitly in `tools` are unaffected, but an MCP server tool cannot be named through this field, so with this flag set a caller with an MCP server connected sees none of its tools regardless of `tools`. One system-prompt block goes with them: withholding in-app view navigation also drops the view-registry block that exists only to describe it. No other prompt content changes. This does not switch off retrieval: a session with forced knowledge retrieval still retrieves, and documents already attached to the session are still placed in the prompt directly. Any promptMode suppresses these too, so false has no effect alongside one."),
|
|
3539
3006
|
includePromptDetails: z$1.boolean().optional(),
|
|
3540
3007
|
includeSystemPrompt: z$1.boolean().optional(),
|
|
3541
3008
|
systemPrompt: z$1.string().max(PROMPT_TEXT_MAX).optional().describe("System-prompt text for this request only, never persisted. Rendered as a defended block appended after every other system-prompt source, with prose instructing the model to defer to organization, session and data-lake guidance. Over the cap is a 422, never truncated.")
|
|
3542
3009
|
});
|
|
3543
|
-
|
|
3010
|
+
/**
|
|
3011
|
+
* Async ACK returned on the default (wait:false) path of POST /api/chat. The
|
|
3012
|
+
* `type`/`errorCode` pair below is the same classifier the `wait: true` body and
|
|
3013
|
+
* the polled quest (`GET /api/quests/{id}`) carry, so it is modelled once here -
|
|
3014
|
+
* the rest of those two bodies is NOT described by this schema. The
|
|
3015
|
+
* handler assembles the ack body inline (apps/client/pages/api/chat.ts), so
|
|
3016
|
+
* this schema MUST stay in sync with that `res.json({...})` shape.
|
|
3017
|
+
*/
|
|
3018
|
+
const ChatAckSchema = z$1.object({
|
|
3544
3019
|
id: z$1.string(),
|
|
3545
3020
|
status: z$1.string(),
|
|
3546
3021
|
message_received: z$1.boolean(),
|
|
3547
3022
|
timestamp: z$1.string(),
|
|
3548
3023
|
model: z$1.string(),
|
|
3549
3024
|
message: z$1.string().optional(),
|
|
3025
|
+
type: z$1.enum(CHAT_HISTORY_ITEM_TYPES).optional(),
|
|
3026
|
+
errorCode: z$1.enum(QUEST_ERROR_CODES).optional(),
|
|
3550
3027
|
tools: z$1.object({
|
|
3551
3028
|
toolMode: z$1.enum(["fast", "smart"]).optional(),
|
|
3552
3029
|
autoSelectedTools: z$1.array(z$1.string()).optional(),
|
|
@@ -3560,6 +3037,44 @@ z$1.object({
|
|
|
3560
3037
|
})
|
|
3561
3038
|
});
|
|
3562
3039
|
/**
|
|
3040
|
+
* The quest a `wait: false` caller polls at `GET /api/quests/{id}` - the outcome
|
|
3041
|
+
* of the turn the ACK above only acknowledged.
|
|
3042
|
+
*
|
|
3043
|
+
* Deliberately the OUTCOME SUBSET, not the whole quest: that endpoint is a plain
|
|
3044
|
+
* handler rather than a contract, so this models only what decides whether the
|
|
3045
|
+
* turn succeeded, and a poll body carries further fields (`images`, `files`,
|
|
3046
|
+
* `toolPayloads`, `promptMeta`, ...). Must stay in sync with that handler's
|
|
3047
|
+
* `res.json` shape (apps/client/pages/api/quests/[id]/index.ts) - unlike a
|
|
3048
|
+
* contract-registered request/response schema, nothing validates this at
|
|
3049
|
+
* runtime. The "parses against the published ChatQuestPollResultSchema"
|
|
3050
|
+
* integration test (index.integration.test.ts) only proves the handler's
|
|
3051
|
+
* CURRENT response satisfies this schema - a non-strict `z.object` strips
|
|
3052
|
+
* unknown keys rather than rejecting them, and only `id` is required, so a
|
|
3053
|
+
* field the handler starts returning without a matching addition here keeps
|
|
3054
|
+
* that test green. The real per-field coverage lives in the sibling
|
|
3055
|
+
* assertions in that same test file; a shape addition still needs a schema
|
|
3056
|
+
* update by hand.
|
|
3057
|
+
*
|
|
3058
|
+
* A failed turn is still `status: 'done'` with the failure text in `reply`, so
|
|
3059
|
+
* `reply` alone cannot tell an answer from a failure - `type` and `errorCode` are
|
|
3060
|
+
* what separate a CLASSIFIED failure. A run recovered from a timeout with partial
|
|
3061
|
+
* content is not one of those: `terminalRecoveryFor` (questTimeoutRecovery.ts)
|
|
3062
|
+
* flips only `status` to preserve the surviving content, so it polls back as
|
|
3063
|
+
* `type: 'message'` even though it never finished.
|
|
3064
|
+
*/
|
|
3065
|
+
const ChatQuestPollResultSchema = z$1.object({
|
|
3066
|
+
id: z$1.string(),
|
|
3067
|
+
status: z$1.enum([
|
|
3068
|
+
"stopped",
|
|
3069
|
+
"running",
|
|
3070
|
+
"done"
|
|
3071
|
+
]).optional(),
|
|
3072
|
+
type: z$1.enum(CHAT_HISTORY_ITEM_TYPES).optional(),
|
|
3073
|
+
errorCode: z$1.enum(QUEST_ERROR_CODES).optional(),
|
|
3074
|
+
reply: z$1.string().nullable().optional(),
|
|
3075
|
+
replies: z$1.array(z$1.string()).optional()
|
|
3076
|
+
});
|
|
3077
|
+
/**
|
|
3563
3078
|
* Reusable JSON error envelope (plain; the OpenAPI layer annotates it).
|
|
3564
3079
|
*
|
|
3565
3080
|
* Must stay in sync with the published `ErrorResponse` component
|
|
@@ -3578,7 +3093,18 @@ const ApiErrorSchema = z$1.object({
|
|
|
3578
3093
|
*/
|
|
3579
3094
|
name: z$1.string().optional()
|
|
3580
3095
|
});
|
|
3581
|
-
|
|
3096
|
+
/**
|
|
3097
|
+
* Error envelope for the 422 a credit-metered endpoint returns for two unrelated
|
|
3098
|
+
* reasons: "your body is invalid" and "you cannot afford this". `errorCode` is
|
|
3099
|
+
* what separates them - `insufficientCreditsError` (see insufficientCredits.ts)
|
|
3100
|
+
* tags the credit case, so its absence means an ordinary validation failure.
|
|
3101
|
+
*
|
|
3102
|
+
* Derived from `ApiErrorSchema` rather than re-declaring `error`/`request_id`:
|
|
3103
|
+
* both of those 422s are *thrown*, so errorHandler serves the body and adds
|
|
3104
|
+
* `name`. Extending is what keeps that documented here (and what drops it again
|
|
3105
|
+
* on the sunset date) instead of leaving a bespoke copy behind to drift.
|
|
3106
|
+
*/
|
|
3107
|
+
const InsufficientCreditsErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.literal("insufficient_credits").optional() });
|
|
3582
3108
|
const supportedVoiceGenerationVendor = z.enum(["openai", "elevenlabs"]);
|
|
3583
3109
|
const voiceOutputFormatSchema = z.enum([
|
|
3584
3110
|
"mp3",
|
|
@@ -3589,13 +3115,12 @@ const voiceOutputFormatSchema = z.enum([
|
|
|
3589
3115
|
"pcm"
|
|
3590
3116
|
]);
|
|
3591
3117
|
const voiceResponseEncodingSchema = z.enum(["binary", "base64"]);
|
|
3592
|
-
const
|
|
3118
|
+
const TTS_ABSOLUTE_MAX_INPUT_CHARS = Math.max(...Object.values({
|
|
3593
3119
|
openai: 4096,
|
|
3594
3120
|
elevenlabs: 1e4
|
|
3595
|
-
};
|
|
3596
|
-
const TTS_ABSOLUTE_MAX_INPUT_CHARS = Math.max(...Object.values(TTS_MAX_INPUT_CHARS));
|
|
3121
|
+
}));
|
|
3597
3122
|
const ttsLanguageCodeSchema = z.string().regex(/^[a-z]{2}$/, "languageCode must be a lowercase ISO 639-1 code, e.g. \"en\" or \"ja\"");
|
|
3598
|
-
z.object({
|
|
3123
|
+
const ttsRequestSchema = z.object({
|
|
3599
3124
|
text: z.string().min(1).max(TTS_ABSOLUTE_MAX_INPUT_CHARS),
|
|
3600
3125
|
provider: supportedVoiceGenerationVendor.optional(),
|
|
3601
3126
|
model: z.string().optional(),
|
|
@@ -3621,7 +3146,18 @@ const audioSaveSkippedReasonSchema = z.enum([
|
|
|
3621
3146
|
"file_too_large",
|
|
3622
3147
|
"error"
|
|
3623
3148
|
]);
|
|
3624
|
-
|
|
3149
|
+
/**
|
|
3150
|
+
* JSON body of `POST /api/ai/tts` when the caller asks for `encoding: 'base64'`.
|
|
3151
|
+
* The default `binary` encoding returns raw audio bytes instead and has no JSON
|
|
3152
|
+
* shape.
|
|
3153
|
+
*
|
|
3154
|
+
* The save + provider fields are all optional because the handler spreads them in
|
|
3155
|
+
* only when they apply: the save fields are absent when no copy was attempted
|
|
3156
|
+
* (`preview: true`, or the saveGeneratedAudio preference is off), and
|
|
3157
|
+
* `provider`/`fallbackFrom` appear only when the requested provider was
|
|
3158
|
+
* unavailable and another one stood in.
|
|
3159
|
+
*/
|
|
3160
|
+
const ttsBase64ResponseSchema = z.object({
|
|
3625
3161
|
/** Base64-encoded audio payload. */
|
|
3626
3162
|
audio: z.string(),
|
|
3627
3163
|
format: voiceOutputFormatSchema,
|
|
@@ -3635,7 +3171,32 @@ z.object({
|
|
|
3635
3171
|
/** The originally requested provider that could not serve the request. */
|
|
3636
3172
|
fallbackFrom: supportedVoiceGenerationVendor.optional()
|
|
3637
3173
|
});
|
|
3638
|
-
|
|
3174
|
+
/**
|
|
3175
|
+
* Error body for `POST /api/ai/tts`, shared by the 401, 422, 429 and 502; the 413
|
|
3176
|
+
* has a shape of its own (`ttsResponseTooLargeSchema`). `errorCode` is present only
|
|
3177
|
+
* on the conditions that carry a classifier; an ordinary validation 422 has none.
|
|
3178
|
+
*
|
|
3179
|
+
* Extends `ApiErrorSchema` because what decides whether a body carries the fields
|
|
3180
|
+
* errorHandler adds - `request_id`, and `name` until its 2026-12-01 sunset - is
|
|
3181
|
+
* whether the body was THROWN, not which status it wears, and three of these four
|
|
3182
|
+
* statuses are reachable both ways:
|
|
3183
|
+
*
|
|
3184
|
+
* - 401: thrown by apiKeyAuth on a rejected key; written by `auth` when no
|
|
3185
|
+
* credential was presented at all, and by the handler for
|
|
3186
|
+
* `provider_not_configured` and for an upstream credential rejection
|
|
3187
|
+
* (`provider_rejected`).
|
|
3188
|
+
* - 422: thrown by request validation and by the char-limit / format guards;
|
|
3189
|
+
* written by the handler for `insufficient_credits` and for an upstream 422.
|
|
3190
|
+
* - 429: thrown by apiKeyRateLimit; written by the handler on an upstream 429.
|
|
3191
|
+
* - 502: only ever written.
|
|
3192
|
+
*
|
|
3193
|
+
* So those two are genuinely optional here, and splitting this per status would be
|
|
3194
|
+
* wrong for the first three. The 502 does advertise both without ever sending them;
|
|
3195
|
+
* that is not worth a fourth error schema on one route, and `request_id` missing from
|
|
3196
|
+
* this handler's written bodies is a gap in the handler rather than something to
|
|
3197
|
+
* enshrine in a schema.
|
|
3198
|
+
*/
|
|
3199
|
+
const ttsErrorResponseSchema = ApiErrorSchema.extend({
|
|
3639
3200
|
provider: supportedVoiceGenerationVendor.optional(),
|
|
3640
3201
|
errorCode: z.enum([
|
|
3641
3202
|
"insufficient_credits",
|
|
@@ -3643,7 +3204,21 @@ ApiErrorSchema.extend({
|
|
|
3643
3204
|
"provider_rejected"
|
|
3644
3205
|
]).optional()
|
|
3645
3206
|
});
|
|
3646
|
-
|
|
3207
|
+
/**
|
|
3208
|
+
* 413 body: the audio was generated and billed but exceeds the serverless
|
|
3209
|
+
* response-size cap. When a browsable copy was saved, `fileUrl` is how the caller
|
|
3210
|
+
* retrieves the audio it paid for.
|
|
3211
|
+
*
|
|
3212
|
+
* Not derived from `ApiErrorSchema`: every 413 on this route is written, never
|
|
3213
|
+
* thrown, so errorHandler never serves one and `name` is genuinely absent rather than
|
|
3214
|
+
* optional - a stronger claim than `ttsErrorResponseSchema` can make for its own
|
|
3215
|
+
* statuses, see the note there. Two writers, and only the first matches the paragraph
|
|
3216
|
+
* above: the exceedsTtsResponseLimit guard, and the upstream-4xx passthrough relaying
|
|
3217
|
+
* a provider 413, where nothing was generated or billed and there is no `fileUrl`. An
|
|
3218
|
+
* oversized *request* body is a third 413 that never reaches this schema at all -
|
|
3219
|
+
* Next's own body parser answers it in plain text before the router runs.
|
|
3220
|
+
*/
|
|
3221
|
+
const ttsResponseTooLargeSchema = z.object({
|
|
3647
3222
|
error: z.string(),
|
|
3648
3223
|
provider: supportedVoiceGenerationVendor,
|
|
3649
3224
|
saved: z.literal(true).optional(),
|
|
@@ -3656,7 +3231,15 @@ z.enum(["openai"]);
|
|
|
3656
3231
|
* New vendors are added here and in the `aiSoundService` factory.
|
|
3657
3232
|
*/
|
|
3658
3233
|
const supportedSoundGenerationVendor = z.enum(["elevenlabs"]);
|
|
3659
|
-
|
|
3234
|
+
/**
|
|
3235
|
+
* Inbound request body for `POST /api/ai/sound-effects`.
|
|
3236
|
+
*
|
|
3237
|
+
* `durationSeconds` and `promptInfluence` bounds mirror the ElevenLabs
|
|
3238
|
+
* sound-generation limits (0.5-30s for the default eleven_text_to_sound_v2
|
|
3239
|
+
* model, prompt influence 0-1). `format` is the provider-specific output
|
|
3240
|
+
* encoding token (e.g. `mp3_44100_128`).
|
|
3241
|
+
*/
|
|
3242
|
+
const soundEffectsRequestSchema = z.object({
|
|
3660
3243
|
provider: supportedSoundGenerationVendor.default("elevenlabs"),
|
|
3661
3244
|
text: z.string().min(1).max(1e3),
|
|
3662
3245
|
durationSeconds: z.number().min(.5).max(30).optional(),
|
|
@@ -3674,13 +3257,23 @@ const supportedMusicGenerationVendor = z.enum(["elevenlabs"]);
|
|
|
3674
3257
|
* change the request surface needs to accept it.
|
|
3675
3258
|
*/
|
|
3676
3259
|
const supportedMusicModel = z.enum(["music_v1"]);
|
|
3677
|
-
|
|
3678
|
-
|
|
3260
|
+
/**
|
|
3261
|
+
* Inbound request body for `POST /api/ai/music`.
|
|
3262
|
+
*
|
|
3263
|
+
* `lengthMs` upper bound is capped below the ElevenLabs Music API ceiling to fit
|
|
3264
|
+
* the serving function's time budget (see MAX_MUSIC_LENGTH_MS). It carries a
|
|
3265
|
+
* default rather than being optional so the billed
|
|
3266
|
+
* length is always known up front (the reserve/settle path needs a deterministic
|
|
3267
|
+
* cost before generation) and the route can force that exact length on the
|
|
3268
|
+
* provider. `format` is the provider-specific output encoding token (e.g.
|
|
3269
|
+
* `mp3_44100_128`).
|
|
3270
|
+
*/
|
|
3271
|
+
const musicRequestSchema = z.object({
|
|
3679
3272
|
provider: supportedMusicGenerationVendor.default("elevenlabs"),
|
|
3680
3273
|
prompt: z.string().min(1).max(2e3),
|
|
3681
3274
|
lengthMs: z.number().int().min(3e3).max(12e4).default(1e4),
|
|
3682
3275
|
forceInstrumental: z.boolean().optional(),
|
|
3683
|
-
modelId: supportedMusicModel.default(
|
|
3276
|
+
modelId: supportedMusicModel.default("music_v1"),
|
|
3684
3277
|
format: z.string().optional()
|
|
3685
3278
|
});
|
|
3686
3279
|
VIDEO_SIZE_CONSTRAINTS.SORA.durations;
|
|
@@ -3776,6 +3369,41 @@ const AGENT_EXECUTION_STATUSES = [
|
|
|
3776
3369
|
"aborted"
|
|
3777
3370
|
];
|
|
3778
3371
|
/**
|
|
3372
|
+
* Why a session summarization happened, stamped on `ISession.summaryTrigger`. Single source for the
|
|
3373
|
+
* places that each used to spell this list out: the Session zod schema (schemas/actions.ts), the
|
|
3374
|
+
* entity type (types/entities/SessionTypes.ts), the Mongoose path (packages/database SessionModel)
|
|
3375
|
+
* and the session.summarize event payload (apps/client server/utils/eventBus.ts). They drifted -
|
|
3376
|
+
* the Mongoose enum said 'milestone'/'growth' for two values nothing produces - and a drift there
|
|
3377
|
+
* is invisible on the update path, because BaseModel's findOneAndUpdate writes without
|
|
3378
|
+
* runValidators.
|
|
3379
|
+
*
|
|
3380
|
+
* Those surfaces now name PERSISTED_SESSION_SUMMARY_TRIGGERS below, not the full union: a stored
|
|
3381
|
+
* field may only carry a reason a run HAPPENED. 'throttling' is the exception that forced the
|
|
3382
|
+
* split - shouldSummarizeSession (b4m-core/services ChatCompletionFeatures) returns it as the
|
|
3383
|
+
* reason it DECLINED to summarize, so it describes no run and belongs to a decision, not a
|
|
3384
|
+
* document. It is typed by SummarizationDecision there, not by the session field.
|
|
3385
|
+
*
|
|
3386
|
+
* 'manual' means someone asked for one notebook's summary. The admin sweep (apps/client
|
|
3387
|
+
* server/events/spider.ts) summarizes every un-summarized notebook of the admin who ran it in one
|
|
3388
|
+
* billed pass, so it stamps 'spider' instead: without that, one deliberate click and a whole sweep
|
|
3389
|
+
* are indistinguishable when someone investigates unexpected summarization spend.
|
|
3390
|
+
*/
|
|
3391
|
+
/**
|
|
3392
|
+
* The triggers a document may actually carry - every reason a summarization HAPPENED. The event
|
|
3393
|
+
* payload and createSessionParametersSchema both name this list rather than the full union below,
|
|
3394
|
+
* so 'throttling' cannot be published, cannot be stored, and therefore cannot reach a copy path.
|
|
3395
|
+
* Add a new reason-it-happened here, not to SESSION_SUMMARY_TRIGGERS, and every one of those
|
|
3396
|
+
* boundaries picks it up.
|
|
3397
|
+
*/
|
|
3398
|
+
const PERSISTED_SESSION_SUMMARY_TRIGGERS = [
|
|
3399
|
+
"manual",
|
|
3400
|
+
"project",
|
|
3401
|
+
"earlyMilestone",
|
|
3402
|
+
"contentGrowth",
|
|
3403
|
+
"spider"
|
|
3404
|
+
];
|
|
3405
|
+
[...PERSISTED_SESSION_SUMMARY_TRIGGERS];
|
|
3406
|
+
/**
|
|
3779
3407
|
* Operator allow-list for the client-authored Mongo filter carried on a `subscribe_query` frame.
|
|
3780
3408
|
*
|
|
3781
3409
|
* The WS data-subscribe handler forwards that filter to `Model.find` and persists it on the
|
|
@@ -4037,11 +3665,24 @@ const QuestExportProgressAction = z$1.object({
|
|
|
4037
3665
|
"failed"
|
|
4038
3666
|
]),
|
|
4039
3667
|
progress: z$1.number(),
|
|
4040
|
-
detail: z$1.string().optional(),
|
|
4041
|
-
downloadUrl: z$1.string().optional(),
|
|
4042
|
-
filename: z$1.string().optional(),
|
|
3668
|
+
detail: z$1.string().optional(),
|
|
3669
|
+
downloadUrl: z$1.string().optional(),
|
|
3670
|
+
filename: z$1.string().optional(),
|
|
3671
|
+
errorMessage: z$1.string().optional(),
|
|
3672
|
+
droppedQuestCount: z$1.number().optional(),
|
|
3673
|
+
clientId: z$1.string().optional()
|
|
3674
|
+
});
|
|
3675
|
+
const OrgFeedbackSummaryProgressAction = z$1.object({
|
|
3676
|
+
action: z$1.literal("org_feedback_summary_progress"),
|
|
3677
|
+
summaryJobId: z$1.string(),
|
|
3678
|
+
organizationId: z$1.string(),
|
|
3679
|
+
status: z$1.enum([
|
|
3680
|
+
"processing",
|
|
3681
|
+
"completed",
|
|
3682
|
+
"failed"
|
|
3683
|
+
]),
|
|
3684
|
+
progress: z$1.number(),
|
|
4043
3685
|
errorMessage: z$1.string().optional(),
|
|
4044
|
-
droppedQuestCount: z$1.number().optional(),
|
|
4045
3686
|
clientId: z$1.string().optional()
|
|
4046
3687
|
});
|
|
4047
3688
|
const SpiderProgressUpdateAction = z$1.object({
|
|
@@ -4128,13 +3769,7 @@ const StreamedChatCompletionAction = z$1.object({
|
|
|
4128
3769
|
replies: z$1.array(z$1.string()).optional(),
|
|
4129
3770
|
images: z$1.array(z$1.string()).optional(),
|
|
4130
3771
|
videos: z$1.array(z$1.string()).optional(),
|
|
4131
|
-
type: z$1.enum(
|
|
4132
|
-
"message",
|
|
4133
|
-
"oob",
|
|
4134
|
-
"error",
|
|
4135
|
-
"system",
|
|
4136
|
-
"voice_transcript"
|
|
4137
|
-
]),
|
|
3772
|
+
type: z$1.enum(CHAT_HISTORY_ITEM_TYPES),
|
|
4138
3773
|
status: z$1.enum([
|
|
4139
3774
|
"stopped",
|
|
4140
3775
|
"running",
|
|
@@ -4935,18 +4570,13 @@ const SessionCreatedAction = shareableDocumentSchema.extend({
|
|
|
4935
4570
|
claudeConversationId: z$1.string().optional(),
|
|
4936
4571
|
summary: z$1.string().optional(),
|
|
4937
4572
|
summaryAt: z$1.date().optional(),
|
|
4938
|
-
summaryTrigger: z$1.enum(
|
|
4939
|
-
"manual",
|
|
4940
|
-
"project",
|
|
4941
|
-
"earlyMilestone",
|
|
4942
|
-
"contentGrowth",
|
|
4943
|
-
"throttling"
|
|
4944
|
-
]).optional(),
|
|
4573
|
+
summaryTrigger: z$1.enum(PERSISTED_SESSION_SUMMARY_TRIGGERS).optional(),
|
|
4945
4574
|
deletedAt: z$1.date().optional(),
|
|
4946
4575
|
tags: z$1.array(z$1.object({
|
|
4947
4576
|
name: z$1.string(),
|
|
4948
4577
|
strength: z$1.number()
|
|
4949
4578
|
})).optional(),
|
|
4579
|
+
taggedAt: z$1.date().optional(),
|
|
4950
4580
|
clonedSourceId: z$1.string().nullable().optional(),
|
|
4951
4581
|
forkedSourceId: z$1.string().nullable().optional(),
|
|
4952
4582
|
isAutoNamed: z$1.boolean().optional(),
|
|
@@ -5137,7 +4767,14 @@ const PermissionRequestAction = z$1.object({
|
|
|
5137
4767
|
executionId: z$1.string(),
|
|
5138
4768
|
toolName: z$1.string(),
|
|
5139
4769
|
toolInput: z$1.unknown(),
|
|
5140
|
-
iteration: z$1.number()
|
|
4770
|
+
iteration: z$1.number(),
|
|
4771
|
+
/**
|
|
4772
|
+
* Provider tool_use id of the specific gated call this card is asking about.
|
|
4773
|
+
* The client echoes it back on `permission_response` so the server can bind
|
|
4774
|
+
* the answer to THIS pause rather than the latest one that happens to share
|
|
4775
|
+
* a tool name - see `handlePermissionResponse`'s toolCallId check.
|
|
4776
|
+
*/
|
|
4777
|
+
toolCallId: z$1.string().optional()
|
|
5141
4778
|
});
|
|
5142
4779
|
const ChildExecutionSnapshotSchema = z$1.lazy(() => z$1.object({
|
|
5143
4780
|
executionId: z$1.string(),
|
|
@@ -5159,7 +4796,8 @@ const ReconnectResultAction = z$1.object({
|
|
|
5159
4796
|
pendingPermission: z$1.object({
|
|
5160
4797
|
toolName: z$1.string(),
|
|
5161
4798
|
toolInput: z$1.unknown(),
|
|
5162
|
-
requestedAt: z$1.union([z$1.string(), z$1.date()])
|
|
4799
|
+
requestedAt: z$1.union([z$1.string(), z$1.date()]),
|
|
4800
|
+
toolCallId: z$1.string().optional()
|
|
5163
4801
|
}).optional(),
|
|
5164
4802
|
totalCreditsUsed: z$1.number().optional(),
|
|
5165
4803
|
iterationCount: z$1.number().optional(),
|
|
@@ -5208,6 +4846,7 @@ z$1.discriminatedUnion("action", [
|
|
|
5208
4846
|
ResearchTaskStatusUpdateAction,
|
|
5209
4847
|
NotebookCurationProgressUpdateAction,
|
|
5210
4848
|
QuestExportProgressAction,
|
|
4849
|
+
OrgFeedbackSummaryProgressAction,
|
|
5211
4850
|
SpiderProgressUpdateAction,
|
|
5212
4851
|
SpiderCompleteAction,
|
|
5213
4852
|
SpiderErrorAction,
|
|
@@ -5250,7 +4889,29 @@ z$1.discriminatedUnion("action", [
|
|
|
5250
4889
|
PermissionRequestAction,
|
|
5251
4890
|
ReconnectResultAction
|
|
5252
4891
|
]);
|
|
5253
|
-
|
|
4892
|
+
/**
|
|
4893
|
+
* Public wire schemas for the agent-executor (ReAct) endpoints:
|
|
4894
|
+
* `POST /api/v1/agent-executions` and `GET /api/v1/agent-executions/{id}`.
|
|
4895
|
+
*
|
|
4896
|
+
* These are the REST twin of the WebSocket `agent_execute` command surface
|
|
4897
|
+
* (apps/client/server/websocket/agentExecute.ts). Both transports funnel into the
|
|
4898
|
+
* same `startAgentExecution` service, but the wire shapes are deliberately separate:
|
|
4899
|
+
* the WS payload carries UI-only fields (routing provenance, an optimistic-bubble
|
|
4900
|
+
* back-reference) that must never become published API surface, and public fields are
|
|
4901
|
+
* snake_case per CONVENTIONS.md section 2 while the WS command is camelCase.
|
|
4902
|
+
*
|
|
4903
|
+
* Public-API rules apply here: no `.catch()`, no top-level `.transform()`.
|
|
4904
|
+
*/
|
|
4905
|
+
/**
|
|
4906
|
+
* Request body for `POST /api/v1/agent-executions`.
|
|
4907
|
+
*
|
|
4908
|
+
* `session_id` is required rather than defaulted (unlike `POST /api/chat`, which falls
|
|
4909
|
+
* back to the caller's last notebook): the session is what determines which agent
|
|
4910
|
+
* profile the executor builds, so guessing it would silently change the run's
|
|
4911
|
+
* behaviour. Everything else is optional and falls back to admin defaults or the
|
|
4912
|
+
* agent's own orchestration profile.
|
|
4913
|
+
*/
|
|
4914
|
+
const AgentExecutionStartRequestSchema = z$1.object({
|
|
5254
4915
|
session_id: z$1.string().min(1),
|
|
5255
4916
|
message: z$1.string().min(1),
|
|
5256
4917
|
/** Falls back to the deployment's default chat model when omitted. */
|
|
@@ -5307,7 +4968,11 @@ z$1.object({
|
|
|
5307
4968
|
*/
|
|
5308
4969
|
enable_artifacts: z$1.boolean().optional()
|
|
5309
4970
|
});
|
|
5310
|
-
|
|
4971
|
+
/**
|
|
4972
|
+
* 202 ACK for `POST /api/v1/agent-executions`. The run is fire-and-forget: nothing is
|
|
4973
|
+
* streamed back over REST, so the caller polls `poll_url` until `status` is terminal.
|
|
4974
|
+
*/
|
|
4975
|
+
const AgentExecutionAckSchema = z$1.object({
|
|
5311
4976
|
id: z$1.string(),
|
|
5312
4977
|
status: z$1.literal("pending"),
|
|
5313
4978
|
session_id: z$1.string(),
|
|
@@ -5337,7 +5002,15 @@ const AgentExecutionStepSchema = z$1.object({
|
|
|
5337
5002
|
/** Set on `action` steps: the tool the agent invoked. */
|
|
5338
5003
|
tool_name: z$1.string().optional()
|
|
5339
5004
|
});
|
|
5340
|
-
|
|
5005
|
+
/**
|
|
5006
|
+
* Poll response for `GET /api/v1/agent-executions/{id}`.
|
|
5007
|
+
*
|
|
5008
|
+
* `steps` is the live trace: it grows while the run is in flight (read from the
|
|
5009
|
+
* checkpoint) and freezes at the final one. `answer` is null until the run reaches a
|
|
5010
|
+
* terminal status, and stays null on `failed` / `aborted` - where `error` carries the
|
|
5011
|
+
* reason instead.
|
|
5012
|
+
*/
|
|
5013
|
+
const AgentExecutionStatusResponseSchema = z$1.object({
|
|
5341
5014
|
id: z$1.string(),
|
|
5342
5015
|
status: z$1.enum([
|
|
5343
5016
|
"pending",
|
|
@@ -5369,7 +5042,8 @@ z$1.object({
|
|
|
5369
5042
|
created_at: z$1.string(),
|
|
5370
5043
|
updated_at: z$1.string()
|
|
5371
5044
|
});
|
|
5372
|
-
|
|
5045
|
+
/** Path parameter for `GET /api/v1/agent-executions/{id}`. */
|
|
5046
|
+
const AgentExecutionIdParamSchema = z$1.object({ id: z$1.string().min(1) });
|
|
5373
5047
|
/**
|
|
5374
5048
|
* Tool schema matching ICompletionOptionTools.toolSchema. The Zod surface only
|
|
5375
5049
|
* covers wire-format fields (toolFn is server-side). Replaces the historical
|
|
@@ -5419,7 +5093,17 @@ const CompletionMessageSchema = z$1.object({
|
|
|
5419
5093
|
content: z$1.union([z$1.string(), z$1.array(z$1.any())]),
|
|
5420
5094
|
cache: z$1.boolean().optional()
|
|
5421
5095
|
});
|
|
5422
|
-
|
|
5096
|
+
/**
|
|
5097
|
+
* Schema for CLI LLM completion requests
|
|
5098
|
+
* Shared between Next.js API route (dev) and Lambda function (production)
|
|
5099
|
+
*
|
|
5100
|
+
* `response_format`, `stream`, `tools`, `temperature`, and `max_tokens` are all
|
|
5101
|
+
* accepted at the top level (OpenAI-compatible, matching how every major LLM
|
|
5102
|
+
* SDK shapes a completion request) AND nested under `options` (legacy shape).
|
|
5103
|
+
* Use `normalizeCompletionRequest()` to collapse both surfaces into the
|
|
5104
|
+
* canonical `options.<field>` location before downstream consumption.
|
|
5105
|
+
*/
|
|
5106
|
+
const CompletionRequestSchema = z$1.object({
|
|
5423
5107
|
model: z$1.string(),
|
|
5424
5108
|
messages: z$1.array(CompletionMessageSchema),
|
|
5425
5109
|
response_format: ResponseFormatSchema.optional(),
|
|
@@ -5466,18 +5150,42 @@ const CompletionContentEventSchema = z$1.object({
|
|
|
5466
5150
|
stopReason: z$1.string().optional(),
|
|
5467
5151
|
thinking: z$1.array(z$1.any()).optional()
|
|
5468
5152
|
});
|
|
5153
|
+
/**
|
|
5154
|
+
* The in-band terminal failure frame. Because this endpoint flushes SSE headers
|
|
5155
|
+
* before it authenticates or prices anything, EVERY failure past the malformed-body
|
|
5156
|
+
* check arrives here under a 200 - there is no pre-stream 422 on this surface the
|
|
5157
|
+
* way there is on /api/chat and /api/embed/chat.
|
|
5158
|
+
*
|
|
5159
|
+
* `code` is the machine-readable classifier, drawn from QUEST_ERROR_CODES (itself a
|
|
5160
|
+
* narrowing of the platform-wide API_ERROR_CODES, so it cannot drift from the codes
|
|
5161
|
+
* the JSON surfaces use). It is what makes mid-generation credit exhaustion
|
|
5162
|
+
* detectable: the `message` is prose and callers must never match on it. Absent for
|
|
5163
|
+
* unclassified failures, so treat it as optional.
|
|
5164
|
+
*
|
|
5165
|
+
* The field is `code` rather than the conventions' `errorCode` because this frame
|
|
5166
|
+
* shipped that way and is a published wire shape; the vocabulary is what had to be
|
|
5167
|
+
* shared, not the key.
|
|
5168
|
+
*/
|
|
5469
5169
|
const CompletionSseErrorEventSchema = z$1.object({
|
|
5470
5170
|
type: z$1.literal("error"),
|
|
5471
5171
|
message: z$1.string(),
|
|
5472
5172
|
requestId: z$1.string().optional(),
|
|
5473
|
-
code: z$1.
|
|
5173
|
+
code: z$1.enum(QUEST_ERROR_CODES).optional()
|
|
5474
5174
|
});
|
|
5475
|
-
|
|
5175
|
+
/** One `data:` event in the `text/event-stream` completions response. */
|
|
5176
|
+
const CompletionStreamEventSchema = z$1.union([
|
|
5476
5177
|
CompletionMetaEventSchema,
|
|
5477
5178
|
CompletionContentEventSchema,
|
|
5478
5179
|
CompletionSseErrorEventSchema
|
|
5479
5180
|
]);
|
|
5480
|
-
|
|
5181
|
+
/**
|
|
5182
|
+
* Server-side tool execution schemas for POST /api/ai/v1/tools.
|
|
5183
|
+
*
|
|
5184
|
+
* Plain Zod (no `.openapi()`) so any runtime can import them; the OpenAPI layer
|
|
5185
|
+
* annotates them via the contract. The tool-name enum MUST stay in sync with
|
|
5186
|
+
* SUPPORTED_TOOLS in apps/client/server/cli/toolsHandler.shared.ts.
|
|
5187
|
+
*/
|
|
5188
|
+
const ToolExecutionRequestSchema = z$1.object({
|
|
5481
5189
|
toolName: z$1.enum([
|
|
5482
5190
|
"weather_info",
|
|
5483
5191
|
"web_search",
|
|
@@ -6156,21 +5864,9 @@ const ALL_IMAGE_MODELS = [
|
|
|
6156
5864
|
...XAI_IMAGE_MODELS,
|
|
6157
5865
|
...GEMINI_IMAGE_MODELS
|
|
6158
5866
|
];
|
|
6159
|
-
const OPENAI_GPT_IMAGE_1_IMAGE_SIZES =
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
"1536x1024"
|
|
6163
|
-
];
|
|
6164
|
-
const OPENAI_GPT_IMAGE_2_IMAGE_SIZES = [
|
|
6165
|
-
"1024x1024",
|
|
6166
|
-
"1536x1024",
|
|
6167
|
-
"1024x1536",
|
|
6168
|
-
"2048x2048",
|
|
6169
|
-
"2048x1152",
|
|
6170
|
-
"3840x2160",
|
|
6171
|
-
"2160x3840",
|
|
6172
|
-
"auto"
|
|
6173
|
-
];
|
|
5867
|
+
const OPENAI_GPT_IMAGE_1_IMAGE_SIZES = IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_1.sizes;
|
|
5868
|
+
/** The UI presets plus the tier's non-resolution `autoSize`, which the picker cannot render. */
|
|
5869
|
+
const OPENAI_GPT_IMAGE_2_IMAGE_SIZES = [...IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.sizes, IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.autoSize];
|
|
6174
5870
|
const BFL_IMAGE_SIZES = ["1024x768"];
|
|
6175
5871
|
const OPENAI_IMAGE_SIZES = [...OPENAI_GPT_IMAGE_1_IMAGE_SIZES];
|
|
6176
5872
|
const ALL_IMAGE_SIZES = [
|
|
@@ -6180,16 +5876,34 @@ const ALL_IMAGE_SIZES = [
|
|
|
6180
5876
|
];
|
|
6181
5877
|
z$1.enum(OPENAI_IMAGE_SIZES);
|
|
6182
5878
|
const ImageSizeSchema = z$1.union([z$1.enum(ALL_IMAGE_SIZES), z$1.string().regex(/^\d+x\d+$/, { error: "Size must be in format 'widthxheight'" })]);
|
|
6183
|
-
const
|
|
5879
|
+
const OPENAI_IMAGE_QUALITIES = [
|
|
6184
5880
|
"standard",
|
|
6185
5881
|
"hd",
|
|
6186
5882
|
"low",
|
|
6187
5883
|
"medium",
|
|
6188
5884
|
"high",
|
|
6189
5885
|
"auto"
|
|
6190
|
-
]
|
|
5886
|
+
];
|
|
5887
|
+
const OpenAIImageQualitySchema = z$1.enum(OPENAI_IMAGE_QUALITIES);
|
|
5888
|
+
OPENAI_IMAGE_QUALITIES.filter((quality) => quality !== "auto");
|
|
6191
5889
|
const OpenAIImageStyleSchema = z$1.enum(["vivid", "natural"]);
|
|
6192
5890
|
/**
|
|
5891
|
+
* Alpha handling for gpt-image renders. `transparent` only yields real alpha when the
|
|
5892
|
+
* output format also carries an alpha channel (png or webp) - OpenAI rejects it alongside
|
|
5893
|
+
* jpeg. Ignored by every other provider (see OpenAIImageService.generate).
|
|
5894
|
+
*/
|
|
5895
|
+
const OpenAIImageBackgroundSchema = z$1.enum([
|
|
5896
|
+
"transparent",
|
|
5897
|
+
"opaque",
|
|
5898
|
+
"auto"
|
|
5899
|
+
]);
|
|
5900
|
+
/** Container for a generated image. `webp` is gpt-image only; BFL and Gemini take png/jpeg. */
|
|
5901
|
+
const ImageOutputFormatSchema = z$1.enum([
|
|
5902
|
+
"png",
|
|
5903
|
+
"jpeg",
|
|
5904
|
+
"webp"
|
|
5905
|
+
]);
|
|
5906
|
+
/**
|
|
6193
5907
|
* Maps legacy/removed image model IDs to their current replacements.
|
|
6194
5908
|
* Prevents Zod validation failures when clients send stale persisted model names.
|
|
6195
5909
|
*
|
|
@@ -6232,6 +5946,7 @@ const OpenAIImageGenerationInput = z$1.object({
|
|
|
6232
5946
|
response_format: z$1.enum(["b64_json", "url"]).optional(),
|
|
6233
5947
|
size: ImageSizeSchema.nullable().optional(),
|
|
6234
5948
|
style: OpenAIImageStyleSchema.optional(),
|
|
5949
|
+
background: OpenAIImageBackgroundSchema.nullable().optional(),
|
|
6235
5950
|
width: z$1.number().optional(),
|
|
6236
5951
|
height: z$1.number().optional(),
|
|
6237
5952
|
aspect_ratio: z$1.string().optional()
|
|
@@ -6347,17 +6062,30 @@ const SessionTagSchema = z$1.object({
|
|
|
6347
6062
|
name: z$1.string(),
|
|
6348
6063
|
strength: z$1.number()
|
|
6349
6064
|
});
|
|
6350
|
-
|
|
6065
|
+
/**
|
|
6066
|
+
* Request schema for PUT /api/sessions/{id}. This is the exact field allowlist
|
|
6067
|
+
* sessionService.updateSession enforces (b4m-core/services/src/sessionService/update.ts,
|
|
6068
|
+
* which extends this schema with `id`) - shared so the public contract can never
|
|
6069
|
+
* document a field the service silently drops, or vice versa.
|
|
6070
|
+
*/
|
|
6071
|
+
const SessionUpdateRequestSchema = z$1.object({
|
|
6351
6072
|
name: z$1.string().min(1).optional(),
|
|
6352
6073
|
knowledgeIds: z$1.array(z$1.string()).optional(),
|
|
6353
6074
|
artifactIds: z$1.array(z$1.string()).optional(),
|
|
6354
6075
|
tags: z$1.array(SessionTagSchema).optional(),
|
|
6355
6076
|
lastUsedModel: z$1.string().min(1).nullish().describe("Pin a specific model id, or omit/send null to leave the current pin unchanged. Sending null does NOT clear it."),
|
|
6356
6077
|
forceKnowledgeRetrieval: z$1.boolean().optional(),
|
|
6078
|
+
lakeScope: z$1.array(z$1.string()).nullable().optional().describe("The data lakes this session grounds on, as lake tags (the `datalakeTag` of each lake from GET /api/data-lakes). Send a list to ground only on those lakes, `[]` to ground on no lake at all, or `null` to clear the choice so retrieval falls back to every lake you can reach. Omit to leave the current choice unchanged. Tags naming a lake you cannot reach are ignored at retrieval time rather than rejected here. Narrowing the scope does not by itself turn retrieval on: pair it with `forceKnowledgeRetrieval: true` for a session that is not already grounded. Conversely `[]` leaves a grounded session nothing to retrieve from, so its forced retrieval is skipped rather than run against every lake."),
|
|
6357
6079
|
propagateToProjects: z$1.boolean().optional().describe("Defaults to true when omitted. When knowledgeIds grows, the newly-added file ids are also appended to every project that contains this session, granting every member of that project access to those files. This propagation is append-only and cannot be undone through the UI - pass false if newly-attached files should not be shared with the project.")
|
|
6358
6080
|
});
|
|
6359
|
-
|
|
6360
|
-
z$1.object({
|
|
6081
|
+
/** Path parameter for session-scoped endpoints, e.g. GET/PUT /api/sessions/{id}. */
|
|
6082
|
+
const SessionIdParamSchema = z$1.object({ id: z$1.string().min(1) });
|
|
6083
|
+
/**
|
|
6084
|
+
* Practical response subset for PUT /api/sessions/{id} - the fields a caller needs to
|
|
6085
|
+
* confirm an update took effect. ISession (types/entities/SessionTypes.ts) carries many
|
|
6086
|
+
* more server-internal fields not documented as public API surface here.
|
|
6087
|
+
*/
|
|
6088
|
+
const SessionResponseSchema = z$1.object({
|
|
6361
6089
|
id: z$1.string(),
|
|
6362
6090
|
name: z$1.string(),
|
|
6363
6091
|
userId: z$1.string(),
|
|
@@ -6365,6 +6093,8 @@ z$1.object({
|
|
|
6365
6093
|
artifactIds: z$1.array(z$1.string()).optional(),
|
|
6366
6094
|
tags: z$1.array(SessionTagSchema).optional(),
|
|
6367
6095
|
forceKnowledgeRetrieval: z$1.boolean().optional(),
|
|
6096
|
+
retrievalTags: z$1.array(z$1.string()).optional(),
|
|
6097
|
+
lakeScopeExplicit: z$1.boolean().optional().describe("True when `retrievalTags` is a deliberate choice, so an empty list means \"no lake\" rather than \"any\"."),
|
|
6368
6098
|
lastUsedModel: z$1.string().nullish(),
|
|
6369
6099
|
firstCreated: z$1.date(),
|
|
6370
6100
|
lastUpdated: z$1.date()
|
|
@@ -6484,15 +6214,6 @@ const CHUNK_STALL_REASONS = [
|
|
|
6484
6214
|
"unchunkedPaused"
|
|
6485
6215
|
];
|
|
6486
6216
|
/**
|
|
6487
|
-
* Whether a file is stalled by the convergence kill switch, by any arm. THE predicate every
|
|
6488
|
-
* reader uses, so adding a stall reason reaches health, convergence and retrieval without separate
|
|
6489
|
-
* comparisons drifting apart. Also the in-memory mirror of a Mongo
|
|
6490
|
-
* `chunkStallReason: { $in: [...CHUNK_STALL_REASONS] }`.
|
|
6491
|
-
*/
|
|
6492
|
-
function isChunkStalled(reason) {
|
|
6493
|
-
return CHUNK_STALL_REASONS.includes(reason);
|
|
6494
|
-
}
|
|
6495
|
-
/**
|
|
6496
6217
|
* Which reasons leave the file with NO passages, as opposed to passages with no vectors. A `Record`
|
|
6497
6218
|
* over every reason rather than a hand-written subset array: a new stall reason then cannot compile
|
|
6498
6219
|
* until it is classified, where a member missing from a literal array would just make a health count
|
|
@@ -6519,76 +6240,7 @@ const CHUNK_STALL_NOTICES = {
|
|
|
6519
6240
|
};
|
|
6520
6241
|
CHUNK_STALL_NOTICES.vectorizePaused;
|
|
6521
6242
|
CHUNK_STALL_NOTICES.rechunkPaused;
|
|
6522
|
-
|
|
6523
|
-
* TRANSITIONAL, and the ONE stall predicate every RETRIEVAL path must use until #2016's migration
|
|
6524
|
-
* has run in every environment. Reads the new field, then falls back to the legacy prose that the
|
|
6525
|
-
* pre-migration rows still carry in `notes`.
|
|
6526
|
-
*
|
|
6527
|
-
* It exists for the FORWARD window only: `migratorInvocation` is a `dependsOn` of the web stack
|
|
6528
|
-
* only (infra/web.ts); the queue stack has none, so the executor can serve forced retrieval and
|
|
6529
|
-
* `knowledge_base_search` while rows still carry the marker in `notes` and no `chunkStallReason`. A
|
|
6530
|
-
* row stalled by the chunk arm then reads as a plain unindexed file: `isRetrievalExcluded` drops it
|
|
6531
|
-
* upstream of the withhold on a vectorizedOnly lake, and `partitionByIndexAvailability` calls it
|
|
6532
|
-
* servable everywhere else. The turn answers around a passage-less file and reports FULL coverage -
|
|
6533
|
-
* the silent degradation this whole path exists to prevent.
|
|
6534
|
-
*
|
|
6535
|
-
* A code ROLLBACK is the mirror image and this arm CANNOT cover it: the rows are already migrated
|
|
6536
|
-
* (`chunkStallReason` set, `notes` unset) and the code restored is pre-#2016, which does not contain
|
|
6537
|
-
* this function. Nothing reverts the data on its own either - `migratorInvocation` only ever runs
|
|
6538
|
-
* `up` and `migrate down` is a manual CLI step - so `migrate down` is a REQUIRED step of any
|
|
6539
|
-
* rollback past #2016, not an optional tidy-up. What this arm does buy is that `down()` is safe to
|
|
6540
|
-
* run FIRST: whichever stack is still new keeps honoring the prose it restores, so a staggered
|
|
6541
|
-
* rollback has no window where a restored marker is invisible. `down()` is a PARTIAL restore
|
|
6542
|
-
* though - it skips a row whose owner typed a note after `up()`, and that row grades as unstalled
|
|
6543
|
-
* on both stacks once the field is dropped. See its own comment.
|
|
6544
|
-
*
|
|
6545
|
-
* Deliberately NOT used by the grading/health/UI readers: they are gated behind the web stack, and
|
|
6546
|
-
* a legacy row there renders the notice line AND the identical text as the owner's note.
|
|
6547
|
-
*
|
|
6548
|
-
* Mirrored in Mongo by `buildFabFileSearchQuery`'s `vectorizedOnly` exemption. Delete the legacy arm
|
|
6549
|
-
* from both together, one release after the migration has landed everywhere.
|
|
6550
|
-
*
|
|
6551
|
-
* Pinned to the two reasons the migration backfilled rather than every notice: `unchunkedPaused`
|
|
6552
|
-
* postdates it, so no row carries its prose, and including it would read an owner who happens to type
|
|
6553
|
-
* that sentence into `notes` as stalled.
|
|
6554
|
-
*/
|
|
6555
|
-
const LEGACY_CHUNK_STALL_NOTES = [CHUNK_STALL_NOTICES.vectorizePaused, CHUNK_STALL_NOTICES.rechunkPaused];
|
|
6556
|
-
function isChunkStalledFile(file) {
|
|
6557
|
-
return isChunkStalled(file.chunkStallReason) || LEGACY_CHUNK_STALL_NOTES.includes(file.notes ?? "");
|
|
6558
|
-
}
|
|
6559
|
-
/**
|
|
6560
|
-
* `FabFile.chunkRebuildRequestedAt`: stamped by `resetChunkStateByIds` in the SAME write that
|
|
6561
|
-
* clears a file's chunk rollups, so "this file's passages are being rebuilt" can never be lost the
|
|
6562
|
-
* way the pair of steps that creates the state can be. The reset and the queue send are two
|
|
6563
|
-
* operations - kill the producer between them, or lose the consumer's marker write, and the file
|
|
6564
|
-
* sits at `chunkCount: 0` with `error: null` and no stall reason, a shape indistinguishable from an
|
|
6565
|
-
* image or a still-uploading row. It then drops out of lake health's denominator, out of the
|
|
6566
|
-
* convergence plan and out of the retrieval withhold at the same moment: every rollup says its
|
|
6567
|
-
* passages are gone, and nothing reports it.
|
|
6568
|
-
*
|
|
6569
|
-
* Deliberately NOT the `rechunkPaused` stall reason pre-written by the producer, which is the obvious
|
|
6570
|
-
* fix and the wrong one: that marker means "halted, needs an administrator", so a file awaiting an
|
|
6571
|
-
* ORDINARY rebuild would read to every reader as permanently paused for the whole rebuild - search
|
|
6572
|
-
* would tell readers it does not return on its own, health would hard-fail P3, and "Rebuild
|
|
6573
|
-
* passages" would offer to repair a file that is already repairing. A flag that cries wolf on the
|
|
6574
|
-
* normal path is worse than the rare window it closes.
|
|
6575
|
-
*
|
|
6576
|
-
* So the two facts are distinct states, and the consumer UPGRADES one to the other: pending means
|
|
6577
|
-
* "in flight, returns on its own", the paused note means "halted, needs intervention". A LOST
|
|
6578
|
-
* upgrade therefore degrades to mislabelled-but-visible rather than invisible, which is the trade
|
|
6579
|
-
* this field exists to make - invisibility is the real harm, labelling is secondary.
|
|
6580
|
-
*
|
|
6581
|
-
* A dedicated field on purpose, and the precedent #2016 followed for the other two machine-written
|
|
6582
|
-
* facts: while they all shared `notes` every writer of that field clobbered the others, including
|
|
6583
|
-
* the user's own note.
|
|
6584
|
-
*
|
|
6585
|
-
* Cleared by `commitFabFileChunks` (the rebuild landed) and by the chunk handler's pause write (the
|
|
6586
|
-
* rebuild was halted instead). A file carrying `error` is settled regardless - see
|
|
6587
|
-
* `isMemberIndexingInFlight`, which is where the precedence between these three lives.
|
|
6588
|
-
*/
|
|
6589
|
-
function isChunkRebuildPending(requestedAt) {
|
|
6590
|
-
return requestedAt !== null && requestedAt !== void 0 && requestedAt !== "";
|
|
6591
|
-
}
|
|
6243
|
+
CHUNK_STALL_NOTICES.vectorizePaused, CHUNK_STALL_NOTICES.rechunkPaused;
|
|
6592
6244
|
/** Ceiling so "adjustable" cannot mean "unbounded" in either direction. */
|
|
6593
6245
|
const LAKE_ACCESS_AUDIT_RETENTION_MAX_DAYS = 2555;
|
|
6594
6246
|
/**
|
|
@@ -6683,46 +6335,7 @@ function defaultEmbeddingModelForEnv() {
|
|
|
6683
6335
|
const hasOllama = !!process.env.OLLAMA_BASE_URL?.trim();
|
|
6684
6336
|
const hasCloudEmbeddingKey = !isPlaceholderApiKey(process.env.OPENAI_API_KEY) || !isPlaceholderApiKey(process.env.VOYAGE_API_KEY);
|
|
6685
6337
|
if (selfHost && hasOllama && !hasCloudEmbeddingKey) return "qwen3-embedding:0.6b";
|
|
6686
|
-
return "text-embedding-
|
|
6687
|
-
}
|
|
6688
|
-
/**
|
|
6689
|
-
* True when this deployment can embed with no provider API key at all: a cloud stage reaches
|
|
6690
|
-
* Bedrock through its task/execution role's AWS credentials.
|
|
6691
|
-
*
|
|
6692
|
-
* Requires POSITIVE evidence of an execution role rather than merely "not self-host". A plain
|
|
6693
|
-
* `next dev` session and a CI job both leave B4M_SELF_HOST unset while holding no AWS credentials
|
|
6694
|
-
* at all, so an absence test would send them to the Bedrock SDK for an opaque `CredentialsProvider
|
|
6695
|
-
* Error` in place of the actionable OPENAI_KEY_MISSING_MESSAGE naming the key to set - the same
|
|
6696
|
-
* actionable-to-opaque trade this fallback exists to avoid, just in a different keyless place.
|
|
6697
|
-
*
|
|
6698
|
-
* BOTH runtimes must be covered, and they carry different markers. The Lambdas (vectorize
|
|
6699
|
-
* subscriber, the crons, the Next API routes) get AWS_LAMBDA_FUNCTION_NAME; ChatCompletion is a
|
|
6700
|
-
* Fargate service (infra/chatCompletion.ts) and gets the ECS task-role URI instead. Since
|
|
6701
|
-
* knowledgeBaseSearch runs inside that container, keying on the Lambda marker alone would leave
|
|
6702
|
-
* chat knowledge-base search failing on exactly the keyless stages this fallback is for.
|
|
6703
|
-
*
|
|
6704
|
-
* SST_RESOURCE_App is the third arm and the one this repo can prove: SST sets it on anything it
|
|
6705
|
-
* links, Lambda and Service alike (infra/chatCompletion.ts:129 and infra/agentExecutor.ts:54 both
|
|
6706
|
-
* note that linking alone exposes SST_RESOURCE_*). It covers the Fargate task whether or not the
|
|
6707
|
-
* ECS credential URI is present, and it is absent from a plain `next dev` and from CI, which is
|
|
6708
|
-
* the case that matters. `sst dev` does set it - correctly, since that session runs against real
|
|
6709
|
-
* AWS credentials.
|
|
6710
|
-
*
|
|
6711
|
-
* Self-host is excluded outright because it has no such role - its keyless path is the local
|
|
6712
|
-
* Ollama embedder (`isLocalEmbedderAvailable` in toolAvailability.ts), not Bedrock.
|
|
6713
|
-
*
|
|
6714
|
-
* Answers "is Bedrock reachable here", NOT "should we use it" - a keyed stage is keyless-capable
|
|
6715
|
-
* too, so this must only ever be asked ALONGSIDE a resolved credential table that came back empty.
|
|
6716
|
-
* `resolveEmbeddingWithKeylessFallback` is where the two questions are paired for callers free to
|
|
6717
|
-
* choose the model, and is what such a caller should use instead of asking this directly. The
|
|
6718
|
-
* direct callers are the ones that additionally need the answer BEFORE resolving, to decide
|
|
6719
|
-
* policy: toolAvailability reports whether embedding-backed tools are usable at all, and
|
|
6720
|
-
* data-lakes/semantic-search decides whether a substitution is permitted for this request before
|
|
6721
|
-
* it knows whether one is needed. Both still pair it with the table.
|
|
6722
|
-
*/
|
|
6723
|
-
function hasKeylessCloudEmbedder() {
|
|
6724
|
-
if (process.env.B4M_SELF_HOST === "true") return false;
|
|
6725
|
-
return !!(process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI || process.env.SST_RESOURCE_App);
|
|
6338
|
+
return "text-embedding-3-small";
|
|
6726
6339
|
}
|
|
6727
6340
|
z$1.union([
|
|
6728
6341
|
z$1.enum(OpenAIEmbeddingModel),
|
|
@@ -6732,15 +6345,15 @@ z$1.union([
|
|
|
6732
6345
|
]);
|
|
6733
6346
|
/**
|
|
6734
6347
|
* The measured per-space floors, rendered for an admin-facing description (e.g. "75 for
|
|
6735
|
-
* text-embedding-ada-002,
|
|
6348
|
+
* text-embedding-ada-002, 58 for text-embedding-3-small").
|
|
6736
6349
|
*
|
|
6737
|
-
* Rendered rather than written out in prose because these numbers
|
|
6738
|
-
*
|
|
6739
|
-
*
|
|
6350
|
+
* Rendered rather than written out in prose because these numbers move as each space is re-measured
|
|
6351
|
+
* against a production lake, and a description that restates the table is a wrong number shown to
|
|
6352
|
+
* operators the moment it drifts, with nothing failing.
|
|
6740
6353
|
*/
|
|
6741
6354
|
const forcedRetrievalFloorsBySpaceSummary = Object.entries({
|
|
6742
6355
|
["text-embedding-ada-002"]: 75,
|
|
6743
|
-
["text-embedding-3-small"]:
|
|
6356
|
+
["text-embedding-3-small"]: 58
|
|
6744
6357
|
}).map(([space, pct]) => `${pct} for ${space}`).join(", ");
|
|
6745
6358
|
/**
|
|
6746
6359
|
* Default text for the artifact-emission system prompt. Single source of truth used BOTH as the
|
|
@@ -6915,20 +6528,6 @@ Call \`search_knowledge_base\` BEFORE answering when that library would settle t
|
|
|
6915
6528
|
Do not search when the answer is already in front of you or out of scope: general knowledge (definitions, mathematics, established theory, public facts); anything answerable from this conversation, from an attached document, or from content already retrieved for you this turn; or a request to transform, summarize or reformat text the user has just supplied. If the library has already been searched on this turn, do not search it again for the same question - a repeat spends a round trip to return the same passages.
|
|
6916
6529
|
|
|
6917
6530
|
When a search does not turn up what was asked for, say so plainly rather than filling the gap from training data, and never imply an answer came from the user's documents when it did not.`;
|
|
6918
|
-
/**
|
|
6919
|
-
* Default text for the formatting system message. Runtime fallback used by
|
|
6920
|
-
* `includeHardcodedSystemMessage` (b4m-core/utils/src/llm/utils.ts) when the `FormatPromptTemplate`
|
|
6921
|
-
* admin setting is blank; that setting's own default is intentionally '' - keep this the sole home.
|
|
6922
|
-
*
|
|
6923
|
-
* Deliberately scoped to formatting ONLY. The previous wording ("Adhere to specific formatting
|
|
6924
|
-
* requests...") read as a general compliance instruction and bled into WHETHER to answer: as the
|
|
6925
|
-
* only system content it roughly halved refusal quality. The opening clause is the fix - it fences
|
|
6926
|
-
* this message off from the answer/abstain decision. Injected only when `UseFormatPrompt` is on.
|
|
6927
|
-
*
|
|
6928
|
-
* NOTE: a stored settings row pins its own wording, so changing this default does not reach an
|
|
6929
|
-
* existing deployment that has already saved a value - the row must be edited in admin settings too.
|
|
6930
|
-
*/
|
|
6931
|
-
const FORMAT_PROMPT_TEMPLATE = `Formatting only - nothing here decides whether or how fully to answer. Format replies to maintain the integrity of the requested style; default to markdown for text. Preserve proper structure for poems, songs, or haikus. When the user specifies an output format (e.g. TypeScript), use that format for the parts you do answer.`;
|
|
6932
6531
|
z$1.enum([
|
|
6933
6532
|
"openaiDemoKey",
|
|
6934
6533
|
"anthropicDemoKey",
|
|
@@ -6972,13 +6571,16 @@ z$1.enum([
|
|
|
6972
6571
|
"EnableDataLakeSlackAdd",
|
|
6973
6572
|
"EnableDataLakeGroundingMode",
|
|
6974
6573
|
"EnableLakeMemory",
|
|
6574
|
+
"EnableLakeModelInconsistencyDetection",
|
|
6975
6575
|
"EnableDataLakeVectorSearch",
|
|
6976
6576
|
"EnableRetrievalSupersessionCollapse",
|
|
6977
6577
|
"PauseLakeConvergence",
|
|
6978
6578
|
"LakeConvergenceBulkChangeSharePct",
|
|
6979
6579
|
"EnforceLakeReadGrants",
|
|
6980
6580
|
"EnableDataLakeDrivePoll",
|
|
6581
|
+
"EnableDataLakeGitHub",
|
|
6981
6582
|
"EnforceLakeAdmission",
|
|
6583
|
+
"EnforceLakeOriginOnIngest",
|
|
6982
6584
|
"EnableBriefcase",
|
|
6983
6585
|
"EnableBriefcaseDefault",
|
|
6984
6586
|
"EnableImageTemplates",
|
|
@@ -7078,6 +6680,7 @@ z$1.enum([
|
|
|
7078
6680
|
"kbSearchMinRelevancePct",
|
|
7079
6681
|
"forcedRetrievalRelativeFloorPct",
|
|
7080
6682
|
"forcedRetrievalMinSimilarityPct",
|
|
6683
|
+
"forcedRetrievalSpreadFloorPct",
|
|
7081
6684
|
"dataLakeEmbeddingSpendEnabled",
|
|
7082
6685
|
"dataLakeEmbeddingBudgetPerRunUsd",
|
|
7083
6686
|
"dataLakeEmbeddingBudgetPerLakeUsd",
|
|
@@ -7183,7 +6786,13 @@ const IntentClassifierConfigSchema = z$1.object({
|
|
|
7183
6786
|
fallbackModels: z$1.array(z$1.string()).default(["gemini-2.5-flash-lite", "gpt-5.4-nano"])
|
|
7184
6787
|
});
|
|
7185
6788
|
const OrchestrationDefaultsSchema = z$1.object({
|
|
7186
|
-
/**
|
|
6789
|
+
/**
|
|
6790
|
+
* Tool names the synthetic profile is allowed to invoke. A DEFAULT toolbelt, not a gate:
|
|
6791
|
+
* an agentless chat dispatch ships the user's ambient Smart Tools and the executor UNIONS
|
|
6792
|
+
* them onto this list (`pickEffectiveEnabledTools`), so narrowing this narrows what the
|
|
6793
|
+
* agent brings of its own rather than capping what the user may select. `deniedTools` below
|
|
6794
|
+
* is the gate.
|
|
6795
|
+
*/
|
|
7187
6796
|
allowedTools: z$1.array(z$1.string()).default([
|
|
7188
6797
|
"web_search",
|
|
7189
6798
|
"retrieve_knowledge_content",
|
|
@@ -7315,7 +6924,7 @@ function makeNumberSetting(config) {
|
|
|
7315
6924
|
return {
|
|
7316
6925
|
...config,
|
|
7317
6926
|
type: "number",
|
|
7318
|
-
schema: numberSchema.prefault(config.defaultValue ?? 0)
|
|
6927
|
+
schema: z$1.preprocess((val) => val === null || typeof val === "string" && val.trim() === "" ? void 0 : val, numberSchema.prefault(config.defaultValue ?? 0))
|
|
7319
6928
|
};
|
|
7320
6929
|
}
|
|
7321
6930
|
function makeBooleanSetting(config) {
|
|
@@ -7392,6 +7001,17 @@ const RapidReplySettingsSchema = z$1.object({
|
|
|
7392
7001
|
lastUpdated: /* @__PURE__ */ new Date()
|
|
7393
7002
|
})
|
|
7394
7003
|
});
|
|
7004
|
+
/**
|
|
7005
|
+
* Canonical repository and branch the What's New generator reads from.
|
|
7006
|
+
*
|
|
7007
|
+
* Every default in the What's New path (zod schema, settings registry, server
|
|
7008
|
+
* config service, cron/backfill fallbacks, admin form seed) must resolve here.
|
|
7009
|
+
* A stale slug is invisible in production: GitHubService.listMergedPullRequests
|
|
7010
|
+
* returns [] for a repository outside the connection allowlist, so generation
|
|
7011
|
+
* records "no PRs today" instead of an error and the surface silently goes dark.
|
|
7012
|
+
*/
|
|
7013
|
+
const WHATS_NEW_DEFAULT_REPOSITORY = "Bike4Mind/bike4mind";
|
|
7014
|
+
const WHATS_NEW_DEFAULT_TARGET_BRANCH = "prod";
|
|
7395
7015
|
const L = {
|
|
7396
7016
|
temperature: {
|
|
7397
7017
|
min: 0,
|
|
@@ -7495,8 +7115,8 @@ const WhatsNewConfigSchema = z$1.object({
|
|
|
7495
7115
|
maxCommitMessageLength: z$1.number().min(L.maxCommitMessageLength.min).max(L.maxCommitMessageLength.max).default(L.maxCommitMessageLength.default),
|
|
7496
7116
|
maxPRBodyLength: z$1.number().min(L.maxPRBodyLength.min).max(L.maxPRBodyLength.max).default(L.maxPRBodyLength.default),
|
|
7497
7117
|
maxChangelogLength: z$1.number().min(L.maxChangelogLength.min).max(L.maxChangelogLength.max).default(L.maxChangelogLength.default),
|
|
7498
|
-
repository: z$1.string().regex(/^[\w.-]+\/[\w.-]+$/, "Must be in owner/repo format (e.g., MyOrg/my-repo)").default(
|
|
7499
|
-
targetBranch: z$1.string().regex(/^[\w./-]+$/, "Must be a valid branch name").default(
|
|
7118
|
+
repository: z$1.string().regex(/^[\w.-]+\/[\w.-]+$/, "Must be in owner/repo format (e.g., MyOrg/my-repo)").default(WHATS_NEW_DEFAULT_REPOSITORY),
|
|
7119
|
+
targetBranch: z$1.string().regex(/^[\w./-]+$/, "Must be a valid branch name").default(WHATS_NEW_DEFAULT_TARGET_BRANCH),
|
|
7500
7120
|
promptTemplate: z$1.string().min(L.promptTemplate.min, `Prompt template must be at least ${L.promptTemplate.min} characters`).max(L.promptTemplate.max, `Prompt template cannot exceed ${L.promptTemplate.max.toLocaleString()} characters`).trim().optional()
|
|
7501
7121
|
});
|
|
7502
7122
|
const WhatsNewSyncConfigSchema = z$1.object({
|
|
@@ -7934,8 +7554,12 @@ const API_SERVICE_GROUPS = {
|
|
|
7934
7554
|
order: 10
|
|
7935
7555
|
},
|
|
7936
7556
|
{
|
|
7937
|
-
key: "
|
|
7557
|
+
key: "forcedRetrievalSpreadFloorPct",
|
|
7938
7558
|
order: 11
|
|
7559
|
+
},
|
|
7560
|
+
{
|
|
7561
|
+
key: "dataLakeSearchMaxChunksPerFile",
|
|
7562
|
+
order: 12
|
|
7939
7563
|
}
|
|
7940
7564
|
]
|
|
7941
7565
|
},
|
|
@@ -8929,6 +8553,16 @@ const settingsMap = {
|
|
|
8929
8553
|
order: 91,
|
|
8930
8554
|
dependsOn: "EnableDataLakes"
|
|
8931
8555
|
}),
|
|
8556
|
+
EnableLakeModelInconsistencyDetection: makeBooleanSetting({
|
|
8557
|
+
key: "EnableLakeModelInconsistencyDetection",
|
|
8558
|
+
name: "Data Lakes: Model-driven contradiction pass",
|
|
8559
|
+
defaultValue: false,
|
|
8560
|
+
description: "Gate for the model-driven reading pass (#3057) that finds cross-document contradictions the lexical pattern rules cannot - two documents stating incompatible things in ordinary prose. Off by default: unlike the free lexical pass, this reads corpus content through an LLM, so it costs real money per run. Findings land in the same durable findings collection (detector: 'model') as the lexical pass, triggered the same way (POST /api/data-lakes/:id/inconsistencies?detector=model), gated separately here and rate-limited far lower per caller. Detect only - see the guardrail on corpusInconsistency.ts.",
|
|
8561
|
+
category: "Experimental",
|
|
8562
|
+
group: API_SERVICE_GROUPS.EXPERIMENTAL.id,
|
|
8563
|
+
order: 98,
|
|
8564
|
+
dependsOn: "EnableDataLakes"
|
|
8565
|
+
}),
|
|
8932
8566
|
EnableDataLakeVectorSearch: makeBooleanSetting({
|
|
8933
8567
|
key: "EnableDataLakeVectorSearch",
|
|
8934
8568
|
name: "Data Lakes: Use Atlas $vectorSearch",
|
|
@@ -9000,6 +8634,16 @@ const settingsMap = {
|
|
|
9000
8634
|
order: 95,
|
|
9001
8635
|
dependsOn: "EnableDataLakes"
|
|
9002
8636
|
}),
|
|
8637
|
+
EnableDataLakeGitHub: makeBooleanSetting({
|
|
8638
|
+
key: "EnableDataLakeGitHub",
|
|
8639
|
+
name: "Data Lakes: GitHub repository source",
|
|
8640
|
+
defaultValue: false,
|
|
8641
|
+
description: "Server-side gate for connecting a GitHub repository to a data lake through the read-only GitHub App (contents:read + metadata:read on the one selected repository). Off by default while the connect, ingest and purge pieces land dark; every GitHub lake route answers 403 until it is on.",
|
|
8642
|
+
category: "Experimental",
|
|
8643
|
+
group: API_SERVICE_GROUPS.EXPERIMENTAL.id,
|
|
8644
|
+
order: 96,
|
|
8645
|
+
dependsOn: "EnableDataLakes"
|
|
8646
|
+
}),
|
|
9003
8647
|
EnforceLakeAdmission: makeBooleanSetting({
|
|
9004
8648
|
key: "EnforceLakeAdmission",
|
|
9005
8649
|
name: "Data Lakes: Enforce the admission contract",
|
|
@@ -9015,6 +8659,21 @@ const settingsMap = {
|
|
|
9015
8659
|
"lake"
|
|
9016
8660
|
] }
|
|
9017
8661
|
}),
|
|
8662
|
+
EnforceLakeOriginOnIngest: makeBooleanSetting({
|
|
8663
|
+
key: "EnforceLakeOriginOnIngest",
|
|
8664
|
+
name: "Data Lakes: Enforce curated-lake origin on ingest",
|
|
8665
|
+
defaultValue: true,
|
|
8666
|
+
description: "ON by default: unattended ingest (the Drive folder sync) refuses to add content to a lake whose owner declared it curated. OFF makes the refusal advisory and lets the write through. Unlike the admission contract this ships ON, because it refuses on an explicit owner declaration rather than a heuristic, and because the origin backfill marks every lake that currently has a connector as connector-fed - so at rollout this refuses nothing that exists. The lake rung is the one that matters; the org and owner rungs disable it across every lake in that scope at once. A flip is not instantaneous: the settings cache is per-instance, so it applies immediately on the instance that served the change and within ~5 min elsewhere.",
|
|
8667
|
+
category: "Experimental",
|
|
8668
|
+
group: API_SERVICE_GROUPS.EXPERIMENTAL.id,
|
|
8669
|
+
order: 97,
|
|
8670
|
+
dependsOn: "EnableDataLakes",
|
|
8671
|
+
scope: { settableAt: [
|
|
8672
|
+
"organization",
|
|
8673
|
+
"owner",
|
|
8674
|
+
"lake"
|
|
8675
|
+
] }
|
|
8676
|
+
}),
|
|
9018
8677
|
EnableBriefcase: makeBooleanSetting({
|
|
9019
8678
|
key: "EnableBriefcase",
|
|
9020
8679
|
name: "Enable Briefcase",
|
|
@@ -10192,7 +9851,7 @@ const settingsMap = {
|
|
|
10192
9851
|
description: "Most chunks from any ONE source document a data-lake semantic search may return in its top-K. A diversity guard for CONTESTED slots: where several documents answer the question, it stops the best-scoring one from taking slots the others could have filled. It is NOT a fix for severe crowding - the cap redistributes only among the candidates retrieval already returned, so a document that supplies enough of the top-scoring chunks to fill that pool on its own is one the cap cannot change at all. On a corpus of book-length documents, expect enabling this to change little beyond widening the vector-search request. 0 (default) disables the cap, byte-identical to behavior before this setting existed. The cap never SHRINKS a result set - once the spread-out picks are in, any slots still open are backfilled with the highest-scoring chunks the cap held back, so a lake whose only match is one document still returns a full top-K. A value at or above the result count is also a no-op, since nothing can ever be held back. Below it, each retrieval stream's candidate pool is widened to a fixed multiple of the result count so the cap has a spread to choose from. The scanned corpus itself does not grow (that is bounded separately), but the vector-search backends are asked for that many more matches, and a larger in-memory ranking pool costs some CPU. 2-3 is the useful range; 1 serves one passage per document, which suits a corpus of many short documents and starves a question whose answer spans one long one. The chat knowledge-base path ranks more passages than it serves, so it applies the cap a second time at the count it actually serves - otherwise the spread-out picks, which are by definition the lowest-scoring ones admitted, would land in the passages that path discards.",
|
|
10193
9852
|
category: "AI",
|
|
10194
9853
|
group: API_SERVICE_GROUPS.EMBEDDING.id,
|
|
10195
|
-
order:
|
|
9854
|
+
order: 12,
|
|
10196
9855
|
scope: { settableAt: ["organization", "owner"] }
|
|
10197
9856
|
}),
|
|
10198
9857
|
forcedRetrievalCharBudget: makeNumberSetting({
|
|
@@ -10275,12 +9934,25 @@ const settingsMap = {
|
|
|
10275
9934
|
min: 1,
|
|
10276
9935
|
max: 100,
|
|
10277
9936
|
int: true,
|
|
10278
|
-
description: `Absolute minimum cosine similarity, as a percent, a chunk must clear to be injected on a Data-Lake-mode turn. This is a sanity floor for genuinely unrelated content, NOT the ranking gate - the relative floor above does the ranking. LEAVE IT AT 75 UNLESS YOU HAVE MEASURED YOUR OWN CORPUS: a raw cosine means nothing outside the embedding model it was fitted to, so while this reads 75 the server ignores it and applies the floor measured for whichever model your documents are actually embedded with (${forcedRetrievalFloorsBySpaceSummary}, and no absolute floor at all for a model nobody has measured - the relative floor still applies). Set any other value and the server uses exactly that, in every space, which is yours to get right: 75 against text-embedding-3-small sits above that band entirely and returns nothing on every query. Where this floor lands inside your band decides a lot - on one measured corpus 74 / 75 / 76 swung recall 91% / 65% / 40% - and the same 75 that is a cliff on one lake rejects nothing at all on another. Re-measure after changing the embedding model
|
|
9937
|
+
description: `Absolute minimum cosine similarity, as a percent, a chunk must clear to be injected on a Data-Lake-mode turn. This is a sanity floor for genuinely unrelated content, NOT the ranking gate - the relative floor above does the ranking. LEAVE IT AT 75 UNLESS YOU HAVE MEASURED YOUR OWN CORPUS: a raw cosine means nothing outside the embedding model it was fitted to, so while this reads 75 the server ignores it and applies the floor measured for whichever model your documents are actually embedded with (${forcedRetrievalFloorsBySpaceSummary}, and no absolute floor at all for a model nobody has measured - the relative floor still applies). Set any other value and the server uses exactly that, in every space, which is yours to get right: 75 against text-embedding-3-small sits above that band entirely and returns nothing on every query. Where this floor lands inside your band decides a lot - on one measured corpus 74 / 75 / 76 swung recall 91% / 65% / 40% - and the same 75 that is a cliff on one lake rejects nothing at all on another. Re-measure after changing the embedding model.`,
|
|
10279
9938
|
category: "AI",
|
|
10280
9939
|
group: API_SERVICE_GROUPS.EMBEDDING.id,
|
|
10281
9940
|
order: 10,
|
|
10282
9941
|
scope: { settableAt: ["organization", "owner"] }
|
|
10283
9942
|
}),
|
|
9943
|
+
forcedRetrievalSpreadFloorPct: makeNumberSetting({
|
|
9944
|
+
key: "forcedRetrievalSpreadFloorPct",
|
|
9945
|
+
name: "Forced Retrieval Spread Floor (%)",
|
|
9946
|
+
defaultValue: 0,
|
|
9947
|
+
min: 0,
|
|
9948
|
+
max: 100,
|
|
9949
|
+
int: true,
|
|
9950
|
+
description: "How far below the best-scoring passage of the SAME turn a chunk may score and still be injected, as a percent of the gap between that best score and a typical one (this turn's median). 0 (the default) disables it. This is the only one of the three floors whose cut depends on the QUESTION rather than on where the score band sits: a question one document answers sharply leaves its answer far above the median and admits few passages, while a broad question leaves many passages bunched near the top and admits many. The other two floors cannot tell those turns apart, which is why retrieved volume otherwise tracks the character budget instead of the question. Because it is measured in units of the band rather than against a fixed cosine, the same value means the same thing in every embedding space and does not need re-tuning when the embedding model changes. Lower is stricter (10 keeps only passages within a tenth of the way down to the median); 100 cuts at the median itself. It can never empty a turn - the best passage always clears its own cutoff. Ships off because no magnitude has been measured yet; measure one offline before turning it on.",
|
|
9951
|
+
category: "AI",
|
|
9952
|
+
group: API_SERVICE_GROUPS.EMBEDDING.id,
|
|
9953
|
+
order: 11,
|
|
9954
|
+
scope: { settableAt: ["organization", "owner"] }
|
|
9955
|
+
}),
|
|
10284
9956
|
LakeAccessAuditRetentionDays: makeNumberSetting({
|
|
10285
9957
|
key: "LakeAccessAuditRetentionDays",
|
|
10286
9958
|
name: "Lake Access Audit Retention (days)",
|
|
@@ -10708,8 +10380,8 @@ const settingsMap = {
|
|
|
10708
10380
|
maxCommitMessageLength: 200,
|
|
10709
10381
|
maxPRBodyLength: 500,
|
|
10710
10382
|
maxChangelogLength: 1e3,
|
|
10711
|
-
repository:
|
|
10712
|
-
targetBranch:
|
|
10383
|
+
repository: WHATS_NEW_DEFAULT_REPOSITORY,
|
|
10384
|
+
targetBranch: WHATS_NEW_DEFAULT_TARGET_BRANCH
|
|
10713
10385
|
},
|
|
10714
10386
|
description: "Configuration for automated What's New modal generation, including LLM model selection, prompt parameters, validation rules, and content sanitization limits.",
|
|
10715
10387
|
category: "Admin",
|
|
@@ -11281,7 +10953,14 @@ const TokensBySourceSchema = z$1.object({
|
|
|
11281
10953
|
fabFiles: z$1.number(),
|
|
11282
10954
|
urlContent: z$1.number(),
|
|
11283
10955
|
toolSchemas: z$1.number(),
|
|
11284
|
-
userPrompt: z$1.number()
|
|
10956
|
+
userPrompt: z$1.number(),
|
|
10957
|
+
/**
|
|
10958
|
+
* Lake-sourced content injected this turn (forced retrieval + the lake-memory hot card), split out
|
|
10959
|
+
* of `systemPrompts`. Optional and absent means UNKNOWN, never zero - telemetry captured before
|
|
10960
|
+
* this bucket existed has no value to report and must not be read as "no lake content". Mirrors
|
|
10961
|
+
* PromptMetaTokensBySourceSchema in promptMeta.ts.
|
|
10962
|
+
*/
|
|
10963
|
+
lakeRetrieval: z$1.number().optional()
|
|
11285
10964
|
});
|
|
11286
10965
|
z$1.object({
|
|
11287
10966
|
/** gen_ai.usage.input_tokens */
|
|
@@ -11528,6 +11207,7 @@ const PromptMetaModelParametersSchema = z$1.object({
|
|
|
11528
11207
|
prompt_upsampling: z$1.boolean().optional(),
|
|
11529
11208
|
seed: z$1.number().optional(),
|
|
11530
11209
|
output_format: z$1.string().optional(),
|
|
11210
|
+
background: z$1.string().optional(),
|
|
11531
11211
|
response_format: z$1.string().optional(),
|
|
11532
11212
|
seconds: z$1.number().optional(),
|
|
11533
11213
|
model: z$1.string().optional()
|
|
@@ -11535,11 +11215,7 @@ const PromptMetaModelParametersSchema = z$1.object({
|
|
|
11535
11215
|
const PromptMetaModelSchema = z$1.object({
|
|
11536
11216
|
name: z$1.string(),
|
|
11537
11217
|
parameters: PromptMetaModelParametersSchema.optional(),
|
|
11538
|
-
type: z$1.enum(
|
|
11539
|
-
"text",
|
|
11540
|
-
"image",
|
|
11541
|
-
"video"
|
|
11542
|
-
]).optional(),
|
|
11218
|
+
type: z$1.enum(PROMPT_META_MODEL_TYPES).optional(),
|
|
11543
11219
|
backend: z$1.string().optional(),
|
|
11544
11220
|
contextWindow: z$1.number().optional(),
|
|
11545
11221
|
maxTokens: z$1.number().optional(),
|
|
@@ -11593,7 +11269,16 @@ const PromptMetaTokensBySourceSchema = z$1.object({
|
|
|
11593
11269
|
fabFiles: z$1.number(),
|
|
11594
11270
|
urlContent: z$1.number(),
|
|
11595
11271
|
toolSchemas: z$1.number(),
|
|
11596
|
-
userPrompt: z$1.number()
|
|
11272
|
+
userPrompt: z$1.number(),
|
|
11273
|
+
/**
|
|
11274
|
+
* Lake-sourced content injected this turn - forced retrieval (`knowledge_retrieval`) plus the
|
|
11275
|
+
* lake-memory hot card (`lake_memory`) - moved out of the `systemPrompts` residual so the
|
|
11276
|
+
* breakdown can price the lake separately. Optional, and absent means UNKNOWN, never zero: turns
|
|
11277
|
+
* recorded before this field existed carry no value and none can be backfilled, because the only
|
|
11278
|
+
* evidence was the residual this split had not yet made. Same absent-is-unknown rule as
|
|
11279
|
+
* `retrieval.injected` below.
|
|
11280
|
+
*/
|
|
11281
|
+
lakeRetrieval: z$1.number().optional()
|
|
11597
11282
|
});
|
|
11598
11283
|
const PromptMetaContextSchema = z$1.object({
|
|
11599
11284
|
attachedFiles: z$1.array(PromptMetaAttachedFileSchema).optional(),
|
|
@@ -11765,7 +11450,28 @@ const CitableSourceSchema = z$1.object({
|
|
|
11765
11450
|
practiceAreas: z$1.array(z$1.string()).optional(),
|
|
11766
11451
|
chunkId: z$1.string().optional(),
|
|
11767
11452
|
relevanceScore: z$1.number().optional(),
|
|
11768
|
-
fullContext: z$1.string().optional()
|
|
11453
|
+
fullContext: z$1.string().optional(),
|
|
11454
|
+
/** web_search's own thumbnail/image cluster for this source, gated on `withImages`. */
|
|
11455
|
+
thumbnail: z$1.string().optional(),
|
|
11456
|
+
images: z$1.array(z$1.string()).optional(),
|
|
11457
|
+
/**
|
|
11458
|
+
* Ids of the other cited sources this one provably disagrees with (#3041). Declared rather
|
|
11459
|
+
* than left to the loose object, for the same reason chunkId/fullContext are: a writer that
|
|
11460
|
+
* stamps the wrong shape should fail here, not render a badge that silently names nobody.
|
|
11461
|
+
*/
|
|
11462
|
+
conflictsWith: z$1.array(z$1.string()).optional(),
|
|
11463
|
+
/** web_search's provider-located place (WebSearchPlace), the only source of map coordinates. */
|
|
11464
|
+
place: z$1.object({
|
|
11465
|
+
id: z$1.string(),
|
|
11466
|
+
name: z$1.string(),
|
|
11467
|
+
lat: z$1.number(),
|
|
11468
|
+
lng: z$1.number(),
|
|
11469
|
+
rating: z$1.number().optional(),
|
|
11470
|
+
reviews: z$1.number().optional(),
|
|
11471
|
+
category: z$1.string().optional(),
|
|
11472
|
+
address: z$1.string().optional(),
|
|
11473
|
+
thumbnail: z$1.string().optional()
|
|
11474
|
+
}).optional()
|
|
11769
11475
|
}).optional()
|
|
11770
11476
|
});
|
|
11771
11477
|
/**
|
|
@@ -11903,12 +11609,28 @@ const RetrievalSummarySchema = z$1.object({
|
|
|
11903
11609
|
* the per-turn routing question is about, and before this it was indistinguishable from a turn
|
|
11904
11610
|
* where forced retrieval was never configured at all.
|
|
11905
11611
|
*/
|
|
11906
|
-
forcedSkipReason: z$1.enum([
|
|
11612
|
+
forcedSkipReason: z$1.enum([
|
|
11613
|
+
"attached_files",
|
|
11614
|
+
"personal_corpus",
|
|
11615
|
+
"no_lake_scope"
|
|
11616
|
+
]).optional(),
|
|
11907
11617
|
/** Which retrieval-capable surface(s) ran this turn, e.g. 'lake-memory', 'knowledgeBaseSearch'. */
|
|
11908
11618
|
surfaces: z$1.array(z$1.string()),
|
|
11909
11619
|
/** Lakes resolved at the moment retrieval ran, stamped point-in-time (not read live from the session). */
|
|
11910
11620
|
dataLakeTags: z$1.array(z$1.string()),
|
|
11911
11621
|
/**
|
|
11622
|
+
* The subset of `dataLakeTags` that actually put files into the ranked scope. `dataLakeTags`
|
|
11623
|
+
* alone says which lakes were REQUESTED, which read as "searched" while a lake could contribute
|
|
11624
|
+
* nothing - the retrieval budget used to be spent in file order and starved whichever lake
|
|
11625
|
+
* sorted last.
|
|
11626
|
+
*
|
|
11627
|
+
* Optional because attribution is best-effort: a file matched by a lake's prefix/membership arm
|
|
11628
|
+
* can carry no reversible `datalake:` tag (see attributeAccessedLakes), and the producer omits
|
|
11629
|
+
* this rather than reporting an inconclusive scope as "no lake contributed". Absent means
|
|
11630
|
+
* unknown, NOT none.
|
|
11631
|
+
*/
|
|
11632
|
+
dataLakeTagsWithCandidates: z$1.array(z$1.string()).optional(),
|
|
11633
|
+
/**
|
|
11912
11634
|
* The lake scope the turn's retrieval surfaces WOULD have searched, resolved at the seed site
|
|
11913
11635
|
* whether or not any of them ran: the caller's accessible lakes narrowed to the session
|
|
11914
11636
|
* (narrowLakeAccessToSession), or empty where the corpus is personal and the lake arms are
|
|
@@ -11995,16 +11717,25 @@ const RetrievalSummarySchema = z$1.object({
|
|
|
11995
11717
|
* not "what reached the model": `ranked.length` and `scored.length` in KnowledgeRetrievalFeature
|
|
11996
11718
|
* - the candidates left after the absolute similarity floor, and after the relative floor
|
|
11997
11719
|
* trims them. `chunks` is what survived the char budget on top of that, so the three
|
|
11998
|
-
* numbers bracket
|
|
11720
|
+
* numbers bracket three independent trimmers:
|
|
11999
11721
|
*
|
|
12000
|
-
* pre -> [relative floor] -> post -> [char budget] -> chunks
|
|
11722
|
+
* pre -> [relative floor] -> post -> [spread floor] -> postSpread -> [char budget] -> chunks
|
|
12001
11723
|
*
|
|
12002
11724
|
* They exist so a low `chunks` is diagnosable - a small corpus and a floor that trimmed a large
|
|
12003
|
-
* pool end in the same `chunks`. `pre - post` is the floor's own effect and nothing
|
|
11725
|
+
* pool end in the same `chunks`. `pre - post` is the relative floor's own effect and nothing
|
|
11726
|
+
* else, and `post - postSpread` the spread floor's;
|
|
12004
11727
|
* `pre - chunks` is NOT, because the budget trims the same walk. Both optional: only forced
|
|
12005
11728
|
* retrieval computes a ranked pool, a surface without one (lake memory, the knowledge tools)
|
|
12006
|
-
* never writes
|
|
12007
|
-
* the same absent-is-not-zero handling as `topScore`.
|
|
11729
|
+
* never writes any of them, and absence must not read as zero candidates. SUMMED like `chunks`,
|
|
11730
|
+
* with the same absent-is-not-zero handling as `topScore`.
|
|
11731
|
+
*
|
|
11732
|
+
* `backgroundScore` is the median of every score the turn compared, and `postSpreadFloorCandidates`
|
|
11733
|
+
* what is left once the spread floor cuts against it. Recorded even while that floor is OFF (its
|
|
11734
|
+
* shipped default), in which case `postSpread` equals `post` and the pair degenerates to a
|
|
11735
|
+
* diagnostic: `topScore - backgroundScore` is the turn's signal spread, and the distribution of
|
|
11736
|
+
* that quantity over production traffic is what a value for `forcedRetrievalSpreadFloorPct` has to
|
|
11737
|
+
* be chosen from. NOT comparable across embedding spaces as an absolute number, for the same
|
|
11738
|
+
* reason `topScore` is not; the RATIO of the two floors' cuts is.
|
|
12008
11739
|
*
|
|
12009
11740
|
* COMPARE THE PAIR ONLY TO ITSELF, never to `chunks`, unless `surfaces` is forced retrieval
|
|
12010
11741
|
* alone. `chunks` and `chars` sum across ALL surfaces while this pair is forced-only, so a mixed
|
|
@@ -12026,7 +11757,9 @@ const RetrievalSummarySchema = z$1.object({
|
|
|
12026
11757
|
chars: z$1.number(),
|
|
12027
11758
|
topScore: z$1.number().optional(),
|
|
12028
11759
|
preRelativeFloorCandidates: z$1.number().optional(),
|
|
12029
|
-
postRelativeFloorCandidates: z$1.number().optional()
|
|
11760
|
+
postRelativeFloorCandidates: z$1.number().optional(),
|
|
11761
|
+
postSpreadFloorCandidates: z$1.number().optional(),
|
|
11762
|
+
backgroundScore: z$1.number().optional()
|
|
12030
11763
|
}).optional(),
|
|
12031
11764
|
/**
|
|
12032
11765
|
* Could the corpus in scope have answered this turn, whether or not the model went looking?
|
|
@@ -12125,7 +11858,44 @@ const RetrievalSummarySchema = z$1.object({
|
|
|
12125
11858
|
* absence is weaker evidence than presence. Date-bound any rollup: turns predating this field
|
|
12126
11859
|
* carry nothing, and no backfill is possible - a past turn's grant rows have moved on.
|
|
12127
11860
|
*/
|
|
12128
|
-
grantedLakeIdsUsed: z$1.array(z$1.string()).optional()
|
|
11861
|
+
grantedLakeIdsUsed: z$1.array(z$1.string()).optional(),
|
|
11862
|
+
/**
|
|
11863
|
+
* How many lakes were excluded from this turn's scope because the caller lacks the access to
|
|
11864
|
+
* search them, and why (#3055). Resolved at the seed alongside `lakeScope`, from a dedicated
|
|
11865
|
+
* count-only query (see excludedByAccessCount on getDynamicDataLakeAccess - NOT derived from
|
|
11866
|
+
* the candidate set `lakeScope` comes from, which already has the gate enforced datastore-side
|
|
11867
|
+
* and so cannot see this population).
|
|
11868
|
+
*
|
|
11869
|
+
* ABSENT MEANS NOT RECORDED, never "nothing was excluded" - a turn with nothing excluded records
|
|
11870
|
+
* `count: 0` explicitly. Three distinct causes collapse into this one absent state and are not
|
|
11871
|
+
* distinguishable from it: a turn predating this field, a turn whose `retrieval` was written
|
|
11872
|
+
* only by a tool arm rather than by the seed, and the count-only query itself failing or not
|
|
11873
|
+
* being wired on this host (mirrors `lakeViewComplete`'s contract on the access resolver: a
|
|
11874
|
+
* failure must report unknown, never a false zero).
|
|
11875
|
+
*
|
|
11876
|
+
* COUNT AND REASON ONLY, DELIBERATELY. Never a lake id, name, or tag: the caller may not be
|
|
11877
|
+
* permitted to know a given excluded lake exists at all, and this field must stay safe to show
|
|
11878
|
+
* them regardless of which specific lake(s) it is counting. `reason` is a closed enum, not free
|
|
11879
|
+
* text - prose could leak a lake's identity through phrasing - so a future exclusion cause (e.g.
|
|
11880
|
+
* an archived or quota-limited lake) adds an enum value here rather than a description.
|
|
11881
|
+
*
|
|
11882
|
+
* 'access' is the only reason today: the caller's org membership or the lake's public listing
|
|
11883
|
+
* surfaced it as a candidate (they could see it exists) but they hold neither its own
|
|
11884
|
+
* gate/entitlement nor an ownership or grant exception for it.
|
|
11885
|
+
*
|
|
11886
|
+
* A session-preauthorized lake (unionPreauthorizedLakeAccess) that is ALSO gate-dropped from
|
|
11887
|
+
* this account-wide count is corrected, not merely narrow: the seed's targeted measurement
|
|
11888
|
+
* (measureIdentityNamedExclusion, ChatCompletionProcess's promptMeta seed) excludes exactly the
|
|
11889
|
+
* tags this turn successfully admitted via preauthorization before running the gate query, so an
|
|
11890
|
+
* admitted-and-searched lake never reports here as excluded. This account-wide number itself
|
|
11891
|
+
* (excludedByAccessCount on getDynamicDataLakeAccess) is still computed before that union and is
|
|
11892
|
+
* NOT corrected the same way - only the per-turn targeted measurement is, which is what a
|
|
11893
|
+
* preauthorized session's own narrowing always uses (see sessionNamesALake's call site).
|
|
11894
|
+
*/
|
|
11895
|
+
excludedLakes: z$1.object({
|
|
11896
|
+
count: z$1.number().int().nonnegative(),
|
|
11897
|
+
reason: z$1.enum(["access"])
|
|
11898
|
+
}).optional()
|
|
12129
11899
|
});
|
|
12130
11900
|
/**
|
|
12131
11901
|
* Why a grounded turn's library scan stopped short of the whole library.
|
|
@@ -12264,9 +12034,18 @@ z$1.object({
|
|
|
12264
12034
|
schemaVersion: z$1.number().int().positive(),
|
|
12265
12035
|
/** Product identifier: 'vibeswire', 'bike4mind', 'stocksandvibes', 'k2kanji', etc. */
|
|
12266
12036
|
productId: z$1.string().min(1).max(64),
|
|
12267
|
-
/**
|
|
12037
|
+
/**
|
|
12038
|
+
* Product's internal user ID, or OVERWATCH_ANONYMOUS_USER_ID when the event has no
|
|
12039
|
+
* identified user. See the session and identity conventions below.
|
|
12040
|
+
*/
|
|
12268
12041
|
userId: z$1.string().min(1).max(256),
|
|
12269
|
-
/**
|
|
12042
|
+
/**
|
|
12043
|
+
* Visit identifier. Overwatch counts distinct values of this per product as the first
|
|
12044
|
+
* stage of its acquisition funnel, so one value per visit is what makes that count a
|
|
12045
|
+
* count of visits. An emitter that cannot tell which visit a request belongs to sends
|
|
12046
|
+
* OVERWATCH_UNKNOWN_SESSION_ID rather than a value of its own invention - see the
|
|
12047
|
+
* conventions below.
|
|
12048
|
+
*/
|
|
12270
12049
|
sessionId: z$1.string().min(1).max(256),
|
|
12271
12050
|
/** Event type: 'session_start', 'signup', 'feature_used', etc. */
|
|
12272
12051
|
event: z$1.string().min(1).max(128),
|
|
@@ -12283,6 +12062,153 @@ z$1.object({
|
|
|
12283
12062
|
z$1.boolean()
|
|
12284
12063
|
])).refine((v) => JSON.stringify(v).length <= 1024, "metadata must be ≤ 1KB serialized").optional()
|
|
12285
12064
|
});
|
|
12065
|
+
/**
|
|
12066
|
+
* Public wire schema for `GET /api/v1/me` - the caller's own identity and
|
|
12067
|
+
* commercial state, and nothing else.
|
|
12068
|
+
*
|
|
12069
|
+
* Deliberately narrower than the user document `/api/identify` returns: no email,
|
|
12070
|
+
* no tags, no internal flags. Everything here answers one of three questions a
|
|
12071
|
+
* downstream app has to ask before it spends - who is this, what have they paid
|
|
12072
|
+
* for, can they afford the next call.
|
|
12073
|
+
*
|
|
12074
|
+
* Public-API rules apply: snake_case wire fields, no `.catch()`, no top-level
|
|
12075
|
+
* `.transform()`.
|
|
12076
|
+
*/
|
|
12077
|
+
/**
|
|
12078
|
+
* Rung on the B4M plan ladder the caller currently sits on.
|
|
12079
|
+
*
|
|
12080
|
+
* `free` means no active subscription. `other` is still a paying customer: either
|
|
12081
|
+
* an active plan that is not on the ladder (`SubscriptionPlanDetail.tier` is
|
|
12082
|
+
* optional - a standalone product omits it so it stays out of the cross-plan
|
|
12083
|
+
* change flow), or one whose Stripe price the deployment can no longer name, as
|
|
12084
|
+
* happens to a subscriber grandfathered on a superseded price. `subscription` is
|
|
12085
|
+
* `null` in that second case.
|
|
12086
|
+
*
|
|
12087
|
+
* Gate on `tier !== 'free'` for "is this caller paying" and on `subscription.
|
|
12088
|
+
* price_id` for "which product" - NOT on `basic`/`pro`, whose ordinals come from
|
|
12089
|
+
* the internal change-flow ladder and do not track a plan's marketing name (the
|
|
12090
|
+
* Professional plan occupies the `basic` rung today).
|
|
12091
|
+
*/
|
|
12092
|
+
const ME_TIERS = [
|
|
12093
|
+
"free",
|
|
12094
|
+
"basic",
|
|
12095
|
+
"pro",
|
|
12096
|
+
"other"
|
|
12097
|
+
];
|
|
12098
|
+
/** The caller's active subscription, or `null` when they have none the deployment can name. */
|
|
12099
|
+
const MeSubscriptionSchema = z$1.object({
|
|
12100
|
+
plan_name: z$1.string(),
|
|
12101
|
+
price_id: z$1.string(),
|
|
12102
|
+
interval: z$1.enum(["monthly", "yearly"]),
|
|
12103
|
+
/** ISO 8601. When the current billing period ends - not a cancellation date. */
|
|
12104
|
+
current_period_ends_at: z$1.string()
|
|
12105
|
+
});
|
|
12106
|
+
const MeResponseSchema = z$1.object({
|
|
12107
|
+
/** Stable B4M user id. Safe to key an integrator's own records on. */
|
|
12108
|
+
id: z$1.string(),
|
|
12109
|
+
/** Display name. Never the email address. */
|
|
12110
|
+
name: z$1.string(),
|
|
12111
|
+
tier: z$1.enum(ME_TIERS),
|
|
12112
|
+
subscription: MeSubscriptionSchema.nullable(),
|
|
12113
|
+
credits: z$1.object({
|
|
12114
|
+
/**
|
|
12115
|
+
* Spendable credits on the caller's PERSONAL ledger. Organization pools are not
|
|
12116
|
+
* included - a call billed to an organization draws on a balance this number
|
|
12117
|
+
* does not describe.
|
|
12118
|
+
*/
|
|
12119
|
+
balance: z$1.number() }),
|
|
12120
|
+
/** Entitlement keys the caller currently holds, e.g. `base`. */
|
|
12121
|
+
entitlements: z$1.array(z$1.string())
|
|
12122
|
+
});
|
|
12123
|
+
/**
|
|
12124
|
+
* Every tool name in `b4mLLMTools`. Exhaustive by annotation - do not widen the type.
|
|
12125
|
+
*/
|
|
12126
|
+
const CORE_TOOL_SIDE_EFFECTS = {
|
|
12127
|
+
dice_roll: "none",
|
|
12128
|
+
weather_info: "none",
|
|
12129
|
+
web_search: "none",
|
|
12130
|
+
web_fetch: "none",
|
|
12131
|
+
wolfram_alpha: "none",
|
|
12132
|
+
deep_research: "none",
|
|
12133
|
+
math_evaluate: "none",
|
|
12134
|
+
current_datetime: "none",
|
|
12135
|
+
prompt_enhancement: "none",
|
|
12136
|
+
wikipedia_on_this_day: "none",
|
|
12137
|
+
moon_phase: "none",
|
|
12138
|
+
sunrise_sunset: "none",
|
|
12139
|
+
iss_tracker: "none",
|
|
12140
|
+
planet_visibility: "none",
|
|
12141
|
+
search_knowledge_base: "none",
|
|
12142
|
+
retrieve_knowledge_content: "none",
|
|
12143
|
+
count_knowledge_base: "none",
|
|
12144
|
+
describe_knowledge_base: "none",
|
|
12145
|
+
chess_engine: "none",
|
|
12146
|
+
fmp_financial_data: "none",
|
|
12147
|
+
bob_panel_read: "none",
|
|
12148
|
+
skill: "none",
|
|
12149
|
+
recharts: "local",
|
|
12150
|
+
mermaid_chart: "local",
|
|
12151
|
+
navigate_view: "local",
|
|
12152
|
+
generate_jupyter_notebook: "local",
|
|
12153
|
+
optihashi_schedule: "local",
|
|
12154
|
+
optihashi_formulate: "local",
|
|
12155
|
+
optihashi_edit_problem: "local",
|
|
12156
|
+
image_generation: "external",
|
|
12157
|
+
edit_image: "external",
|
|
12158
|
+
music_generation: "external",
|
|
12159
|
+
audio_generation: "external",
|
|
12160
|
+
excel_generation: "external",
|
|
12161
|
+
edit_file: "external",
|
|
12162
|
+
blog_publish: "external",
|
|
12163
|
+
blog_edit: "external",
|
|
12164
|
+
blog_draft: "external",
|
|
12165
|
+
delegate_to_agent: "external"
|
|
12166
|
+
};
|
|
12167
|
+
/**
|
|
12168
|
+
* Tool names that reach the pipeline from outside `b4mLLMTools` - the CLI tool set, the
|
|
12169
|
+
* Slack tool set, and premium-overlay tools supplied at runtime via the `externalTools`
|
|
12170
|
+
* merge. No enum to key off, so these get no compile-time exhaustiveness; an unlisted name
|
|
12171
|
+
* falls through to the gated default, which is the correct failure direction.
|
|
12172
|
+
*/
|
|
12173
|
+
const EXTERNAL_REGISTRY_SIDE_EFFECTS = {
|
|
12174
|
+
file_read: "none",
|
|
12175
|
+
glob_files: "none",
|
|
12176
|
+
grep_search: "none",
|
|
12177
|
+
recent_changes: "none",
|
|
12178
|
+
check_shell_output: "none",
|
|
12179
|
+
list_background_shells: "none",
|
|
12180
|
+
lattice_query: "none",
|
|
12181
|
+
lattice_explain: "none",
|
|
12182
|
+
ask_user_question: "local",
|
|
12183
|
+
create_file: "external",
|
|
12184
|
+
edit_local_file: "external",
|
|
12185
|
+
delete_file: "external",
|
|
12186
|
+
bash_execute: "external",
|
|
12187
|
+
write_shell_stdin: "external",
|
|
12188
|
+
kill_background_shell: "external",
|
|
12189
|
+
lattice_create_model: "external",
|
|
12190
|
+
lattice_add_entity: "external",
|
|
12191
|
+
lattice_set_value: "external",
|
|
12192
|
+
lattice_create_rule: "external",
|
|
12193
|
+
slackbot_help: "none",
|
|
12194
|
+
list_curated_files: "none",
|
|
12195
|
+
notebook_status: "none",
|
|
12196
|
+
share_curated_file: "external",
|
|
12197
|
+
notebook_new: "external",
|
|
12198
|
+
confirm_pending_action: "external",
|
|
12199
|
+
cancel_pending_action: "external",
|
|
12200
|
+
mission_status: "none",
|
|
12201
|
+
optihashi_decompose: "local",
|
|
12202
|
+
optihashi_solve: "local",
|
|
12203
|
+
send_slack_message: "external",
|
|
12204
|
+
coordinate_task: "external",
|
|
12205
|
+
code_execute: "external",
|
|
12206
|
+
video_generation: "external"
|
|
12207
|
+
};
|
|
12208
|
+
({
|
|
12209
|
+
...CORE_TOOL_SIDE_EFFECTS,
|
|
12210
|
+
...EXTERNAL_REGISTRY_SIDE_EFFECTS
|
|
12211
|
+
});
|
|
12286
12212
|
const InternalTeamMemberSchema = z$1.object({
|
|
12287
12213
|
name: z$1.string().min(1, "Name is required"),
|
|
12288
12214
|
phone: z$1.string().min(1, "Phone is required"),
|
|
@@ -12827,22 +12753,17 @@ const DATA_LAKES = [{
|
|
|
12827
12753
|
}
|
|
12828
12754
|
})()];
|
|
12829
12755
|
new Set(DATA_LAKES.map((l) => l.id));
|
|
12830
|
-
/**
|
|
12831
|
-
* Canonical normalization for entitlement keys + `requiredEntitlement` values - the ONE
|
|
12832
|
-
* rule, applied at write time (create/update/stamp) and at match time. Mirrors the
|
|
12833
|
-
* entitlement registry's `normalizeTag` (trim + lowercase) so a value authored in any
|
|
12834
|
-
* casing matches the lowercase keys the resolver produces.
|
|
12835
|
-
*/
|
|
12836
|
-
const normalizeEntitlementKey = (key) => key.trim().toLowerCase();
|
|
12837
12756
|
const sha256Regex = /^[a-f0-9]{64}$/;
|
|
12757
|
+
const requiredUserTagValue = z.string().trim().min(1).max(100).refine((s) => !/[,;]/.test(s), "User tag must be a single tag with no commas or semicolons (e.g. \"vip\" or \"Sales Team\")");
|
|
12838
12758
|
z.object({
|
|
12839
12759
|
name: z.string().min(1).max(200),
|
|
12840
12760
|
slug: z.string().min(2).max(60).regex(DATA_LAKE_SLUG_REGEX, "Slug must be lowercase alphanumeric with hyphens (e.g. \"my-data-lake\")"),
|
|
12841
12761
|
description: z.string().max(2e3).optional(),
|
|
12842
12762
|
fileTagPrefix: z.string().trim().min(2).max(30).refine((s) => s.endsWith(":"), "Tag prefix must end with \":\" (e.g. \"acme:\")").refine((s) => !hasBlankTagPrefixSegment(s), "Tag prefix segments must be non-empty (e.g. \"acme:\" or \"acme:legal:\")").refine((s) => !isReservedTagPrefix(s), `Tag prefix cannot use the reserved "${DATALAKE_TAG_PREFIX}" namespace`),
|
|
12843
|
-
requiredUserTag:
|
|
12763
|
+
requiredUserTag: requiredUserTagValue.optional(),
|
|
12844
12764
|
requiredEntitlement: z.string().min(3).max(100).refine((s) => s.includes(":") && s.split(":").every((part) => part.length > 0), "Entitlement key must be namespaced with non-empty parts (e.g. \"product:pro\")").optional(),
|
|
12845
|
-
organizationId: z.string().optional()
|
|
12765
|
+
organizationId: z.string().optional(),
|
|
12766
|
+
origin: z.enum(DATA_LAKE_ORIGINS).optional()
|
|
12846
12767
|
});
|
|
12847
12768
|
z.object({
|
|
12848
12769
|
name: z.string().min(1).max(200).optional(),
|
|
@@ -12850,11 +12771,12 @@ z.object({
|
|
|
12850
12771
|
systemPrompt: z.string().optional(),
|
|
12851
12772
|
preferredSystemPromptId: z.union([z.literal(""), z.string().min(1).max(200)]).optional(),
|
|
12852
12773
|
groundingMode: z.enum(DATA_LAKE_GROUNDING_MODES).optional(),
|
|
12853
|
-
requiredUserTag: z.union([z.literal(""),
|
|
12774
|
+
requiredUserTag: z.union([z.literal(""), requiredUserTagValue]).optional(),
|
|
12854
12775
|
requiredEntitlement: z.union([z.literal(""), z.string().min(3).max(100).refine((s) => s.includes(":") && s.split(":").every((part) => part.length > 0), "Entitlement key must be namespaced with non-empty parts (e.g. \"product:pro\")")]).optional(),
|
|
12855
12776
|
auditQueryTextEnabled: z.boolean().optional(),
|
|
12856
12777
|
lakeMemoryEnabled: z.boolean().optional(),
|
|
12857
|
-
requiredPassageTokenTarget: z.number().int().min(64).max(OVERSIZED_PASSAGE_TOKEN_THRESHOLD).nullable().optional()
|
|
12778
|
+
requiredPassageTokenTarget: z.number().int().min(64).max(OVERSIZED_PASSAGE_TOKEN_THRESHOLD).nullable().optional(),
|
|
12779
|
+
origin: z.enum(DATA_LAKE_ORIGINS).optional()
|
|
12858
12780
|
});
|
|
12859
12781
|
z.object({
|
|
12860
12782
|
groundingMode: z.enum(DATA_LAKE_GROUNDING_MODES).optional(),
|
|
@@ -12989,14 +12911,387 @@ const ImageTemplateSettingsSchema = z$1.object({
|
|
|
12989
12911
|
safety_tolerance: z$1.number().min(0).max(6).optional(),
|
|
12990
12912
|
prompt_upsampling: z$1.boolean().optional()
|
|
12991
12913
|
});
|
|
12992
|
-
z$1.object({
|
|
12993
|
-
name: z$1.string().min(1).max(100),
|
|
12994
|
-
description: z$1.string().max(500).optional(),
|
|
12995
|
-
category: z$1.string().max(50).optional(),
|
|
12996
|
-
model: ImageTemplateModelSchema,
|
|
12997
|
-
settings: ImageTemplateSettingsSchema
|
|
12998
|
-
}).omit({ model: true }).partial();
|
|
12999
|
-
|
|
12914
|
+
z$1.object({
|
|
12915
|
+
name: z$1.string().min(1).max(100),
|
|
12916
|
+
description: z$1.string().max(500).optional(),
|
|
12917
|
+
category: z$1.string().max(50).optional(),
|
|
12918
|
+
model: ImageTemplateModelSchema,
|
|
12919
|
+
settings: ImageTemplateSettingsSchema
|
|
12920
|
+
}).omit({ model: true }).partial();
|
|
12921
|
+
/**
|
|
12922
|
+
* Identity factory that pins an endpoint contract's type. The `const` type
|
|
12923
|
+
* parameter preserves the concrete request-schema type so downstream adapters
|
|
12924
|
+
* can infer the validated body type (`z.infer<contract['request']>`) rather than
|
|
12925
|
+
* collapsing to the `z.ZodTypeAny` constraint.
|
|
12926
|
+
*
|
|
12927
|
+
* Otherwise deliberately a no-op at runtime (no registry side effect, no
|
|
12928
|
+
* `.openapi()`) so a contract stays a plain, transport-agnostic value that any
|
|
12929
|
+
* runtime can import - the one exception is the `pathParams`/`queryParams`
|
|
12930
|
+
* overlap check below, a plain assertion with no side effect of its own.
|
|
12931
|
+
*/
|
|
12932
|
+
function defineEndpoint(contract) {
|
|
12933
|
+
if (contract.pathParams && contract.queryParams) {
|
|
12934
|
+
const queryKeys = new Set(Object.keys(contract.queryParams.shape));
|
|
12935
|
+
const overlap = Object.keys(contract.pathParams.shape).filter((key) => queryKeys.has(key));
|
|
12936
|
+
if (overlap.length > 0) throw new Error(`defineEndpoint(${contract.operationId}): pathParams and queryParams both declare ${JSON.stringify(overlap)}. Next merges both into req.query by name, so the path segment would silently win over the query value. Rename one side.`);
|
|
12937
|
+
}
|
|
12938
|
+
return contract;
|
|
12939
|
+
}
|
|
12940
|
+
defineEndpoint({
|
|
12941
|
+
method: "post",
|
|
12942
|
+
path: "/api/chat",
|
|
12943
|
+
operationId: "sendChatMessage",
|
|
12944
|
+
summary: "Send a chat message",
|
|
12945
|
+
description: "Sends a message to the AI and creates a quest to process it. By default (async) the call returns immediately with a quest id; poll `GET /api/quests/{id}` for the reply. Send `wait: true` to block until the reply is ready and receive it inline. A tool that produced machine-readable state reports it under `toolPayloads` - an array of `{ type, payload }` entries in emission order, alongside (never instead of) the prose reply - on the `wait: true` body and on the polled quest. A turn can FAIL after the ACK - reported on the polled quest as `type: \"error\"`, since the ACK was already sent. A terminal `status: \"stopped\"` (a missing session, a user-cancelled turn) is ALSO a failure even without `type: \"error\"` - it carries an explanatory string in `reply`/`replies` rather than an answer. `type` is the failure signal for the classified failure classes (an abort, a provider timeout, credit exhaustion); `errorCode` is an optional refinement present only when the failure is a classified billing reason. A recovered stuck quest is NOT in that list: one that still has renderable content resolves as a success by design. `QUEST_ERROR_CODES` has two members, but only `insufficient_credits` is raised as a quest errorCode by any current throw site on this endpoint - `spend_cap_exceeded` is thrown only by the embed chat route's pre-flight, which fires outside the process try/catch that would classify it onto a quest. A caller must treat `type: \"error\"` OR a terminal `status: \"stopped\"` as failure even when `errorCode` is absent, and must not read `reply` as an answer without checking those first. Authenticate with an API key (`b4m_live_`) or a JWT.",
|
|
12946
|
+
tags: ["AI"],
|
|
12947
|
+
auth: "apiKeyOrJwt",
|
|
12948
|
+
scopes: ["ai:chat", "ai:generate"],
|
|
12949
|
+
request: SimplifiedChatRequestSchema,
|
|
12950
|
+
requestExample: {
|
|
12951
|
+
message: "How do I reset my password?",
|
|
12952
|
+
toolMode: "smart"
|
|
12953
|
+
},
|
|
12954
|
+
emitsRateLimitHeaders: true,
|
|
12955
|
+
responses: {
|
|
12956
|
+
200: {
|
|
12957
|
+
description: "Message accepted - NOT a completed turn. The default (async) path returns this queued ACK; the outcome arrives on `GET /api/quests/{id}` (see the `sendChatMessage200PollResult` schema). With `wait: true` the body additionally carries the completed reply (`response`/`responses`), `toolPayloads`, `createdAt`, and `performance` timings - fields not modelled here yet; the synchronous response shape is a follow-up. A turn that FAILS still resolves with `200`, never a 4xx, on both that `wait: true` body and the polled quest (`GET /api/quests/{id}`) - the prose explaining why lands in `reply`/`response` like any other answer, so the reply text alone cannot tell a failure from an answer. `type` is the field that can: both surfaces carry it unconditionally, so match on `type: \"error\"` first - it covers credit exhaustion, a provider timeout or overload, and an in-process aborted turn; a real answer carries the turn's actual completion type instead (`\"message\"` for an ordinary reply). Two related states do NOT set `type: \"error\"`: a user-cancelled turn resolves as `status: \"stopped\"` with `type` left at `\"message\"`, and a recovered stuck quest that still has renderable content resolves as a success (`status: \"done\"`, no error) by design, to avoid destroying content to report a failure. `errorCode` then names the failure reason, but only for the billing failures that have one - `\"insufficient_credits\"` today; it is absent on every other `type: \"error\"` turn, so never use its absence to infer success. On a real answer `errorCode` is absent from the `wait: true` body. Contrast the tts/music/soundEffects contracts, which reject synchronously with a 422 carrying the same `errorCode` vocabulary.",
|
|
12958
|
+
schema: ChatAckSchema,
|
|
12959
|
+
pollResult: {
|
|
12960
|
+
schema: ChatQuestPollResultSchema,
|
|
12961
|
+
description: "Outcome fields of the quest polled at `GET /api/quests/{id}` after this ACK. A finished turn that failed is `status: \"done\"` with `type: \"error\"` and the failure text in `reply`, so a caller reading `reply` alone cannot tell a failure from an answer - check `type` first, and also treat a terminal `status: \"stopped\"` (a missing session, a user-cancelled turn) as failure even though it never sets `type`. `errorCode` is an optional refinement of `type: \"error\"`, present only for a classified billing failure; credit exhaustion arrives here as `insufficient_credits`, the same vocabulary the synchronous 422s on `/api/ai/music`, `/api/ai/sound-effects` and `/api/ai/tts` use. `QUEST_ERROR_CODES` publishes a second member, `spend_cap_exceeded`, but no current throw site on this endpoint raises it as a quest errorCode: its only one, the embed chat route's pre-flight 422, fires outside the process try/catch that would classify it onto the quest. Most `type: \"error\"` turns - an abort, a provider timeout or overload - have NO `errorCode`; its absence does not mean success, only that the failure is unclassified. A recovered stuck quest that still has renderable content is not a failure at all: it keeps `type: \"message\"` even though it did not finish, so a caller gets the content rather than an error. The poll body carries further fields not modelled here, including `images`, `files`, `toolPayloads`, `promptMeta`, and the attachment report (`attachmentNotices`/`attachmentDelivery`) - only the outcome subset is modelled here.",
|
|
12962
|
+
example: {
|
|
12963
|
+
id: "664f1c2b9a1e4d0012ab34cd",
|
|
12964
|
+
status: "done",
|
|
12965
|
+
type: "error",
|
|
12966
|
+
errorCode: "insufficient_credits",
|
|
12967
|
+
reply: "You're out of credits. This request needs about 12 credits, but only 3 are available."
|
|
12968
|
+
}
|
|
12969
|
+
}
|
|
12970
|
+
},
|
|
12971
|
+
400: {
|
|
12972
|
+
description: "No usable default chat model is configured and none was supplied.",
|
|
12973
|
+
schema: ApiErrorSchema
|
|
12974
|
+
},
|
|
12975
|
+
404: {
|
|
12976
|
+
description: "No notebook/session exists to attach the message to.",
|
|
12977
|
+
schema: ApiErrorSchema
|
|
12978
|
+
},
|
|
12979
|
+
422: {
|
|
12980
|
+
description: "Request body failed schema validation.",
|
|
12981
|
+
schema: ApiErrorSchema
|
|
12982
|
+
},
|
|
12983
|
+
429: {
|
|
12984
|
+
description: "Per-user rate limit exceeded.",
|
|
12985
|
+
schema: ApiErrorSchema
|
|
12986
|
+
}
|
|
12987
|
+
},
|
|
12988
|
+
codeSample: {
|
|
12989
|
+
authToken: "b4m_live_<key>",
|
|
12990
|
+
streaming: false,
|
|
12991
|
+
body: {
|
|
12992
|
+
message: "How do I reset my password?",
|
|
12993
|
+
toolMode: "smart"
|
|
12994
|
+
}
|
|
12995
|
+
}
|
|
12996
|
+
});
|
|
12997
|
+
defineEndpoint({
|
|
12998
|
+
method: "post",
|
|
12999
|
+
path: "/api/v1/agent-executions",
|
|
13000
|
+
operationId: "startAgentExecution",
|
|
13001
|
+
summary: "Start an agent execution",
|
|
13002
|
+
description: "Runs the tool-using agent (ReAct) loop against a session - the same pipeline the product UI's Agent Mode toggle dispatches to, and the REST equivalent of the `agent_execute` WebSocket command. The run is asynchronous: this returns `202` with an execution id, and the caller polls `GET /api/v1/agent-executions/{id}` until `status` is terminal (`completed`, `failed`, or `aborted`). Nothing is streamed back over REST - for live iteration events, use the WebSocket route instead. Omit `agent_id` to get the profile the session's own surface resolves to, which is what reproduces the in-app toggle. The final reply is also written to the session as a normal chat message, so it appears in history. Naming a tool in `tools` also PRE-APPROVES it for the run: there is no interactive client to answer a permission prompt, so a run that calls an approval-gated tool you did not name fails with that tool named in `error` rather than hanging. Authenticate with an API key (`b4m_live_`) or a JWT.",
|
|
13003
|
+
tags: ["AI"],
|
|
13004
|
+
auth: "apiKeyOrJwt",
|
|
13005
|
+
scopes: ["ai:chat", "ai:generate"],
|
|
13006
|
+
request: AgentExecutionStartRequestSchema,
|
|
13007
|
+
requestExample: {
|
|
13008
|
+
session_id: "<sessionId>",
|
|
13009
|
+
message: "Audit this data set and summarize what stands out."
|
|
13010
|
+
},
|
|
13011
|
+
emitsRateLimitHeaders: true,
|
|
13012
|
+
responses: {
|
|
13013
|
+
202: {
|
|
13014
|
+
description: "Run accepted and dispatched. Poll `tracking_info.poll_url` for status and the answer.",
|
|
13015
|
+
schema: AgentExecutionAckSchema
|
|
13016
|
+
},
|
|
13017
|
+
400: {
|
|
13018
|
+
description: "No usable default chat model is configured and none was supplied.",
|
|
13019
|
+
schema: ApiErrorSchema
|
|
13020
|
+
},
|
|
13021
|
+
404: {
|
|
13022
|
+
description: "No session with the given `session_id` is owned by the caller, or `organization_id` names an organization the caller does not belong to. \"Exists but is not yours\" is reported as 404 too, so neither session ids nor org membership can be probed through this endpoint.",
|
|
13023
|
+
schema: ApiErrorSchema
|
|
13024
|
+
},
|
|
13025
|
+
409: {
|
|
13026
|
+
description: "The caller already has the maximum number of agent runs in flight - a per-user cap shared with runs started from the product UI. Wait for one to reach a terminal status before starting another; aborting a run is currently only possible over the WebSocket route.",
|
|
13027
|
+
schema: ApiErrorSchema
|
|
13028
|
+
},
|
|
13029
|
+
429: {
|
|
13030
|
+
description: "Per-user rate limit exceeded.",
|
|
13031
|
+
schema: ApiErrorSchema
|
|
13032
|
+
},
|
|
13033
|
+
502: {
|
|
13034
|
+
description: "The executor could not be dispatched. The run did not start, so a retry is safe.",
|
|
13035
|
+
schema: ApiErrorSchema
|
|
13036
|
+
}
|
|
13037
|
+
},
|
|
13038
|
+
codeSample: {
|
|
13039
|
+
authToken: "b4m_live_<key>",
|
|
13040
|
+
streaming: false,
|
|
13041
|
+
body: {
|
|
13042
|
+
session_id: "<sessionId>",
|
|
13043
|
+
message: "Audit this data set and summarize what stands out."
|
|
13044
|
+
}
|
|
13045
|
+
}
|
|
13046
|
+
});
|
|
13047
|
+
defineEndpoint({
|
|
13048
|
+
method: "get",
|
|
13049
|
+
path: "/api/v1/agent-executions/{id}",
|
|
13050
|
+
operationId: "getAgentExecution",
|
|
13051
|
+
summary: "Get an agent execution",
|
|
13052
|
+
description: "Returns the status, reasoning trace, and (once terminal) the final answer of a run started by `POST /api/v1/agent-executions`. `steps` grows while the run is in flight, so polling this endpoint is also how a REST caller follows the loop. Safe (GET) requests on this route are exempt from the per-day API-key quota so polling a single run costs one daily slot, not one per poll; the per-minute burst limit still applies.",
|
|
13053
|
+
tags: ["AI"],
|
|
13054
|
+
auth: "apiKeyOrJwt",
|
|
13055
|
+
scopes: ["ai:chat", "ai:generate"],
|
|
13056
|
+
pathParams: AgentExecutionIdParamSchema,
|
|
13057
|
+
emitsRateLimitHeaders: true,
|
|
13058
|
+
responses: {
|
|
13059
|
+
200: {
|
|
13060
|
+
description: "The execution, its trace, and its answer if it has finished.",
|
|
13061
|
+
schema: AgentExecutionStatusResponseSchema
|
|
13062
|
+
},
|
|
13063
|
+
404: {
|
|
13064
|
+
description: "No execution with that id is visible to the caller.",
|
|
13065
|
+
schema: ApiErrorSchema
|
|
13066
|
+
},
|
|
13067
|
+
429: {
|
|
13068
|
+
description: "Per-user rate limit exceeded.",
|
|
13069
|
+
schema: ApiErrorSchema
|
|
13070
|
+
}
|
|
13071
|
+
},
|
|
13072
|
+
codeSample: {
|
|
13073
|
+
authToken: "b4m_live_<key>",
|
|
13074
|
+
streaming: false,
|
|
13075
|
+
body: {}
|
|
13076
|
+
}
|
|
13077
|
+
});
|
|
13078
|
+
defineEndpoint({
|
|
13079
|
+
method: "put",
|
|
13080
|
+
path: "/api/sessions/{id}",
|
|
13081
|
+
operationId: "updateSession",
|
|
13082
|
+
summary: "Update a session",
|
|
13083
|
+
description: "Updates a session (called a \"notebook\" in the product UI): its name, attached knowledge files, tags, or retrieval settings. Set `knowledgeIds` and `forceKnowledgeRetrieval: true` together to enable grounded retrieval for `POST /api/chat` against this session - retrieval is gated by these session fields, not by the chat request. `lakeScope` narrows that retrieval to a chosen set of data lakes; omit it to leave the current choice unchanged, or send `null` to clear it so retrieval reaches every lake you can access. Authenticate with an API key (`b4m_live_`) or a JWT. Warning: adding to `knowledgeIds` shares those files with every member of every project containing this session by default (see `propagateToProjects`), and that sharing cannot be undone through the UI.",
|
|
13084
|
+
tags: ["Sessions"],
|
|
13085
|
+
auth: "apiKeyOrJwt",
|
|
13086
|
+
scopes: ["notebooks:write"],
|
|
13087
|
+
pathParams: SessionIdParamSchema,
|
|
13088
|
+
request: SessionUpdateRequestSchema,
|
|
13089
|
+
emitsRateLimitHeaders: true,
|
|
13090
|
+
requestExample: {
|
|
13091
|
+
knowledgeIds: ["<fabFileId>"],
|
|
13092
|
+
forceKnowledgeRetrieval: true
|
|
13093
|
+
},
|
|
13094
|
+
responses: {
|
|
13095
|
+
200: {
|
|
13096
|
+
description: "The updated session.",
|
|
13097
|
+
schema: SessionResponseSchema
|
|
13098
|
+
},
|
|
13099
|
+
404: {
|
|
13100
|
+
description: "No session exists with the given id.",
|
|
13101
|
+
schema: ApiErrorSchema
|
|
13102
|
+
}
|
|
13103
|
+
},
|
|
13104
|
+
codeSample: {
|
|
13105
|
+
authToken: "b4m_live_<key>",
|
|
13106
|
+
streaming: false,
|
|
13107
|
+
body: {
|
|
13108
|
+
knowledgeIds: ["<fabFileId>"],
|
|
13109
|
+
forceKnowledgeRetrieval: true
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
13112
|
+
});
|
|
13113
|
+
defineEndpoint({
|
|
13114
|
+
method: "post",
|
|
13115
|
+
path: "/api/ai/v1/tools",
|
|
13116
|
+
operationId: "executeTool",
|
|
13117
|
+
summary: "Execute a server-side tool",
|
|
13118
|
+
description: "Runs one of the built-in server-side tools (`weather_info`, `web_search`, `web_fetch`) and returns its result as JSON. Authenticate with a JWT access token only - API keys are NOT accepted on this endpoint. Rate-limited to 100 requests/hour. `request_id` echoes the X-Request-ID response header.",
|
|
13119
|
+
tags: ["AI"],
|
|
13120
|
+
auth: "jwtOnly",
|
|
13121
|
+
request: ToolExecutionRequestSchema,
|
|
13122
|
+
requestExample: {
|
|
13123
|
+
toolName: "web_search",
|
|
13124
|
+
input: { query: "how to reset a password" }
|
|
13125
|
+
},
|
|
13126
|
+
responses: {
|
|
13127
|
+
200: {
|
|
13128
|
+
description: "Tool executed successfully (`success` is always true here).",
|
|
13129
|
+
schema: ToolExecutionResponseSchema,
|
|
13130
|
+
example: {
|
|
13131
|
+
success: true,
|
|
13132
|
+
result: { summary: "Top results for the query." },
|
|
13133
|
+
executionTimeMs: 842,
|
|
13134
|
+
request_id: "abc-123"
|
|
13135
|
+
}
|
|
13136
|
+
},
|
|
13137
|
+
400: {
|
|
13138
|
+
description: "Malformed JSON body.",
|
|
13139
|
+
schema: ApiErrorSchema
|
|
13140
|
+
},
|
|
13141
|
+
401: {
|
|
13142
|
+
description: "Missing or invalid JWT (an API key is rejected here).",
|
|
13143
|
+
schema: ApiErrorSchema
|
|
13144
|
+
},
|
|
13145
|
+
429: {
|
|
13146
|
+
description: "Rate limit exceeded (100 requests/hour).",
|
|
13147
|
+
schema: ApiErrorSchema
|
|
13148
|
+
},
|
|
13149
|
+
500: {
|
|
13150
|
+
description: "Tool execution failed (`success: false` with `error`) or an unexpected server error.",
|
|
13151
|
+
schema: z$1.union([ToolExecutionResponseSchema, ApiErrorSchema]),
|
|
13152
|
+
bespokeErrorShape: "A tool that ran but failed returns the full ToolExecutionResponse (success: false), not an error envelope."
|
|
13153
|
+
}
|
|
13154
|
+
},
|
|
13155
|
+
codeSample: {
|
|
13156
|
+
authToken: "<access_token>",
|
|
13157
|
+
streaming: false,
|
|
13158
|
+
body: {
|
|
13159
|
+
toolName: "web_search",
|
|
13160
|
+
input: { query: "how to reset a password" }
|
|
13161
|
+
}
|
|
13162
|
+
}
|
|
13163
|
+
});
|
|
13164
|
+
defineEndpoint({
|
|
13165
|
+
method: "post",
|
|
13166
|
+
path: "/api/ai/v1/completions",
|
|
13167
|
+
operationId: "createCompletion",
|
|
13168
|
+
summary: "Create a chat completion",
|
|
13169
|
+
description: "OpenAI-compatible completion. The response is ALWAYS an SSE stream (`text/event-stream`), regardless of the `stream` flag: a `meta` event, then `content`/`tool_use` events carrying `usage`/`credits`, terminated by `data: [DONE]`. Once the stream has opened the HTTP status stays 200 and failures arrive as an in-band `error` event. The terminal event carries `stopReason`; treat `max_tokens` as a TRUNCATED reply rather than a complete one, and note that omitting `max_tokens` on the request lets the server size the output ceiling for the model (recommended for reasoning models, which spend thinking tokens inside that ceiling). A message `content` may be a string or an array of parts; image parts are accepted in OpenAI Chat (`image_url`), OpenAI Responses (`input_image`) or Anthropic (`image` with a `source`) form and are translated to whatever the target model speaks. Authenticate with an API key (`b4m_live_`) or a JWT.\n\nBILLING FAILURES. Headers are flushed before authentication or pricing, so unlike the JSON surfaces this endpoint has no pre-stream `422` + `errorCode: \"insufficient_credits\"` to pair with: credit exhaustion ALWAYS arrives as the in-band `error` event, whether it is caught by the reservation before the first token or by settlement mid-generation. Branch on that event's `code` (`insufficient_credits` - buy credits; `spend_cap_exceeded` - the owner is solvent but this key hit its admin-set ceiling, so raise the cap), never on `message`, which is prose and may change. `code` is absent on unclassified failures.",
|
|
13170
|
+
tags: ["AI"],
|
|
13171
|
+
auth: "apiKeyOrJwt",
|
|
13172
|
+
scopes: ["ai:chat", "ai:generate"],
|
|
13173
|
+
request: CompletionRequestSchema,
|
|
13174
|
+
requestExample: {
|
|
13175
|
+
model: "claude-opus-4-8",
|
|
13176
|
+
messages: [{
|
|
13177
|
+
role: "user",
|
|
13178
|
+
content: "How do I reset my password?"
|
|
13179
|
+
}],
|
|
13180
|
+
max_tokens: 500
|
|
13181
|
+
},
|
|
13182
|
+
streaming: true,
|
|
13183
|
+
responses: {
|
|
13184
|
+
200: {
|
|
13185
|
+
description: "SSE stream of completion events (see the CompletionStreamEvent shape).",
|
|
13186
|
+
contentType: "text/event-stream",
|
|
13187
|
+
schema: CompletionStreamEventSchema,
|
|
13188
|
+
example: {
|
|
13189
|
+
type: "content",
|
|
13190
|
+
text: "To reset your password, click 'Forgot password' on the login screen.",
|
|
13191
|
+
usage: {
|
|
13192
|
+
inputTokens: 42,
|
|
13193
|
+
outputTokens: 12
|
|
13194
|
+
},
|
|
13195
|
+
credits: {
|
|
13196
|
+
used: 1,
|
|
13197
|
+
usdCost: 37e-5
|
|
13198
|
+
},
|
|
13199
|
+
stopReason: "end_turn"
|
|
13200
|
+
}
|
|
13201
|
+
},
|
|
13202
|
+
400: {
|
|
13203
|
+
description: "Malformed JSON body (rejected before the stream opens).",
|
|
13204
|
+
schema: ApiErrorSchema
|
|
13205
|
+
}
|
|
13206
|
+
},
|
|
13207
|
+
codeSample: {
|
|
13208
|
+
authToken: "b4m_live_<key>",
|
|
13209
|
+
streaming: true,
|
|
13210
|
+
body: {
|
|
13211
|
+
model: "claude-opus-4-8",
|
|
13212
|
+
messages: [{
|
|
13213
|
+
role: "user",
|
|
13214
|
+
content: "How do I reset my password?"
|
|
13215
|
+
}],
|
|
13216
|
+
max_tokens: 500
|
|
13217
|
+
}
|
|
13218
|
+
}
|
|
13219
|
+
});
|
|
13220
|
+
defineEndpoint({
|
|
13221
|
+
method: "post",
|
|
13222
|
+
path: "/api/ai/tts",
|
|
13223
|
+
operationId: "synthesizeSpeech",
|
|
13224
|
+
summary: "Synthesize speech from text",
|
|
13225
|
+
description: "Generates speech from text using OpenAI or ElevenLabs. The default `encoding: \"binary\"` streams raw audio bytes with an `audio/*` Content-Type; `encoding: \"base64\"` returns JSON instead. When the requested provider has no usable key (or the provider rejects it), another configured provider stands in and the substitution is reported via `provider`/`fallbackFrom` and the `X-B4M-Tts-Provider*` headers. Input length is capped per provider (OpenAI 4096 characters, ElevenLabs 10000), and an output `format` the chosen provider cannot produce is rejected with a 422 before any provider cost is incurred. Generated audio is saved to the file browser by default (opt out per-user via the saveGeneratedAudio preference, or per-call with `preview`); the outcome is reported via `saved`/`fabFileId` and the `X-B4M-Audio-*` headers. Authenticate with an API key (`b4m_live_`) or a JWT.",
|
|
13226
|
+
tags: ["Audio"],
|
|
13227
|
+
auth: "apiKeyOrJwt",
|
|
13228
|
+
scopes: ["ai:generate"],
|
|
13229
|
+
request: ttsRequestSchema,
|
|
13230
|
+
requestExample: {
|
|
13231
|
+
text: "Your password has been reset.",
|
|
13232
|
+
provider: "openai",
|
|
13233
|
+
voice: "alloy",
|
|
13234
|
+
format: "mp3"
|
|
13235
|
+
},
|
|
13236
|
+
responses: {
|
|
13237
|
+
200: {
|
|
13238
|
+
description: "Speech synthesized. The default `encoding: \"binary\"` returns raw audio bytes whose Content-Type follows the requested `format` (`audio/mpeg` for mp3, else `audio/wav`, `audio/opus`, `audio/aac`, `audio/flac`, `audio/pcm`); `encoding: \"base64\"` returns the JSON body.",
|
|
13239
|
+
schema: ttsBase64ResponseSchema,
|
|
13240
|
+
example: {
|
|
13241
|
+
audio: "SUQzBAAAAAAA...",
|
|
13242
|
+
format: "mp3",
|
|
13243
|
+
contentType: "audio/mpeg",
|
|
13244
|
+
saved: true,
|
|
13245
|
+
fabFileId: "664f1c2b9a1e4d0012ab34cd"
|
|
13246
|
+
},
|
|
13247
|
+
alsoReturns: [
|
|
13248
|
+
{ contentType: "audio/mpeg" },
|
|
13249
|
+
{ contentType: "audio/wav" },
|
|
13250
|
+
{ contentType: "audio/opus" },
|
|
13251
|
+
{ contentType: "audio/aac" },
|
|
13252
|
+
{ contentType: "audio/flac" },
|
|
13253
|
+
{ contentType: "audio/pcm" }
|
|
13254
|
+
],
|
|
13255
|
+
headers: {
|
|
13256
|
+
"X-B4M-Tts-Provider": "The provider that produced the audio. Present only when a fallback happened.",
|
|
13257
|
+
"X-B4M-Tts-Provider-Fallback-From": "The originally requested provider that could not serve the request. Present only on a fallback.",
|
|
13258
|
+
"X-B4M-Audio-Saved": "Whether a browsable copy was saved to the file browser (\"true\"/\"false\").",
|
|
13259
|
+
"X-B4M-Audio-Fab-File-Id": "Id of the saved file. Present only when the copy was saved."
|
|
13260
|
+
}
|
|
13261
|
+
},
|
|
13262
|
+
401: {
|
|
13263
|
+
description: "Missing/invalid credentials, or no provider has a usable key (`provider_not_configured`).",
|
|
13264
|
+
schema: ttsErrorResponseSchema
|
|
13265
|
+
},
|
|
13266
|
+
413: {
|
|
13267
|
+
description: "The audio was generated (and billed) but is too large to return over this endpoint. Retrieve it from `fileUrl` when a browsable copy was saved.",
|
|
13268
|
+
schema: ttsResponseTooLargeSchema
|
|
13269
|
+
},
|
|
13270
|
+
422: {
|
|
13271
|
+
description: "Request body failed validation, the text exceeds the provider character limit, the provider cannot produce the requested `format`, or the caller cannot afford the synthesis - the last of those is the only one tagged `errorCode: \"insufficient_credits\"`, so match on the classifier rather than the status to tell a billing failure from a bad request.",
|
|
13272
|
+
schema: ttsErrorResponseSchema
|
|
13273
|
+
},
|
|
13274
|
+
429: {
|
|
13275
|
+
description: "The provider rate-limited the request.",
|
|
13276
|
+
schema: ttsErrorResponseSchema
|
|
13277
|
+
},
|
|
13278
|
+
502: {
|
|
13279
|
+
description: "The provider failed to generate speech.",
|
|
13280
|
+
schema: ttsErrorResponseSchema
|
|
13281
|
+
}
|
|
13282
|
+
},
|
|
13283
|
+
emitsRateLimitHeaders: true,
|
|
13284
|
+
codeSample: {
|
|
13285
|
+
authToken: "b4m_live_<key>",
|
|
13286
|
+
streaming: false,
|
|
13287
|
+
body: {
|
|
13288
|
+
text: "Your password has been reset.",
|
|
13289
|
+
provider: "openai",
|
|
13290
|
+
voice: "alloy",
|
|
13291
|
+
encoding: "base64"
|
|
13292
|
+
}
|
|
13293
|
+
}
|
|
13294
|
+
});
|
|
13000
13295
|
/**
|
|
13001
13296
|
* Response details shared by the endpoints that return generated audio as raw
|
|
13002
13297
|
* bytes (music, sound effects). Not a contract - just the pieces both of their
|
|
@@ -13033,8 +13328,179 @@ const generatedAudioBody = () => ({
|
|
|
13033
13328
|
alsoReturns: GENERATED_AUDIO_CONTENT_TYPES.slice(1).map((contentType) => ({ contentType })),
|
|
13034
13329
|
headers: GENERATED_AUDIO_SAVE_HEADERS
|
|
13035
13330
|
});
|
|
13036
|
-
({
|
|
13037
|
-
|
|
13331
|
+
defineEndpoint({
|
|
13332
|
+
method: "post",
|
|
13333
|
+
path: "/api/ai/music",
|
|
13334
|
+
operationId: "generateMusic",
|
|
13335
|
+
summary: "Generate background music",
|
|
13336
|
+
description: "Generates an instrumental or vocal background-music track from a text prompt and returns the raw audio bytes. `lengthMs` (3000-120000, default 10000) is forced on the provider, so the generated track always matches the billed length; credits are reserved before generation and refunded if it fails. Generated audio is saved to the file browser by default (opt out via the saveGeneratedAudio preference); the outcome is reported via the `X-B4M-Audio-Saved` / `X-B4M-Audio-Fab-File-Id` / `X-B4M-Audio-File-Url` response headers - use `X-B4M-Audio-File-Url` to fetch the saved copy, since `GET /api/files/{id}` fails closed until moderation completes. Authenticate with an API key (`b4m_live_`) or a JWT.",
|
|
13337
|
+
tags: ["Audio"],
|
|
13338
|
+
auth: "apiKeyOrJwt",
|
|
13339
|
+
scopes: ["ai:generate"],
|
|
13340
|
+
request: musicRequestSchema,
|
|
13341
|
+
requestExample: {
|
|
13342
|
+
prompt: "calm lo-fi study beat with soft piano",
|
|
13343
|
+
lengthMs: 3e4,
|
|
13344
|
+
forceInstrumental: true
|
|
13345
|
+
},
|
|
13346
|
+
responses: {
|
|
13347
|
+
200: {
|
|
13348
|
+
description: "Raw audio bytes; the Content-Type follows the requested `format` (mp3 by default).",
|
|
13349
|
+
...generatedAudioBody()
|
|
13350
|
+
},
|
|
13351
|
+
400: {
|
|
13352
|
+
description: "The billing user or organization could not be resolved.",
|
|
13353
|
+
schema: ApiErrorSchema
|
|
13354
|
+
},
|
|
13355
|
+
422: {
|
|
13356
|
+
description: "Request body failed validation, or the caller cannot afford the track - the latter is tagged `errorCode: \"insufficient_credits\"` (the balance is short, or the org member credit cap is exhausted).",
|
|
13357
|
+
schema: InsufficientCreditsErrorSchema
|
|
13358
|
+
},
|
|
13359
|
+
502: {
|
|
13360
|
+
description: "The provider failed to generate the track; reserved credits are refunded.",
|
|
13361
|
+
schema: ApiErrorSchema
|
|
13362
|
+
},
|
|
13363
|
+
503: {
|
|
13364
|
+
description: "No provider API key is configured for this deployment.",
|
|
13365
|
+
schema: ApiErrorSchema
|
|
13366
|
+
}
|
|
13367
|
+
},
|
|
13368
|
+
emitsRateLimitHeaders: true,
|
|
13369
|
+
codeSample: {
|
|
13370
|
+
authToken: "b4m_live_<key>",
|
|
13371
|
+
streaming: false,
|
|
13372
|
+
body: {
|
|
13373
|
+
prompt: "calm lo-fi study beat with soft piano",
|
|
13374
|
+
lengthMs: 3e4,
|
|
13375
|
+
forceInstrumental: true
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
});
|
|
13379
|
+
defineEndpoint({
|
|
13380
|
+
method: "post",
|
|
13381
|
+
path: "/api/ai/sound-effects",
|
|
13382
|
+
operationId: "generateSoundEffect",
|
|
13383
|
+
summary: "Generate a sound effect",
|
|
13384
|
+
description: "Generates a short sound effect from a text description and returns the raw audio bytes. Omitting `durationSeconds` lets the provider pick the length (and bills at its default); `promptInfluence` trades prompt fidelity (1) against variation (0). Credits are reserved before generation and refunded if it fails. Generated audio is saved to the file browser by default (opt out via the saveGeneratedAudio preference); the outcome is reported via the `X-B4M-Audio-Saved` / `X-B4M-Audio-Fab-File-Id` / `X-B4M-Audio-File-Url` response headers - use `X-B4M-Audio-File-Url` to fetch the saved copy, since `GET /api/files/{id}` fails closed until moderation completes. Authenticate with an API key (`b4m_live_`) or a JWT.",
|
|
13385
|
+
tags: ["Audio"],
|
|
13386
|
+
auth: "apiKeyOrJwt",
|
|
13387
|
+
scopes: ["ai:generate"],
|
|
13388
|
+
request: soundEffectsRequestSchema,
|
|
13389
|
+
requestExample: {
|
|
13390
|
+
text: "heavy wooden door creaking open",
|
|
13391
|
+
durationSeconds: 3,
|
|
13392
|
+
promptInfluence: .5
|
|
13393
|
+
},
|
|
13394
|
+
responses: {
|
|
13395
|
+
200: {
|
|
13396
|
+
description: "Raw audio bytes; the Content-Type follows the requested `format` (mp3 by default).",
|
|
13397
|
+
...generatedAudioBody()
|
|
13398
|
+
},
|
|
13399
|
+
400: {
|
|
13400
|
+
description: "The billing user or organization could not be resolved.",
|
|
13401
|
+
schema: ApiErrorSchema
|
|
13402
|
+
},
|
|
13403
|
+
422: {
|
|
13404
|
+
description: "Request body failed validation, or the caller cannot afford the effect - the latter is tagged `errorCode: \"insufficient_credits\"` (the balance is short, or the org member credit cap is exhausted).",
|
|
13405
|
+
schema: InsufficientCreditsErrorSchema
|
|
13406
|
+
},
|
|
13407
|
+
502: {
|
|
13408
|
+
description: "The provider failed to generate the effect; reserved credits are refunded.",
|
|
13409
|
+
schema: ApiErrorSchema
|
|
13410
|
+
},
|
|
13411
|
+
503: {
|
|
13412
|
+
description: "No provider API key is configured for this deployment.",
|
|
13413
|
+
schema: ApiErrorSchema
|
|
13414
|
+
}
|
|
13415
|
+
},
|
|
13416
|
+
emitsRateLimitHeaders: true,
|
|
13417
|
+
codeSample: {
|
|
13418
|
+
authToken: "b4m_live_<key>",
|
|
13419
|
+
streaming: false,
|
|
13420
|
+
body: {
|
|
13421
|
+
text: "heavy wooden door creaking open",
|
|
13422
|
+
durationSeconds: 3,
|
|
13423
|
+
promptInfluence: .5
|
|
13424
|
+
}
|
|
13425
|
+
}
|
|
13426
|
+
});
|
|
13427
|
+
defineEndpoint({
|
|
13428
|
+
method: "get",
|
|
13429
|
+
path: "/api/v1/me",
|
|
13430
|
+
operationId: "getMe",
|
|
13431
|
+
summary: "Get the authenticated caller",
|
|
13432
|
+
description: "Returns the authenticated caller: stable id, display name, plan tier, personal credit balance, and entitlement keys. The subject is always the credential holder - this endpoint accepts no user id, owner id, or impersonation parameter of any kind, so a key can only ever read its own owner. `credits.balance` is the caller's personal ledger; a call billed to an organization draws on a pool this number does not describe. Gate on `tier != \"free\"` for \"is this caller paying\" and on `subscription.price_id` for which product - the `basic`/`pro` rungs come from an internal plan ladder and do not track a plan's marketing name. Responses are never cacheable. Authenticate with an API key (`b4m_live_`) carrying `me:read`, or a JWT.",
|
|
13433
|
+
tags: ["Account"],
|
|
13434
|
+
auth: "apiKeyOrJwt",
|
|
13435
|
+
scopes: ["me:read"],
|
|
13436
|
+
emitsRateLimitHeaders: true,
|
|
13437
|
+
responses: {
|
|
13438
|
+
200: {
|
|
13439
|
+
description: "The caller, their tier, their personal credit balance, and their entitlement keys.",
|
|
13440
|
+
schema: MeResponseSchema,
|
|
13441
|
+
example: {
|
|
13442
|
+
id: "507f1f77bcf86cd799439011",
|
|
13443
|
+
name: "Ada Lovelace",
|
|
13444
|
+
tier: "basic",
|
|
13445
|
+
subscription: {
|
|
13446
|
+
plan_name: "Professional",
|
|
13447
|
+
price_id: "price_123",
|
|
13448
|
+
interval: "monthly",
|
|
13449
|
+
current_period_ends_at: "2026-10-18T00:00:00.000Z"
|
|
13450
|
+
},
|
|
13451
|
+
credits: { balance: 31667 },
|
|
13452
|
+
entitlements: ["base"]
|
|
13453
|
+
}
|
|
13454
|
+
},
|
|
13455
|
+
429: {
|
|
13456
|
+
description: "Per-user rate limit exceeded.",
|
|
13457
|
+
schema: ApiErrorSchema
|
|
13458
|
+
}
|
|
13459
|
+
},
|
|
13460
|
+
codeSample: {
|
|
13461
|
+
authToken: "b4m_live_<key>",
|
|
13462
|
+
streaming: false,
|
|
13463
|
+
body: {}
|
|
13464
|
+
}
|
|
13465
|
+
});
|
|
13466
|
+
/**
|
|
13467
|
+
* The fence language the model writes to place an inline map of search-result places in a reply.
|
|
13468
|
+
*
|
|
13469
|
+
* Same contract as SEARCH_RESULT_CARDS_LANGUAGE (searchResultCards.ts): `\w`-only and lowercase,
|
|
13470
|
+
* taught by WEB_SEARCH_MAP_PROMPT (@bike4mind/services), rendered by the reply renderer
|
|
13471
|
+
* (apps/client .../Session/PromptReplies.tsx), and rewritten to a plain list for every other
|
|
13472
|
+
* surface by `stripSearchResultCardFences`.
|
|
13473
|
+
*/
|
|
13474
|
+
const LOCATION_MAP_LANGUAGE = "b4m_map";
|
|
13475
|
+
function googleMapsSearchUrl(name, placeId) {
|
|
13476
|
+
const params = new URLSearchParams({
|
|
13477
|
+
api: "1",
|
|
13478
|
+
query: name
|
|
13479
|
+
});
|
|
13480
|
+
if (placeId?.startsWith("ChIJ")) params.set("query_place_id", placeId);
|
|
13481
|
+
return `https://www.google.com/maps/search/?${params.toString()}`;
|
|
13482
|
+
}
|
|
13483
|
+
/**
|
|
13484
|
+
* The fence language the model writes to place image cards inline in a reply.
|
|
13485
|
+
*
|
|
13486
|
+
* Two families of consumer must agree on this constant:
|
|
13487
|
+
* - WEB_SEARCH_CARDS_PROMPT (@bike4mind/services) teaches the model to emit it, and the reply
|
|
13488
|
+
* renderer (apps/client .../Session/PromptReplies.tsx) intercepts it to render cards instead
|
|
13489
|
+
* of raw text.
|
|
13490
|
+
* - Every other surface that reads reply markdown for an export, download, copy, publish, or
|
|
13491
|
+
* Slack delivery must strip the fenced block entirely with `stripSearchResultCardFences`
|
|
13492
|
+
* below, rather than passing the model-authored card JSON through verbatim. See that
|
|
13493
|
+
* function's own doc comment for the current list of call sites.
|
|
13494
|
+
*
|
|
13495
|
+
* MUST contain only `\w` characters: both the renderer (`/language-(\w+)/`) and the notebook
|
|
13496
|
+
* curation extractor (```` /```(\w+)?/ ````) capture the language with `\w+`, so a hyphen would
|
|
13497
|
+
* silently truncate this and the cards would never render or be skipped.
|
|
13498
|
+
*
|
|
13499
|
+
* MUST already be lowercase: some consumers lowercase the language they capture before
|
|
13500
|
+
* comparing, so an uppercase-containing value would pass the `\w`-only rule above while silently
|
|
13501
|
+
* breaking that comparison.
|
|
13502
|
+
*/
|
|
13503
|
+
const SEARCH_RESULT_CARDS_LANGUAGE = "b4m_cards";
|
|
13038
13504
|
z$1.enum(["user", "convergence"]).optional().catch(void 0), z$1.string().optional();
|
|
13039
13505
|
/**
|
|
13040
13506
|
* Blessed, self-hosted artifact library script paths (root-relative).
|
|
@@ -13096,6 +13562,7 @@ const OPTIONAL_DEP_BLESSED_SCRIPT_PATHS = Object.values({
|
|
|
13096
13562
|
}
|
|
13097
13563
|
}).map((d) => d.path);
|
|
13098
13564
|
[...REACT_BLESSED_SCRIPT_PATHS, ...OPTIONAL_DEP_BLESSED_SCRIPT_PATHS];
|
|
13565
|
+
[...OPENAI_IMAGE_MODELS, ...GEMINI_IMAGE_MODELS];
|
|
13099
13566
|
const DashboardParamsSchema = z$1.object({
|
|
13100
13567
|
dashboardDataSources: z$1.array(z$1.object({
|
|
13101
13568
|
sourceName: z$1.string(),
|
|
@@ -13139,11 +13606,21 @@ OpenAIImageGenerationInput.extend({
|
|
|
13139
13606
|
height: z$1.number().optional(),
|
|
13140
13607
|
aspect_ratio: z$1.string().optional(),
|
|
13141
13608
|
fabFileIds: z$1.array(z$1.string()).prefault([]),
|
|
13609
|
+
/**
|
|
13610
|
+
* Extra gpt-image "style anchor" images, as fabFile ids. They ride alongside the primary
|
|
13611
|
+
* input image (the first image-type entry in `fabFileIds`) rather than replacing it, and
|
|
13612
|
+
* OpenAI receives them in this order - which matters, because a mask always applies to the
|
|
13613
|
+
* first image in the array. fabFile ids rather than URLs so the existing access +
|
|
13614
|
+
* moderation gates (findAccessibleInIds, isImageServeable) still apply. Ignored by every
|
|
13615
|
+
* non-gpt-image provider. Repeated ids collapse to one anchor. See MAX_REFERENCE_IMAGES for
|
|
13616
|
+
* why the cap is 4 and not OpenAI's 16.
|
|
13617
|
+
*/
|
|
13618
|
+
referenceImageFabFileIds: z$1.array(z$1.string()).max(4).optional(),
|
|
13142
13619
|
tools: z$1.array(z$1.union([b4mLLMTools, z$1.string()])).optional(),
|
|
13143
13620
|
safety_tolerance: BFLSafetyToleranceSchema,
|
|
13144
13621
|
prompt_upsampling: z$1.boolean().optional(),
|
|
13145
13622
|
seed: z$1.number().nullable().optional(),
|
|
13146
|
-
output_format:
|
|
13623
|
+
output_format: ImageOutputFormatSchema.nullable().optional(),
|
|
13147
13624
|
/** Resolved by the API route's prompt resolver. Defaults to 'fresh' for first-turn or sessions with no prior image. */
|
|
13148
13625
|
intent: PromptIntentSchema.optional(),
|
|
13149
13626
|
promptEnhancement: z$1.object({
|
|
@@ -13161,7 +13638,7 @@ OpenAIImageGenerationInput.extend({
|
|
|
13161
13638
|
const GenerateImageToolCallSchema = OpenAIImageGenerationInput.extend({
|
|
13162
13639
|
safety_tolerance: z$1.number().optional(),
|
|
13163
13640
|
prompt_upsampling: z$1.boolean().optional(),
|
|
13164
|
-
output_format:
|
|
13641
|
+
output_format: ImageOutputFormatSchema.nullable().optional(),
|
|
13165
13642
|
seed: z$1.number().nullable().optional(),
|
|
13166
13643
|
editModel: z$1.string().optional()
|
|
13167
13644
|
}).omit({ prompt: true });
|
|
@@ -13198,15 +13675,19 @@ OpenAIImageGenerationInput.extend({
|
|
|
13198
13675
|
organizationId: z$1.string().nullable().optional(),
|
|
13199
13676
|
aspect_ratio: z$1.string().optional(),
|
|
13200
13677
|
fabFileIds: z$1.array(z$1.string()).prefault([]),
|
|
13201
|
-
|
|
13678
|
+
/**
|
|
13679
|
+
* Extra gpt-image "style anchor" images, as fabFile ids. On this endpoint `fabFileIds` is
|
|
13680
|
+
* the inpainting mask, not an image input, so references need their own field: they are
|
|
13681
|
+
* appended after `image` (the edit source), and OpenAI applies the mask to the first entry
|
|
13682
|
+
* of that array - i.e. always to `image`, never to a reference. fabFile ids rather than URLs
|
|
13683
|
+
* so the existing access + moderation gates (findAccessibleInIds, isImageServeable) still
|
|
13684
|
+
* apply. Ignored by BFL and Gemini. Repeated ids collapse to one anchor. See
|
|
13685
|
+
* MAX_REFERENCE_IMAGES for why the cap is 4, not 16.
|
|
13686
|
+
*/
|
|
13687
|
+
referenceImageFabFileIds: z$1.array(z$1.string()).max(4).optional(),
|
|
13688
|
+
image: z$1.string(),
|
|
13689
|
+
output_format: ImageOutputFormatSchema.nullable().optional()
|
|
13202
13690
|
});
|
|
13203
|
-
const isUnlimitedHistory = (historyCount) => historyCount === -1;
|
|
13204
|
-
/**
|
|
13205
|
-
* A usable number for callers that need one (page size, overflow math, telemetry). Unlimited
|
|
13206
|
-
* history has no count, so it resolves to the default page size, which is what an unwindowed
|
|
13207
|
-
* request has always actually fetched.
|
|
13208
|
-
*/
|
|
13209
|
-
const resolveHistoryFetchLimit = (historyCount) => isUnlimitedHistory(historyCount) || historyCount == null ? 14 : historyCount;
|
|
13210
13691
|
z$1.object({
|
|
13211
13692
|
/** Notebook session ID */
|
|
13212
13693
|
sessionId: z$1.string(),
|
|
@@ -13232,6 +13713,13 @@ z$1.object({
|
|
|
13232
13713
|
message: z$1.string(),
|
|
13233
13714
|
messageFileIds: z$1.array(z$1.string()).prefault([]),
|
|
13234
13715
|
questId: z$1.string().optional(),
|
|
13716
|
+
/**
|
|
13717
|
+
* Correct-and-retry: the quest whose answer the user says was wrong. Produces a NEW quest
|
|
13718
|
+
* carrying the user's correction, rather than overwriting the flagged one the way `questId`
|
|
13719
|
+
* (retry) does - the original answer has to survive for the evaluation-pair export to read it.
|
|
13720
|
+
* Validated server-side against the resolved session before it is persisted.
|
|
13721
|
+
*/
|
|
13722
|
+
correctsQuestId: z$1.string().optional(),
|
|
13235
13723
|
/** Extra context messages to include in the conversation from external sources */
|
|
13236
13724
|
extraContextMessages: z$1.array(z$1.object({
|
|
13237
13725
|
role: z$1.enum([
|
|
@@ -13267,11 +13755,19 @@ z$1.object({
|
|
|
13267
13755
|
* was the only switch for the offer and it also strips every authored prompt, so no caller could
|
|
13268
13756
|
* have an arm that went unoffered AND kept the abstention licence.
|
|
13269
13757
|
*
|
|
13270
|
-
* Gates the
|
|
13758
|
+
* Gates the auto-add sites (the knowledge offer in resolveEnabledTools, the navigate_view
|
|
13271
13759
|
* auto-add, the blog/skill gate), unioned with `Boolean(promptMode)` by
|
|
13272
13760
|
* resolveSkipAutoOffers. A force-on, not an override: `false` under a promptMode still suppresses.
|
|
13273
13761
|
* Withholding navigate_view also drops the viewRegistry system block, which only describes it.
|
|
13274
13762
|
*
|
|
13763
|
+
* `buildSharedTools`' `offerOnlyNamedTools` (see sharedToolBuilder.ts) reads the same union for
|
|
13764
|
+
* the same reason: MCP tools merged past the `enabledTools` filter are withheld too, since the
|
|
13765
|
+
* caller never named them either. Unlike the auto-add sites, this one can still be reached per
|
|
13766
|
+
* tool - a caller with `session.enabledTools` (not the public `tools` field, which
|
|
13767
|
+
* `filterKnownTools` strips non-native ids from before this flag is even consulted) can name one
|
|
13768
|
+
* MCP tool by its namespaced `server__tool` id and keep it while every unnamed sibling is
|
|
13769
|
+
* withheld.
|
|
13770
|
+
*
|
|
13275
13771
|
* Withholds the OFFER, not knowledge: `session.forceKnowledgeRetrieval` is untouched, and an
|
|
13276
13772
|
* already-attached corpus is inlined rather than deferred to the tool. An arm that must see no
|
|
13277
13773
|
* knowledge at all also needs a session with no attachments and forced retrieval off.
|
|
@@ -13713,6 +14209,18 @@ const ArtifactVersionMetaSchema = z$1.object({
|
|
|
13713
14209
|
}),
|
|
13714
14210
|
sha256Index: z$1.string()
|
|
13715
14211
|
});
|
|
14212
|
+
/** One no-sign-in share link. `token` is the capability itself and is stripped from every
|
|
14213
|
+
* serialized response, so it is optional here: an owner-facing read carries the metadata
|
|
14214
|
+
* (id to revoke by, timestamps, per-link view count) with no token at all. `id` is the
|
|
14215
|
+
* subdocument `_id`, rendered as a string. */
|
|
14216
|
+
const ShareTokenEntrySchema = z$1.object({
|
|
14217
|
+
id: z$1.string().optional(),
|
|
14218
|
+
token: z$1.string().optional(),
|
|
14219
|
+
createdAt: z$1.date().optional(),
|
|
14220
|
+
revokedAt: z$1.date().nullish(),
|
|
14221
|
+
viewCount: z$1.int().nonnegative().prefault(0),
|
|
14222
|
+
lastViewedAt: z$1.date().nullish()
|
|
14223
|
+
});
|
|
13716
14224
|
/** Reserved slugs - must include every tier URL token so a slug can't shadow routing. */
|
|
13717
14225
|
const RESERVED_SLUGS = [
|
|
13718
14226
|
"api",
|
|
@@ -13836,6 +14344,10 @@ z$1.object({
|
|
|
13836
14344
|
shareToken: z$1.string().optional(),
|
|
13837
14345
|
/** When `shareToken` was last minted/rotated; drives the owner-facing "link created" surface. */
|
|
13838
14346
|
shareTokenUpdatedAt: z$1.date().nullish(),
|
|
14347
|
+
/** Every share link ever minted, revoked ones included. Mirrors the two fields above during
|
|
14348
|
+
* the rollout and becomes the source of truth once the backfill has run everywhere; `token`
|
|
14349
|
+
* is stripped from serialized responses, so an owner-facing read sees only the metadata. */
|
|
14350
|
+
shareTokens: z$1.array(ShareTokenEntrySchema).prefault([]),
|
|
13839
14351
|
/** Collaboration gate: who (among viewers) may annotate. Orthogonal to
|
|
13840
14352
|
* `visibility`. Defaults to `none` (read-only) until the owner opts in. */
|
|
13841
14353
|
commentPolicy: CommentPolicySchema.prefault("none"),
|
|
@@ -13866,6 +14378,9 @@ z$1.object({
|
|
|
13866
14378
|
declaredApiEndpoints: z$1.array(z$1.string()).prefault([]),
|
|
13867
14379
|
/** Rendered body snapshot for reply/fabfile viewer pages (markdown or text). */
|
|
13868
14380
|
renderedBody: z$1.string().optional(),
|
|
14381
|
+
/** Snapshot of the source reply's citables (reply source only), so a `b4m_map` fence in
|
|
14382
|
+
* `renderedBody` can still resolve its place ids after the source Quest is edited or deleted. */
|
|
14383
|
+
citables: z$1.array(CitableSourceSchema).optional(),
|
|
13869
14384
|
publishedAt: z$1.date(),
|
|
13870
14385
|
previousVersionMeta: ArtifactVersionMetaSchema.optional(),
|
|
13871
14386
|
/** Full version history (oldest to newest); each entry's bytes are archived at
|
|
@@ -14119,56 +14634,71 @@ z$1.object({
|
|
|
14119
14634
|
createdAt: z$1.union([z$1.string(), z$1.date()]).optional(),
|
|
14120
14635
|
updatedAt: z$1.union([z$1.string(), z$1.date()]).optional()
|
|
14121
14636
|
});
|
|
14637
|
+
ClaudeArtifactMimeTypes.RECHARTS, ClaudeArtifactMimeTypes.MERMAID, ClaudeArtifactMimeTypes.LATTICE, ClaudeArtifactMimeTypes.BLOG_DRAFT, ClaudeArtifactMimeTypes.CHESS;
|
|
14638
|
+
[...IMAGE_SIZE_CONSTRAINTS.DALL_E_2.sizes, ...IMAGE_SIZE_CONSTRAINTS.DALL_E_3.sizes];
|
|
14122
14639
|
/**
|
|
14123
|
-
*
|
|
14124
|
-
*
|
|
14125
|
-
* (packages/scripts/datalake/backfill-chunk-char-length.ts) compute the same number server-side
|
|
14126
|
-
* without reading chunk text out of the database. Deliberately NOT `text.length` (UTF-16 code
|
|
14127
|
-
* units): the two differ on astral characters (surrogate pairs), and the write path and the
|
|
14128
|
-
* backfill must agree exactly.
|
|
14640
|
+
* Zero-width space spliced into a defanged marker. It is invisible wherever the text is
|
|
14641
|
+
* rendered, so escaping never changes what the reader sees - only what the parser matches.
|
|
14129
14642
|
*/
|
|
14130
|
-
const
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14643
|
+
const ZERO_WIDTH_SPACE = "";
|
|
14644
|
+
/**
|
|
14645
|
+
* Defangs think-marker-shaped substrings inside provider-authored reasoning text so they can
|
|
14646
|
+
* never be mistaken for the real control markers adapters wrap around that same text.
|
|
14647
|
+
*
|
|
14648
|
+
* A reasoning delta is provider output, not our control plane - a model can say `<think>` or
|
|
14649
|
+
* `</think>` as literal content (reasoning about the protocol itself, or a leading/trailing
|
|
14650
|
+
* `</think>` from a provider that already delimits its own monologue). Adapters wrap the whole
|
|
14651
|
+
* delta in real markers via plain string concatenation, so an unescaped literal is
|
|
14652
|
+
* indistinguishable from a genuine open/close once it lands in the same string. Call this on
|
|
14653
|
+
* every raw reasoning delta before it is concatenated with THINK_OPEN_TAG/THINK_CLOSE_TAG.
|
|
14654
|
+
*/
|
|
14655
|
+
function escapeThinkMarkers(text) {
|
|
14656
|
+
if (!text) return text;
|
|
14657
|
+
return text.replace(/<(\/?)think>/g, `<${ZERO_WIDTH_SPACE}$1think>`);
|
|
14138
14658
|
}
|
|
14139
|
-
/**
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14659
|
+
/** One less than the longer marker's length: the most characters a real marker prefix can span. */
|
|
14660
|
+
const MAX_PARTIAL_MARKER_LENGTH = 7;
|
|
14661
|
+
/** Length of the longest suffix of `text` that is a proper prefix of either marker token. */
|
|
14662
|
+
function partialMarkerSuffixLength(text) {
|
|
14663
|
+
const max = Math.min(MAX_PARTIAL_MARKER_LENGTH, text.length);
|
|
14664
|
+
for (let len = max; len > 0; len--) {
|
|
14665
|
+
const suffix = text.slice(-len);
|
|
14666
|
+
if ("<think>".startsWith(suffix) || "</think>".startsWith(suffix)) return len;
|
|
14667
|
+
}
|
|
14668
|
+
return 0;
|
|
14143
14669
|
}
|
|
14144
14670
|
/**
|
|
14145
|
-
*
|
|
14146
|
-
* rule, `getAccessibleDataLakes`): a non-admin reaches the model via
|
|
14147
|
-
* `allowedUserTags ∩ userTags` OR `allowedEntitlements ∩ entitlementKeys`.
|
|
14671
|
+
* Stateful counterpart to escapeThinkMarkers for text that arrives in streamed pieces.
|
|
14148
14672
|
*
|
|
14149
|
-
*
|
|
14150
|
-
*
|
|
14151
|
-
* (
|
|
14152
|
-
*
|
|
14673
|
+
* escapeThinkMarkers alone is only safe on a complete string: adapters call it once per
|
|
14674
|
+
* delta, but a provider is free to split a marker-shaped substring across two adjacent
|
|
14675
|
+
* deltas (e.g. 'wrote <th' then 'ink> tag'). Escaping each half independently leaves both
|
|
14676
|
+
* halves unescaped, and concatenating them reassembles a literal `<think>`/`</think>` that
|
|
14677
|
+
* is then indistinguishable from the real control marker wrapped around the same text.
|
|
14153
14678
|
*
|
|
14154
|
-
*
|
|
14155
|
-
*
|
|
14156
|
-
*
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
|
|
14169
|
-
|
|
14679
|
+
* This holds back any trailing substring of the buffered text that could still extend into
|
|
14680
|
+
* a marker (up to `<think>`/`</think>`'s length minus one) until the next push resolves it
|
|
14681
|
+
* one way or the other, or flush() is called at the end of the reasoning span.
|
|
14682
|
+
*/
|
|
14683
|
+
function createThinkMarkerEscaper() {
|
|
14684
|
+
let pending = "";
|
|
14685
|
+
return {
|
|
14686
|
+
push(chunk) {
|
|
14687
|
+
if (!chunk) return "";
|
|
14688
|
+
const combined = pending + chunk;
|
|
14689
|
+
const holdLength = partialMarkerSuffixLength(combined);
|
|
14690
|
+
const safeLength = combined.length - holdLength;
|
|
14691
|
+
const safe = combined.slice(0, safeLength);
|
|
14692
|
+
pending = combined.slice(safeLength);
|
|
14693
|
+
return escapeThinkMarkers(safe);
|
|
14694
|
+
},
|
|
14695
|
+
flush() {
|
|
14696
|
+
const remaining = pending;
|
|
14697
|
+
pending = "";
|
|
14698
|
+
return escapeThinkMarkers(remaining);
|
|
14699
|
+
}
|
|
14700
|
+
};
|
|
14170
14701
|
}
|
|
14171
|
-
[...OPENAI_IMAGE_MODELS, ...GEMINI_IMAGE_MODELS];
|
|
14172
14702
|
/** Generation was cut off against the output-token ceiling. */
|
|
14173
14703
|
const TRUNCATED_FINISH_REASON = "max_tokens";
|
|
14174
14704
|
/**
|
|
@@ -14195,6 +14725,115 @@ function isEarlyStop(stopReason) {
|
|
|
14195
14725
|
return !!stopReason && EARLY_STOP_FINISH_REASONS.has(stopReason);
|
|
14196
14726
|
}
|
|
14197
14727
|
/**
|
|
14728
|
+
* Signs the image URLs `web_search` writes into its tool output, so `/api/search-image` (the
|
|
14729
|
+
* same-origin proxy that fetches them server-side) can verify a URL actually came from our own
|
|
14730
|
+
* search-provider results, not from a hostile page's snippet text steering the model into writing
|
|
14731
|
+
* an attacker-controlled URL with exfiltrated data in the query string.
|
|
14732
|
+
*
|
|
14733
|
+
* The signature is a trailing `&b4mSig=<hex>` (or `?b4mSig=<hex>` with no prior query string)
|
|
14734
|
+
* appended by plain string concatenation - never by reparsing the URL through `URLSearchParams`,
|
|
14735
|
+
* which re-serializes every existing param (e.g. turning a literal space into `+`) and would send
|
|
14736
|
+
* a byte-different URL to a host whose own signature (an imgix/S3-presigned link) covers the exact
|
|
14737
|
+
* query string the provider returned. Anchoring the signature to the END of the string, and
|
|
14738
|
+
* requiring it to be the only occurrence, is what makes verification unambiguous: anything a
|
|
14739
|
+
* tamperer appends after signing - including a second `b4mSig` - changes what's left of the anchor
|
|
14740
|
+
* match, which changes the canonical text, which invalidates the signature. There is deliberately
|
|
14741
|
+
* no "read the first/last `b4mSig` param and ignore the rest" step, since that step is exactly
|
|
14742
|
+
* where an earlier version of this file's bypass lived (verify read the URL's first `b4mSig` via
|
|
14743
|
+
* `searchParams.get` while signing canonicalized by deleting *every* `b4mSig`, so a validly-signed
|
|
14744
|
+
* URL with a second, attacker-authored `b4mSig` appended still verified, and was then forwarded to
|
|
14745
|
+
* `safeFetch` with that attacker data still attached).
|
|
14746
|
+
*
|
|
14747
|
+
* The model is already told to copy an `Images:` line's URL verbatim, so no new instruction is
|
|
14748
|
+
* needed for the signature to survive.
|
|
14749
|
+
*
|
|
14750
|
+
* The signature has no expiry: a stored reply, citable, published page, or curation transcript
|
|
14751
|
+
* persists indefinitely, and nothing re-signs a URL on read, so a TTL would make every card
|
|
14752
|
+
* permanently break once it lapsed rather than bound anything meaningful. What actually bounds
|
|
14753
|
+
* a leaked signed URL's usefulness is the same thing that bounds the route otherwise: it's only
|
|
14754
|
+
* reachable at all behind `jwtOnly` auth (apps/client/pages/api/search-image.ts) and is capped by
|
|
14755
|
+
* a per-user rate limit there.
|
|
14756
|
+
*/
|
|
14757
|
+
const SIGNATURE_PARAM = "b4mSig";
|
|
14758
|
+
const TRAILING_SIGNATURE_RE = new RegExp(`[?&]${SIGNATURE_PARAM}=([0-9a-f]{32})$`);
|
|
14759
|
+
const KNOWN_PLACEHOLDER_SECRETS = /* @__PURE__ */ new Set([
|
|
14760
|
+
"",
|
|
14761
|
+
"my-secret-placeholder-value",
|
|
14762
|
+
"not-configured"
|
|
14763
|
+
]);
|
|
14764
|
+
/**
|
|
14765
|
+
* True for an empty or known-placeholder signing secret. Shared so any caller that would
|
|
14766
|
+
* otherwise sign-and-fail (e.g. web_search deciding whether to pay for image results at all)
|
|
14767
|
+
* uses the same definition `verifyImageUrlSignature` fails closed on, rather than a second one
|
|
14768
|
+
* that could drift out of sync.
|
|
14769
|
+
*/
|
|
14770
|
+
function isPlaceholderImageSigningSecret(secret) {
|
|
14771
|
+
return KNOWN_PLACEHOLDER_SECRETS.has(secret ?? "");
|
|
14772
|
+
}
|
|
14773
|
+
function computeSignature(canonical, secret) {
|
|
14774
|
+
return createHmac("sha256", secret).update(canonical).digest("hex").slice(0, 32);
|
|
14775
|
+
}
|
|
14776
|
+
/**
|
|
14777
|
+
* Appends expiry + signature query params by string concatenation (never reparses or
|
|
14778
|
+
* re-serializes the existing query string - see the file-level comment). Returns the URL
|
|
14779
|
+
* unchanged if it fails to parse as a URL, or if it already carries a trailing signature.
|
|
14780
|
+
*/
|
|
14781
|
+
function signImageUrl(rawUrl, secret) {
|
|
14782
|
+
try {
|
|
14783
|
+
new URL(rawUrl);
|
|
14784
|
+
} catch {
|
|
14785
|
+
return rawUrl;
|
|
14786
|
+
}
|
|
14787
|
+
if (TRAILING_SIGNATURE_RE.test(rawUrl)) return rawUrl;
|
|
14788
|
+
const separator = rawUrl.includes("?") ? "&" : "?";
|
|
14789
|
+
const signature = computeSignature(rawUrl, secret);
|
|
14790
|
+
return `${rawUrl}${separator}${SIGNATURE_PARAM}=${signature}`;
|
|
14791
|
+
}
|
|
14792
|
+
/**
|
|
14793
|
+
* promptMeta.functionCalls fields that must never reach a viewer who only holds a
|
|
14794
|
+
* "read this conversation" grant - a session share, a live subscription, a bug-report
|
|
14795
|
+
* egress to a third party (Slack/email), or a session clone made by a share holder
|
|
14796
|
+
* (b4m-core/services/src/sessionService/clone.ts, which copies quest.promptMeta wholesale
|
|
14797
|
+
* onto a session the sharee then OWNS - a share/subscribe grant must not become a durable
|
|
14798
|
+
* unredacted copy just because the copy has a new owner).
|
|
14799
|
+
*
|
|
14800
|
+
* `returnValue` is the verbatim output of a tool call the OWNER's turn made - up to 8000 chars
|
|
14801
|
+
* per call (see recordToolResult.ts) - and a tool can read the owner's private corpus, files, or
|
|
14802
|
+
* connected integrations. A share/subscribe grant authorizes reading the conversation the owner
|
|
14803
|
+
* had, not re-reading whatever the owner's tools touched on the owner's behalf. `error` is
|
|
14804
|
+
* currently unwritten but carries the same class of content on the failure path, so it is
|
|
14805
|
+
* redacted alongside it.
|
|
14806
|
+
*
|
|
14807
|
+
* This list is the single source of truth: add a field here and every response boundary that
|
|
14808
|
+
* routes through {@link redactFunctionCallsForViewer} inherits the redaction - PROVIDED the field
|
|
14809
|
+
* is a top-level, directly-owned-content field like these two. The redaction itself is a shallow
|
|
14810
|
+
* `delete` per field (see below), so a future owner-only value nested inside another field (e.g. a
|
|
14811
|
+
* private blob inside `parameters`) would NOT be caught by adding its name here; that shape needs
|
|
14812
|
+
* its own per-field handling, not just a new entry in this array.
|
|
14813
|
+
*
|
|
14814
|
+
* Adjacent unredacted shape, not yet a leak: `promptMeta.executionTracking.steps[].result`/
|
|
14815
|
+
* `.error` (PromptMetaZodSchema) are the same class of owner-only content but nothing writes them
|
|
14816
|
+
* today, so there is nothing to redact yet. A future writer landing there bypasses this list
|
|
14817
|
+
* entirely unless it is added here too.
|
|
14818
|
+
*/
|
|
14819
|
+
const OWNER_ONLY_FUNCTION_CALL_FIELDS = ["returnValue", "error"];
|
|
14820
|
+
/**
|
|
14821
|
+
* Verbatim retrieved passage text on a citation chip (#3038). Owner-only for exactly the reason
|
|
14822
|
+
* `returnValue` is: it is a slice of a document the OWNER's retrieval read on the owner's behalf,
|
|
14823
|
+
* and a share/subscribe/clone grant authorizes reading the conversation, not re-reading the
|
|
14824
|
+
* owner's corpus through it. The sibling `chunkId` is deliberately kept - an opaque id is not
|
|
14825
|
+
* content, and `citables[].id` (the file id) is already unredacted beside it.
|
|
14826
|
+
*
|
|
14827
|
+
* `conflictsWith` (#3041) is kept for the same reason, recorded here so the next reader does not
|
|
14828
|
+
* re-derive it: it holds `fabFileId`s of other cited sources, so it adds a RELATIONSHIP between
|
|
14829
|
+
* chips the viewer can already see rather than a slice of the owner's corpus. That holds only while
|
|
14830
|
+
* the field stays ids - a future version carrying the conflicting SENTENCES (the detector has them:
|
|
14831
|
+
* InconsistencyEvidence.excerpt) would be `fullContext`'s class exactly and would have to join the
|
|
14832
|
+
* list below rather than ride along inside this one.
|
|
14833
|
+
*/
|
|
14834
|
+
const OWNER_ONLY_CITABLE_METADATA_FIELDS = ["fullContext"];
|
|
14835
|
+
[...OWNER_ONLY_FUNCTION_CALL_FIELDS.map((field) => `promptMeta.functionCalls.${field}`), ...OWNER_ONLY_CITABLE_METADATA_FIELDS.map((field) => `promptMeta.citables.metadata.${field}`)];
|
|
14836
|
+
/**
|
|
14198
14837
|
* Trigger-word validation, shared between client form and server agent
|
|
14199
14838
|
* endpoints so the validation rules can't drift.
|
|
14200
14839
|
*
|
|
@@ -14227,47 +14866,7 @@ z$1.array(triggerWordSchema).max(20, "Up to 20 trigger words allowed.").transfor
|
|
|
14227
14866
|
}
|
|
14228
14867
|
return out;
|
|
14229
14868
|
});
|
|
14230
|
-
|
|
14231
|
-
* Serve gate for uploaded FabFiles: hold-until-scanned, fail-closed on ALL mime
|
|
14232
|
-
* types, not just images. A file is serveable only once moderation has run to
|
|
14233
|
-
* completion on it, REGARDLESS of its declared `mimeType`.
|
|
14234
|
-
*
|
|
14235
|
-
* Why gate non-images too: `mimeType` is client-declared at upload time and is only
|
|
14236
|
-
* corrected by the S3 scan ~1-2s later (see `moderateUploadedFile`'s byte-sniffing). If this
|
|
14237
|
-
* gate special-cased "non-images always serveable" based on that same untrusted declared
|
|
14238
|
-
* mimeType, a file uploaded as `application/pdf` but actually a PNG (or vice versa) would be
|
|
14239
|
-
* served during that window before the sniff/scan ever runs. Gating on `moderationStatus`
|
|
14240
|
-
* alone closes that window for every file, image or not.
|
|
14241
|
-
*
|
|
14242
|
-
* `moderationStatus` semantics:
|
|
14243
|
-
* - 'clean' -> serveable
|
|
14244
|
-
* - 'pending' | 'scanning' -> NOT serveable (not yet through the scan)
|
|
14245
|
-
* - 'blocked' -> NOT serveable (confirmed block / unscannable format)
|
|
14246
|
-
* - null | undefined -> NOT serveable (fail-closed; legacy rows are
|
|
14247
|
-
* backfilled to 'clean', see backfill-fabfile-moderation-status.ts)
|
|
14248
|
-
*
|
|
14249
|
-
* Non-image files (PDFs, docs, text, ...) are NOT scanned by Rekognition, but they still
|
|
14250
|
-
* pass through `moderateUploadedFile`/`objectCreated`, which resolves them to 'clean'
|
|
14251
|
-
* immediately (no image bytes to hold on), so the hold is brief (one S3 event round trip),
|
|
14252
|
-
* not an indefinite block.
|
|
14253
|
-
*/
|
|
14254
|
-
function isImageServeable(f) {
|
|
14255
|
-
return f.moderationStatus === "clean";
|
|
14256
|
-
}
|
|
14257
|
-
/**
|
|
14258
|
-
* Is this mime type an image? `image/svg+xml` counts, since vision models receive it
|
|
14259
|
-
* as an image.
|
|
14260
|
-
*
|
|
14261
|
-
* The repo has ~50 inline `startsWith('image/')` checks and they disagree on the edges
|
|
14262
|
-
* (case, null handling). Only the ones on the attachment pipeline - composer upload,
|
|
14263
|
-
* chat context assembly, attachment capability warnings - have been converted here.
|
|
14264
|
-
* Icon pickers, avatar validation and resize eligibility still carry their own copies:
|
|
14265
|
-
* same question, unrelated subsystems, and folding them in would have made this a
|
|
14266
|
-
* repo-wide diff.
|
|
14267
|
-
*/
|
|
14268
|
-
function isImageAttachment(mimeType) {
|
|
14269
|
-
return typeof mimeType === "string" && mimeType.toLowerCase().startsWith("image/");
|
|
14270
|
-
}
|
|
14869
|
+
IMAGE_SIZE_CONSTRAINTS.BFL.minWidth, IMAGE_SIZE_CONSTRAINTS.BFL.maxWidth;
|
|
14271
14870
|
Array.from(new Set([
|
|
14272
14871
|
{
|
|
14273
14872
|
id: "opti.root",
|
|
@@ -15174,71 +15773,10 @@ Array.from(new Set([
|
|
|
15174
15773
|
const [, top] = v.target.split("/");
|
|
15175
15774
|
return `/${top}`;
|
|
15176
15775
|
})));
|
|
15177
|
-
function getHeader(headers, name) {
|
|
15178
|
-
if (!headers || typeof headers !== "object") return null;
|
|
15179
|
-
if (typeof headers.get === "function") {
|
|
15180
|
-
const value = headers.get(name);
|
|
15181
|
-
return typeof value === "string" ? value : null;
|
|
15182
|
-
}
|
|
15183
|
-
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
15184
|
-
return typeof value === "string" ? value : null;
|
|
15185
|
-
}
|
|
15186
|
-
function parseCount(value) {
|
|
15187
|
-
if (value === null) return null;
|
|
15188
|
-
const trimmed = value.trim();
|
|
15189
|
-
if (!trimmed) return null;
|
|
15190
|
-
const parsed = Number(trimmed);
|
|
15191
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
15192
|
-
}
|
|
15193
|
-
const UNIT_MS = {
|
|
15194
|
-
ms: 1,
|
|
15195
|
-
s: 1e3,
|
|
15196
|
-
m: 6e4,
|
|
15197
|
-
h: 36e5
|
|
15198
|
-
};
|
|
15199
|
-
const DURATION_PART = /(\d+(?:\.\d+)?)(ms|h|m|s)/g;
|
|
15200
|
-
/**
|
|
15201
|
-
* Parse a Go-style duration ("6ms", "0s", "1m30s", "1h2m3s") to milliseconds.
|
|
15202
|
-
*
|
|
15203
|
-
* Exported for its own tests: it is the part of this module that can be wrong in a way the
|
|
15204
|
-
* numbers still look plausible.
|
|
15205
|
-
*/
|
|
15206
|
-
function parseDurationMs(value) {
|
|
15207
|
-
if (typeof value !== "string") return null;
|
|
15208
|
-
const trimmed = value.trim();
|
|
15209
|
-
if (!trimmed) return null;
|
|
15210
|
-
DURATION_PART.lastIndex = 0;
|
|
15211
|
-
let total = 0;
|
|
15212
|
-
let matched = 0;
|
|
15213
|
-
let consumed = 0;
|
|
15214
|
-
for (const part of trimmed.matchAll(DURATION_PART)) {
|
|
15215
|
-
total += Number(part[1]) * UNIT_MS[part[2]];
|
|
15216
|
-
consumed += part[0].length;
|
|
15217
|
-
matched += 1;
|
|
15218
|
-
}
|
|
15219
|
-
if (matched === 0 || consumed !== trimmed.length) return null;
|
|
15220
|
-
return total;
|
|
15221
|
-
}
|
|
15222
|
-
/** Read both rate-limit dimensions off a provider response. */
|
|
15223
|
-
function parseEmbeddingRateLimitHeaders(headers) {
|
|
15224
|
-
return {
|
|
15225
|
-
limitTokens: parseCount(getHeader(headers, "x-ratelimit-limit-tokens")),
|
|
15226
|
-
limitRequests: parseCount(getHeader(headers, "x-ratelimit-limit-requests")),
|
|
15227
|
-
remainingTokens: parseCount(getHeader(headers, "x-ratelimit-remaining-tokens")),
|
|
15228
|
-
remainingRequests: parseCount(getHeader(headers, "x-ratelimit-remaining-requests")),
|
|
15229
|
-
resetTokensMs: parseDurationMs(getHeader(headers, "x-ratelimit-reset-tokens")),
|
|
15230
|
-
resetRequestsMs: parseDurationMs(getHeader(headers, "x-ratelimit-reset-requests"))
|
|
15231
|
-
};
|
|
15232
|
-
}
|
|
15233
|
-
/** True when the provider reported at least one usable ceiling. */
|
|
15234
|
-
function hasUsableLimits(snapshot) {
|
|
15235
|
-
return snapshot.limitTokens !== null || snapshot.limitRequests !== null;
|
|
15236
|
-
}
|
|
15237
15776
|
dayjs.extend(utc);
|
|
15238
15777
|
dayjs.extend(timezone);
|
|
15239
15778
|
dayjs.extend(relativeTime);
|
|
15240
15779
|
dayjs.extend(localizedFormat);
|
|
15241
|
-
var dayjsConfig_default = dayjs;
|
|
15242
15780
|
/**
|
|
15243
15781
|
* Default retryable errors for LLM API calls
|
|
15244
15782
|
*/
|
|
@@ -15526,6 +16064,225 @@ function getEnvironmentName(configApiConfig) {
|
|
|
15526
16064
|
if (/^https?:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/i.test(endpoint.url)) return "Local Dev";
|
|
15527
16065
|
return "Self-Hosted";
|
|
15528
16066
|
}
|
|
16067
|
+
//#endregion
|
|
16068
|
+
//#region src/utils/validateSessionId.ts
|
|
16069
|
+
/**
|
|
16070
|
+
* Session and resume ids arrive from the environment (`B4M_SESSION_ID`,
|
|
16071
|
+
* `B4M_RESUME_ID`) and are used as filesystem path components by the session
|
|
16072
|
+
* store and the debug logger. Restrict them to a strict charset (which still
|
|
16073
|
+
* covers UUIDs) so a hostile launcher cannot traverse out of the base dir via
|
|
16074
|
+
* e.g. `B4M_SESSION_ID=../config`.
|
|
16075
|
+
*/
|
|
16076
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
16077
|
+
function isValidSessionId(value) {
|
|
16078
|
+
return SESSION_ID_PATTERN.test(value);
|
|
16079
|
+
}
|
|
16080
|
+
//#endregion
|
|
16081
|
+
//#region src/config/toolSafety.ts
|
|
16082
|
+
/**
|
|
16083
|
+
* Tool safety categories determine when permission is required
|
|
16084
|
+
*/
|
|
16085
|
+
const ToolCategorySchema = z$1.enum([
|
|
16086
|
+
"auto_approve",
|
|
16087
|
+
"prompt_always",
|
|
16088
|
+
"prompt_default"
|
|
16089
|
+
]);
|
|
16090
|
+
z$1.object({
|
|
16091
|
+
categories: z$1.record(z$1.string(), ToolCategorySchema),
|
|
16092
|
+
trustedTools: z$1.array(z$1.string())
|
|
16093
|
+
});
|
|
16094
|
+
/**
|
|
16095
|
+
* Default tool categories
|
|
16096
|
+
*
|
|
16097
|
+
* Categories:
|
|
16098
|
+
* - auto_approve: Safe tools that don't need permission (math, search, datetime)
|
|
16099
|
+
* - prompt_always: Dangerous tools that ALWAYS need permission, cannot be trusted (file edits, shell commands)
|
|
16100
|
+
* - prompt_default: Tools that prompt by default but users can trust them (file reads, searches)
|
|
16101
|
+
*/
|
|
16102
|
+
const DEFAULT_TOOL_CATEGORIES = {
|
|
16103
|
+
math_evaluate: "auto_approve",
|
|
16104
|
+
current_datetime: "auto_approve",
|
|
16105
|
+
dice_roll: "auto_approve",
|
|
16106
|
+
prompt_enhancement: "auto_approve",
|
|
16107
|
+
find_definition: "auto_approve",
|
|
16108
|
+
ask_user_question: "auto_approve",
|
|
16109
|
+
weather_info: "prompt_default",
|
|
16110
|
+
edit_file: "prompt_always",
|
|
16111
|
+
edit_local_file: "prompt_always",
|
|
16112
|
+
create_file: "prompt_always",
|
|
16113
|
+
delete_file: "prompt_always",
|
|
16114
|
+
shell_execute: "prompt_always",
|
|
16115
|
+
bash_execute: "prompt_always",
|
|
16116
|
+
write_shell_stdin: "prompt_always",
|
|
16117
|
+
kill_background_shell: "prompt_always",
|
|
16118
|
+
git_commit: "prompt_always",
|
|
16119
|
+
git_push: "prompt_always",
|
|
16120
|
+
skill: "prompt_always",
|
|
16121
|
+
web_search: "prompt_default",
|
|
16122
|
+
check_shell_output: "prompt_default",
|
|
16123
|
+
list_background_shells: "prompt_default",
|
|
16124
|
+
web_fetch: "prompt_default",
|
|
16125
|
+
deep_research: "prompt_default",
|
|
16126
|
+
file_read: "prompt_default",
|
|
16127
|
+
grep_search: "prompt_default",
|
|
16128
|
+
glob_files: "prompt_default",
|
|
16129
|
+
get_file_tree: "prompt_default",
|
|
16130
|
+
get_file_structure: "prompt_default",
|
|
16131
|
+
git_status: "prompt_default",
|
|
16132
|
+
git_diff: "prompt_default",
|
|
16133
|
+
git_log: "prompt_default",
|
|
16134
|
+
git_branch: "prompt_default"
|
|
16135
|
+
};
|
|
16136
|
+
/**
|
|
16137
|
+
* Get the category for a tool
|
|
16138
|
+
* Returns 'prompt_default' if tool is not in the default categories
|
|
16139
|
+
*/
|
|
16140
|
+
function getToolCategory(toolName, customCategories) {
|
|
16141
|
+
if (toolName.startsWith("agent_hook:") || toolName.startsWith("skill_hook:")) return "prompt_always";
|
|
16142
|
+
if (customCategories && toolName in customCategories) return customCategories[toolName];
|
|
16143
|
+
if (toolName in DEFAULT_TOOL_CATEGORIES) return DEFAULT_TOOL_CATEGORIES[toolName];
|
|
16144
|
+
if (toolName.startsWith("mcp__")) return "prompt_always";
|
|
16145
|
+
return "prompt_default";
|
|
16146
|
+
}
|
|
16147
|
+
/**
|
|
16148
|
+
* Check if a tool can be trusted (not prompt_always)
|
|
16149
|
+
*/
|
|
16150
|
+
function canTrustTool(toolName, customCategories) {
|
|
16151
|
+
return getToolCategory(toolName, customCategories) !== "prompt_always";
|
|
16152
|
+
}
|
|
16153
|
+
/**
|
|
16154
|
+
* Check if a tool is read-only (safe for parallel execution).
|
|
16155
|
+
* Write tools (prompt_always category) must always be sequential.
|
|
16156
|
+
*
|
|
16157
|
+
* @param toolName - Name of the tool to check
|
|
16158
|
+
* @param customCategories - Optional custom category overrides
|
|
16159
|
+
* @returns true if the tool is read-only, false if it's a write tool
|
|
16160
|
+
*/
|
|
16161
|
+
function isReadOnlyTool(toolName, customCategories) {
|
|
16162
|
+
return getToolCategory(toolName, customCategories) !== "prompt_always";
|
|
16163
|
+
}
|
|
16164
|
+
const PROJECT_CONTEXT_FILES = [
|
|
16165
|
+
"CLAUDE.local.md",
|
|
16166
|
+
"CLAUDE.md",
|
|
16167
|
+
"AGENTS.md",
|
|
16168
|
+
"AI.local.md",
|
|
16169
|
+
"AI.md",
|
|
16170
|
+
"INSTRUCTIONS.md"
|
|
16171
|
+
];
|
|
16172
|
+
const GLOBAL_CONTEXT_FILES = ["AI.local.md", "AI.md"];
|
|
16173
|
+
/**
|
|
16174
|
+
* Format file size for display
|
|
16175
|
+
*/
|
|
16176
|
+
function formatFileSize(bytes) {
|
|
16177
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
16178
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
16179
|
+
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
16180
|
+
}
|
|
16181
|
+
/**
|
|
16182
|
+
* Try to read a context file from a directory
|
|
16183
|
+
*
|
|
16184
|
+
* Security: Only reads regular files (not directories or symlinks) within the specified directory.
|
|
16185
|
+
* Files must be under 100KB to prevent abuse. Symlinks are rejected to prevent reading
|
|
16186
|
+
* files outside the intended directory.
|
|
16187
|
+
*
|
|
16188
|
+
* @param dir - The directory to read from (must be a controlled location)
|
|
16189
|
+
* @param filename - The filename to read (must not contain path separators)
|
|
16190
|
+
* @param source - Whether this is a 'global' or 'project' context file
|
|
16191
|
+
* @returns The file result, an error object, or null if file doesn't exist
|
|
16192
|
+
*/
|
|
16193
|
+
function tryReadContextFile(dir, filename, source) {
|
|
16194
|
+
const filePath = path$1.join(dir, filename);
|
|
16195
|
+
try {
|
|
16196
|
+
const stats = fs$1.lstatSync(filePath);
|
|
16197
|
+
if (stats.isDirectory()) return null;
|
|
16198
|
+
if (stats.isSymbolicLink()) return { error: `${source === "global" ? "Global" : "Project"} ${filename} is a symlink (not allowed for security)` };
|
|
16199
|
+
if (stats.size > 102400) return { error: `${source === "global" ? "Global" : "Project"} ${filename} exceeds 100KB limit (${formatFileSize(stats.size)})` };
|
|
16200
|
+
return {
|
|
16201
|
+
filename,
|
|
16202
|
+
content: fs$1.readFileSync(filePath, "utf-8"),
|
|
16203
|
+
source,
|
|
16204
|
+
path: filePath
|
|
16205
|
+
};
|
|
16206
|
+
} catch (err) {
|
|
16207
|
+
if (err.code === "ENOENT") return null;
|
|
16208
|
+
if (err.code === "EACCES") return { error: `Cannot read ${source} ${filename}: permission denied` };
|
|
16209
|
+
return { error: `Cannot read ${source} ${filename}: ${err instanceof Error ? err.message : "Unknown error"}` };
|
|
16210
|
+
}
|
|
16211
|
+
}
|
|
16212
|
+
/**
|
|
16213
|
+
* Find the first context file in a directory from a list of candidates
|
|
16214
|
+
*/
|
|
16215
|
+
function findContextFile(dir, candidates, source) {
|
|
16216
|
+
for (const filename of candidates) {
|
|
16217
|
+
const result = tryReadContextFile(dir, filename, source);
|
|
16218
|
+
if (result === null) continue;
|
|
16219
|
+
if ("error" in result) return {
|
|
16220
|
+
result: null,
|
|
16221
|
+
error: result.error
|
|
16222
|
+
};
|
|
16223
|
+
return {
|
|
16224
|
+
result,
|
|
16225
|
+
error: null
|
|
16226
|
+
};
|
|
16227
|
+
}
|
|
16228
|
+
return {
|
|
16229
|
+
result: null,
|
|
16230
|
+
error: null
|
|
16231
|
+
};
|
|
16232
|
+
}
|
|
16233
|
+
/**
|
|
16234
|
+
* Merge global and project context into a single string
|
|
16235
|
+
*/
|
|
16236
|
+
function mergeContextContent(global, project) {
|
|
16237
|
+
if (global && project) return `${global.content}\n\n---\n\n${project.content}`;
|
|
16238
|
+
if (global) return global.content;
|
|
16239
|
+
if (project) return project.content;
|
|
16240
|
+
return "";
|
|
16241
|
+
}
|
|
16242
|
+
/**
|
|
16243
|
+
* Load context files from global and project directories
|
|
16244
|
+
*
|
|
16245
|
+
* Global files are loaded from ~/.bike4mind/
|
|
16246
|
+
* Project files are loaded from `projectDir`. When it is null the project layer
|
|
16247
|
+
* is SKIPPED entirely (not defaulted to cwd) - the folder-trust gate passes null
|
|
16248
|
+
* for an untrusted project so its repo context file is never loaded.
|
|
16249
|
+
*
|
|
16250
|
+
* Returns the first matching file from each layer based on priority order
|
|
16251
|
+
*/
|
|
16252
|
+
async function loadContextFiles(projectDir) {
|
|
16253
|
+
const errors = [];
|
|
16254
|
+
const globalDir = path$1.join(homedir$1(), ".bike4mind");
|
|
16255
|
+
const [globalResult, projectResult] = await Promise.all([Promise.resolve(findContextFile(globalDir, GLOBAL_CONTEXT_FILES, "global")), Promise.resolve(projectDir ? findContextFile(projectDir, PROJECT_CONTEXT_FILES, "project") : {
|
|
16256
|
+
result: null,
|
|
16257
|
+
error: null
|
|
16258
|
+
})]);
|
|
16259
|
+
if (globalResult.error) errors.push(globalResult.error);
|
|
16260
|
+
if (projectResult.error) errors.push(projectResult.error);
|
|
16261
|
+
const mergedContent = mergeContextContent(globalResult.result, projectResult.result);
|
|
16262
|
+
return {
|
|
16263
|
+
globalContext: globalResult.result,
|
|
16264
|
+
projectContext: projectResult.result,
|
|
16265
|
+
mergedContent,
|
|
16266
|
+
errors
|
|
16267
|
+
};
|
|
16268
|
+
}
|
|
16269
|
+
/**
|
|
16270
|
+
* Extract "# Compact Instructions" or "## Compact Instructions" section from CLAUDE.md content
|
|
16271
|
+
*
|
|
16272
|
+
* This section provides project-specific instructions for how conversations should be
|
|
16273
|
+
* summarized when compacting context.
|
|
16274
|
+
*
|
|
16275
|
+
* @param contextContent - The merged context content from CLAUDE.md files
|
|
16276
|
+
* @returns The extracted instructions content, or undefined if not found
|
|
16277
|
+
*/
|
|
16278
|
+
function extractCompactInstructions(contextContent) {
|
|
16279
|
+
const match = contextContent.match(/^#{1,2}\s*Compact\s*Instructions\s*$/im);
|
|
16280
|
+
if (!match || match.index === void 0) return;
|
|
16281
|
+
const startIndex = match.index + match[0].length;
|
|
16282
|
+
const remainingContent = contextContent.slice(startIndex);
|
|
16283
|
+
const endIndex = remainingContent.match(/^#{1,2}\s+\S/m)?.index ?? remainingContent.length;
|
|
16284
|
+
return remainingContent.slice(0, endIndex).trim() || void 0;
|
|
16285
|
+
}
|
|
15529
16286
|
const logger = class Logger {
|
|
15530
16287
|
static {
|
|
15531
16288
|
this.instance = null;
|
|
@@ -15544,9 +16301,10 @@ const logger = class Logger {
|
|
|
15544
16301
|
* Initialize the logger with a session ID
|
|
15545
16302
|
*/
|
|
15546
16303
|
async initialize(sessionId) {
|
|
16304
|
+
if (!isValidSessionId(sessionId)) throw new Error(`Invalid session id "${sessionId}": must match ${SESSION_ID_PATTERN.source}`);
|
|
15547
16305
|
this.sessionId = sessionId;
|
|
15548
16306
|
const debugDir = path.join(os.homedir(), ".bike4mind", "debug");
|
|
15549
|
-
await fs
|
|
16307
|
+
await fs.mkdir(debugDir, { recursive: true });
|
|
15550
16308
|
this.logFilePath = path.join(debugDir, `${sessionId}.txt`);
|
|
15551
16309
|
await this.writeToFile("INFO", "=== CLI SESSION START ===");
|
|
15552
16310
|
}
|
|
@@ -15598,7 +16356,7 @@ const logger = class Logger {
|
|
|
15598
16356
|
if (!this.fileLoggingEnabled || !this.logFilePath) return;
|
|
15599
16357
|
try {
|
|
15600
16358
|
const logEntry = `[${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19)}] [${level}] ${message}\n`;
|
|
15601
|
-
await fs
|
|
16359
|
+
await fs.appendFile(this.logFilePath, logEntry, "utf-8");
|
|
15602
16360
|
} catch (error) {
|
|
15603
16361
|
console.error("File logging failed:", error);
|
|
15604
16362
|
}
|
|
@@ -15715,11 +16473,11 @@ const logger = class Logger {
|
|
|
15715
16473
|
if (!this.fileLoggingEnabled) return;
|
|
15716
16474
|
try {
|
|
15717
16475
|
const debugDir = path.join(os.homedir(), ".bike4mind", "debug");
|
|
15718
|
-
const files = await fs
|
|
16476
|
+
const files = await fs.readdir(debugDir);
|
|
15719
16477
|
const thirtyDaysAgo = Date.now() - 2592e6;
|
|
15720
16478
|
for (const file of files) {
|
|
15721
16479
|
const filePath = path.join(debugDir, file);
|
|
15722
|
-
if ((await fs
|
|
16480
|
+
if ((await fs.stat(filePath)).mtime.getTime() < thirtyDaysAgo) await fs.unlink(filePath);
|
|
15723
16481
|
}
|
|
15724
16482
|
} catch (error) {
|
|
15725
16483
|
console.error("Failed to cleanup old logs:", error);
|
|
@@ -15943,6 +16701,7 @@ const CliConfigSchema = z$1.object({
|
|
|
15943
16701
|
hearth: z$1.boolean().optional()
|
|
15944
16702
|
}).catchall(z$1.boolean())).optional().prefault({}),
|
|
15945
16703
|
trustedTools: z$1.array(z$1.string()).optional().prefault([]),
|
|
16704
|
+
trustedProjects: z$1.array(z$1.string()).optional().prefault([]),
|
|
15946
16705
|
sandbox: SandboxConfigSchema.optional(),
|
|
15947
16706
|
additionalDirectories: z$1.array(z$1.string()).optional().prefault([]),
|
|
15948
16707
|
fallbackModels: z$1.array(z$1.string()).optional()
|
|
@@ -16051,6 +16810,7 @@ const DEFAULT_CONFIG = {
|
|
|
16051
16810
|
config: {}
|
|
16052
16811
|
},
|
|
16053
16812
|
trustedTools: [],
|
|
16813
|
+
trustedProjects: [],
|
|
16054
16814
|
additionalDirectories: []
|
|
16055
16815
|
};
|
|
16056
16816
|
/**
|
|
@@ -16071,6 +16831,19 @@ function findProjectConfigDir(startDir = process.cwd()) {
|
|
|
16071
16831
|
return process.cwd();
|
|
16072
16832
|
}
|
|
16073
16833
|
/**
|
|
16834
|
+
* Canonicalize a path via realpath, returning null if it doesn't exist or can't
|
|
16835
|
+
* be resolved. Used so a symlinked or relative project root is compared against
|
|
16836
|
+
* the trust set by its real location, and a resolve failure fails safe
|
|
16837
|
+
* (untrusted) rather than crashing the launch.
|
|
16838
|
+
*/
|
|
16839
|
+
async function safeRealpath(p) {
|
|
16840
|
+
try {
|
|
16841
|
+
return await promises.realpath(p);
|
|
16842
|
+
} catch {
|
|
16843
|
+
return null;
|
|
16844
|
+
}
|
|
16845
|
+
}
|
|
16846
|
+
/**
|
|
16074
16847
|
* Load project config from .bike4mind/config.json
|
|
16075
16848
|
* Returns null if file doesn't exist (this is normal - config.json is optional)
|
|
16076
16849
|
*/
|
|
@@ -16178,32 +16951,89 @@ function mergeMcpServers(...serverArrays) {
|
|
|
16178
16951
|
for (const servers of serverArrays) if (servers) for (const server of servers) serverMap.set(server.name, server);
|
|
16179
16952
|
return Array.from(serverMap.values());
|
|
16180
16953
|
}
|
|
16954
|
+
/** Sandbox modes ordered from weakest to strongest posture. */
|
|
16955
|
+
const SANDBOX_MODE_RANK = {
|
|
16956
|
+
disabled: 0,
|
|
16957
|
+
"auto-allow": 1,
|
|
16958
|
+
permissions: 2
|
|
16959
|
+
};
|
|
16960
|
+
function intersectStrings(a, b) {
|
|
16961
|
+
const set = new Set(a);
|
|
16962
|
+
return b.filter((x) => set.has(x));
|
|
16963
|
+
}
|
|
16964
|
+
function unionStrings(a, b) {
|
|
16965
|
+
return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
|
|
16966
|
+
}
|
|
16967
|
+
/** True when `target` is `root` itself or nested under it (no `..` escape). */
|
|
16968
|
+
function isWithin(root, target) {
|
|
16969
|
+
const rel = path.relative(root, target);
|
|
16970
|
+
return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
16971
|
+
}
|
|
16181
16972
|
/**
|
|
16182
|
-
*
|
|
16183
|
-
*
|
|
16973
|
+
* Merge a repo-sourced sandbox override onto the user's base posture so it can
|
|
16974
|
+
* only ever TIGHTEN it, never loosen it. A repo layer may enable the sandbox,
|
|
16975
|
+
* raise the mode (disabled < auto-allow < permissions) but never select
|
|
16976
|
+
* auto-allow itself, add denied paths, narrow read paths / domains / excluded
|
|
16977
|
+
* commands, turn the network filter on, and force allowUnsandboxedCommands off.
|
|
16978
|
+
* Every loosening value is ignored with a warning. `platform` is ignored.
|
|
16184
16979
|
*/
|
|
16185
|
-
function
|
|
16186
|
-
const
|
|
16187
|
-
if (!
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
filesystem: {
|
|
16192
|
-
|
|
16193
|
-
...override.filesystem ?? {}
|
|
16194
|
-
},
|
|
16195
|
-
network: {
|
|
16196
|
-
...resolved.network,
|
|
16197
|
-
...override.network ?? {}
|
|
16198
|
-
},
|
|
16199
|
-
excludedCommands: override.excludedCommands ?? resolved.excludedCommands,
|
|
16200
|
-
allowUnsandboxedCommands: override.allowUnsandboxedCommands ?? resolved.allowUnsandboxedCommands,
|
|
16201
|
-
platform: override.platform ?? resolved.platform
|
|
16980
|
+
function tightenSandbox(base, repo) {
|
|
16981
|
+
const b = base ?? DEFAULT_SANDBOX_CONFIG;
|
|
16982
|
+
if (!repo) return b;
|
|
16983
|
+
const warn = (what) => logger.warn(`Ignoring repo sandbox override that would loosen posture: ${what}`);
|
|
16984
|
+
const result = {
|
|
16985
|
+
...b,
|
|
16986
|
+
filesystem: { ...b.filesystem },
|
|
16987
|
+
network: { ...b.network }
|
|
16202
16988
|
};
|
|
16989
|
+
if (repo.enabled === true) result.enabled = true;
|
|
16990
|
+
else if (repo.enabled === false && b.enabled) warn("sandbox.enabled=false");
|
|
16991
|
+
if (repo.mode !== void 0) if (repo.mode === "auto-allow") warn("sandbox.mode=auto-allow");
|
|
16992
|
+
else if (SANDBOX_MODE_RANK[repo.mode] >= SANDBOX_MODE_RANK[b.mode]) result.mode = repo.mode;
|
|
16993
|
+
else warn(`sandbox.mode=${repo.mode} weaker than ${b.mode}`);
|
|
16994
|
+
if (repo.filesystem) {
|
|
16995
|
+
const fsr = repo.filesystem;
|
|
16996
|
+
if (fsr.deniedPaths) result.filesystem.deniedPaths = unionStrings(result.filesystem.deniedPaths, fsr.deniedPaths);
|
|
16997
|
+
if (fsr.allowedReadPaths) result.filesystem.allowedReadPaths = intersectStrings(result.filesystem.allowedReadPaths, fsr.allowedReadPaths);
|
|
16998
|
+
if (fsr.writeOnlyToWorkingDir === true) result.filesystem.writeOnlyToWorkingDir = true;
|
|
16999
|
+
else if (fsr.writeOnlyToWorkingDir === false && b.filesystem.writeOnlyToWorkingDir) warn("filesystem.writeOnlyToWorkingDir=false");
|
|
17000
|
+
}
|
|
17001
|
+
if (repo.network) {
|
|
17002
|
+
const nr = repo.network;
|
|
17003
|
+
if (nr.enabled === true) result.network.enabled = true;
|
|
17004
|
+
else if (nr.enabled === false && b.network.enabled) warn("network.enabled=false");
|
|
17005
|
+
if (nr.allowedDomains) result.network.allowedDomains = intersectStrings(result.network.allowedDomains, nr.allowedDomains);
|
|
17006
|
+
}
|
|
17007
|
+
if (repo.excludedCommands) result.excludedCommands = intersectStrings(result.excludedCommands, repo.excludedCommands);
|
|
17008
|
+
if (repo.allowUnsandboxedCommands === false) result.allowUnsandboxedCommands = false;
|
|
17009
|
+
else if (repo.allowUnsandboxedCommands === true && !b.allowUnsandboxedCommands) warn("allowUnsandboxedCommands=true");
|
|
17010
|
+
if (result.enabled && result.mode === "disabled") result.mode = "permissions";
|
|
17011
|
+
if (!result.enabled) result.mode = "disabled";
|
|
17012
|
+
return result;
|
|
16203
17013
|
}
|
|
16204
17014
|
/**
|
|
16205
|
-
*
|
|
16206
|
-
*
|
|
17015
|
+
* Fold repo-sourced MCP servers under the global set so a repo entry can never
|
|
17016
|
+
* REPLACE a same-named global server. Global definitions always win; repo
|
|
17017
|
+
* entries only fill names global does not already use. Later repo layers win
|
|
17018
|
+
* over earlier ones for names global does not define.
|
|
17019
|
+
*/
|
|
17020
|
+
function mergeMcpServersGlobalWins(global, ...repoLayers) {
|
|
17021
|
+
const byName = /* @__PURE__ */ new Map();
|
|
17022
|
+
for (const layer of repoLayers) {
|
|
17023
|
+
if (!layer) continue;
|
|
17024
|
+
for (const server of layer) byName.set(server.name, server);
|
|
17025
|
+
}
|
|
17026
|
+
if (global) for (const server of global) byName.set(server.name, server);
|
|
17027
|
+
return Array.from(byName.values());
|
|
17028
|
+
}
|
|
17029
|
+
/**
|
|
17030
|
+
* Merge configs with priority: global -> project -> local, with the invariant
|
|
17031
|
+
* that repo layers may only TIGHTEN the user's global security posture, never
|
|
17032
|
+
* loosen it. Sandbox goes through `tightenSandbox`; the repo `trustedTools`
|
|
17033
|
+
* union is filtered to tools that can actually be trusted and aren't globally
|
|
17034
|
+
* disabled; and any tool a repo tries to re-enable while it is disabled is
|
|
17035
|
+
* dropped. `mcpServers` is NOT merged here - callers fold repo servers in via
|
|
17036
|
+
* `mergeMcpServersGlobalWins` where all repo layers are visible together.
|
|
16207
17037
|
*/
|
|
16208
17038
|
function mergeConfigs(global, project, local) {
|
|
16209
17039
|
const merged = { ...global };
|
|
@@ -16222,8 +17052,7 @@ function mergeConfigs(global, project, local) {
|
|
|
16222
17052
|
...project.tools.config
|
|
16223
17053
|
}
|
|
16224
17054
|
};
|
|
16225
|
-
if (project.
|
|
16226
|
-
if (project.sandbox) merged.sandbox = mergeSandboxConfig(merged.sandbox, project.sandbox);
|
|
17055
|
+
if (project.sandbox) merged.sandbox = tightenSandbox(merged.sandbox, project.sandbox);
|
|
16227
17056
|
}
|
|
16228
17057
|
if (local) {
|
|
16229
17058
|
if (local.trustedTools) {
|
|
@@ -16238,9 +17067,15 @@ function mergeConfigs(global, project, local) {
|
|
|
16238
17067
|
...merged.preferences,
|
|
16239
17068
|
...local.preferences
|
|
16240
17069
|
};
|
|
16241
|
-
if (local.
|
|
16242
|
-
if (local.sandbox) merged.sandbox = mergeSandboxConfig(merged.sandbox, local.sandbox);
|
|
17070
|
+
if (local.sandbox) merged.sandbox = tightenSandbox(merged.sandbox, local.sandbox);
|
|
16243
17071
|
}
|
|
17072
|
+
const disabledSet = new Set(merged.tools.disabled);
|
|
17073
|
+
merged.tools = {
|
|
17074
|
+
...merged.tools,
|
|
17075
|
+
enabled: merged.tools.enabled.filter((t) => !disabledSet.has(t))
|
|
17076
|
+
};
|
|
17077
|
+
const globalTrusted = new Set(global.trustedTools || []);
|
|
17078
|
+
merged.trustedTools = (merged.trustedTools || []).filter((t) => globalTrusted.has(t) || canTrustTool(t) && !disabledSet.has(t));
|
|
16244
17079
|
return merged;
|
|
16245
17080
|
}
|
|
16246
17081
|
/**
|
|
@@ -16270,6 +17105,12 @@ var ConfigStore = class {
|
|
|
16270
17105
|
constructor(configPath) {
|
|
16271
17106
|
this.config = null;
|
|
16272
17107
|
this.projectConfigDir = null;
|
|
17108
|
+
this.globalConfig = null;
|
|
17109
|
+
this.projectRealPath = null;
|
|
17110
|
+
this.projectTrusted = false;
|
|
17111
|
+
this.rawProjectConfig = null;
|
|
17112
|
+
this.rawProjectLocalConfig = null;
|
|
17113
|
+
this.rawMcpJsonServers = null;
|
|
16273
17114
|
this.configPath = configPath || path.join(homedir(), ".bike4mind", "config.json");
|
|
16274
17115
|
}
|
|
16275
17116
|
/**
|
|
@@ -16336,35 +17177,33 @@ var ConfigStore = class {
|
|
|
16336
17177
|
trustedTools: validated.trustedTools || []
|
|
16337
17178
|
};
|
|
16338
17179
|
} catch (error) {
|
|
16339
|
-
if (error.code === "ENOENT") globalConfig =
|
|
17180
|
+
if (error.code === "ENOENT") globalConfig = structuredClone(DEFAULT_CONFIG);
|
|
16340
17181
|
else if (error instanceof z$1.ZodError) {
|
|
16341
17182
|
console.error("Global config validation error:", error.issues);
|
|
16342
17183
|
console.error("Using default configuration");
|
|
16343
|
-
globalConfig =
|
|
17184
|
+
globalConfig = structuredClone(DEFAULT_CONFIG);
|
|
16344
17185
|
} else throw error;
|
|
16345
17186
|
}
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
17187
|
+
this.globalConfig = globalConfig;
|
|
17188
|
+
this.projectConfigDir = null;
|
|
17189
|
+
this.projectRealPath = null;
|
|
17190
|
+
this.projectTrusted = false;
|
|
17191
|
+
this.rawProjectConfig = null;
|
|
17192
|
+
this.rawProjectLocalConfig = null;
|
|
17193
|
+
this.rawMcpJsonServers = null;
|
|
16349
17194
|
if (process.env.B4M_NO_PROJECT_CONFIG !== "1") {
|
|
16350
17195
|
this.projectConfigDir = findProjectConfigDir();
|
|
16351
17196
|
if (this.projectConfigDir) {
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
17197
|
+
this.projectRealPath = await safeRealpath(this.projectConfigDir) ?? this.projectConfigDir;
|
|
17198
|
+
this.projectTrusted = (globalConfig.trustedProjects || []).includes(this.projectRealPath);
|
|
17199
|
+
if (this.projectTrusted) {
|
|
17200
|
+
const loaded = await this.loadProjectLayers();
|
|
17201
|
+
if (loaded.hasConfig) logger.debug(`📁 Project config loaded from: ${this.projectConfigDir}/.bike4mind/`);
|
|
17202
|
+
if (loaded.mcpCount > 0) logger.debug(`📁 Project MCP config loaded from: ${this.projectConfigDir}/.mcp.json`);
|
|
17203
|
+
}
|
|
16357
17204
|
}
|
|
16358
|
-
} else this.projectConfigDir = null;
|
|
16359
|
-
const mergedConfig = mergeConfigs(globalConfig, projectConfig, projectLocalConfig);
|
|
16360
|
-
if (mcpJsonServers && mcpJsonServers.length > 0) mergedConfig.mcpServers = mergeMcpServers(mcpJsonServers, mergedConfig.mcpServers);
|
|
16361
|
-
const mcpConfigFile = process.env.B4M_MCP_CONFIG_FILE;
|
|
16362
|
-
if (mcpConfigFile) {
|
|
16363
|
-
const injected = await loadMcpConfigFile(mcpConfigFile);
|
|
16364
|
-
if (process.env.B4M_STRICT_MCP_CONFIG === "1") mergedConfig.mcpServers = injected ?? [];
|
|
16365
|
-
else if (injected) mergedConfig.mcpServers = mergeMcpServers(mergedConfig.mcpServers, injected);
|
|
16366
17205
|
}
|
|
16367
|
-
this.config =
|
|
17206
|
+
this.config = await this.computeMerged();
|
|
16368
17207
|
return this.config;
|
|
16369
17208
|
} catch (error) {
|
|
16370
17209
|
if (error.code === "ENOENT") return this.reset();
|
|
@@ -16378,6 +17217,116 @@ var ConfigStore = class {
|
|
|
16378
17217
|
}
|
|
16379
17218
|
}
|
|
16380
17219
|
/**
|
|
17220
|
+
* Load the raw repo config layers for the current project root. Called only
|
|
17221
|
+
* for a trusted root; stores them on `this` so trust changes can re-merge
|
|
17222
|
+
* without re-reading the global config file.
|
|
17223
|
+
*/
|
|
17224
|
+
async loadProjectLayers() {
|
|
17225
|
+
if (!this.projectConfigDir) return {
|
|
17226
|
+
hasConfig: false,
|
|
17227
|
+
mcpCount: 0
|
|
17228
|
+
};
|
|
17229
|
+
this.rawProjectConfig = await loadProjectConfig(this.projectConfigDir);
|
|
17230
|
+
this.rawProjectLocalConfig = await loadProjectLocalConfig(this.projectConfigDir);
|
|
17231
|
+
this.rawMcpJsonServers = await loadMcpJsonConfig(this.projectConfigDir);
|
|
17232
|
+
return {
|
|
17233
|
+
hasConfig: !!this.rawProjectConfig,
|
|
17234
|
+
mcpCount: this.rawMcpJsonServers?.length ?? 0
|
|
17235
|
+
};
|
|
17236
|
+
}
|
|
17237
|
+
/**
|
|
17238
|
+
* Build the merged effective config from the global layer plus the raw repo
|
|
17239
|
+
* layers - but only when the project is trusted, so an untrusted root
|
|
17240
|
+
* contributes nothing. Repo MCP servers are folded in global-wins (a repo
|
|
17241
|
+
* name can never replace a global server); an explicit `--mcp-config` is host-
|
|
17242
|
+
* injected (not repo-sourced) and keeps its override-by-name / strict scope.
|
|
17243
|
+
*/
|
|
17244
|
+
async computeMerged() {
|
|
17245
|
+
const global = this.globalConfig;
|
|
17246
|
+
const project = this.projectTrusted ? this.rawProjectConfig : null;
|
|
17247
|
+
const local = this.projectTrusted ? this.rawProjectLocalConfig : null;
|
|
17248
|
+
const mcpJson = this.projectTrusted ? this.rawMcpJsonServers : null;
|
|
17249
|
+
const merged = mergeConfigs(global, project, local);
|
|
17250
|
+
merged.mcpServers = mergeMcpServersGlobalWins(global.mcpServers, mcpJson, project?.mcpServers, local?.mcpServers);
|
|
17251
|
+
const mcpConfigFile = process.env.B4M_MCP_CONFIG_FILE;
|
|
17252
|
+
if (mcpConfigFile) {
|
|
17253
|
+
const injected = await loadMcpConfigFile(mcpConfigFile);
|
|
17254
|
+
if (process.env.B4M_STRICT_MCP_CONFIG === "1") merged.mcpServers = injected ?? [];
|
|
17255
|
+
else if (injected) merged.mcpServers = mergeMcpServers(merged.mcpServers, injected);
|
|
17256
|
+
}
|
|
17257
|
+
return merged;
|
|
17258
|
+
}
|
|
17259
|
+
/** Whether the current project root is trusted (folder-trust gate). */
|
|
17260
|
+
isProjectTrusted() {
|
|
17261
|
+
return this.projectTrusted;
|
|
17262
|
+
}
|
|
17263
|
+
/** Canonicalized project root discovered this session, or null. */
|
|
17264
|
+
getProjectRealPath() {
|
|
17265
|
+
return this.projectRealPath;
|
|
17266
|
+
}
|
|
17267
|
+
/** The realpath'd roots the user has explicitly trusted. */
|
|
17268
|
+
getTrustedProjects() {
|
|
17269
|
+
return this.globalConfig?.trustedProjects ? [...this.globalConfig.trustedProjects] : [];
|
|
17270
|
+
}
|
|
17271
|
+
/**
|
|
17272
|
+
* Whether the current project root ships any repo-committed b4m files that the
|
|
17273
|
+
* trust gate governs. Used to decide whether the startup trust prompt is even
|
|
17274
|
+
* worth showing (nothing to gate = no prompt).
|
|
17275
|
+
*/
|
|
17276
|
+
projectHasB4mFiles() {
|
|
17277
|
+
const root = this.projectConfigDir;
|
|
17278
|
+
if (!root) return false;
|
|
17279
|
+
return [
|
|
17280
|
+
[".bike4mind", "config.json"],
|
|
17281
|
+
[".bike4mind", "local.json"],
|
|
17282
|
+
[".bike4mind", "agents"],
|
|
17283
|
+
[".bike4mind", "commands"],
|
|
17284
|
+
[".mcp.json"],
|
|
17285
|
+
[".claude", "agents"],
|
|
17286
|
+
[".claude", "skills"],
|
|
17287
|
+
[".claude", "commands"],
|
|
17288
|
+
...PROJECT_CONTEXT_FILES.map((name) => [name])
|
|
17289
|
+
].some((parts) => existsSync(path.join(root, ...parts)));
|
|
17290
|
+
}
|
|
17291
|
+
/**
|
|
17292
|
+
* Trust a project root (default: the current one). Persists the realpath'd
|
|
17293
|
+
* root to the global `trustedProjects` and, when it's the current root, loads
|
|
17294
|
+
* its repo layers and re-merges so they take effect for this session.
|
|
17295
|
+
*/
|
|
17296
|
+
async trustProject(root) {
|
|
17297
|
+
await this.load();
|
|
17298
|
+
const target = root ? await safeRealpath(root) : this.projectRealPath;
|
|
17299
|
+
if (!target) return false;
|
|
17300
|
+
const g = this.globalConfig;
|
|
17301
|
+
if (!g.trustedProjects) g.trustedProjects = [];
|
|
17302
|
+
if (!g.trustedProjects.includes(target)) g.trustedProjects.push(target);
|
|
17303
|
+
if (target === this.projectRealPath && this.projectConfigDir && process.env.B4M_NO_PROJECT_CONFIG !== "1") {
|
|
17304
|
+
this.projectTrusted = true;
|
|
17305
|
+
await this.loadProjectLayers();
|
|
17306
|
+
}
|
|
17307
|
+
await this.save();
|
|
17308
|
+
return true;
|
|
17309
|
+
}
|
|
17310
|
+
/**
|
|
17311
|
+
* Revoke trust for a project root (default: the current one). Next launch
|
|
17312
|
+
* re-prompts and repo layers stay inert until re-trusted. Revoking the current
|
|
17313
|
+
* root drops its raw layers so nothing repo-sourced survives in this session.
|
|
17314
|
+
*/
|
|
17315
|
+
async untrustProject(root) {
|
|
17316
|
+
await this.load();
|
|
17317
|
+
const target = root ? await safeRealpath(root) : this.projectRealPath;
|
|
17318
|
+
if (!target) return;
|
|
17319
|
+
const g = this.globalConfig;
|
|
17320
|
+
g.trustedProjects = (g.trustedProjects || []).filter((p) => p !== target);
|
|
17321
|
+
if (target === this.projectRealPath) {
|
|
17322
|
+
this.projectTrusted = false;
|
|
17323
|
+
this.rawProjectConfig = null;
|
|
17324
|
+
this.rawProjectLocalConfig = null;
|
|
17325
|
+
this.rawMcpJsonServers = null;
|
|
17326
|
+
}
|
|
17327
|
+
await this.save();
|
|
17328
|
+
}
|
|
17329
|
+
/**
|
|
16381
17330
|
* Read the features map straight from the global config file, bypassing the
|
|
16382
17331
|
* in-memory cache. save() merges over this so concurrent writers (the
|
|
16383
17332
|
* interactive session vs `b4m plugin add`) don't clobber each other's keys.
|
|
@@ -16412,47 +17361,52 @@ var ConfigStore = class {
|
|
|
16412
17361
|
/**
|
|
16413
17362
|
* Save configuration to disk
|
|
16414
17363
|
*/
|
|
16415
|
-
async save(config) {
|
|
17364
|
+
async save(config, opts) {
|
|
16416
17365
|
await this.init();
|
|
17366
|
+
if (!this.globalConfig) await this.load();
|
|
17367
|
+
const global = this.globalConfig;
|
|
16417
17368
|
if (config) {
|
|
16418
|
-
const
|
|
16419
|
-
this.
|
|
16420
|
-
...
|
|
16421
|
-
...
|
|
16422
|
-
auth:
|
|
17369
|
+
const { mcpServers, trustedTools, additionalDirectories, trustedProjects, tools, sandbox, ...rest } = config;
|
|
17370
|
+
this.globalConfig = {
|
|
17371
|
+
...global,
|
|
17372
|
+
...rest,
|
|
17373
|
+
auth: "auth" in config ? config.auth : global.auth,
|
|
16423
17374
|
preferences: {
|
|
16424
|
-
...
|
|
17375
|
+
...global.preferences,
|
|
16425
17376
|
...config.preferences || {}
|
|
16426
17377
|
},
|
|
16427
|
-
tools: {
|
|
16428
|
-
...existingConfig.tools,
|
|
16429
|
-
...config.tools || {}
|
|
16430
|
-
},
|
|
16431
17378
|
toolApiKeys: {
|
|
16432
|
-
...
|
|
17379
|
+
...global.toolApiKeys,
|
|
16433
17380
|
...config.toolApiKeys || {}
|
|
16434
17381
|
},
|
|
16435
|
-
features: this.mergeFeatures(await this.readDiskFeatures(),
|
|
17382
|
+
features: opts?.clearFeatures ? {} : this.mergeFeatures(await this.readDiskFeatures(), global.features, config.features ?? global.features)
|
|
16436
17383
|
};
|
|
16437
|
-
}
|
|
16438
|
-
|
|
17384
|
+
} else this.globalConfig = {
|
|
17385
|
+
...global,
|
|
17386
|
+
features: opts?.clearFeatures ? {} : await this.readDiskFeatures()
|
|
17387
|
+
};
|
|
16439
17388
|
try {
|
|
16440
|
-
await promises.writeFile(this.configPath, JSON.stringify(this.
|
|
17389
|
+
await promises.writeFile(this.configPath, JSON.stringify(this.globalConfig, null, 2), "utf-8");
|
|
16441
17390
|
await promises.chmod(this.configPath, 384);
|
|
16442
17391
|
} catch (error) {
|
|
16443
17392
|
console.error("Failed to save config:", error);
|
|
16444
17393
|
throw error;
|
|
16445
17394
|
}
|
|
17395
|
+
this.config = await this.computeMerged();
|
|
16446
17396
|
}
|
|
16447
17397
|
/**
|
|
16448
17398
|
* Reset configuration to defaults
|
|
16449
17399
|
*/
|
|
16450
17400
|
async reset() {
|
|
16451
|
-
this.
|
|
16452
|
-
...DEFAULT_CONFIG,
|
|
17401
|
+
this.globalConfig = {
|
|
17402
|
+
...structuredClone(DEFAULT_CONFIG),
|
|
16453
17403
|
userId: v4()
|
|
16454
17404
|
};
|
|
16455
|
-
|
|
17405
|
+
this.projectTrusted = false;
|
|
17406
|
+
this.rawProjectConfig = null;
|
|
17407
|
+
this.rawProjectLocalConfig = null;
|
|
17408
|
+
this.rawMcpJsonServers = null;
|
|
17409
|
+
await this.save(void 0, { clearFeatures: true });
|
|
16456
17410
|
return this.config;
|
|
16457
17411
|
}
|
|
16458
17412
|
/**
|
|
@@ -16468,56 +17422,71 @@ var ConfigStore = class {
|
|
|
16468
17422
|
await this.save(updates);
|
|
16469
17423
|
}
|
|
16470
17424
|
/**
|
|
17425
|
+
* Persist a sandbox config to the GLOBAL layer (the `/sandbox` handlers' path).
|
|
17426
|
+
* Sandbox is excluded from the generic `save()` allowlist so it flows only
|
|
17427
|
+
* through here - a merged-config save() can never launder a repo-tightened
|
|
17428
|
+
* sandbox into the user's global default.
|
|
17429
|
+
*/
|
|
17430
|
+
async saveSandboxConfig(sandbox) {
|
|
17431
|
+
await this.load();
|
|
17432
|
+
this.globalConfig.sandbox = structuredClone(sandbox);
|
|
17433
|
+
await this.save();
|
|
17434
|
+
}
|
|
17435
|
+
/**
|
|
16471
17436
|
* Add MCP server configuration
|
|
16472
17437
|
*/
|
|
16473
17438
|
async addMcpServer(server) {
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
17439
|
+
await this.load();
|
|
17440
|
+
const g = this.globalConfig;
|
|
17441
|
+
g.mcpServers = g.mcpServers.filter((s) => s.name !== server.name);
|
|
17442
|
+
g.mcpServers.push(server);
|
|
17443
|
+
await this.save();
|
|
16478
17444
|
}
|
|
16479
17445
|
/**
|
|
16480
17446
|
* Remove MCP server configuration
|
|
16481
17447
|
*/
|
|
16482
17448
|
async removeMcpServer(name) {
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
17449
|
+
await this.load();
|
|
17450
|
+
const g = this.globalConfig;
|
|
17451
|
+
g.mcpServers = g.mcpServers.filter((s) => s.name !== name);
|
|
17452
|
+
await this.save();
|
|
16486
17453
|
}
|
|
16487
17454
|
/**
|
|
16488
17455
|
* Enable/disable MCP server
|
|
16489
17456
|
*/
|
|
16490
17457
|
async toggleMcpServer(name, enabled) {
|
|
16491
|
-
|
|
16492
|
-
const server =
|
|
17458
|
+
await this.load();
|
|
17459
|
+
const server = this.globalConfig.mcpServers.find((s) => s.name === name);
|
|
16493
17460
|
if (server) {
|
|
16494
17461
|
server.enabled = enabled;
|
|
16495
|
-
await this.save(
|
|
17462
|
+
await this.save();
|
|
16496
17463
|
}
|
|
16497
17464
|
}
|
|
16498
17465
|
/**
|
|
16499
17466
|
* Add a tool to trusted tools list
|
|
16500
17467
|
*/
|
|
16501
17468
|
async trustTool(toolName) {
|
|
16502
|
-
|
|
16503
|
-
|
|
16504
|
-
if (!
|
|
16505
|
-
|
|
16506
|
-
|
|
17469
|
+
await this.load();
|
|
17470
|
+
const g = this.globalConfig;
|
|
17471
|
+
if (!g.trustedTools) g.trustedTools = [];
|
|
17472
|
+
if (!g.trustedTools.includes(toolName)) {
|
|
17473
|
+
g.trustedTools.push(toolName);
|
|
17474
|
+
await this.save();
|
|
16507
17475
|
}
|
|
16508
17476
|
}
|
|
16509
17477
|
/**
|
|
16510
17478
|
* Remove a tool from trusted tools list
|
|
16511
17479
|
*/
|
|
16512
17480
|
async untrustTool(toolName) {
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
17481
|
+
await this.load();
|
|
17482
|
+
const g = this.globalConfig;
|
|
17483
|
+
if (g.trustedTools) {
|
|
17484
|
+
g.trustedTools = g.trustedTools.filter((t) => t !== toolName);
|
|
17485
|
+
await this.save();
|
|
16517
17486
|
}
|
|
16518
17487
|
}
|
|
16519
17488
|
/**
|
|
16520
|
-
* Get list of trusted tools
|
|
17489
|
+
* Get list of trusted tools (merged effective view)
|
|
16521
17490
|
*/
|
|
16522
17491
|
async getTrustedTools() {
|
|
16523
17492
|
return (await this.load()).trustedTools || [];
|
|
@@ -16526,9 +17495,9 @@ var ConfigStore = class {
|
|
|
16526
17495
|
* Clear all trusted tools
|
|
16527
17496
|
*/
|
|
16528
17497
|
async clearTrustedTools() {
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
await this.save(
|
|
17498
|
+
await this.load();
|
|
17499
|
+
this.globalConfig.trustedTools = [];
|
|
17500
|
+
await this.save();
|
|
16532
17501
|
}
|
|
16533
17502
|
/**
|
|
16534
17503
|
* Get authentication tokens
|
|
@@ -16540,17 +17509,17 @@ var ConfigStore = class {
|
|
|
16540
17509
|
* Set authentication tokens
|
|
16541
17510
|
*/
|
|
16542
17511
|
async setAuthTokens(tokens) {
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
await this.save(
|
|
17512
|
+
await this.load();
|
|
17513
|
+
this.globalConfig.auth = tokens;
|
|
17514
|
+
await this.save();
|
|
16546
17515
|
}
|
|
16547
17516
|
/**
|
|
16548
17517
|
* Clear authentication tokens (logout)
|
|
16549
17518
|
*/
|
|
16550
17519
|
async clearAuthTokens() {
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
await this.save(
|
|
17520
|
+
await this.load();
|
|
17521
|
+
this.globalConfig.auth = void 0;
|
|
17522
|
+
await this.save();
|
|
16554
17523
|
}
|
|
16555
17524
|
/**
|
|
16556
17525
|
* Check if user is authenticated
|
|
@@ -16571,10 +17540,9 @@ var ConfigStore = class {
|
|
|
16571
17540
|
* Pass null to reset to the build-time default service.
|
|
16572
17541
|
*/
|
|
16573
17542
|
async setCustomApiUrl(url) {
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
await this.save(config);
|
|
17543
|
+
await this.load();
|
|
17544
|
+
this.globalConfig.apiConfig = url === null ? void 0 : { customUrl: url };
|
|
17545
|
+
await this.save();
|
|
16578
17546
|
}
|
|
16579
17547
|
/**
|
|
16580
17548
|
* Switch the active API environment, caching auth tokens per-environment so
|
|
@@ -16591,8 +17559,9 @@ var ConfigStore = class {
|
|
|
16591
17559
|
* preserve the previous `auth` and defeat the per-env swap.
|
|
16592
17560
|
*/
|
|
16593
17561
|
async switchApiEnvironment(target) {
|
|
16594
|
-
|
|
16595
|
-
const
|
|
17562
|
+
await this.load();
|
|
17563
|
+
const g = this.globalConfig;
|
|
17564
|
+
const prevKey = normalizeEnvKey(g.apiConfig?.customUrl || getDefaultApiUrl());
|
|
16596
17565
|
let newUrl;
|
|
16597
17566
|
let newApiConfig;
|
|
16598
17567
|
if (target === "prod") {
|
|
@@ -16611,16 +17580,15 @@ var ConfigStore = class {
|
|
|
16611
17580
|
url: newUrl,
|
|
16612
17581
|
envName,
|
|
16613
17582
|
changed: false,
|
|
16614
|
-
authenticated: hasValidAuth(
|
|
17583
|
+
authenticated: hasValidAuth(g.auth)
|
|
16615
17584
|
};
|
|
16616
|
-
const authByEnv = { ...
|
|
16617
|
-
if (
|
|
17585
|
+
const authByEnv = { ...g.authByEnv || {} };
|
|
17586
|
+
if (g.auth) authByEnv[prevKey] = g.auth;
|
|
16618
17587
|
else delete authByEnv[prevKey];
|
|
16619
17588
|
const restored = authByEnv[newKey];
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
config.features = await this.readDiskFeatures();
|
|
17589
|
+
g.apiConfig = newApiConfig;
|
|
17590
|
+
g.authByEnv = authByEnv;
|
|
17591
|
+
g.auth = restored;
|
|
16624
17592
|
await this.save();
|
|
16625
17593
|
return {
|
|
16626
17594
|
url: newUrl,
|
|
@@ -16708,40 +17676,48 @@ var ConfigStore = class {
|
|
|
16708
17676
|
* Persists to global config
|
|
16709
17677
|
*/
|
|
16710
17678
|
async addDirectory(dirPath) {
|
|
16711
|
-
|
|
16712
|
-
|
|
17679
|
+
await this.load();
|
|
17680
|
+
const g = this.globalConfig;
|
|
17681
|
+
if (!g.additionalDirectories) g.additionalDirectories = [];
|
|
16713
17682
|
const resolvedPath = path.resolve(dirPath);
|
|
16714
|
-
if (!
|
|
16715
|
-
|
|
16716
|
-
await this.save(
|
|
17683
|
+
if (!g.additionalDirectories.includes(resolvedPath)) {
|
|
17684
|
+
g.additionalDirectories.push(resolvedPath);
|
|
17685
|
+
await this.save();
|
|
16717
17686
|
}
|
|
16718
17687
|
}
|
|
16719
17688
|
/**
|
|
16720
17689
|
* Remove a directory from the allowed directories list
|
|
16721
17690
|
*/
|
|
16722
17691
|
async removeDirectory(dirPath) {
|
|
16723
|
-
|
|
16724
|
-
|
|
17692
|
+
await this.load();
|
|
17693
|
+
const g = this.globalConfig;
|
|
17694
|
+
if (g.additionalDirectories) {
|
|
16725
17695
|
const resolvedPath = path.resolve(dirPath);
|
|
16726
|
-
|
|
16727
|
-
await this.save(
|
|
17696
|
+
g.additionalDirectories = g.additionalDirectories.filter((d) => path.resolve(d) !== resolvedPath);
|
|
17697
|
+
await this.save();
|
|
16728
17698
|
}
|
|
16729
17699
|
}
|
|
16730
17700
|
/**
|
|
16731
|
-
* Get all additional directories (
|
|
16732
|
-
* Returns resolved absolute paths
|
|
17701
|
+
* Get all additional directories (global config + trusted-project config).
|
|
17702
|
+
* Returns resolved absolute paths. Project-declared directories are included
|
|
17703
|
+
* ONLY when the project is trusted, and each must resolve inside the project
|
|
17704
|
+
* root (a repo cannot widen file access beyond its own tree).
|
|
16733
17705
|
*/
|
|
16734
17706
|
async getAdditionalDirectories() {
|
|
16735
|
-
|
|
17707
|
+
await this.load();
|
|
17708
|
+
const g = this.globalConfig;
|
|
16736
17709
|
const dirs = /* @__PURE__ */ new Set();
|
|
16737
|
-
if (
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
17710
|
+
if (g.additionalDirectories) for (const dir of g.additionalDirectories) dirs.add(path.resolve(dir));
|
|
17711
|
+
if (this.projectTrusted && this.rawProjectConfig?.additionalDirectories) {
|
|
17712
|
+
const projectRoot = this.projectRealPath || this.projectConfigDir;
|
|
17713
|
+
if (projectRoot) for (const dir of this.rawProjectConfig.additionalDirectories) {
|
|
17714
|
+
const real = await safeRealpath(path.resolve(projectRoot, dir));
|
|
17715
|
+
if (real && isWithin(projectRoot, real)) dirs.add(real);
|
|
17716
|
+
else logger.warn(`Ignoring project additionalDirectory outside project root: ${dir}`);
|
|
17717
|
+
}
|
|
16742
17718
|
}
|
|
16743
17719
|
return Array.from(dirs);
|
|
16744
17720
|
}
|
|
16745
17721
|
};
|
|
16746
17722
|
//#endregion
|
|
16747
|
-
export {
|
|
17723
|
+
export { withRetry as $, NotFoundError as A, WORK_ITEM_STATUSES as B, CREDIT_DEDUCT_TRANSACTION_TYPES as C, MODEL_INFO_FIELD_GROUP_OF as D, LOCATION_MAP_LANGUAGE as E, REVIEW_GATE_STATUS_VALUES as F, googleMapsSearchUrl as G, escapeThinkMarkers as H, SEARCH_RESULT_CARDS_LANGUAGE as I, isRetryableError as J, isEarlyStop as K, SUBQUEST_STATUS_VALUES as L, OpenAIEmbeddingModel as M, PROMPT_TEXT_MAX as N, McpServerName as O, PermissionDeniedError as P, signImageUrl as Q, SupportedFabFileMimeTypes as R, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as S, DEGENERATE_FINISH_REASON as T, getMcpProviderMetadata as U, createThinkMarkerEscaper as V, getQuestErrorCode as W, obfuscateApiKey as X, isUserInitiatedAbort as Y, secureParameters as Z, AGENT_QUEST_ID as _, canTrustTool as a, ApiKeyType as b, SESSION_ID_PATTERN as c, LOCAL_DEV_URL as d, ACTOR_COLOR_SLOTS as et, getCreditsUrl as f, resolveApiEndpoint as g, requireApiUrl as h, loadContextFiles as i, selfClaimedActorKindSchema as it, OllamaEmbeddingModel as j, ModelBackend as k, isValidSessionId as l, parseApiUrl as m, logger as n, actorKindMarker as nt, getToolCategory as o, getEnvironmentName as p, isPlaceholderImageSigningSecret as q, extractCompactInstructions as r, actorKindSchema as rt, isReadOnlyTool as s, ConfigStore as t, actorColorIndex as tt, ApiEndpointUnconfiguredError as u, AGENT_QUEST_MANIFEST as v, ChatModels as w, BedrockEmbeddingModel as x, AGENT_QUEST_MCP_URI as y, VoyageAIEmbeddingModel as z };
|