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