mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
[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:
@@ -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", () => {
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user