Files
CC-Switch/src/lib/api/model-fetch.ts
T
Jason 73bc4eb65d feat(codex-oauth): fetch model list from ChatGPT backend on demand
- Add `get_codex_oauth_models` Tauri command reusing the managed OAuth
  access token to hit `chatgpt.com/backend-api/codex/models`; HTTP and
  multi-shape JSON parsing live in `services::codex_oauth_models` so the
  command stays thin.
- Unify the Claude form's "fetch models" button across normal / Copilot /
  Codex OAuth presets, drop the auto-load effect for Copilot in favor of
  explicit clicks, and guard against stale responses with a requestId ref.
- Add Vitest coverage for both Copilot and Codex OAuth paths asserting no
  request on mount and the correct account id on click; add Rust unit
  tests for the four model-list payload shapes.
2026-05-14 15:03:31 +08:00

93 lines
2.5 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
import type { TFunction } from "i18next";
import { toast } from "sonner";
export interface FetchedModel {
id: string;
ownedBy: string | null;
}
/**
* 从供应商获取可用模型列表
*
* 使用 OpenAI 兼容的 GET /v1/models 端点。优先用 `modelsUrl` 精确覆写;
* 否则后端会对 baseURL 生成候选列表并按序尝试(含"剥离 /anthropic 等兼容子路径"兜底)。
*/
export async function fetchModelsForConfig(
baseUrl: string,
apiKey: string,
isFullUrl?: boolean,
modelsUrl?: string,
): Promise<FetchedModel[]> {
return invoke("fetch_models_for_config", {
baseUrl,
apiKey,
isFullUrl,
modelsUrl,
});
}
/**
* 获取 Codex OAuth (ChatGPT Plus/Pro 反代) 可用模型列表
*
* Codex OAuth 使用 ChatGPT 的 backend-api/codex 端点,不兼容普通 /v1/models。
*/
export async function fetchCodexOauthModels(
accountId?: string | null,
): Promise<FetchedModel[]> {
return invoke("get_codex_oauth_models", {
accountId: accountId || null,
});
}
/**
* 根据错误类型显示对应的 toast 提示
*/
export function showFetchModelsError(
err: unknown,
t: TFunction,
opts?: { hasApiKey: boolean; hasBaseUrl: boolean },
): void {
// 前端预检:缺少必填字段
if (opts && !opts.hasBaseUrl && !opts.hasApiKey) {
toast.error(t("providerForm.fetchModelsNeedConfig"));
return;
}
if (opts && !opts.hasApiKey) {
toast.error(t("providerForm.fetchModelsNeedApiKey"));
return;
}
if (opts && !opts.hasBaseUrl) {
toast.error(t("providerForm.fetchModelsNeedEndpoint"));
return;
}
// 解析后端错误字符串
const msg = String(err);
if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) {
toast.error(t("providerForm.fetchModelsAuthFailed"));
return;
}
// 所有候选端点均返回 404/405:供应商可能未开放 /models 接口,或 Base URL 有误
if (msg.includes("All candidates failed")) {
toast.error(t("providerForm.fetchModelsEndpointNotFound"));
return;
}
if (msg.includes("HTTP 404") || msg.includes("HTTP 405")) {
toast.error(t("providerForm.fetchModelsEndpointNotFound"));
return;
}
if (msg.includes("timeout") || msg.includes("timed out")) {
toast.error(t("providerForm.fetchModelsTimeout"));
return;
}
if (msg.includes("Failed to parse")) {
toast.error(t("providerForm.fetchModelsNotSupported"));
return;
}
// 通用兜底
toast.error(t("providerForm.fetchModelsFailed"));
}