@@ -2,8 +2,11 @@ import { models } from '@codebuff/common/old-constants'
22import { isExplicitlyDefinedModel } from '@codebuff/common/util/model-utils'
33import { env } from '@codebuff/internal/env'
44import { createOpenRouter } from '@codebuff/internal/openrouter-ai-sdk'
5+ import { cloneDeep } from 'lodash'
6+ import z from 'zod/v4'
57
68import type { Model } from '@codebuff/common/old-constants'
9+ import type { Request , Response } from 'express'
710
811// Provider routing documentation: https://openrouter.ai/docs/features/provider-routing
912const providerOrder = {
@@ -45,3 +48,178 @@ export function openRouterLanguageModel(model: Model) {
4548 logprobs : true ,
4649 } )
4750}
51+
52+ const openrouterUsageSchema = z
53+ . object ( {
54+ prompt_tokens : z . number ( ) ,
55+ prompt_tokens_details : z
56+ . object ( {
57+ cached_tokens : z . number ( ) ,
58+ } )
59+ . nullish ( ) ,
60+ completion_tokens : z . number ( ) ,
61+ completion_tokens_details : z
62+ . object ( {
63+ reasoning_tokens : z . number ( ) ,
64+ } )
65+ . nullish ( ) ,
66+ total_tokens : z . number ( ) ,
67+ cost : z . number ( ) . optional ( ) ,
68+ cost_details : z
69+ . object ( {
70+ upstream_inference_cost : z . number ( ) . nullish ( ) ,
71+ } )
72+ . nullish ( ) ,
73+ } )
74+ . nullish ( )
75+
76+ export async function handleOpenrouterStream ( {
77+ req,
78+ res,
79+ userId,
80+ } : {
81+ req : Request
82+ res : Response
83+ userId : string
84+ } ) {
85+ res . writeHead ( 200 , {
86+ // Mandatory SSE headers
87+ 'Content-Type' : 'text/event-stream' ,
88+ 'Cache-Control' : 'no-cache' ,
89+ Connection : 'keep-alive' ,
90+ // (optional) allow local browser demos
91+ 'Access-Control-Allow-Origin' : '*' ,
92+ } )
93+
94+ res . write ( `: connected ${ new Date ( ) . toISOString ( ) } \n` )
95+ const heartbeat = setInterval ( ( ) => {
96+ res . write ( `: heartbeat ${ new Date ( ) . toISOString ( ) } \n\n` )
97+ } , 30000 )
98+ res . on ( 'close' , ( ) => {
99+ clearInterval ( heartbeat )
100+ } )
101+
102+ const body = cloneDeep ( req . body )
103+ if ( body . usage === undefined ) {
104+ body . usage = { }
105+ }
106+ body . usage . include = true
107+ const response = await fetch (
108+ 'https://openrouter.ai/api/v1/chat/completions' ,
109+ {
110+ method : 'POST' ,
111+ headers : {
112+ Authorization : `Bearer ${ process . env . OPEN_ROUTER_API_KEY } ` ,
113+ 'HTTP-Referer' : 'https://codebuff.com' ,
114+ 'X-Title' : 'Codebuff' ,
115+ 'Content-Type' : 'application/json' ,
116+ } ,
117+ body : JSON . stringify ( body ) ,
118+ } ,
119+ )
120+
121+ const reader = response . body ?. getReader ( )
122+ if ( ! reader ) {
123+ res . status ( 500 ) . json ( { message : 'Failed to get response reader' } )
124+ return
125+ }
126+
127+ const decoder = new TextDecoder ( )
128+ let buffer = ''
129+ try {
130+ while ( true ) {
131+ const { done, value } = await reader . read ( )
132+ console . log ( 'asdf' , {
133+ done,
134+ value : decoder . decode ( value , { stream : true } ) ,
135+ } )
136+ if ( done ) {
137+ break
138+ }
139+
140+ buffer += decoder . decode ( value , { stream : true } )
141+ let lineEnd = buffer . indexOf ( '\n' )
142+ while ( lineEnd !== - 1 ) {
143+ const line = buffer . slice ( 0 , lineEnd + 1 )
144+ buffer = buffer . slice ( lineEnd + 1 )
145+ // if (line.startsWith('data: ')) {
146+ // const data = line.trim().slice('data: '.length)
147+ // await processData(data, userId)
148+ // }
149+ res . write ( line )
150+ lineEnd = buffer . indexOf ( '\n' )
151+ }
152+ }
153+ } finally {
154+ reader . cancel ( )
155+ }
156+ res . end ( )
157+ }
158+
159+ /*
160+
161+ async function processData(data: string, userId: string) {
162+ if (data === '[DONE]') {
163+ return
164+ }
165+
166+ let obj
167+ try {
168+ obj = JSON.parse(data)
169+ } catch (error) {
170+ trackEvent(
171+ AnalyticsEvent.OPENROUTER_MALFORMED_JSON_RESPONSE_CHUNK,
172+ userId,
173+ {
174+ data,
175+ },
176+ )
177+ return
178+ }
179+
180+ if (typeof obj !== 'object') {
181+ return
182+ }
183+ if (typeof obj.usage !== 'object') {
184+ return
185+ }
186+ const parseResult = openrouterUsageSchema.safeParse(obj.usage)
187+ if (!parseResult.success) {
188+ trackEvent(
189+ AnalyticsEvent.OPENROUTER_MALFORMED_JSON_RESPONSE_CHUNK,
190+ userId,
191+ {
192+ message: `Usage does not match schema:\n${parseResult.error.message}`,
193+ data,
194+ },
195+ )
196+ return
197+ }
198+
199+ const directCost = parseResult?.data?.cost ?? 0
200+ const upstreamCost = parseResult?.data?.cost_details?.upstream_inference_cost
201+
202+ saveMessage({
203+ messageId: obj.id,
204+ userId,
205+ clientSessionId: generateCompactId('direct-'),
206+ fingerprintId: generateCompactId('direct-'),
207+ userInputId: generateCompactId('direct-'),
208+ model,
209+ request,
210+ request: Message[]
211+ response: string
212+ inputTokens: number
213+ outputTokens: number
214+ cacheCreationInputTokens?: number
215+ cacheReadInputTokens?: number
216+ finishedAt: Date
217+ latencyMs: number
218+ usesUserApiKey?: boolean
219+ chargeUser?: boolean
220+ costOverrideDollars?: number
221+ agentId?: string
222+ })
223+ }
224+
225+ */
0 commit comments