Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | /**
* Service OpenAI-compatible — Chat avec tool use via API REST.
*
* Supporte tous les providers utilisant le format OpenAI Chat Completions :
* OpenAI, Mistral, Google Gemini, DeepSeek, xAI Grok.
*
* @module services/openai
*/
const { SYSTEM_PROMPT, OPENAI_TOOLS, executeTool } = require('./tools');
const MAX_TURNS = 10;
const PROVIDER_CONFIG = {
openai: { baseUrl: 'https://api.openai.com/v1' },
mistral: { baseUrl: 'https://api.mistral.ai/v1' },
google: { baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai' },
deepseek: { baseUrl: 'https://api.deepseek.com/v1' },
xai: { baseUrl: 'https://api.x.ai/v1' },
};
/**
* Appelle l'API Chat Completions (OpenAI ou Mistral).
*/
async function callChatAPI({ baseUrl, apiKey, model, messages, tools }) {
const res = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages,
tools,
max_tokens: 4096,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `Erreur API ${res.status}`);
}
return res.json();
}
/**
* Chat avec OpenAI ou Mistral en mode agentic (tool use).
* Même interface que le service Claude.
*
* @param {Object} params
* @param {string} params.provider — 'openai' ou 'mistral'
* @param {string} params.userId
* @param {string} params.serverId
* @param {string} params.apiKey
* @param {string} params.model
* @param {Array} params.messages — Format Anthropic (sera converti)
* @param {string} [params.context]
* @param {Function} [params.onPendingAction]
* @returns {Promise<{ reply: string, messages: Array }>}
*/
async function chat({ provider, userId, serverId, apiKey, model, messages, context, allowTools = true, onPendingAction }) {
const config = PROVIDER_CONFIG[provider];
if (!config) throw new Error(`Provider non supporté : ${provider}`);
const systemWithContext = context
? `${SYSTEM_PROMPT}\n\nContexte actuel : ${context}`
: SYSTEM_PROMPT;
// Convertir les messages du format Anthropic vers le format OpenAI
const openaiMessages = [
{ role: 'system', content: systemWithContext },
...messages.map((m) => ({ role: m.role, content: m.content })),
];
for (let turn = 0; turn < MAX_TURNS; turn++) {
const response = await callChatAPI({
baseUrl: config.baseUrl,
apiKey,
model,
messages: openaiMessages,
tools: allowTools ? OPENAI_TOOLS : undefined,
});
const choice = response.choices?.[0];
if (!choice) throw new Error('Réponse vide du modèle.');
const msg = choice.message;
openaiMessages.push(msg);
// Si pas d'appel d'outil — on a la réponse finale
if (choice.finish_reason !== 'tool_calls' || !msg.tool_calls?.length) {
return { reply: msg.content || '', messages };
}
// Exécuter les outils demandés
for (const toolCall of msg.tool_calls) {
const name = toolCall.function.name;
const input = JSON.parse(toolCall.function.arguments);
let result;
try {
result = await executeTool(name, { userId, serverId, input, onPendingAction });
} catch (err) {
result = `Erreur : ${err.message}`;
}
openaiMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: result,
});
}
}
return { reply: 'Limite de tours atteinte. Reformulez votre demande.', messages };
}
module.exports = { chat };
|