imprint-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/examples/discoverandgo/README.md +57 -0
  5. package/examples/discoverandgo/book_discoverandgo_museum_pass/cron.json +8 -0
  6. package/examples/discoverandgo/book_discoverandgo_museum_pass/index.ts +89 -0
  7. package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +39 -0
  8. package/examples/echo/README.md +37 -0
  9. package/examples/echo/echo_test/index.ts +31 -0
  10. package/examples/google-flights/search_google_flights/index.ts +101 -0
  11. package/examples/google-flights/search_google_flights/parser.test.ts +140 -0
  12. package/examples/google-flights/search_google_flights/parser.ts +189 -0
  13. package/examples/google-flights/search_google_flights/playbook.yaml +130 -0
  14. package/examples/google-flights/search_google_flights/workflow.json +48 -0
  15. package/examples/google-hotels/search_google_hotels/index.ts +194 -0
  16. package/examples/google-hotels/search_google_hotels/parser.test.ts +168 -0
  17. package/examples/google-hotels/search_google_hotels/parser.ts +330 -0
  18. package/examples/google-hotels/search_google_hotels/playbook.yaml +125 -0
  19. package/examples/google-hotels/search_google_hotels/workflow.json +111 -0
  20. package/examples/namecheap-domains/search_namecheap_domains/index.ts +144 -0
  21. package/examples/namecheap-domains/search_namecheap_domains/parser.ts +380 -0
  22. package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +50 -0
  23. package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +136 -0
  24. package/examples/namecheap-domains/search_namecheap_domains/workflow.json +97 -0
  25. package/examples/southwest/README.md +81 -0
  26. package/examples/southwest/search_southwest_flights/backends.json +23 -0
  27. package/examples/southwest/search_southwest_flights/cron.json +19 -0
  28. package/examples/southwest/search_southwest_flights/index.ts +110 -0
  29. package/examples/southwest/search_southwest_flights/playbook.yaml +46 -0
  30. package/examples/southwest/search_southwest_flights/workflow.json +54 -0
  31. package/package.json +78 -0
  32. package/prompts/compile-agent.md +580 -0
  33. package/prompts/intent-detection.md +198 -0
  34. package/prompts/playbook-compilation.md +279 -0
  35. package/prompts/request-triage.md +74 -0
  36. package/prompts/tool-candidate-detection.md +104 -0
  37. package/src/cli.ts +1287 -0
  38. package/src/imprint/agent.ts +468 -0
  39. package/src/imprint/app-api-hosts.ts +53 -0
  40. package/src/imprint/backend-ladder.ts +568 -0
  41. package/src/imprint/check.ts +136 -0
  42. package/src/imprint/chromium.ts +211 -0
  43. package/src/imprint/claude-cli-compile.ts +640 -0
  44. package/src/imprint/cli-credential.ts +394 -0
  45. package/src/imprint/codex-cli-compile.ts +712 -0
  46. package/src/imprint/compile-agent-types.ts +40 -0
  47. package/src/imprint/compile-agent.ts +404 -0
  48. package/src/imprint/compile-tools.ts +1389 -0
  49. package/src/imprint/compile.ts +720 -0
  50. package/src/imprint/cookie-jar.ts +246 -0
  51. package/src/imprint/credential-bundle.ts +195 -0
  52. package/src/imprint/credential-extract.ts +290 -0
  53. package/src/imprint/credential-store.ts +707 -0
  54. package/src/imprint/cron.ts +312 -0
  55. package/src/imprint/doctor.ts +223 -0
  56. package/src/imprint/emit.ts +154 -0
  57. package/src/imprint/etld.ts +134 -0
  58. package/src/imprint/freeform-redact.ts +216 -0
  59. package/src/imprint/inject-listener.ts +137 -0
  60. package/src/imprint/install.ts +795 -0
  61. package/src/imprint/integrations.ts +385 -0
  62. package/src/imprint/is-compiled.ts +2 -0
  63. package/src/imprint/json-path.ts +100 -0
  64. package/src/imprint/llm.ts +998 -0
  65. package/src/imprint/load-json.ts +54 -0
  66. package/src/imprint/log.ts +33 -0
  67. package/src/imprint/login.ts +166 -0
  68. package/src/imprint/mcp-compile-server.ts +282 -0
  69. package/src/imprint/mcp-maintenance.ts +1790 -0
  70. package/src/imprint/mcp-server.ts +350 -0
  71. package/src/imprint/multi-progress.ts +69 -0
  72. package/src/imprint/notify.ts +155 -0
  73. package/src/imprint/paths.ts +64 -0
  74. package/src/imprint/playbook-parser.ts +21 -0
  75. package/src/imprint/playbook-runner.ts +465 -0
  76. package/src/imprint/probe-backends.ts +251 -0
  77. package/src/imprint/progress.ts +28 -0
  78. package/src/imprint/record.ts +470 -0
  79. package/src/imprint/redact.ts +550 -0
  80. package/src/imprint/replay-capture.ts +387 -0
  81. package/src/imprint/request-context.ts +66 -0
  82. package/src/imprint/runtime-link.ts +73 -0
  83. package/src/imprint/runtime.ts +942 -0
  84. package/src/imprint/sensitive-keys.ts +156 -0
  85. package/src/imprint/session-diff.ts +409 -0
  86. package/src/imprint/session-merge.ts +198 -0
  87. package/src/imprint/session-writer.ts +149 -0
  88. package/src/imprint/sites.ts +27 -0
  89. package/src/imprint/stealth-fetch.ts +434 -0
  90. package/src/imprint/teach-state.ts +235 -0
  91. package/src/imprint/teach.ts +2120 -0
  92. package/src/imprint/tool-candidates.ts +423 -0
  93. package/src/imprint/tool-loader.ts +186 -0
  94. package/src/imprint/tool-selection.ts +70 -0
  95. package/src/imprint/tracing.ts +508 -0
  96. package/src/imprint/types.ts +472 -0
  97. package/src/imprint/version.ts +21 -0
