Proxy CORS et createStreamFn
Côté navigateur, appeler l'API LLM passe inévitablement par le CORS. Le fichier proxy-utils.ts de pi-web-ui centralise trois décisions — « faut-il passer par le proxy ? », « comment réécrire baseUrl ? », « l'erreur est-elle du CORS ? » — puis les emballe via createStreamFn en une fonction compatible avec Agent.streamFn. AgentInterface la branche sur session.streamFn dans setupSessionSubscription ; ensuite, chaque appel LLM passe par cette couche.
Responsabilités
- Décider du proxy selon le provider :
shouldUseProxyForProvider(provider, apiKey)embarque une liste blanche ;zaietopenai-codexpassent toujours par le proxy, les jetons OAuth Anthropic (sk-ant-oat-*) passent par le proxy, les autres providers sont en connexion directe, voirpackages/web-ui/src/utils/proxy-utils.ts:19-51. - Réécrire baseUrl :
applyProxyIfNeededenveloppemodel.baseUrlen${proxyUrl}/?url=${encodeURIComponent(baseUrl)}, sans muter le model d'origine, voirpackages/web-ui/src/utils/proxy-utils.ts:61-82. - Reconnaître les erreurs CORS :
isCorsErrormatcheTypeError: Failed to fetch,NetworkError, ainsi que les messages contenantcors/cross-origin, voirpackages/web-ui/src/utils/proxy-utils.ts:94-118. - Emballer streamFn :
createStreamFn(getProxyUrl)retourne(model, context, options) => Promise; il décide en interne entre connexion directe et proxy, voirpackages/web-ui/src/utils/proxy-utils.ts:127-139.
Motivations de design
Pourquoi ne pas laisser l'hôte modifier le model avant chaque appel ? Parce que l'hôte ne sait pas quel provider nécessite un proxy — une clé API Anthropic classique fonctionne en direct, mais un jeton OAuth doit passer par le proxy ; OpenAI Codex passe toujours par le proxy ; Z-Ai toujours aussi. Ces règles sont écrites dans proxy-utils.ts et centralisées : ajouter un provider ne demande qu'une seule modification. createStreamFn reçoit un callback getProxyUrl plutôt que de lire directement le storage, parce que AgentInterface doit lire dynamiquement les réglages proxy.enabled et proxy.url choisis par l'utilisateur, sans les figer une fois pour toutes à l'assemblage.
Stratégie connexion directe prioritaire : quand !apiKey || !proxyUrl, on appelle directement streamSimple(model, context, options) pour éviter une copie inutile du model. Ce n'est qu'avec à la fois la clé et le proxyUrl qu'on appelle applyProxyIfNeeded pour décider d'une éventuelle réécriture — la plupart des providers retournent false à shouldUseProxyForProvider et récupèrent le model d'origine.
Fichiers clés
packages/web-ui/src/utils/proxy-utils.ts:19-51—shouldUseProxyForProvider, décision par provider et préfixe de clé.packages/web-ui/src/utils/proxy-utils.ts:61-82—applyProxyIfNeeded, retourne un nouveau model sans toucher à l'original.packages/web-ui/src/utils/proxy-utils.ts:94-118—isCorsError, matche plusieurs formes d'erreurs navigateur.packages/web-ui/src/utils/proxy-utils.ts:127-139—createStreamFn, point d'assemblage.packages/web-ui/src/components/AgentInterface.ts:138-143—AgentInterfaceappellecreateStreamFnavec une fermeture qui litproxy.urldepuisAppStorage.
La logique de createStreamFn est très courte ; le cœur, c'est un appel conditionnel à streamSimple :
// 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 énumère les stratégies par provider dans un switch, avec false par défaut :
// 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":
// ... autres providers en connexion directe ...
return false;
default:
return false;
}
}Flux de données
Le chemin de décision d'une requête LLM, de l'Agent vers streamSimple :
Limites et cas d'échec
- Proxy non configuré :
getProxyUrlretourneundefined, on prend directementstreamSimpleen connexion directe ; pas de crash si le proxy n'est pas configuré, voirpackages/web-ui/src/utils/proxy-utils.ts:132-134. - Model sans baseUrl :
applyProxyIfNeededretourne le model d'origine sans erreur ; à l'hôte de gérer, voirpackages/web-ui/src/utils/proxy-utils.ts:67-70. - Provider inconnu :
shouldUseProxyForProviderrenvoie false par défaut ; un nouveau provider fonctionne en connexion directe sans modification de code — en contrepartie, s'il a réellement besoin d'un proxy, on déclenchera une erreur CORS. - Faux positifs sur l'identification CORS :
TypeError: Failed to fetchpeut aussi provenir d'une coupure réseau ;isCorsErrorle classera à tort comme CORS.extract-document.tss'en sert pour décider de basculer vers le chemin proxy, voirpackages/web-ui/src/tools/extract-document.ts:108-119. - Condition de remplacement de streamFn :
AgentInterfacene remplace que sisession.streamFn === streamSimple; si l'hôte a déjà injecté son propre streamFn, on ne l'écrase pas, voirpackages/web-ui/src/components/AgentInterface.ts:138.
Pour résumer
proxy-utils ramène les décisions CORS côté navigateur à quelques fonctions pures : shouldUseProxyForProvider, applyProxyIfNeeded, isCorsError, createStreamFn. À l'assemblage, AgentInterface emballe le tout via createStreamFn ; ensuite, chaque appel LLM est décidé dynamiquement selon provider et clé. Pour le flux d'assemblage, voir AgentInterface, hôte de session ; pour les réglages proxy dépendant du storage, voir AppStorage et IndexedDB.