mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(agents): accept LAN local auth markers
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
Reference in New Issue
Block a user