-
Notifications
You must be signed in to change notification settings - Fork 14
feat: add Anthropic instrumentation support for TS #618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: LILYPAD-209
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## LILYPAD-209 #618 +/- ##
===============================================
- Coverage 93.74% 88.49% -5.25%
===============================================
Files 229 231 +2
Lines 16332 17757 +1425
Branches 713 763 +50
===============================================
+ Hits 15310 15714 +404
- Misses 1018 2039 +1021
Partials 4 4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lot of code duplication (seems like around 2-4 copies of span creation, message recording, stream chunk processing, etc). I think this will really be difficult to keep track of and maintain as time goes on. I suggest some significant cleanup of the implementation
AnthropicMessageChunk, | ||
} from '../types/anthropic'; | ||
|
||
// Import GenAI semantic conventions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit : unnecessary comment
suppressInternalInstrumentation?: boolean; | ||
} | ||
|
||
// Symbol to mark instrumented modules |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: probably unnecessary?
} | ||
|
||
private _applyPatch(moduleExports: any): any { | ||
// Check if already instrumented by Lilypad |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More vibe coded comments :P
|
||
// Start span using the OTel way with active context | ||
const span = tracer.startSpan( | ||
`anthropic.messages ${params?.model || 'unknown'}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: wonder if it make sense to make the unknown string like unknown-model
so it's a little clearer what is unknown?
{ | ||
kind: SpanKind.CLIENT, | ||
attributes: { | ||
[SEMATTRS_GEN_AI_SYSTEM]: 'anthropic', | ||
[SEMATTRS_GEN_AI_REQUEST_MODEL]: params?.model, | ||
[SEMATTRS_GEN_AI_REQUEST_TEMPERATURE]: params?.temperature, | ||
[SEMATTRS_GEN_AI_REQUEST_MAX_TOKENS]: params?.max_tokens, | ||
[SEMATTRS_GEN_AI_REQUEST_TOP_P]: params?.top_p, | ||
'gen_ai.request.top_k': params?.top_k, | ||
[SEMATTRS_GEN_AI_OPERATION_NAME]: 'chat', | ||
'lilypad.type': 'llm', | ||
'server.address': 'api.anthropic.com', | ||
}, | ||
}, | ||
activeContext, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this span creation logic is duplicated 3 times in this file, can we refactor to DRY?
if (chunk.type === 'message_start' && chunk.message) { | ||
// Initial message with usage info | ||
if (chunk.message.usage) { | ||
usage.input_tokens = chunk.message.usage.input_tokens; | ||
usage.output_tokens = chunk.message.usage.output_tokens; | ||
} | ||
} else if (chunk.type === 'content_block_delta' && chunk.delta) { | ||
// Text content | ||
if (chunk.delta.type === 'text_delta' && chunk.delta.text) { | ||
content += chunk.delta.text; | ||
} | ||
} else if (chunk.type === 'message_delta' && chunk.delta) { | ||
// Message delta with stop reason | ||
if (chunk.delta.stop_reason) { | ||
stopReason = chunk.delta.stop_reason; | ||
} | ||
} else if (chunk.type === 'message_stop' && chunk.usage) { | ||
// Final usage info | ||
usage.output_tokens = chunk.usage.output_tokens; | ||
} | ||
|
||
// Add chunk event | ||
span.addEvent('gen_ai.chunk', { | ||
size: chunk.delta?.text?.length || 0, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first copy of chunk processing
if (chunk.type === 'message_start' && chunk.message) { | ||
// Initial message with usage info | ||
if (chunk.message.usage) { | ||
usage.input_tokens = chunk.message.usage.input_tokens; | ||
usage.output_tokens = chunk.message.usage.output_tokens; | ||
} | ||
} else if (chunk.type === 'content_block_delta' && chunk.delta) { | ||
// Text content | ||
if (chunk.delta.type === 'text_delta' && chunk.delta.text) { | ||
content += chunk.delta.text; | ||
} | ||
} else if (chunk.type === 'message_delta' && chunk.delta) { | ||
// Message delta with stop reason | ||
if (chunk.delta.stop_reason) { | ||
stopReason = chunk.delta.stop_reason; | ||
} | ||
} else if (chunk.type === 'message_stop' && chunk.usage) { | ||
// Final usage info | ||
usage.output_tokens = chunk.usage.output_tokens; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
second copy of chunk processing
// Record system message if present | ||
if (params?.system) { | ||
span.addEvent('gen_ai.system.message', { | ||
'gen_ai.system': 'anthropic', | ||
content: params.system, | ||
}); | ||
} | ||
|
||
// Record messages | ||
if (params?.messages) { | ||
params.messages.forEach((message: any) => { | ||
const content = | ||
typeof message.content === 'string' ? message.content : JSON.stringify(message.content); | ||
|
||
span.addEvent(`gen_ai.${message.role}.message`, { | ||
'gen_ai.system': 'anthropic', | ||
content: content, | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
second copy of message recording logic
// Record system message if present | ||
if (params?.system) { | ||
span.addEvent('gen_ai.system.message', { | ||
'gen_ai.system': 'anthropic', | ||
content: params.system, | ||
}); | ||
} | ||
|
||
// Record messages | ||
if (params?.messages) { | ||
params.messages.forEach((message: any) => { | ||
const content = | ||
typeof message.content === 'string' | ||
? message.content | ||
: JSON.stringify(message.content); | ||
|
||
span.addEvent(`gen_ai.${message.role}.message`, { | ||
'gen_ai.system': 'anthropic', | ||
content: content, | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
third copy of message recording logic
// Record system message if present | ||
if (params?.system) { | ||
span.addEvent('gen_ai.system.message', { | ||
'gen_ai.system': 'anthropic', | ||
content: params.system, | ||
}); | ||
} | ||
|
||
// Record messages | ||
if (params?.messages) { | ||
params.messages.forEach((message: any) => { | ||
const content = | ||
typeof message.content === 'string' | ||
? message.content | ||
: JSON.stringify(message.content); | ||
|
||
span.addEvent(`gen_ai.${message.role}.message`, { | ||
'gen_ai.system': 'anthropic', | ||
content: content, | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first copy of message recording logic
No description provided.