Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/extensions/lmstudio/src/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 36 kB image not shown  

Quelle  setup.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { CUSTOM_LOCAL_AUTH_MARKER } from "openclaw/plugin-sdk/provider-auth";
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard";
import {
  SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
  type ProviderAuthMethodNonInteractiveContext,
  type ProviderCatalogContext,
} from "openclaw/plugin-sdk/provider-setup";
import type { WizardPrompter } from "openclaw/plugin-sdk/setup";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
  LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
  LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
} from "./defaults.js";
import {
  configureLmstudioNonInteractive,
  discoverLmstudioProvider,
  promptAndConfigureLmstudioInteractive,
} from "./setup.js";

const fetchLmstudioModelsMock = vi.hoisted(() => vi.fn());
const discoverLmstudioModelsMock = vi.hoisted(() => vi.fn());
const configureSelfHostedNonInteractiveMock = vi.hoisted(() => vi.fn());
const removeProviderAuthProfilesWithLockMock = vi.hoisted(() => vi.fn());

vi.mock("./models.fetch.js", () => ({
  fetchLmstudioModels: (...args: unknown[]) => fetchLmstudioModelsMock(...args),
  discoverLmstudioModels: (...args: unknown[]) => discoverLmstudioModelsMock(...args),
  ensureLmstudioModelLoaded: vi.fn(),
}));

vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
  const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
  return {
    ...actual,
    removeProviderAuthProfilesWithLock: (...args: unknown[]) =>
      removeProviderAuthProfilesWithLockMock(...args),
  };
});

vi.mock("openclaw/plugin-sdk/provider-setup", async (importOriginal) => {
  const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-setup")>();
  return {
    ...actual,
    configureOpenAICompatibleSelfHostedProviderNonInteractive: (...args: unknown[]) =>
      configureSelfHostedNonInteractiveMock(...args),
  };
});

function createModel(id: string, name = id): ModelDefinitionConfig {
  return {
    id,
    name,
    reasoning: false,
    input: ["text"],
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    contextWindow: 8192,
    maxTokens: 8192,
  };
}

function buildConfig(): OpenClawConfig {
  return {
    models: {
      providers: {
        lmstudio: {
          baseUrl: "http://localhost:1234/v1",
          apiKey: "LM_API_TOKEN",
          api: "openai-completions",
          models: [],
        },
      },
    },
  };
}

function buildDiscoveryContext(params?: {
  config?: OpenClawConfig;
  apiKey?: string;
  discoveryApiKey?: string;
  env?: NodeJS.ProcessEnv;
}): ProviderCatalogContext {
  return {
    config: params?.config ?? ({} as OpenClawConfig),
    env: params?.env ?? {},
    resolveProviderApiKey: () => ({
      apiKey: params?.apiKey,
      discoveryApiKey: params?.discoveryApiKey,
    }),
    resolveProviderAuth: () => ({
      apiKey: params?.apiKey,
      discoveryApiKey: params?.discoveryApiKey,
      mode: "none" as const,
      source: "none" as const,
    }),
  };
}

function buildNonInteractiveContext(params?: {
  config?: OpenClawConfig;
  customBaseUrl?: string;
  customApiKey?: string;
  lmstudioApiKey?: string;
  customModelId?: string;
  resolvedApiKey?: string | null;
  resolvedApiKeySource?: "flag" | "env" | "profile";
}): ProviderAuthMethodNonInteractiveContext & {
  runtime: {
    error: ReturnType<typeof vi.fn>;
    exit: ReturnType<typeof vi.fn>;
    log: ReturnType<typeof vi.fn>;
  };
  resolveApiKey: ReturnType<typeof vi.fn>;
  toApiKeyCredential: ReturnType<typeof vi.fn>;
} {
  const error = vi.fn<(...args: unknown[]) => void>();
  const exit = vi.fn<(code: number) => void>();
  const log = vi.fn<(...args: unknown[]) => void>();
  const resolveApiKey = vi.fn(async () =>
    params?.resolvedApiKey === null
      ? null
      : {
          key: params?.resolvedApiKey ?? "lmstudio-test-key",
          source: params?.resolvedApiKeySource ?? "flag",
        },
  );
  const toApiKeyCredential = vi.fn();
  return {
    authChoice: "lmstudio",
    config: params?.config ?? buildConfig(),
    baseConfig: params?.config ?? buildConfig(),
    opts: {
      customBaseUrl: params?.customBaseUrl,
      customApiKey: params?.customApiKey ?? "lmstudio-test-key",
      lmstudioApiKey: params?.lmstudioApiKey,
      customModelId: params?.customModelId,
    } as ProviderAuthMethodNonInteractiveContext["opts"],
    runtime: { error, exit, log },
    resolveApiKey,
    toApiKeyCredential,
  };
}

