@sentry/junior 0.203.0 → 0.205.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-D4nu8MQV.js +17521 -0
- package/dist/agent-hooks-DXHpTJ4m.js +4042 -0
- package/dist/agent-runner-C04colea.js +678 -0
- package/dist/api/schema.d.ts +2736 -2639
- package/dist/api/schema.js +4 -169
- package/dist/api.d.ts +23 -17
- package/dist/api.js +3881 -5326
- package/dist/app-Cb776lL8.d.ts +247 -0
- package/dist/app.d.ts +4 -520
- package/dist/app.js +9524 -11875
- package/dist/auth-pause-state-C2P3guS1.js +168 -0
- package/dist/catalog-runtime-CXw3asJD.js +7 -0
- package/dist/checkpoint-CrViJ8Ps.js +2450 -0
- package/dist/cli/chat.d.ts +20 -0
- package/dist/cli/chat.js +229 -336
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +513 -806
- package/dist/cli/env.d.ts +13 -0
- package/dist/cli/env.js +63 -12
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +90 -159
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +43 -64
- package/dist/cli/plugins.d.ts +13 -0
- package/dist/cli/plugins.js +148 -227
- package/dist/cli/run.d.ts +16 -0
- package/dist/cli/run.js +52 -8
- package/dist/cli/snapshot-create.d.ts +4 -0
- package/dist/cli/snapshot-create.js +71 -125
- package/dist/cli/upgrade.d.ts +15 -0
- package/dist/cli/upgrade.js +232 -335
- package/dist/config-BOtJQ7Pc.js +941 -0
- package/dist/context-B45_WqVX.js +298 -0
- package/dist/conversation-privacy-BZRA6Pes.js +182 -0
- package/dist/conversation-work-C6y3T5Hx.js +51 -0
- package/dist/credential-sync-Bep34IB_.js +2661 -0
- package/dist/db-BeyXKJOI.d.ts +5 -0
- package/dist/destinations-BeBiJeEM.js +29 -0
- package/dist/execution-stats-B6lgWxCK.js +373 -0
- package/dist/executor-BlUhFL7_.js +721 -0
- package/dist/experimental-CIXf7Uye.js +46 -0
- package/dist/history-ClE4toxe.d.ts +32 -0
- package/dist/history-CqTuSAdI.js +441 -0
- package/dist/instrumentation.d.ts +3 -3
- package/dist/instrumentation.js +130 -176
- package/dist/job-queue-DLMqIK74.js +63 -0
- package/dist/logging-BMJzdDlP.js +1210 -0
- package/dist/model-profile-CZmVoGdX.js +49 -0
- package/dist/nitro.d.ts +31 -36
- package/dist/nitro.js +275 -456
- package/dist/oauth-callback-server-WwG5_Zgm.js +130 -0
- package/dist/oauth-relay-CdCnu80W.js +62 -0
- package/dist/package-discovery-7a9YqLhE.js +455 -0
- package/dist/pending-auth-CUbXq06G.js +185 -0
- package/dist/plugin-module-BKGOXca2.js +81 -0
- package/dist/plugins-BI9A1vKA.d.ts +95 -0
- package/dist/plugins-egKWjgue.js +99 -0
- package/dist/private-trace-result-BLvBjy_e.js +16 -0
- package/dist/profile-T0tXE4zu.js +861 -0
- package/dist/registry-BA0__8ai.js +1419 -0
- package/dist/reporting-schema-DosTN__Q.js +105 -0
- package/dist/resolve-bvD1ORdv.js +546 -0
- package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
- package/dist/runner-BXYxfIUL.js +392 -0
- package/dist/sandbox-egress-signals-ngEPjSEW.js +46 -0
- package/dist/schema-Rn8-XEaN.js +1140 -0
- package/dist/sentry-CD3SdJl-.js +26 -0
- package/dist/skills-DJOlYdBs.js +316 -0
- package/dist/spawn-h0R1bTEo.js +29 -0
- package/dist/stats-3YZn-Swi.js +2648 -0
- package/dist/store-C5qxF2ac.d.ts +208 -0
- package/dist/task-queue-CoXwd2eq.js +136 -0
- package/dist/task-runner-B2_mJZj0.js +639 -0
- package/dist/thread-state-CqZ1m-es.js +431 -0
- package/dist/turn-execution-fI6PZ0iI.js +47 -0
- package/dist/turn-lifecycle-CcnwrOcp.js +54 -0
- package/dist/usage-B5SfMb3H.js +296 -0
- package/dist/usage-schema-CKaDkH_j.js +20 -0
- package/dist/validation-DfWcvhuS.js +40 -0
- package/dist/vercel-queue-DIkVcfK4.js +167 -0
- package/dist/vercel.d.ts +6 -6
- package/dist/vercel.js +14 -6
- package/dist/version-CnXZ614E.d.ts +5 -0
- package/dist/version-oD-oPrbO.js +8 -0
- package/dist/version.d.ts +2 -4
- package/dist/version.js +2 -6
- package/dist/web-authorization-Crbzlmzo.js +76 -0
- package/dist/web-input-BOuq3oKK.js +494 -0
- package/dist/work-B91LQKHr.js +509 -0
- package/dist/worker-BcRRTe6k.js +625 -0
- package/package.json +9 -9
- package/dist/agent-hooks-GB2VPTNA.js +0 -51
- package/dist/catalog-runtime-UBLIWD3O.js +0 -14
- package/dist/chunk-2LYHWMR4.js +0 -46
- package/dist/chunk-352OWAZH.js +0 -320
- package/dist/chunk-3Y47VMHV.js +0 -6
- package/dist/chunk-3YJSKOWF.js +0 -164
- package/dist/chunk-4KSSKTQ5.js +0 -229
- package/dist/chunk-4UO6FK4G.js +0 -64
- package/dist/chunk-5AHX53NQ.js +0 -717
- package/dist/chunk-5J3E5FWA.js +0 -700
- package/dist/chunk-5PDAG6O3.js +0 -206
- package/dist/chunk-6Y5B5PEX.js +0 -80
- package/dist/chunk-7GWQ5S5R.js +0 -2086
- package/dist/chunk-7H2VZTWN.js +0 -103
- package/dist/chunk-7NL7T6VU.js +0 -406
- package/dist/chunk-A5L27LE4.js +0 -387
- package/dist/chunk-ADIAWL34.js +0 -81
- package/dist/chunk-ASTEMBKJ.js +0 -519
- package/dist/chunk-CEMMLV5L.js +0 -98
- package/dist/chunk-CM5HNV2W.js +0 -131
- package/dist/chunk-CRTRBBRI.js +0 -105
- package/dist/chunk-DA2KGZZI.js +0 -13
- package/dist/chunk-DIB4GX3G.js +0 -8
- package/dist/chunk-DKIJ2RP5.js +0 -68
- package/dist/chunk-DWPN6SCH.js +0 -172
- package/dist/chunk-EDKUFGPL.js +0 -64
- package/dist/chunk-ENY6SKIU.js +0 -1228
- package/dist/chunk-F4MGCZOX.js +0 -2943
- package/dist/chunk-FA6POGLH.js +0 -70
- package/dist/chunk-FSUR7GTG.js +0 -20475
- package/dist/chunk-GKEOHJL5.js +0 -1856
- package/dist/chunk-GMCB6W5Y.js +0 -333
- package/dist/chunk-IE2VPG7O.js +0 -1135
- package/dist/chunk-IZ5B5IHQ.js +0 -3656
- package/dist/chunk-JAL3CH4G.js +0 -27
- package/dist/chunk-KIIEUXD3.js +0 -1764
- package/dist/chunk-KXU4NIL7.js +0 -692
- package/dist/chunk-MTM5IFCY.js +0 -1190
- package/dist/chunk-N7TMSOZH.js +0 -235
- package/dist/chunk-NCEBK6PH.js +0 -36
- package/dist/chunk-NHJU2ZAL.js +0 -56
- package/dist/chunk-P6KTRNVC.js +0 -321
- package/dist/chunk-PDO5BLNM.js +0 -22
- package/dist/chunk-PDRNLLFT.js +0 -3295
- package/dist/chunk-PEQXYSSE.js +0 -45
- package/dist/chunk-PKEX2AQX.js +0 -198
- package/dist/chunk-PSJCGIGO.js +0 -1163
- package/dist/chunk-PZUSUL55.js +0 -506
- package/dist/chunk-R45M4TUS.js +0 -37
- package/dist/chunk-RC745GGZ.js +0 -122
- package/dist/chunk-SP3QKZXJ.js +0 -820
- package/dist/chunk-TXKUKMVO.js +0 -417
- package/dist/chunk-UE4GPWSD.js +0 -931
- package/dist/chunk-UIE3R5XU.js +0 -17
- package/dist/chunk-UXPG6ZIN.js +0 -84
- package/dist/chunk-VFUK3X5B.js +0 -50
- package/dist/chunk-VNMJWREK.js +0 -787
- package/dist/chunk-VZ7CSOX2.js +0 -706
- package/dist/chunk-WCNQJKFT.js +0 -10
- package/dist/chunk-WDZR3QQM.js +0 -135
- package/dist/chunk-WLYVJCHV.js +0 -1433
- package/dist/chunk-WS7NAFTM.js +0 -1520
- package/dist/chunk-X6GDDWQH.js +0 -60
- package/dist/chunk-XPDEYNQI.js +0 -102
- package/dist/chunk-XRW44VGM.js +0 -58
- package/dist/chunk-Y2CM7HXH.js +0 -111
- package/dist/chunk-Y7X25LFY.js +0 -15
- package/dist/conversation-work-VIOQ5ZF3.js +0 -54
- package/dist/db-NGDJV7YH.js +0 -26
- package/dist/discovery-DWVDM7UK.js +0 -44
- package/dist/experimental-P5FZM4CU.js +0 -12
- package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
- package/dist/oauth-relay-5XPVRPYB.js +0 -8
- package/dist/plugins-YTYJMMJF.js +0 -14
- package/dist/runner-ORTNSB2Q.js +0 -522
- package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
- package/dist/sentry-MB3NL3IR.js +0 -42
- package/dist/skills-H376WQOH.js +0 -27
- package/dist/spawn-T43WHLOS.js +0 -42
- package/dist/task-runner-LBZWF32V.js +0 -36
- package/dist/validation-REJLRKI6.js +0 -17
- package/dist/work-3KJ3RE76.js +0 -51
- package/dist/worker-QFEBU72E.js +0 -24
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
import { C as withSpan, f as logWarn, h as serializeGenAiAttribute, i as extractGenAiUsageAttributes, p as normalizeGenAiFinishReason, u as logException } from "./logging-BMJzdDlP.js";
|
|
2
|
+
import { a as toCanonicalInputMessage, n as getCurrentConversationPrivacy, o as toCanonicalOutputMessage, s as toGenAiMessagesTraceAttributes } from "./conversation-privacy-BZRA6Pes.js";
|
|
3
|
+
import { c as TURN_REASONING_LEVELS, l as parseTurnReasoningLevel, o as modelProfileSchema } from "./model-profile-CZmVoGdX.js";
|
|
4
|
+
import { calculateCost, completeSimple, getEnvApiKey, getModel, getModels, registerApiProvider } from "@earendil-works/pi-ai/compat";
|
|
5
|
+
import { stream as streamAnthropic, streamSimple as streamSimpleAnthropic } from "@earendil-works/pi-ai/api/anthropic-messages";
|
|
6
|
+
import { createGatewayProvider } from "@ai-sdk/gateway";
|
|
7
|
+
import { getVercelOidcToken } from "@vercel/oidc";
|
|
8
|
+
import { NoObjectGeneratedError, embedMany, generateObject } from "ai";
|
|
9
|
+
//#region src/chat/optional-string.ts
|
|
10
|
+
/**
|
|
11
|
+
* Normalize optional env-style strings so blank values do not masquerade as
|
|
12
|
+
* configured inputs.
|
|
13
|
+
*/
|
|
14
|
+
function toOptionalTrimmed(value) {
|
|
15
|
+
if (!value) return;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/chat/pi/gateway-auth.ts
|
|
21
|
+
/**
|
|
22
|
+
* AI Gateway credential resolution for every Junior model path.
|
|
23
|
+
*
|
|
24
|
+
* Prefer Vercel project OIDC so gateway usage attributes to the deploying
|
|
25
|
+
* project. Fall back to AI_GATEWAY_API_KEY for local/CI and non-Vercel hosts.
|
|
26
|
+
* Never cache resolved tokens: runtime OIDC rotates on each invocation.
|
|
27
|
+
*/
|
|
28
|
+
const MISSING_GATEWAY_CREDENTIALS_ERROR = "Missing AI gateway credentials (enable Vercel OIDC or set AI_GATEWAY_API_KEY)";
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the preferred AI Gateway bearer credential.
|
|
31
|
+
*
|
|
32
|
+
* Order:
|
|
33
|
+
* 1. Vercel OIDC via `@vercel/oidc` (request header `x-vercel-oidc-token`,
|
|
34
|
+
* then `VERCEL_OIDC_TOKEN`, with local refresh when needed)
|
|
35
|
+
* 2. Explicit `AI_GATEWAY_API_KEY` / pi-ai env mapping for vercel-ai-gateway
|
|
36
|
+
*/
|
|
37
|
+
async function resolveGatewayCredential() {
|
|
38
|
+
try {
|
|
39
|
+
const oidcToken = toOptionalTrimmed(await getVercelOidcToken());
|
|
40
|
+
if (oidcToken) return {
|
|
41
|
+
mode: "oidc",
|
|
42
|
+
token: oidcToken
|
|
43
|
+
};
|
|
44
|
+
} catch {}
|
|
45
|
+
const apiKey = toOptionalTrimmed(getEnvApiKey("vercel-ai-gateway"));
|
|
46
|
+
if (apiKey) return {
|
|
47
|
+
mode: "api_key",
|
|
48
|
+
token: apiKey
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Resolve the bearer token string for Authorization headers and Pi getApiKey. */
|
|
52
|
+
async function getGatewayApiKey() {
|
|
53
|
+
return (await resolveGatewayCredential())?.token;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/chat/services/provider-error.ts
|
|
57
|
+
/** Structured failure produced at an AI provider boundary. */
|
|
58
|
+
var ProviderError = class extends Error {
|
|
59
|
+
code = "provider_error";
|
|
60
|
+
kind;
|
|
61
|
+
modelId;
|
|
62
|
+
retryable;
|
|
63
|
+
retryAfterMs;
|
|
64
|
+
status;
|
|
65
|
+
constructor(details) {
|
|
66
|
+
super(`AI provider error: ${details.kind}`, details.cause !== void 0 ? { cause: details.cause } : {});
|
|
67
|
+
this.name = "ProviderError";
|
|
68
|
+
this.kind = details.kind;
|
|
69
|
+
this.modelId = details.modelId;
|
|
70
|
+
this.retryable = details.retryable;
|
|
71
|
+
this.retryAfterMs = details.retryAfterMs;
|
|
72
|
+
this.status = details.status;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
76
|
+
"ECONNREFUSED",
|
|
77
|
+
"ECONNRESET",
|
|
78
|
+
"ENOTFOUND",
|
|
79
|
+
"EAI_AGAIN"
|
|
80
|
+
]);
|
|
81
|
+
const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
82
|
+
"ETIMEDOUT",
|
|
83
|
+
"ECONNABORTED",
|
|
84
|
+
"ESOCKETTIMEDOUT"
|
|
85
|
+
]);
|
|
86
|
+
const CONTENT_POLICY_PATTERN = /\b(?:content|safety)[ _-]?policy\b|\b(?:content|safety) (?:filter|violation)\b|\bmoderation (?:blocked|rejected|refused)\b/i;
|
|
87
|
+
const QUOTA_PATTERN = /insufficient.?quota|quota exceeded|usage limit|available balance|out of budget|billing (?:limit|quota|error)|payment required/i;
|
|
88
|
+
const AUTH_PATTERN = /unauthenticated|invalid.?api.?key|no api key|authentication (?:failed|error|required)|(?:invalid|missing|expired|revoked).{0,24}\bcredentials?\b|\bcredentials?\b.{0,24}(?:invalid|missing|expired|revoked)|\bno\b.{0,16}\bcredentials?\b/i;
|
|
89
|
+
const PERMISSION_PATTERN = /permission denied|forbidden|not authorized|authorization (?:failed|required|denied)/i;
|
|
90
|
+
const INVALID_REQUEST_PATTERN = /context.?length|context.?window|validation (?:error|failed)|bad request|unsupported model|invalid model|unknown (?:ai gateway )?model|mismatched api/i;
|
|
91
|
+
const CAPACITY_PATTERN = /overloaded|at capacity|capacity exceeded/i;
|
|
92
|
+
const TIMEOUT_PATTERN = /timed? out|timeout|gateway timeout/i;
|
|
93
|
+
const NETWORK_PATTERN = /network.?error|connection (?:error|refused|lost|closed)|fetch failed|socket (?:hang up|closed)|stream ended before/i;
|
|
94
|
+
const SERVER_PATTERN = /service(?: temporarily)?[ _-]?unavailable|server.?error|internal.?error|bad gateway|upstream (?:request )?failed/i;
|
|
95
|
+
const TERMINAL_KINDS = /* @__PURE__ */ new Set([
|
|
96
|
+
"auth",
|
|
97
|
+
"permission",
|
|
98
|
+
"invalid_request",
|
|
99
|
+
"invalid_response",
|
|
100
|
+
"quota",
|
|
101
|
+
"content_policy"
|
|
102
|
+
]);
|
|
103
|
+
const RETRYABLE_KINDS = /* @__PURE__ */ new Set([
|
|
104
|
+
"rate_limit",
|
|
105
|
+
"capacity",
|
|
106
|
+
"timeout",
|
|
107
|
+
"network",
|
|
108
|
+
"server"
|
|
109
|
+
]);
|
|
110
|
+
function providerMessage(error) {
|
|
111
|
+
return (error instanceof Error ? error.message : String(error)).trim();
|
|
112
|
+
}
|
|
113
|
+
const PROVIDER_ERROR_SUMMARY_MAX_CHARS = 240;
|
|
114
|
+
/** Short provider-boundary text for telemetry. Drop trailing JSON bodies. */
|
|
115
|
+
function summarizeProviderErrorMessage(message) {
|
|
116
|
+
const normalized = message.trim().replace(/\s+/g, " ");
|
|
117
|
+
if (!normalized) return void 0;
|
|
118
|
+
const jsonStart = normalized.indexOf("{");
|
|
119
|
+
const summary = (jsonStart >= 0 ? normalized.slice(0, jsonStart) : normalized).trim();
|
|
120
|
+
if (!summary) return void 0;
|
|
121
|
+
return summary.length > PROVIDER_ERROR_SUMMARY_MAX_CHARS ? `${summary.slice(0, PROVIDER_ERROR_SUMMARY_MAX_CHARS)}...` : summary;
|
|
122
|
+
}
|
|
123
|
+
function extractTransportKind(error, depth = 0) {
|
|
124
|
+
if (!(error instanceof Error) || depth > 4) return void 0;
|
|
125
|
+
const providerError = error;
|
|
126
|
+
const code = typeof providerError.code === "string" ? providerError.code.toUpperCase() : void 0;
|
|
127
|
+
if (code && TIMEOUT_ERROR_CODES.has(code)) return "timeout";
|
|
128
|
+
if (code && NETWORK_ERROR_CODES.has(code)) return "network";
|
|
129
|
+
return extractTransportKind(providerError.cause, depth + 1);
|
|
130
|
+
}
|
|
131
|
+
function extractStatus(error, message) {
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
const providerError = error;
|
|
134
|
+
if (typeof providerError.status === "number") return providerError.status;
|
|
135
|
+
if (typeof providerError.statusCode === "number") return providerError.statusCode;
|
|
136
|
+
}
|
|
137
|
+
const match = message.match(/^(?:Error:\s*)?([45]\d\d)\b|\bstatus(?:Code)?["'=:\s]+([45]\d\d)\b/i);
|
|
138
|
+
const status = match?.[1] ?? match?.[2];
|
|
139
|
+
return status ? Number(status) : void 0;
|
|
140
|
+
}
|
|
141
|
+
function extractRetryAfterMs(error, message) {
|
|
142
|
+
const headers = error instanceof Error ? error.responseHeaders ?? error.response?.headers : void 0;
|
|
143
|
+
const retryAfter = headers instanceof Headers ? headers.get("retry-after") : headers && typeof headers === "object" ? Object.entries(headers).find(([name]) => name.toLowerCase() === "retry-after")?.[1] : void 0;
|
|
144
|
+
const raw = typeof retryAfter === "string" || typeof retryAfter === "number" ? String(retryAfter) : message.match(/"retry-after"\s*:\s*"?([^",}]+)"?/i)?.[1];
|
|
145
|
+
if (!raw) return void 0;
|
|
146
|
+
const seconds = Number(raw);
|
|
147
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
148
|
+
const date = Date.parse(raw);
|
|
149
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
150
|
+
}
|
|
151
|
+
/** Classify stable provider signals without copying Pi's retry vocabulary. */
|
|
152
|
+
function classifyProviderError(status, message, transportKind) {
|
|
153
|
+
if (CONTENT_POLICY_PATTERN.test(message)) return "content_policy";
|
|
154
|
+
if (QUOTA_PATTERN.test(message)) return "quota";
|
|
155
|
+
if (status === 401 || AUTH_PATTERN.test(message)) return "auth";
|
|
156
|
+
if (status === 403 || PERMISSION_PATTERN.test(message)) return "permission";
|
|
157
|
+
if (INVALID_REQUEST_PATTERN.test(message)) return "invalid_request";
|
|
158
|
+
if (status === 429 || /rate.?limit|too many requests/i.test(message)) return "rate_limit";
|
|
159
|
+
if (status === 408) return "timeout";
|
|
160
|
+
if (CAPACITY_PATTERN.test(message)) return "capacity";
|
|
161
|
+
if (transportKind === "timeout" || TIMEOUT_PATTERN.test(message)) return "timeout";
|
|
162
|
+
if (transportKind === "network" || NETWORK_PATTERN.test(message)) return "network";
|
|
163
|
+
if (status !== void 0 && status >= 500 || SERVER_PATTERN.test(message)) return "server";
|
|
164
|
+
if (status !== void 0 && status >= 400) return "invalid_request";
|
|
165
|
+
return "unknown";
|
|
166
|
+
}
|
|
167
|
+
/** Normalize SDK, Gateway, and Pi failures into one provider error contract. */
|
|
168
|
+
function createProviderError(error, options = {}) {
|
|
169
|
+
if (error instanceof ProviderError) return error;
|
|
170
|
+
const message = providerMessage(error);
|
|
171
|
+
const status = extractStatus(error, message);
|
|
172
|
+
const kind = options.kind ?? classifyProviderError(status, message, extractTransportKind(error));
|
|
173
|
+
const retryable = !TERMINAL_KINDS.has(kind) && (options.retryable ?? RETRYABLE_KINDS.has(kind));
|
|
174
|
+
return new ProviderError({
|
|
175
|
+
cause: error,
|
|
176
|
+
kind,
|
|
177
|
+
modelId: options.modelId,
|
|
178
|
+
retryable,
|
|
179
|
+
retryAfterMs: extractRetryAfterMs(error, message),
|
|
180
|
+
status
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Return whether a provider-boundary error should be retried. */
|
|
184
|
+
function isProviderRetryError(error) {
|
|
185
|
+
return error instanceof ProviderError && error.retryable;
|
|
186
|
+
}
|
|
187
|
+
/** Find the provider failure preserved inside a domain error cause chain. */
|
|
188
|
+
function findProviderError(error) {
|
|
189
|
+
const visited = /* @__PURE__ */ new Set();
|
|
190
|
+
let current = error;
|
|
191
|
+
while (current instanceof Error && !visited.has(current)) {
|
|
192
|
+
if (current instanceof ProviderError) return current;
|
|
193
|
+
visited.add(current);
|
|
194
|
+
current = current.cause;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Return stable, sanitized copy suitable for a terminal user response. */
|
|
198
|
+
function getProviderErrorUserMessage(error) {
|
|
199
|
+
switch (error.kind) {
|
|
200
|
+
case "capacity": return "The selected model is temporarily unavailable because it is at capacity. Please try again shortly.";
|
|
201
|
+
case "rate_limit": {
|
|
202
|
+
const seconds = error.retryAfterMs ? Math.max(1, Math.ceil(error.retryAfterMs / 1e3)) : void 0;
|
|
203
|
+
return seconds ? `The model is rate-limited. Please try again in about ${seconds} seconds.` : "The model is rate-limited. Please try again shortly.";
|
|
204
|
+
}
|
|
205
|
+
case "timeout":
|
|
206
|
+
case "network":
|
|
207
|
+
case "server": return "The model provider had a temporary connection problem. Please try again.";
|
|
208
|
+
case "auth": return "The model provider rejected Junior's credentials. This needs an administrator or configuration fix.";
|
|
209
|
+
case "permission": return "The model provider denied access to this model or request. Ask an administrator to check model access, organization policy, and region availability.";
|
|
210
|
+
case "quota": return "The model provider's usage quota has been exhausted. This needs an administrator or billing configuration fix.";
|
|
211
|
+
case "content_policy": return "The model provider blocked this request under its content policy. Please revise the request and try again.";
|
|
212
|
+
case "invalid_request": return "The model provider rejected this request as invalid. Please revise the request or ask an administrator to check the model configuration.";
|
|
213
|
+
case "invalid_response": return "The model provider returned an invalid response. Please try again.";
|
|
214
|
+
default: return "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Return safe structured fields for terminal failure telemetry. */
|
|
218
|
+
function getProviderErrorAttributes(error) {
|
|
219
|
+
const cause = error.cause;
|
|
220
|
+
const causeMessage = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
|
|
221
|
+
const summary = causeMessage ? summarizeProviderErrorMessage(causeMessage) : void 0;
|
|
222
|
+
return {
|
|
223
|
+
"app.ai.provider_error.kind": error.kind,
|
|
224
|
+
"app.ai.provider_error.retryable": error.retryable,
|
|
225
|
+
...error.status !== void 0 ? { "app.ai.provider_error.status": error.status } : void 0,
|
|
226
|
+
...error.retryAfterMs !== void 0 ? { "app.ai.provider_error.retry_after_ms": error.retryAfterMs } : void 0,
|
|
227
|
+
...summary ? { "app.ai.provider_error.summary": summary } : void 0,
|
|
228
|
+
...error.modelId ? { "gen_ai.request.model": error.modelId } : void 0
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/chat/xml.ts
|
|
233
|
+
function escapeXml(value) {
|
|
234
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
235
|
+
}
|
|
236
|
+
/** Recover text escaped for an internal XML prompt boundary. */
|
|
237
|
+
function unescapeXml(value) {
|
|
238
|
+
return value.replaceAll(""", "\"").replaceAll("'", "'").replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/chat/current-instruction.ts
|
|
242
|
+
const CURRENT_INSTRUCTION_TAG = "current-instruction";
|
|
243
|
+
const CURRENT_INSTRUCTION_OPEN_PREFIX = `<${CURRENT_INSTRUCTION_TAG}`;
|
|
244
|
+
const CURRENT_INSTRUCTION_OPEN_BARE = `<${CURRENT_INSTRUCTION_TAG}>`;
|
|
245
|
+
const CURRENT_INSTRUCTION_OPEN_ATTR_PREFIX = `<${CURRENT_INSTRUCTION_TAG} `;
|
|
246
|
+
const CURRENT_INSTRUCTION_CLOSE = `\n</${CURRENT_INSTRUCTION_TAG}>`;
|
|
247
|
+
function isCurrentInstructionOpeningTag(value) {
|
|
248
|
+
return value === CURRENT_INSTRUCTION_OPEN_BARE || value.startsWith(CURRENT_INSTRUCTION_OPEN_ATTR_PREFIX) && value.endsWith(">");
|
|
249
|
+
}
|
|
250
|
+
function readCurrentInstructionBody(text) {
|
|
251
|
+
const start = text.indexOf(CURRENT_INSTRUCTION_OPEN_PREFIX);
|
|
252
|
+
if (start < 0) return;
|
|
253
|
+
const openingEnd = text.indexOf(">\n", start);
|
|
254
|
+
if (openingEnd < 0) return;
|
|
255
|
+
if (!isCurrentInstructionOpeningTag(text.slice(start, openingEnd + 1))) return;
|
|
256
|
+
const bodyStart = openingEnd + 2;
|
|
257
|
+
const bodyEnd = text.indexOf(CURRENT_INSTRUCTION_CLOSE, bodyStart);
|
|
258
|
+
if (bodyEnd < bodyStart) return;
|
|
259
|
+
return text.slice(bodyStart, bodyEnd);
|
|
260
|
+
}
|
|
261
|
+
/** Render the active user task in a stable prompt boundary. */
|
|
262
|
+
function renderCurrentInstruction(instruction, attrs) {
|
|
263
|
+
const renderedAttrs = [
|
|
264
|
+
attrs?.authorId ? `author_id="${escapeXml(attrs.authorId)}"` : void 0,
|
|
265
|
+
attrs?.authorName ? `author_name="${escapeXml(attrs.authorName)}"` : void 0,
|
|
266
|
+
attrs?.slackTs ? `slack_ts="${escapeXml(attrs.slackTs)}"` : void 0
|
|
267
|
+
].filter((attr) => Boolean(attr)).join(" ");
|
|
268
|
+
return [
|
|
269
|
+
renderedAttrs ? `<${CURRENT_INSTRUCTION_TAG} ${renderedAttrs}>` : `<${CURRENT_INSTRUCTION_TAG}>`,
|
|
270
|
+
escapeXml(instruction),
|
|
271
|
+
`</${CURRENT_INSTRUCTION_TAG}>`
|
|
272
|
+
].join("\n");
|
|
273
|
+
}
|
|
274
|
+
/** Recover display text from the internal current-task prompt boundary. */
|
|
275
|
+
function unwrapCurrentInstruction(text) {
|
|
276
|
+
const body = readCurrentInstructionBody(text);
|
|
277
|
+
return body === void 0 ? void 0 : unescapeXml(body);
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/chat/services/context-compaction-marker.ts
|
|
281
|
+
const COMPACTION_SUMMARY_PREFIX = "Context compaction summary for future Junior turns:";
|
|
282
|
+
const ACTIVE_TURN_COMPACTION_SUMMARY_PREFIX = "Active-turn context checkpoint. If no newer user instruction follows this checkpoint, continue the same unfinished task now using the authoritative instructions above and this summary as internal continuation state. If a newer user instruction follows, treat this checkpoint as prior context and follow that newer instruction. Do not reply with a plan or summary solely because this checkpoint appeared:";
|
|
283
|
+
const MODEL_HANDOFF_SUMMARY_PREFIX = "Model handoff checkpoint. Continue the outstanding request now using this summary as the complete prior context:";
|
|
284
|
+
/** Return whether text is one of Junior's durable compacted-context markers. */
|
|
285
|
+
function isCompactionSummaryText(text) {
|
|
286
|
+
const normalized = (unwrapCurrentInstruction(text) ?? text).trimStart();
|
|
287
|
+
return normalized.startsWith("Context compaction summary for future Junior turns:") || normalized.startsWith("Active-turn context checkpoint. If no newer user instruction follows this checkpoint, continue the same unfinished task now using the authoritative instructions above and this summary as internal continuation state. If a newer user instruction follows, treat this checkpoint as prior context and follow that newer instruction. Do not reply with a plan or summary solely because this checkpoint appeared:") || normalized.startsWith("Model handoff checkpoint. Continue the outstanding request now using this summary as the complete prior context:");
|
|
288
|
+
}
|
|
289
|
+
/** Return whether model-visible history contains a durable compacted context marker. */
|
|
290
|
+
function hasCompactedConversationContext(messages) {
|
|
291
|
+
return messages.some((message) => {
|
|
292
|
+
const content = message.content;
|
|
293
|
+
if (typeof content === "string") return isCompactionSummaryText(content);
|
|
294
|
+
if (!Array.isArray(content)) return false;
|
|
295
|
+
return content.some((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && isCompactionSummaryText(part.text));
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/chat/pi/client.ts
|
|
300
|
+
registerApiProvider({
|
|
301
|
+
api: "anthropic-messages",
|
|
302
|
+
stream: streamAnthropic,
|
|
303
|
+
streamSimple: streamSimpleAnthropic
|
|
304
|
+
});
|
|
305
|
+
const GATEWAY_PROVIDER = "vercel-ai-gateway";
|
|
306
|
+
const GEN_AI_PROVIDER_NAME = GATEWAY_PROVIDER;
|
|
307
|
+
const GEN_AI_SERVER_ADDRESS = "ai-gateway.vercel.sh";
|
|
308
|
+
const GEN_AI_OPERATION_CHAT = "chat";
|
|
309
|
+
const GEN_AI_OPERATION_EMBEDDINGS = "embeddings";
|
|
310
|
+
const EMBEDDING_INPUT_COST_USD_PER_MILLION_TOKENS = {
|
|
311
|
+
"cohere/embed-v4.0": .12,
|
|
312
|
+
"google/gemini-embedding-001": .15,
|
|
313
|
+
"google/text-embedding-005": .025,
|
|
314
|
+
"mistral/mistral-embed": .1,
|
|
315
|
+
"openai/text-embedding-3-large": .13,
|
|
316
|
+
"openai/text-embedding-3-small": .02,
|
|
317
|
+
"openai/text-embedding-ada-002": .1,
|
|
318
|
+
"voyage/voyage-3.5": .06,
|
|
319
|
+
"voyage/voyage-3.5-lite": .02
|
|
320
|
+
};
|
|
321
|
+
function embeddingCostUsd(modelId, inputTokens) {
|
|
322
|
+
const costPerMillionTokens = EMBEDDING_INPUT_COST_USD_PER_MILLION_TOKENS[modelId];
|
|
323
|
+
return costPerMillionTokens === void 0 ? void 0 : inputTokens * costPerMillionTokens / 1e6;
|
|
324
|
+
}
|
|
325
|
+
function extractText(message) {
|
|
326
|
+
return (message.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text ?? "").join("").trim();
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Look up a gateway model by id. Throws `Unknown AI Gateway model id: …` if
|
|
330
|
+
* the id is not in pi-ai's registry — callers at the config boundary can use
|
|
331
|
+
* this to fail fast at startup instead of mid-turn.
|
|
332
|
+
*/
|
|
333
|
+
function resolveGatewayModel(modelId) {
|
|
334
|
+
const matched = getModels(GATEWAY_PROVIDER).find((model) => model.id === modelId);
|
|
335
|
+
if (!matched) throw new Error(`Unknown AI Gateway model id: ${modelId}`);
|
|
336
|
+
return matched;
|
|
337
|
+
}
|
|
338
|
+
/** Execute a direct chat completion inside a dedicated `gen_ai.chat` span. */
|
|
339
|
+
async function completeText(params) {
|
|
340
|
+
const model = resolveGatewayModel(params.modelId);
|
|
341
|
+
const credential = await resolveGatewayCredential();
|
|
342
|
+
const apiKey = credential?.token;
|
|
343
|
+
const authMode = credential?.mode ?? "api_key";
|
|
344
|
+
const effectivePrivacy = getCurrentConversationPrivacy() ?? "private";
|
|
345
|
+
const messageAttributeMode = params.messageAttributeMode ?? (effectivePrivacy === "public" ? "content" : "metadata");
|
|
346
|
+
const requestMessagesAttribute = messageAttributeMode === "content" ? serializeGenAiAttribute(params.messages.map(toCanonicalInputMessage)) : void 0;
|
|
347
|
+
const systemInstructionsAttribute = params.system && messageAttributeMode === "content" ? serializeGenAiAttribute([{
|
|
348
|
+
type: "text",
|
|
349
|
+
content: params.system
|
|
350
|
+
}]) : void 0;
|
|
351
|
+
const baseAttributes = {
|
|
352
|
+
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
|
|
353
|
+
"gen_ai.operation.name": GEN_AI_OPERATION_CHAT,
|
|
354
|
+
"gen_ai.request.model": params.modelId,
|
|
355
|
+
"gen_ai.output.type": "text",
|
|
356
|
+
...params.promptName ? { "gen_ai.prompt.name": params.promptName } : void 0,
|
|
357
|
+
"server.address": GEN_AI_SERVER_ADDRESS,
|
|
358
|
+
"server.port": 443,
|
|
359
|
+
...hasCompactedConversationContext(params.messages) ? { "gen_ai.conversation.compacted": true } : void 0,
|
|
360
|
+
"app.conversation.privacy": effectivePrivacy,
|
|
361
|
+
...params.thinkingLevel ? { "gen_ai.request.reasoning.level": params.thinkingLevel } : void 0,
|
|
362
|
+
...params.temperature !== void 0 ? { "gen_ai.request.temperature": params.temperature } : void 0,
|
|
363
|
+
...params.maxTokens !== void 0 ? { "gen_ai.request.max_tokens": params.maxTokens } : void 0
|
|
364
|
+
};
|
|
365
|
+
const startAttributes = {
|
|
366
|
+
...baseAttributes,
|
|
367
|
+
...toGenAiMessagesTraceAttributes("gen_ai.input", params.messages),
|
|
368
|
+
...params.system ? { "gen_ai.system_instructions.content_chars": params.system.length } : void 0,
|
|
369
|
+
...systemInstructionsAttribute ? { "gen_ai.system_instructions": systemInstructionsAttribute } : void 0,
|
|
370
|
+
...requestMessagesAttribute ? { "gen_ai.input.messages": requestMessagesAttribute } : void 0,
|
|
371
|
+
"gen_ai.provider.auth_mode": authMode
|
|
372
|
+
};
|
|
373
|
+
return withSpan(`${GEN_AI_OPERATION_CHAT} ${params.modelId}`, "gen_ai.chat", logContextFromMetadata(params.modelId, params.metadata), async (setSpanAttributes) => {
|
|
374
|
+
let message;
|
|
375
|
+
try {
|
|
376
|
+
message = await completeSimple(model, {
|
|
377
|
+
systemPrompt: params.system,
|
|
378
|
+
messages: params.messages
|
|
379
|
+
}, {
|
|
380
|
+
...apiKey ? { apiKey } : void 0,
|
|
381
|
+
temperature: params.temperature,
|
|
382
|
+
maxTokens: params.maxTokens,
|
|
383
|
+
reasoning: params.thinkingLevel,
|
|
384
|
+
signal: params.signal,
|
|
385
|
+
metadata: params.metadata
|
|
386
|
+
});
|
|
387
|
+
} catch (error) {
|
|
388
|
+
throw createProviderError(error, { modelId: params.modelId });
|
|
389
|
+
}
|
|
390
|
+
const outputText = extractText(message);
|
|
391
|
+
const outputMessagesAttribute = serializeGenAiAttribute(messageAttributeMode === "content" ? [toCanonicalOutputMessage(message)] : void 0);
|
|
392
|
+
const usageAttributes = extractGenAiUsageAttributes(message);
|
|
393
|
+
setSpanAttributes({
|
|
394
|
+
...baseAttributes,
|
|
395
|
+
...toGenAiMessagesTraceAttributes("gen_ai.output", [{
|
|
396
|
+
role: "assistant",
|
|
397
|
+
content: outputText ? [{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: outputText
|
|
400
|
+
}] : []
|
|
401
|
+
}]),
|
|
402
|
+
...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : void 0,
|
|
403
|
+
...usageAttributes,
|
|
404
|
+
...message.stopReason ? { "gen_ai.response.finish_reasons": [normalizeGenAiFinishReason(message.stopReason)] } : void 0,
|
|
405
|
+
...message.model ? { "gen_ai.response.model": message.model } : void 0
|
|
406
|
+
});
|
|
407
|
+
if (message.stopReason === "error") {
|
|
408
|
+
const providerMessage = message.errorMessage?.trim() || "Unknown provider error";
|
|
409
|
+
logWarn("ai.completion.provider_error", {
|
|
410
|
+
...baseAttributes,
|
|
411
|
+
"exception.message": providerMessage
|
|
412
|
+
});
|
|
413
|
+
throw createProviderError(providerMessage, { modelId: params.modelId });
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
message,
|
|
417
|
+
text: outputText
|
|
418
|
+
};
|
|
419
|
+
}, startAttributes);
|
|
420
|
+
}
|
|
421
|
+
function logContextFromMetadata(modelId, metadata) {
|
|
422
|
+
const conversationId = typeof metadata?.conversationId === "string" ? metadata.conversationId : typeof metadata?.threadId === "string" ? metadata.threadId : void 0;
|
|
423
|
+
const messageConversationId = typeof metadata?.threadId === "string" ? metadata.threadId : void 0;
|
|
424
|
+
const destinationName = typeof metadata?.channelId === "string" ? metadata.channelId : void 0;
|
|
425
|
+
const runId = typeof metadata?.runId === "string" ? metadata.runId : void 0;
|
|
426
|
+
return {
|
|
427
|
+
modelId,
|
|
428
|
+
...conversationId ? { conversationId } : void 0,
|
|
429
|
+
...messageConversationId ? { messageConversationId } : void 0,
|
|
430
|
+
...destinationName ? { destinationName } : void 0,
|
|
431
|
+
...runId ? { runId } : void 0
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function objectCompletionCost(modelId, usage) {
|
|
435
|
+
const cacheRead = usage.inputTokenDetails.cacheReadTokens;
|
|
436
|
+
const cacheWrite = usage.inputTokenDetails.cacheWriteTokens;
|
|
437
|
+
const input = usage.inputTokenDetails.noCacheTokens ?? (usage.inputTokens === void 0 ? void 0 : Math.max(0, usage.inputTokens - (cacheRead ?? 0) - (cacheWrite ?? 0)));
|
|
438
|
+
if (input === void 0 && usage.outputTokens === void 0 && cacheRead === void 0 && cacheWrite === void 0) return;
|
|
439
|
+
return calculateCost(resolveGatewayModel(modelId), {
|
|
440
|
+
input: input ?? 0,
|
|
441
|
+
output: usage.outputTokens ?? 0,
|
|
442
|
+
cacheRead: cacheRead ?? 0,
|
|
443
|
+
cacheWrite: cacheWrite ?? 0,
|
|
444
|
+
...usage.outputTokenDetails.reasoningTokens !== void 0 ? { reasoning: usage.outputTokenDetails.reasoningTokens } : void 0,
|
|
445
|
+
totalTokens: usage.totalTokens ?? (input ?? 0) + (usage.outputTokens ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
446
|
+
cost: {
|
|
447
|
+
input: 0,
|
|
448
|
+
output: 0,
|
|
449
|
+
cacheRead: 0,
|
|
450
|
+
cacheWrite: 0,
|
|
451
|
+
total: 0
|
|
452
|
+
}
|
|
453
|
+
}).total;
|
|
454
|
+
}
|
|
455
|
+
/** Estimate cost without changing the outcome of a successful completion. */
|
|
456
|
+
function bestEffortObjectCompletionCost(modelId, usage) {
|
|
457
|
+
try {
|
|
458
|
+
return objectCompletionCost(modelId, usage);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
logWarn("ai.completion.cost_estimate.failed", {
|
|
461
|
+
"exception.message": error instanceof Error ? error.message : String(error),
|
|
462
|
+
"gen_ai.request.model": modelId
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/** Execute a schema-constrained completion using the AI SDK structured output path. */
|
|
468
|
+
async function completeObject(params) {
|
|
469
|
+
const credential = await resolveGatewayCredential();
|
|
470
|
+
const provider = createGatewayProvider(credential ? { apiKey: credential.token } : {});
|
|
471
|
+
let result;
|
|
472
|
+
try {
|
|
473
|
+
result = await withSpan(`${GEN_AI_OPERATION_CHAT} ${params.modelId}`, "gen_ai.chat", logContextFromMetadata(params.modelId, params.metadata), async (setSpanAttributes) => {
|
|
474
|
+
const result = await generateObject({
|
|
475
|
+
model: provider.chat(params.modelId),
|
|
476
|
+
schema: params.schema,
|
|
477
|
+
prompt: params.prompt,
|
|
478
|
+
...params.system !== void 0 ? { system: params.system } : void 0,
|
|
479
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : void 0,
|
|
480
|
+
...params.maxTokens !== void 0 ? { maxOutputTokens: params.maxTokens } : void 0,
|
|
481
|
+
...params.recordTelemetryPayloads === false ? { experimental_telemetry: {
|
|
482
|
+
isEnabled: true,
|
|
483
|
+
recordInputs: false,
|
|
484
|
+
recordOutputs: false
|
|
485
|
+
} } : void 0,
|
|
486
|
+
...params.signal !== void 0 ? { abortSignal: params.signal } : void 0
|
|
487
|
+
});
|
|
488
|
+
setSpanAttributes({
|
|
489
|
+
"gen_ai.response.finish_reasons": [result.finishReason],
|
|
490
|
+
...extractGenAiUsageAttributes(result.usage)
|
|
491
|
+
});
|
|
492
|
+
return result;
|
|
493
|
+
}, {
|
|
494
|
+
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
|
|
495
|
+
"gen_ai.operation.name": GEN_AI_OPERATION_CHAT,
|
|
496
|
+
"gen_ai.request.model": params.modelId,
|
|
497
|
+
"gen_ai.output.type": "json",
|
|
498
|
+
...params.promptName ? { "gen_ai.prompt.name": params.promptName } : void 0,
|
|
499
|
+
"server.address": GEN_AI_SERVER_ADDRESS,
|
|
500
|
+
"server.port": 443,
|
|
501
|
+
"gen_ai.provider.auth_mode": credential?.mode ?? "api_key",
|
|
502
|
+
...params.thinkingLevel ? { "gen_ai.request.reasoning.level": params.thinkingLevel } : void 0,
|
|
503
|
+
...params.temperature !== void 0 ? { "gen_ai.request.temperature": params.temperature } : void 0,
|
|
504
|
+
...params.maxTokens !== void 0 ? { "gen_ai.request.max_tokens": params.maxTokens } : void 0
|
|
505
|
+
});
|
|
506
|
+
} catch (error) {
|
|
507
|
+
throw createProviderError(error, {
|
|
508
|
+
...NoObjectGeneratedError.isInstance(error) ? { kind: "invalid_response" } : void 0,
|
|
509
|
+
modelId: params.modelId
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const costUsd = bestEffortObjectCompletionCost(params.modelId, result.usage);
|
|
513
|
+
return {
|
|
514
|
+
object: result.object,
|
|
515
|
+
...costUsd !== void 0 ? { costUsd } : void 0
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/** Generate text embeddings through the host-owned AI Gateway provider. */
|
|
519
|
+
async function embedTexts(params) {
|
|
520
|
+
const texts = params.texts.map((text) => text.trim());
|
|
521
|
+
if (texts.length === 0 || texts.some((text) => text.length === 0)) throw new Error("Embedding text is required.");
|
|
522
|
+
const credential = await resolveGatewayCredential();
|
|
523
|
+
const provider = createGatewayProvider(credential ? { apiKey: credential.token } : {});
|
|
524
|
+
try {
|
|
525
|
+
const result = await withSpan(`${GEN_AI_OPERATION_EMBEDDINGS} ${params.modelId}`, "gen_ai.embeddings", logContextFromMetadata(params.modelId, params.metadata), async (setSpanAttributes) => {
|
|
526
|
+
const result = await embedMany({
|
|
527
|
+
model: provider.embeddingModel(params.modelId),
|
|
528
|
+
values: texts,
|
|
529
|
+
...params.signal !== void 0 ? { abortSignal: params.signal } : void 0
|
|
530
|
+
});
|
|
531
|
+
const dimensions = result.embeddings[0]?.length;
|
|
532
|
+
if (result.embeddings.length !== texts.length || !dimensions || !result.embeddings.every((embedding) => embedding.length === dimensions)) throw createProviderError("Embedding provider returned invalid vectors.", {
|
|
533
|
+
kind: "invalid_response",
|
|
534
|
+
modelId: params.modelId
|
|
535
|
+
});
|
|
536
|
+
const costUsd = embeddingCostUsd(params.modelId, result.usage.tokens);
|
|
537
|
+
setSpanAttributes({
|
|
538
|
+
"gen_ai.embeddings.dimension.count": dimensions,
|
|
539
|
+
...extractGenAiUsageAttributes({
|
|
540
|
+
inputTokens: result.usage.tokens,
|
|
541
|
+
...costUsd !== void 0 ? { cost: {
|
|
542
|
+
input: costUsd,
|
|
543
|
+
total: costUsd
|
|
544
|
+
} } : void 0
|
|
545
|
+
})
|
|
546
|
+
});
|
|
547
|
+
return {
|
|
548
|
+
costUsd,
|
|
549
|
+
dimensions,
|
|
550
|
+
result
|
|
551
|
+
};
|
|
552
|
+
}, {
|
|
553
|
+
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
|
|
554
|
+
"gen_ai.operation.name": GEN_AI_OPERATION_EMBEDDINGS,
|
|
555
|
+
"gen_ai.request.model": params.modelId,
|
|
556
|
+
"server.address": GEN_AI_SERVER_ADDRESS,
|
|
557
|
+
"server.port": 443,
|
|
558
|
+
"gen_ai.provider.auth_mode": credential?.mode ?? "api_key"
|
|
559
|
+
});
|
|
560
|
+
return {
|
|
561
|
+
...result.costUsd !== void 0 ? { costUsd: result.costUsd } : void 0,
|
|
562
|
+
dimensions: result.dimensions,
|
|
563
|
+
model: params.modelId,
|
|
564
|
+
provider: GEN_AI_PROVIDER_NAME,
|
|
565
|
+
vectors: result.result.embeddings
|
|
566
|
+
};
|
|
567
|
+
} catch (error) {
|
|
568
|
+
const providerError = createProviderError(error, { modelId: params.modelId });
|
|
569
|
+
if (isProviderRetryError(providerError)) throw providerError;
|
|
570
|
+
logException(providerError, "ai.embeddings.failed", {
|
|
571
|
+
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
|
|
572
|
+
"gen_ai.operation.name": GEN_AI_OPERATION_EMBEDDINGS,
|
|
573
|
+
"gen_ai.request.model": params.modelId
|
|
574
|
+
});
|
|
575
|
+
throw providerError;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/chat/slack/emoji.ts
|
|
580
|
+
const SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
|
|
581
|
+
function normalizeSlackEmojiName(value) {
|
|
582
|
+
const trimmed = value.trim().toLowerCase();
|
|
583
|
+
if (!trimmed) return null;
|
|
584
|
+
const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
|
|
585
|
+
return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/chat/config.ts
|
|
589
|
+
const MIN_AGENT_TURN_TIMEOUT_MS = 1e4;
|
|
590
|
+
const DEFAULT_AGENT_TURN_TIMEOUT_MS = 72e4;
|
|
591
|
+
const MAX_SLICES_PER_TURN = 100;
|
|
592
|
+
/**
|
|
593
|
+
* Max tool calls for one turn, including later execution slices of that turn.
|
|
594
|
+
* Chosen from production dispatch traces: healthy runs cluster well under 50;
|
|
595
|
+
* runaway CI-watch loops land in the hundreds. Raised from 150 after that cap
|
|
596
|
+
* fired on legitimate long-form human turns, not just runaway automation.
|
|
597
|
+
*/
|
|
598
|
+
const MAX_TOOL_CALLS_PER_TURN = 250;
|
|
599
|
+
/**
|
|
600
|
+
* Max consecutive automated turns before event wakes stop until a user message.
|
|
601
|
+
* Resource-event CI watches and event-task loops are the common runaway paths.
|
|
602
|
+
* Sentry telemetry has no direct instrumentation for how long real automated
|
|
603
|
+
* chains run (only failure paths on the notice are logged, and those show zero
|
|
604
|
+
* hits in the last 30d), so this is sized qualitatively: it stays well under
|
|
605
|
+
* the runaway CI-watch-loop range noted for maxToolCallsPerTurn (hundreds of
|
|
606
|
+
* tool calls) while giving multi-phase event-task/scheduled-task work enough
|
|
607
|
+
* headroom to avoid pausing on legitimate longer-running automation.
|
|
608
|
+
*/
|
|
609
|
+
const MAX_CONSECUTIVE_AUTOMATED_TURNS = 25;
|
|
610
|
+
const DEFAULT_FUNCTION_MAX_DURATION_SECONDS = 300;
|
|
611
|
+
const DEFAULT_SLACK_SLASH_COMMAND = "/jr";
|
|
612
|
+
const DEFAULT_PROCESSING_REACTION_EMOJI = "eyes";
|
|
613
|
+
const DEFAULT_COMPLETED_REACTION_EMOJI = "white_check_mark";
|
|
614
|
+
const DEFAULT_MODEL_CONTEXT_WINDOW_TOKENS = 4e5;
|
|
615
|
+
const DEFAULT_SQL_STATEMENT_TIMEOUT_MS = 3e4;
|
|
616
|
+
const DEFAULT_ASSISTANT_LOADING_MESSAGES = [
|
|
617
|
+
"Consulting the orb",
|
|
618
|
+
"Bribing the gremlins",
|
|
619
|
+
"Shuffling the papers dramatically",
|
|
620
|
+
"Summoning the right stack trace",
|
|
621
|
+
"Negotiating with the mutex",
|
|
622
|
+
"Poking the internet with a stick",
|
|
623
|
+
"Asking the docs nicely",
|
|
624
|
+
"Searching for the least cursed path",
|
|
625
|
+
"Pretending this was obvious",
|
|
626
|
+
"Waking up the test suite",
|
|
627
|
+
"Untangling the spaghetti carefully",
|
|
628
|
+
"Rattling the command line"
|
|
629
|
+
];
|
|
630
|
+
function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
|
|
631
|
+
const value = Number.parseInt(rawValue ?? "", 10);
|
|
632
|
+
if (Number.isNaN(value)) return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
|
|
633
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
|
|
634
|
+
}
|
|
635
|
+
function resolveFunctionMaxDurationSeconds(functionMaxDurationSeconds) {
|
|
636
|
+
return functionMaxDurationSeconds ?? DEFAULT_FUNCTION_MAX_DURATION_SECONDS;
|
|
637
|
+
}
|
|
638
|
+
function resolveConversationWorkSoftYieldAfterMs(functionMaxDurationSeconds) {
|
|
639
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, (functionMaxDurationSeconds - 40) * 1e3);
|
|
640
|
+
}
|
|
641
|
+
function resolveMaxTurnTimeoutMs(functionMaxDurationSeconds) {
|
|
642
|
+
const budgetSeconds = functionMaxDurationSeconds - 20;
|
|
643
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
|
|
644
|
+
}
|
|
645
|
+
function parseLoadingMessages(rawValue) {
|
|
646
|
+
const trimmed = rawValue?.trim();
|
|
647
|
+
if (!trimmed) return [...DEFAULT_ASSISTANT_LOADING_MESSAGES];
|
|
648
|
+
let parsed;
|
|
649
|
+
try {
|
|
650
|
+
parsed = JSON.parse(trimmed);
|
|
651
|
+
} catch {
|
|
652
|
+
throw new Error("JUNIOR_LOADING_MESSAGES must be a JSON array of strings");
|
|
653
|
+
}
|
|
654
|
+
if (!Array.isArray(parsed)) throw new Error("JUNIOR_LOADING_MESSAGES must be a JSON array of strings");
|
|
655
|
+
return parsed.map((value, index) => {
|
|
656
|
+
if (typeof value !== "string") throw new Error(`JUNIOR_LOADING_MESSAGES[${index}] must be a string`);
|
|
657
|
+
return value.trim();
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function parseOptionalPositiveInteger(envName, rawValue) {
|
|
661
|
+
const trimmed = toOptionalTrimmed(rawValue);
|
|
662
|
+
if (trimmed === void 0) return;
|
|
663
|
+
const value = Number.parseInt(trimmed, 10);
|
|
664
|
+
if (!Number.isSafeInteger(value) || value <= 0 || String(value) !== trimmed) throw new Error(`${envName} must be a positive integer`);
|
|
665
|
+
return value;
|
|
666
|
+
}
|
|
667
|
+
function parseSlashCommand(rawValue) {
|
|
668
|
+
const command = toOptionalTrimmed(rawValue) ?? DEFAULT_SLACK_SLASH_COMMAND;
|
|
669
|
+
if (!command.startsWith("/") || /\s/.test(command)) throw new Error("JUNIOR_SLASH_COMMAND must start with / and contain no whitespace");
|
|
670
|
+
return command;
|
|
671
|
+
}
|
|
672
|
+
function parseConversationWorkEnabled(rawValue) {
|
|
673
|
+
const value = toOptionalTrimmed(rawValue)?.toLowerCase();
|
|
674
|
+
if (value === void 0 || value === "true") return true;
|
|
675
|
+
if (value === "false") return false;
|
|
676
|
+
throw new Error("JUNIOR_CONVERSATION_WORK_ENABLED must be true or false");
|
|
677
|
+
}
|
|
678
|
+
function parseCrossActorMidRunMode(rawValue) {
|
|
679
|
+
const value = toOptionalTrimmed(rawValue) ?? "follow_up";
|
|
680
|
+
if (value === "follow_up" || value === "steer") return value;
|
|
681
|
+
throw new Error("JUNIOR_CROSS_ACTOR_MID_RUN_MODE must be follow_up or steer");
|
|
682
|
+
}
|
|
683
|
+
const DEFAULT_MODEL_ID = getModel("vercel-ai-gateway", "xai/grok-4.5").id;
|
|
684
|
+
const DEFAULT_FAST_MODEL_ID = getModel("vercel-ai-gateway", "anthropic/claude-haiku-4.5").id;
|
|
685
|
+
const DEFAULT_GUARDIAN_MODEL_ID = getModel("vercel-ai-gateway", "openai/gpt-5.6-luna").id;
|
|
686
|
+
const DEFAULT_HANDOFF_MODEL_ID = getModel("vercel-ai-gateway", "openai/gpt-5.6-sol").id;
|
|
687
|
+
const DEFAULT_WEB_SEARCH_MODEL_ID = getModel("vercel-ai-gateway", "openai/gpt-5.4").id;
|
|
688
|
+
const DEFAULT_EMBEDDING_MODEL_ID = "openai/text-embedding-3-small";
|
|
689
|
+
const DEFAULT_IMAGE_GENERATION_MODEL_ID = "google/gemini-3-pro-image";
|
|
690
|
+
function validateGatewayModelId(raw) {
|
|
691
|
+
const trimmed = toOptionalTrimmed(raw);
|
|
692
|
+
if (trimmed === void 0) return void 0;
|
|
693
|
+
resolveGatewayModel(trimmed);
|
|
694
|
+
return trimmed;
|
|
695
|
+
}
|
|
696
|
+
function validateEmbeddingModelId(raw) {
|
|
697
|
+
return toOptionalTrimmed(raw);
|
|
698
|
+
}
|
|
699
|
+
function requireModelId(configName, raw, validate) {
|
|
700
|
+
const modelId = validate(raw);
|
|
701
|
+
if (!modelId) throw new Error(`${configName} must not be empty`);
|
|
702
|
+
return modelId;
|
|
703
|
+
}
|
|
704
|
+
const DEFAULT_STANDARD_PROFILE_DESCRIPTION = "Use for default assistant work: lookups, explanations, ordinary tool use, short answers, and light investigation of one source. Avoid for implementation, debugging, multi-file changes, architecture decisions, or research across several systems.";
|
|
705
|
+
const DEFAULT_HANDOFF_PROFILE_DESCRIPTION = "Use for coding and difficult multi-step work: implementation, debugging, root-cause analysis, broad refactors, multi-file changes, architecture decisions, and research across several systems. Avoid for simple lookups, short answers, single-file reads, or ordinary tool use that the default profile can finish.";
|
|
706
|
+
function parseOptionalProfileDescription(rawDescription, configName, profile) {
|
|
707
|
+
if (rawDescription === void 0) return;
|
|
708
|
+
if (typeof rawDescription !== "string") throw new Error(`${configName}.${profile}.description must be a string when set`);
|
|
709
|
+
const description = toOptionalTrimmed(rawDescription);
|
|
710
|
+
if (!description) throw new Error(`${configName}.${profile}.description must not be empty when set`);
|
|
711
|
+
return description;
|
|
712
|
+
}
|
|
713
|
+
function parseOptionalProfileReasoningLevel(rawReasoningLevel, configName, profile) {
|
|
714
|
+
if (rawReasoningLevel === void 0) return;
|
|
715
|
+
try {
|
|
716
|
+
return parseTurnReasoningLevel(rawReasoningLevel);
|
|
717
|
+
} catch {
|
|
718
|
+
throw new Error(`${configName}.${profile}.reasoningLevel must be one of ${TURN_REASONING_LEVELS.join(", ")}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function parseProfileConfig(rawProfile, configName, profile) {
|
|
722
|
+
if (typeof rawProfile === "string") {
|
|
723
|
+
const modelId = validateGatewayModelId(rawProfile);
|
|
724
|
+
if (!modelId) throw new Error(`${configName}.${profile} must not be empty`);
|
|
725
|
+
return { modelId };
|
|
726
|
+
}
|
|
727
|
+
if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile)) throw new Error(`${configName}.${profile} must be a model id string or an object with modelId`);
|
|
728
|
+
const rawModelId = rawProfile.modelId;
|
|
729
|
+
if (typeof rawModelId !== "string") throw new Error(`${configName}.${profile}.modelId must be a model id string`);
|
|
730
|
+
const modelId = validateGatewayModelId(rawModelId);
|
|
731
|
+
if (!modelId) throw new Error(`${configName}.${profile}.modelId must not be empty`);
|
|
732
|
+
const description = parseOptionalProfileDescription(rawProfile.description, configName, profile);
|
|
733
|
+
const reasoningLevel = parseOptionalProfileReasoningLevel(rawProfile.reasoningLevel, configName, profile);
|
|
734
|
+
return {
|
|
735
|
+
modelId,
|
|
736
|
+
...description ? { description } : void 0,
|
|
737
|
+
...reasoningLevel ? { reasoningLevel } : void 0
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function parseProfileMap(rawProfiles, configName) {
|
|
741
|
+
if (!rawProfiles || typeof rawProfiles !== "object" || Array.isArray(rawProfiles)) throw new Error(`${configName} must be ${configName === "AI_MODEL_PROFILES" ? "a JSON object" : "an object"}`);
|
|
742
|
+
const profiles = {};
|
|
743
|
+
for (const [profile, rawProfile] of Object.entries(rawProfiles)) {
|
|
744
|
+
if (!modelProfileSchema.safeParse(profile).success) throw new Error(`${configName} profile "${profile}" must match ^[a-z][a-z0-9_-]*$`);
|
|
745
|
+
profiles[profile] = parseProfileConfig(rawProfile, configName, profile);
|
|
746
|
+
}
|
|
747
|
+
return profiles;
|
|
748
|
+
}
|
|
749
|
+
function parseProfiles(rawValue, standardModelId, handoffModelId) {
|
|
750
|
+
const profiles = {
|
|
751
|
+
standard: {
|
|
752
|
+
modelId: standardModelId,
|
|
753
|
+
description: DEFAULT_STANDARD_PROFILE_DESCRIPTION
|
|
754
|
+
},
|
|
755
|
+
handoff: {
|
|
756
|
+
modelId: handoffModelId,
|
|
757
|
+
description: DEFAULT_HANDOFF_PROFILE_DESCRIPTION,
|
|
758
|
+
reasoningLevel: "high"
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const trimmed = toOptionalTrimmed(rawValue);
|
|
762
|
+
if (trimmed === void 0) return profiles;
|
|
763
|
+
let parsed;
|
|
764
|
+
try {
|
|
765
|
+
parsed = JSON.parse(trimmed);
|
|
766
|
+
} catch {
|
|
767
|
+
throw new Error("AI_MODEL_PROFILES must be a JSON object");
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
...profiles,
|
|
771
|
+
...parseProfileMap(parsed, "AI_MODEL_PROFILES")
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function parseReactionEmoji(envName, rawValue, defaultEmoji) {
|
|
775
|
+
const trimmed = toOptionalTrimmed(rawValue);
|
|
776
|
+
if (trimmed === void 0) return defaultEmoji;
|
|
777
|
+
const normalized = normalizeSlackEmojiName(trimmed);
|
|
778
|
+
if (!normalized) throw new Error(`${envName} must be a valid Slack emoji name (for example "eyes" or ":white_check_mark:")`);
|
|
779
|
+
return normalized;
|
|
780
|
+
}
|
|
781
|
+
function warnDeprecatedProfileEnv(env) {
|
|
782
|
+
for (const envName of [
|
|
783
|
+
"AI_MODEL",
|
|
784
|
+
"AI_HANDOFF_MODEL",
|
|
785
|
+
"AI_MODEL_PROFILES"
|
|
786
|
+
]) if (toOptionalTrimmed(env[envName]) !== void 0) logWarn("config.profile_env.deprecated", {
|
|
787
|
+
"app.config.env_name": envName,
|
|
788
|
+
"app.config.replacement": "createApp({ defaultProfile, profiles })"
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function readBotConfig(env, functionMaxDurationSeconds) {
|
|
792
|
+
warnDeprecatedProfileEnv(env);
|
|
793
|
+
const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(functionMaxDurationSeconds);
|
|
794
|
+
const modelId = validateGatewayModelId(env.AI_MODEL) ?? DEFAULT_MODEL_ID;
|
|
795
|
+
const reasoningLevel = toOptionalTrimmed(env.AI_REASONING_LEVEL);
|
|
796
|
+
const fastModelId = validateGatewayModelId(env.AI_FAST_MODEL ?? env.AI_MODEL) ?? DEFAULT_FAST_MODEL_ID;
|
|
797
|
+
const guardianModelId = validateGatewayModelId(env.AI_GUARDIAN_MODEL) ?? DEFAULT_GUARDIAN_MODEL_ID;
|
|
798
|
+
const handoffModelId = validateGatewayModelId(env.AI_HANDOFF_MODEL) ?? DEFAULT_HANDOFF_MODEL_ID;
|
|
799
|
+
return {
|
|
800
|
+
userName: toOptionalTrimmed(env.JUNIOR_BOT_NAME) ?? "junior",
|
|
801
|
+
defaultProfile: "standard",
|
|
802
|
+
crossActorMidRunMode: parseCrossActorMidRunMode(env.JUNIOR_CROSS_ACTOR_MID_RUN_MODE),
|
|
803
|
+
profiles: parseProfiles(env.AI_MODEL_PROFILES, modelId, handoffModelId),
|
|
804
|
+
contextWindowTokens: parseOptionalPositiveInteger("AI_MODEL_CONTEXT_WINDOW_TOKENS", env.AI_MODEL_CONTEXT_WINDOW_TOKENS) ?? DEFAULT_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
805
|
+
reasoningLevel: reasoningLevel === void 0 ? void 0 : parseTurnReasoningLevel(reasoningLevel),
|
|
806
|
+
fastModelId,
|
|
807
|
+
guardianModelId,
|
|
808
|
+
imageGenerationModelId: toOptionalTrimmed(env.AI_IMAGE_MODEL) ?? DEFAULT_IMAGE_GENERATION_MODEL_ID,
|
|
809
|
+
embeddingModelId: validateEmbeddingModelId(env.AI_EMBEDDING_MODEL) ?? DEFAULT_EMBEDDING_MODEL_ID,
|
|
810
|
+
loadingMessages: parseLoadingMessages(env.JUNIOR_LOADING_MESSAGES),
|
|
811
|
+
visionModelId: validateGatewayModelId(env.AI_VISION_MODEL),
|
|
812
|
+
maxSlicesPerTurn: MAX_SLICES_PER_TURN,
|
|
813
|
+
maxToolCallsPerTurn: MAX_TOOL_CALLS_PER_TURN,
|
|
814
|
+
maxConsecutiveAutomatedTurns: MAX_CONSECUTIVE_AUTOMATED_TURNS,
|
|
815
|
+
turnTimeoutMs: parseAgentTurnTimeoutMs(env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs),
|
|
816
|
+
webSearchModelId: validateGatewayModelId(env.AI_WEB_SEARCH_MODEL) ?? DEFAULT_WEB_SEARCH_MODEL_ID
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function readDatabaseUrl(env) {
|
|
820
|
+
const databaseUrl = toOptionalTrimmed(env.DATABASE_URL);
|
|
821
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
|
822
|
+
return databaseUrl;
|
|
823
|
+
}
|
|
824
|
+
function isLocalDatabaseUrl(databaseUrl) {
|
|
825
|
+
try {
|
|
826
|
+
const { hostname } = new URL(databaseUrl);
|
|
827
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
828
|
+
} catch {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function readSqlDriver(env, databaseUrl) {
|
|
833
|
+
const value = toOptionalTrimmed(env.JUNIOR_DATABASE_DRIVER);
|
|
834
|
+
if (value === void 0) {
|
|
835
|
+
if (isLocalDatabaseUrl(databaseUrl)) return "postgres";
|
|
836
|
+
return "neon";
|
|
837
|
+
}
|
|
838
|
+
if (value === "neon" || value === "postgres") return value;
|
|
839
|
+
throw new Error("JUNIOR_DATABASE_DRIVER must be postgres or neon");
|
|
840
|
+
}
|
|
841
|
+
function readSqlStatementTimeoutMs(env) {
|
|
842
|
+
const rawValue = toOptionalTrimmed(env.JUNIOR_SQL_STATEMENT_TIMEOUT_MS);
|
|
843
|
+
if (rawValue === void 0) return DEFAULT_SQL_STATEMENT_TIMEOUT_MS;
|
|
844
|
+
const value = Number.parseInt(rawValue, 10);
|
|
845
|
+
if (!Number.isSafeInteger(value) || value < 0 || String(value) !== rawValue) throw new Error("JUNIOR_SQL_STATEMENT_TIMEOUT_MS must be a non-negative integer");
|
|
846
|
+
return value === 0 ? false : value;
|
|
847
|
+
}
|
|
848
|
+
/** Parse all chat configuration from environment variables. */
|
|
849
|
+
function readChatConfig(env = process.env, functionMaxDurationSeconds = DEFAULT_FUNCTION_MAX_DURATION_SECONDS) {
|
|
850
|
+
const databaseUrl = readDatabaseUrl(env);
|
|
851
|
+
const resolvedFunctionMaxDurationSeconds = resolveFunctionMaxDurationSeconds(functionMaxDurationSeconds);
|
|
852
|
+
return {
|
|
853
|
+
bot: readBotConfig(env, resolvedFunctionMaxDurationSeconds),
|
|
854
|
+
functionMaxDurationSeconds: resolvedFunctionMaxDurationSeconds,
|
|
855
|
+
conversationWorkEnabled: parseConversationWorkEnabled(env.JUNIOR_CONVERSATION_WORK_ENABLED),
|
|
856
|
+
conversationWorkSoftYieldAfterMs: resolveConversationWorkSoftYieldAfterMs(resolvedFunctionMaxDurationSeconds),
|
|
857
|
+
sql: {
|
|
858
|
+
databaseUrl,
|
|
859
|
+
driver: readSqlDriver(env, databaseUrl),
|
|
860
|
+
statementTimeoutMs: readSqlStatementTimeoutMs(env)
|
|
861
|
+
},
|
|
862
|
+
slack: {
|
|
863
|
+
botToken: toOptionalTrimmed(env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(env.SLACK_BOT_USER_TOKEN),
|
|
864
|
+
clientId: toOptionalTrimmed(env.SLACK_CLIENT_ID),
|
|
865
|
+
clientSecret: toOptionalTrimmed(env.SLACK_CLIENT_SECRET),
|
|
866
|
+
completedReactionEmoji: DEFAULT_COMPLETED_REACTION_EMOJI,
|
|
867
|
+
processingReactionEmoji: DEFAULT_PROCESSING_REACTION_EMOJI,
|
|
868
|
+
signingSecret: toOptionalTrimmed(env.SLACK_SIGNING_SECRET),
|
|
869
|
+
slashCommand: parseSlashCommand(env.JUNIOR_SLASH_COMMAND)
|
|
870
|
+
},
|
|
871
|
+
state: {
|
|
872
|
+
adapter: env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory" ? "memory" : "redis",
|
|
873
|
+
keyPrefix: toOptionalTrimmed(env.JUNIOR_STATE_KEY_PREFIX),
|
|
874
|
+
redisUrl: toOptionalTrimmed(env.REDIS_URL)
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
/** Chat configuration parsed once at module load from the process environment. */
|
|
879
|
+
const chatConfig = readChatConfig(process.env);
|
|
880
|
+
/** Apply the host execution budget injected by juniorNitro(). */
|
|
881
|
+
function configureFunctionMaxDurationSeconds(functionMaxDurationSeconds) {
|
|
882
|
+
const resolved = resolveFunctionMaxDurationSeconds(functionMaxDurationSeconds);
|
|
883
|
+
chatConfig.functionMaxDurationSeconds = resolved;
|
|
884
|
+
chatConfig.conversationWorkSoftYieldAfterMs = resolveConversationWorkSoftYieldAfterMs(resolved);
|
|
885
|
+
chatConfig.bot.turnTimeoutMs = parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, resolveMaxTurnTimeoutMs(resolved));
|
|
886
|
+
}
|
|
887
|
+
/** Return the chat configuration (parsed once at startup). */
|
|
888
|
+
function getChatConfig() {
|
|
889
|
+
return chatConfig;
|
|
890
|
+
}
|
|
891
|
+
/** Bot configuration derived from environment at module load. */
|
|
892
|
+
const botConfig = chatConfig.bot;
|
|
893
|
+
function getSlackBotToken() {
|
|
894
|
+
return chatConfig.slack.botToken;
|
|
895
|
+
}
|
|
896
|
+
function getSlackSigningSecret() {
|
|
897
|
+
return chatConfig.slack.signingSecret;
|
|
898
|
+
}
|
|
899
|
+
function getSlackClientId() {
|
|
900
|
+
return chatConfig.slack.clientId;
|
|
901
|
+
}
|
|
902
|
+
function getSlackClientSecret() {
|
|
903
|
+
return chatConfig.slack.clientSecret;
|
|
904
|
+
}
|
|
905
|
+
/** Return runtime metadata (version from deploy environment). */
|
|
906
|
+
function getRuntimeMetadata() {
|
|
907
|
+
return { version: toOptionalTrimmed(process.env.VERCEL_GIT_COMMIT_SHA) };
|
|
908
|
+
}
|
|
909
|
+
/** Apply model overrides from createApp(). */
|
|
910
|
+
function setBotModelConfig(config) {
|
|
911
|
+
if (config.embeddingModelId !== void 0) botConfig.embeddingModelId = requireModelId("embeddingModelId", config.embeddingModelId, validateEmbeddingModelId);
|
|
912
|
+
if (config.fastModelId !== void 0) botConfig.fastModelId = requireModelId("fastModelId", config.fastModelId, validateGatewayModelId);
|
|
913
|
+
if (config.guardianModelId !== void 0) botConfig.guardianModelId = requireModelId("guardianModelId", config.guardianModelId, validateGatewayModelId);
|
|
914
|
+
if (config.imageGenerationModelId !== void 0) botConfig.imageGenerationModelId = requireModelId("imageGenerationModelId", config.imageGenerationModelId, validateEmbeddingModelId);
|
|
915
|
+
if (config.visionModelId !== void 0) botConfig.visionModelId = requireModelId("visionModelId", config.visionModelId, validateGatewayModelId);
|
|
916
|
+
if (config.webSearchModelId !== void 0) botConfig.webSearchModelId = requireModelId("webSearchModelId", config.webSearchModelId, validateGatewayModelId);
|
|
917
|
+
}
|
|
918
|
+
/** Apply profiles from createApp(). */
|
|
919
|
+
function setProfiles(profiles, defaultProfile) {
|
|
920
|
+
if (!profiles || !defaultProfile) throw new Error("profiles and defaultProfile must be configured together");
|
|
921
|
+
const configuredProfiles = parseProfileMap(profiles, "profiles");
|
|
922
|
+
const selectedDefault = defaultProfile;
|
|
923
|
+
if (!modelProfileSchema.safeParse(selectedDefault).success) throw new Error(`defaultProfile "${selectedDefault}" must match ^[a-z][a-z0-9_-]*$`);
|
|
924
|
+
if (!Object.hasOwn(configuredProfiles, selectedDefault)) throw new Error(`defaultProfile "${selectedDefault}" is not configured`);
|
|
925
|
+
botConfig.profiles = configuredProfiles;
|
|
926
|
+
botConfig.defaultProfile = selectedDefault;
|
|
927
|
+
}
|
|
928
|
+
/** Return the current Slack reaction emoji config. */
|
|
929
|
+
function getSlackReactionConfig() {
|
|
930
|
+
return {
|
|
931
|
+
completedReactionEmoji: chatConfig.slack.completedReactionEmoji,
|
|
932
|
+
processingReactionEmoji: chatConfig.slack.processingReactionEmoji
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/** Apply Slack reaction emoji overrides from createApp() options, validating names. */
|
|
936
|
+
function setSlackReactionConfig(overrides) {
|
|
937
|
+
if (overrides.processingReactionEmoji !== void 0) chatConfig.slack.processingReactionEmoji = parseReactionEmoji("processingReactionEmoji", overrides.processingReactionEmoji, chatConfig.slack.processingReactionEmoji);
|
|
938
|
+
if (overrides.completedReactionEmoji !== void 0) chatConfig.slack.completedReactionEmoji = parseReactionEmoji("completedReactionEmoji", overrides.completedReactionEmoji, chatConfig.slack.completedReactionEmoji);
|
|
939
|
+
}
|
|
940
|
+
//#endregion
|
|
941
|
+
export { createProviderError as A, MODEL_HANDOFF_SUMMARY_PREFIX as C, unwrapCurrentInstruction as D, renderCurrentInstruction as E, MISSING_GATEWAY_CREDENTIALS_ERROR as F, getGatewayApiKey as I, resolveGatewayCredential as L, getProviderErrorAttributes as M, getProviderErrorUserMessage as N, escapeXml as O, isProviderRetryError as P, toOptionalTrimmed as R, COMPACTION_SUMMARY_PREFIX as S, isCompactionSummaryText as T, completeObject as _, getRuntimeMetadata as a, resolveGatewayModel as b, getSlackClientSecret as c, setBotModelConfig as d, setProfiles as f, GEN_AI_SERVER_ADDRESS as g, GEN_AI_PROVIDER_NAME as h, getChatConfig as i, findProviderError as j, ProviderError as k, getSlackReactionConfig as l, normalizeSlackEmojiName as m, botConfig as n, getSlackBotToken as o, setSlackReactionConfig as p, configureFunctionMaxDurationSeconds as r, getSlackClientId as s, DEFAULT_SQL_STATEMENT_TIMEOUT_MS as t, getSlackSigningSecret as u, completeText as v, hasCompactedConversationContext as w, ACTIVE_TURN_COMPACTION_SUMMARY_PREFIX as x, embedTexts as y };
|