All files / src/services openai.js

0% Statements 0/34
0% Branches 0/19
0% Functions 0/4
0% Lines 0/30

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 };