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
JavaScript
;
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