Ir al contenido

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 recomendada
wu.ai.provider('openai', { endpoint: '/api/ai/chat', model: 'gpt-4o' });
// Directo, solo en desarrollo
wu.ai.provider('anthropic', {
endpoint: 'https://api.anthropic.com/v1/messages',
apiKey: 'sk-ant-…',
model: 'claude-sonnet-4-5-20250929',
});
// Local
wu.ai.provider('ollama', { endpoint: 'http://localhost:11434/api/chat', model: 'llama3' });
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
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.

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 };
// Esto se elimina en silencio en producción
wu.ai.provider('openai', { endpoint: 'https://api.openai.com/v1/chat/completions', apiKey: 'sk-…' });

“Parece de producción” significa:

  1. process.env.NODE_ENV === 'production' → producción. === 'development' → no. Cualquier otro valor pasa a la comprobación del hostname.
  2. 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.
  3. Rangos RFC1918 (10.*, 192.168.*, 172.16–31.*) → no es producción (LAN y pruebas en dispositivo).
  4. 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.

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' } } } },
});
// 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.