@posthog/ai
Version:
PostHog Node.js AI integrations
205 lines (190 loc) • 7.32 kB
text/typescript
import AnthropicOriginal from '@anthropic-ai/sdk'
import { PostHog } from 'posthog-node'
import { v4 as uuidv4 } from 'uuid'
import { formatResponseAnthropic, mergeSystemPrompt, MonitoringParams, sendEventToPosthog } from '../utils'
type MessageCreateParamsNonStreaming = AnthropicOriginal.Messages.MessageCreateParamsNonStreaming
type MessageCreateParamsStreaming = AnthropicOriginal.Messages.MessageCreateParamsStreaming
type MessageCreateParams = AnthropicOriginal.Messages.MessageCreateParams
type Message = AnthropicOriginal.Messages.Message
type RawMessageStreamEvent = AnthropicOriginal.Messages.RawMessageStreamEvent
type MessageCreateParamsBase = AnthropicOriginal.Messages.MessageCreateParams
import type { APIPromise, RequestOptions } from '@anthropic-ai/sdk/core'
import type { Stream } from '@anthropic-ai/sdk/streaming'
interface MonitoringAnthropicConfig {
apiKey: string
posthog: PostHog
baseURL?: string
}
export class PostHogAnthropic extends AnthropicOriginal {
private readonly phClient: PostHog
public messages: WrappedMessages
constructor(config: MonitoringAnthropicConfig) {
const { posthog, ...anthropicConfig } = config
super(anthropicConfig)
this.phClient = posthog
this.messages = new WrappedMessages(this, this.phClient)
}
}
export class WrappedMessages extends AnthropicOriginal.Messages {
private readonly phClient: PostHog
constructor(parentClient: PostHogAnthropic, phClient: PostHog) {
super(parentClient)
this.phClient = phClient
}
public create(body: MessageCreateParamsNonStreaming, options?: RequestOptions): APIPromise<Message>
public create(
body: MessageCreateParamsStreaming & MonitoringParams,
options?: RequestOptions
): APIPromise<Stream<RawMessageStreamEvent>>
public create(
body: MessageCreateParamsBase & MonitoringParams,
options?: RequestOptions
): APIPromise<Stream<RawMessageStreamEvent> | Message>
public create(
body: MessageCreateParams & MonitoringParams,
options?: RequestOptions
): APIPromise<Message> | APIPromise<Stream<RawMessageStreamEvent>> {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
...anthropicParams
} = body
const traceId = posthogTraceId ?? uuidv4()
const startTime = Date.now()
const parentPromise = super.create(anthropicParams, options)
if (anthropicParams.stream) {
return parentPromise.then((value) => {
let accumulatedContent = ''
const usage: {
inputTokens: number
outputTokens: number
cacheCreationInputTokens?: number
cacheReadInputTokens?: number
} = {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
}
if ('tee' in value) {
const [stream1, stream2] = value.tee()
;(async () => {
try {
for await (const chunk of stream1) {
if ('delta' in chunk) {
if ('text' in chunk.delta) {
const delta = chunk?.delta?.text ?? ''
accumulatedContent += delta
}
}
if (chunk.type == 'message_start') {
usage.inputTokens = chunk.message.usage.input_tokens ?? 0
usage.cacheCreationInputTokens = chunk.message.usage.cache_creation_input_tokens ?? 0
usage.cacheReadInputTokens = chunk.message.usage.cache_read_input_tokens ?? 0
}
if ('usage' in chunk) {
usage.outputTokens = chunk.usage.output_tokens ?? 0
}
}
const latency = (Date.now() - startTime) / 1000
sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId ?? traceId,
traceId,
model: anthropicParams.model,
provider: 'anthropic',
input: mergeSystemPrompt(anthropicParams, 'anthropic'),
output: [{ content: accumulatedContent, role: 'assistant' }],
latency,
baseURL: (this as any).baseURL ?? '',
params: body,
httpStatus: 200,
usage,
})
} catch (error: any) {
// error handling
sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId ?? traceId,
traceId,
model: anthropicParams.model,
provider: 'anthropic',
input: mergeSystemPrompt(anthropicParams, 'anthropic'),
output: [],
latency: 0,
baseURL: (this as any).baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0,
},
isError: true,
error: JSON.stringify(error),
})
}
})()
// Return the other stream to the user
return stream2
}
return value
}) as APIPromise<Stream<RawMessageStreamEvent>>
} else {
const wrappedPromise = parentPromise.then(
(result) => {
if ('content' in result) {
const latency = (Date.now() - startTime) / 1000
sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId ?? traceId,
traceId,
model: anthropicParams.model,
provider: 'anthropic',
input: mergeSystemPrompt(anthropicParams, 'anthropic'),
output: formatResponseAnthropic(result),
latency,
baseURL: (this as any).baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage.input_tokens ?? 0,
outputTokens: result.usage.output_tokens ?? 0,
cacheCreationInputTokens: result.usage.cache_creation_input_tokens ?? 0,
cacheReadInputTokens: result.usage.cache_read_input_tokens ?? 0,
},
})
}
return result
},
(error: any) => {
sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId ?? traceId,
traceId,
model: anthropicParams.model,
provider: 'anthropic',
input: mergeSystemPrompt(anthropicParams, 'anthropic'),
output: [],
latency: 0,
baseURL: (this as any).baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0,
},
isError: true,
error: JSON.stringify(error),
})
throw error
}
) as APIPromise<Message>
return wrappedPromise
}
}
}
export default PostHogAnthropic