mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
refactor(lmstudio): share non-interactive discovery validation (#129808)
This commit is contained in:
committed by
GitHub
parent
c406ef4b4d
commit
a6a4da70d1
@@ -335,6 +335,53 @@ describe("lmstudio plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "the local placeholder when no credential exists",
|
||||
resolvedApiKey: null,
|
||||
expectedApiKey: LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
|
||||
},
|
||||
{
|
||||
name: "a preserved credential profile",
|
||||
resolvedApiKey: { key: "profile-api-key", source: "profile" as const },
|
||||
expectedApiKey: "profile-api-key",
|
||||
},
|
||||
{
|
||||
name: "an environment credential",
|
||||
resolvedApiKey: { key: "environment-api-key", source: "env" as const },
|
||||
expectedApiKey: "environment-api-key",
|
||||
},
|
||||
])("uses $name with the post-reset empty config", async ({ resolvedApiKey, expectedApiKey }) => {
|
||||
fetchLmstudioModelsMock.mockResolvedValue({
|
||||
reachable: true,
|
||||
status: 200,
|
||||
models: [
|
||||
{
|
||||
type: "llm",
|
||||
key: "qwen/qwen3.5-9b",
|
||||
loaded_instances: [{ id: "qwen", config: { context_length: 32_768 } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const ctx = createLmstudioResetValidationContext(
|
||||
{
|
||||
customBaseUrl: "http://lmstudio.internal:1234/v1",
|
||||
customModelId: "qwen/qwen3.5-9b",
|
||||
},
|
||||
resolvedApiKey,
|
||||
);
|
||||
|
||||
await expect(requireLmstudioResetValidator()(ctx)).resolves.toBe(true);
|
||||
|
||||
expect(fetchLmstudioModelsMock).toHaveBeenCalledExactlyOnceWith({
|
||||
baseUrl: "http://lmstudio.internal:1234/v1",
|
||||
apiKey: expectedApiKey,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
expect(ctx.runtime.exit).not.toHaveBeenCalled();
|
||||
expect(ctx.config).toEqual({});
|
||||
});
|
||||
|
||||
it("rejects an unreachable LM Studio endpoint before destructive reset", async () => {
|
||||
fetchLmstudioModelsMock.mockResolvedValue({ reachable: false, models: [] });
|
||||
const ctx = createLmstudioResetValidationContext({
|
||||
@@ -349,19 +396,26 @@ describe("lmstudio plugin", () => {
|
||||
expect(ctx.runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("rejects LM Studio authentication failures before destructive reset", async () => {
|
||||
fetchLmstudioModelsMock.mockResolvedValue({ reachable: true, status: 401, models: [] });
|
||||
const ctx = createLmstudioResetValidationContext({
|
||||
customBaseUrl: "http://lmstudio.internal:1234/v1",
|
||||
});
|
||||
it.each([401, 403, 404, 408, 425, 429, 500, 503])(
|
||||
"preserves the existing HTTP %i reset failure guidance",
|
||||
async (httpStatus) => {
|
||||
fetchLmstudioModelsMock.mockResolvedValue({
|
||||
reachable: true,
|
||||
status: httpStatus,
|
||||
models: [],
|
||||
});
|
||||
const ctx = createLmstudioResetValidationContext({
|
||||
customBaseUrl: "http://lmstudio.internal:1234/v1",
|
||||
});
|
||||
|
||||
await expect(requireLmstudioResetValidator()(ctx)).resolves.toBe(false);
|
||||
await expect(requireLmstudioResetValidator()(ctx)).resolves.toBe(false);
|
||||
|
||||
expect(ctx.runtime.error).toHaveBeenCalledWith(
|
||||
"LM Studio returned HTTP 401 while listing models at http://lmstudio.internal:1234/v1.\nCheck the base URL and API key, then re-run setup.",
|
||||
);
|
||||
expect(ctx.runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
expect(ctx.runtime.error).toHaveBeenCalledExactlyOnceWith(
|
||||
`LM Studio returned HTTP ${httpStatus} while listing models at http://lmstudio.internal:1234/v1.\nCheck the base URL and API key, then re-run setup.`,
|
||||
);
|
||||
expect(ctx.runtime.exit).toHaveBeenCalledExactlyOnceWith(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a missing requested LM Studio model before destructive reset", async () => {
|
||||
fetchLmstudioModelsMock.mockResolvedValue({
|
||||
|
||||
@@ -5,123 +5,26 @@ import {
|
||||
type OpenClawConfig,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethod,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
type ProviderAuthResult,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
CUSTOM_LOCAL_AUTH_MARKER,
|
||||
normalizeOptionalSecretInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { CUSTOM_LOCAL_AUTH_MARKER } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
|
||||
import { lmstudioMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
|
||||
import {
|
||||
LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
LMSTUDIO_DEFAULT_INFERENCE_BASE_URL,
|
||||
LMSTUDIO_DOCKER_HOST_INFERENCE_BASE_URL,
|
||||
LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
|
||||
LMSTUDIO_PROVIDER_LABEL,
|
||||
} from "./src/defaults.js";
|
||||
import {
|
||||
normalizeLmstudioConfiguredCatalogEntries,
|
||||
normalizeLmstudioProviderConfig,
|
||||
resolveLoadedContextWindow,
|
||||
resolveLmstudioInferenceBase,
|
||||
} from "./src/models.js";
|
||||
import { shouldUseLmstudioSyntheticAuth } from "./src/provider-auth.js";
|
||||
import { wrapLmstudioInferencePreload } from "./src/stream.js";
|
||||
|
||||
const PROVIDER_ID = "lmstudio";
|
||||
|
||||
type LmstudioNonInteractiveValidationContext = Parameters<
|
||||
NonNullable<ProviderAuthMethod["validateNonInteractive"]>
|
||||
>[0];
|
||||
|
||||
async function validateLmstudioNonInteractive(
|
||||
ctx: LmstudioNonInteractiveValidationContext,
|
||||
): Promise<boolean> {
|
||||
const configuredBaseUrl = normalizeOptionalSecretInput(ctx.opts.customBaseUrl);
|
||||
const dockerSetup = ["1", "true", "yes", "on"].includes(
|
||||
process.env.OPENCLAW_DOCKER_SETUP?.trim().toLowerCase() ?? "",
|
||||
);
|
||||
const baseUrl = resolveLmstudioInferenceBase(
|
||||
configuredBaseUrl ||
|
||||
(dockerSetup ? LMSTUDIO_DOCKER_HOST_INFERENCE_BASE_URL : LMSTUDIO_DEFAULT_INFERENCE_BASE_URL),
|
||||
);
|
||||
const providerApiKey = normalizeOptionalSecretInput(ctx.opts.lmstudioApiKey);
|
||||
const resolvedApiKey = await ctx.resolveApiKey({
|
||||
provider: PROVIDER_ID,
|
||||
flagValue: providerApiKey ?? normalizeOptionalSecretInput(ctx.opts.customApiKey),
|
||||
flagName: providerApiKey === undefined ? "--custom-api-key" : "--lmstudio-api-key",
|
||||
envVar: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
envVarName: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
required: false,
|
||||
});
|
||||
|
||||
// A reset preflight may inspect the model catalog but must never invoke
|
||||
// setup, write credentials, load a model, or mutate the model server.
|
||||
const { fetchLmstudioModels } = await import("./src/models.fetch.js");
|
||||
const discovery = await fetchLmstudioModels({
|
||||
baseUrl,
|
||||
apiKey: resolvedApiKey?.key ?? LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
if (!discovery.reachable) {
|
||||
ctx.runtime.error(
|
||||
`LM Studio could not be reached at ${baseUrl}.\nStart LM Studio (or run lms server start) and re-run setup.`,
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return false;
|
||||
}
|
||||
if (discovery.status !== undefined && discovery.status >= 400) {
|
||||
ctx.runtime.error(
|
||||
`LM Studio returned HTTP ${discovery.status} while listing models at ${baseUrl}.\nCheck the base URL and API key, then re-run setup.`,
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
const installedModels = discovery.models
|
||||
.filter((model) => model.type === "llm")
|
||||
.map((model) => model.key?.trim())
|
||||
.filter((model): model is string => Boolean(model));
|
||||
const loadedModels = discovery.models
|
||||
.filter(
|
||||
(model) =>
|
||||
model.type === "llm" &&
|
||||
Boolean(model.key?.trim()) &&
|
||||
resolveLoadedContextWindow(model) !== null,
|
||||
)
|
||||
.map((model) => model.key?.trim())
|
||||
.filter((model): model is string => Boolean(model));
|
||||
// Setup matches the requested wire key unchanged. Accepting provider-
|
||||
// qualified refs here would permit reset before setup rejects the model.
|
||||
const requestedModel = normalizeOptionalSecretInput(ctx.opts.customModelId);
|
||||
if (requestedModel && !installedModels.includes(requestedModel)) {
|
||||
ctx.runtime.error(
|
||||
`LM Studio model ${requestedModel} was not found at ${baseUrl}.\nAvailable models: ${installedModels.join(", ")}`,
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return false;
|
||||
}
|
||||
if (requestedModel && !loadedModels.includes(requestedModel)) {
|
||||
ctx.runtime.error(
|
||||
`LM Studio model ${requestedModel} is installed but not loaded at ${baseUrl}.\nLoad that model in LM Studio, then re-run setup.`,
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return false;
|
||||
}
|
||||
if (loadedModels.length === 0) {
|
||||
ctx.runtime.error(
|
||||
`No loaded LM Studio LLM models were found at ${baseUrl}.\nLoad a model in LM Studio (or run lms load <model>), then re-run setup.`,
|
||||
);
|
||||
ctx.runtime.exit(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveLmstudioAugmentedCatalogEntries(config: OpenClawConfig | undefined) {
|
||||
if (!config) {
|
||||
return [];
|
||||
@@ -142,7 +45,7 @@ function resolveLmstudioAugmentedCatalogEntries(config: OpenClawConfig | undefin
|
||||
|
||||
/** Lazily loads setup helpers so provider wiring stays lightweight at startup. */
|
||||
async function loadProviderSetup() {
|
||||
return await import("./api.js");
|
||||
return await import("./src/setup.js");
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
@@ -199,7 +102,10 @@ export default definePluginEntry({
|
||||
signal: ctx.signal,
|
||||
});
|
||||
},
|
||||
validateNonInteractive: validateLmstudioNonInteractive,
|
||||
validateNonInteractive: async (ctx) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.validateLmstudioNonInteractive(ctx);
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.configureLmstudioNonInteractive(ctx);
|
||||
|
||||
@@ -219,6 +219,8 @@ function collectLoadedLmstudioModelIds(discovery: LmstudioDiscoveryResult): Set<
|
||||
function resolveLmstudioDiscoveryFailure(params: {
|
||||
baseUrl: string;
|
||||
discovery: LmstudioDiscoveryResult;
|
||||
requestedModelId?: string;
|
||||
resetPreflight?: boolean;
|
||||
}): { noteLines: [string, string]; retryLine?: string; reason: string } | null {
|
||||
const { baseUrl, discovery } = params;
|
||||
if (!discovery.reachable) {
|
||||
@@ -240,7 +242,7 @@ function resolveLmstudioDiscoveryFailure(params: {
|
||||
return {
|
||||
noteLines: [
|
||||
`LM Studio returned HTTP ${discovery.status} while listing models at ${baseUrl}.`,
|
||||
retryable
|
||||
retryable && !params.resetPreflight
|
||||
? "Wait for LM Studio to recover, then re-run setup."
|
||||
: "Check the base URL and API key, then re-run setup.",
|
||||
],
|
||||
@@ -248,7 +250,10 @@ function resolveLmstudioDiscoveryFailure(params: {
|
||||
reason: `LM Studio discovery failed (${discovery.status})`,
|
||||
};
|
||||
}
|
||||
if (collectLoadedLmstudioModelIds(discovery).size === 0) {
|
||||
if (
|
||||
!(params.resetPreflight && params.requestedModelId) &&
|
||||
collectLoadedLmstudioModelIds(discovery).size === 0
|
||||
) {
|
||||
return {
|
||||
noteLines: [
|
||||
`No loaded LM Studio LLM models were found at ${baseUrl}.`,
|
||||
@@ -387,6 +392,8 @@ async function discoverLmstudioSetupModels(params: {
|
||||
baseUrl: string;
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
requestedModelId?: string;
|
||||
resetPreflight?: boolean;
|
||||
timeoutMs?: number;
|
||||
}): Promise<
|
||||
| { value: LmstudioSetupDiscovery }
|
||||
@@ -401,6 +408,8 @@ async function discoverLmstudioSetupModels(params: {
|
||||
const failure = resolveLmstudioDiscoveryFailure({
|
||||
baseUrl: params.baseUrl,
|
||||
discovery,
|
||||
requestedModelId: params.requestedModelId,
|
||||
resetPreflight: params.resetPreflight,
|
||||
});
|
||||
if (failure) {
|
||||
return { failure };
|
||||
@@ -751,53 +760,31 @@ export async function promptAndConfigureLmstudioInteractive(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Non-interactive setup path backed by the shared self-hosted helper. */
|
||||
export async function configureLmstudioNonInteractive(
|
||||
ctx: ProviderAuthMethodNonInteractiveContext,
|
||||
): Promise<OpenClawConfig | null> {
|
||||
async function validateNonInteractiveLmstudioDiscovery(
|
||||
ctx: Omit<ProviderAuthMethodNonInteractiveContext, "toApiKeyCredential">,
|
||||
resetPreflight = false,
|
||||
) {
|
||||
const customBaseUrl = normalizeOptionalSecretInput(ctx.opts.customBaseUrl);
|
||||
const baseUrl = resolveLmstudioInferenceBase(
|
||||
customBaseUrl || resolveLmstudioSetupDefaultInferenceBaseUrl(),
|
||||
);
|
||||
const normalizedCtx = customBaseUrl
|
||||
? {
|
||||
...ctx,
|
||||
opts: {
|
||||
...ctx.opts,
|
||||
customBaseUrl: baseUrl,
|
||||
},
|
||||
}
|
||||
: ctx;
|
||||
const configureShared = async (configureCtx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
await configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx: configureCtx,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: LMSTUDIO_PROVIDER_LABEL,
|
||||
defaultBaseUrl: resolveLmstudioSetupDefaultInferenceBaseUrl(),
|
||||
defaultApiKeyEnvVar: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: LMSTUDIO_MODEL_PLACEHOLDER,
|
||||
});
|
||||
const requestedModelId = normalizeOptionalSecretInput(normalizedCtx.opts.customModelId);
|
||||
const resolved = await normalizedCtx.resolveApiKey({
|
||||
const requestedModelId = normalizeOptionalSecretInput(ctx.opts.customModelId);
|
||||
const providerApiKey = normalizeOptionalSecretInput(ctx.opts.lmstudioApiKey);
|
||||
const resolved = await ctx.resolveApiKey({
|
||||
provider: PROVIDER_ID,
|
||||
flagValue:
|
||||
normalizeOptionalSecretInput(normalizedCtx.opts.lmstudioApiKey) ??
|
||||
normalizeOptionalSecretInput(normalizedCtx.opts.customApiKey),
|
||||
flagName:
|
||||
normalizeOptionalSecretInput(normalizedCtx.opts.lmstudioApiKey) !== undefined
|
||||
? "--lmstudio-api-key"
|
||||
: "--custom-api-key",
|
||||
flagValue: providerApiKey ?? normalizeOptionalSecretInput(ctx.opts.customApiKey),
|
||||
flagName: providerApiKey === undefined ? "--custom-api-key" : "--lmstudio-api-key",
|
||||
envVar: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
envVarName: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
required: false,
|
||||
});
|
||||
|
||||
const existingProvider = normalizedCtx.config.models?.providers?.[PROVIDER_ID];
|
||||
const existingProvider = ctx.config.models?.providers?.[PROVIDER_ID];
|
||||
// Auth setup updates auth/profile/provider model fields but does not mutate
|
||||
// user-provided header overrides. Runtime request assembly is the source of truth for auth.
|
||||
const persistedHeaders = existingProvider?.headers;
|
||||
const resolvedHeaders = await resolveLmstudioProviderHeaders({
|
||||
config: normalizedCtx.config,
|
||||
config: ctx.config,
|
||||
env: process.env,
|
||||
headers: persistedHeaders,
|
||||
});
|
||||
@@ -813,21 +800,23 @@ export async function configureLmstudioNonInteractive(
|
||||
? LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER
|
||||
: undefined);
|
||||
if (!setupDiscoveryApiKey && !hasAuthorizationHeader) {
|
||||
normalizedCtx.runtime.error(
|
||||
ctx.runtime.error(
|
||||
`LM Studio API key is required. Set ${LMSTUDIO_DEFAULT_API_KEY_ENV_VAR} or pass --lmstudio-api-key.`,
|
||||
);
|
||||
normalizedCtx.runtime.exit(1);
|
||||
ctx.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
const setupDiscovery = await discoverLmstudioSetupModels({
|
||||
baseUrl,
|
||||
apiKey: setupDiscoveryApiKey,
|
||||
...(resolvedHeaders ? { headers: resolvedHeaders } : {}),
|
||||
requestedModelId,
|
||||
resetPreflight,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
if ("failure" in setupDiscovery) {
|
||||
normalizedCtx.runtime.error(setupDiscovery.failure.noteLines.join("\n"));
|
||||
normalizedCtx.runtime.exit(1);
|
||||
ctx.runtime.error(setupDiscovery.failure.noteLines.join("\n"));
|
||||
ctx.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
const discoveredModels = setupDiscovery.value.models;
|
||||
@@ -839,7 +828,7 @@ export async function configureLmstudioNonInteractive(
|
||||
selectedModelId !== undefined && setupDiscovery.value.loadedModelIds.has(selectedModelId);
|
||||
if (!selectedModelId || !selectedModel || !selectedModelLoaded) {
|
||||
const availableModels = discoveredModels.map((model) => model.id).join(", ");
|
||||
normalizedCtx.runtime.error(
|
||||
ctx.runtime.error(
|
||||
requestedModelId && selectedModel && !selectedModelLoaded
|
||||
? [
|
||||
`LM Studio model ${requestedModelId} is installed but not loaded at ${baseUrl}.`,
|
||||
@@ -855,9 +844,60 @@ export async function configureLmstudioNonInteractive(
|
||||
`Available models: ${availableModels || "(none)"}`,
|
||||
].join("\n"),
|
||||
);
|
||||
normalizedCtx.runtime.exit(1);
|
||||
ctx.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
customBaseUrl,
|
||||
discoveredModels,
|
||||
existingProvider,
|
||||
persistedHeaders,
|
||||
resolved,
|
||||
resolvedHeaders,
|
||||
selectedModelId,
|
||||
setupDiscoveryApiKey,
|
||||
useHeaderOnlyAuth,
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks endpoint auth and loaded models without mutating config, profiles, or the server. */
|
||||
export async function validateLmstudioNonInteractive(
|
||||
ctx: Omit<ProviderAuthMethodNonInteractiveContext, "toApiKeyCredential">,
|
||||
): Promise<boolean> {
|
||||
return Boolean(await validateNonInteractiveLmstudioDiscovery(ctx, true));
|
||||
}
|
||||
|
||||
/** Non-interactive setup path backed by the shared self-hosted helper. */
|
||||
export async function configureLmstudioNonInteractive(
|
||||
ctx: ProviderAuthMethodNonInteractiveContext,
|
||||
): Promise<OpenClawConfig | null> {
|
||||
const validated = await validateNonInteractiveLmstudioDiscovery(ctx);
|
||||
if (!validated) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
baseUrl,
|
||||
customBaseUrl,
|
||||
discoveredModels,
|
||||
existingProvider,
|
||||
persistedHeaders,
|
||||
resolved,
|
||||
resolvedHeaders,
|
||||
selectedModelId,
|
||||
setupDiscoveryApiKey,
|
||||
useHeaderOnlyAuth,
|
||||
} = validated;
|
||||
const normalizedCtx = customBaseUrl
|
||||
? {
|
||||
...ctx,
|
||||
opts: {
|
||||
...ctx.opts,
|
||||
customBaseUrl: baseUrl,
|
||||
},
|
||||
}
|
||||
: ctx;
|
||||
if (useHeaderOnlyAuth) {
|
||||
await removeProviderAuthProfilesWithLock({
|
||||
provider: PROVIDER_ID,
|
||||
@@ -900,13 +940,20 @@ export async function configureLmstudioNonInteractive(
|
||||
// state and credential storage are handled consistently. The pre-resolved key
|
||||
// is injected via resolveApiKey to skip a second prompt. The returned config
|
||||
// is then post-patched below to add the discovered model list and base URL.
|
||||
const configured = await configureShared({
|
||||
...normalizedCtx,
|
||||
opts: {
|
||||
...normalizedCtx.opts,
|
||||
customModelId: selectedModelId,
|
||||
const configured = await configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx: {
|
||||
...normalizedCtx,
|
||||
opts: {
|
||||
...normalizedCtx.opts,
|
||||
customModelId: selectedModelId,
|
||||
},
|
||||
resolveApiKey: async () => resolvedOrSynthetic,
|
||||
},
|
||||
resolveApiKey: async () => resolvedOrSynthetic,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: LMSTUDIO_PROVIDER_LABEL,
|
||||
defaultBaseUrl: resolveLmstudioSetupDefaultInferenceBaseUrl(),
|
||||
defaultApiKeyEnvVar: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: LMSTUDIO_MODEL_PLACEHOLDER,
|
||||
});
|
||||
if (!configured) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user