function createQueuedWizardPrompterHarness(textValues: string[]): {
  prompter: WizardPrompter;
  note: ReturnType<typeof vi.fn>;
  text: ReturnType<typeof vi.fn>;
} {
  const queue = [...textValues];
  const note = vi.fn(async (_message: string, _title?: string) => {});
  const text = vi.fn(async () => queue.shift() ?? "");
  const prompter: WizardPrompter = {
    intro: async () => {},
    outro: async () => {},
    note,
    select: async <T>(params: { options: Array<{ value: T }> }) => {
      const firstOption = params.options[0];
      if (!firstOption) {
        throw new Error("select called without options");
      }
      return firstOption.value;
    },
    multiselect: async () => [],
    text,
    confirm: async () => false,
    progress: () => ({
      update: () => {},
      stop: () => {},
    }),
  };
  return { prompter, note, text };
}

describe("lmstudio setup", () => {
  beforeEach(() => {
    fetchLmstudioModelsMock.mockReset();
    discoverLmstudioModelsMock.mockReset();
    configureSelfHostedNonInteractiveMock.mockReset();
    removeProviderAuthProfilesWithLockMock.mockReset();

    fetchLmstudioModelsMock.mockResolvedValue({
      reachable: true,
      status: 200,
      models: [
        {
          type: "llm",
          key: "qwen3-8b-instruct",
        },
      ],
    });
    discoverLmstudioModelsMock.mockResolvedValue([createModel("qwen3-8b-instruct", "Qwen3 8B")]);
    configureSelfHostedNonInteractiveMock.mockImplementation(
      async ({
        providerId,
        ctx,
      }: {
        providerId: string;
        ctx: ProviderAuthMethodNonInteractiveContext;
      }) => {
        const modelId =
          (typeof ctx.opts.customModelId === "string" ? ctx.opts.customModelId.trim() : "") ||
          "qwen3-8b-instruct";
        return {
          agents: { defaults: { model: { primary: `${providerId}/${modelId}` } } },
          models: {
            providers: {
              [providerId]: { api: "openai-completions", auth: "api-key", apiKey: "LM_API_TOKEN" },
            },
          },
        };
      },
    );
  });

  it("non-interactive setup discovers catalog and writes LM Studio provider config", async () => {
    const ctx = buildNonInteractiveContext({
      customBaseUrl: "http://localhost:1234/api/v1/",
      customModelId: "qwen3-8b-instruct",
    });
    fetchLmstudioModelsMock.mockResolvedValueOnce({
      reachable: true,
      status: 200,
      models: [
        {
          type: "llm",
          key: "qwen3-8b-instruct",
          display_name: "Qwen3 8B",
          loaded_instances: [{ id: "inst-1", config: { context_length: 64000 } }],
        },
        {
          type: "embedding",
          key: "text-embedding-nomic-embed-text-v1.5",
        },
      ],
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(fetchLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: "lmstudio-test-key",
      timeoutMs: 5000,
    });
    expect(result?.models?.providers?.lmstudio).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      auth: "api-key",
      apiKey: "LM_API_TOKEN",
      models: [
        {
          id: "qwen3-8b-instruct",
          contextWindow: SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
          contextTokens: 64000,
        },
      ],
    });
    expect(resolveAgentModelPrimaryValue(result?.agents?.defaults?.model)).toBe(
      "lmstudio/qwen3-8b-instruct",
    );
  });

  it("non-interactive setup preserves existing custom headers when CLI auth is provided", async () => {
    const ctx = buildNonInteractiveContext({
      config: {
        models: {
          providers: {
            lmstudio: {
              baseUrl: "http://localhost:1234/v1",
              api: "openai-completions",
              apiKey: "LM_API_TOKEN",
              headers: {
                Authorization: "Bearer stale-token",
                "X-Proxy-Auth": "proxy-token",
              },
              models: [],
            },
          },
        },
      } as OpenClawConfig,
      customBaseUrl: "http://localhost:1234/api/v1/",
      customModelId: "qwen3-8b-instruct",
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(result?.models?.providers?.lmstudio).toMatchObject({
      auth: "api-key",
      apiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
      headers: {
        Authorization: "Bearer stale-token",
        "X-Proxy-Auth": "proxy-token",
      },
    });
  });

  it("non-interactive setup auto-selects a discovered LM Studio model when none is provided", async () => {
    const ctx = buildNonInteractiveContext({
      customBaseUrl: "http://localhost:1234/api/v1/",
    });
    fetchLmstudioModelsMock.mockResolvedValueOnce({
      reachable: true,
      status: 200,
      models: [
        {
          type: "llm",
          key: "phi-4",
          max_context_length: 65536,
        },
        {
          type: "llm",
          key: "qwen3-8b-instruct",
          display_name: "Qwen3 8B",
        },
      ],
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(configureSelfHostedNonInteractiveMock).toHaveBeenCalledWith(
      expect.objectContaining({
        ctx: expect.objectContaining({
          opts: expect.objectContaining({
            customModelId: "phi-4",
          }),
        }),
      }),
    );
    expect(resolveAgentModelPrimaryValue(result?.agents?.defaults?.model)).toBe("lmstudio/phi-4");
    expect(result?.models?.providers?.lmstudio?.models).toEqual([
      expect.objectContaining({
        id: "phi-4",
        contextWindow: 65536,
      }),
      expect.objectContaining({
        id: "qwen3-8b-instruct",
      }),
    ]);
  });

  it("non-interactive setup synthesizes lmstudio-local when API key is missing", async () => {
    const ctx = buildNonInteractiveContext({
      customBaseUrl: "http://localhost:1234/api/v1/",
      customModelId: "qwen3-8b-instruct",
      resolvedApiKey: null,
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(fetchLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
      timeoutMs: 5000,
    });
    expect(result?.models?.providers?.lmstudio).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      apiKey: LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
      models: [
        {
          id: "qwen3-8b-instruct",
        },
      ],
    });
  });

  it("non-interactive setup keeps Authorization header auth without writing a synthetic key", async () => {
    const ctx = buildNonInteractiveContext({
      config: {
        auth: {
          profiles: {
            "lmstudio:default": {
              provider: "lmstudio",
              mode: "api_key",
            },
          },
          order: {
            lmstudio: ["lmstudio:default"],
          },
        },
        models: {
          providers: {
            lmstudio: {
              baseUrl: "http://localhost:1234/v1",
              apiKey: "stale-config-key",
              auth: "api-key",
              api: "openai-completions",
              headers: {
                Authorization: "Bearer proxy-token",
              },
              models: [],
            },
          },
        },
      } as OpenClawConfig,
      customBaseUrl: "http://localhost:1234/api/v1/",
      customApiKey: "",
      customModelId: "qwen3-8b-instruct",
      resolvedApiKey: null,
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(removeProviderAuthProfilesWithLockMock).toHaveBeenCalledWith({
      provider: "lmstudio",
      agentDir: undefined,
    });
    expect(fetchLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: undefined,
      headers: {
        Authorization: "Bearer proxy-token",
      },
      timeoutMs: 5000,
    });
    expect(configureSelfHostedNonInteractiveMock).not.toHaveBeenCalled();
    expect(resolveAgentModelPrimaryValue(result?.agents?.defaults?.model)).toBe(
      "lmstudio/qwen3-8b-instruct",
    );
    expect(result?.models?.providers?.lmstudio).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      headers: {
        Authorization: "Bearer proxy-token",
      },
      models: [
        {
          id: "qwen3-8b-instruct",
        },
      ],
    });
    expect(result?.models?.providers?.lmstudio).not.toHaveProperty("apiKey");
    expect(result?.models?.providers?.lmstudio).not.toHaveProperty("auth");
    expect(result?.auth).toBeUndefined();
  });

  it("non-interactive setup clears stale profile auth before switching to Authorization header auth", async () => {
    const ctx = buildNonInteractiveContext({
      config: {
        auth: {
          profiles: {
            "lmstudio:default": {
              provider: "lmstudio",
              mode: "api_key",
            },
          },
          order: {
            lmstudio: ["lmstudio:default"],
          },
        },
        models: {
          providers: {
            lmstudio: {
              baseUrl: "http://localhost:1234/v1",
              apiKey: "stale-config-key",
              auth: "api-key",
              api: "openai-completions",
              headers: {
                Authorization: "Bearer proxy-token",
              },
              models: [],
            },
          },
        },
      } as OpenClawConfig,
      customBaseUrl: "http://localhost:1234/api/v1/",
      customApiKey: "",
      customModelId: "qwen3-8b-instruct",
      resolvedApiKey: "stale-profile-key",
      resolvedApiKeySource: "profile",
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(removeProviderAuthProfilesWithLockMock).toHaveBeenCalledWith({
      provider: "lmstudio",
      agentDir: undefined,
    });
    expect(fetchLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: undefined,
      headers: {
        Authorization: "Bearer proxy-token",
      },
      timeoutMs: 5000,
    });
    expect(configureSelfHostedNonInteractiveMock).not.toHaveBeenCalled();
    expect(resolveAgentModelPrimaryValue(result?.agents?.defaults?.model)).toBe(
      "lmstudio/qwen3-8b-instruct",
    );
    expect(result?.models?.providers?.lmstudio).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      headers: {
        Authorization: "Bearer proxy-token",
      },
      models: [
        {
          id: "qwen3-8b-instruct",
        },
      ],
    });
    expect(result?.models?.providers?.lmstudio).not.toHaveProperty("apiKey");
    expect(result?.models?.providers?.lmstudio).not.toHaveProperty("auth");
    expect(result?.auth).toBeUndefined();
  });

  it("non-interactive setup clears env fallback auth before switching to Authorization header auth", async () => {
    const ctx = buildNonInteractiveContext({
      config: {
        models: {
          providers: {
            lmstudio: {
              baseUrl: "http://localhost:1234/v1",
              auth: "api-key",
              api: "openai-completions",
              headers: {
                Authorization: "Bearer proxy-token",
              },
              models: [],
            },
          },
        },
      } as OpenClawConfig,
      customBaseUrl: "http://localhost:1234/api/v1/",
      customApiKey: "",
      customModelId: "qwen3-8b-instruct",
      resolvedApiKey: "env-fallback-key",
      resolvedApiKeySource: "env",
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(removeProviderAuthProfilesWithLockMock).toHaveBeenCalledWith({
      provider: "lmstudio",
      agentDir: undefined,
    });
    expect(fetchLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: undefined,
      headers: {
        Authorization: "Bearer proxy-token",
      },
      timeoutMs: 5000,
    });
    expect(configureSelfHostedNonInteractiveMock).not.toHaveBeenCalled();
    expect(resolveAgentModelPrimaryValue(result?.agents?.defaults?.model)).toBe(
      "lmstudio/qwen3-8b-instruct",
    );
    expect(result?.models?.providers?.lmstudio).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      headers: {
        Authorization: "Bearer proxy-token",
      },
      models: [
        {
          id: "qwen3-8b-instruct",
        },
      ],
    });
    expect(result?.models?.providers?.lmstudio).not.toHaveProperty("apiKey");
    expect(result?.models?.providers?.lmstudio).not.toHaveProperty("auth");
    expect(result?.auth).toBeUndefined();
  });

  it("non-interactive setup prefers --lmstudio-api-key over --custom-api-key", async () => {
    const ctx = buildNonInteractiveContext({
      customBaseUrl: "http://localhost:1234/api/v1/",
      customModelId: "qwen3-8b-instruct",
      customApiKey: "old-custom-key",
      lmstudioApiKey: "new-lmstudio-key",
    });

    await configureLmstudioNonInteractive(ctx);

    expect(ctx.resolveApiKey).toHaveBeenCalledWith(
      expect.objectContaining({
        flagValue: "new-lmstudio-key",
        flagName: "--lmstudio-api-key",
      }),
    );
  });

  it("non-interactive setup overwrites existing config apiKey during re-auth", async () => {
    const ctx = buildNonInteractiveContext({
      config: {
        models: {
          providers: {
            lmstudio: {
              baseUrl: "http://localhost:1234/v1",
              auth: "api-key",
              apiKey: "stale-config-key",
              api: "openai-completions",
              models: [],
            },
          },
        },
      } as OpenClawConfig,
      customBaseUrl: "http://localhost:1234/api/v1/",
      customModelId: "qwen3-8b-instruct",
      lmstudioApiKey: "fresh-cli-key",
      resolvedApiKey: "fresh-cli-key",
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(result?.models?.providers?.lmstudio).toMatchObject({
      auth: "api-key",
      apiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
    });
    expect(result?.models?.providers?.lmstudio?.apiKey).not.toBe("stale-config-key");
  });

  it("non-interactive setup fails when requested model is missing", async () => {
    const ctx = buildNonInteractiveContext({
      customModelId: "missing-model",
    });

    await expect(configureLmstudioNonInteractive(ctx)).resolves.toBeNull();

    expect(ctx.runtime.error).toHaveBeenCalledWith(
      expect.stringContaining("LM Studio model missing-model was not found"),
    );
    expect(ctx.runtime.exit).toHaveBeenCalledWith(1);
    expect(configureSelfHostedNonInteractiveMock).not.toHaveBeenCalled();
  });

  it("interactive setup canonicalizes base URL and persists provider/default model", async () => {
    const promptText = vi
      .fn()
      .mockResolvedValueOnce("http://localhost:1234/api/v1/")
      .mockResolvedValueOnce("lmstudio-test-key");

    const result = await promptAndConfigureLmstudioInteractive({
      config: buildConfig(),
      promptText,
    });

    expect(result.configPatch?.models?.mode).toBe("merge");
    expect(result.configPatch?.models?.providers?.lmstudio).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      auth: "api-key",
      apiKey: "LM_API_TOKEN",
    });
    expect(result.defaultModel).toBe("lmstudio/qwen3-8b-instruct");
    expect(result.profiles[0]).toMatchObject({
      profileId: "lmstudio:default",
      credential: {
        type: "api_key",
        provider: "lmstudio",
        key: "lmstudio-test-key",
      },
    });
  });

  it("interactive setup applies an optional preferred context length to all discovered LM Studio models", async () => {
    fetchLmstudioModelsMock.mockResolvedValueOnce({
      reachable: true,
      status: 200,
      models: [
        {
          type: "llm",
          key: "phi-4",
          display_name: "Phi 4",
          max_context_length: 65536,
        },
        {
          type: "llm",
          key: "qwen3-8b-instruct",
          display_name: "Qwen3 8B",
          max_context_length: 32768,
        },
      ],
    });
    const { prompter, text } = createQueuedWizardPrompterHarness([
      "http://localhost:1234/api/v1/",
      "lmstudio-test-key",
      "4096",
    ]);

    const result = await promptAndConfigureLmstudioInteractive({
      config: buildConfig(),
      prompter,
    });

    expect(text).toHaveBeenCalledTimes(3);
    expect(result.configPatch?.models?.providers?.lmstudio?.models).toEqual([
      expect.objectContaining({
        id: "phi-4",
        contextWindow: 65536,
        contextTokens: 4096,
        maxTokens: 4096,
      }),
      expect.objectContaining({
        id: "qwen3-8b-instruct",
        contextWindow: 32768,
        contextTokens: 4096,
        maxTokens: 4096,
      }),
    ]);
  });

  it("interactive setup overwrites existing config apiKey during re-auth", async () => {
    const config = {
      models: {
        providers: {
          lmstudio: {
            baseUrl: "http://localhost:1234/v1",
            auth: "api-key",
            apiKey: "stale-config-key",
            api: "openai-completions",
            models: [],
          },
        },
      },
    } as OpenClawConfig;
    const promptText = vi
      .fn()
      .mockResolvedValueOnce("http://localhost:1234/api/v1/")
      .mockResolvedValueOnce("fresh-prompt-key");

    const result = await promptAndConfigureLmstudioInteractive({
      config,
      promptText,
    });
    expect(result.configPatch?.models?.providers?.lmstudio).toMatchObject({
      auth: "api-key",
      apiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
    });
    expect(result.configPatch?.models?.providers?.lmstudio?.apiKey).not.toBe("stale-config-key");
    expect(result.profiles[0]).toMatchObject({
      profileId: "lmstudio:default",
      credential: {
        type: "api_key",
        provider: "lmstudio",
        key: "fresh-prompt-key",
      },
    });
  });

  it("interactive setup preserves existing custom headers when switching to api-key auth", async () => {
    const config = {
      models: {
        providers: {
          lmstudio: {
            baseUrl: "http://localhost:1234/v1",
            api: "openai-completions",
            apiKey: "LM_API_TOKEN",
            headers: {
              Authorization: "Bearer stale-token",
              "X-Proxy-Auth": "proxy-token",
            },
            models: [],
          },
        },
      },
    } as OpenClawConfig;
    const promptText = vi
      .fn()
      .mockResolvedValueOnce("http://localhost:1234/api/v1/")
      .mockResolvedValueOnce("lmstudio-test-key");

    const result = await promptAndConfigureLmstudioInteractive({
      config,
      promptText,
    });
    expect(result.configPatch?.models?.providers?.lmstudio).toMatchObject({
      auth: "api-key",
      apiKey: "LM_API_TOKEN",
      headers: {
        Authorization: "Bearer stale-token",
        "X-Proxy-Auth": "proxy-token",
      },
    });
  });

  it("interactive setup preserves existing agent model allowlist entries", async () => {
    const config = {
      agents: {
        defaults: {
          models: {
            "anthropic/claude-sonnet-4-6": {
              alias: "Sonnet",
            },
          },
        },
      },
      models: {
        providers: {
          lmstudio: {
            baseUrl: "http://localhost:1234/v1",
            api: "openai-completions",
            apiKey: "LM_API_TOKEN",
            models: [],
          },
        },
      },
    } as OpenClawConfig;
    const promptText = vi
      .fn()
      .mockResolvedValueOnce("http://localhost:1234/api/v1/")
      .mockResolvedValueOnce("lmstudio-test-key");

    const result = await promptAndConfigureLmstudioInteractive({
      config,
      promptText,
    });
    expect(result.configPatch?.agents?.defaults?.models).toEqual({
      "anthropic/claude-sonnet-4-6": {
        alias: "Sonnet",
      },
      "lmstudio/qwen3-8b-instruct": {},
    });
  });

  it("interactive setup returns clear errors for unreachable/http-empty results", async () => {
    const cases = [
      {
        name: "unreachable",
        discovery: { reachable: false, models: [] },
        expectedError: "LM Studio not reachable",
      },
      {
        name: "http error",
        discovery: { reachable: true, status: 401, models: [] },
        expectedError: "LM Studio discovery failed (401)",
      },
      {
        name: "no llm models",
        discovery: {
          reachable: true,
          status: 200,
          models: [{ type: "embedding", key: "text-embedding-nomic-embed-text-v1.5" }],
        },
        expectedError: "No LM Studio models found",
      },
    ];

    for (const testCase of cases) {
      const promptText = vi
        .fn()
        .mockResolvedValueOnce("http://localhost:1234/v1")
        .mockResolvedValueOnce("lmstudio-test-key");
      fetchLmstudioModelsMock.mockResolvedValueOnce(testCase.discovery);
      await expect(
        promptAndConfigureLmstudioInteractive({
          config: buildConfig(),
          promptText,
        }),
        testCase.name,
      ).rejects.toThrow(testCase.expectedError);
    }
  });

  it.each([
    {
      name: "injects lmstudio-local for explicit models by default",
      providerPatch: {},
      expectedProviderPatch: {
        apiKey: LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
      },
    },
    {
      name: "keeps api-key auth backed by default env marker",
      providerPatch: {
        auth: "api-key",
      },
      expectedProviderPatch: {
        auth: "api-key",
        apiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
      },
    },
    {
      name: "does not inject api-key marker when Authorization header is configured",
      providerPatch: {
        apiKey: "stale-legacy-key",
        headers: {
          Authorization: "Bearer custom-token",
        },
      },
      expectedProviderPatch: {
        headers: {
          Authorization: "Bearer custom-token",
        },
      },
    },
    {
      name: "still injects lmstudio-local when only non-auth headers are configured",
      providerPatch: {
        headers: {
          "X-Proxy-Auth": "proxy-token",
        },
      },
      expectedProviderPatch: {
        apiKey: LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
        headers: {
          "X-Proxy-Auth": "proxy-token",
        },
      },
    },
  ])(
    "discoverLmstudioProvider short-circuits explicit models and $name",
    async ({ providerPatch, expectedProviderPatch }) => {
      const explicitModels = [createModel("qwen3-8b-instruct", "Qwen3 8B")];
      const result = await discoverLmstudioProvider(
        buildDiscoveryContext({
          config: {
            models: {
              providers: {
                lmstudio: {
                  baseUrl: "http://localhost:1234/api/v1/",
                  models: explicitModels,
                  ...providerPatch,
                },
              },
            },
          } as OpenClawConfig,
        }),
      );

      expect(discoverLmstudioModelsMock).not.toHaveBeenCalled();
      expect(result).toEqual({
        provider: {
          baseUrl: "http://localhost:1234/v1",
          api: "openai-completions",
          ...expectedProviderPatch,
          models: explicitModels,
        },
      });
    },
  );

  it("discoverLmstudioProvider uses resolved key/headers and non-quiet discovery", async () => {
    discoverLmstudioModelsMock.mockResolvedValueOnce([
      createModel("qwen3-8b-instruct", "Qwen3 8B"),
    ]);

    const result = await discoverLmstudioProvider(
      buildDiscoveryContext({
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                apiKey: {
                  source: "env",
                  provider: "default",
                  id: "LMSTUDIO_DISCOVERY_TOKEN",
                },
                headers: {
                  "X-Proxy-Auth": {
                    source: "env",
                    provider: "default",
                    id: "LMSTUDIO_PROXY_TOKEN",
                  },
                },
                models: [],
              },
            },
          },
        } as OpenClawConfig,
        env: {
          LMSTUDIO_DISCOVERY_TOKEN: "secretref-lmstudio-key",
          LMSTUDIO_PROXY_TOKEN: "proxy-token-from-env",
        },
      }),
    );

    expect(discoverLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: "secretref-lmstudio-key",
      headers: {
        "X-Proxy-Auth": "proxy-token-from-env",
      },
      quiet: false,
    });
    expect(result?.provider.models?.map((model) => model.id)).toEqual(["qwen3-8b-instruct"]);
  });

  it("discoverLmstudioProvider returns null for unresolved header refs", async () => {
    const result = await discoverLmstudioProvider(
      buildDiscoveryContext({
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                headers: {
                  "X-Proxy-Auth": {
                    source: "env",
                    provider: "default",
                    id: "LMSTUDIO_PROXY_TOKEN",
                  },
                },
                models: [],
              },
            },
          },
        } as OpenClawConfig,
        env: {},
      }),
    );

    expect(result).toBeNull();
    expect(discoverLmstudioModelsMock).not.toHaveBeenCalled();
  });

  it("discoverLmstudioProvider returns null for an unresolved apiKey ref", async () => {
    const result = await discoverLmstudioProvider(
      buildDiscoveryContext({
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                apiKey: {
                  source: "env",
                  provider: "default",
                  id: "LMSTUDIO_DISCOVERY_TOKEN",
                },
                models: [],
              },
            },
          },
        } as OpenClawConfig,
        env: {},
      }),
    );

    expect(result).toBeNull();
    expect(discoverLmstudioModelsMock).not.toHaveBeenCalled();
  });

  it("discoverLmstudioProvider uses configured direct apiKey for discovery", async () => {
    discoverLmstudioModelsMock.mockResolvedValueOnce([
      createModel("qwen3-8b-instruct", "Qwen3 8B"),
    ]);

    await discoverLmstudioProvider(
      buildDiscoveryContext({
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                apiKey: "configured-direct-key",
                models: [],
              },
            },
          },
        } as OpenClawConfig,
      }),
    );

    expect(discoverLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: "configured-direct-key",
      headers: undefined,
      quiet: false,
    });
  });

  it("discoverLmstudioProvider prefers resolved discoveryApiKey over configured apiKey", async () => {
    discoverLmstudioModelsMock.mockResolvedValueOnce([
      createModel("qwen3-8b-instruct", "Qwen3 8B"),
    ]);

    await discoverLmstudioProvider(
      buildDiscoveryContext({
        discoveryApiKey: "resolved-discovery-key",
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                apiKey: "configured-direct-key",
                models: [],
              },
            },
          },
        } as OpenClawConfig,
      }),
    );

    expect(discoverLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: "resolved-discovery-key",
      headers: undefined,
      quiet: false,
    });
  });

  it("discoverLmstudioProvider suppresses stale discovery apiKey when Authorization header auth is configured", async () => {
    discoverLmstudioModelsMock.mockResolvedValueOnce([
      createModel("qwen3-8b-instruct", "Qwen3 8B"),
    ]);

    await discoverLmstudioProvider(
      buildDiscoveryContext({
        discoveryApiKey: "resolved-stale-key",
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                apiKey: "configured-direct-key",
                headers: {
                  Authorization: "Bearer custom-token",
                },
                models: [],
              },
            },
          },
        } as OpenClawConfig,
      }),
    );

    expect(discoverLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: "",
      headers: {
        Authorization: "Bearer custom-token",
      },
      quiet: false,
    });
  });

  it("discoverLmstudioProvider rewrites stale api-key auth without a persisted key", async () => {
    const result = await discoverLmstudioProvider(
      buildDiscoveryContext({
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                auth: "api-key",
                models: [],
              },
            },
          },
        } as OpenClawConfig,
      }),
    );

    expect(result?.provider).toMatchObject({
      auth: "api-key",
      apiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
      models: [expect.objectContaining({ id: "qwen3-8b-instruct" })],
    });
  });

  it("discoverLmstudioProvider drops stale apiKey when Authorization header auth is configured", async () => {
    const result = await discoverLmstudioProvider(
      buildDiscoveryContext({
        config: {
          models: {
            providers: {
              lmstudio: {
                baseUrl: "http://localhost:1234/v1",
                api: "openai-completions",
                apiKey: "stale-legacy-key",
                headers: {
                  Authorization: "Bearer custom-token",
                },
                models: [],
              },
            },
          },
        } as OpenClawConfig,
      }),
    );

    expect(result?.provider).toMatchObject({
      baseUrl: "http://localhost:1234/v1",
      api: "openai-completions",
      headers: {
        Authorization: "Bearer custom-token",
      },
      models: [expect.objectContaining({ id: "qwen3-8b-instruct" })],
    });
    expect(result?.provider.apiKey).toBeUndefined();
    expect(result?.provider.auth).toBeUndefined();
  });

  it("discoverLmstudioProvider uses quiet mode and returns null when unconfigured", async () => {
    discoverLmstudioModelsMock.mockResolvedValueOnce([]);

    const result = await discoverLmstudioProvider(buildDiscoveryContext());

    expect(discoverLmstudioModelsMock).toHaveBeenCalledWith({
      baseUrl: "http://localhost:1234/v1",
      apiKey: "",
      quiet: true,
      headers: undefined,
    });
    expect(result).toBeNull();
  });

  it("non-interactive setup replaces local auth markers when enabling api-key auth", async () => {
    const ctx = buildNonInteractiveContext({
      config: {
        models: {
          providers: {
            lmstudio: {
              baseUrl: "http://localhost:1234/v1",
              apiKey: CUSTOM_LOCAL_AUTH_MARKER,
              api: "openai-completions",
              models: [],
            },
          },
        },
      } as OpenClawConfig,
      customBaseUrl: "http://localhost:1234/api/v1/",
      customModelId: "qwen3-8b-instruct",
    });

    const result = await configureLmstudioNonInteractive(ctx);

    expect(result?.models?.providers?.lmstudio).toMatchObject({
      auth: "api-key",
      apiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
    });
  });
});

¤ Dauer der Verarbeitung: 0.3 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.