UNPKG

llmforge

Version:

One API, every AI model, instant switching. Change from GPT-4 to Gemini to local models with a single config update. LLMForge is the lightweight, TypeScript-first solution for multi-provider AI applications with zero vendor lock-in.

297 lines 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamProcessor = exports.OpenAIStreamProcessor = void 0; class OpenAIStreamProcessor { constructor() { this.decoder = new TextDecoder(); } async processStream(response, options = {}) { if (!response.body) { throw new Error('Response body is empty'); } const reader = response.body.getReader(); let buffer = ''; let aggregatedResponse = { candidates: [], }; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += this.decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const openAIChunk = this.parseOpenAIStreamChunk(line); if (openAIChunk) { // Handle reasoning content if (this.hasReasoningContent(openAIChunk)) { const reasoning = this.extractReasoningContent(openAIChunk); if (reasoning && options.onReasoning) { options.onReasoning(reasoning); } } // Handle thinking content (from JSON schema responses) if (this.hasThinkingContent(openAIChunk)) { const thinking = this.extractThinkingContent(openAIChunk); if (thinking && options.onThinking) { options.onThinking(thinking); } } // Convert to Gemini format and aggregate const geminiChunk = this.convertToGeminiChunk(openAIChunk); this.aggregateChunk(aggregatedResponse, geminiChunk); // Call chunk handler with converted chunk if (options.onChunk) { options.onChunk(geminiChunk); } } } catch (error) { console.warn('Failed to parse stream chunk:', line, error); } } } } // Process any remaining buffer content if (buffer.trim()) { try { const openAIChunk = this.parseOpenAIStreamChunk(buffer); if (openAIChunk) { const geminiChunk = this.convertToGeminiChunk(openAIChunk); this.aggregateChunk(aggregatedResponse, geminiChunk); if (options.onChunk) { options.onChunk(geminiChunk); } } } catch (error) { console.warn('Failed to parse final chunk:', buffer, error); } } if (options.onComplete) { options.onComplete(aggregatedResponse); } return aggregatedResponse; } catch (error) { if (options.onError) { options.onError(error); } throw error; } finally { reader.releaseLock(); } } parseOpenAIStreamChunk(line) { // Remove "data: " prefix if present const cleanLine = line.replace(/^data:\s*/, '').trim(); if (!cleanLine || cleanLine === '[DONE]') { return null; } try { return JSON.parse(cleanLine); } catch (error) { // Some lines might not be valid JSON return null; } } convertToGeminiChunk(openAIChunk) { const geminiChunk = { candidates: [], }; if (openAIChunk.choices) { geminiChunk.candidates = openAIChunk.choices.map(choice => { var _a, _b; return ({ content: { role: 'model', parts: [{ text: ((_a = choice.delta) === null || _a === void 0 ? void 0 : _a.content) || ((_b = choice.message) === null || _b === void 0 ? void 0 : _b.content) || '' }] }, finishReason: choice.finish_reason, index: choice.index }); }); } if (openAIChunk.usage) { geminiChunk.usageMetadata = { promptTokenCount: openAIChunk.usage.prompt_tokens, candidatesTokenCount: openAIChunk.usage.completion_tokens, totalTokenCount: openAIChunk.usage.total_tokens }; } return geminiChunk; } hasReasoningContent(chunk) { return chunk.type === 'reasoning' || !!chunk.reasoning; } extractReasoningContent(chunk) { return chunk.reasoning; } hasThinkingContent(chunk) { var _a; // Check if the content contains JSON with thinking field return ((_a = chunk.choices) === null || _a === void 0 ? void 0 : _a.some(choice => { var _a, _b; const content = ((_a = choice.delta) === null || _a === void 0 ? void 0 : _a.content) || ((_b = choice.message) === null || _b === void 0 ? void 0 : _b.content); if (!content) return false; try { const parsed = JSON.parse(content); return parsed.thinking !== undefined; } catch (_c) { return content.includes('"thinking"') || content.includes('thinking:'); } })) || false; } extractThinkingContent(chunk) { var _a, _b; for (const choice of chunk.choices || []) { const content = ((_a = choice.delta) === null || _a === void 0 ? void 0 : _a.content) || ((_b = choice.message) === null || _b === void 0 ? void 0 : _b.content); if (!content) continue; try { const parsed = JSON.parse(content); if (parsed.thinking) { return parsed.thinking; } } catch (_c) { // Try to extract thinking from partial JSON or text const thinkingMatch = content.match(/"thinking":\s*"([^"]*)"/) || content.match(/thinking:\s*(.+?)(?:,|\}|$)/); if (thinkingMatch) { return thinkingMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"'); } } } return null; } aggregateChunk(aggregated, chunk) { var _a; if (!chunk.candidates) return; for (let i = 0; i < chunk.candidates.length; i++) { const candidate = chunk.candidates[i]; if (!aggregated.candidates) { aggregated.candidates = []; } if (!aggregated.candidates[i]) { aggregated.candidates[i] = { content: { parts: [] }, index: i, }; } const aggregatedCandidate = aggregated.candidates[i]; // Merge content parts if ((_a = candidate.content) === null || _a === void 0 ? void 0 : _a.parts) { for (let j = 0; j < candidate.content.parts.length; j++) { const part = candidate.content.parts[j]; if (!aggregatedCandidate.content.parts[j]) { aggregatedCandidate.content.parts[j] = { text: '' }; } if ('text' in part && part.text) { const aggregatedPart = aggregatedCandidate.content.parts[j]; if ('text' in aggregatedPart) { aggregatedPart.text += part.text; } } } } if (candidate.finishReason) { aggregatedCandidate.finishReason = candidate.finishReason; } if (candidate.safetyRatings) { aggregatedCandidate.safetyRatings = candidate.safetyRatings; } } if (chunk.usageMetadata) { aggregated.usageMetadata = chunk.usageMetadata; } } async *createAsyncGenerator(response) { if (!response.body) { throw new Error('Response body is empty'); } const reader = response.body.getReader(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += this.decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { const openAIChunk = this.parseOpenAIStreamChunk(line); if (openAIChunk) { const geminiChunk = this.convertToGeminiChunk(openAIChunk); yield geminiChunk; } } } } if (buffer.trim()) { const openAIChunk = this.parseOpenAIStreamChunk(buffer); if (openAIChunk) { const geminiChunk = this.convertToGeminiChunk(openAIChunk); yield geminiChunk; } } } finally { reader.releaseLock(); } } // OpenAI-specific method to handle raw OpenAI chunks without conversion async *createOpenAIAsyncGenerator(response) { if (!response.body) { throw new Error('Response body is empty'); } const reader = response.body.getReader(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += this.decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { const chunk = this.parseOpenAIStreamChunk(line); if (chunk) { yield chunk; } } } } if (buffer.trim()) { const chunk = this.parseOpenAIStreamChunk(buffer); if (chunk) { yield chunk; } } } finally { reader.releaseLock(); } } } exports.OpenAIStreamProcessor = OpenAIStreamProcessor; // Legacy compatibility - extend the original StreamProcessor class StreamProcessor extends OpenAIStreamProcessor { } exports.StreamProcessor = StreamProcessor; //# sourceMappingURL=openai.stream.js.map