@tyroneross/navgator 0.1.1 → 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/dist/cli/index.js +201 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/diagram.d.ts.map +1 -1
- package/dist/diagram.js +33 -0
- package/dist/diagram.js.map +1 -1
- package/dist/scanner.d.ts +2 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +131 -12
- package/dist/scanner.js.map +1 -1
- package/dist/scanners/connections/ast-scanner.d.ts.map +1 -1
- package/dist/scanners/connections/ast-scanner.js +0 -3
- package/dist/scanners/connections/ast-scanner.js.map +1 -1
- package/dist/scanners/connections/llm-call-tracer.d.ts +83 -0
- package/dist/scanners/connections/llm-call-tracer.d.ts.map +1 -0
- package/dist/scanners/connections/llm-call-tracer.js +801 -0
- package/dist/scanners/connections/llm-call-tracer.js.map +1 -0
- package/dist/scanners/connections/service-calls.d.ts.map +1 -1
- package/dist/scanners/connections/service-calls.js +44 -47
- package/dist/scanners/connections/service-calls.js.map +1 -1
- package/dist/scanners/infrastructure/index.d.ts.map +1 -1
- package/dist/scanners/infrastructure/index.js +34 -4
- package/dist/scanners/infrastructure/index.js.map +1 -1
- package/dist/scanners/packages/swift.d.ts +14 -0
- package/dist/scanners/packages/swift.d.ts.map +1 -0
- package/dist/scanners/packages/swift.js +320 -0
- package/dist/scanners/packages/swift.js.map +1 -0
- package/dist/scanners/prompts/detector.d.ts +16 -0
- package/dist/scanners/prompts/detector.d.ts.map +1 -1
- package/dist/scanners/prompts/detector.js +90 -1
- package/dist/scanners/prompts/detector.js.map +1 -1
- package/dist/scanners/prompts/types.d.ts +4 -0
- package/dist/scanners/prompts/types.d.ts.map +1 -1
- package/dist/scanners/prompts/types.js.map +1 -1
- package/dist/scanners/swift/code-scanner.d.ts +14 -0
- package/dist/scanners/swift/code-scanner.d.ts.map +1 -0
- package/dist/scanners/swift/code-scanner.js +803 -0
- package/dist/scanners/swift/code-scanner.js.map +1 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +41 -2
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui-server.d.ts +7 -7
- package/dist/ui-server.d.ts.map +1 -1
- package/dist/ui-server.js +80 -792
- package/dist/ui-server.js.map +1 -1
- package/package.json +15 -9
- package/web/public/apple-icon.png +0 -0
- package/web/public/icon-dark-32x32.png +0 -0
- package/web/public/icon-light-32x32.png +0 -0
- package/web/public/icon.svg +26 -0
- package/web/public/navgator-logo.png +0 -0
- package/web/public/placeholder-logo.png +0 -0
- package/web/public/placeholder-logo.svg +1 -0
- package/web/public/placeholder-user.jpg +0 -0
- package/web/public/placeholder.jpg +0 -0
- package/web/public/placeholder.svg +1 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Call Tracer
|
|
3
|
+
*
|
|
4
|
+
* Anchor-based detection of AI/LLM API calls in source code.
|
|
5
|
+
* Instead of searching for "prompt-like" patterns everywhere,
|
|
6
|
+
* starts from unambiguous API call sites and traces backwards
|
|
7
|
+
* to find the provider, model, prompt content, and configuration.
|
|
8
|
+
*
|
|
9
|
+
* 4-pass approach:
|
|
10
|
+
* Pass 1: Find SDK imports & client initializations
|
|
11
|
+
* Pass 2: Find API call sites (anchors)
|
|
12
|
+
* Pass 3: Map wrapper functions
|
|
13
|
+
* Pass 4: Extract call arguments (model, messages, config)
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { glob } from 'glob';
|
|
18
|
+
import { generateConnectionId, generateComponentId, } from '../../types.js';
|
|
19
|
+
const SDK_DEFINITIONS = [
|
|
20
|
+
// OpenAI
|
|
21
|
+
{
|
|
22
|
+
packageNames: ['openai'],
|
|
23
|
+
providerName: 'openai',
|
|
24
|
+
classNames: ['OpenAI', 'OpenAIApi'],
|
|
25
|
+
callPatterns: [
|
|
26
|
+
{ pattern: /\.chat\.completions\.create\s*\(/, method: 'chat.completions.create', callType: 'chat', requiresClientVar: true },
|
|
27
|
+
{ pattern: /\.completions\.create\s*\(/, method: 'completions.create', callType: 'completion', requiresClientVar: true },
|
|
28
|
+
{ pattern: /\.embeddings\.create\s*\(/, method: 'embeddings.create', callType: 'embedding', requiresClientVar: true },
|
|
29
|
+
{ pattern: /\.images\.generate\s*\(/, method: 'images.generate', callType: 'image', requiresClientVar: true },
|
|
30
|
+
{ pattern: /\.audio\.transcriptions\s*\.create\s*\(/, method: 'audio.transcriptions.create', callType: 'audio', requiresClientVar: true },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
// Anthropic
|
|
34
|
+
{
|
|
35
|
+
packageNames: ['@anthropic-ai/sdk'],
|
|
36
|
+
providerName: 'anthropic',
|
|
37
|
+
classNames: ['Anthropic'],
|
|
38
|
+
callPatterns: [
|
|
39
|
+
{ pattern: /\.messages\.create\s*\(/, method: 'messages.create', callType: 'chat', requiresClientVar: true },
|
|
40
|
+
{ pattern: /\.completions\.create\s*\(/, method: 'completions.create', callType: 'completion', requiresClientVar: true },
|
|
41
|
+
{ pattern: /\.beta\./, method: 'beta', callType: 'chat', requiresClientVar: true },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
// Groq
|
|
45
|
+
{
|
|
46
|
+
packageNames: ['groq-sdk'],
|
|
47
|
+
providerName: 'groq',
|
|
48
|
+
classNames: ['Groq'],
|
|
49
|
+
callPatterns: [
|
|
50
|
+
{ pattern: /\.chat\.completions\.create\s*\(/, method: 'chat.completions.create', callType: 'chat', requiresClientVar: true },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
// Cohere
|
|
54
|
+
{
|
|
55
|
+
packageNames: ['cohere-ai', 'cohere'],
|
|
56
|
+
providerName: 'cohere',
|
|
57
|
+
classNames: ['CohereClient', 'Cohere'],
|
|
58
|
+
callPatterns: [
|
|
59
|
+
{ pattern: /\.generate\s*\(/, method: 'generate', callType: 'completion', requiresClientVar: true },
|
|
60
|
+
{ pattern: /\.chat\s*\(/, method: 'chat', callType: 'chat', requiresClientVar: true },
|
|
61
|
+
{ pattern: /\.embed\s*\(/, method: 'embed', callType: 'embedding', requiresClientVar: true },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
// Mistral
|
|
65
|
+
{
|
|
66
|
+
packageNames: ['@mistralai/mistralai'],
|
|
67
|
+
providerName: 'mistral',
|
|
68
|
+
classNames: ['MistralClient', 'Mistral'],
|
|
69
|
+
callPatterns: [
|
|
70
|
+
{ pattern: /\.chat\s*\(/, method: 'chat', callType: 'chat', requiresClientVar: true },
|
|
71
|
+
{ pattern: /\.chatStream\s*\(/, method: 'chatStream', callType: 'chat', requiresClientVar: true },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
// Vercel AI SDK (functional, no client var)
|
|
75
|
+
{
|
|
76
|
+
packageNames: ['ai', '@ai-sdk/openai', '@ai-sdk/anthropic', '@ai-sdk/google'],
|
|
77
|
+
providerName: 'vercel-ai-sdk',
|
|
78
|
+
classNames: [],
|
|
79
|
+
callPatterns: [
|
|
80
|
+
{ pattern: /\bgenerateText\s*\(/, method: 'generateText', callType: 'completion', requiresClientVar: false },
|
|
81
|
+
{ pattern: /\bstreamText\s*\(/, method: 'streamText', callType: 'chat', requiresClientVar: false },
|
|
82
|
+
{ pattern: /\bgenerateObject\s*\(/, method: 'generateObject', callType: 'function-call', requiresClientVar: false },
|
|
83
|
+
{ pattern: /\bstreamObject\s*\(/, method: 'streamObject', callType: 'function-call', requiresClientVar: false },
|
|
84
|
+
{ pattern: /\bembed\s*\(/, method: 'embed', callType: 'embedding', requiresClientVar: false },
|
|
85
|
+
{ pattern: /\bembedMany\s*\(/, method: 'embedMany', callType: 'embedding', requiresClientVar: false },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
// LangChain
|
|
89
|
+
{
|
|
90
|
+
packageNames: ['@langchain/openai', '@langchain/anthropic', '@langchain/groq', '@langchain/core', '@langchain/community', 'langchain'],
|
|
91
|
+
providerName: 'langchain',
|
|
92
|
+
classNames: ['ChatOpenAI', 'ChatAnthropic', 'ChatGroq', 'ChatGoogleGenerativeAI'],
|
|
93
|
+
callPatterns: [
|
|
94
|
+
{ pattern: /\.invoke\s*\(/, method: 'invoke', callType: 'chat', requiresClientVar: true },
|
|
95
|
+
{ pattern: /\.call\s*\(/, method: 'call', callType: 'chat', requiresClientVar: true },
|
|
96
|
+
{ pattern: /\.stream\s*\(/, method: 'stream', callType: 'chat', requiresClientVar: true },
|
|
97
|
+
{ pattern: /\.batch\s*\(/, method: 'batch', callType: 'chat', requiresClientVar: true },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// FILE EXCLUSIONS
|
|
103
|
+
// =============================================================================
|
|
104
|
+
function shouldExcludeFile(file) {
|
|
105
|
+
const excludePatterns = [
|
|
106
|
+
/NavGator\/src\//,
|
|
107
|
+
/NavGator\/web\//,
|
|
108
|
+
/\/__tests__\//,
|
|
109
|
+
/\/test\//,
|
|
110
|
+
/\/tests\//,
|
|
111
|
+
/\/mocks?\//,
|
|
112
|
+
/\/fixtures?\//,
|
|
113
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
114
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
115
|
+
/\.mock\.(ts|tsx|js|jsx)$/,
|
|
116
|
+
/\.(d\.ts|map|min\.js)$/,
|
|
117
|
+
/\/dist\//,
|
|
118
|
+
/\/build\//,
|
|
119
|
+
/\/generated\//,
|
|
120
|
+
];
|
|
121
|
+
return excludePatterns.some(p => p.test(file));
|
|
122
|
+
}
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// PASS 1: FIND SDK IMPORTS
|
|
125
|
+
// =============================================================================
|
|
126
|
+
function findSDKImports(content, lines, file) {
|
|
127
|
+
const imports = [];
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
const line = lines[i];
|
|
130
|
+
for (const sdk of SDK_DEFINITIONS) {
|
|
131
|
+
for (const pkg of sdk.packageNames) {
|
|
132
|
+
// Static imports: import X from 'pkg' or import { X } from 'pkg'
|
|
133
|
+
const staticImport = line.match(new RegExp(`import\\s+(?:(?:\\{\\s*([^}]+)\\s*\\})|(?:(\\w+)))\\s+from\\s+['"]${escapeRegex(pkg)}['"]`));
|
|
134
|
+
if (staticImport) {
|
|
135
|
+
const names = staticImport[1]
|
|
136
|
+
? staticImport[1].split(',').map(n => n.trim().split(/\s+as\s+/).pop().trim())
|
|
137
|
+
: [staticImport[2]];
|
|
138
|
+
imports.push({
|
|
139
|
+
file,
|
|
140
|
+
line: i + 1,
|
|
141
|
+
sdk: pkg,
|
|
142
|
+
providerName: sdk.providerName,
|
|
143
|
+
importedNames: names.filter(Boolean),
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Dynamic imports: const { X } = await import('pkg')
|
|
148
|
+
const dynamicImport = line.match(new RegExp(`(?:const|let|var)\\s+(?:\\{\\s*([^}]+)\\s*\\}|(\\w+))\\s*=\\s*(?:await\\s+)?import\\s*\\(\\s*['"]${escapeRegex(pkg)}['"]`));
|
|
149
|
+
if (dynamicImport) {
|
|
150
|
+
const names = dynamicImport[1]
|
|
151
|
+
? dynamicImport[1].split(',').map(n => n.trim())
|
|
152
|
+
: [dynamicImport[2]];
|
|
153
|
+
imports.push({
|
|
154
|
+
file,
|
|
155
|
+
line: i + 1,
|
|
156
|
+
sdk: pkg,
|
|
157
|
+
providerName: sdk.providerName,
|
|
158
|
+
importedNames: names.filter(Boolean),
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// require(): const X = require('pkg')
|
|
163
|
+
const requireImport = line.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*['"]${escapeRegex(pkg)}['"]`));
|
|
164
|
+
if (requireImport) {
|
|
165
|
+
imports.push({
|
|
166
|
+
file,
|
|
167
|
+
line: i + 1,
|
|
168
|
+
sdk: pkg,
|
|
169
|
+
providerName: sdk.providerName,
|
|
170
|
+
importedNames: [requireImport[1]],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return imports;
|
|
177
|
+
}
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// PASS 1B: FIND CLIENT INITIALIZATIONS
|
|
180
|
+
// =============================================================================
|
|
181
|
+
function findClientInits(lines, file, imports) {
|
|
182
|
+
const inits = [];
|
|
183
|
+
// Build set of imported class names for this file
|
|
184
|
+
const importedClasses = new Map();
|
|
185
|
+
for (const imp of imports) {
|
|
186
|
+
for (const name of imp.importedNames) {
|
|
187
|
+
importedClasses.set(name, imp);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
// Match: const x = new ClassName(...)
|
|
193
|
+
const initMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+(\w+)\s*\(/);
|
|
194
|
+
if (initMatch) {
|
|
195
|
+
const [, varName, className] = initMatch;
|
|
196
|
+
// Check if the class is from a known SDK
|
|
197
|
+
const imp = importedClasses.get(className);
|
|
198
|
+
if (imp) {
|
|
199
|
+
inits.push({
|
|
200
|
+
file,
|
|
201
|
+
line: i + 1,
|
|
202
|
+
variableName: varName,
|
|
203
|
+
sdk: imp.sdk,
|
|
204
|
+
providerName: imp.providerName,
|
|
205
|
+
className,
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Also check SDK definitions directly for the class name
|
|
210
|
+
for (const sdk of SDK_DEFINITIONS) {
|
|
211
|
+
if (sdk.classNames.includes(className)) {
|
|
212
|
+
inits.push({
|
|
213
|
+
file,
|
|
214
|
+
line: i + 1,
|
|
215
|
+
variableName: varName,
|
|
216
|
+
sdk: sdk.packageNames[0],
|
|
217
|
+
providerName: sdk.providerName,
|
|
218
|
+
className,
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Match: this.client = new ClassName(...)
|
|
225
|
+
const thisInitMatch = line.match(/this\.(\w+)\s*=\s*new\s+(\w+)\s*\(/);
|
|
226
|
+
if (thisInitMatch) {
|
|
227
|
+
const [, propName, className] = thisInitMatch;
|
|
228
|
+
const imp = importedClasses.get(className);
|
|
229
|
+
if (imp) {
|
|
230
|
+
inits.push({
|
|
231
|
+
file,
|
|
232
|
+
line: i + 1,
|
|
233
|
+
variableName: `this.${propName}`,
|
|
234
|
+
sdk: imp.sdk,
|
|
235
|
+
providerName: imp.providerName,
|
|
236
|
+
className,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return inits;
|
|
242
|
+
}
|
|
243
|
+
// =============================================================================
|
|
244
|
+
// PASS 2: FIND API CALL ANCHORS
|
|
245
|
+
// =============================================================================
|
|
246
|
+
function findCallAnchors(lines, file, imports, clientInits) {
|
|
247
|
+
const anchors = [];
|
|
248
|
+
// Build lookup of known client variables
|
|
249
|
+
const clientVars = new Map();
|
|
250
|
+
for (const init of clientInits) {
|
|
251
|
+
clientVars.set(init.variableName, init);
|
|
252
|
+
// Also track without 'this.' prefix for method access
|
|
253
|
+
if (init.variableName.startsWith('this.')) {
|
|
254
|
+
clientVars.set(init.variableName.replace('this.', ''), init);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Build set of imported function names (for functional SDKs like Vercel AI)
|
|
258
|
+
const importedFunctions = new Set();
|
|
259
|
+
for (const imp of imports) {
|
|
260
|
+
for (const name of imp.importedNames) {
|
|
261
|
+
importedFunctions.add(name);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (let i = 0; i < lines.length; i++) {
|
|
265
|
+
const line = lines[i];
|
|
266
|
+
// Skip comments
|
|
267
|
+
const trimmed = line.trimStart();
|
|
268
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('#')) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
for (const sdk of SDK_DEFINITIONS) {
|
|
272
|
+
for (const cp of sdk.callPatterns) {
|
|
273
|
+
if (!cp.pattern.test(line))
|
|
274
|
+
continue;
|
|
275
|
+
if (cp.requiresClientVar) {
|
|
276
|
+
// OOP style: find the variable calling the method
|
|
277
|
+
// Match: varName.method( or this.varName.method(
|
|
278
|
+
const varMatch = line.match(/(\w+(?:\.\w+)?)\.(?:chat|messages|completions|embeddings|images|audio|beta)/);
|
|
279
|
+
if (!varMatch)
|
|
280
|
+
continue;
|
|
281
|
+
const callerVar = varMatch[1];
|
|
282
|
+
// Check if this variable is a known client
|
|
283
|
+
const clientInit = clientVars.get(callerVar);
|
|
284
|
+
if (clientInit && clientInit.providerName === sdk.providerName) {
|
|
285
|
+
const funcName = extractFunctionName(lines, i);
|
|
286
|
+
anchors.push({
|
|
287
|
+
file,
|
|
288
|
+
line: i + 1,
|
|
289
|
+
code: line.trim().slice(0, 120),
|
|
290
|
+
method: cp.method,
|
|
291
|
+
clientVariable: callerVar,
|
|
292
|
+
providerName: sdk.providerName,
|
|
293
|
+
sdk: clientInit.sdk,
|
|
294
|
+
callType: cp.callType,
|
|
295
|
+
containingFunction: funcName,
|
|
296
|
+
});
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
// For LangChain .invoke() / .call() — match chains and LangChain model instances
|
|
300
|
+
if (sdk.providerName === 'langchain') {
|
|
301
|
+
// Check if file has any LangChain imports
|
|
302
|
+
const hasLangChainImport = imports.some(imp => imp.providerName === 'langchain');
|
|
303
|
+
if (hasLangChainImport) {
|
|
304
|
+
const funcName = extractFunctionName(lines, i);
|
|
305
|
+
anchors.push({
|
|
306
|
+
file,
|
|
307
|
+
line: i + 1,
|
|
308
|
+
code: line.trim().slice(0, 120),
|
|
309
|
+
method: cp.method,
|
|
310
|
+
clientVariable: callerVar,
|
|
311
|
+
providerName: 'langchain',
|
|
312
|
+
sdk: imports.find(imp => imp.providerName === 'langchain')?.sdk || 'langchain',
|
|
313
|
+
callType: cp.callType,
|
|
314
|
+
containingFunction: funcName,
|
|
315
|
+
});
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// Functional style (Vercel AI SDK): generateText(...)
|
|
322
|
+
// Check if the function was imported from the right package
|
|
323
|
+
const funcMatch = line.match(new RegExp(`\\b(${sdk.callPatterns.map(p => {
|
|
324
|
+
const src = p.pattern.source;
|
|
325
|
+
// Extract function name from pattern like \bgenerateText\s*\(
|
|
326
|
+
const m = src.match(/\\b(\w+)/);
|
|
327
|
+
return m ? m[1] : '';
|
|
328
|
+
}).filter(Boolean).join('|')})\\s*\\(`));
|
|
329
|
+
if (funcMatch && importedFunctions.has(funcMatch[1])) {
|
|
330
|
+
const funcName = extractFunctionName(lines, i);
|
|
331
|
+
anchors.push({
|
|
332
|
+
file,
|
|
333
|
+
line: i + 1,
|
|
334
|
+
code: line.trim().slice(0, 120),
|
|
335
|
+
method: funcMatch[1],
|
|
336
|
+
clientVariable: funcMatch[1],
|
|
337
|
+
providerName: sdk.providerName,
|
|
338
|
+
sdk: sdk.packageNames[0],
|
|
339
|
+
callType: cp.callType,
|
|
340
|
+
containingFunction: funcName,
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return anchors;
|
|
349
|
+
}
|
|
350
|
+
// =============================================================================
|
|
351
|
+
// PASS 3: MAP WRAPPER FUNCTIONS
|
|
352
|
+
// =============================================================================
|
|
353
|
+
function mapWrapperFunctions(anchors, fileContents) {
|
|
354
|
+
const wrappers = [];
|
|
355
|
+
// Group anchors by file+function
|
|
356
|
+
const anchorsByFunction = new Map();
|
|
357
|
+
for (const anchor of anchors) {
|
|
358
|
+
if (anchor.containingFunction) {
|
|
359
|
+
const key = `${anchor.file}::${anchor.containingFunction}`;
|
|
360
|
+
if (!anchorsByFunction.has(key)) {
|
|
361
|
+
anchorsByFunction.set(key, []);
|
|
362
|
+
}
|
|
363
|
+
anchorsByFunction.get(key).push(anchor);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// For each function that contains an anchor, build a wrapper entry
|
|
367
|
+
for (const [key, fnAnchors] of anchorsByFunction) {
|
|
368
|
+
const [file, funcName] = key.split('::');
|
|
369
|
+
const fileData = fileContents.get(file);
|
|
370
|
+
if (!fileData)
|
|
371
|
+
continue;
|
|
372
|
+
// Check if function has @traceable() decorator or LangSmith wrapping
|
|
373
|
+
const hasTraceable = fileData.content.includes('traceable(') ||
|
|
374
|
+
fileData.content.includes('@traceable');
|
|
375
|
+
// Detect class membership
|
|
376
|
+
const className = detectClassName(fileData.lines, fnAnchors[0].line - 1);
|
|
377
|
+
// Check if the function is exported
|
|
378
|
+
const exportedAs = detectExport(fileData.lines, funcName);
|
|
379
|
+
wrappers.push({
|
|
380
|
+
file,
|
|
381
|
+
functionName: funcName,
|
|
382
|
+
className,
|
|
383
|
+
exportedAs,
|
|
384
|
+
containedAnchors: fnAnchors,
|
|
385
|
+
hasTraceable,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return wrappers;
|
|
389
|
+
}
|
|
390
|
+
// =============================================================================
|
|
391
|
+
// PASS 4: EXTRACT CALL ARGUMENTS
|
|
392
|
+
// =============================================================================
|
|
393
|
+
function extractCallArguments(anchor, lines) {
|
|
394
|
+
const anchorLine = anchor.line - 1;
|
|
395
|
+
// Look at the anchor line and the following lines for the arguments object
|
|
396
|
+
const contextLines = lines.slice(anchorLine, Math.min(anchorLine + 30, lines.length));
|
|
397
|
+
const context = contextLines.join('\n');
|
|
398
|
+
// Extract model
|
|
399
|
+
const model = extractModel(context, lines, anchorLine);
|
|
400
|
+
// Extract prompt/messages
|
|
401
|
+
const prompt = extractPromptInfo(context, lines, anchorLine);
|
|
402
|
+
// Extract config
|
|
403
|
+
const config = extractConfig(context);
|
|
404
|
+
return { model, prompt, config };
|
|
405
|
+
}
|
|
406
|
+
function extractModel(context, lines, anchorLine) {
|
|
407
|
+
// Look for model: "value" or model: variable
|
|
408
|
+
const modelStringMatch = context.match(/model\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
409
|
+
if (modelStringMatch) {
|
|
410
|
+
return {
|
|
411
|
+
value: modelStringMatch[1],
|
|
412
|
+
isDynamic: false,
|
|
413
|
+
line: findLineOffset(lines, anchorLine, modelStringMatch[0]),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
// Model as variable reference
|
|
417
|
+
const modelVarMatch = context.match(/model\s*:\s*(\w+(?:\.\w+)*)/);
|
|
418
|
+
if (modelVarMatch) {
|
|
419
|
+
const varName = modelVarMatch[1];
|
|
420
|
+
// Try to resolve the variable value
|
|
421
|
+
const resolved = resolveVariable(lines, anchorLine, varName);
|
|
422
|
+
return {
|
|
423
|
+
value: resolved || null,
|
|
424
|
+
isDynamic: !resolved,
|
|
425
|
+
variableName: varName,
|
|
426
|
+
line: findLineOffset(lines, anchorLine, modelVarMatch[0]),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return { value: null, isDynamic: true, line: anchorLine + 1 };
|
|
430
|
+
}
|
|
431
|
+
function extractPromptInfo(context, lines, anchorLine) {
|
|
432
|
+
// Check for messages array
|
|
433
|
+
const messagesMatch = context.match(/messages\s*:\s*\[/);
|
|
434
|
+
if (messagesMatch) {
|
|
435
|
+
// Try to extract system prompt from inline messages
|
|
436
|
+
const systemMatch = context.match(/role\s*:\s*['"]system['"]\s*,\s*content\s*:\s*(?:['"`]([^'"`]{0,500})['"`]|(\w+))/);
|
|
437
|
+
// Try to extract user template
|
|
438
|
+
const userMatch = context.match(/role\s*:\s*['"]user['"]\s*,\s*content\s*:\s*(?:['"`]([^'"`]{0,500})['"`]|(\w+))/);
|
|
439
|
+
// Detect template variables
|
|
440
|
+
const variables = detectTemplateVars(context);
|
|
441
|
+
return {
|
|
442
|
+
type: 'messages-array',
|
|
443
|
+
content: userMatch?.[1] || userMatch?.[2] || undefined,
|
|
444
|
+
systemPrompt: systemMatch?.[1] || systemMatch?.[2] || undefined,
|
|
445
|
+
hasUserTemplate: !!userMatch,
|
|
446
|
+
variables,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// Check for messages as variable reference
|
|
450
|
+
const messagesVarMatch = context.match(/messages\s*:\s*(\w+)/);
|
|
451
|
+
if (messagesVarMatch) {
|
|
452
|
+
return {
|
|
453
|
+
type: 'variable-ref',
|
|
454
|
+
content: undefined,
|
|
455
|
+
systemPrompt: undefined,
|
|
456
|
+
hasUserTemplate: false,
|
|
457
|
+
variables: [],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
// Check for prompt: string (Vercel AI SDK style)
|
|
461
|
+
const promptStringMatch = context.match(/prompt\s*:\s*['"`]([^'"`]{0,500})/);
|
|
462
|
+
if (promptStringMatch) {
|
|
463
|
+
return {
|
|
464
|
+
type: 'string-prompt',
|
|
465
|
+
content: promptStringMatch[1],
|
|
466
|
+
hasUserTemplate: true,
|
|
467
|
+
variables: detectTemplateVars(promptStringMatch[1]),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
// Check for system: string (Vercel AI SDK style)
|
|
471
|
+
const systemStringMatch = context.match(/system\s*:\s*['"`]([^'"`]{0,500})/);
|
|
472
|
+
return {
|
|
473
|
+
type: 'string-prompt',
|
|
474
|
+
content: undefined,
|
|
475
|
+
systemPrompt: systemStringMatch?.[1] || undefined,
|
|
476
|
+
hasUserTemplate: false,
|
|
477
|
+
variables: [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function extractConfig(context) {
|
|
481
|
+
const config = {};
|
|
482
|
+
const tempMatch = context.match(/temperature\s*:\s*([\d.]+)/);
|
|
483
|
+
if (tempMatch)
|
|
484
|
+
config.temperature = parseFloat(tempMatch[1]);
|
|
485
|
+
const maxTokensMatch = context.match(/(?:max_tokens|maxTokens)\s*:\s*(\d+)/);
|
|
486
|
+
if (maxTokensMatch)
|
|
487
|
+
config.maxTokens = parseInt(maxTokensMatch[1]);
|
|
488
|
+
const streamMatch = context.match(/stream\s*:\s*(true|false)/);
|
|
489
|
+
if (streamMatch)
|
|
490
|
+
config.stream = streamMatch[1] === 'true';
|
|
491
|
+
const toolsMatch = context.match(/tools\s*:\s*\[/);
|
|
492
|
+
if (toolsMatch)
|
|
493
|
+
config.tools = ['detected'];
|
|
494
|
+
return config;
|
|
495
|
+
}
|
|
496
|
+
export async function traceLLMCalls(projectRoot) {
|
|
497
|
+
const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,py}', {
|
|
498
|
+
cwd: projectRoot,
|
|
499
|
+
ignore: [
|
|
500
|
+
'node_modules/**', 'dist/**', 'build/**', '.next/**',
|
|
501
|
+
'__pycache__/**', 'venv/**', '**/node_modules/**', '**/.git/**',
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
// Read all source files
|
|
505
|
+
const fileContents = new Map();
|
|
506
|
+
for (const file of sourceFiles) {
|
|
507
|
+
if (shouldExcludeFile(file))
|
|
508
|
+
continue;
|
|
509
|
+
const filePath = path.join(projectRoot, file);
|
|
510
|
+
try {
|
|
511
|
+
const stat = await fs.promises.stat(filePath);
|
|
512
|
+
if (!stat.isFile())
|
|
513
|
+
continue;
|
|
514
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
515
|
+
fileContents.set(file, { content, lines: content.split('\n') });
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// -------------------------------------------------------------------------
|
|
522
|
+
// Pass 1: Find SDK imports and client initializations
|
|
523
|
+
// -------------------------------------------------------------------------
|
|
524
|
+
const allImports = [];
|
|
525
|
+
const allClientInits = [];
|
|
526
|
+
for (const [file, { content, lines }] of fileContents) {
|
|
527
|
+
const imports = findSDKImports(content, lines, file);
|
|
528
|
+
allImports.push(...imports);
|
|
529
|
+
const inits = findClientInits(lines, file, imports);
|
|
530
|
+
allClientInits.push(...inits);
|
|
531
|
+
}
|
|
532
|
+
// -------------------------------------------------------------------------
|
|
533
|
+
// Pass 2: Find API call anchors
|
|
534
|
+
// -------------------------------------------------------------------------
|
|
535
|
+
const allAnchors = [];
|
|
536
|
+
for (const [file, { content, lines }] of fileContents) {
|
|
537
|
+
// Only scan files that have SDK imports or that use known client variables
|
|
538
|
+
const fileImports = allImports.filter(i => i.file === file);
|
|
539
|
+
const fileInits = allClientInits.filter(i => i.file === file);
|
|
540
|
+
// Also include inits from other files that might be imported here
|
|
541
|
+
const importedVars = findImportedClientVars(lines, file, allClientInits);
|
|
542
|
+
const combinedInits = [...fileInits, ...importedVars];
|
|
543
|
+
if (fileImports.length === 0 && combinedInits.length === 0)
|
|
544
|
+
continue;
|
|
545
|
+
const anchors = findCallAnchors(lines, file, fileImports, combinedInits);
|
|
546
|
+
allAnchors.push(...anchors);
|
|
547
|
+
}
|
|
548
|
+
// -------------------------------------------------------------------------
|
|
549
|
+
// Pass 3: Map wrapper functions
|
|
550
|
+
// -------------------------------------------------------------------------
|
|
551
|
+
const wrappers = mapWrapperFunctions(allAnchors, fileContents);
|
|
552
|
+
// -------------------------------------------------------------------------
|
|
553
|
+
// Pass 4: Extract arguments and build TracedLLMCalls
|
|
554
|
+
// -------------------------------------------------------------------------
|
|
555
|
+
const tracedCalls = [];
|
|
556
|
+
const seen = new Set(); // Dedup by file:line
|
|
557
|
+
for (const anchor of allAnchors) {
|
|
558
|
+
const dedupKey = `${anchor.file}:${anchor.line}`;
|
|
559
|
+
if (seen.has(dedupKey))
|
|
560
|
+
continue;
|
|
561
|
+
seen.add(dedupKey);
|
|
562
|
+
const fileData = fileContents.get(anchor.file);
|
|
563
|
+
if (!fileData)
|
|
564
|
+
continue;
|
|
565
|
+
const args = extractCallArguments(anchor, fileData.lines);
|
|
566
|
+
// Find the matching SDK import
|
|
567
|
+
const matchingImport = allImports.find(i => i.file === anchor.file && i.providerName === anchor.providerName) || allImports.find(i => i.providerName === anchor.providerName);
|
|
568
|
+
const call = {
|
|
569
|
+
id: `TRACE_${anchor.file.replace(/[^a-zA-Z0-9]/g, '_')}_L${anchor.line}`,
|
|
570
|
+
name: anchor.containingFunction || `anonymous_${anchor.line}`,
|
|
571
|
+
anchor: {
|
|
572
|
+
file: anchor.file,
|
|
573
|
+
line: anchor.line,
|
|
574
|
+
code: anchor.code,
|
|
575
|
+
method: anchor.method,
|
|
576
|
+
},
|
|
577
|
+
provider: {
|
|
578
|
+
name: anchor.providerName,
|
|
579
|
+
sdk: anchor.sdk,
|
|
580
|
+
importLine: matchingImport?.line || 0,
|
|
581
|
+
clientVariable: anchor.clientVariable,
|
|
582
|
+
},
|
|
583
|
+
model: args.model || { value: null, isDynamic: true, line: anchor.line },
|
|
584
|
+
prompt: args.prompt || {
|
|
585
|
+
type: 'variable-ref',
|
|
586
|
+
hasUserTemplate: false,
|
|
587
|
+
variables: [],
|
|
588
|
+
},
|
|
589
|
+
config: args.config || {},
|
|
590
|
+
callType: anchor.callType,
|
|
591
|
+
confidence: computeConfidence(anchor, matchingImport, args),
|
|
592
|
+
};
|
|
593
|
+
tracedCalls.push(call);
|
|
594
|
+
}
|
|
595
|
+
// -------------------------------------------------------------------------
|
|
596
|
+
// Convert to NavGator ScanResult format
|
|
597
|
+
// -------------------------------------------------------------------------
|
|
598
|
+
const scanResult = convertToScanResult(tracedCalls, allImports);
|
|
599
|
+
return { calls: tracedCalls, wrappers, scanResult };
|
|
600
|
+
}
|
|
601
|
+
// =============================================================================
|
|
602
|
+
// CONVERSION TO SCAN RESULT
|
|
603
|
+
// =============================================================================
|
|
604
|
+
function convertToScanResult(calls, imports) {
|
|
605
|
+
const components = [];
|
|
606
|
+
const connections = [];
|
|
607
|
+
const timestamp = Date.now();
|
|
608
|
+
// Create a component per unique provider
|
|
609
|
+
const providerComponents = new Map();
|
|
610
|
+
for (const call of calls) {
|
|
611
|
+
const providerKey = call.provider.name;
|
|
612
|
+
if (!providerComponents.has(providerKey)) {
|
|
613
|
+
const comp = {
|
|
614
|
+
component_id: generateComponentId('llm', providerKey),
|
|
615
|
+
name: providerKey,
|
|
616
|
+
type: 'llm',
|
|
617
|
+
role: {
|
|
618
|
+
purpose: `${providerKey} AI API`,
|
|
619
|
+
layer: 'external',
|
|
620
|
+
critical: true,
|
|
621
|
+
},
|
|
622
|
+
source: {
|
|
623
|
+
detection_method: 'auto',
|
|
624
|
+
config_files: [],
|
|
625
|
+
confidence: Math.max(...calls.filter(c => c.provider.name === providerKey).map(c => c.confidence)),
|
|
626
|
+
},
|
|
627
|
+
connects_to: [],
|
|
628
|
+
connected_from: [],
|
|
629
|
+
status: 'active',
|
|
630
|
+
tags: ['llm', 'external', providerKey],
|
|
631
|
+
timestamp,
|
|
632
|
+
last_updated: timestamp,
|
|
633
|
+
};
|
|
634
|
+
providerComponents.set(providerKey, comp);
|
|
635
|
+
components.push(comp);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Create a connection per call site
|
|
639
|
+
for (const call of calls) {
|
|
640
|
+
const providerComp = providerComponents.get(call.provider.name);
|
|
641
|
+
const conn = {
|
|
642
|
+
connection_id: generateConnectionId('service-call'),
|
|
643
|
+
from: {
|
|
644
|
+
component_id: `FILE:${call.anchor.file}`,
|
|
645
|
+
location: {
|
|
646
|
+
file: call.anchor.file,
|
|
647
|
+
line: call.anchor.line,
|
|
648
|
+
function: call.name,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
to: {
|
|
652
|
+
component_id: providerComp.component_id,
|
|
653
|
+
},
|
|
654
|
+
connection_type: 'service-call',
|
|
655
|
+
code_reference: {
|
|
656
|
+
file: call.anchor.file,
|
|
657
|
+
symbol: call.name,
|
|
658
|
+
symbol_type: 'function',
|
|
659
|
+
line_start: call.anchor.line,
|
|
660
|
+
code_snippet: call.anchor.code,
|
|
661
|
+
},
|
|
662
|
+
description: `${call.provider.name}.${call.anchor.method}${call.model.value ? ` (${call.model.value})` : ''}`,
|
|
663
|
+
detected_from: 'LLM call tracer (anchor-based)',
|
|
664
|
+
confidence: call.confidence,
|
|
665
|
+
timestamp,
|
|
666
|
+
last_verified: timestamp,
|
|
667
|
+
};
|
|
668
|
+
connections.push(conn);
|
|
669
|
+
}
|
|
670
|
+
return { components, connections, warnings: [] };
|
|
671
|
+
}
|
|
672
|
+
// =============================================================================
|
|
673
|
+
// HELPERS
|
|
674
|
+
// =============================================================================
|
|
675
|
+
function escapeRegex(str) {
|
|
676
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
677
|
+
}
|
|
678
|
+
function extractFunctionName(lines, lineIndex) {
|
|
679
|
+
for (let i = lineIndex; i >= Math.max(0, lineIndex - 30); i--) {
|
|
680
|
+
const line = lines[i];
|
|
681
|
+
// JS/TS function patterns
|
|
682
|
+
const funcMatch = line.match(/(?:async\s+)?(?:function\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|[\(:])/);
|
|
683
|
+
if (funcMatch && funcMatch[1] !== 'if' && funcMatch[1] !== 'for' && funcMatch[1] !== 'while') {
|
|
684
|
+
return funcMatch[1];
|
|
685
|
+
}
|
|
686
|
+
// Arrow function assignment
|
|
687
|
+
const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/);
|
|
688
|
+
if (arrowMatch)
|
|
689
|
+
return arrowMatch[1];
|
|
690
|
+
// Method definition
|
|
691
|
+
const methodMatch = line.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
|
|
692
|
+
if (methodMatch && methodMatch[1] !== 'if' && methodMatch[1] !== 'for') {
|
|
693
|
+
return methodMatch[1];
|
|
694
|
+
}
|
|
695
|
+
// Python function
|
|
696
|
+
const pyMatch = line.match(/(?:async\s+)?def\s+(\w+)\s*\(/);
|
|
697
|
+
if (pyMatch)
|
|
698
|
+
return pyMatch[1];
|
|
699
|
+
}
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
function detectClassName(lines, lineIndex) {
|
|
703
|
+
for (let i = lineIndex; i >= Math.max(0, lineIndex - 100); i--) {
|
|
704
|
+
const line = lines[i];
|
|
705
|
+
const classMatch = line.match(/class\s+(\w+)/);
|
|
706
|
+
if (classMatch)
|
|
707
|
+
return classMatch[1];
|
|
708
|
+
}
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
function detectExport(lines, funcName) {
|
|
712
|
+
for (const line of lines) {
|
|
713
|
+
if (line.includes(`export`) && line.includes(funcName)) {
|
|
714
|
+
return funcName;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
function detectTemplateVars(content) {
|
|
720
|
+
const vars = [];
|
|
721
|
+
const seen = new Set();
|
|
722
|
+
// JS template literals: ${varName}
|
|
723
|
+
const jsVars = content.matchAll(/\$\{(\w+)\}/g);
|
|
724
|
+
for (const m of jsVars) {
|
|
725
|
+
if (!seen.has(m[1])) {
|
|
726
|
+
seen.add(m[1]);
|
|
727
|
+
vars.push(m[1]);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// Jinja/Mustache: {{varName}}
|
|
731
|
+
const jinjaVars = content.matchAll(/\{\{\s*(\w+)\s*\}\}/g);
|
|
732
|
+
for (const m of jinjaVars) {
|
|
733
|
+
if (!seen.has(m[1])) {
|
|
734
|
+
seen.add(m[1]);
|
|
735
|
+
vars.push(m[1]);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return vars;
|
|
739
|
+
}
|
|
740
|
+
function resolveVariable(lines, fromLine, varName) {
|
|
741
|
+
// Simple resolution: look for const/let/var assignment above
|
|
742
|
+
const parts = varName.split('.');
|
|
743
|
+
const baseName = parts[0];
|
|
744
|
+
for (let i = fromLine; i >= Math.max(0, fromLine - 50); i--) {
|
|
745
|
+
const line = lines[i];
|
|
746
|
+
const match = line.match(new RegExp(`(?:const|let|var)\\s+${escapeRegex(baseName)}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`));
|
|
747
|
+
if (match)
|
|
748
|
+
return match[1];
|
|
749
|
+
}
|
|
750
|
+
// Check for common model constant patterns
|
|
751
|
+
for (let i = 0; i < Math.min(50, lines.length); i++) {
|
|
752
|
+
const line = lines[i];
|
|
753
|
+
const match = line.match(new RegExp(`(?:const|let|var)\\s+${escapeRegex(baseName)}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`));
|
|
754
|
+
if (match)
|
|
755
|
+
return match[1];
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
function findImportedClientVars(lines, file, allInits) {
|
|
760
|
+
// If file imports a module that exports a client init, track it
|
|
761
|
+
const imported = [];
|
|
762
|
+
for (const line of lines) {
|
|
763
|
+
// Look for imports that might bring in client instances
|
|
764
|
+
const importMatch = line.match(/import\s+\{?\s*(\w+)\s*\}?\s+from\s+['"]([^'"]+)['"]/);
|
|
765
|
+
if (importMatch) {
|
|
766
|
+
const importedName = importMatch[1];
|
|
767
|
+
// Check if any file exports a client init with this name
|
|
768
|
+
for (const init of allInits) {
|
|
769
|
+
if (init.variableName === importedName || init.className === importedName) {
|
|
770
|
+
imported.push({ ...init, file, variableName: importedName });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return imported;
|
|
776
|
+
}
|
|
777
|
+
function findLineOffset(lines, startLine, searchStr) {
|
|
778
|
+
for (let i = startLine; i < Math.min(startLine + 30, lines.length); i++) {
|
|
779
|
+
if (lines[i].includes(searchStr.split(':')[0])) {
|
|
780
|
+
return i + 1;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return startLine + 1;
|
|
784
|
+
}
|
|
785
|
+
function computeConfidence(anchor, matchingImport, args) {
|
|
786
|
+
let confidence = 0.6; // Base: we found an anchor
|
|
787
|
+
// Has corroborating import
|
|
788
|
+
if (matchingImport)
|
|
789
|
+
confidence += 0.15;
|
|
790
|
+
// Has resolved model
|
|
791
|
+
if (args.model?.value)
|
|
792
|
+
confidence += 0.1;
|
|
793
|
+
// Has prompt content
|
|
794
|
+
if (args.prompt?.content || args.prompt?.systemPrompt)
|
|
795
|
+
confidence += 0.1;
|
|
796
|
+
// Has config extracted
|
|
797
|
+
if (args.config && Object.keys(args.config).length > 0)
|
|
798
|
+
confidence += 0.05;
|
|
799
|
+
return Math.min(confidence, 1.0);
|
|
800
|
+
}
|
|
801
|
+
//# sourceMappingURL=llm-call-tracer.js.map
|