fix(agents): accept LAN local auth markers

This commit is contained in:
Peter Steinberger
2026-04-27 10:57:28 +01:00
parent a6eb051b3a
commit fee16865b2
3 changed files with 121 additions and 18 deletions

View File

@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
- Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock `ContentBlock` blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu.
- Agents/LM Studio: promote standalone bracketed local-model tool requests into registered tool calls and hide unsupported bracket blocks from visible replies, so MemPalace MCP lookups do not print raw `[tool]` JSON scaffolding in chat. Fixes #66178. Thanks @detroit357.
- Local models: warn when an assistant reply looks like a tool call but the provider emitted plain text instead of a structured tool invocation, making fake/non-executed tool calls visible in logs. Fixes #51332. Thanks @emilclaw.
- Local models: accept persisted non-secret local auth markers for private-LAN custom OpenAI-compatible providers, so LAN Ollama configs no longer fail with missing auth when `ollama-local` is saved as the key. Fixes #49736. Thanks @charles-zh.
- TUI/local models: treat visible gateway client labels such as `openclaw-tui` as the current requester session for session-aware tools, so Ollama tool calls no longer fail by resolving the UI label as a session id. Fixes #66391. Thanks @kickingzebra.
- Local models: route self-hosted OpenAI-compatible model discovery through the guarded fetch path pinned to the configured host, covering vLLM and SGLang setup without reopening local/LAN SSRF probes. Supersedes #46359. Thanks @cdxiaodong.
- Local models: classify terminated, reset, closed, timeout, and aborted model-call failures and attach a process memory snapshot to the diagnostic event, making LM Studio/Ollama RAM-pressure failures easier to prove from stability bundles. Refs #65551. Thanks @BigWiLLi111.

View File

@@ -823,6 +823,17 @@ describe("resolveApiKeyForProvider synthetic local auth for custom providers
expect(auth.source).toContain("synthetic local key");
});
it("synthesizes a local auth marker for private LAN custom providers with no apiKey", async () => {
const auth = await resolveCustomProviderAuth(
"custom-192-168-0-222-11434",
"http://192.168.0.222:11434/v1",
"qwen3.5:9b",
"Qwen 3.5 9B",
);
expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER);
expect(auth.source).toContain("synthetic local key");
});
it("synthesizes a local auth marker for localhost custom providers", async () => {
const auth = await resolveCustomProviderAuth("my-local", "http://localhost:11434/v1");
expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER);
@@ -912,6 +923,72 @@ describe("resolveApiKeyForProvider synthetic local auth for custom providers
});
});
it("accepts non-secret local markers for private LAN custom OpenAI-compatible providers", async () => {
const auth = await resolveApiKeyForProvider({
provider: "custom-192-168-0-222-11434",
cfg: {
models: {
providers: {
"custom-192-168-0-222-11434": {
baseUrl: "http://192.168.0.222:11434/v1",
api: "openai-completions",
apiKey: "ollama-local",
models: [
{
id: "qwen3.5:9b",
name: "Qwen 3.5 9B",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 4096,
},
],
},
},
},
},
store: { version: 1, profiles: {} },
});
expect(auth).toMatchObject({
apiKey: CUSTOM_LOCAL_AUTH_MARKER,
source: "models.json (local marker)",
mode: "api-key",
});
});
it("does not accept non-secret local markers for remote custom providers", async () => {
await expect(
resolveApiKeyForProvider({
provider: "custom-remote",
cfg: {
models: {
providers: {
"custom-remote": {
baseUrl: "https://api.example.com/v1",
api: "openai-completions",
apiKey: "ollama-local",
models: [
{
id: "qwen3.5:9b",
name: "Qwen 3.5 9B",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 4096,
},
],
},
},
},
},
store: { version: 1, profiles: {} },
}),
).rejects.toThrow('No API key found for provider "custom-remote"');
});
it("does not synthesize local auth when apiKey is explicitly configured but unresolved", async () => {
const previous = process.env.OPENAI_API_KEY;
delete process.env.OPENAI_API_KEY;

View File

@@ -157,22 +157,30 @@ export function resolveUsableCustomProviderApiKey(params: {
if (!isNonSecretApiKeyMarker(customKey)) {
return { apiKey: customKey, source: "models.json" };
}
if (!isKnownEnvApiKeyMarker(customKey)) {
return null;
if (isKnownEnvApiKeyMarker(customKey)) {
const envValue = normalizeOptionalSecretInput((params.env ?? process.env)[customKey]);
if (!envValue) {
return null;
}
const applied = new Set(getShellEnvAppliedKeys());
return {
apiKey: envValue,
source: resolveEnvSourceLabel({
applied,
envVars: [customKey],
label: `${customKey} (models.json marker)`,
}),
};
}
const envValue = normalizeOptionalSecretInput((params.env ?? process.env)[customKey]);
if (!envValue) {
return null;
if (
customProviderConfig &&
isCustomLocalProviderConfig(customProviderConfig) &&
customProviderConfig.baseUrl &&
isLocalBaseUrl(customProviderConfig.baseUrl)
) {
return { apiKey: CUSTOM_LOCAL_AUTH_MARKER, source: "models.json (local marker)" };
}
const applied = new Set(getShellEnvAppliedKeys());
return {
apiKey: envValue,
source: resolveEnvSourceLabel({
applied,
envVars: [customKey],
label: `${customKey} (models.json marker)`,
}),
};
return null;
}
export function hasUsableCustomProviderApiKey(
@@ -209,20 +217,37 @@ function resolveProviderAuthOverride(
function isLocalBaseUrl(baseUrl: string): boolean {
try {
const host = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname);
let host = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname);
if (host.startsWith("[") && host.endsWith("]")) {
host = host.slice(1, -1);
}
return (
host === "localhost" ||
host === "127.0.0.1" ||
host === "0.0.0.0" ||
host === "[::1]" ||
host === "[::ffff:7f00:1]" ||
host === "[::ffff:127.0.0.1]"
host === "::1" ||
host === "::ffff:7f00:1" ||
host === "::ffff:127.0.0.1" ||
host.endsWith(".local") ||
isPrivateIpv4Host(host)
);
} catch {
return false;
}
}
function isPrivateIpv4Host(host: string): boolean {
if (!/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
return false;
}
const octets = host.split(".").map((part) => Number.parseInt(part, 10));
if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
return false;
}
const [a, b] = octets;
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
}
function hasExplicitProviderApiKeyConfig(providerConfig: ModelProviderConfig): boolean {
return (
normalizeOptionalSecretInput(providerConfig.apiKey) !== undefined ||