@unified-llm/core
Version:
Unified LLM interface (in-memory).
328 lines • 13.7 kB
JavaScript
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