Explorar o código

修复 CLI 服务覆盖和 env key 兼容

fanghanjun hai 6 meses
pai
achega
ffa92f6b37

+ 39 - 0
packages/core/src/__tests__/effective-llm-config.test.ts

@@ -193,6 +193,45 @@ describe("resolveEffectiveLLMConfig", () => {
     expect(result.llm.apiKey).toBe("sk-env");
   });
 
+  it("legacy-env 模式下 CLI --service 覆盖会切换到目标 service 的 endpoint 默认值", async () => {
+    await writeProject({
+      configSource: "env",
+      provider: "custom",
+      baseUrl: "https://api.moonshot.cn/v1",
+      model: "kimi-k2.5",
+    });
+
+    const result = await resolveEffectiveLLMConfig({
+      consumer: "cli",
+      projectRoot: root,
+      envLayers: {
+        global: {
+          INKOS_LLM_PROVIDER: "custom",
+          INKOS_LLM_BASE_URL: "https://api.moonshot.cn/v1",
+          INKOS_LLM_MODEL: "kimi-k2.5",
+          INKOS_LLM_API_KEY: "sk-moon",
+        },
+        project: {},
+        process: {
+          GOOGLE_API_KEY: "sk-google",
+        },
+      },
+      cli: {
+        service: "google",
+        model: "gemini-2.5-flash",
+        apiKeyEnv: "GOOGLE_API_KEY",
+      },
+    });
+
+    expect(result.diagnostics.configMode).toBe("legacy-env");
+    expect(result.llm.service).toBe("google");
+    expect(result.llm.provider).toBe("openai");
+    expect(result.llm.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta/openai");
+    expect(result.llm.apiFormat).toBe("chat");
+    expect(result.llm.model).toBe("gemini-2.5-flash");
+    expect(result.llm.apiKey).toBe("sk-google");
+  });
+
   it("保留旧 INKOS_LLM_EXTRA_* 和 INKOS_DEFAULT_LANGUAGE 行为", async () => {
     await writeProject({
       configSource: "env",

+ 31 - 0
packages/core/src/__tests__/llm-env.test.ts

@@ -0,0 +1,31 @@
+import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+import { loadLLMEnvLayers } from "../utils/llm-env.js";
+
+describe("loadLLMEnvLayers", () => {
+  let root = "";
+  const previousGoogleKey = process.env.GOOGLE_API_KEY;
+
+  afterEach(async () => {
+    if (previousGoogleKey === undefined) delete process.env.GOOGLE_API_KEY;
+    else process.env.GOOGLE_API_KEY = previousGoogleKey;
+
+    if (root) {
+      await rm(root, { recursive: true, force: true });
+      root = "";
+    }
+  });
+
+  it("exposes project .env variables through process.env for apiKeyEnv consumers", async () => {
+    root = await mkdtemp(join(tmpdir(), "inkos-llm-env-"));
+    delete process.env.GOOGLE_API_KEY;
+    await writeFile(join(root, ".env"), "GOOGLE_API_KEY=sk-from-project-env\n", "utf-8");
+
+    const layers = await loadLLMEnvLayers(root);
+
+    expect(layers.project.GOOGLE_API_KEY).toBe("sk-from-project-env");
+    expect(process.env.GOOGLE_API_KEY).toBe("sk-from-project-env");
+  });
+});

+ 10 - 1
packages/core/src/utils/effective-llm-config.ts

@@ -276,10 +276,19 @@ async function applyLegacyEnvConfig(
   }
 
   if (input.cli?.service) {
-    llm.service = input.cli.service;
+    const entry = synthesizeServiceEntry(input.cli.service);
+    if (entry) {
+      applyServiceEntry(llm, entry);
+      if (!input.cli.model) {
+        llm.model = resolveServiceModel(entry, undefined, stringValue(llm.defaultModel));
+      }
+    } else {
+      llm.service = input.cli.service;
+    }
     diagnostics.serviceSource = "cli";
   }
   if (input.cli?.model) {
+    assertModelBelongsToService(synthesizeServiceEntry(stringValue(llm.service)), input.cli.model);
     llm.model = input.cli.model;
     diagnostics.modelSource = "cli";
   }

+ 20 - 2
packages/core/src/utils/llm-env.ts

@@ -18,9 +18,14 @@ export async function loadLLMEnvLayers(
   root: string,
   processEnv: NodeJS.ProcessEnv = process.env,
 ): Promise<LLMEnvLayers> {
+  const global = await parseEnvFile(GLOBAL_ENV_PATH);
+  const project = await parseEnvFile(join(root, ".env"));
+  // Compatibility: modelOverrides.apiKeyEnv and detector config still read process.env directly.
+  hydrateProcessEnvFromEnvFiles(processEnv, global, project);
+
   return {
-    global: await parseEnvFile(GLOBAL_ENV_PATH),
-    project: await parseEnvFile(join(root, ".env")),
+    global,
+    project,
     process: { ...processEnv },
   };
 }
@@ -54,3 +59,16 @@ async function parseEnvFile(path: string): Promise<LLMEnvMap> {
     return {};
   }
 }
+
+function hydrateProcessEnvFromEnvFiles(
+  processEnv: NodeJS.ProcessEnv,
+  global: LLMEnvMap,
+  project: LLMEnvMap,
+): void {
+  const fileEnv = mergeEnvMaps(global, project);
+  for (const [key, value] of Object.entries(fileEnv)) {
+    if (value !== undefined && processEnv[key] === undefined) {
+      processEnv[key] = value;
+    }
+  }
+}