11import { Router } from 'express' ;
22import { Logger } from '@pgpmjs/logger' ;
3+ import type { Pool } from 'pg' ;
4+ import { logInferenceUsage } from './inference-meter' ;
35
46const log = new Logger ( 'agentic-server' ) ;
57
@@ -12,6 +14,8 @@ export interface AgenticRouterOptions {
1214 defaultModel ?: string ;
1315 /** Provider type: 'openai' | 'ollama' | 'anthropic' */
1416 providerType ?: string ;
17+ /** Optional pg pool for inference metering (fire-and-forget writes) */
18+ pgPool ?: Pool ;
1519}
1620
1721/**
@@ -25,7 +29,7 @@ export interface AgenticRouterOptions {
2529 */
2630export const createRouter = ( options : AgenticRouterOptions ) : Router => {
2731 const router = Router ( ) ;
28- const { providerBaseUrl, providerApiKey, defaultModel, providerType } = options ;
32+ const { providerBaseUrl, providerApiKey, defaultModel, providerType, pgPool } = options ;
2933
3034 // Resolve upstream URL based on provider type
3135 const resolveUpstreamUrl = ( path : string ) : string => {
@@ -112,6 +116,8 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
112116 const internalService = req . get ( 'X-Internal-Service' ) ;
113117 const databaseId = req . get ( 'X-Database-Id' ) ;
114118 const entityId = req . get ( 'X-Entity-Id' ) ;
119+ const actorId = req . get ( 'X-Actor-Id' ) ;
120+ const startTime = process . hrtime . bigint ( ) ;
115121
116122 log . info ( 'chat/completions' , {
117123 internal : ! ! internalService ,
@@ -137,9 +143,26 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
137143 body : JSON . stringify ( body )
138144 } ) ;
139145
146+ const latencyMs = Number ( process . hrtime . bigint ( ) - startTime ) / 1e6 ;
147+
140148 if ( ! upstream . ok ) {
141149 const text = await upstream . text ( ) . catch ( ( ) => '' ) ;
142150 log . error ( 'upstream error' , { status : upstream . status , body : text } ) ;
151+
152+ if ( pgPool ) {
153+ logInferenceUsage ( pgPool , {
154+ databaseId, entityId, actorId,
155+ model : String ( body . model || '' ) ,
156+ provider : providerType || 'openai' ,
157+ service : 'chat' ,
158+ operation : 'chat/completions' ,
159+ inputTokens : 0 , outputTokens : 0 , totalTokens : 0 ,
160+ latencyMs,
161+ status : 'error' ,
162+ errorType : `upstream_${ upstream . status } `
163+ } ) ;
164+ }
165+
143166 res . status ( upstream . status ) . json ( {
144167 error : { message : `LLM provider error: ${ upstream . status } ` , upstream : text }
145168 } ) ;
@@ -150,7 +173,6 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
150173 const data = await upstream . json ( ) as Record < string , unknown > ;
151174 const response = transformChatResponse ( data , type ) ;
152175
153- // Log usage for billing (async, don't block response)
154176 const usage = ( response . usage || { } ) as Record < string , number > ;
155177 log . info ( 'inference complete' , {
156178 databaseId,
@@ -161,9 +183,41 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
161183 totalTokens : usage . total_tokens
162184 } ) ;
163185
186+ if ( pgPool ) {
187+ logInferenceUsage ( pgPool , {
188+ databaseId, entityId, actorId,
189+ model : String ( body . model || '' ) ,
190+ provider : providerType || 'openai' ,
191+ service : 'chat' ,
192+ operation : 'chat/completions' ,
193+ inputTokens : usage . prompt_tokens || 0 ,
194+ outputTokens : usage . completion_tokens || 0 ,
195+ totalTokens : usage . total_tokens || 0 ,
196+ latencyMs,
197+ status : 'ok' ,
198+ rawUsage : usage
199+ } ) ;
200+ }
201+
164202 res . json ( response ) ;
165203 } catch ( err : any ) {
204+ const latencyMs = Number ( process . hrtime . bigint ( ) - startTime ) / 1e6 ;
166205 log . error ( 'chat/completions error' , err ) ;
206+
207+ if ( pgPool ) {
208+ logInferenceUsage ( pgPool , {
209+ databaseId, entityId, actorId,
210+ model : String ( req . body ?. model || '' ) ,
211+ provider : providerType || 'openai' ,
212+ service : 'chat' ,
213+ operation : 'chat/completions' ,
214+ inputTokens : 0 , outputTokens : 0 , totalTokens : 0 ,
215+ latencyMs,
216+ status : 'error' ,
217+ errorType : err . message
218+ } ) ;
219+ }
220+
167221 res . status ( 502 ) . json ( {
168222 error : { message : 'Failed to reach LLM provider' , details : err . message }
169223 } ) ;
@@ -174,6 +228,8 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
174228 router . post ( '/v1/embeddings' , async ( req : any , res : any ) => {
175229 const databaseId = req . get ( 'X-Database-Id' ) ;
176230 const entityId = req . get ( 'X-Entity-Id' ) ;
231+ const actorId = req . get ( 'X-Actor-Id' ) ;
232+ const startTime = process . hrtime . bigint ( ) ;
177233
178234 log . info ( 'embeddings' , { databaseId, entityId, model : req . body ?. model } ) ;
179235
@@ -194,9 +250,26 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
194250 body : JSON . stringify ( body )
195251 } ) ;
196252
253+ const latencyMs = Number ( process . hrtime . bigint ( ) - startTime ) / 1e6 ;
254+
197255 if ( ! upstream . ok ) {
198256 const text = await upstream . text ( ) . catch ( ( ) => '' ) ;
199257 log . error ( 'upstream embed error' , { status : upstream . status , body : text } ) ;
258+
259+ if ( pgPool ) {
260+ logInferenceUsage ( pgPool , {
261+ databaseId, entityId, actorId,
262+ model : String ( body . model || '' ) ,
263+ provider : providerType || 'openai' ,
264+ service : 'embed' ,
265+ operation : 'embeddings' ,
266+ inputTokens : 0 , outputTokens : 0 , totalTokens : 0 ,
267+ latencyMs,
268+ status : 'error' ,
269+ errorType : `upstream_${ upstream . status } `
270+ } ) ;
271+ }
272+
200273 res . status ( upstream . status ) . json ( {
201274 error : { message : `LLM provider error: ${ upstream . status } ` , upstream : text }
202275 } ) ;
@@ -207,11 +280,44 @@ export const createRouter = (options: AgenticRouterOptions): Router => {
207280 const data = await upstream . json ( ) as Record < string , unknown > ;
208281 const response = transformEmbedResponse ( data , type ) ;
209282
283+ const usage = ( response . usage || { } ) as Record < string , number > ;
210284 log . info ( 'embed complete' , { databaseId, entityId } ) ;
211285
286+ if ( pgPool ) {
287+ logInferenceUsage ( pgPool , {
288+ databaseId, entityId, actorId,
289+ model : String ( body . model || '' ) ,
290+ provider : providerType || 'openai' ,
291+ service : 'embed' ,
292+ operation : 'embeddings' ,
293+ inputTokens : usage . prompt_tokens || 0 ,
294+ outputTokens : 0 ,
295+ totalTokens : usage . total_tokens || 0 ,
296+ latencyMs,
297+ status : 'ok' ,
298+ rawUsage : usage
299+ } ) ;
300+ }
301+
212302 res . json ( response ) ;
213303 } catch ( err : any ) {
304+ const latencyMs = Number ( process . hrtime . bigint ( ) - startTime ) / 1e6 ;
214305 log . error ( 'embeddings error' , err ) ;
306+
307+ if ( pgPool ) {
308+ logInferenceUsage ( pgPool , {
309+ databaseId, entityId, actorId,
310+ model : String ( req . body ?. model || '' ) ,
311+ provider : providerType || 'openai' ,
312+ service : 'embed' ,
313+ operation : 'embeddings' ,
314+ inputTokens : 0 , outputTokens : 0 , totalTokens : 0 ,
315+ latencyMs,
316+ status : 'error' ,
317+ errorType : err . message
318+ } ) ;
319+ }
320+
215321 res . status ( 502 ) . json ( {
216322 error : { message : 'Failed to reach LLM provider' , details : err . message }
217323 } ) ;
0 commit comments