@intelli-1113/stratos-sdk 1.2.3 → 1.3.2

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.
@@ -0,0 +1,428 @@
1
+ // LangChain / Stratos compatibility inspect + optional fix (client project cwd).
2
+ //
3
+ // Traceloop instrumentation peer (via @traceloop/instrumentation-langchain):
4
+ // @langchain/core >=1.0.0 <2.0.0
5
+ //
6
+ // Usage (CLI):
7
+ // npx stratos-inspect
8
+ // npx stratos-inspect --fix
9
+ // npx stratos-inspect --fix --yes
10
+
11
+ import { createRequire } from "node:module";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { spawnSync } from "node:child_process";
15
+ import { createInterface } from "node:readline";
16
+
17
+ /** Peer range of @traceloop/instrumentation-langchain@0.27 */
18
+ export const LANGCHAIN_CORE_RANGE = {
19
+ minInclusive: "1.0.0",
20
+ maxExclusive: "2.0.0",
21
+ npmInstall: "@langchain/core@^1.2.4",
22
+ };
23
+
24
+ /** Packages we may touch when --fix is used (only if present / related). */
25
+ export const LANGCHAIN_FAMILY = [
26
+ "langchain",
27
+ "@langchain/core",
28
+ "@langchain/community",
29
+ "@langchain/openai",
30
+ "@langchain/anthropic",
31
+ "@langchain/mistralai",
32
+ "@langchain/google-genai",
33
+ "@langchain/google-vertexai",
34
+ "@langchain/tavily",
35
+ "@langchain/langgraph",
36
+ "@langchain/langgraph-checkpoint",
37
+ "@langchain/classic",
38
+ "@langchain/textsplitters",
39
+ ];
40
+
41
+ /** Safe 1.x installs for a typical Stratos + LangChain agent. */
42
+ export const RECOMMENDED_INSTALL = [
43
+ "@langchain/core@^1.2.4",
44
+ "langchain@^1.5.4",
45
+ ];
46
+
47
+ function parseSemver(v) {
48
+ if (!v || typeof v !== "string") return null;
49
+ const cleaned = v.replace(/^[=v^~<>]*/, "").split("-")[0].trim();
50
+ const m = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
51
+ if (!m) {
52
+ // package.json range like "^0.3.0" → take first numbers
53
+ const m2 = v.match(/(\d+)\.(\d+)\.(\d+)/) || v.match(/(\d+)\.(\d+)/);
54
+ if (!m2) return null;
55
+ return {
56
+ major: Number(m2[1]),
57
+ minor: Number(m2[2]),
58
+ patch: Number(m2[3] || 0),
59
+ raw: v,
60
+ };
61
+ }
62
+ return {
63
+ major: Number(m[1]),
64
+ minor: Number(m[2]),
65
+ patch: Number(m[3]),
66
+ raw: v,
67
+ };
68
+ }
69
+
70
+ function cmp(a, b) {
71
+ if (a.major !== b.major) return a.major - b.major;
72
+ if (a.minor !== b.minor) return a.minor - b.minor;
73
+ return a.patch - b.patch;
74
+ }
75
+
76
+ /**
77
+ * @returns {"ok"|"too_old"|"too_new"|"unknown"|"absent"}
78
+ */
79
+ export function classifyCoreVersion(versionStr) {
80
+ if (!versionStr) return "absent";
81
+ const v = parseSemver(versionStr);
82
+ if (!v) return "unknown";
83
+ const min = parseSemver(LANGCHAIN_CORE_RANGE.minInclusive);
84
+ const max = parseSemver(LANGCHAIN_CORE_RANGE.maxExclusive);
85
+ if (cmp(v, min) < 0) return "too_old";
86
+ if (cmp(v, max) >= 0) return "too_new";
87
+ return "ok";
88
+ }
89
+
90
+ function readPkgJson(cwd) {
91
+ const p = join(cwd, "package.json");
92
+ if (!existsSync(p)) return null;
93
+ try {
94
+ return JSON.parse(readFileSync(p, "utf8"));
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function declaredDeps(pkg) {
101
+ return {
102
+ ...(pkg?.dependencies || {}),
103
+ ...(pkg?.devDependencies || {}),
104
+ ...(pkg?.optionalDependencies || {}),
105
+ };
106
+ }
107
+
108
+ function resolveInstalledVersion(cwd, name) {
109
+ try {
110
+ const req = createRequire(join(cwd, "package.json"));
111
+ const pkgPath = req.resolve(`${name}/package.json`);
112
+ const meta = JSON.parse(readFileSync(pkgPath, "utf8"));
113
+ return { version: meta.version, path: pkgPath };
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function detectPackageManager(cwd) {
120
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
121
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
122
+ if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock")))
123
+ return "bun";
124
+ return "npm";
125
+ }
126
+
127
+ /**
128
+ * Inspect LangChain compatibility for a project directory (default: cwd).
129
+ * @param {{ cwd?: string }} [opts]
130
+ */
131
+ export function inspectLangChain(opts = {}) {
132
+ const cwd = opts.cwd || process.cwd();
133
+ const pkg = readPkgJson(cwd);
134
+ const declared = pkg ? declaredDeps(pkg) : {};
135
+
136
+ const found = [];
137
+ for (const name of LANGCHAIN_FAMILY) {
138
+ const declaredRange = declared[name] || null;
139
+ const installed = resolveInstalledVersion(cwd, name);
140
+ if (!declaredRange && !installed) continue;
141
+ found.push({
142
+ name,
143
+ declared: declaredRange,
144
+ installed: installed?.version || null,
145
+ installPath: installed?.path || null,
146
+ });
147
+ }
148
+
149
+ const core =
150
+ found.find((f) => f.name === "@langchain/core") ||
151
+ (declared["@langchain/core"]
152
+ ? {
153
+ name: "@langchain/core",
154
+ declared: declared["@langchain/core"],
155
+ installed: null,
156
+ installPath: null,
157
+ }
158
+ : null);
159
+
160
+ const coreVersion = core?.installed || core?.declared || null;
161
+ const status = core
162
+ ? classifyCoreVersion(core?.installed || core?.declared)
163
+ : found.length
164
+ ? "unknown"
165
+ : "absent";
166
+
167
+ const compatible = status === "ok" || status === "absent";
168
+ // absent = no LangChain → Stratos still fine (other frameworks)
169
+ // too_old / too_new / unknown with langchain present → problem for LC metrics
170
+
171
+ const hasLangChain = found.length > 0 || !!core;
172
+ const needsUpgrade = status === "too_old";
173
+ const needsAttention = status === "too_old" || status === "too_new";
174
+
175
+ return {
176
+ cwd,
177
+ packageManager: detectPackageManager(cwd),
178
+ hasLangChain,
179
+ status,
180
+ compatible: !hasLangChain || status === "ok",
181
+ needsUpgrade,
182
+ needsAttention,
183
+ core: core
184
+ ? {
185
+ declared: core.declared,
186
+ installed: core.installed,
187
+ versionForCheck: coreVersion,
188
+ status,
189
+ }
190
+ : null,
191
+ packages: found,
192
+ required: {
193
+ peer: `@langchain/core >=${LANGCHAIN_CORE_RANGE.minInclusive} <${LANGCHAIN_CORE_RANGE.maxExclusive}`,
194
+ recommendedInstall: RECOMMENDED_INSTALL,
195
+ },
196
+ message: buildMessage(status, hasLangChain, coreVersion),
197
+ };
198
+ }
199
+
200
+ function buildMessage(status, hasLangChain, coreVersion) {
201
+ if (!hasLangChain || status === "absent") {
202
+ return "No LangChain packages found — nothing to upgrade for LangChain metrics.";
203
+ }
204
+ if (status === "ok") {
205
+ return `@langchain/core@${coreVersion} is compatible with Stratos LangChain auto-instrumentation.`;
206
+ }
207
+ if (status === "too_old") {
208
+ return (
209
+ `@langchain/core@${coreVersion} is too old for Stratos LangChain metrics. ` +
210
+ `Required: >=1.0.0 <2.0.0. Uninstall old LangChain packages, then install 1.x.`
211
+ );
212
+ }
213
+ if (status === "too_new") {
214
+ return (
215
+ `@langchain/core@${coreVersion} is newer than the current OpenLLMetry peer range (<2.0.0). ` +
216
+ `LangChain metrics may not work until Stratos/Traceloop supports core 2.x.`
217
+ );
218
+ }
219
+ return `Could not parse @langchain/core version (${coreVersion}). Expected >=1.0.0 <2.0.0.`;
220
+ }
221
+
222
+ function runPm(pm, args, cwd) {
223
+ const cmd = pm === "npm" ? "npm" : pm;
224
+ const result = spawnSync(cmd, args, {
225
+ cwd,
226
+ stdio: "inherit",
227
+ shell: process.platform === "win32",
228
+ env: process.env,
229
+ });
230
+ return result.status === 0;
231
+ }
232
+
233
+ /**
234
+ * Uninstall detected LangChain family packages, then install recommended 1.x set
235
+ * (+ re-install any previously present family packages at ^1 if they were there).
236
+ *
237
+ * @param {{ cwd?: string, yes?: boolean, dryRun?: boolean }} [opts]
238
+ */
239
+ export async function fixLangChain(opts = {}) {
240
+ const cwd = opts.cwd || process.cwd();
241
+ const report = inspectLangChain({ cwd });
242
+ const pm = report.packageManager;
243
+
244
+ if (!report.hasLangChain) {
245
+ return {
246
+ ok: true,
247
+ skipped: true,
248
+ reason: "no-langchain",
249
+ report,
250
+ };
251
+ }
252
+
253
+ if (report.status === "ok") {
254
+ return {
255
+ ok: true,
256
+ skipped: true,
257
+ reason: "already-compatible",
258
+ report,
259
+ };
260
+ }
261
+
262
+ if (report.status === "too_new") {
263
+ return {
264
+ ok: false,
265
+ skipped: true,
266
+ reason: "too-new",
267
+ report,
268
+ message:
269
+ "Core is >=2.0.0 — automatic downgrade is not performed. Pin <2 or wait for SDK support.",
270
+ };
271
+ }
272
+
273
+ if (report.status !== "too_old" && report.status !== "unknown") {
274
+ return { ok: false, skipped: true, reason: report.status, report };
275
+ }
276
+
277
+ const toRemove = report.packages.map((p) => p.name);
278
+ // Also remove declared-only names
279
+ const pkg = readPkgJson(cwd);
280
+ const declared = pkg ? declaredDeps(pkg) : {};
281
+ for (const name of LANGCHAIN_FAMILY) {
282
+ if (declared[name] && !toRemove.includes(name)) toRemove.push(name);
283
+ }
284
+
285
+ // Reinstall: recommended base + any extra family packages client had, on ^1
286
+ const reinstall = new Set(RECOMMENDED_INSTALL);
287
+ for (const name of toRemove) {
288
+ if (name === "@langchain/core" || name === "langchain") continue;
289
+ // keep provider packages if they were present
290
+ if (report.packages.some((p) => p.name === name) || declared[name]) {
291
+ reinstall.add(`${name}@^1`);
292
+ }
293
+ }
294
+
295
+ const plan = {
296
+ packageManager: pm,
297
+ uninstall: toRemove,
298
+ install: [...reinstall],
299
+ };
300
+
301
+ if (opts.dryRun) {
302
+ return { ok: true, dryRun: true, plan, report };
303
+ }
304
+
305
+ if (!opts.yes) {
306
+ const confirmed = await confirm(
307
+ `\n[stratos-inspect] About to:\n` +
308
+ ` 1) uninstall: ${toRemove.join(", ")}\n` +
309
+ ` 2) install: ${[...reinstall].join(", ")}\n` +
310
+ `in ${cwd}\n\nProceed? [y/N] `
311
+ );
312
+ if (!confirmed) {
313
+ return { ok: false, cancelled: true, plan, report };
314
+ }
315
+ }
316
+
317
+ console.log(`\n[stratos-inspect] Using package manager: ${pm}`);
318
+ console.log(`[stratos-inspect] Uninstalling old LangChain packages…`);
319
+
320
+ let uninstallOk = true;
321
+ if (toRemove.length) {
322
+ if (pm === "npm") {
323
+ uninstallOk = runPm("npm", ["uninstall", ...toRemove], cwd);
324
+ } else if (pm === "pnpm") {
325
+ uninstallOk = runPm("pnpm", ["remove", ...toRemove], cwd);
326
+ } else if (pm === "yarn") {
327
+ uninstallOk = runPm("yarn", ["remove", ...toRemove], cwd);
328
+ } else if (pm === "bun") {
329
+ uninstallOk = runPm("bun", ["remove", ...toRemove], cwd);
330
+ }
331
+ }
332
+
333
+ if (!uninstallOk) {
334
+ return {
335
+ ok: false,
336
+ step: "uninstall",
337
+ plan,
338
+ report,
339
+ message: "Uninstall failed — fix manually and re-run stratos-inspect --fix",
340
+ };
341
+ }
342
+
343
+ console.log(`[stratos-inspect] Installing LangChain 1.x packages…`);
344
+ const installList = [...reinstall];
345
+ let installOk = false;
346
+ if (pm === "npm") {
347
+ installOk = runPm("npm", ["install", ...installList], cwd);
348
+ } else if (pm === "pnpm") {
349
+ installOk = runPm("pnpm", ["add", ...installList], cwd);
350
+ } else if (pm === "yarn") {
351
+ installOk = runPm("yarn", ["add", ...installList], cwd);
352
+ } else if (pm === "bun") {
353
+ installOk = runPm("bun", ["add", ...installList], cwd);
354
+ }
355
+
356
+ const after = inspectLangChain({ cwd });
357
+ return {
358
+ ok: installOk && after.status === "ok",
359
+ step: "install",
360
+ plan,
361
+ reportBefore: report,
362
+ reportAfter: after,
363
+ message: after.message,
364
+ };
365
+ }
366
+
367
+ function confirm(question) {
368
+ if (!process.stdin.isTTY) {
369
+ console.warn(
370
+ "[stratos-inspect] Non-interactive terminal — pass --yes to apply fix, or run with a TTY."
371
+ );
372
+ return Promise.resolve(false);
373
+ }
374
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
375
+ return new Promise((resolve) => {
376
+ rl.question(question, (answer) => {
377
+ rl.close();
378
+ const a = String(answer || "").trim().toLowerCase();
379
+ resolve(a === "y" || a === "yes");
380
+ });
381
+ });
382
+ }
383
+
384
+ /** Human-readable print for CLI / postinstall */
385
+ export function printInspectReport(report, { json = false } = {}) {
386
+ if (json) {
387
+ console.log(JSON.stringify(report, null, 2));
388
+ return;
389
+ }
390
+
391
+ console.log("\n[stratos-inspect] LangChain compatibility");
392
+ console.log(` project: ${report.cwd}`);
393
+ console.log(` pm: ${report.packageManager}`);
394
+ console.log(` status: ${report.status}`);
395
+ console.log(` required: ${report.required.peer}`);
396
+
397
+ if (report.core) {
398
+ console.log(
399
+ ` core: installed=${report.core.installed || "—"} declared=${report.core.declared || "—"}`
400
+ );
401
+ }
402
+
403
+ if (report.packages.length) {
404
+ console.log(" packages:");
405
+ for (const p of report.packages) {
406
+ console.log(
407
+ ` - ${p.name}: ${p.installed || p.declared || "?"}${p.installed && p.declared ? ` (declared ${p.declared})` : ""}`
408
+ );
409
+ }
410
+ }
411
+
412
+ console.log(`\n ${report.message}`);
413
+
414
+ if (report.needsUpgrade) {
415
+ console.log(`
416
+ Fix (interactive):
417
+ npx stratos-inspect --fix
418
+
419
+ Fix (non-interactive):
420
+ npx stratos-inspect --fix --yes
421
+
422
+ Or manually:
423
+ npm uninstall ${report.packages.map((p) => p.name).join(" ")}
424
+ npm install ${report.required.recommendedInstall.join(" ")}
425
+ `);
426
+ }
427
+ console.log("");
428
+ }
package/src/mcp-proxy.js CHANGED
@@ -23,6 +23,12 @@ console.log = console.info = (...a) => process.stderr.write(a.map(String).join("
23
23
 
24
24
  import { spawn } from "node:child_process";
25
25
  import { randomBytes } from "node:crypto";
26
+ // The network guard installed by register.js (see enforcement.js) patches
27
+ // THIS process's fetch/http — it has no effect on the target MCP server, which
28
+ // runs as a separate child process spawned below, possibly in a different
29
+ // language/runtime entirely. The only place budget/lifecycle enforcement can
30
+ // act on that child is here, at the JSON-RPC boundary between the host and it.
31
+ import { setBlocked, isBlocked } from "./enforcement.js";
26
32
 
27
33
  let argv = process.argv.slice(2);
28
34
  const sep = argv.indexOf("--");
@@ -65,9 +71,26 @@ function reportTool(name, argsObj, out, isErr, startMs, endMs) {
65
71
  fetch(`${URL}/api/ingest`, { method: "POST", headers: hdr, body: JSON.stringify(body) }).catch(() => {});
66
72
  }
67
73
 
74
+ let warnedBlocked = false;
68
75
  function heartbeat() {
69
76
  if (!TOKEN) return;
70
- fetch(`${URL}/api/heartbeat`, { method: "POST", headers: hdr }).catch(() => {});
77
+ fetch(`${URL}/api/heartbeat`, { method: "POST", headers: hdr })
78
+ .then((r) => r.json().catch(() => null))
79
+ .then((data) => {
80
+ if (!data || typeof data.blocked !== "boolean") return;
81
+ setBlocked(data.blocked, data.reason);
82
+ // Visible feedback in the proxy's own log (stderr — stdout is the
83
+ // protocol channel). Without this there is no way to see a block take
84
+ // effect other than a tool call failing with no explanation.
85
+ if (data.blocked && !warnedBlocked) {
86
+ warnedBlocked = true;
87
+ process.stderr.write(`[stratos-proxy] BLOCKED: ${data.reason || "budget or lifecycle limit reached"} — tools/call will be refused until this clears.\n`);
88
+ } else if (!data.blocked && warnedBlocked) {
89
+ warnedBlocked = false;
90
+ process.stderr.write("[stratos-proxy] unblocked — tool calls resumed.\n");
91
+ }
92
+ })
93
+ .catch(() => {});
71
94
  }
72
95
  heartbeat();
73
96
  const hbTimer = setInterval(heartbeat, HEARTBEAT_MS);
@@ -83,7 +106,24 @@ linePump(process.stdin, (line) => {
83
106
  let m; try { m = JSON.parse(line); } catch { child.stdin.write(line + "\n"); return; }
84
107
  if (m && m.id != null && m.method === "tools/call") {
85
108
  const name = (m.params && m.params.name) || "tool";
86
- pendingCalls.set(m.id, { name, args: (m.params && m.params.arguments) || {}, start: Date.now() });
109
+ const args = (m.params && m.params.arguments) || {};
110
+ const { blocked, reason } = isBlocked();
111
+ if (blocked) {
112
+ // Refuse before the child ever sees this call — do NOT forward to
113
+ // child.stdin. The host gets a normal JSON-RPC error response (not a
114
+ // dropped connection), and the refusal itself is still reported so it is
115
+ // visible on Stratos rather than silently vanishing.
116
+ const now = Date.now();
117
+ const errResp = {
118
+ jsonrpc: "2.0",
119
+ id: m.id,
120
+ error: { code: -32001, message: `Stratos: blocked — ${reason || "budget or lifecycle limit reached"}` },
121
+ };
122
+ process.stdout.write(JSON.stringify(errResp) + "\n");
123
+ reportTool(name, args, { blocked: true, reason: reason || null }, true, now, now);
124
+ return;
125
+ }
126
+ pendingCalls.set(m.id, { name, args, start: Date.now() });
87
127
  }
88
128
  child.stdin.write(line + "\n");
89
129
  });
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // Runs after `npm install @intelli-1113/stratos-sdk`.
3
+ // Warns only — never auto-uninstalls (use: npx stratos-inspect --fix).
4
+ //
5
+ // npm sets INIT_CWD to the project where the user ran `npm install`
6
+ // (not node_modules/@intelli-1113/stratos-sdk).
7
+
8
+ import { inspectLangChain, printInspectReport } from "./inspect-langchain.js";
9
+
10
+ try {
11
+ if (process.env.STRATOS_SKIP_POSTINSTALL === "1") process.exit(0);
12
+
13
+ // Prefer the consumer project root
14
+ const cwd = process.env.INIT_CWD || process.cwd();
15
+ const report = inspectLangChain({ cwd });
16
+ if (!report.hasLangChain) process.exit(0);
17
+ if (report.compatible) process.exit(0);
18
+
19
+ console.warn(
20
+ "\n[stratos] ⚠ LangChain version is not compatible with Stratos auto-metrics."
21
+ );
22
+ printInspectReport(report);
23
+ console.warn(
24
+ "[stratos] In your project, run:\n npx stratos-inspect --fix\n"
25
+ );
26
+ } catch {
27
+ // Never fail the host install
28
+ process.exit(0);
29
+ }
package/src/register.js CHANGED
@@ -1,5 +1,57 @@
1
- // Side-effect entry so telemetry starts before your app code loads.
2
- // node --import @intelli-1113/stratos-sdk/register server.js (zero code changes)
3
- // import "@intelli-1113/stratos-sdk/register"; (first line of entrypoint)
4
- import { start } from "./index.js";
5
- start();
1
+ // Side-effect entry so telemetry starts before your app code loads.
2
+ // node --import @intelli-1113/stratos-sdk/register server.js (zero code changes)
3
+ // import "@intelli-1113/stratos-sdk/register"; (first line of entrypoint)
4
+ //
5
+ // Order matters:
6
+ // 1) Load .env from cwd (so --import sees STRATOS_TOKEN before start)
7
+ // 2) Install the network guard (see enforcement.js) — before ANY provider SDK
8
+ // (openai, @anthropic-ai/sdk, langchain, ai, …) has a chance to import and
9
+ // capture a reference to the unpatched global fetch/http/https.
10
+ // 3) Register Vercel AI ESM loader (intercepts `import … from "ai"`)
11
+ // 4) start() → Traceloop + OTLP export to Stratos, and the heartbeat that
12
+ // keeps the network guard's blocked/unblocked state up to date.
13
+
14
+ import { register as registerModuleHook } from "node:module";
15
+ import { pathToFileURL } from "node:url";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { existsSync } from "node:fs";
19
+ import { installNetworkGuard } from "./enforcement.js";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+
23
+ // So `node --import …/register app.js` picks up STRATOS_* without client code
24
+ try {
25
+ const envPath = join(process.cwd(), ".env");
26
+ if (existsSync(envPath) && typeof process.loadEnvFile === "function") {
27
+ process.loadEnvFile(envPath);
28
+ }
29
+ } catch {
30
+ /* optional */
31
+ }
32
+
33
+ // Must run before any consumer imports of a provider SDK — it patches the
34
+ // shared, global fetch/http/https that every one of them ultimately calls
35
+ // through, so a single install here covers openai/anthropic/langchain/ai/…
36
+ // without a per-library wrapper. See enforcement.js for why this is the
37
+ // network layer and not the SDK layer.
38
+ try {
39
+ installNetworkGuard();
40
+ } catch (err) {
41
+ console.warn("[stratos] could not install network guard:", err?.message || err);
42
+ }
43
+
44
+ // Must run before any consumer imports of `ai`
45
+ try {
46
+ registerModuleHook(
47
+ pathToFileURL(join(__dirname, "vercel-ai-loader.js")).href
48
+ );
49
+ } catch (err) {
50
+ console.warn(
51
+ "[stratos] could not register Vercel AI loader:",
52
+ err?.message || err
53
+ );
54
+ }
55
+
56
+ import { start } from "./index.js";
57
+ start();
@@ -0,0 +1,78 @@
1
+ // Pure helpers for Vercel AI telemetry injection (no side effects).
2
+ // Used by the ESM loader shim and optional call-site wrappers.
3
+
4
+ export function flagEnabled(name, defaultOn = true) {
5
+ const v = process.env[name];
6
+ if (v == null || v === "") return defaultOn;
7
+ return !["0", "false", "off", "no"].includes(String(v).toLowerCase());
8
+ }
9
+
10
+ export function defaultTelemetrySettings() {
11
+ return {
12
+ isEnabled: true,
13
+ recordInputs: flagEnabled("STRATOS_RECORD_INPUTS", true),
14
+ recordOutputs: flagEnabled("STRATOS_RECORD_OUTPUTS", true),
15
+ functionId: process.env.STRATOS_FUNCTION_ID || "stratos-vercel-ai",
16
+ metadata: {
17
+ framework: "vercel-ai",
18
+ agent:
19
+ process.env.STRATOS_APP_NAME ||
20
+ process.env.npm_package_name ||
21
+ "agent",
22
+ },
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Merge Stratos defaults into AI SDK call options.
28
+ * Respects client opt-out: experimental_telemetry/telemetry isEnabled: false.
29
+ */
30
+ export function injectTelemetryOptions(opts) {
31
+ if (!opts || typeof opts !== "object" || Array.isArray(opts)) return opts;
32
+
33
+ const defaults = defaultTelemetrySettings();
34
+
35
+ if (opts.experimental_telemetry?.isEnabled === false) return opts;
36
+ if (opts.telemetry?.isEnabled === false) return opts;
37
+
38
+ const next = { ...opts };
39
+
40
+ next.experimental_telemetry = {
41
+ ...defaults,
42
+ ...(opts.experimental_telemetry || {}),
43
+ isEnabled: opts.experimental_telemetry?.isEnabled ?? true,
44
+ metadata: {
45
+ ...defaults.metadata,
46
+ ...(opts.experimental_telemetry?.metadata || {}),
47
+ },
48
+ };
49
+
50
+ if (opts.telemetry == null || typeof opts.telemetry === "object") {
51
+ next.telemetry = {
52
+ ...defaults,
53
+ ...(opts.telemetry || {}),
54
+ isEnabled: opts.telemetry?.isEnabled ?? true,
55
+ metadata: {
56
+ ...defaults.metadata,
57
+ ...(opts.telemetry?.metadata || {}),
58
+ },
59
+ };
60
+ }
61
+
62
+ return next;
63
+ }
64
+
65
+ /** Functions whose first argument is call options that accept telemetry. */
66
+ export const TELEMETRY_CALL_NAMES = [
67
+ "generateText",
68
+ "streamText",
69
+ "generateObject",
70
+ "streamObject",
71
+ "embed",
72
+ "embedMany",
73
+ "experimental_generateImage",
74
+ "experimental_generateSpeech",
75
+ "generateImage",
76
+ "generateSpeech",
77
+ "experimental_transcribe",
78
+ ];