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.
- package/CHANGELOG.md +168 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/examples/discoverandgo/README.md +57 -0
- package/examples/discoverandgo/book_discoverandgo_museum_pass/cron.json +8 -0
- package/examples/discoverandgo/book_discoverandgo_museum_pass/index.ts +89 -0
- package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +39 -0
- package/examples/echo/README.md +37 -0
- package/examples/echo/echo_test/index.ts +31 -0
- package/examples/google-flights/search_google_flights/index.ts +101 -0
- package/examples/google-flights/search_google_flights/parser.test.ts +140 -0
- package/examples/google-flights/search_google_flights/parser.ts +189 -0
- package/examples/google-flights/search_google_flights/playbook.yaml +130 -0
- package/examples/google-flights/search_google_flights/workflow.json +48 -0
- package/examples/google-hotels/search_google_hotels/index.ts +194 -0
- package/examples/google-hotels/search_google_hotels/parser.test.ts +168 -0
- package/examples/google-hotels/search_google_hotels/parser.ts +330 -0
- package/examples/google-hotels/search_google_hotels/playbook.yaml +125 -0
- package/examples/google-hotels/search_google_hotels/workflow.json +111 -0
- package/examples/namecheap-domains/search_namecheap_domains/index.ts +144 -0
- package/examples/namecheap-domains/search_namecheap_domains/parser.ts +380 -0
- package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +50 -0
- package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +136 -0
- package/examples/namecheap-domains/search_namecheap_domains/workflow.json +97 -0
- package/examples/southwest/README.md +81 -0
- package/examples/southwest/search_southwest_flights/backends.json +23 -0
- package/examples/southwest/search_southwest_flights/cron.json +19 -0
- package/examples/southwest/search_southwest_flights/index.ts +110 -0
- package/examples/southwest/search_southwest_flights/playbook.yaml +46 -0
- package/examples/southwest/search_southwest_flights/workflow.json +54 -0
- package/package.json +78 -0
- package/prompts/compile-agent.md +580 -0
- package/prompts/intent-detection.md +198 -0
- package/prompts/playbook-compilation.md +279 -0
- package/prompts/request-triage.md +74 -0
- package/prompts/tool-candidate-detection.md +104 -0
- package/src/cli.ts +1287 -0
- package/src/imprint/agent.ts +468 -0
- package/src/imprint/app-api-hosts.ts +53 -0
- package/src/imprint/backend-ladder.ts +568 -0
- package/src/imprint/check.ts +136 -0
- package/src/imprint/chromium.ts +211 -0
- package/src/imprint/claude-cli-compile.ts +640 -0
- package/src/imprint/cli-credential.ts +394 -0
- package/src/imprint/codex-cli-compile.ts +712 -0
- package/src/imprint/compile-agent-types.ts +40 -0
- package/src/imprint/compile-agent.ts +404 -0
- package/src/imprint/compile-tools.ts +1389 -0
- package/src/imprint/compile.ts +720 -0
- package/src/imprint/cookie-jar.ts +246 -0
- package/src/imprint/credential-bundle.ts +195 -0
- package/src/imprint/credential-extract.ts +290 -0
- package/src/imprint/credential-store.ts +707 -0
- package/src/imprint/cron.ts +312 -0
- package/src/imprint/doctor.ts +223 -0
- package/src/imprint/emit.ts +154 -0
- package/src/imprint/etld.ts +134 -0
- package/src/imprint/freeform-redact.ts +216 -0
- package/src/imprint/inject-listener.ts +137 -0
- package/src/imprint/install.ts +795 -0
- package/src/imprint/integrations.ts +385 -0
- package/src/imprint/is-compiled.ts +2 -0
- package/src/imprint/json-path.ts +100 -0
- package/src/imprint/llm.ts +998 -0
- package/src/imprint/load-json.ts +54 -0
- package/src/imprint/log.ts +33 -0
- package/src/imprint/login.ts +166 -0
- package/src/imprint/mcp-compile-server.ts +282 -0
- package/src/imprint/mcp-maintenance.ts +1790 -0
- package/src/imprint/mcp-server.ts +350 -0
- package/src/imprint/multi-progress.ts +69 -0
- package/src/imprint/notify.ts +155 -0
- package/src/imprint/paths.ts +64 -0
- package/src/imprint/playbook-parser.ts +21 -0
- package/src/imprint/playbook-runner.ts +465 -0
- package/src/imprint/probe-backends.ts +251 -0
- package/src/imprint/progress.ts +28 -0
- package/src/imprint/record.ts +470 -0
- package/src/imprint/redact.ts +550 -0
- package/src/imprint/replay-capture.ts +387 -0
- package/src/imprint/request-context.ts +66 -0
- package/src/imprint/runtime-link.ts +73 -0
- package/src/imprint/runtime.ts +942 -0
- package/src/imprint/sensitive-keys.ts +156 -0
- package/src/imprint/session-diff.ts +409 -0
- package/src/imprint/session-merge.ts +198 -0
- package/src/imprint/session-writer.ts +149 -0
- package/src/imprint/sites.ts +27 -0
- package/src/imprint/stealth-fetch.ts +434 -0
- package/src/imprint/teach-state.ts +235 -0
- package/src/imprint/teach.ts +2120 -0
- package/src/imprint/tool-candidates.ts +423 -0
- package/src/imprint/tool-loader.ts +186 -0
- package/src/imprint/tool-selection.ts +70 -0
- package/src/imprint/tracing.ts +508 -0
- package/src/imprint/types.ts +472 -0
- 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
|
+
}
|