Proxy CORS y createStreamFn
Llamar a la API del LLM desde el navegador no puede evitar CORS. proxy-utils.ts de pi-web-ui concentra tres decisiones: "ir o no por proxy", "cómo reescribir baseUrl" y "es este error CORS", y las envuelve vía createStreamFn en una función compatible con Agent.streamFn. AgentInterface la cuelga en session.streamFn al hacer setupSessionSubscription, y a partir de ahí toda llamada al LLM pasa por esta capa.
Responsabilidades
- Decidir proxy por provider:
shouldUseProxyForProvider(provider, apiKey)mantiene una lista blanca;zaiyopenai-codexfuerzan proxy; el token OAuth de Anthropic (sk-ant-oat-*) va por proxy; el resto va directo. Verpackages/web-ui/src/utils/proxy-utils.ts:19-51. - Reescribir baseUrl:
applyProxyIfNeededenvuelvemodel.baseUrlcomo${proxyUrl}/?url=${encodeURIComponent(baseUrl)}, sin mutar el model original. Verpackages/web-ui/src/utils/proxy-utils.ts:61-82. - Identificar errores CORS:
isCorsErrorcubreTypeError: Failed to fetch,NetworkErrory mensajes que contienencors/cross-origin. Verpackages/web-ui/src/utils/proxy-utils.ts:94-118. - Envolver streamFn:
createStreamFn(getProxyUrl)devuelve(model, context, options) => Promise, que internamente decide directo o proxy. Verpackages/web-ui/src/utils/proxy-utils.ts:127-139.
Motivación de diseño
¿Por qué no dejar que el host mute el model antes de cada llamada? Porque el host no sabe qué provider necesita proxy: la API key común de Anthropic va directa, pero el token OAuth tiene que ir por proxy; OpenAI Codex siempre va por proxy; Z-AI siempre va por proxy. Estas reglas se centralizan en proxy-utils.ts y al añadir un provider basta tocar un sitio. createStreamFn recibe un callback getProxyUrl en vez de leer storage directamente, porque AgentInterface necesita leer dinámicamente proxy.enabled y proxy.url del usuario; no se puede leer una vez al ensamblar.
Estrategia de directo primero: si !apiKey || !proxyUrl se llama directo a streamSimple(model, context, options), evitando copiar el objeto model sin necesidad. Sólo cuando hay key y proxyUrl a la vez se llama applyProxyIfNeeded para decidir si reescribir; la mayoría de los providers no serán reescritos porque shouldUseProxyForProvider devuelve false y se quedan con el model original.
Archivos clave
packages/web-ui/src/utils/proxy-utils.ts:19-51—shouldUseProxyForProvider, decisión por provider y prefijo de key.packages/web-ui/src/utils/proxy-utils.ts:61-82—applyProxyIfNeeded, devuelve un model nuevo, sin tocar el original.packages/web-ui/src/utils/proxy-utils.ts:94-118—isCorsError, cubre varias formas de error de navegador.packages/web-ui/src/utils/proxy-utils.ts:127-139—createStreamFn, punto de ensamblaje.packages/web-ui/src/components/AgentInterface.ts:138-143—AgentInterfaceinvocacreateStreamFnpasando un closure que lee proxy.url desdeAppStorage.
La lógica entera de createStreamFn es corta; el núcleo es invocar streamSimple según el caso:
// packages/web-ui/src/utils/proxy-utils.ts:127-139
export function createStreamFn(getProxyUrl: () => Promise<string | undefined>) {
return async (model: Model<any>, context: Context, options?: SimpleStreamOptions) => {
const apiKey = options?.apiKey;
const proxyUrl = await getProxyUrl();
if (!apiKey || !proxyUrl) {
return streamSimple(model, context, options);
}
const proxiedModel = applyProxyIfNeeded(model, apiKey, proxyUrl);
return streamSimple(proxiedModel, context, options);
};
}shouldUseProxyForProvider enumera con switch la política por provider, por defecto false:
// packages/web-ui/src/utils/proxy-utils.ts:19-51
export function shouldUseProxyForProvider(provider: string, apiKey: string): boolean {
switch (provider.toLowerCase()) {
case "zai":
return true;
case "anthropic":
return apiKey.startsWith("sk-ant-oat") || apiKey.startsWith("{");
case "openai-codex":
return true;
case "openai":
case "google":
// ... otros providers directos ...
return false;
default:
return false;
}
}Flujo de datos
Camino de decisión de la petición LLM desde Agent hasta streamSimple:
Límites y fallos
- Sin proxy configurado:
getProxyUrldevuelveundefinedy se llama directo astreamSimple, sin crashear por falta de proxy. Verpackages/web-ui/src/utils/proxy-utils.ts:132-134. - Model sin baseUrl:
applyProxyIfNeededdevuelve el model original sin error, deja al host gestionarlo. Verpackages/web-ui/src/utils/proxy-utils.ts:67-70. - Falsos positivos en isCorsError:
TypeError: Failed to fetchtambién puede ser red caída;isCorsErrorlo marca como CORS;extract-document.tslo usa para decidir si reintenta por proxy. Verpackages/web-ui/src/tools/extract-document.ts:108-119. - Condición de reemplazo de streamFn:
AgentInterfacesólo reemplaza sisession.streamFn === streamSimple; si el host ya inyectó un streamFn custom, no lo sobrescribe. Verpackages/web-ui/src/components/AgentInterface.ts:138.
Resumen
proxy-utils consolida las decisiones CORS del navegador en unas pocas funciones puras: shouldUseProxyForProvider, applyProxyIfNeeded, isCorsError, createStreamFn. AgentInterface las envuelve al ensamblar, y cada llamada al LLM decide dinámicamente según provider y key. El flujo de ensamblaje en host de sesión AgentInterface; la configuración de proxy que lee storage en AppStorage e IndexedDB.