|
@@ -0,0 +1,95 @@
|
|
|
|
|
+import { getProvider } from "./index.js";
|
|
|
|
|
+import { resolveServiceModelsBaseUrl } from "../service-presets.js";
|
|
|
|
|
+
|
|
|
|
|
+export interface VerifyResult {
|
|
|
|
|
+ readonly probe: {
|
|
|
|
|
+ readonly ok: boolean;
|
|
|
|
|
+ readonly models: number;
|
|
|
|
|
+ readonly error?: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ readonly chat: {
|
|
|
|
|
+ readonly ok: boolean;
|
|
|
|
|
+ readonly latencyMs?: number;
|
|
|
|
|
+ readonly error?: string;
|
|
|
|
|
+ } | null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Step 1: 用 apikey 调 /models endpoint 探测。
|
|
|
|
|
+ * 返回 models 条数 + ok 标志;失败原因落到 error 字段。
|
|
|
|
|
+ */
|
|
|
|
|
+async function probe(
|
|
|
|
|
+ service: string,
|
|
|
|
|
+ apiKey: string,
|
|
|
|
|
+ baseUrl?: string,
|
|
|
|
|
+): Promise<VerifyResult["probe"]> {
|
|
|
|
|
+ const provider = getProvider(service);
|
|
|
|
|
+ const probeBaseUrl = baseUrl || provider?.modelsBaseUrl || provider?.baseUrl || resolveServiceModelsBaseUrl(service);
|
|
|
|
|
+ if (!probeBaseUrl) {
|
|
|
|
|
+ return { ok: false, models: 0, error: "无 baseUrl 可探测(custom / newapi / higress 需要用户填)" };
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const url = probeBaseUrl.replace(/\/$/, "") + "/models";
|
|
|
|
|
+ const res = await fetch(url, {
|
|
|
|
|
+ headers: { Authorization: `Bearer ${apiKey}` },
|
|
|
|
|
+ signal: AbortSignal.timeout(10_000),
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ return { ok: false, models: 0, error: `HTTP ${res.status} ${res.statusText}` };
|
|
|
|
|
+ }
|
|
|
|
|
+ const json = (await res.json()) as { data?: Array<{ id: string }> };
|
|
|
|
|
+ const count = json.data?.length ?? 0;
|
|
|
|
|
+ return { ok: count > 0, models: count };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ok: false,
|
|
|
|
|
+ models: 0,
|
|
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * verifyService: 两步验证。
|
|
|
|
|
+ * Step 1 probe /models。
|
|
|
|
|
+ * Step 2 chat hello 到 checkModel(命中 provider.checkModel 才做;custom / newapi / higress 没有 checkModel,chat 字段返回 null)。
|
|
|
|
|
+ */
|
|
|
|
|
+export async function verifyService(
|
|
|
|
|
+ service: string,
|
|
|
|
|
+ apiKey: string,
|
|
|
|
|
+ opts?: { checkModel?: string; baseUrl?: string },
|
|
|
|
|
+): Promise<VerifyResult> {
|
|
|
|
|
+ const probeResult = await probe(service, apiKey, opts?.baseUrl);
|
|
|
|
|
+
|
|
|
|
|
+ const provider = getProvider(service);
|
|
|
|
|
+ const checkModel = opts?.checkModel ?? provider?.checkModel;
|
|
|
|
|
+ if (!checkModel) {
|
|
|
|
|
+ return { probe: probeResult, chat: null };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const start = Date.now();
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { createLLMClient, chatCompletion } = await import("../provider.js");
|
|
|
|
|
+ const { LLMConfigSchema } = await import("../../models/project.js");
|
|
|
|
|
+ const client = createLLMClient(LLMConfigSchema.parse({
|
|
|
|
|
+ provider: provider?.api === "anthropic-messages" ? "anthropic" : "openai",
|
|
|
|
|
+ service,
|
|
|
|
|
+ model: checkModel,
|
|
|
|
|
+ apiKey,
|
|
|
|
|
+ baseUrl: opts?.baseUrl ?? provider?.baseUrl ?? "",
|
|
|
|
|
+ configSource: "studio",
|
|
|
|
|
+ stream: false,
|
|
|
|
|
+ }));
|
|
|
|
|
+ await chatCompletion(client, checkModel, [{ role: "user", content: "hi" }], { maxTokens: 10 });
|
|
|
|
|
+ return { probe: probeResult, chat: { ok: true, latencyMs: Date.now() - start } };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ probe: probeResult,
|
|
|
|
|
+ chat: {
|
|
|
|
|
+ ok: false,
|
|
|
|
|
+ latencyMs: Date.now() - start,
|
|
|
|
|
+ error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|