Intelligence Artificielle angularforall.com

- OpenAI API : intégration complète en JavaScript

Openai Javascript Api Gpt-4 Ia-Generative Streaming Tokens Node-Js Fetch-Api Llm Chatbot Prompt-Engineering Securite-Api
OpenAI API : intégration complète en JavaScript

Intégrez l'API OpenAI en JavaScript pour créer des applications IA avec GPT-4 : appels en streaming, gestion des tokens et bonnes pratiques de sécurité.

Installation et configuration sécurisée

Le SDK officiel OpenAI supporte Node.js, Deno, Bun et les runtimes edge (Cloudflare Workers). La règle absolue : la clé API ne touche jamais le navigateur.

# Installer le SDK officiel
npm install openai

# Optionnel : Zod pour valider les Structured Outputs
npm install zod
// lib/openai.ts — Client singleton côté serveur UNIQUEMENT
import OpenAI from 'openai';

// Variables d'environnement — jamais dans le bundle frontend
const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,   // .env (ajouté dans .gitignore)
    maxRetries: 3,           // retry automatique sur 429/500
    timeout: 30_000,         // timeout 30s
});

export default openai;
Sécurité : Ne jamais passer dangerouslyAllowBrowser: true en production. Toujours mettre un proxy Express/Next.js entre le navigateur et l'API OpenAI. Un utilisateur malveillant qui intercepte la clé peut générer des milliers de requêtes en ton nom.

Modèles GPT — comparatif et choix

Modèle Context window Input ($/1M) Output ($/1M) Usage recommandé
gpt-4o128k tokens$5$15Production, raisonnement complexe
gpt-4o-mini128k tokens$0.15$0.60Fort volume, classification, extraction
gpt-4-turbo128k tokens$10$30Vision, long context
o1-mini128k tokens$3$12Maths, code complexe, raisonnement
o3-mini128k tokens$1.10$4.40Raisonnement avancé, efficace
text-embedding-3-small8k tokens$0.02Embeddings / RAG
Stratégie multi-modèle : Utilise gpt-4o-mini pour classer/router la requête, puis gpt-4o seulement si la tâche est complexe. Réduit les coûts de 80% sur les apps à fort volume.

Chat Completion et conversations multi-tours

L'API Chat Completion accepte un tableau de messages avec les rôles system, user et assistant. Le contexte de conversation est entièrement géré par le client.

// chat.ts — Premier appel avec paramètres clés
import openai from './lib/openai';

async function chat(userMessage: string): Promise<string> {
    const response = await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [
            {
                role: 'system',
                content: 'Tu es un assistant expert en développement web Angular. Réponds en français, de manière concise et avec des exemples de code.'
            },
            { role: 'user', content: userMessage }
        ],
        max_tokens: 1500,
        temperature: 0.3,     // 0 = déterministe | 1 = créatif | 2 = très aléatoire
        top_p: 1,
        frequency_penalty: 0, // pénalise les répétitions de tokens (0-2)
        presence_penalty: 0,  // encourage les nouveaux sujets (0-2)
    });

    return response.choices[0].message.content ?? '';
}

Gestionnaire de conversation avec troncature

// conversation-manager.ts — Historique avec limite de tokens
class ConversationManager {
    private messages: { role: string; content: string }[] = [];
    private readonly maxHistoryLength = 20; // garder 20 derniers échanges

    constructor(private systemPrompt: string) {
        this.messages.push({ role: 'system', content: systemPrompt });
    }

    async sendMessage(userInput: string): Promise<string> {
        // Ajouter le message utilisateur
        this.messages.push({ role: 'user', content: userInput });

        // Tronquer si trop long (garder system + N derniers)
        if (this.messages.length > this.maxHistoryLength + 1) {
            const systemMsg = this.messages[0];
            this.messages = [
                systemMsg,
                ...this.messages.slice(-(this.maxHistoryLength))
            ];
        }

        const response = await openai.chat.completions.create({
            model: 'gpt-4o',
            messages: this.messages as any,
        });

        const reply = response.choices[0].message.content ?? '';
        // Enregistrer la réponse pour le prochain tour
        this.messages.push({ role: 'assistant', content: reply });

        return reply;
    }

    reset(): void {
        this.messages = [{ role: 'system', content: this.systemPrompt }];
    }
}

// Utilisation
const conv = new ConversationManager('Tu es un assistant dev Angular.');
const r1 = await conv.sendMessage('Qu\'est-ce que RxJS ?');
const r2 = await conv.sendMessage('Donne-moi un exemple avec switchMap');

