feat(pi): fetch and refresh upstream model lists, searchable model picker
- POST /pi/upstreams/{id}/models/refresh calls the provider's model list
endpoint (OpenAI-shaped /models, Anthropic /v1/models, Google
/v1beta/models, OpenRouter /api/v1/models; Radius via pi-ai refresh),
normalizes {id, displayName, contextWindow, supportsVision,
supportsTools, pricing} and stores it in pi_upstream.models (0044)
- GET .../models returns {source, fetchedAt, models[]}: remote merged
with the static catalog (remote first); failures (401/network/
unsupported) fall back to static with a one-line error
- POST /pi/models/probe for trying credentials before creating
- wizard and edit drawer: fetch button, searchable dropdown with
vision/tool tags, manual id, source line; default upstream needs a model
- tests: mock /models, 401, network failure; smoke step; runbook
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e5abfa6217
commit
0765264a47
16 files changed
+1178
-52
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
usePiProviders,
|
||||
usePiUpstreamModels,
|
||||
usePiUpstreams,
|
||||
useRefreshPiModels,
|
||||
useRemovePiUpstream,
|
||||
useUpdatePiUpstream,
|
||||
} from "~/api/pi.ts";
|
||||
@@ -22,6 +23,7 @@ import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { PiOAuthLogin } from "./pi-oauth.tsx";
|
||||
import {
|
||||
AUTH_LABEL,
|
||||
ModelFetchBar,
|
||||
ModelInput,
|
||||
providerName,
|
||||
THINKING_LABEL,
|
||||
@@ -113,6 +115,7 @@ function UpstreamDrawer({
|
||||
const remove = useRemovePiUpstream();
|
||||
const logout = useLogoutPiOAuth();
|
||||
const models = usePiUpstreamModels(upstream?.id ?? null);
|
||||
const refresh = useRefreshPiModels();
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [v, setV] = useState(() => ({
|
||||
name: upstream?.name ?? "",
|
||||
@@ -139,6 +142,16 @@ function UpstreamDrawer({
|
||||
v.enabled !== upstream.enabled ||
|
||||
envFields.some((f) => (v.env[f.name] ?? "") !== (upstream.extra.env?.[f.name] ?? ""));
|
||||
|
||||
// 获取用已保存的配置:凭证或端点改了没保存时先保存
|
||||
const fetchBlocked =
|
||||
upstream.authKind !== "none" && !custom && !upstream.hasCredential
|
||||
? upstream.authKind === "oauth"
|
||||
? "登录后可获取"
|
||||
: "先配置 API Key"
|
||||
: v.apiKey.trim() || v.baseUrl !== (upstream.baseUrl ?? "")
|
||||
? "先保存 API Key / Base URL 再获取"
|
||||
: null;
|
||||
|
||||
const save = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const env = Object.fromEntries(Object.entries(v.env).filter(([, x]) => x.trim()));
|
||||
@@ -166,8 +179,16 @@ function UpstreamDrawer({
|
||||
};
|
||||
|
||||
const makeDefault = async () => {
|
||||
const model = v.model.trim();
|
||||
if (!model) {
|
||||
setErrors((e) => ({ ...e, defaultModel: "设为默认上游前先选默认模型" }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await update.mutateAsync({ id: upstream.id, body: { makeDefault: true } });
|
||||
await update.mutateAsync({
|
||||
id: upstream.id,
|
||||
body: { makeDefault: true, ...(model !== (upstream.defaultModel ?? "") ? { defaultModel: model } : {}) },
|
||||
});
|
||||
toast.success(`${upstream.name}已设为默认上游`);
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "设置失败"));
|
||||
@@ -294,15 +315,27 @@ function UpstreamDrawer({
|
||||
label="默认模型"
|
||||
htmlFor="pu-model"
|
||||
error={errors.defaultModel}
|
||||
hint={models.data?.refreshError ? `模型列表刷新失败:${models.data.refreshError}` : "可从列表选,也可手填"}
|
||||
hint="可从列表选,也可手填"
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<ModelFetchBar
|
||||
data={models.data}
|
||||
pending={refresh.isPending}
|
||||
onFetch={() =>
|
||||
refresh.mutate(upstream.id, { onError: (err) => toast.error(errorMessage(err, "获取失败")) })
|
||||
}
|
||||
disabledReason={fetchBlocked}
|
||||
/>
|
||||
<ModelInput
|
||||
id="pu-model"
|
||||
value={v.model}
|
||||
onChange={(x) => setV((s) => ({ ...s, model: x }))}
|
||||
models={models.data?.items ?? []}
|
||||
onChange={(x) => {
|
||||
setV((s) => ({ ...s, model: x }));
|
||||
setErrors(({ defaultModel: _, ...rest }) => rest);
|
||||
}}
|
||||
models={models.data?.models ?? []}
|
||||
disabled={pending}
|
||||
invalid={!!errors.defaultModel}
|
||||
/>
|
||||
</FormField>
|
||||
{showThinking ? (
|
||||
|
||||
Reference in new issue
Block a user