Skip to content

Proxy CORS y createStreamFn

源码版本v0.73.1

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

  1. Decidir proxy por provider: shouldUseProxyForProvider(provider, apiKey) mantiene una lista blanca; zai y openai-codex fuerzan proxy; el token OAuth de Anthropic (sk-ant-oat-*) va por proxy; el resto va directo. Ver packages/web-ui/src/utils/proxy-utils.ts:19-51.
  2. Reescribir baseUrl: applyProxyIfNeeded envuelve model.baseUrl como ${proxyUrl}/?url=${encodeURIComponent(baseUrl)}, sin mutar el model original. Ver packages/web-ui/src/utils/proxy-utils.ts:61-82.
  3. Identificar errores CORS: isCorsError cubre TypeError: Failed to fetch, NetworkError y mensajes que contienen cors/cross-origin. Ver packages/web-ui/src/utils/proxy-utils.ts:94-118.
  4. Envolver streamFn: createStreamFn(getProxyUrl) devuelve (model, context, options) => Promise, que internamente decide directo o proxy. Ver packages/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

La lógica entera de createStreamFn es corta; el núcleo es invocar streamSimple según el caso:

typescript
// 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:

typescript
// 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

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.