Proveedores
Un proveedor es un endpoint más un adaptador que normaliza peticiones y respuestas. Hay tres adaptadores integrados; cualquier otro lo aportas tú.
// A través de tu propio proxy: la forma recomendadawu.ai.provider('openai', { endpoint: '/api/ai/chat', model: 'gpt-4o' });
// Directo, solo en desarrollowu.ai.provider('anthropic', { endpoint: 'https://api.anthropic.com/v1/messages', apiKey: 'sk-ant-…', model: 'claude-sonnet-4-5-20250929',});
// Localwu.ai.provider('ollama', { endpoint: 'http://localhost:11434/api/chat', model: 'llama3' });Adaptadores integrados
Sección titulada «Adaptadores integrados»| Nombre | Modelo por defecto | Formato de red |
|---|---|---|
openai |
gpt-4o |
Forma de /chat/completions, Authorization: Bearer |
anthropic |
claude-sonnet-4-5-20250929 |
Messages API, x-api-key + anthropic-version: 2023-06-01 |
ollama |
llama3 |
Forma de /api/chat, sin cabecera de autenticación |
Configuración
Sección titulada «Configuración»function provider(name: string, config: { adapter?: 'openai' | 'anthropic' | 'ollama'; endpoint?: string; baseUrl?: string; apiKey?: string; unsafeAllowDirectKey?: boolean; model?: string; active?: boolean; send?: (messages: Message[], options: object) => Promise<Response>; stream?: (messages: Message[], options: object) => AsyncGenerator<Chunk>;}): Wu;| Opción | Por defecto | Significado |
|---|---|---|
adapter |
el name |
Qué adaptador integrado usar cuando el nombre del proveedor no es uno de ellos |
endpoint |
— | URL a la que hacer POST. Una ruta como /api/ai/chat se resuelve contra el origen actual |
baseUrl |
— | Alias; se usa cuando no hay endpoint |
apiKey |
— | Se envía como cabecera de autenticación del adaptador. Bloqueado en producción (ver más abajo) |
unsafeAllowDirectKey |
false |
Desactivación explícita de ese bloqueo |
model |
según el adaptador | Se puede sobrescribir por llamada con options.model |
active |
true |
Con active: false se registra sin cambiar el proveedor activo |
send / stream |
— | Aportar cualquiera de los dos lo convierte en un adaptador personalizado |
Registra varios y elige por llamada:
wu.ai.provider('openai', { endpoint: '/api/openai' });wu.ai.provider('anthropic', { endpoint: '/api/anthropic', active: false });
await wu.ai.send('Summarize this', { provider: 'anthropic' });El primer proveedor registrado pasa a ser el activo. Cualquier registro posterior también pasa a activo salvo que use active: false.
Proveedores personalizados
Sección titulada «Proveedores personalizados»Aporta send, stream o ambos. Los adaptadores personalizados se saltan por completo formatRequest/parseResponse: recibes los mensajes normalizados de wu y debes devolver las formas normalizadas de wu.
wu.ai.provider('my-llm', { async send(messages, options) { const r = await fetch('/my/llm', { method: 'POST', body: JSON.stringify({ messages, tools: options.tools }), signal: options.signal, }); const data = await r.json(); return { content: data.text, tool_calls: data.calls?.map(c => ({ id: c.id, name: c.fn, arguments: c.args })), usage: { prompt_tokens: data.in, completion_tokens: data.out }, }; },
async *stream(messages, options) { for await (const piece of myStream(messages)) { yield { type: 'text', content: piece }; } yield { type: 'done' }; },});Los formatos normalizados:
type Message = { role: 'system' | 'user' | 'assistant' | 'tool'; content: string; tool_calls?: Array<{ id: string; name: string; arguments: object }>; tool_call_id?: string;};
type Response = { content: string; tool_calls?: Array<{ id: string; name: string; arguments: object }>; usage?: { prompt_tokens: number; completion_tokens: number };};
type Chunk = | { type: 'text'; content: string } | { type: 'tool_call_start'; id: string; name: string } | { type: 'tool_call_delta'; index?: number; id?: string; name?: string; argumentsDelta: string } | { type: 'usage'; usage: object } | { type: 'done' } | { type: 'error'; error: string };API keys en el navegador
Sección titulada «API keys en el navegador»// Esto se elimina en silencio en producciónwu.ai.provider('openai', { endpoint: 'https://api.openai.com/v1/chat/completions', apiKey: 'sk-…' });“Parece de producción” significa:
process.env.NODE_ENV === 'production'→ producción.=== 'development'→ no. Cualquier otro valor pasa a la comprobación del hostname.- El hostname es
localhost,127.0.0.1,0.0.0.0,[::1],::1,host.docker.internal, o termina en.local/.localhost/.test→ no es producción. - Rangos RFC1918 (
10.*,192.168.*,172.16–31.*) → no es producción (LAN y pruebas en dispositivo). - Cualquier otra cosa → producción.
Para un host de staging o un túnel que la heurística no puede distinguir de producción, desactívalo explícitamente:
wu.ai.provider('openai', { endpoint: '…', apiKey: '…', unsafeAllowDirectKey: true });El nombre es la documentación. La respuesta correcta es un proxy en servidor que guarde la clave y reenvíe la petición.
Salida JSON
Sección titulada «Salida JSON»Pasa responseFormat a send(), o usa wu.ai.json().
| Proveedor | 'json' |
{ type: 'json_schema', schema, name? } |
|---|---|---|
| OpenAI | response_format: { type: 'json_object' } |
Modo json_schema nativo (strict: true salvo que pases strict: false) |
| Ollama | format: 'json' |
format: <schema> |
| Anthropic | Instrucción añadida al prompt de sistema | Esquema serializado dentro del prompt de sistema |
Cuando responseFormat está definido, wu intenta un JSON.parse del resultado y expone parsed o parseError. wu.ai.json() envuelve eso:
const { data, raw, error, usage } = await wu.ai.json('List 5 colors', { schema: { type: 'object', properties: { colors: { type: 'array', items: { type: 'string' } } } },});Reintentos
Sección titulada «Reintentos»// Valores por defecto{ maxRetries: 3, baseDelayMs: 1000 } // 1s, 2s, 4s — exponencial| Condición | Comportamiento |
|---|---|
429 |
Se reintenta |
>= 500 |
Se reintenta |
Otros 4xx |
No se reintenta: falla de inmediato |
| Error de red | Se reintenta |
AbortError |
Se relanza de inmediato |
Los fallos repetidos también alimentan el circuit breaker, que es lo que evita machacar un endpoint que está fallando.
