UNPKG

@posthog/ai

Version:
205 lines (190 loc) 7.32 kB
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