Skip to content

CORS 代理与 createStreamFn

源码版本v0.73.1

浏览器侧调 LLM API,绕不开 CORS。pi-web-uiproxy-utils.ts 把「要不要走代理」「怎么改 baseUrl」「错误是不是 CORS」三个判断集中起来,再通过 createStreamFn 包装成 Agent.streamFn 兼容的函数。AgentInterfacesetupSessionSubscription 时把它挂到 session.streamFn,之后每次 LLM 调用都走这层。

职责

  1. 按 provider 决定走代理:shouldUseProxyForProvider(provider, apiKey) 内置一张白名单,zaiopenai-codex 强制走代理,Anthropic OAuth token(sk-ant-oat-*)走代理,其余 provider 直连,见 packages/web-ui/src/utils/proxy-utils.ts:19-51
  2. 改写 baseUrl:applyProxyIfNeededmodel.baseUrl 包成 ${proxyUrl}/?url=${encodeURIComponent(baseUrl)},不改原 model,见 packages/web-ui/src/utils/proxy-utils.ts:61-82
  3. 识别 CORS 错误:isCorsError 匹配 TypeError: Failed to fetchNetworkError、消息含 cors/cross-origin,见 packages/web-ui/src/utils/proxy-utils.ts:94-118
  4. 包装 streamFn:createStreamFn(getProxyUrl) 返回 (model, context, options) => Promise,内部决定直连还是代理,见 packages/web-ui/src/utils/proxy-utils.ts:127-139

设计动机

为什么不在每次调用前让 host 自己改 model?因为 host 不知道哪个 provider 需要代理——Anthropic 的普通 API key 直连就行,但 OAuth token 必须走代理;OpenAI Codex 永远走代理;Z-AI 永远走代理。这些规则写在 proxy-utils.ts 里集中维护,新增 provider 只需要改一处。createStreamFn 接收一个 getProxyUrl 回调而不是直接读 storage,是因为 AgentInterface 需要动态读取用户设置的 proxy.enabledproxy.url,不能在装配时一次性读死。

直连优先策略:!apiKey || !proxyUrl 时直接 streamSimple(model, context, options),避免无谓的 model 对象复制。只有同时有 key 和 proxyUrl 时才调 applyProxyIfNeeded 判断是否真改写——大多数 provider 会因 shouldUseProxyForProvider 返回 false 而拿到原 model。

关键文件

createStreamFn 的全部逻辑很短,核心是按情况调 streamSimple:

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 用 switch 罗列各 provider 的策略,默认 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":
        // ... 其他直连 provider ...
            return false;
        default:
            return false;
    }
}

数据流

LLM 请求从 AgentstreamSimple 的决策路径:

边界与失败

小结

proxy-utils 把浏览器侧的 CORS 决策收敛到几个纯函数:shouldUseProxyForProviderapplyProxyIfNeededisCorsErrorcreateStreamFnAgentInterface 装配时用 createStreamFn 包一层,后续每次 LLM 调用都按 provider 与 key 动态决策。装配流程看 AgentInterface 会话宿主,依赖 storage 的 proxy 设置看 AppStorage 与 IndexedDB