@@ -0,0 +1,508 @@
1
+ import {
2
+ MimeType,
3
+ type NodeTracerProvider,
4
+ type OpenInferenceSpanKind,
5
+ SemanticConventions,
6
+ SpanStatusCode,
7
+ getLLMAttributes,
8
+ register,
9
+ trace,
10
+ } from '@arizeai/phoenix-otel';
11
+ import type { AttributeValue, Attributes, Span } from '@opentelemetry/api';
12
+
13
+ type TraceKind = OpenInferenceSpanKind | `${OpenInferenceSpanKind}`;
14
+ type TraceAttributes = Record<string, unknown>;
15
+ type TraceLlmMessage = { role?: string; content?: string };
16
+
17
+ let provider: NodeTracerProvider | null = null;
18
+ let attemptedInit = false;
19
+ let suppressInit = false;
20
+ const NOOP_SPAN: Span = trace.wrapSpanContext({
21
+ traceId: '0'.repeat(32),
22
+ spanId: '0'.repeat(16),
23
+ traceFlags: 0,
24
+ });
25
+
26
+ export function suppressTracingInit(): void {
27
+ suppressInit = true;
28
+ }
29
+ const DEFAULT_TRACE_IO_MAX_CHARS = 50_000;
30
+ const CACHE_READ_MULTIPLIER = 0.1;
31
+ const CACHE_WRITE_MULTIPLIER = 1.25;
32
+
33
+ function isTracingEnabled(): boolean {
34
+ return (
35
+ isTruthy(process.env.IMPRINT_TRACE) ||
36
+ isTruthy(process.env.IMPRINT_TRACING) ||
37
+ isTruthy(process.env.OPENINFERENCE_TRACE) ||
38
+ !!process.env.PHOENIX_COLLECTOR_ENDPOINT ||
39
+ !!process.env.PHOENIX_HOST
40
+ );
41
+ }
42
+
43
+ function validateTracingUrl(raw: string | undefined): string | undefined {
44
+ if (!raw) return undefined;
45
+ try {
46
+ const parsed = new URL(raw);
47
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
48
+ process.stderr.write(
49
+ `[imprint] warning: ignoring tracing endpoint with unsupported protocol: ${raw}\n`,
50
+ );
51
+ return undefined;
52
+ }
53
+ return raw;
54
+ } catch {
55
+ process.stderr.write(`[imprint] warning: ignoring invalid tracing endpoint URL: ${raw}\n`);
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ function ensureTracingInitialized(): void {
61
+ if (attemptedInit || suppressInit || !isTracingEnabled()) return;
62
+ attemptedInit = true;
63
+ // The OTEL SDK default is 128 attributes per span. getLLMAttributes() flattens
64
+ // each input message into ~2+ attributes (role, content, tool_calls…), so a
65
+ // 60-message conversation exceeds the cap and silently drops later attributes
66
+ // including token_count and finish_reason. Bump to 1000 to avoid this.
67
+ if (!process.env.OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT) {
68
+ process.env.OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = '1000';
69
+ }
70
+ const url = validateTracingUrl(
71
+ process.env.PHOENIX_COLLECTOR_ENDPOINT ?? process.env.PHOENIX_HOST,
72
+ );
73
+ provider = register({
74
+ projectName: process.env.IMPRINT_TRACE_PROJECT ?? 'imprint',
75
+ url,
76
+ apiKey: process.env.PHOENIX_API_KEY,
77
+ batch: traceBatchEnabled(process.env.IMPRINT_TRACE_BATCH),
78
+ });
79
+ }
80
+
81
+ export function traceBatchEnabled(value: string | undefined): boolean {
82
+ return value === undefined ? true : isTruthy(value);
83
+ }
84
+
85
+ export function traceLlmIoEnabled(): boolean {
86
+ if (process.env.IMPRINT_TRACE_LLM_IO !== undefined)
87
+ return isTruthy(process.env.IMPRINT_TRACE_LLM_IO);
88
+ if (process.env.IMPRINT_TRACE_IO !== undefined) return isTruthy(process.env.IMPRINT_TRACE_IO);
89
+ if (process.env.IMPRINT_TRACE_FULL !== undefined) return isTruthy(process.env.IMPRINT_TRACE_FULL);
90
+ return isTracingEnabled();
91
+ }
92
+
93
+ export function traceToolIoEnabled(): boolean {
94
+ if (process.env.IMPRINT_TRACE_TOOL_IO !== undefined)
95
+ return isTruthy(process.env.IMPRINT_TRACE_TOOL_IO);
96
+ if (process.env.IMPRINT_TRACE_IO !== undefined) return isTruthy(process.env.IMPRINT_TRACE_IO);
97
+ if (process.env.IMPRINT_TRACE_FULL !== undefined) return isTruthy(process.env.IMPRINT_TRACE_FULL);
98
+ return isTracingEnabled();
99
+ }
100
+
101
+ export function traceIoMaxChars(value = process.env.IMPRINT_TRACE_IO_MAX_CHARS): number {
102
+ if (value === undefined || value === '') return DEFAULT_TRACE_IO_MAX_CHARS;
103
+ const parsed = Number(value);
104
+ if (!Number.isFinite(parsed) || Math.trunc(parsed) < 0) {
105
+ process.stderr.write(
106
+ `[imprint] warning: IMPRINT_TRACE_IO_MAX_CHARS="${value}" is not a valid non-negative integer, using default ${DEFAULT_TRACE_IO_MAX_CHARS}\n`,
107
+ );
108
+ return DEFAULT_TRACE_IO_MAX_CHARS;
109
+ }
110
+ return Math.trunc(parsed);
111
+ }
112
+
113
+ export function estimateTokensFromText(text: string): number {
114
+ if (!text) return 0;
115
+ return Math.max(1, Math.ceil(text.length / 4));
116
+ }
117
+
118
+ export function resolveTraceTokenCount(
119
+ providerTokens: number | null | undefined,
120
+ fallbackText: string | undefined,
121
+ ): { tokens?: number; source: 'provider' | 'estimated' | 'missing' } {
122
+ if (typeof providerTokens === 'number' && Number.isFinite(providerTokens)) {
123
+ // Sanity check: CLI providers sometimes report impossibly low counts
124
+ // (e.g. 6 tokens for a 50K-char prompt). Prefer estimation in that case.
125
+ if (fallbackText !== undefined && providerTokens > 0) {
126
+ const estimated = estimateTokensFromText(fallbackText);
127
+ if (estimated > 0 && providerTokens < estimated / 10) {
128
+ return { tokens: estimated, source: 'estimated' };
129
+ }
130
+ }
131
+ return { tokens: providerTokens, source: 'provider' };
132
+ }
133
+ if (fallbackText !== undefined) {
134
+ return { tokens: estimateTokensFromText(fallbackText), source: 'estimated' };
135
+ }
136
+ return { source: 'missing' };
137
+ }
138
+
139
+ const DEFAULT_MODEL_RATES: Record<string, { inputUsdPer1M: number; outputUsdPer1M: number }> = {
140
+ 'claude-opus-4-7': { inputUsdPer1M: 5, outputUsdPer1M: 25 },
141
+ 'claude-opus-4-6': { inputUsdPer1M: 5, outputUsdPer1M: 25 },
142
+ 'claude-opus-4-5': { inputUsdPer1M: 5, outputUsdPer1M: 25 },
143
+ 'claude-opus-4-1': { inputUsdPer1M: 15, outputUsdPer1M: 75 },
144
+ 'claude-sonnet-4-6': { inputUsdPer1M: 3, outputUsdPer1M: 15 },
145
+ 'claude-sonnet-4-5': { inputUsdPer1M: 3, outputUsdPer1M: 15 },
146
+ 'claude-haiku-4-5': { inputUsdPer1M: 1, outputUsdPer1M: 5 },
147
+ };
148
+
149
+ export function traceLlmCostRates(
150
+ providerName: string,
151
+ modelName?: string,
152
+ ): { inputUsdPer1M: number; outputUsdPer1M: number } | null {
153
+ const inputUsdPer1M = envNumber(rateEnvNames(providerName, modelName, 'INPUT'));
154
+ const outputUsdPer1M = envNumber(rateEnvNames(providerName, modelName, 'OUTPUT'));
155
+ if (inputUsdPer1M !== null && outputUsdPer1M !== null) {
156
+ return { inputUsdPer1M, outputUsdPer1M };
157
+ }
158
+ if (modelName) {
159
+ const defaultRate = DEFAULT_MODEL_RATES[modelName];
160
+ if (defaultRate) return defaultRate;
161
+ }
162
+ return null;
163
+ }
164
+
165
+ export function traceInputOutputAttributes(
166
+ direction: 'input' | 'output',
167
+ value: string,
168
+ mimeType: string = MimeType.TEXT,
169
+ prefix: string = direction,
170
+ ): Attributes {
171
+ const captured = captureTraceText(value);
172
+ const valueKey =
173
+ direction === 'input' ? SemanticConventions.INPUT_VALUE : SemanticConventions.OUTPUT_VALUE;
174
+ const mimeKey =
175
+ direction === 'input'
176
+ ? SemanticConventions.INPUT_MIME_TYPE
177
+ : SemanticConventions.OUTPUT_MIME_TYPE;
178
+ return {
179
+ [valueKey]: captured.text,
180
+ [mimeKey]: mimeType,
181
+ [`imprint.trace.${prefix}.chars`]: captured.originalChars,
182
+ [`imprint.trace.${prefix}.truncated`]: captured.truncated,
183
+ ...(captured.maxChars === null
184
+ ? {}
185
+ : { [`imprint.trace.${prefix}.max_chars`]: captured.maxChars }),
186
+ };
187
+ }
188
+
189
+ export function traceJsonInputOutputAttributes(
190
+ direction: 'input' | 'output',
191
+ value: unknown,
192
+ prefix: string = direction,
193
+ ): Attributes {
194
+ return traceInputOutputAttributes(direction, stringifyTraceValue(value), MimeType.JSON, prefix);
195
+ }
196
+
197
+ export async function shutdownTracing(): Promise<void> {
198
+ if (!provider) return;
199
+ const activeProvider = provider;
200
+ provider = null;
201
+ await activeProvider.shutdown();
202
+ }
203
+
204
+ export async function traced<T>(
205
+ name: string,
206
+ kind: TraceKind,
207
+ attributes: TraceAttributes | undefined,
208
+ fn: (span: Span) => Promise<T> | T,
209
+ ): Promise<T> {
210
+ if (!isTracingEnabled()) {
211
+ return await fn(NOOP_SPAN);
212
+ }
213
+ ensureTracingInitialized();
214
+ const tracer = trace.getTracer('imprint');
215
+ return await tracer.startActiveSpan(
216
+ name,
217
+ { attributes: openInferenceAttributes(kind, attributes) },
218
+ async (span) => {
219
+ try {
220
+ const result = await fn(span);
221
+ span.setStatus({ code: SpanStatusCode.OK });
222
+ return result;
223
+ } catch (err) {
224
+ recordSpanError(span, err);
225
+ throw err;
226
+ } finally {
227
+ span.end();
228
+ }
229
+ },
230
+ );
231
+ }
232
+
233
+ export function startTraceSpan(
234
+ name: string,
235
+ kind: TraceKind,
236
+ attributes?: TraceAttributes,
237
+ ): Span | null {
238
+ if (!isTracingEnabled()) return null;
239
+ ensureTracingInitialized();
240
+ return trace.getTracer('imprint').startSpan(name, {
241
+ attributes: openInferenceAttributes(kind, attributes),
242
+ });
243
+ }
244
+
245
+ export function setSpanAttributes(
246
+ span: Span | null | undefined,
247
+ attributes: TraceAttributes,
248
+ ): void {
249
+ if (!span) return;
250
+ span.setAttributes(cleanAttributes(attributes));
251
+ }
252
+
253
+ export function endTraceSpan(span: Span | null | undefined, err?: unknown): void {
254
+ if (!span) return;
255
+ if (err) {
256
+ recordSpanError(span, err);
257
+ } else {
258
+ span.setStatus({ code: SpanStatusCode.OK });
259
+ }
260
+ span.end();
261
+ }
262
+
263
+ export function llmSpanAttributes(opts: {
264
+ provider: string;
265
+ model?: string;
266
+ inputTokens?: number | null;
267
+ outputTokens?: number | null;
268
+ cacheReadTokens?: number | null;
269
+ cacheWriteTokens?: number | null;
270
+ tokenCountsEstimated?: boolean;
271
+ inputTokenSource?: string;
272
+ outputTokenSource?: string;
273
+ stopReason?: string | null;
274
+ inputMessages?: TraceLlmMessage[];
275
+ outputMessages?: TraceLlmMessage[];
276
+ inputValue?: string;
277
+ outputValue?: string;
278
+ inputMimeType?: string;
279
+ outputMimeType?: string;
280
+ invocationParameters?: Record<string, unknown>;
281
+ }): Attributes {
282
+ const prompt = opts.inputTokens ?? undefined;
283
+ const completion = opts.outputTokens ?? undefined;
284
+ const costRates = traceLlmCostRates(opts.provider, opts.model);
285
+ const cost =
286
+ costRates && (prompt !== undefined || completion !== undefined)
287
+ ? llmCostAttributes({
288
+ inputTokens: prompt,
289
+ outputTokens: completion,
290
+ cacheReadTokens: opts.cacheReadTokens ?? undefined,
291
+ cacheWriteTokens: opts.cacheWriteTokens ?? undefined,
292
+ inputUsdPer1M: costRates.inputUsdPer1M,
293
+ outputUsdPer1M: costRates.outputUsdPer1M,
294
+ })
295
+ : {};
296
+ return {
297
+ ...getLLMAttributes({
298
+ provider: openInferenceProvider(opts.provider),
299
+ system: opts.provider,
300
+ modelName: opts.model,
301
+ invocationParameters: opts.invocationParameters,
302
+ inputMessages: opts.inputMessages,
303
+ outputMessages: opts.outputMessages,
304
+ tokenCount:
305
+ prompt === undefined && completion === undefined
306
+ ? undefined
307
+ : {
308
+ prompt,
309
+ completion,
310
+ total:
311
+ prompt === undefined && completion === undefined
312
+ ? undefined
313
+ : (prompt ?? 0) + (completion ?? 0),
314
+ },
315
+ }),
316
+ ...(opts.inputValue
317
+ ? traceInputOutputAttributes('input', opts.inputValue, opts.inputMimeType ?? MimeType.TEXT)
318
+ : {}),
319
+ ...(opts.outputValue
320
+ ? traceInputOutputAttributes('output', opts.outputValue, opts.outputMimeType ?? MimeType.TEXT)
321
+ : {}),
322
+ ...cost,
323
+ ...(opts.stopReason ? { [SemanticConventions.LLM_FINISH_REASON]: opts.stopReason } : {}),
324
+ 'imprint.llm.provider': opts.provider,
325
+ ...(opts.tokenCountsEstimated !== undefined
326
+ ? { 'imprint.llm.tokens_estimated': opts.tokenCountsEstimated }
327
+ : {}),
328
+ ...(opts.inputTokenSource ? { 'imprint.llm.input_tokens_source': opts.inputTokenSource } : {}),
329
+ ...(opts.outputTokenSource
330
+ ? { 'imprint.llm.output_tokens_source': opts.outputTokenSource }
331
+ : {}),
332
+ ...(costRates
333
+ ? {
334
+ 'imprint.llm.cost.input_usd_per_1m': costRates.inputUsdPer1M,
335
+ 'imprint.llm.cost.output_usd_per_1m': costRates.outputUsdPer1M,
336
+ }
337
+ : {}),
338
+ };
339
+ }
340
+
341
+ export function traceLlmMessages(messages: TraceLlmMessage[]): TraceLlmMessage[] {
342
+ return messages.map((message) => ({
343
+ ...message,
344
+ content: message.content === undefined ? undefined : captureTraceText(message.content).text,
345
+ }));
346
+ }
347
+
348
+ function openInferenceAttributes(kind: TraceKind, attributes?: TraceAttributes): Attributes {
349
+ return cleanAttributes({
350
+ [SemanticConventions.OPENINFERENCE_SPAN_KIND]: kind,
351
+ ...attributes,
352
+ });
353
+ }
354
+
355
+ function cleanAttributes(attributes: TraceAttributes): Attributes {
356
+ const out: Attributes = {};
357
+ for (const [key, value] of Object.entries(attributes)) {
358
+ const cleaned = cleanAttributeValue(value);
359
+ if (cleaned !== undefined) out[key] = cleaned;
360
+ }
361
+ return out;
362
+ }
363
+
364
+ function cleanAttributeValue(value: unknown): AttributeValue | undefined {
365
+ if (value === undefined || value === null) return undefined;
366
+ if (Array.isArray(value)) {
367
+ if (value.every((v) => typeof v === 'string')) return value;
368
+ if (value.every((v) => typeof v === 'number')) return value;
369
+ if (value.every((v) => typeof v === 'boolean')) return value;
370
+ return JSON.stringify(value);
371
+ }
372
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
373
+ return value;
374
+ }
375
+ return JSON.stringify(value);
376
+ }
377
+
378
+ function recordSpanError(span: Span, err: unknown): void {
379
+ const error = err instanceof Error ? err : new Error(String(err));
380
+ span.recordException(error);
381
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
382
+ }
383
+
384
+ function openInferenceProvider(provider: string): string {
385
+ if (provider === 'codex-cli') return 'openai';
386
+ if (provider === 'claude-cli' || provider === 'anthropic-api') return 'anthropic';
387
+ return provider;
388
+ }
389
+
390
+ function captureTraceText(text: string): {
391
+ text: string;
392
+ originalChars: number;
393
+ truncated: boolean;
394
+ maxChars: number | null;
395
+ } {
396
+ const maxChars = traceIoMaxChars();
397
+ if (text.length <= maxChars) {
398
+ return {
399
+ text,
400
+ originalChars: text.length,
401
+ truncated: false,
402
+ maxChars,
403
+ };
404
+ }
405
+ if (maxChars === 0) {
406
+ return {
407
+ text: `...[truncated ${text.length} chars]`,
408
+ originalChars: text.length,
409
+ truncated: true,
410
+ maxChars,
411
+ };
412
+ }
413
+ return {
414
+ text: `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]`,
415
+ originalChars: text.length,
416
+ truncated: true,
417
+ maxChars,
418
+ };
419
+ }
420
+
421
+ function stringifyTraceValue(value: unknown): string {
422
+ if (typeof value === 'string') return value;
423
+ try {
424
+ return JSON.stringify(value);
425
+ } catch {
426
+ return String(value);
427
+ }
428
+ }
429
+
430
+ function llmCostAttributes(opts: {
431
+ inputTokens?: number;
432
+ outputTokens?: number;
433
+ cacheReadTokens?: number;
434
+ cacheWriteTokens?: number;
435
+ inputUsdPer1M: number;
436
+ outputUsdPer1M: number;
437
+ }): Attributes {
438
+ const cacheRead = opts.cacheReadTokens ?? 0;
439
+ const cacheWrite = opts.cacheWriteTokens ?? 0;
440
+ const hasCacheBreakdown = cacheRead > 0 || cacheWrite > 0;
441
+ const uncachedInput =
442
+ opts.inputTokens === undefined
443
+ ? undefined
444
+ : hasCacheBreakdown
445
+ ? Math.max(0, opts.inputTokens - cacheRead - cacheWrite)
446
+ : opts.inputTokens;
447
+ const prompt =
448
+ uncachedInput === undefined
449
+ ? undefined
450
+ : hasCacheBreakdown
451
+ ? (uncachedInput / 1_000_000) * opts.inputUsdPer1M +
452
+ (cacheRead / 1_000_000) * opts.inputUsdPer1M * CACHE_READ_MULTIPLIER +
453
+ (cacheWrite / 1_000_000) * opts.inputUsdPer1M * CACHE_WRITE_MULTIPLIER
454
+ : (uncachedInput / 1_000_000) * opts.inputUsdPer1M;
455
+ const completion =
456
+ opts.outputTokens === undefined
457
+ ? undefined
458
+ : (opts.outputTokens / 1_000_000) * opts.outputUsdPer1M;
459
+ const total = (prompt ?? 0) + (completion ?? 0);
460
+ return {
461
+ ...(prompt !== undefined ? { [SemanticConventions.LLM_COST_PROMPT]: prompt } : {}),
462
+ ...(completion !== undefined ? { [SemanticConventions.LLM_COST_COMPLETION]: completion } : {}),
463
+ [SemanticConventions.LLM_COST_TOTAL]: total,
464
+ 'imprint.llm.cost_estimated': true,
465
+ };
466
+ }
467
+
468
+ function rateEnvNames(
469
+ providerName: string,
470
+ modelName: string | undefined,
471
+ side: 'INPUT' | 'OUTPUT',
472
+ ): string[] {
473
+ const providerKey = envKey(providerName);
474
+ const modelKey = modelName ? envKey(modelName) : undefined;
475
+ const aliases = side === 'INPUT' ? ['INPUT', 'PROMPT'] : ['OUTPUT', 'COMPLETION'];
476
+ const names: string[] = [];
477
+ for (const alias of aliases) {
478
+ if (providerKey && modelKey) {
479
+ names.push(`IMPRINT_TRACE_COST_${providerKey}_${modelKey}_${alias}_USD_PER_1M`);
480
+ }
481
+ if (modelKey) names.push(`IMPRINT_TRACE_COST_${modelKey}_${alias}_USD_PER_1M`);
482
+ if (providerKey) names.push(`IMPRINT_TRACE_COST_${providerKey}_${alias}_USD_PER_1M`);
483
+ names.push(`IMPRINT_TRACE_${alias}_USD_PER_1M`);
484
+ }
485
+ return names;
486
+ }
487
+
488
+ function envKey(value: string): string {
489
+ return value
490
+ .toUpperCase()
491
+ .replace(/[^A-Z0-9]+/g, '_')
492
+ .replace(/^_+|_+$/g, '');
493
+ }
494
+
495
+ function envNumber(names: string[]): number | null {
496
+ for (const name of names) {
497
+ const raw = process.env[name];
498
+ if (raw === undefined || raw === '') continue;
499
+ const parsed = Number(raw);
500
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
501
+ }
502
+ return null;
503
+ }
504
+
505
+ function isTruthy(value: string | undefined): boolean {
506
+ if (!value) return false;
507
+ return !['0', 'false', 'no', 'off'].includes(value.toLowerCase());
508
+ }