Integrați API-ul OpenAI și Anthropic într-o aplicație web
Integrarea modelului de limbă API este una dintre cele mai solicitate abilități in modern web development. Fie că este vorba despre construirea unui chatbot, a unui asistent pentru generarea de conținut sau un sistem de analiză a textului, capacitatea de a comunica effectively with the APIs of OpenAI e antropică este fundamental.
În acest al patrulea articol al seriei AI pentru dezvoltatori web, vom explora în detaliu cum să integrați ambele API-uri într-o aplicație web completă. Vom începe de la configurare inițial, vom analiza structurile mesajelor, vom implementa fluxul de răspuns și vom construi o arhitectură sigură cu modele proxy pentru a proteja cheile API.
Ce vei învăța
- Configurați și autentificați-vă cu API-urile OpenAI și Anthropic
- Înțelegeți diferențele structurale dintre cele două sisteme de mesaje
- Implementați fluxul de răspuns pentru o experiență în timp real
- Gestionați robust erorile, reîncercați și limitarea ratei
- Numărați jetoanele și optimizați costurile de utilizare
- Creați un proxy backend securizat pentru a proteja cheile API
- Creați o interfață completă de chat cu suport pentru streaming
Prezentare generală a seriei
| # | Articol | Concentrează-te |
|---|---|---|
| 1 | Introducere în RAG | Concepte fundamentale |
| 2 | RAG cu TypeScript și LangChain | Implementare practică |
| 3 | Baza de date vectorială comparată | Chromadb, Pinecone, Weaviate |
| 4 | OpenAI și API-uri antropice (ești aici) | Integrare API |
| 5 | LLM Local cu Ollama | AI fără cloud |
| 6 | Reglaj fin vs RAG | Când să folosești ce |
| 7 | Agenți AI | Arhitectură bazată pe agenți |
| 8 | AI în CI/CD | Automatizare inteligentă |
1. Configurare API și autentificare
Înainte de a putea trimite prima cerere, trebuie să obțineți datele de conectare și configurați corect mediul de dezvoltare. Ambii furnizori necesită a Cheie API care trebuie gestionată în siguranță.
Obțineți chei API
Pentru OpenAI, înregistrează-te pe platform.openai.com și generează o cheie
în secțiunea Chei API. Pentru antropică, accesați consola la
console.anthropic.com și creați o cheie API în panoul de setări.
Securitate cheie API
Nu includeți niciodată cheile API direct în codul sursă sau în fișierele comise pe Git. Utilizați întotdeauna variabile de mediu sau un serviciu de management secret. O cheie expusă poate genera costuri neașteptate și poate compromite securitatea.
# File .env (mai committare su Git!)
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxx
# Configurazione opzionale
OPENAI_ORG_ID=org-xxxxxxxxxxxxxxxx
OPENAI_MODEL=gpt-4o
ANTHROPIC_MODEL=claude-sonnet-4-20250514
Instalarea SDK-urilor
Ambii furnizori oferă SDK-uri oficiale pentru TypeScript/JavaScript care facilitează acest lucru integrare semnificativ. SDK-urile se ocupă automat de serializare, deserializarea de bază și reîncercarea.
# SDK ufficiali
npm install openai @anthropic-ai/sdk
# Gestione variabili d'ambiente
npm install dotenv
# Per il server proxy Express
npm install express cors
npm install -D @types/express @types/cors
Inițializarea clientului
// src/lib/ai-clients.ts
import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import dotenv from 'dotenv';
dotenv.config();
// Client OpenAI
export const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
organization: process.env.OPENAI_ORG_ID, // opzionale
});
// Client Anthropic
export const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Verifica configurazione
export function validateConfig(): boolean {
if (!process.env.OPENAI_API_KEY) {
console.error('OPENAI_API_KEY non configurata');
return false;
}
if (!process.env.ANTHROPIC_API_KEY) {
console.error('ANTHROPIC_API_KEY non configurata');
return false;
}
return true;
}
2. Compararea structurii mesajului
Deși ambele API-uri funcționează cu conceptul de mesaje într-o conversație, structurile diferă semnificativ. Înțelegeți aceste diferențe este esenţială pentru construirea unei abstracţiuni unificate.
Compararea structurilor
| Caracteristică | OpenAI | antropică |
|---|---|---|
| Roluri de mesaje | sistem, utilizator, asistent, instrument | utilizator, asistent (sistem separat) |
| prompt de sistem | În mesaje | Parametru dedicat system |
| Model | model: "gpt-4o" | model: "claude-sonnet-4-20250514" |
| Ieșire maximă | max_tokens (optional) | max_tokens (obligatoriu) |
| Temperatură | 0,0 - 2,0 (implicit 1,0) | 0,0 - 1,0 (implicit 1,0) |
| Streaming | Evenimente trimise de server | Evenimente trimise de server |
| Puncte finale | /v1/chat/completions | /v1/messages |
Solicitat cu OpenAI
// src/lib/openai-chat.ts
import { openai } from './ai-clients';
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export async function chatWithOpenAI(
messages: ChatMessage[],
model: string = 'gpt-4o'
): Promise<string> {
const response = await openai.chat.completions.create({
model,
messages,
temperature: 0.7,
max_tokens: 2048,
});
return response.choices[0].message.content ?? '';
}
// Esempio di utilizzo
const risposta = await chatWithOpenAI([
{ role: 'system', content: 'Sei un assistente tecnico esperto.' },
{ role: 'user', content: 'Spiega il pattern Observer in TypeScript.' },
]);
console.log(risposta);
Solicitați cu Anthropic
// src/lib/anthropic-chat.ts
import { anthropic } from './ai-clients';
interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}
export async function chatWithAnthropic(
messages: ChatMessage[],
systemPrompt: string = '',
model: string = 'claude-sonnet-4-20250514'
): Promise<string> {
const response = await anthropic.messages.create({
model,
max_tokens: 2048,
system: systemPrompt,
messages,
});
// Il contenuto è un array di blocchi
const textBlock = response.content.find(
(block) => block.type === 'text'
);
return textBlock?.text ?? '';
}
// Esempio di utilizzo
const risposta = await chatWithAnthropic(
[{ role: 'user', content: 'Spiega il pattern Observer in TypeScript.' }],
'Sei un assistente tecnico esperto.'
);
console.log(risposta);
3. Streaming de răspunsuri
Streamingul este cheia pentru a oferi o experiență perfectă pentru utilizator. În loc să aștepți că întregul mesaj este generat, jetoanele sunt trimise ca model le produce, reducând drastic timpul de așteptare perceput.
Streaming cu OpenAI
// src/lib/openai-stream.ts
import { openai } from './ai-clients';
export async function* streamOpenAI(
messages: Array<{ role: string; content: string }>,
model: string = 'gpt-4o'
): AsyncGenerator<string> {
const stream = await openai.chat.completions.create({
model,
messages: messages as any,
stream: true,
temperature: 0.7,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
// Utilizzo
async function main() {
const messages = [
{ role: 'user', content: 'Scrivi una funzione di ordinamento.' }
];
for await (const token of streamOpenAI(messages)) {
process.stdout.write(token);
}
}
Streaming cu Anthropic
// src/lib/anthropic-stream.ts
import { anthropic } from './ai-clients';
export async function* streamAnthropic(
messages: Array<{ role: string; content: string }>,
systemPrompt: string = '',
model: string = 'claude-sonnet-4-20250514'
): AsyncGenerator<string> {
const stream = anthropic.messages.stream({
model,
max_tokens: 2048,
system: systemPrompt,
messages: messages as any,
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
yield event.delta.text;
}
}
}
// Utilizzo con metriche
async function main() {
const start = Date.now();
let totalTokens = 0;
for await (const token of streamAnthropic(
[{ role: 'user', content: 'Scrivi una funzione di ordinamento.' }],
'Sei un programmatore esperto.'
)) {
process.stdout.write(token);
totalTokens++;
}
console.log(`\nTempo: ${Date.now() - start}ms, Token: ${totalTokens}`);
}
4. Gestionarea erorilor și reîncercați
API-urile externe pot eșua din mai multe motive: limite de rată, timeout-uri, erori de server sau probleme de rețea. Un sistem robust de reîncercare este esențial pentru a asigura fiabilitatea aplicației.
Coduri de eroare comune
| Cod | Sens | Strategie |
|---|---|---|
| 400 | Cerere incorectă | Nu mai încercați, corectați parametrii |
| 401 | Autentificarea eșuată | Verificați cheia API |
| 429 | Limita ratei a fost depășită | Vă rugăm să așteptați și să încercați din nou cu backoff |
| 500 | Eroare de server | Încercați din nou cu backoff exponențial |
| 503 | Serviciu indisponibil | Încercați din nou după câteva secunde |
| 529 | Supraîncărcare (antropică) | Încercați din nou cu retragere lungă |
// src/lib/retry.ts
interface RetryConfig {
maxRetries: number;
baseDelay: number; // millisecondi
maxDelay: number; // millisecondi
backoffFactor: number;
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
backoffFactor: 2,
};
export async function withRetry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const cfg = { ...DEFAULT_CONFIG, ...config };
let lastError: Error | null = null;
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Non ritentare per errori client (4xx eccetto 429)
if (error.status && error.status >= 400 &&
error.status < 500 && error.status !== 429) {
throw error;
}
if (attempt === cfg.maxRetries) break;
// Calcola il delay con jitter
const delay = Math.min(
cfg.baseDelay * Math.pow(cfg.backoffFactor, attempt) +
Math.random() * 1000,
cfg.maxDelay
);
console.warn(
`Tentativo ${attempt + 1} fallito. ` +
`Retry tra ${Math.round(delay)}ms...`
);
// Usa Retry-After se disponibile
const retryAfter = error.headers?.get?.('retry-after');
const waitTime = retryAfter
? parseInt(retryAfter) * 1000
: delay;
await new Promise(r => setTimeout(r, waitTime));
}
}
throw lastError;
}
5. Limitarea ratei și controlul frecvenței
Când aplicația dvs. gestionează mulți utilizatori concurenți, este esențial să o implementați un sistem de limitare a ratei la nivelul clientului pentru a evita depășirea limitelor impuse de către furnizori și să asigure utilizarea echitabilă a resurselor.
// src/lib/rate-limiter.ts
export class TokenBucketRateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private maxTokens: number,
private refillRate: number, // token al secondo
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.maxTokens,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens < 1) {
const waitTime = (1 - this.tokens) / this.refillRate * 1000;
await new Promise(r => setTimeout(r, waitTime));
this.refill();
}
this.tokens -= 1;
}
}
// Limiti per provider
const openaiLimiter = new TokenBucketRateLimiter(60, 1); // 60 RPM
const anthropicLimiter = new TokenBucketRateLimiter(50, 0.83); // 50 RPM
6. Numărarea jetoanelor și optimizarea costurilor
Costurile API sunt direct proporționale cu numărul de jetoane procesate. Monitorizarea și optimizarea consumului de token-uri este crucială pentru menținerea costurilor sub control, în special în aplicațiile cu trafic intens.
Costuri indicative pentru 1M Token (februarie 2026)
| Model | Intrare (token 1M) | Ieșire (token 1M) |
|---|---|---|
| GPT-4o | 2,50 USD | 10,00 USD |
| GPT-4o mini | 0,15 USD | 0,60 USD |
| Claude Sonetul 4 | 3,00 USD | 15,00 USD |
| Claude Haiku 3.5 | 0,80 USD | 4,00 USD |
Nota: Preturile sunt orientative si pot fi modificate. Verificați întotdeauna documentația oficială pentru costuri actualizate.
// src/lib/token-counter.ts
import { encoding_for_model } from 'tiktoken';
export class TokenCounter {
private encoder;
constructor(model: string = 'gpt-4o') {
this.encoder = encoding_for_model(model as any);
}
count(text: string): number {
return this.encoder.encode(text).length;
}
countMessages(
messages: Array<{ role: string; content: string }>
): number {
let total = 0;
for (const msg of messages) {
total += 4; // overhead per messaggio
total += this.count(msg.role);
total += this.count(msg.content);
}
total += 2; // overhead della richiesta
return total;
}
estimateCost(
inputTokens: number,
outputTokens: number,
inputCostPer1M: number,
outputCostPer1M: number
): number {
return (
(inputTokens / 1_000_000) * inputCostPer1M +
(outputTokens / 1_000_000) * outputCostPer1M
);
}
dispose(): void {
this.encoder.free();
}
}
7. Model proxy pentru securitatea cheii API
Cheile API nu trebuie nu fi expus la front-end. Modelul recomandat este să creați un server proxy care să acționeze ca intermediar între browserul utilizatorului și API-urile furnizorului AI. Frontend-ul comunică cu al tău server, iar serverul gestionează cheile în siguranță.
// src/server/proxy.ts
import express from 'express';
import cors from 'cors';
import { openai, anthropic } from '../lib/ai-clients';
import { withRetry } from '../lib/retry';
const app = express();
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(express.json());
// Middleware di rate limiting per IP
const ipRequests = new Map<string, number[]>();
function rateLimitMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const ip = req.ip ?? 'unknown';
const now = Date.now();
const windowMs = 60_000; // 1 minuto
const maxRequests = 20;
const requests = (ipRequests.get(ip) ?? [])
.filter(t => now - t < windowMs);
if (requests.length >= maxRequests) {
return res.status(429).json({
error: 'Troppe richieste. Riprova tra un minuto.'
});
}
requests.push(now);
ipRequests.set(ip, requests);
next();
}
app.use('/api', rateLimitMiddleware);
// Endpoint chat unificato
app.post('/api/chat', async (req, res) => {
const { provider, messages, system, model } = req.body;
try {
let content: string;
if (provider === 'openai') {
const result = await withRetry(() =>
openai.chat.completions.create({
model: model ?? 'gpt-4o',
messages,
temperature: 0.7,
})
);
content = result.choices[0].message.content ?? '';
} else {
const result = await withRetry(() =>
anthropic.messages.create({
model: model ?? 'claude-sonnet-4-20250514',
max_tokens: 2048,
system: system ?? '',
messages,
})
);
const textBlock = result.content.find(b => b.type === 'text');
content = textBlock?.text ?? '';
}
res.json({ content });
} catch (error: any) {
res.status(error.status ?? 500).json({
error: error.message ?? 'Errore interno del server'
});
}
});
app.listen(3001, () =>
console.log('Proxy AI in ascolto su porta 3001')
);
8. Streaming de răspunsuri către front-end
Pentru a aduce experiența de streaming la browserul utilizatorului, serverul proxy trebuie să sprijine Evenimente trimise de server (SSE). Acest protocol permite către server pentru a trimite date către client în mod incremental printr-un singur conexiune HTTP.
// src/server/stream-endpoint.ts
app.post('/api/chat/stream', async (req, res) => {
const { provider, messages, system, model } = req.body;
// Configura headers SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
if (provider === 'openai') {
const stream = await openai.chat.completions.create({
model: model ?? 'gpt-4o',
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
} else {
const stream = anthropic.messages.stream({
model: model ?? 'claude-sonnet-4-20250514',
max_tokens: 2048,
system: system ?? '',
messages,
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
res.write(
`data: ${JSON.stringify({ content: event.delta.text })}\n\n`
);
}
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error: any) {
res.write(
`data: ${JSON.stringify({ error: error.message })}\n\n`
);
res.end();
}
});
Client front-end pentru streaming
// src/app/services/ai-chat.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AiChatService {
private apiUrl = '/api/chat';
streamChat(
provider: 'openai' | 'anthropic',
messages: Array<{ role: string; content: string }>,
system?: string
): Observable<string> {
return new Observable(subscriber => {
const controller = new AbortController();
fetch(`${this.apiUrl}/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, messages, system }),
signal: controller.signal,
})
.then(response => {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
function read() {
reader.read().then(({ done, value }) => {
if (done) {
subscriber.complete();
return;
}
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
subscriber.complete();
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.content) {
subscriber.next(parsed.content);
}
} catch {
// Ignora righe non parsabili
}
}
}
read();
});
}
read();
})
.catch(err => subscriber.error(err));
return () => controller.abort();
});
}
}
9. Construiți o interfață completă de chat
Combinând toate componentele construite până acum, putem crea o interfață Chat complet care acceptă ambii furnizori, gestionează istoricul mesajelor și arată răspunsurile în flux în timp real.
// src/app/components/chat/chat.component.ts
import { Component, signal } from '@angular/core';
import { AiChatService } from '../../services/ai-chat.service';
interface Message {
role: 'user' | 'assistant';
content: string;
provider?: string;
timestamp: Date;
}
@Component({
selector: 'app-chat',
standalone: true,
template: `...`,
})
export class ChatComponent {
messages = signal<Message[]>([]);
inputText = signal('');
isLoading = signal(false);
provider = signal<'openai' | 'anthropic'>('anthropic');
currentStream = signal('');
constructor(private chatService: AiChatService) {}
sendMessage(): void {
const text = this.inputText().trim();
if (!text || this.isLoading()) return;
// Aggiungi messaggio utente
this.messages.update(msgs => [
...msgs,
{ role: 'user', content: text, timestamp: new Date() }
]);
this.inputText.set('');
this.isLoading.set(true);
this.currentStream.set('');
// Prepara messaggi per l'API
const apiMessages = this.messages().map(m => ({
role: m.role,
content: m.content,
}));
// Avvia streaming
this.chatService.streamChat(
this.provider(),
apiMessages,
'Sei un assistente esperto e amichevole.'
).subscribe({
next: (token) => {
this.currentStream.update(s => s + token);
},
complete: () => {
this.messages.update(msgs => [
...msgs,
{
role: 'assistant',
content: this.currentStream(),
provider: this.provider(),
timestamp: new Date(),
}
]);
this.currentStream.set('');
this.isLoading.set(false);
},
error: (err) => {
console.error('Errore streaming:', err);
this.isLoading.set(false);
},
});
}
}
10. Cele mai bune practici și optimizare
După implementarea integrării de bază, există mai multe practici avansate care îmbunătățesc semnificativ calitatea, fiabilitatea și eficiența a aplicației dvs.
Abstracție unificată a furnizorilor
Crearea unei interfețe comune vă permite să treceți de la un furnizor la altul fără a modifica codul aplicației. Acest lucru facilitează, de asemenea, implementarea a strategiilor de rezervă.
// src/lib/unified-ai.ts
interface AIProvider {
chat(messages: ChatMessage[], system?: string): Promise<string>;
stream(messages: ChatMessage[], system?: string): AsyncGenerator<string>;
}
export class UnifiedAI {
private providers: Map<string, AIProvider> = new Map();
private primaryProvider: string;
constructor(primary: string) {
this.primaryProvider = primary;
}
registerProvider(name: string, provider: AIProvider): void {
this.providers.set(name, provider);
}
async chatWithFallback(
messages: ChatMessage[],
system?: string
): Promise<{ content: string; provider: string }> {
const providerOrder = [
this.primaryProvider,
...Array.from(this.providers.keys())
.filter(p => p !== this.primaryProvider)
];
for (const name of providerOrder) {
const provider = this.providers.get(name);
if (!provider) continue;
try {
const content = await withRetry(
() => provider.chat(messages, system),
{ maxRetries: 2 }
);
return { content, provider: name };
} catch (error) {
console.warn(`Provider ${name} fallito, provo il successivo...`);
}
}
throw new Error('Tutti i provider AI sono non disponibili');
}
}
Greșeli frecvente de evitat
- Expuneți cheile API în interfață: utilizați întotdeauna un server proxy
- Ignorarea limitelor de rată: implementează un limitator de rată pe partea clientului
- Nu gestionați timeout-urile: configurați timeout-uri adecvate pentru fiecare cerere
- Trimiteți întregul istoric: trunchiați mesajele vechi pentru a salva jetoane
- Costurile care nu se urmăresc: implementează alerte privind pragurile de cheltuieli
- Codați modelul: faceți șablonul configurabil pentru actualizări ușoare
Lista de verificare a integrării
- Cheile API salvate în variabilele de mediu și niciodată în cod
- Server proxy configurat cu CORS și limitarea ratei
- Reîncercați cu backoff-ul exponențial implementat
- Streamingul funcționează pentru ambii furnizori
- Numărarea activă a jetoanelor și monitorizarea costurilor
- Gestionarea erorilor cu mesaje informative pentru utilizator
- Teste de integrare pentru ambii furnizori
- Înregistrare centralizată pentru depanare
Concluzie
Integrarea OpenAI și a API-urilor antropice necesită atenție la detalii dar, odată configurat corect, oferă o bază solidă pe care să o construiți aplicații AI puternice și fiabile. Modelul proxy vă protejează cheile, sistemul de reîncercare garantează rezistență, iar streamingul oferă o experiență utilizator de calitate.
În articolul următor vom explora cum să o facem LLM locale cu Ollama, eliminând dependența de cloud și deschizând posibilități interesante pentru confidențialitate, costuri și dezvoltare offline.







