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.

195 lines 7.49 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamProcessor = void 0; class StreamProcessor { 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 chunk = this.parseStreamChunk(line); if (chunk) { // Handle thinking content if (this.hasThinkingContent(chunk)) { const thinking = this.extractThinkingContent(chunk); if (thinking && options.onThinking) { options.onThinking(thinking); } } // Aggregate the response this.aggregateChunk(aggregatedResponse, chunk); // Call chunk handler if (options.onChunk) { options.onChunk(chunk); } } } catch (error) { console.warn('Failed to parse stream chunk:', line, error); } } } } // Process any remaining buffer content if (buffer.trim()) { try { const chunk = this.parseStreamChunk(buffer); if (chunk) { this.aggregateChunk(aggregatedResponse, chunk); if (options.onChunk) { options.onChunk(chunk); } } } 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(); } } parseStreamChunk(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; } } hasThinkingContent(chunk) { var _a; return ((_a = chunk.candidates) === null || _a === void 0 ? void 0 : _a.some((candidate) => { var _a, _b; return (_b = (_a = candidate.content) === null || _a === void 0 ? void 0 : _a.parts) === null || _b === void 0 ? void 0 : _b.some((part) => { var _a; return 'text' in part && ((_a = part.text) === null || _a === void 0 ? void 0 : _a.includes('**')); }); })) || false; } extractThinkingContent(chunk) { var _a; for (const candidate of chunk.candidates || []) { for (const part of ((_a = candidate.content) === null || _a === void 0 ? void 0 : _a.parts) || []) { if ('text' in part && part.text) { // Extract thinking content (usually between ** markers) const thinkingMatch = part.text.match(/\*\*([\s\S]*?)\*\*/); if (thinkingMatch) { return thinkingMatch[1]; } } } } 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 chunk = this.parseStreamChunk(line); if (chunk) { yield chunk; } } } } if (buffer.trim()) { const chunk = this.parseStreamChunk(buffer); if (chunk) { yield chunk; } } } finally { reader.releaseLock(); } } } exports.StreamProcessor = StreamProcessor; //# sourceMappingURL=google.stream.js.map