99 * 4. Converts developer role → system for non-OpenAI providers
1010 */
1111
12- // ── Standard OpenAI ChatCompletion fields ──────────────────────────────────
13- const ALLOWED_TOP_LEVEL_FIELDS = new Set ( [
14- "id" ,
15- "object" ,
16- "created" ,
17- "model" ,
18- "choices" ,
19- "usage" ,
20- "system_fingerprint" ,
21- ] ) ;
22-
2312const ALLOWED_USAGE_FIELDS = new Set ( [
2413 "prompt_tokens" ,
2514 "completion_tokens" ,
@@ -28,16 +17,20 @@ const ALLOWED_USAGE_FIELDS = new Set([
2817 "completion_tokens_details" ,
2918] ) ;
3019
31- const ALLOWED_MESSAGE_FIELDS = new Set ( [
32- "role" ,
33- "content" ,
34- "tool_calls" ,
35- "function_call" ,
36- "refusal" ,
37- "reasoning_content" ,
38- ] ) ;
20+ type JsonRecord = Record < string , unknown > ;
21+
22+ function toRecord ( value : unknown ) : JsonRecord | null {
23+ if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) return null ;
24+ return value as JsonRecord ;
25+ }
26+
27+ function toString ( value : unknown ) : string | undefined {
28+ return typeof value === "string" ? value : undefined ;
29+ }
3930
40- const ALLOWED_CHOICE_FIELDS = new Set ( [ "index" , "message" , "delta" , "finish_reason" , "logprobs" ] ) ;
31+ function toNumber ( value : unknown ) : number | undefined {
32+ return typeof value === "number" && Number . isFinite ( value ) ? value : undefined ;
33+ }
4134
4235// ── Think tag regex ────────────────────────────────────────────────────────
4336// Matches <think>...</think> blocks (greedy, dotAll)
@@ -81,33 +74,34 @@ export function extractThinkingFromContent(text: string): {
8174 * Sanitize a non-streaming OpenAI ChatCompletion response.
8275 * Strips non-standard fields and normalizes required fields.
8376 */
84- export function sanitizeOpenAIResponse ( body : any ) : any {
85- if ( ! body || typeof body !== "object" ) return body ;
77+ export function sanitizeOpenAIResponse ( body : unknown ) : unknown {
78+ const bodyRecord = toRecord ( body ) ;
79+ if ( ! bodyRecord ) return body ;
8680
8781 // Build sanitized response with only allowed top-level fields
88- const sanitized : Record < string , any > = { } ;
82+ const sanitized : JsonRecord = { } ;
8983
9084 // Ensure required fields exist
91- sanitized . id = normalizeResponseId ( body . id ) ;
92- sanitized . object = body . object || "chat.completion" ;
93- sanitized . created = body . created || Math . floor ( Date . now ( ) / 1000 ) ;
94- sanitized . model = body . model || "unknown" ;
85+ sanitized . id = normalizeResponseId ( bodyRecord . id ) ;
86+ sanitized . object = toString ( bodyRecord . object ) || "chat.completion" ;
87+ sanitized . created = toNumber ( bodyRecord . created ) ?? Math . floor ( Date . now ( ) / 1000 ) ;
88+ sanitized . model = toString ( bodyRecord . model ) || "unknown" ;
9589
9690 // Sanitize choices
97- if ( Array . isArray ( body . choices ) ) {
98- sanitized . choices = body . choices . map ( ( choice : any , idx : number ) => sanitizeChoice ( choice , idx ) ) ;
91+ if ( Array . isArray ( bodyRecord . choices ) ) {
92+ sanitized . choices = bodyRecord . choices . map ( ( choice , idx ) => sanitizeChoice ( choice , idx ) ) ;
9993 } else {
10094 sanitized . choices = [ ] ;
10195 }
10296
10397 // Sanitize usage
104- if ( body . usage && typeof body . usage === "object" ) {
105- sanitized . usage = sanitizeUsage ( body . usage ) ;
98+ if ( bodyRecord . usage !== undefined ) {
99+ sanitized . usage = sanitizeUsage ( bodyRecord . usage ) ;
106100 }
107101
108102 // Keep system_fingerprint if present (it's a valid OpenAI field)
109- if ( body . system_fingerprint ) {
110- sanitized . system_fingerprint = body . system_fingerprint ;
103+ if ( bodyRecord . system_fingerprint ) {
104+ sanitized . system_fingerprint = bodyRecord . system_fingerprint ;
111105 }
112106
113107 return sanitized ;
@@ -116,23 +110,32 @@ export function sanitizeOpenAIResponse(body: any): any {
116110/**
117111 * Sanitize a single choice object.
118112 */
119- function sanitizeChoice ( choice : any , defaultIndex : number ) : any {
120- const sanitized : Record < string , any > = {
121- index : choice . index ?? defaultIndex ,
122- finish_reason : choice . finish_reason || null ,
113+ function sanitizeChoice ( choice : unknown , defaultIndex : number ) : JsonRecord {
114+ const choiceRecord = toRecord ( choice ) ;
115+ const sanitized : JsonRecord = {
116+ index : defaultIndex ,
117+ finish_reason : null ,
123118 } ;
124119
120+ if ( choiceRecord ?. index !== undefined ) {
121+ sanitized . index = choiceRecord . index ;
122+ }
123+
124+ if ( choiceRecord ?. finish_reason !== undefined ) {
125+ sanitized . finish_reason = choiceRecord . finish_reason ;
126+ }
127+
125128 // Sanitize message (non-streaming) or delta (streaming)
126- if ( choice . message ) {
127- sanitized . message = sanitizeMessage ( choice . message ) ;
129+ if ( choiceRecord ? .message !== undefined ) {
130+ sanitized . message = sanitizeMessage ( choiceRecord . message ) ;
128131 }
129- if ( choice . delta ) {
130- sanitized . delta = sanitizeMessage ( choice . delta ) ;
132+ if ( choiceRecord ? .delta !== undefined ) {
133+ sanitized . delta = sanitizeMessage ( choiceRecord . delta ) ;
131134 }
132135
133136 // Keep logprobs if present
134- if ( choice . logprobs !== undefined ) {
135- sanitized . logprobs = choice . logprobs ;
137+ if ( choiceRecord ? .logprobs !== undefined ) {
138+ sanitized . logprobs = choiceRecord . logprobs ;
136139 }
137140
138141 return sanitized ;
@@ -141,41 +144,42 @@ function sanitizeChoice(choice: any, defaultIndex: number): any {
141144/**
142145 * Sanitize a message object, extracting <think> tags if present.
143146 */
144- function sanitizeMessage ( msg : any ) : any {
145- if ( ! msg || typeof msg !== "object" ) return msg ;
147+ function sanitizeMessage ( msg : unknown ) : unknown {
148+ const msgRecord = toRecord ( msg ) ;
149+ if ( ! msgRecord ) return msg ;
146150
147- const sanitized : Record < string , any > = { } ;
151+ const sanitized : JsonRecord = { } ;
148152
149153 // Copy only allowed fields
150- if ( msg . role ) sanitized . role = msg . role ;
151- if ( msg . refusal !== undefined ) sanitized . refusal = msg . refusal ;
154+ if ( msgRecord . role ) sanitized . role = msgRecord . role ;
155+ if ( msgRecord . refusal !== undefined ) sanitized . refusal = msgRecord . refusal ;
152156
153157 // Handle content — extract <think> tags
154- if ( typeof msg . content === "string" ) {
155- const { content, thinking } = extractThinkingFromContent ( msg . content ) ;
158+ if ( typeof msgRecord . content === "string" ) {
159+ const { content, thinking } = extractThinkingFromContent ( msgRecord . content ) ;
156160 sanitized . content = content ;
157161
158162 // Set reasoning_content from <think> tags (if not already set)
159- if ( thinking && ! msg . reasoning_content ) {
163+ if ( thinking && ! msgRecord . reasoning_content ) {
160164 sanitized . reasoning_content = thinking ;
161165 }
162- } else if ( msg . content !== undefined ) {
163- sanitized . content = msg . content ;
166+ } else if ( msgRecord . content !== undefined ) {
167+ sanitized . content = msgRecord . content ;
164168 }
165169
166170 // Preserve existing reasoning_content (from providers that natively support it)
167- if ( msg . reasoning_content && ! sanitized . reasoning_content ) {
168- sanitized . reasoning_content = msg . reasoning_content ;
171+ if ( msgRecord . reasoning_content && ! sanitized . reasoning_content ) {
172+ sanitized . reasoning_content = msgRecord . reasoning_content ;
169173 }
170174
171175 // Preserve tool_calls
172- if ( msg . tool_calls ) {
173- sanitized . tool_calls = msg . tool_calls ;
176+ if ( msgRecord . tool_calls ) {
177+ sanitized . tool_calls = msgRecord . tool_calls ;
174178 }
175179
176180 // Preserve function_call (legacy)
177- if ( msg . function_call ) {
178- sanitized . function_call = msg . function_call ;
181+ if ( msgRecord . function_call ) {
182+ sanitized . function_call = msgRecord . function_call ;
179183 }
180184
181185 return sanitized ;
@@ -184,30 +188,33 @@ function sanitizeMessage(msg: any): any {
184188/**
185189 * Sanitize usage object — keep only standard fields.
186190 */
187- function sanitizeUsage ( usage : any ) : any {
188- if ( ! usage || typeof usage !== "object" ) return usage ;
191+ function sanitizeUsage ( usage : unknown ) : unknown {
192+ const usageRecord = toRecord ( usage ) ;
193+ if ( ! usageRecord ) return usage ;
189194
190- const sanitized : Record < string , any > = { } ;
195+ const sanitized : JsonRecord = { } ;
191196 for ( const key of ALLOWED_USAGE_FIELDS ) {
192- if ( usage [ key ] !== undefined ) {
193- sanitized [ key ] = usage [ key ] ;
197+ if ( usageRecord [ key ] !== undefined ) {
198+ sanitized [ key ] = usageRecord [ key ] ;
194199 }
195200 }
196201
197202 // Ensure required fields
198- if ( sanitized . prompt_tokens === undefined ) sanitized . prompt_tokens = 0 ;
199- if ( sanitized . completion_tokens === undefined ) sanitized . completion_tokens = 0 ;
200- if ( sanitized . total_tokens === undefined ) {
201- sanitized . total_tokens = sanitized . prompt_tokens + sanitized . completion_tokens ;
202- }
203+ const promptTokens = toNumber ( sanitized . prompt_tokens ) ?? 0 ;
204+ const completionTokens = toNumber ( sanitized . completion_tokens ) ?? 0 ;
205+ const totalTokens = toNumber ( sanitized . total_tokens ) ?? promptTokens + completionTokens ;
206+
207+ sanitized . prompt_tokens = promptTokens ;
208+ sanitized . completion_tokens = completionTokens ;
209+ sanitized . total_tokens = totalTokens ;
203210
204211 return sanitized ;
205212}
206213
207214/**
208215 * Normalize response ID to use chatcmpl- prefix.
209216 */
210- function normalizeResponseId ( id : any ) : string {
217+ function normalizeResponseId ( id : unknown ) : string {
211218 if ( ! id || typeof id !== "string" ) {
212219 return `chatcmpl-${ crypto . randomUUID ( ) . replace ( / - / g, "" ) . slice ( 0 , 29 ) } ` ;
213220 }
@@ -221,48 +228,60 @@ function normalizeResponseId(id: any): string {
221228 * Sanitize a streaming SSE chunk for passthrough mode.
222229 * Lighter than full sanitization — only strips problematic extra fields.
223230 */
224- export function sanitizeStreamingChunk ( parsed : any ) : any {
225- if ( ! parsed || typeof parsed !== "object" ) return parsed ;
231+ export function sanitizeStreamingChunk ( parsed : unknown ) : unknown {
232+ const parsedRecord = toRecord ( parsed ) ;
233+ if ( ! parsedRecord ) return parsed ;
226234
227235 // Build sanitized chunk
228- const sanitized : Record < string , any > = { } ;
236+ const sanitized : JsonRecord = { } ;
229237
230238 // Keep only standard fields
231- if ( parsed . id !== undefined ) sanitized . id = parsed . id ;
232- sanitized . object = parsed . object || "chat.completion.chunk" ;
233- if ( parsed . created !== undefined ) sanitized . created = parsed . created ;
234- if ( parsed . model !== undefined ) sanitized . model = parsed . model ;
239+ if ( parsedRecord . id !== undefined ) sanitized . id = parsedRecord . id ;
240+ sanitized . object = toString ( parsedRecord . object ) || "chat.completion.chunk" ;
241+ if ( parsedRecord . created !== undefined ) sanitized . created = parsedRecord . created ;
242+ if ( parsedRecord . model !== undefined ) sanitized . model = parsedRecord . model ;
235243
236244 // Sanitize choices with delta
237- if ( Array . isArray ( parsed . choices ) ) {
238- sanitized . choices = parsed . choices . map ( ( choice : any ) => {
239- const c : Record < string , any > = {
240- index : choice . index ?? 0 ,
241- } ;
242- if ( choice . delta !== undefined ) {
243- c . delta = { } ;
244- const delta = choice . delta ;
245- if ( delta . role !== undefined ) c . delta . role = delta . role ;
246- if ( delta . content !== undefined ) c . delta . content = delta . content ;
247- if ( delta . reasoning_content !== undefined )
248- c . delta . reasoning_content = delta . reasoning_content ;
249- if ( delta . tool_calls !== undefined ) c . delta . tool_calls = delta . tool_calls ;
250- if ( delta . function_call !== undefined ) c . delta . function_call = delta . function_call ;
245+ if ( Array . isArray ( parsedRecord . choices ) ) {
246+ sanitized . choices = parsedRecord . choices . map ( ( choice ) => {
247+ const c : JsonRecord = { index : 0 } ;
248+ const choiceRecord = toRecord ( choice ) ;
249+ if ( ! choiceRecord ) return c ;
250+
251+ c . index = toNumber ( choiceRecord . index ) ?? 0 ;
252+
253+ if ( choiceRecord . delta !== undefined ) {
254+ const deltaRecord = toRecord ( choiceRecord . delta ) ;
255+ if ( deltaRecord ) {
256+ const delta : JsonRecord = { } ;
257+ if ( deltaRecord . role !== undefined ) delta . role = deltaRecord . role ;
258+ if ( deltaRecord . content !== undefined ) delta . content = deltaRecord . content ;
259+ if ( deltaRecord . reasoning_content !== undefined ) {
260+ delta . reasoning_content = deltaRecord . reasoning_content ;
261+ }
262+ if ( deltaRecord . tool_calls !== undefined ) delta . tool_calls = deltaRecord . tool_calls ;
263+ if ( deltaRecord . function_call !== undefined )
264+ delta . function_call = deltaRecord . function_call ;
265+ c . delta = delta ;
266+ } else {
267+ c . delta = choiceRecord . delta ;
268+ }
251269 }
252- if ( choice . finish_reason !== undefined ) c . finish_reason = choice . finish_reason ;
253- if ( choice . logprobs !== undefined ) c . logprobs = choice . logprobs ;
270+
271+ if ( choiceRecord . finish_reason !== undefined ) c . finish_reason = choiceRecord . finish_reason ;
272+ if ( choiceRecord . logprobs !== undefined ) c . logprobs = choiceRecord . logprobs ;
254273 return c ;
255274 } ) ;
256275 }
257276
258277 // Sanitize usage if present
259- if ( parsed . usage && typeof parsed . usage === "object" ) {
260- sanitized . usage = sanitizeUsage ( parsed . usage ) ;
278+ if ( parsedRecord . usage !== undefined ) {
279+ sanitized . usage = sanitizeUsage ( parsedRecord . usage ) ;
261280 }
262281
263282 // Keep system_fingerprint if present
264- if ( parsed . system_fingerprint ) {
265- sanitized . system_fingerprint = parsed . system_fingerprint ;
283+ if ( parsedRecord . system_fingerprint ) {
284+ sanitized . system_fingerprint = parsedRecord . system_fingerprint ;
266285 }
267286
268287 return sanitized ;
0 commit comments