Streaming avec Express SSE

Le streaming affiche la réponse token par token dès réception — essentiel pour une bonne UX. Côté serveur : Server-Sent Events (SSE). Côté client : EventSource ou fetch avec ReadableStream.

// server/routes/chat.ts — Express SSE streaming
import express from 'express';
import openai from '../lib/openai';

const router = express.Router();

router.post('/stream', async (req, res) => {
    const { message, history = [] } = req.body;

    // Headers SSE
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders();

    try {
        const stream = await openai.chat.completions.create({
            model: 'gpt-4o',
            messages: [
                { role: 'system', content: 'Tu es un assistant utile.' },
                ...history,
                { role: 'user', content: message }
            ],
            stream: true, // activer le streaming
        });

        // Émettre chaque chunk au client
        for await (const chunk of stream) {
            const delta = chunk.choices[0]?.delta?.content ?? '';
            if (delta) {
                res.write(`data: ${JSON.stringify({ delta })}\n\n`);
            }
        }

        // Signaler la fin du stream
        res.write('data: [DONE]\n\n');
        res.end();

    } catch (error) {
        res.write(`data: ${JSON.stringify({ error: 'Erreur serveur' })}\n\n`);
        res.end();
    }
});

export default router;

Consommation côté Angular

// chat.service.ts — Angular service avec fetch streaming
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ChatStreamService {
    response = signal('');
    loading = signal(false);

    async sendMessage(message: string): Promise<void> {
        this.response.set('');
        this.loading.set(true);

        try {
            const res = await fetch('/api/chat/stream', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message }),
            });

            if (!res.body) return;

            const reader = res.body.getReader();
            const decoder = new TextDecoder();

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                // Parser les lignes SSE
                const lines = decoder.decode(value).split('\n');
                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    const data = line.slice(6);
                    if (data === '[DONE]') break;

                    const { delta } = JSON.parse(data);
                    // Mettre à jour le signal avec chaque nouveau token
                    this.response.update(r => r + delta);
                }
            }
        } finally {
            this.loading.set(false);
        }
    }
}

Function Calling — cycle complet

Le Function Calling permet au modèle de déclencher des fonctions dans ton code. Le cycle est : requête → modèle décide d'appeler un tool → tu exécutes → tu renvoies le résultat → modèle répond.

// function-calling.ts — Cycle complet avec plusieurs tools
import openai from './lib/openai';

// Définition des tools disponibles
const tools = [
    {
        type: 'function' as const,
        function: {
            name: 'get_weather',
            description: 'Obtenir la météo actuelle et les prévisions pour une ville',
            parameters: {
                type: 'object',
                properties: {
                    city: { type: 'string', description: 'Nom de la ville (ex: Paris)' },
                    unit: { type: 'string', enum: ['celsius', 'fahrenheit'], default: 'celsius' }
                },
                required: ['city']
            }
        }
    },
    {
        type: 'function' as const,
        function: {
            name: 'search_articles',
            description: 'Rechercher des articles techniques sur un sujet',
            parameters: {
                type: 'object',
                properties: {
                    query: { type: 'string', description: 'Termes de recherche' },
                    limit: { type: 'number', description: 'Nombre de résultats (max 10)', default: 5 }
                },
                required: ['query']
            }
        }
    }
];

// Implémentation réelle des fonctions
async function executeToolCall(name: string, args: Record<string, any>): Promise<string> {
    switch (name) {
        case 'get_weather': {
            // Appel réel à une API météo
            const weather = await fetchWeatherApi(args.city, args.unit);
            return JSON.stringify(weather);
        }
        case 'search_articles': {
            const articles = await searchInDatabase(args.query, args.limit);
            return JSON.stringify(articles);
        }
        default:
            return JSON.stringify({ error: `Tool inconnu: ${name}` });
    }
}

