| import { logDebug } from '../logger.js'; | |
| export class OpenAIResponseTransformer { | |
| constructor(model, requestId) { | |
| this.model = model; | |
| this.requestId = requestId || `chatcmpl-${Date.now()}`; | |
| this.created = Math.floor(Date.now() / 1000); | |
| } | |
| parseSSELine(line) { | |
| if (line.startsWith('event:')) { | |
| return { type: 'event', value: line.slice(6).trim() }; | |
| } | |
| if (line.startsWith('data:')) { | |
| const dataStr = line.slice(5).trim(); | |
| try { | |
| return { type: 'data', value: JSON.parse(dataStr) }; | |
| } catch (e) { | |
| return { type: 'data', value: dataStr }; | |
| } | |
| } | |
| return null; | |
| } | |
| transformEvent(eventType, eventData) { | |
| logDebug(`Target OpenAI event: ${eventType}`); | |
| if (eventType === 'response.created') { | |
| return this.createOpenAIChunk('', 'assistant', false); | |
| } | |
| if (eventType === 'response.in_progress') { | |
| return null; | |
| } | |
| if (eventType === 'response.output_text.delta') { | |
| const text = eventData.delta || eventData.text || ''; | |
| return this.createOpenAIChunk(text, null, false); | |
| } | |
| if (eventType === 'response.output_text.done') { | |
| return null; | |
| } | |
| if (eventType === 'response.done') { | |
| const status = eventData.response?.status; | |
| let finishReason = 'stop'; | |
| if (status === 'completed') { | |
| finishReason = 'stop'; | |
| } else if (status === 'incomplete') { | |
| finishReason = 'length'; | |
| } | |
| const finalChunk = this.createOpenAIChunk('', null, true, finishReason); | |
| const done = this.createDoneSignal(); | |
| return finalChunk + done; | |
| } | |
| return null; | |
| } | |
| createOpenAIChunk(content, role = null, finish = false, finishReason = null) { | |
| const chunk = { | |
| id: this.requestId, | |
| object: 'chat.completion.chunk', | |
| created: this.created, | |
| model: this.model, | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: {}, | |
| finish_reason: finish ? finishReason : null | |
| } | |
| ] | |
| }; | |
| if (role) { | |
| chunk.choices[0].delta.role = role; | |
| } | |
| if (content) { | |
| chunk.choices[0].delta.content = content; | |
| } | |
| return `data: ${JSON.stringify(chunk)}\n\n`; | |
| } | |
| createDoneSignal() { | |
| return 'data: [DONE]\n\n'; | |
| } | |
| async *transformStream(sourceStream) { | |
| let buffer = ''; | |
| let currentEvent = null; | |
| try { | |
| for await (const chunk of sourceStream) { | |
| buffer += chunk.toString(); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| const parsed = this.parseSSELine(line); | |
| if (!parsed) continue; | |
| if (parsed.type === 'event') { | |
| currentEvent = parsed.value; | |
| } else if (parsed.type === 'data' && currentEvent) { | |
| const transformed = this.transformEvent(currentEvent, parsed.value); | |
| if (transformed) { | |
| yield transformed; | |
| } | |
| } | |
| } | |
| } | |
| if (currentEvent === 'response.done' || currentEvent === 'response.completed') { | |
| yield this.createDoneSignal(); | |
| } | |
| } catch (error) { | |
| logDebug('Error in OpenAI stream transformation', error); | |
| throw error; | |
| } | |
| } | |
| } | |