diff --git a/extensions/lmstudio/src/models.test.ts b/extensions/lmstudio/src/models.test.ts index ec565dca355..bbf293ebc6b 100644 --- a/extensions/lmstudio/src/models.test.ts +++ b/extensions/lmstudio/src/models.test.ts @@ -42,6 +42,8 @@ describe("lmstudio-models", () => { expect(resolveLmstudioInferenceBase("http://localhost:1234/api/v1")).toBe( "http://localhost:1234/v1", ); + expect(resolveLmstudioServerBase("localhost:1234/api/v1")).toBe("http://localhost:1234"); + expect(resolveLmstudioInferenceBase("localhost:1234/api/v1")).toBe("http://localhost:1234/v1"); }); it("resolves reasoning capability for supported and unsupported options", () => { diff --git a/extensions/lmstudio/src/models.ts b/extensions/lmstudio/src/models.ts index d473a19694d..ef43b58ca86 100644 --- a/extensions/lmstudio/src/models.ts +++ b/extensions/lmstudio/src/models.ts @@ -118,13 +118,36 @@ function normalizeUrlPath(pathname: string): string { return trimmed.replace(/\/api\/v1$/i, "").replace(/\/v1$/i, ""); } +function hasExplicitHttpScheme(value: string): boolean { + return /^https?:\/\//i.test(value); +} + +function isLikelyHostBaseUrl(value: string): boolean { + return ( + /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3}|[a-z0-9.-]+\.[a-z]{2,}|[^/\s?#]+:\d+)(?:[/?#].*)?$/i.test( + value, + ) && !value.startsWith("/") + ); +} + +function toFetchableLmstudioBaseUrl(value: string): string { + if (hasExplicitHttpScheme(value) || !isLikelyHostBaseUrl(value)) { + return value; + } + return `http://${value}`; +} + /** Resolves LM Studio server base URL (without /v1 or /api/v1). */ export function resolveLmstudioServerBase(configuredBaseUrl?: string): string { // Use configured value when present; otherwise target local LM Studio default. const configured = configuredBaseUrl?.trim(); const resolved = configured && configured.length > 0 ? configured : LMSTUDIO_DEFAULT_BASE_URL; + const fetchableBaseUrl = toFetchableLmstudioBaseUrl(resolved); try { - const parsed = new URL(resolved); + const parsed = new URL(fetchableBaseUrl); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new TypeError(`Unsupported LM Studio protocol: ${parsed.protocol}`); + } const pathname = normalizeUrlPath(parsed.pathname); parsed.pathname = pathname.length > 0 ? pathname : "/"; parsed.search = ""; diff --git a/extensions/lmstudio/src/runtime.test.ts b/extensions/lmstudio/src/runtime.test.ts index f2d33aec012..a48c962c9fa 100644 --- a/extensions/lmstudio/src/runtime.test.ts +++ b/extensions/lmstudio/src/runtime.test.ts @@ -135,6 +135,60 @@ describe("lmstudio-runtime", () => { ).resolves.toBeUndefined(); }); + it("suppresses profile runtime auth when Authorization is configured", async () => { + resolveApiKeyForProviderMock.mockResolvedValueOnce({ + apiKey: "stale-profile-key", + source: "profile:lmstudio:default", + mode: "api-key", + }); + + await expect( + resolveLmstudioRuntimeApiKey({ + config: buildLmstudioConfig({ + headers: { + Authorization: "Bearer proxy-token", + }, + }), + }), + ).resolves.toBeUndefined(); + }); + + it("suppresses env runtime auth when Authorization is configured", async () => { + resolveApiKeyForProviderMock.mockResolvedValueOnce({ + apiKey: "stale-env-key", + source: "env:LM_API_TOKEN", + mode: "api-key", + }); + + await expect( + resolveLmstudioRuntimeApiKey({ + config: buildLmstudioConfig({ + headers: { + Authorization: "Bearer proxy-token", + }, + }), + }), + ).resolves.toBeUndefined(); + }); + + it("suppresses shell env runtime auth when Authorization is configured", async () => { + resolveApiKeyForProviderMock.mockResolvedValueOnce({ + apiKey: "stale-shell-env-key", + source: "shell env: LM_API_TOKEN", + mode: "api-key", + }); + + await expect( + resolveLmstudioRuntimeApiKey({ + config: buildLmstudioConfig({ + headers: { + Authorization: "Bearer proxy-token", + }, + }), + }), + ).resolves.toBeUndefined(); + }); + it("throws when explicit api-key mode cannot resolve any key", async () => { resolveApiKeyForProviderMock.mockRejectedValue( new Error('No API key found for provider "lmstudio". Auth store: /tmp/auth-profiles.json.'), diff --git a/extensions/lmstudio/src/runtime.ts b/extensions/lmstudio/src/runtime.ts index 7cb5579a6a5..738bf6ed89f 100644 --- a/extensions/lmstudio/src/runtime.ts +++ b/extensions/lmstudio/src/runtime.ts @@ -60,6 +60,16 @@ function sanitizeStringHeaders(headers: unknown): Record | undef return Object.keys(next).length > 0 ? next : undefined; } +function shouldSuppressResolvedRuntimeApiKeyForHeaderAuth( + source: string | undefined, + hasAuthorizationHeader: boolean, +): boolean { + if (!hasAuthorizationHeader || !source) { + return false; + } + return /^profile:|^(?:shell )?env(?::|$)/.test(source); +} + export async function resolveLmstudioConfiguredApiKey(params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; @@ -230,6 +240,9 @@ export async function resolveLmstudioRuntimeApiKey(params: { if (!resolvedApiKey || resolvedApiKey.length === 0) { return await resolveConfiguredApiKeyOrThrow(); } + if (shouldSuppressResolvedRuntimeApiKeyForHeaderAuth(resolved.source, hasAuthorizationHeader)) { + return await resolveConfiguredApiKeyOrThrow(); + } if (isNonSecretApiKeyMarker(resolvedApiKey) && resolvedApiKey !== CUSTOM_LOCAL_AUTH_MARKER) { return await resolveConfiguredApiKeyOrThrow(); }