// Boucle principale : répéter jusqu'à obtenir une réponse finale
async function chatWithTools(userMessage: string): Promise<string> {
    const messages: any[] = [
        { role: 'system', content: 'Tu es un assistant. Utilise les tools disponibles si nécessaire.' },
        { role: 'user', content: userMessage }
    ];

    // Boucle pour gérer les appels de tools en chaîne
    while (true) {
        const response = await openai.chat.completions.create({
            model: 'gpt-4o',
            messages,
            tools,
            tool_choice: 'auto', // 'auto' | 'none' | { type: 'function', function: { name } }
        });

        const message = response.choices[0].message;
        messages.push(message); // toujours ajouter le message assistant

        // Si pas de tool call → réponse finale
        if (response.choices[0].finish_reason === 'stop') {
            return message.content ?? '';
        }

        // Exécuter tous les tool calls en parallèle
        const toolResults = await Promise.all(
            (message.tool_calls ?? []).map(async (toolCall) => {
                const args = JSON.parse(toolCall.function.arguments);
                const result = await executeToolCall(toolCall.function.name, args);
                return {
                    role: 'tool' as const,
                    tool_call_id: toolCall.id,
                    content: result,
                };
            })
        );

        // Ajouter les résultats et continuer la boucle
        messages.push(...toolResults);
    }
}

Structured Outputs avec JSON Schema

Structured Outputs (disponible avec gpt-4o depuis août 2024) garantit que la réponse du modèle correspond exactement au schéma JSON fourni — zéro chance d'un JSON mal formé.

// structured-outputs.ts — Extraction structurée garantie
import openai from './lib/openai';
import { zodResponseFormat } from 'openai/helpers/zod';
import { z } from 'zod';

// Schéma Zod → JSON Schema automatique
const ArticleSchema = z.object({
    titre: z.string().describe('Titre de l\'article, max 60 caractères'),
    resume: z.string().describe('Résumé en 2-3 phrases'),
    tags: z.array(z.string()).describe('5 tags SEO pertinents'),
    difficulte: z.enum(['debutant', 'intermediaire', 'avance']),
    duree_lecture: z.number().describe('Durée de lecture estimée en minutes'),
    sections: z.array(z.object({
        id: z.string(),
        titre: z.string(),
        contenu_cle: z.string().describe('1-2 phrases du contenu principal')
    }))
});

type Article = z.infer<typeof ArticleSchema>;

async function extractArticleMetadata(articleText: string): Promise<Article> {
    const response = await openai.beta.chat.completions.parse({
        model: 'gpt-4o-2024-08-06', // requis pour Structured Outputs
        messages: [
            { role: 'system', content: 'Extrais les métadonnées structurées de cet article technique.' },
            { role: 'user', content: articleText }
        ],
        response_format: zodResponseFormat(ArticleSchema, 'article_metadata'),
    });

    // .parsed est typé ArticleSchema — jamais null si pas d'erreur
    const parsed = response.choices[0].message.parsed;
    if (!parsed) throw new Error('Parsing échoué');
    return parsed;
}

// Utilisation — résultat est 100% typé TypeScript
const metadata = await extractArticleMetadata(monArticle);
console.log(metadata.titre);         // string — TypeScript sait que c'est string
console.log(metadata.difficulte);    // 'debutant' | 'intermediaire' | 'avance'
console.log(metadata.sections[0].id); // string
JSON mode vs Structured Outputs : response_format: { type: 'json_object' } (ancien mode) ne garantit que du JSON valide, pas la structure. Structured Outputs garantit le respect exact du schéma — utilise-le en production.

Embeddings et recherche sémantique

Les embeddings transforment du texte en vecteurs numériques. Deux textes similaires ont des vecteurs proches — base de toute recherche sémantique et RAG.

// embeddings.ts — Génération et recherche par similarité cosinus
import openai from './lib/openai';

// Générer un embedding pour un texte
async function embed(text: string): Promise<number[]> {
    const response = await openai.embeddings.create({
        model: 'text-embedding-3-small', // 1536 dimensions, $0.02/1M tokens
        input: text,
        encoding_format: 'float',
    });
    return response.data[0].embedding;
}

// Batch : embarquer plusieurs textes en un seul appel (plus efficace)
async function embedBatch(texts: string[]): Promise<number[][]> {
    const response = await openai.embeddings.create({
        model: 'text-embedding-3-small',
        input: texts, // tableau de strings
    });
    return response.data.map(d => d.embedding);
}

// Similarité cosinus entre deux vecteurs
function cosineSimilarity(a: number[], b: number[]): number {
    const dot = a.reduce((sum, val, i) => sum + val * b[i], 0);
    const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
    const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
    return dot / (magA * magB);
}

// Recherche sémantique dans une collection d'articles
async function searchArticles(
    query: string,
    articles: { id: string; title: string; content: string; embedding?: number[] }[],
    topK = 5
) {
    const queryEmbedding = await embed(query);

    // Calculer la similarité avec chaque article
    const scored = articles
        .filter(a => a.embedding) // seulement ceux avec embedding précalculé
        .map(article => ({
            ...article,
            score: cosineSimilarity(queryEmbedding, article.embedding!)
        }))
        .sort((a, b) => b.score - a.score)
        .slice(0, topK);

    return scored;
}

