mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06: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/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.
|
- 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: 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.
|
- 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: 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.
|
- 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");
|
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 () => {
|
it("synthesizes a local auth marker for localhost custom providers", async () => {
|
||||||
const auth = await resolveCustomProviderAuth("my-local", "http://localhost:11434/v1");
|
const auth = await resolveCustomProviderAuth("my-local", "http://localhost:11434/v1");
|
||||||
expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER);
|
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 () => {
|
it("does not synthesize local auth when apiKey is explicitly configured but unresolved", async () => {
|
||||||
const previous = process.env.OPENAI_API_KEY;
|
const previous = process.env.OPENAI_API_KEY;
|
||||||
delete process.env.OPENAI_API_KEY;
|
delete process.env.OPENAI_API_KEY;
|
||||||
|
|||||||
+43
-18
@@ -157,22 +157,30 @@ export function resolveUsableCustomProviderApiKey(params: {
|
|||||||
if (!isNonSecretApiKeyMarker(customKey)) {
|
if (!isNonSecretApiKeyMarker(customKey)) {
|
||||||
return { apiKey: customKey, source: "models.json" };
|
return { apiKey: customKey, source: "models.json" };
|
||||||
}
|
}
|
||||||
if (!isKnownEnvApiKeyMarker(customKey)) {
|
if (isKnownEnvApiKeyMarker(customKey)) {
|
||||||
return null;
|
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 (
|
||||||
if (!envValue) {
|
customProviderConfig &&
|
||||||
return null;
|
isCustomLocalProviderConfig(customProviderConfig) &&
|
||||||
|
customProviderConfig.baseUrl &&
|
||||||
|
isLocalBaseUrl(customProviderConfig.baseUrl)
|
||||||
|
) {
|
||||||
|
return { apiKey: CUSTOM_LOCAL_AUTH_MARKER, source: "models.json (local marker)" };
|
||||||
}
|
}
|
||||||
const applied = new Set(getShellEnvAppliedKeys());
|
return null;
|
||||||
return {
|
|
||||||
apiKey: envValue,
|
|
||||||
source: resolveEnvSourceLabel({
|
|
||||||
applied,
|
|
||||||
envVars: [customKey],
|
|
||||||
label: `${customKey} (models.json marker)`,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasUsableCustomProviderApiKey(
|
export function hasUsableCustomProviderApiKey(
|
||||||
@@ -209,20 +217,37 @@ function resolveProviderAuthOverride(
|
|||||||
|
|
||||||
function isLocalBaseUrl(baseUrl: string): boolean {
|
function isLocalBaseUrl(baseUrl: string): boolean {
|
||||||
try {
|
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 (
|
return (
|
||||||
host === "localhost" ||
|
host === "localhost" ||
|
||||||
host === "127.0.0.1" ||
|
host === "127.0.0.1" ||
|
||||||
host === "0.0.0.0" ||
|
host === "0.0.0.0" ||
|
||||||
host === "[::1]" ||
|
host === "::1" ||
|
||||||
host === "[::ffff:7f00:1]" ||
|
host === "::ffff:7f00:1" ||
|
||||||
host === "[::ffff:127.0.0.1]"
|
host === "::ffff:127.0.0.1" ||
|
||||||
|
host.endsWith(".local") ||
|
||||||
|
isPrivateIpv4Host(host)
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
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 {
|
function hasExplicitProviderApiKeyConfig(providerConfig: ModelProviderConfig): boolean {
|
||||||
return (
|
return (
|
||||||
normalizeOptionalSecretInput(providerConfig.apiKey) !== undefined ||
|
normalizeOptionalSecretInput(providerConfig.apiKey) !== undefined ||
|
||||||
|
|||||||
Reference in New Issue
Block a user