Jelajahi Sumber

fix(studio): prefer live Volcengine probe models

Ma 6 bulan lalu
induk
melakukan
3d4a4e1070

+ 1 - 1
packages/core/src/llm/providers/endpoints/volcengine.ts

@@ -13,7 +13,7 @@ export const VOLCENGINE: InkosEndpoint = {
   group: "china",
   group: "china",
   api: "openai-completions",
   api: "openai-completions",
   baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
   baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
-  checkModel: "doubao-lite-32k",
+  checkModel: "doubao-seed-2.0-lite",
   temperatureRange: [0, 2],
   temperatureRange: [0, 2],
   defaultTemperature: 0.7,
   defaultTemperature: 0.7,
   writingTemperature: 1,
   writingTemperature: 1,

+ 43 - 0
packages/studio/src/api/server.test.ts

@@ -100,6 +100,7 @@ const endpointMocks = [
     ...(id === "google" ? { checkModel: "gemini-2.5-flash" } : {}),
     ...(id === "google" ? { checkModel: "gemini-2.5-flash" } : {}),
     ...(id === "minimax" ? { checkModel: "MiniMax-M2.7" } : {}),
     ...(id === "minimax" ? { checkModel: "MiniMax-M2.7" } : {}),
     ...(id === "ollama" ? { checkModel: "llama3.2:3b" } : {}),
     ...(id === "ollama" ? { checkModel: "llama3.2:3b" } : {}),
+    ...(id === "volcengine" ? { checkModel: "doubao-lite-32k" } : {}),
     models: [
     models: [
       { id: `${id}-model`, maxOutput: 4096, contextWindowTokens: 32768, enabled: true },
       { id: `${id}-model`, maxOutput: 4096, contextWindowTokens: 32768, enabled: true },
       { id: `${id}-disabled`, maxOutput: 4096, contextWindowTokens: 32768, enabled: false },
       { id: `${id}-disabled`, maxOutput: 4096, contextWindowTokens: 32768, enabled: false },
@@ -1305,6 +1306,48 @@ describe("createStudioServer daemon lifecycle", () => {
     expect(chatCompletionMock.mock.calls.map((call) => call[1])).not.toContain("MiniMax-M2.7");
     expect(chatCompletionMock.mock.calls.map((call) => call[1])).not.toContain("MiniMax-M2.7");
   });
   });
 
 
+  it("uses discovered Volcengine models before the stale built-in check model", async () => {
+    const fetchMock = vi.fn().mockResolvedValue({
+      ok: true,
+      json: async () => ({ data: [{ id: "doubao-seed-2.0-lite" }] }),
+    });
+    vi.stubGlobal("fetch", fetchMock as typeof fetch);
+    createLLMClientMock.mockImplementation(((cfg: unknown) => cfg) as any);
+    chatCompletionMock.mockImplementation(async (_client: any, model: string) => {
+      if (model === "doubao-seed-2.0-lite") {
+        return {
+          content: "pong",
+          usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
+        };
+      }
+      throw new Error(`unexpected model: ${model}`);
+    });
+
+    const { createStudioServer } = await import("./server.js");
+    const app = createStudioServer(cloneProjectConfig() as never, root);
+
+    const response = await app.request("http://localhost/api/v1/services/volcengine/test", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        apiKey: "volc-key",
+        baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
+        apiFormat: "responses",
+        stream: true,
+      }),
+    });
+
+    expect(response.status).toBe(200);
+    await expect(response.json()).resolves.toMatchObject({
+      ok: true,
+      selectedModel: "doubao-seed-2.0-lite",
+      detected: {
+        modelsSource: "api",
+      },
+    });
+    expect(chatCompletionMock.mock.calls.map((call) => call[1])).not.toContain("doubao-lite-32k");
+  });
+
   it("uses discovered Ollama models without requiring an API key or the built-in check model", async () => {
   it("uses discovered Ollama models without requiring an API key or the built-in check model", async () => {
     await writeFile(join(root, "inkos.json"), JSON.stringify({
     await writeFile(join(root, "inkos.json"), JSON.stringify({
       ...projectConfig,
       ...projectConfig,

+ 11 - 4
packages/studio/src/api/server.ts

@@ -662,15 +662,22 @@ async function probeServiceCapabilities(args: {
     };
     };
   }
   }
   const discoveredModels = modelsResponse.models;
   const discoveredModels = modelsResponse.models;
-  // For bank services, probe with the service's own check model first — not the global default.
   const endpoint = getAllEndpoints().find((ep) => ep.id === baseService);
   const endpoint = getAllEndpoints().find((ep) => ep.id === baseService);
   const preset = resolveServicePreset(baseService);
   const preset = resolveServicePreset(baseService);
+  const discoveredFirstModel =
+    discoveredModels.find((model) => isTextChatModelId(model.id))?.id
+    ?? discoveredModels[0]?.id;
+  // Prefer live /models results; if unavailable, probe with the service's own check model before global defaults.
   const serviceFirstModel =
   const serviceFirstModel =
-    endpoint?.checkModel
+    discoveredFirstModel
+    ?? endpoint?.checkModel
     ?? preset?.knownModels?.[0]
     ?? preset?.knownModels?.[0]
     ?? endpoint?.models.find((model) => model.enabled !== false)?.id;
     ?? endpoint?.models.find((model) => model.enabled !== false)?.id;
   const useDynamicLocalModels = baseService === "ollama";
   const useDynamicLocalModels = baseService === "ollama";
-  const useEndpointCheckModel = !useDynamicLocalModels && !isCustomServiceId(args.service) && Boolean(endpoint?.checkModel);
+  const useEndpointCheckModel = !useDynamicLocalModels
+    && !isCustomServiceId(args.service)
+    && discoveredModels.length === 0
+    && Boolean(endpoint?.checkModel);
   const configService = typeof llm.service === "string" ? llm.service : undefined;
   const configService = typeof llm.service === "string" ? llm.service : undefined;
   const configModel = !useEndpointCheckModel && configService === args.service
   const configModel = !useEndpointCheckModel && configService === args.service
     ? typeof llm.defaultModel === "string"
     ? typeof llm.defaultModel === "string"
@@ -681,7 +688,7 @@ async function probeServiceCapabilities(args: {
     : undefined;
     : undefined;
   const useCustomFallbacks = isCustomServiceId(args.service);
   const useCustomFallbacks = isCustomServiceId(args.service);
   const modelCandidates = buildModelCandidates({
   const modelCandidates = buildModelCandidates({
-    preferredModel: args.preferredModel ?? (useDynamicLocalModels ? discoveredModels[0]?.id ?? serviceFirstModel : serviceFirstModel),
+    preferredModel: args.preferredModel ?? serviceFirstModel,
     configModel,
     configModel,
     envModel: useCustomFallbacks ? envModel : undefined,
     envModel: useCustomFallbacks ? envModel : undefined,
     discoveredModels: useEndpointCheckModel ? [] : discoveredModels,
     discoveredModels: useEndpointCheckModel ? [] : discoveredModels,