// En pratique : pré-calculer les embeddings à l'ingestion et stocker en DB
// PostgreSQL pgvector : SELECT * FROM articles ORDER BY embedding <-> $1 LIMIT 5

Tokens, coûts et optimisation

Chaque appel consomme des tokens (prompt + completion). L'objet usage détaille la consommation pour le monitoring des coûts.

// Surveiller la consommation de tokens
const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: 'Bonjour' }],
});

console.log(response.usage);
// {
//   prompt_tokens: 10,         // tokens du prompt (input)
//   completion_tokens: 25,     // tokens générés (output)
//   total_tokens: 35,
//   prompt_tokens_details: {   // avec gpt-4o : détail du cache
//     cached_tokens: 0,        // tokens servis depuis le cache (gratuits)
//     audio_tokens: 0
//   }
// }

// Estimer les tokens AVANT l'appel (éviter les dépassements)
import { encoding_for_model } from '@dqbd/tiktoken';

function countTokens(text: string, model = 'gpt-4o'): number {
    const enc = encoding_for_model(model as any);
    const tokens = enc.encode(text);
    enc.free();
    return tokens.length;
}

// Stratégie de compression du contexte
function compressHistory(
    history: { role: string; content: string }[],
    maxTokens = 3000
): { role: string; content: string }[] {
    let total = 0;
    const result = [];

    // Parcourir en sens inverse → garder les plus récents
    for (let i = history.length - 1; i >= 0; i--) {
        const tokens = countTokens(history[i].content);
        if (total + tokens > maxTokens) break;
        total += tokens;
        result.unshift(history[i]);
    }

    return result;
}
  • Prompt caching (GPT-4o) : prompts système identiques de +1024 tokens → mis en cache automatiquement (-50% coût)
  • Utiliser gpt-4o-mini pour classification, extraction de données, résumé court
  • Définir max_tokens explicitement pour éviter les réponses longues involontaires
  • Batch API : jusqu'à 50% de réduction pour les traitements asynchrones non urgents

Proxy Express + retry exponentiel

Architecture recommandée en production : Angular → Express proxy → OpenAI. Le proxy gère l'authentification, le rate limiting et la journalisation.

// server/middleware/openai-proxy.ts — Proxy sécurisé avec retry
import OpenAI, { APIError, RateLimitError } from 'openai';
import rateLimit from 'express-rate-limit';

// Rate limiting par utilisateur (éviter les abus)
export const chatRateLimit = rateLimit({
    windowMs: 60 * 1000,  // 1 minute
    max: 20,              // 20 requêtes/minute par IP
    message: { error: 'Trop de requêtes — attendez 1 minute' }
});

// Retry exponentiel sur erreurs temporaires
async function callWithRetry<T>(
    fn: () => Promise<T>,
    maxRetries = 3
): Promise<T> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            return await fn();
        } catch (error) {
            if (error instanceof RateLimitError) {
                // 429 : attendre avant de réessayer
                const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s
                console.warn(`Rate limit — retry dans ${delay}ms`);
                await new Promise(r => setTimeout(r, delay));
            } else if (error instanceof APIError && error.status >= 500) {
                // 5xx : retry sur erreur serveur OpenAI
                const delay = Math.pow(2, attempt) * 500;
                await new Promise(r => setTimeout(r, delay));
            } else {
                throw error; // erreur non retryable
            }

            if (attempt === maxRetries) throw error;
        }
    }
    throw new Error('Max retries atteint');
}

// Sanitiser l'input utilisateur
function sanitizeInput(input: string): string {
    return input
        .trim()
        .slice(0, 4000)       // limiter la longueur
        .replace(/[<>]/g, '') // supprimer HTML basique
        // Ne pas filtrer trop agressivement → dégrade l'expérience
}
Journalisation et monitoring : Enregistre chaque appel API (tokens consommés, modèle, durée, erreurs) dans une base de données pour calculer les coûts réels et détecter les abus avant que la facture explose.
  • Proxy Express/Next.js comme seul point d'accès à l'API
  • Rate limiting par IP et par utilisateur authentifié
  • Retry exponentiel sur 429 et 5xx (jamais sur 4xx)
  • Sanitiser les inputs (longueur max, caractères dangereux)
  • Logger chaque appel avec tokens + coût estimé
  • Alertes budget via OpenAI dashboard (seuil mensuel)

Partager