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
@@ -1792,10 +1792,28 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/pi/upstreams/{id}/models",
|
||||
scope: "admin",
|
||||
summary: "上游可用模型(静态目录;Radius 先动态刷新)",
|
||||
summary: "上游模型列表:已获取过远程列表时返回远程与静态目录的合并(远程优先),否则静态目录",
|
||||
params: IdParam,
|
||||
response: pi.PiUpstreamModels,
|
||||
}),
|
||||
modelsRefresh: ep({
|
||||
method: "post",
|
||||
path: "/pi/upstreams/{id}/models/refresh",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "获取模型列表:用该上游的凭证调 provider 的模型列表接口并存库;失败或不支持时回静态目录并给出原因",
|
||||
params: IdParam,
|
||||
response: pi.PiUpstreamModels,
|
||||
}),
|
||||
modelsProbe: ep({
|
||||
method: "post",
|
||||
path: "/pi/models/probe",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "新建上游前试拉模型列表(凭证只用于这一次请求,不落库)",
|
||||
body: pi.PiModelsProbeRequest,
|
||||
response: pi.PiUpstreamModels,
|
||||
}),
|
||||
oauthStart: ep({
|
||||
method: "post",
|
||||
path: "/pi/upstreams/{id}/oauth/start",
|
||||
|
||||
@@ -299,15 +299,57 @@ export const PiUpstreamTestResult = z.object({
|
||||
});
|
||||
export type PiUpstreamTestResult = z.infer<typeof PiUpstreamTestResult>;
|
||||
|
||||
/** 模型列表来源:remote = 从上游的模型列表接口拉取(已存库);static = 随包静态目录(没拉过、拉取失败或 provider 没有列表接口) */
|
||||
export const PiModelListSource = z.enum(["remote", "static"]);
|
||||
export type PiModelListSource = z.infer<typeof PiModelListSource>;
|
||||
|
||||
/** 列表里的一个模型(字段按上游接口能给的归一化;拿不到为 null) */
|
||||
export const PiListedModel = z.object({
|
||||
id: z.string().min(1).max(300),
|
||||
displayName: z.string().max(300).nullable(),
|
||||
contextWindow: z.number().int().nonnegative().nullable(),
|
||||
/** 支持图片输入 */
|
||||
supportsVision: z.boolean().nullable(),
|
||||
/** 支持工具调用 */
|
||||
supportsTools: z.boolean().nullable(),
|
||||
/** 价格(美元 / 百万 token) */
|
||||
pricing: z.object({ input: z.number().nullable(), output: z.number().nullable() }).nullable(),
|
||||
/** 这一条来自哪里:remote = 上游列表;static = 只在静态目录里 */
|
||||
origin: PiModelListSource,
|
||||
});
|
||||
export type PiListedModel = z.infer<typeof PiListedModel>;
|
||||
|
||||
/** 获取失败的分类:unsupported = provider 没有模型列表接口 */
|
||||
export const PiModelFetchErrorKind = z.enum(["auth", "network", "unsupported", "config", "other"]);
|
||||
export type PiModelFetchErrorKind = z.infer<typeof PiModelFetchErrorKind>;
|
||||
|
||||
/**
|
||||
* GET /pi/upstreams/{id}/models、POST …/models/refresh、POST /pi/models/probe 的响应。
|
||||
* models = 远程列表与静态目录合并去重(同 id 远程优先,远程缺的字段用静态目录补),远程在前。
|
||||
*/
|
||||
export const PiUpstreamModels = z.object({
|
||||
items: z.array(PiModelInfo),
|
||||
/** 动态模型表是否已从上游刷新成功(静态 provider 恒为 false) */
|
||||
refreshed: z.boolean(),
|
||||
/** 动态刷新失败原因(仍返回静态表) */
|
||||
refreshError: z.string().nullable(),
|
||||
source: PiModelListSource,
|
||||
/** 远程列表拉取时间;static 为 null */
|
||||
fetchedAt: Timestamp.nullable(),
|
||||
models: z.array(PiListedModel),
|
||||
/** 该 provider 有模型列表接口(没有时「获取模型列表」直接回静态表) */
|
||||
remoteSupported: z.boolean(),
|
||||
/** 本次获取失败的原因(一句话,已脱敏);成功或只是读取时为 null */
|
||||
error: z.string().nullable(),
|
||||
errorKind: PiModelFetchErrorKind.nullable(),
|
||||
});
|
||||
export type PiUpstreamModels = z.infer<typeof PiUpstreamModels>;
|
||||
|
||||
/** 新建上游前试拉模型列表(不落库;凭证只用于这一次请求) */
|
||||
export const PiModelsProbeRequest = z.object({
|
||||
provider: PiProviderId,
|
||||
authKind: PiAuthKind,
|
||||
apiKey: ApiKeyInput.optional(),
|
||||
baseUrl: z.url().nullable().optional(),
|
||||
extra: PiUpstreamExtra.optional(),
|
||||
});
|
||||
export type PiModelsProbeRequest = z.infer<typeof PiModelsProbeRequest>;
|
||||
|
||||
// ── 订阅登录状态机 ──
|
||||
|
||||
export const PiOAuthStartRequest = z.object({
|
||||
|
||||
Reference in new issue
Block a user