@@ -7,6 +7,19 @@ import { env } from '@codebuff/internal/env'
77import type { InsertMessageBigqueryFn } from '@codebuff/common/types/contracts/bigquery'
88import type { Logger } from '@codebuff/common/types/contracts/logger'
99
10+ export const OPENAI_SUPPORTED_MODELS = [ 'gpt-5' ] as const
11+ export type OpenAIModel = ( typeof OPENAI_SUPPORTED_MODELS ) [ number ]
12+
13+ const INPUT_TOKEN_COSTS : Record < OpenAIModel , number > = {
14+ 'gpt-5' : 1.25 ,
15+ } as const
16+ const CACHED_INPUT_TOKEN_COSTS : Record < OpenAIModel , number > = {
17+ 'gpt-5' : 0.125 ,
18+ } as const
19+ const OUTPUT_TOKEN_COSTS : Record < OpenAIModel , number > = {
20+ 'gpt-5' : 10 ,
21+ } as const
22+
1023type StreamState = { responseText : string ; reasoningText : string }
1124
1225function extractRequestMetadata ( params : { body : unknown ; logger : Logger } ) {
@@ -25,11 +38,6 @@ function extractRequestMetadata(params: { body: unknown; logger: Logger }) {
2538 return { clientId, clientRequestId }
2639}
2740
28- function normalizeOpenAIModel ( model : unknown ) : string | undefined {
29- if ( typeof model !== 'string' ) return undefined
30- return model . startsWith ( 'openai/' ) ? model . slice ( 'openai/' . length ) : model
31- }
32-
3341type OpenAIUsage = {
3442 prompt_tokens ?: number
3543 prompt_tokens_details ?: { cached_tokens ?: number } | null
@@ -41,41 +49,19 @@ type OpenAIUsage = {
4149 cost_details ?: { upstream_inference_cost ?: number | null } | null
4250}
4351
44- function getOpenAIRatesPerMTokens ( model : string ) : {
45- inUsd : number
46- outUsd : number
47- } {
48- const m = model . toLowerCase ( )
49- if (
50- m . includes ( 'gpt-4o-mini' ) ||
51- m . includes ( '4o-mini' ) ||
52- m . includes ( 'o4-mini' )
53- ) {
54- return { inUsd : 0.15 , outUsd : 0.6 }
55- }
56- if ( m . includes ( 'gpt-4o' ) ) {
57- return { inUsd : 2.5 , outUsd : 10 }
58- }
59- if ( m . includes ( 'gpt-4.1' ) ) {
60- return { inUsd : 5 , outUsd : 15 }
61- }
62- if ( m . startsWith ( 'o3-pro' ) ) {
63- return { inUsd : 5 , outUsd : 15 }
64- }
65- if ( m . startsWith ( 'o3' ) ) {
66- return { inUsd : 5 , outUsd : 15 }
67- }
68- if ( m . startsWith ( 'gpt-5' ) ) {
69- return { inUsd : 5 , outUsd : 15 }
70- }
71- return { inUsd : 2.5 , outUsd : 10 }
72- }
52+ function computeCostDollars ( usage : OpenAIUsage , model : OpenAIModel ) : number {
53+ const inputTokenCost = INPUT_TOKEN_COSTS [ model ]
54+ const cachedInputTokenCost = CACHED_INPUT_TOKEN_COSTS [ model ]
55+ const outputTokenCost = OUTPUT_TOKEN_COSTS [ model ]
7356
74- function computeCostDollars ( usage : OpenAIUsage , model : string ) : number {
75- const { inUsd, outUsd } = getOpenAIRatesPerMTokens ( model )
7657 const inTokens = usage . prompt_tokens ?? 0
58+ const cachedInTokens = usage . prompt_tokens_details ?. cached_tokens ?? 0
7759 const outTokens = usage . completion_tokens ?? 0
78- return ( inTokens / 1_000_000 ) * inUsd + ( outTokens / 1_000_000 ) * outUsd
60+ return (
61+ ( inTokens / 1_000_000 ) * inputTokenCost +
62+ ( cachedInTokens / 1_000_000 ) * cachedInputTokenCost +
63+ ( outTokens / 1_000_000 ) * outputTokenCost
64+ )
7965}
8066
8167export async function handleOpenAIStream ( {
@@ -96,10 +82,24 @@ export async function handleOpenAIStream({
9682 const startTime = new Date ( )
9783 const { clientId, clientRequestId } = extractRequestMetadata ( { body, logger } )
9884
99- const model = normalizeOpenAIModel ( ( body as any ) ?. model )
85+ const { model } = body
86+ const modelShortName =
87+ typeof model === 'string' ? model . split ( '/' ) [ 1 ] : undefined
88+ if (
89+ ! modelShortName ||
90+ ! OPENAI_SUPPORTED_MODELS . includes ( modelShortName as OpenAIModel )
91+ ) {
92+ throw new Error (
93+ `Unsupported OpenAI model: ${ model } (supported models include only: ${ OPENAI_SUPPORTED_MODELS . map ( ( m ) => `'${ m } '` ) . join ( ', ' ) } )` ,
94+ )
95+ }
10096
10197 // Build OpenAI-compatible body
102- const openaiBody : Record < string , unknown > = { ...body , model, stream : true }
98+ const openaiBody : Record < string , unknown > = {
99+ ...body ,
100+ model : modelShortName ,
101+ stream : true ,
102+ }
103103 // Ensure usage in final chunk
104104 const streamOptions = ( openaiBody . stream_options as any ) ?? { }
105105 streamOptions . include_usage = true
@@ -182,6 +182,7 @@ export async function handleOpenAIStream({
182182 startTime,
183183 request : openaiBody ,
184184 line,
185+ modelShortName : modelShortName as OpenAIModel ,
185186 state,
186187 logger,
187188 insertMessage : insertMessageBigquery ,
@@ -239,6 +240,7 @@ async function handleOpenAILine({
239240 clientId,
240241 clientRequestId,
241242 startTime,
243+ modelShortName,
242244 request,
243245 line,
244246 state,
@@ -250,6 +252,7 @@ async function handleOpenAILine({
250252 clientId : string | null
251253 clientRequestId : string | null
252254 startTime : Date
255+ modelShortName : OpenAIModel
253256 request : unknown
254257 line : string
255258 state : StreamState
@@ -292,14 +295,7 @@ async function handleOpenAILine({
292295 // If usage present, it's the final chunk. Compute cost, log, and consume credits.
293296 if ( obj && obj . usage ) {
294297 const usage : OpenAIUsage = obj . usage
295- const model : string =
296- typeof obj . model === 'string'
297- ? obj . model
298- : typeof ( request as any ) ?. model === 'string'
299- ? ( request as any ) . model
300- : ''
301-
302- const cost = computeCostDollars ( usage , model )
298+ const cost = computeCostDollars ( usage , modelShortName )
303299 obj . usage . cost = cost
304300 obj . usage . cost_details = { upstream_inference_cost : null }
305301
0 commit comments