UNPKG

@unified-llm/core

Version:

Unified LLM interface (in-memory).

328 lines 13.7 kB
import { validateChatRequest } from '../../utils/validation.js'; import { validateOpenAILogLevel } from '../../validators/index.js'; import { normalizeFunctionForCompletions } from '../../utils/tool-schema.js'; import BaseProvider from '../base-provider.js'; export class OpenAIResponsesProvider extends BaseProvider { constructor({ apiKey, model, baseURL, tools, logLevel = 'warn' }) { super({ model, tools }); this.apiKey = apiKey; this.baseURL = baseURL; // Validate log level for OpenAI responses (uses env var like SDK) const validatedLogLevel = validateOpenAILogLevel(logLevel); if (validatedLogLevel) { process.env.OPENAI_LOG = validatedLogLevel; } } async chat(request) { validateChatRequest(request); try { return await this.chatWithResponsesAPI(request); } catch (error) { throw this.handleError(error); } } async *stream(request) { validateChatRequest(request); try { yield* this.streamWithResponsesAPI(request); } catch (error) { throw this.handleError(error); } } async chatWithResponsesAPI(request) { var _a; const responsesRequest = this.convertToResponsesAPIFormat(request); const url = this.baseURL ? `${this.baseURL}/responses` : 'https://api.openai.com/v1/responses'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, 'OpenAI-Beta': 'responses-v1', }, body: JSON.stringify(responsesRequest), }); if (!response.ok) { let message = 'API request failed'; try { const error = await response.json(); message = ((_a = error.error) === null || _a === void 0 ? void 0 : _a.message) || message; } catch (_b) { } throw new Error(message); } const data = await response.json(); return this.convertFromResponsesAPIFormat(data); } async *streamWithResponsesAPI(request) { var _a, _b, _c; const responsesRequest = this.convertToResponsesAPIFormat(request); const url = this.baseURL ? `${this.baseURL}/responses` : 'https://api.openai.com/v1/responses'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, 'Accept': 'text/event-stream', 'OpenAI-Beta': 'responses-v1', }, body: JSON.stringify({ ...responsesRequest, stream: true, }), }); if (!response.ok) { let message = 'API request failed'; try { const error = await response.json(); message = ((_a = error.error) === null || _a === void 0 ? void 0 : _a.message) || message; } catch (_d) { } throw new Error(message); } const reader = (_b = response.body) === null || _b === void 0 ? void 0 : _b.getReader(); if (!reader) throw new Error('No response body'); const decoder = new TextDecoder(); let buffer = ''; let finalResponse = undefined; // Buffer deltas then emit unified events: start -> text_delta* -> stop const textPieces = []; let doneReading = false; while (!doneReading) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (trimmed.startsWith('data:')) { const data = trimmed.slice(5).trim(); if (data === '[DONE]') { doneReading = true; break; } try { const json = JSON.parse(data); if (json.type === 'response.output_text.delta' && ((_c = json.delta) === null || _c === void 0 ? void 0 : _c.text)) { textPieces.push(json.delta.text); } // capture final full response from SSE if (json.type === 'response.completed' && json.response) { finalResponse = json.response; } } catch (_e) { // ignore } } } } // emit events yield { id: this.generateMessageId(), model: responsesRequest.model || this.model, provider: 'openai', message: { id: this.generateMessageId(), role: 'assistant', content: [], createdAt: new Date() }, text: '', createdAt: new Date(), rawResponse: undefined, eventType: 'start', outputIndex: 0, }; let acc = ''; for (const piece of textPieces) { acc += piece; const ev = { id: this.generateMessageId(), model: responsesRequest.model || this.model, provider: 'openai', message: { id: this.generateMessageId(), role: 'assistant', content: [{ type: 'text', text: piece }], createdAt: new Date() }, text: acc, createdAt: new Date(), rawResponse: undefined, eventType: 'text_delta', outputIndex: 0, delta: { type: 'text', text: piece }, }; yield ev; } yield { id: this.generateMessageId(), model: responsesRequest.model || this.model, provider: 'openai', message: { id: this.generateMessageId(), role: 'assistant', content: acc ? [{ type: 'text', text: acc }] : [], createdAt: new Date() }, text: acc, createdAt: new Date(), rawResponse: finalResponse, eventType: 'stop', outputIndex: 0, }; } convertToResponsesAPIFormat(request) { var _a, _b, _c, _d, _e, _f, _g, _h; const latestMessage = request.messages[request.messages.length - 1]; const content = this.normalizeContent(latestMessage.content); let input; if (content.length === 1 && content[0].type === 'text') { input = content[0].text; } else { input = content.map(c => { switch (c.type) { case 'text': return { type: 'input_text', text: c.text }; case 'image': return { type: 'input_image', image_url: { url: c.source.url || `data:${c.source.mediaType};base64,${c.source.data}`, }, }; case 'tool_result': return { type: 'tool_result_content', toolUseId: c.toolUseId, content: Array.isArray(c.content) ? c.content.map(item => item.type === 'text' ? item.text : '[Non-text content]').join('\n') : '[Tool result]' }; default: return { type: 'input_text', text: '[Unsupported content type]' }; } }); } return { model: request.model || this.model, input, temperature: (_a = request.generationConfig) === null || _a === void 0 ? void 0 : _a.temperature, max_outputTokens: (_b = request.generationConfig) === null || _b === void 0 ? void 0 : _b.max_tokens, top_p: (_c = request.generationConfig) === null || _c === void 0 ? void 0 : _c.top_p, tools: [ ...(((_d = request.tools) === null || _d === void 0 ? void 0 : _d.map(tool => ({ type: 'function', function: normalizeFunctionForCompletions(tool.function), }))) || []), ...(((_e = this.tools) === null || _e === void 0 ? void 0 : _e.map(func => ({ type: 'function', function: normalizeFunctionForCompletions(func.function), }))) || []), ].length > 0 ? [ ...(((_f = request.tools) === null || _f === void 0 ? void 0 : _f.map(tool => ({ type: 'function', function: normalizeFunctionForCompletions(tool.function), }))) || []), ...(((_g = this.tools) === null || _g === void 0 ? void 0 : _g.map(func => ({ type: 'function', function: normalizeFunctionForCompletions(func.function), }))) || []), ] : undefined, tool_choice: request.tool_choice, text: ((_h = request.generationConfig) === null || _h === void 0 ? void 0 : _h.responseFormat) ? { format: request.generationConfig.responseFormat } : undefined, previous_response_id: undefined, store: true, }; } convertFromResponsesAPIFormat(response) { var _a; const outputMessage = (_a = response.output) === null || _a === void 0 ? void 0 : _a.find((item) => item.type === 'message'); if (!outputMessage) { throw new Error('No message in response output'); } const content = []; if (outputMessage.content) { outputMessage.content.forEach((item) => { switch (item.type) { case 'output_text': content.push({ type: 'text', text: item.text }); break; case 'tool_use': content.push({ type: 'tool_use', id: item.id, name: item.name, input: item.input, }); break; } }); } const unifiedMessage = { id: outputMessage.id || this.generateMessageId(), role: outputMessage.role || 'assistant', content, createdAt: new Date(), }; const usage = response.usage ? { inputTokens: response.usage.inputTokens, outputTokens: response.usage.outputTokens, totalTokens: response.usage.total_tokens, } : undefined; const contentArray = Array.isArray(unifiedMessage.content) ? unifiedMessage.content : [{ type: 'text', text: unifiedMessage.content }]; const textContent = contentArray.find((c) => c.type === 'text'); return { id: response.id, model: response.model, provider: 'openai', message: unifiedMessage, text: (textContent === null || textContent === void 0 ? void 0 : textContent.text) || '', usage, finish_reason: outputMessage.status === 'completed' ? 'stop' : undefined, createdAt: new Date(response.createdAt * 1000), rawResponse: response, }; } convertResponsesStreamChunk(chunk) { var _a, _b; const content = []; if ((_a = chunk.delta) === null || _a === void 0 ? void 0 : _a.content) { content.push({ type: 'text', text: chunk.delta.content }); } const unifiedMessage = { id: this.generateMessageId(), role: ((_b = chunk.delta) === null || _b === void 0 ? void 0 : _b.role) || 'assistant', content, createdAt: new Date(), }; const contentArray = Array.isArray(unifiedMessage.content) ? unifiedMessage.content : [{ type: 'text', text: unifiedMessage.content }]; const textContent = contentArray.find((c) => c.type === 'text'); return { id: chunk.id, model: chunk.model, provider: 'openai', message: unifiedMessage, text: (textContent === null || textContent === void 0 ? void 0 : textContent.text) || '', finish_reason: chunk.status === 'completed' ? 'stop' : undefined, createdAt: new Date(chunk.createdAt || Date.now()), rawResponse: chunk, }; } handleError(error) { return { code: 'openai_responses_error', message: (error === null || error === void 0 ? void 0 : error.message) || 'Unknown error occurred', type: 'api_error', provider: 'openai', details: error, }; } } //# sourceMappingURL=responses-provider.js.map