"use strict"; /** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenAICompatible = void 0; const fetchWithTimeout_1 = require("../fetchWithTimeout"); const types_1 = require("../types"); class OpenAICompatible { name = 'openai-compatible'; async complete(conversation, options) { return complete(conversation, options); } } exports.OpenAICompatible = OpenAICompatible; async function complete(conversation, options) { // Convert generic messages to OpenAI format const systemMessage = { role: 'system', content: systemPrompt(conversation.systemPrompt) }; const openaiMessages = [systemMessage, ...conversation.messages.map(toCompletionsMessages).flat()]; const openaiTools = conversation.tools.map(t => toCompletionsTool(t)); const { response, error } = await create({ model: options.model, max_completion_tokens: options.maxTokens, temperature: options.temperature, messages: openaiMessages, tools: openaiTools, tool_choice: conversation.tools.length > 0 ? 'auto' : undefined, reasoning_effort: toCompletionsReasoning(options.reasoning), parallel_tool_calls: false, }, options); if (error || !response) return { result: (0, types_1.assistantMessageFromError)(error?.message ?? 'No response from OpenAI compatible API'), usage: (0, types_1.emptyUsage)() }; const result = { role: 'assistant', content: [], stopReason: { code: 'ok' } }; const finishReason = response.choices[0]?.finish_reason; for (const choice of response.choices) { const message = choice.message; if (message.content) result.content.push({ type: 'text', text: message.content }); for (const entry of message.tool_calls || []) { if (entry.type !== 'function') continue; result.content.push(toToolCall(entry)); } } if (finishReason === 'length') result.stopReason = { code: 'max_tokens' }; const usage = { input: response.usage?.prompt_tokens ?? 0, output: response.usage?.completion_tokens ?? 0, }; return { result, usage }; } async function create(createParams, options) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${options.apiKey}`, }; const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` }; options.debug?.('lowire:openai')('Request:', JSON.stringify(debugBody, null, 2)); const response = await (0, fetchWithTimeout_1.fetchWithTimeout)(options.apiEndpoint ?? `https://api.openai.com/v1/chat/completions`, { method: 'POST', headers, body: JSON.stringify(createParams), signal: options.signal, timeout: options.apiTimeout }); const responseText = await response.text(); const responseBody = JSON.parse(responseText); if (!response.ok) { try { return { error: responseBody }; } catch { return { error: { type: 'unknown', message: responseText } }; } } options.debug?.('lowire:openai')('Response:', JSON.stringify(responseBody, null, 2)); return { response: responseBody }; } function toCopilotResultContentPart(part) { if (part.type === 'text') { return { type: 'text', text: part.text, }; } if (part.type === 'image') { return { type: 'image_url', image_url: { url: `data:${part.mimeType};base64,${part.data}`, }, }; } throw new Error(`Cannot convert content part of type ${part.type} to text content part`); } function toCompletionsMessages(message) { if (message.role === 'user') { return [{ role: 'user', content: message.content }]; } if (message.role === 'assistant') { const assistantMessage = { role: 'assistant' }; const textParts = message.content.filter(part => part.type === 'text'); const toolCallParts = message.content.filter(part => part.type === 'tool_call'); if (textParts.length === 1) assistantMessage.content = textParts[0].text; else assistantMessage.content = textParts; const toolCalls = []; const toolResultMessages = []; for (const toolCall of toolCallParts) { toolCalls.push({ id: toolCall.id, type: 'function', function: { name: toolCall.name, arguments: JSON.stringify(toolCall.arguments) } }); if (toolCall.result) { toolResultMessages.push({ role: 'tool', tool_call_id: toolCall.id, content: toolCall.result.content.map(toCopilotResultContentPart), }); } } if (toolCalls.length > 0) assistantMessage.tool_calls = toolCalls; if (message.toolError) { toolResultMessages.push({ role: 'user', content: [{ type: 'text', text: message.toolError, }] }); } return [assistantMessage, ...toolResultMessages]; } throw new Error(`Unsupported message role: ${message.role}`); } function toCompletionsTool(tool) { return { type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, }; } function toToolCall(entry) { return { type: 'tool_call', name: entry.type === 'function' ? entry.function.name : entry.custom.name, arguments: JSON.parse(entry.type === 'function' ? entry.function.arguments : entry.custom.input), id: entry.id, }; } function toCompletionsReasoning(reasoning) { switch (reasoning) { case 'none': return 'none'; case 'medium': return 'medium'; case 'high': return 'high'; } } const systemPrompt = (prompt) => ` ### System instructions ${prompt} ### Tool calling instructions - Make sure every message contains a tool call. - When you use a tool, you may provide a brief thought or explanation in the content field immediately before the tool_call. Do not split this into separate messages. - Every reply must include a tool call. `;