[codex] Fix LM Studio header-auth follow-ups (#65806)

* fix: harden lmstudio header auth handling

* fix: suppress lmstudio shell env auth
This commit is contained in:
Frank Yang
2026-04-13 17:45:06 +08:00
committed by GitHub
parent abe33319d3
commit 431db078f2
4 changed files with 93 additions and 1 deletions

View File

@@ -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", () => {

View File

@@ -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 = "";

View File

@@ -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.'),

View File

@@ -60,6 +60,16 @@ function sanitizeStringHeaders(headers: unknown): Record<string, string> | 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();
}