@nomyx/assistant
Version:
A powerful assistant library and cli for your AI projects. works with Vertex AI (Claude and Gemini)
147 lines (131 loc) • 4.45 kB
text/typescript
import axios from 'axios';
import { ChatMessage, ChatOptions, ProviderResponse } from '../../../types/chat';
import { StandardizedToolCall, Tool } from '../../../types/tool';
import { OpenRouterConfig } from './config';
import { OpenRouterChatRequestBody, OpenRouterChatResponse, OpenRouterContentPart, OpenRouterMessage } from './types';
import { convertToolSchema, convertMessages, extractToolCalls} from './tools';
import { getMaxTokens, supportsTools } from './modelConfig';
export async function chat(
config: OpenRouterConfig,
messages: ChatMessage[],
options: ChatOptions,
tools?: Tool[]
): Promise<ProviderResponse> {
const url = config.getApiUrl('chat/completions');
const model = config.model;
const logger = config.getLogger();
const requestBody: OpenRouterChatRequestBody = {
messages: convertMessages(messages).map(msg => ({
...msg,
content: convertContentToOpenRouterFormat(msg.content)
})),
model: model,
max_tokens: options.maxTokens || getMaxTokens(model),
temperature: options.temperature,
stream: false
};
const modelSupportsTools = supportsTools(model);
if (tools && tools.length > 0) {
if (modelSupportsTools) {
requestBody.tools = tools.map(tool => convertToolSchema(tool.getSchema()));
requestBody.tool_choice = 'auto';
} else {
// Append instructions for using tools
const toolInstructions = `
You have access to the following tools:
${tools.map(tool => `- ${tool.name}: ${tool.description}`).join('\n')}
To use a tool, output a function call in a code block like this:
\`\`\`function_call
{
"name": "tool_name",
"arguments": {
"arg1": "value1",
"arg2": "value2"
}
}
\`\`\`
Replace "tool_name" with the actual tool name and provide the necessary arguments.
For example, to use the "tool_name" tool with arguments "arg1" and "arg2":
\`\`\`function_call
{
"name": "tool_name",
"arguments": {
"arg1": "value1",
"arg2": "value2"
}
}
\`\`\
`;
requestBody.messages[requestBody.messages.length - 1].content += '\n\n' + toolInstructions;
}
}
const headers = {
...config.getHeaders(),
'HTTP-Referer': config.httpReferer,
'X-Title': config.xTitle
};
logger.debug('OpenRouter API request', { url, model, toolsCount: tools?.length });
try {
const response = await axios.post<OpenRouterChatResponse>(url, requestBody, { headers });
logger.debug('OpenRouter API response received', { status: response.status });
return createProviderResponse(response.data, modelSupportsTools, logger);
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error('OpenRouter API error', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data
});
} else {
logger.error('OpenRouter API error', { error });
}
throw error;
}
}
function createProviderResponse(
data: OpenRouterChatResponse,
modelSupportsTools: boolean,
logger: ReturnType<OpenRouterConfig['getLogger']>
): ProviderResponse {
const choice = data.choices[0];
if (!choice) {
throw new Error('Unexpected response format from OpenRouter API');
}
const message = choice.message || { role: 'assistant', content: '' };
let content = message.content || '';
let toolCalls: StandardizedToolCall[] = modelSupportsTools ? extractToolCalls(message as OpenRouterMessage) : [];
if (!modelSupportsTools) {
const functionCallRegex = /```function_call\n([\s\S]*?)```/g;
let match;
while ((match = functionCallRegex.exec(content)) !== null) {
try {
const functionCall = JSON.parse(match[1]);
toolCalls.push({
name: functionCall.name,
arguments: functionCall.arguments
});
} catch (error) {
logger.error('Error parsing function call', { error });
}
}
content = content.replace(functionCallRegex, '');
}
return {
content: content.trim(),
toolCalls: toolCalls,
usage: {
promptTokens: data.usage?.prompt_tokens || 0,
completionTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0
},
};
}
function convertContentToOpenRouterFormat(content: string | OpenRouterContentPart[]): any {
if (typeof content === 'string') {
return content;
}
return content.map(part => ({
type: part.type,
content: (part as any)[part.type]
}));
}