@@ -4,31 +4,124 @@ import type { InputMessages, OutputMessages, TextPart } from '../otel/genai'
44import { type JsonData , safe } from '../providers/default'
55import type { ProviderID } from '../types'
66
7- export abstract class BaseAPI < RequestBody , ResponseBody >
8- implements GenAIAttributesExtractor < RequestBody , ResponseBody >
7+ export interface ExtractedRequest {
8+ requestModel ?: string
9+ temperature ?: number
10+ maxTokens ?: number
11+ systemInstructions ?: TextPart [ ]
12+ topP ?: number
13+ topK ?: number
14+ stopSequences ?: string [ ]
15+ seed ?: number
16+ inputMessages ?: InputMessages
17+ }
18+
19+ export interface ExtractedResponse {
20+ responseModel : string
21+ responseId : string
22+ finishReasons : string [ ]
23+ outputMessages : OutputMessages
24+ usage : Usage
25+ }
26+
27+ export type FieldExtractor < Data > = ( data : Data ) => void
28+
29+ export type ExtractorConfig < Data , Target > = {
30+ [ K in keyof Target ] ?: FieldExtractor < Data >
31+ }
32+
33+ export type ExtractedData = ExtractedRequest & ExtractedResponse
34+
35+ export interface SafeExtractor < RequestBody , ResponseBody , StreamChunk > {
36+ extractedRequest : ExtractedRequest
37+ extractedResponse : Partial < ExtractedResponse >
38+
39+ processRequest ( request : RequestBody ) : void
40+ requestExtractors : ExtractorConfig < RequestBody , ExtractedRequest >
41+
42+ processResponse ( response : ResponseBody ) : void
43+
44+ processChunk ( chunk : StreamChunk ) : void
45+ chunkExtractors : ExtractorConfig < StreamChunk , ExtractedResponse >
46+ }
47+
48+ export abstract class BaseAPI < RequestBody , ResponseBody , StreamChunk = JsonData >
49+ implements GenAIAttributesExtractor < RequestBody , ResponseBody > , SafeExtractor < RequestBody , ResponseBody , StreamChunk >
950{
1051 /** @apiFlavor : the flavor of the API, used to determine the response model and usage */
1152 apiFlavor : string | undefined = undefined
1253
1354 readonly providerId : ProviderID
1455 readonly requestModel ?: string
1556
57+ extractedRequest : ExtractedRequest = { }
58+ extractedResponse : Partial < ExtractedResponse > = { }
59+
1660 constructor ( providerId : ProviderID , requestModel ?: string ) {
1761 this . providerId = providerId
1862 this . requestModel = requestModel
1963 }
2064
21- // TODO(Marcelo): This is not used anywhere yet! We should remove this note when we use it.
22- extractUsage ( responseBody : ResponseBody ) : Usage | undefined {
23- const provider = findProvider ( { providerId : this . providerId } )
24- if ( ! provider ) {
25- // This should never happen, but we will throw an error to be safe.
26- throw new Error ( `Provider not found for provider ID: ${ this . providerId } ` )
65+ requestExtractors : ExtractorConfig < RequestBody , ExtractedRequest > = { }
66+ chunkExtractors : ExtractorConfig < StreamChunk , ExtractedResponse > = { }
67+
68+ processRequest ( request : RequestBody ) : void {
69+ for ( const extractor of Object . values ( this . requestExtractors ) ) {
70+ safe ( extractor ) ( request )
71+ }
72+ }
73+
74+ processResponse ( _response : ResponseBody ) : void {
75+ throw new Error ( 'Method not implemented.' )
76+ }
77+
78+ // This runs O(K * N) where K is the number of chunkExtractors and N is the number of chunks.
79+ // Although this seems inefficient, K is a constant and N is typically small.
80+ // We do this because we want to ensure that we extract each field separately, so the logic of one of the extractors
81+ // doesn't make another one to fail.
82+ processChunk ( chunk : StreamChunk ) : void {
83+ for ( const extractor of Object . values ( this . chunkExtractors ) ) {
84+ safe ( extractor ) ( chunk )
2785 }
86+ }
87+
88+ extractUsage ( responseBody : ResponseBody | StreamChunk ) : Usage | undefined {
89+ const provider = findProvider ( { providerId : this . providerId } )
90+ // This should never happen because we know the provider ID is valid, but we will throw an error to be safe.
91+ if ( ! provider ) throw new Error ( `Provider not found for provider ID: ${ this . providerId } ` )
2892 const { usage } = extractUsage ( provider , responseBody , this . apiFlavor )
2993 return usage
3094 }
3195
96+ toGenAiOtelAttributes ( ) : GenAIAttributes {
97+ return omitUndefined ( {
98+ 'gen_ai.system' : this . providerId ,
99+ 'gen_ai.operation.name' : 'chat' ,
100+ // Request Attributes
101+ 'gen_ai.request.model' : this . extractedRequest ?. requestModel ,
102+ 'gen_ai.request.max_tokens' : this . extractedRequest ?. maxTokens ,
103+ 'gen_ai.request.temperature' : this . extractedRequest ?. temperature ,
104+ 'gen_ai.request.top_p' : this . extractedRequest ?. topP ,
105+ 'gen_ai.request.top_k' : this . extractedRequest ?. topK ,
106+ 'gen_ai.request.stop_sequences' : this . extractedRequest ?. stopSequences ,
107+ 'gen_ai.request.seed' : this . extractedRequest ?. seed ,
108+ 'gen_ai.system_instructions' : this . extractedRequest ?. systemInstructions ,
109+ 'gen_ai.input.messages' : this . extractedRequest ?. inputMessages ,
110+ // Response Attributes
111+ 'gen_ai.response.model' : this . extractedResponse ?. responseModel ,
112+ 'gen_ai.response.id' : this . extractedResponse ?. responseId ,
113+ 'gen_ai.response.finish_reasons' : this . extractedResponse ?. finishReasons ,
114+ 'gen_ai.output.messages' : this . extractedResponse ?. outputMessages ,
115+ 'gen_ai.usage.input_tokens' : this . extractedResponse ?. usage ?. input_tokens ,
116+ 'gen_ai.usage.cache_read_tokens' : this . extractedResponse ?. usage ?. cache_read_tokens ,
117+ 'gen_ai.usage.cache_write_tokens' : this . extractedResponse ?. usage ?. cache_write_tokens ,
118+ 'gen_ai.usage.output_tokens' : this . extractedResponse ?. usage ?. output_tokens ,
119+ 'gen_ai.usage.input_audio_tokens' : this . extractedResponse ?. usage ?. input_audio_tokens ,
120+ 'gen_ai.usage.cache_audio_read_tokens' : this . extractedResponse ?. usage ?. cache_audio_read_tokens ,
121+ 'gen_ai.usage.output_audio_tokens' : this . extractedResponse ?. usage ?. output_audio_tokens ,
122+ } )
123+ }
124+
32125 // GenAIAttributesExtractor implementation
33126
34127 requestMaxTokens ?: ( requestBody : RequestBody ) => number | undefined
@@ -45,6 +138,8 @@ export abstract class BaseAPI<RequestBody, ResponseBody>
45138
46139 extractOtelAttributes ( requestBody : JsonData , responseBody : JsonData ) : GenAIAttributes {
47140 return {
141+ 'gen_ai.system' : this . providerId ,
142+ 'gen_ai.operation.name' : 'chat' ,
48143 'gen_ai.request.max_tokens' : this . genAIAttributes ( 'requestMaxTokens' , requestBody as RequestBody ) ,
49144 'gen_ai.request.top_k' : this . genAIAttributes ( 'requestTopK' , requestBody as RequestBody ) ,
50145 'gen_ai.request.top_p' : this . genAIAttributes ( 'requestTopP' , requestBody as RequestBody ) ,
@@ -72,3 +167,7 @@ export abstract class BaseAPI<RequestBody, ResponseBody>
72167 return undefined
73168 }
74169}
170+
171+ function omitUndefined < T extends Record < string , unknown > > ( obj : T ) : Partial < T > {
172+ return Object . fromEntries ( Object . entries ( obj ) . filter ( ( [ _ , v ] ) => v !== undefined ) ) as Partial < T >
173+ }
0 commit comments