Skip to content

CORS-Proxy und createStreamFn

源码版本v0.73.1

Wer im Browser LLM-APIs aufruft, kommt an CORS nicht vorbei. pi-web-ui bündelt in proxy-utils.ts drei Entscheidungen — „Proxy nutzen oder nicht", „wie baseUrl umschreiben", „ist der Fehler wirklich CORS" — und wickelt sie über createStreamFn in eine Funktion ein, die mit Agent.streamFn kompatibel ist. AgentInterface hängt sie in setupSessionSubscription an session.streamFn; danach läuft jeder LLM-Aufruf über diese Schicht.

Zuständigkeiten

  1. Proxy-Entscheidung nach provider: shouldUseProxyForProvider(provider, apiKey) hat eine eingebaute Whitelist: zai und openai-codex gehen immer über den Proxy; Anthropic OAuth-Token (sk-ant-oat-*) über den Proxy, die restlichen Provider direkt. Siehe packages/web-ui/src/utils/proxy-utils.ts:19-51.
  2. baseUrl umschreiben: applyProxyIfNeeded wickelt model.baseUrl in ${proxyUrl}/?url=${encodeURIComponent(baseUrl)}, ohne das ursprüngliche model anzufassen. Siehe packages/web-ui/src/utils/proxy-utils.ts:61-82.
  3. CORS-Fehler erkennen: isCorsError matcht TypeError: Failed to fetch, NetworkError sowie Meldungen mit cors/cross-origin. Siehe packages/web-ui/src/utils/proxy-utils.ts:94-118.
  4. streamFn einwickeln: createStreamFn(getProxyUrl) liefert (model, context, options) => Promise und entscheidet intern zwischen Direktverbindung und Proxy. Siehe packages/web-ui/src/utils/proxy-utils.ts:127-139.

Designmotivation

Warum lässt man den Host nicht vor jedem Aufruf selbst das model umschreiben? Weil der Host nicht weiß, welcher Provider Proxy braucht — ein normaler Anthropic API key geht direkt, ein OAuth-Token zwingend über den Proxy; OpenAI Codex immer über den Proxy; Z-AI immer über den Proxy. Diese Regeln liegen gebündelt in proxy-utils.ts; ein neuer Provider bedeutet nur eine Stelle. createStreamFn bekommt einen getProxyUrl-Callback und liest nicht selbst aus dem storage, weil AgentInterface dynamisch die vom Nutzer gesetzten proxy.enabled und proxy.url lesen will und das nicht einmalig beim Verdrahten einfrieren kann.

Direkt-Strategie hat Vorrang: Bei !apiKey || !proxyUrl wird direkt streamSimple(model, context, options) aufgerufen und das model-Objekt unnötig kopiert. Nur wenn key und proxyUrl vorliegen, entscheidet applyProxyIfNeeded, ob wirklich umgeschrieben wird — bei den meisten Providern liefert shouldUseProxyForProvider false und das model bleibt unverändert.

Wichtige Dateien

Die gesamte Logik von createStreamFn ist kurz; im Kern wird nach Fall streamSimple aufgerufen:

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 listet die Strategien je Provider in einer switch, default 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":
        // ... weitere Direktverbindungs-Provider ...
            return false;
        default:
            return false;
    }
}

Datenfluss

Der Entscheidungspfad einer LLM-Anfrage vom Agent zu streamSimple:

Randbedingungen und Fehler

  • Kein Proxy konfiguriert: getProxyUrl liefert undefined, es geht direkt per streamSimple weiter; die Anwendung crasht nicht, weil kein Proxy konfiguriert ist. Siehe packages/web-ui/src/utils/proxy-utils.ts:132-134.
  • model ohne baseUrl: applyProxyIfNeeded liefert das Original-model zurück, wirft nicht und überlässt es dem Host. Siehe packages/web-ui/src/utils/proxy-utils.ts:67-70.
  • Unbekannter provider: shouldUseProxyForProvider ist default false; ein neuer Provider funktioniert ohne Code-Änderung direkt — der Preis ist, dass bei einem Provider, der tatsächlich Proxy bräuchte, ein CORS-Fehler hochkommt.
  • Falsch-positive CORS-Erkennung: TypeError: Failed to fetch kann auch ein Netz weg sein; isCorsError wertet es als CORS. extract-document.ts nutzt das, um zu entscheiden, ob auf den Proxy-Pfad geschaltet wird. Siehe packages/web-ui/src/tools/extract-document.ts:108-119.
  • Bedingung für streamFn-Ersetzung: AgentInterface ersetzt nur, wenn session.streamFn === streamSimple; hat der Host einen eigenen streamFn injiziert, wird er nicht überschrieben. Siehe packages/web-ui/src/components/AgentInterface.ts:138.

Zusammenfassung

proxy-utils zieht die CORS-Entscheidungen der Browser-Seite auf wenige reine Funktionen zusammen: shouldUseProxyForProvider, applyProxyIfNeeded, isCorsError, createStreamFn. Beim Verdrahten wickelt AgentInterface mit createStreamFn eine Schicht ein; jeder folgende LLM-Aufruf entscheidet dynamisch nach provider und key. Der Verdrahtungsfluss steht in AgentInterface Session-Host; die Proxy-Einstellungen, die aus dem storage gelesen werden, in AppStorage und IndexedDB.