- Implemented OpenAI compatible API proxy server - Support for Anthropic and custom OpenAI format conversion - Automatic API key refresh with WorkOS OAuth - SSE streaming response transformation - Smart header management for Factory endpoints - Chinese documentation
128 lines
3.2 KiB
JavaScript
128 lines
3.2 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|