CORS-Proxy und createStreamFn
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
- Proxy-Entscheidung nach provider:
shouldUseProxyForProvider(provider, apiKey)hat eine eingebaute Whitelist:zaiundopenai-codexgehen immer über den Proxy; Anthropic OAuth-Token (sk-ant-oat-*) über den Proxy, die restlichen Provider direkt. Siehepackages/web-ui/src/utils/proxy-utils.ts:19-51. - baseUrl umschreiben:
applyProxyIfNeededwickeltmodel.baseUrlin${proxyUrl}/?url=${encodeURIComponent(baseUrl)}, ohne das ursprüngliche model anzufassen. Siehepackages/web-ui/src/utils/proxy-utils.ts:61-82. - CORS-Fehler erkennen:
isCorsErrormatchtTypeError: Failed to fetch,NetworkErrorsowie Meldungen mitcors/cross-origin. Siehepackages/web-ui/src/utils/proxy-utils.ts:94-118. - streamFn einwickeln:
createStreamFn(getProxyUrl)liefert(model, context, options) => Promiseund entscheidet intern zwischen Direktverbindung und Proxy. Siehepackages/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
packages/web-ui/src/utils/proxy-utils.ts:19-51—shouldUseProxyForProvider, entschieden nach provider und key-Präfix.packages/web-ui/src/utils/proxy-utils.ts:61-82—applyProxyIfNeeded, liefert ein neues model; das originale bleibt unangetastet.packages/web-ui/src/utils/proxy-utils.ts:94-118—isCorsError, matcht verschiedene Browser-Fehlermeldungen.packages/web-ui/src/utils/proxy-utils.ts:127-139—createStreamFn, Verdrahtungspunkt.packages/web-ui/src/components/AgentInterface.ts:138-143—AgentInterfaceruftcreateStreamFnauf und gibt eine Closure mit, die proxy.url ausAppStorageliest.
Die gesamte Logik von createStreamFn ist kurz; im Kern wird nach Fall streamSimple aufgerufen:
// 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:
// 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:
getProxyUrlliefertundefined, es geht direkt perstreamSimpleweiter; die Anwendung crasht nicht, weil kein Proxy konfiguriert ist. Siehepackages/web-ui/src/utils/proxy-utils.ts:132-134. - model ohne baseUrl:
applyProxyIfNeededliefert das Original-model zurück, wirft nicht und überlässt es dem Host. Siehepackages/web-ui/src/utils/proxy-utils.ts:67-70. - Unbekannter provider:
shouldUseProxyForProviderist 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 fetchkann auch ein Netz weg sein;isCorsErrorwertet es als CORS.extract-document.tsnutzt das, um zu entscheiden, ob auf den Proxy-Pfad geschaltet wird. Siehepackages/web-ui/src/tools/extract-document.ts:108-119. - Bedingung für streamFn-Ersetzung:
AgentInterfaceersetzt nur, wennsession.streamFn === streamSimple; hat der Host einen eigenen streamFn injiziert, wird er nicht überschrieben. Siehepackages/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.