Files
CC-Switch/src/components/providers/forms/GeminiFormFields.tsx
T
Jason 3a15420770 Update default models and pricing across presets
Bump default model names project-wide: gpt-5.4 -> gpt-5.5,
gemini-3.x -> gemini-3.5-flash, glm-5 -> glm-5.1, and
grok-code-fast-1 -> grok-build-0.1 across all provider presets
(claude, codex, gemini, hermes, openclaw, opencode, universal),
Gemini config, and stream check defaults.

Pricing:
- Seed gemini-3.5-flash, gemini-3.1-flash-lite, step-3.5-flash-2603,
  doubao-seed-2.0-code, mimo-v2.5(/pro), qwen3-coder-480b, grok-build-0.1.
- Correct deepseek-v4-flash/pro, glm-5/5.1, grok pricing.
- Add repair_current_model_pricing: idempotent pass that fixes only
  rows still equal to the outdated built-in values, preserving any
  user-customized prices (seed uses INSERT OR IGNORE and cannot update
  existing rows).

Fixes from review:
- opencode: drop duplicate gemini-3.5-flash variant (unreachable via
  .find), keep the entry with the full minimal/low/medium/high set.
- Align stale display names/costs to gemini-3.5-flash (hermes, openclaw,
  opencode); openclaw cost -> {1.5, 9, 0.15} to match seed.
- i18n (zh/en/ja/zh-TW): refresh OMO category tooltips for new model
  names; fix writing tooltip to Kimi K2.5 to match its recommended.

Update tests accordingly and add a regression test asserting unique
model ids in the Google opencode preset variants.
2026-05-29 22:52:25 +08:00

213 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form";
import { Download, Info, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared";
import {
fetchModelsForConfig,
showFetchModelsError,
type FetchedModel,
} from "@/lib/api/model-fetch";
import type { ProviderCategory } from "@/types";
interface EndpointCandidate {
url: string;
}
interface GeminiFormFieldsProps {
providerId?: string;
// API Key
shouldShowApiKey: boolean;
apiKey: string;
onApiKeyChange: (key: string) => void;
category?: ProviderCategory;
shouldShowApiKeyLink: boolean;
websiteUrl: string;
isPartner?: boolean;
partnerPromotionKey?: string;
// Base URL
shouldShowSpeedTest: boolean;
baseUrl: string;
onBaseUrlChange: (url: string) => void;
isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model
shouldShowModelField: boolean;
model: string;
onModelChange: (value: string) => void;
// Speed Test Endpoints
speedTestEndpoints: EndpointCandidate[];
}
export function GeminiFormFields({
providerId,
shouldShowApiKey,
apiKey,
onApiKeyChange,
category,
shouldShowApiKeyLink,
websiteUrl,
isPartner,
partnerPromotionKey,
shouldShowSpeedTest,
baseUrl,
onBaseUrlChange,
isEndpointModalOpen,
onEndpointModalToggle,
onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelField,
model,
onModelChange,
speedTestEndpoints,
}: GeminiFormFieldsProps) {
const { t } = useTranslation();
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const handleFetchModels = useCallback(() => {
if (!baseUrl || !apiKey) {
showFetchModelsError(null, t, {
hasApiKey: !!apiKey,
hasBaseUrl: !!baseUrl,
});
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
showFetchModelsError(err, t);
})
.finally(() => setIsFetchingModels(false));
}, [baseUrl, apiKey, t]);
// 检测是否为 Google 官方(使用 OAuth
const isGoogleOfficial =
partnerPromotionKey?.toLowerCase() === "google-official";
return (
<>
{/* Google OAuth 提示 */}
{isGoogleOfficial && (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950">
<div className="flex gap-3">
<Info className="h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400" />
<div className="space-y-1">
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
{t("provider.form.gemini.oauthTitle", {
defaultValue: "OAuth 认证模式",
})}
</p>
<p className="text-sm text-blue-700 dark:text-blue-300">
{t("provider.form.gemini.oauthHint", {
defaultValue:
"Google 官方使用 OAuth 个人认证,无需填写 API Key。首次使用时会自动打开浏览器进行登录。",
})}
</p>
</div>
</div>
</div>
)}
{/* API Key 输入框 */}
{shouldShowApiKey && !isGoogleOfficial && (
<ApiKeySection
value={apiKey}
onChange={onApiKeyChange}
category={category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
/>
)}
{/* Base URL 输入框(统一使用与 Codex 相同的样式与交互) */}
{shouldShowSpeedTest && (
<EndpointField
id="baseUrl"
label={t("providerForm.apiEndpoint", { defaultValue: "API 端点" })}
value={baseUrl}
onChange={onBaseUrlChange}
placeholder={t("providerForm.apiEndpointPlaceholder", {
defaultValue: "https://your-api-endpoint.com/",
})}
onManageClick={() => onEndpointModalToggle(true)}
/>
)}
{/* Model 输入框 */}
{shouldShowModelField && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel htmlFor="gemini-model">
{t("provider.form.gemini.model", { defaultValue: "模型" })}
</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
</div>
<ModelInputWithFetch
id="gemini-model"
value={model}
onChange={onModelChange}
placeholder="gemini-3.5-flash"
fetchedModels={fetchedModels}
isLoading={isFetchingModels}
/>
</div>
)}
{/* 端点测速弹窗 */}
{shouldShowSpeedTest && isEndpointModalOpen && (
<EndpointSpeedTest
appId="gemini"
providerId={providerId}
value={baseUrl}
onChange={onBaseUrlChange}
initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange}
/>
)}
</>
);
}