From e96e46660d449fbd156529488e40bb26ef47afd1 Mon Sep 17 00:00:00 2001 From: Khaled Sulayman Date: Wed, 24 Jun 2026 22:29:35 -0400 Subject: [PATCH] fix: inject placeholder API key for keyless custom endpoints Custom endpoints that don't require authentication (e.g., vLLM InferenceService) fail at startup because no apiKey ref is set in the provider config and the auth store is never populated. When a custom endpoint is configured without an API key, inject a placeholder value ("no-key-required") so OpenClaw's auth requirement is satisfied. Applies to both local and Kubernetes deployers. Fixes #166 Co-Authored-By: Claude Opus 4.6 --- .../deployers/__tests__/k8s-helpers.test.ts | 30 +++++++++++++++++-- .../deployers/__tests__/k8s-manifests.test.ts | 26 ++++++++++++++++ src/server/deployers/k8s-helpers.ts | 3 +- src/server/deployers/k8s-manifests.ts | 7 ++++- src/server/deployers/local.ts | 5 +++- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/server/deployers/__tests__/k8s-helpers.test.ts b/src/server/deployers/__tests__/k8s-helpers.test.ts index 7f33a92..c82ea9b 100644 --- a/src/server/deployers/__tests__/k8s-helpers.test.ts +++ b/src/server/deployers/__tests__/k8s-helpers.test.ts @@ -934,7 +934,7 @@ describe("model config generation", () => { }); }); - it("uses the local no-auth marker for unauthenticated model endpoints", () => { + it("injects placeholder apiKey ref for keyless custom endpoints", () => { const config = makeConfig({ inferenceProvider: "custom-endpoint", openaiApiKey: "openai-key", @@ -945,7 +945,7 @@ describe("model config generation", () => { const rendered = buildOpenClawConfig(config, "gateway-token") as { models?: { providers?: Record; @@ -954,7 +954,11 @@ describe("model config generation", () => { expect(rendered.models?.providers?.endpoint?.baseUrl).toBe("http://localhost:8080/v1"); expect(rendered.models?.providers?.endpoint?.api).toBe("openai-completions"); - expect(rendered.models?.providers?.endpoint?.apiKey).toBeUndefined(); + expect(rendered.models?.providers?.endpoint?.apiKey).toEqual({ + source: "env", + provider: "default", + id: "MODEL_ENDPOINT_API_KEY", + }); }); it("adds installer-provided provider models to the OpenClaw picker allowlist", () => { @@ -1530,4 +1534,24 @@ describe("MCP servers from agent source", () => { expect(rendered.mcp).toBeUndefined(); }); + + it("sets apiKey secret ref for keyless custom endpoints", () => { + const config = makeConfig({ + inferenceProvider: "custom-endpoint", + modelEndpoint: "http://vllm.local:8000/v1", + modelEndpointModel: "mistral-small", + }); + + const rendered = buildOpenClawConfig(config, "gateway-token") as { + models?: { + providers?: Record; + }; + }; + + expect(rendered.models?.providers?.endpoint?.apiKey).toEqual({ + source: "env", + provider: "default", + id: "MODEL_ENDPOINT_API_KEY", + }); + }); }); diff --git a/src/server/deployers/__tests__/k8s-manifests.test.ts b/src/server/deployers/__tests__/k8s-manifests.test.ts index fa91fb6..c33fb58 100644 --- a/src/server/deployers/__tests__/k8s-manifests.test.ts +++ b/src/server/deployers/__tests__/k8s-manifests.test.ts @@ -767,4 +767,30 @@ describe("litellm sidecar env vars in proxy mode", () => { expect(gwEnvNames).toContain("OPENAI_API_KEY"); expect(gwEnvNames).toContain("ANTHROPIC_API_KEY"); }); + + it("injects placeholder MODEL_ENDPOINT_API_KEY in Secret for keyless custom endpoints", () => { + const config = makeConfig({ + inferenceProvider: "custom-endpoint", + modelEndpoint: "http://vllm.local:8000/v1", + modelEndpointModel: "mistral-small", + }); + + const secret = secretManifest("ns", config, "gateway-token"); + + expect(secret.stringData?.MODEL_ENDPOINT_API_KEY).toBe("no-key-required"); + expect(secret.stringData?.MODEL_ENDPOINT).toBe("http://vllm.local:8000/v1"); + }); + + it("uses real API key in Secret when provided for custom endpoints", () => { + const config = makeConfig({ + inferenceProvider: "custom-endpoint", + modelEndpoint: "http://vllm.local:8000/v1", + modelEndpointApiKey: "real-key", + modelEndpointModel: "mistral-small", + }); + + const secret = secretManifest("ns", config, "gateway-token"); + + expect(secret.stringData?.MODEL_ENDPOINT_API_KEY).toBe("real-key"); + }); }); diff --git a/src/server/deployers/k8s-helpers.ts b/src/server/deployers/k8s-helpers.ts index e23e4e5..b6c041c 100644 --- a/src/server/deployers/k8s-helpers.ts +++ b/src/server/deployers/k8s-helpers.ts @@ -36,6 +36,7 @@ export const DEFAULT_IMAGE = process.env.OPENCLAW_IMAGE || "ghcr.io/openclaw/ope export const DEFAULT_OPENSHELL_IMAGE = process.env.OPENCLAW_OPENSHELL_IMAGE || "quay.io/sallyom/openclaw:latest"; export const DEFAULT_VERTEX_IMAGE = process.env.OPENCLAW_VERTEX_IMAGE || DEFAULT_IMAGE; export const CUSTOM_ENDPOINT_PROVIDER = "endpoint"; +export const KEYLESS_ENDPOINT_PLACEHOLDER = "no-key-required"; export const GOOGLE_PROVIDER = "google"; export const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; export const OPENAI_BASE_URL = "https://api.openai.com/v1"; @@ -705,7 +706,7 @@ function attachSecretHandlingConfig(ocConfig: Record, config: D const openrouterApiKeyRef = resolveEffectiveOpenRouterApiKeyRef(config); const modelEndpointApiKeyRef = hasSecretRef(config.modelEndpointApiKeyRef) ? config.modelEndpointApiKeyRef - : config.modelEndpointApiKey + : (config.modelEndpointApiKey || config.modelEndpoint?.trim()) ? envSecretRef("MODEL_ENDPOINT_API_KEY") : undefined; if (openaiApiKeyRef) { diff --git a/src/server/deployers/k8s-manifests.ts b/src/server/deployers/k8s-manifests.ts index 5cf1d22..7749798 100644 --- a/src/server/deployers/k8s-manifests.ts +++ b/src/server/deployers/k8s-manifests.ts @@ -6,6 +6,7 @@ import { buildOpenClawConfig, buildManagedAgentAuthProfilesSecretJson, resolveEnvSecretRefId, + KEYLESS_ENDPOINT_PLACEHOLDER, } from "./k8s-helpers.js"; import type { DeployConfig } from "./types.js"; import { shouldUseLitellmProxy, LITELLM_IMAGE, LITELLM_PORT } from "./litellm.js"; @@ -416,7 +417,11 @@ export function secretManifest(ns: string, config: DeployConfig, gatewayToken: s data[openrouterEnvRefId] = config.openrouterApiKey; } if (config.modelEndpoint) data.MODEL_ENDPOINT = config.modelEndpoint; - if (config.modelEndpointApiKey) data.MODEL_ENDPOINT_API_KEY = config.modelEndpointApiKey; + if (config.modelEndpointApiKey) { + data.MODEL_ENDPOINT_API_KEY = config.modelEndpointApiKey; + } else if (config.modelEndpoint) { + data.MODEL_ENDPOINT_API_KEY = KEYLESS_ENDPOINT_PLACEHOLDER; + } const authProfilesJson = buildManagedAgentAuthProfilesSecretJson(config); if (authProfilesJson) data[CODEX_AUTH_PROFILES_SECRET_KEY] = authProfilesJson; const telegramEnvRefId = resolveEnvSecretRefId(config.telegramBotTokenRef, "TELEGRAM_BOT_TOKEN"); diff --git a/src/server/deployers/local.ts b/src/server/deployers/local.ts index 5379b8a..5d8145f 100644 --- a/src/server/deployers/local.ts +++ b/src/server/deployers/local.ts @@ -44,6 +44,7 @@ import { buildVaultSecretProviderConfig, buildConfiguredAgentModelCatalog, CUSTOM_ENDPOINT_PROVIDER, + KEYLESS_ENDPOINT_PLACEHOLDER, GOOGLE_BASE_URL, GOOGLE_PROVIDER, OPENAI_BASE_URL, @@ -823,7 +824,7 @@ function attachSecretHandlingConfig(ocConfig: Record, config: D const modelEndpointApiKeyRef = hasSecretRef(config.modelEndpointApiKeyRef) ? config.modelEndpointApiKeyRef : ( - config.modelEndpointApiKey || hasPodmanSecretTarget(config.podmanSecretMappings, "MODEL_ENDPOINT_API_KEY") + config.modelEndpointApiKey || hasPodmanSecretTarget(config.podmanSecretMappings, "MODEL_ENDPOINT_API_KEY") || config.modelEndpoint?.trim() ) ? envSecretRef("MODEL_ENDPOINT_API_KEY") : undefined; @@ -1287,6 +1288,8 @@ export function buildRunArgs( } if (effectiveConfig.modelEndpointApiKey) { env.MODEL_ENDPOINT_API_KEY = effectiveConfig.modelEndpointApiKey; + } else if (effectiveConfig.modelEndpoint?.trim()) { + env.MODEL_ENDPOINT_API_KEY = KEYLESS_ENDPOINT_PLACEHOLDER; } if (effectiveConfig.vaultSecretsEnabled) { if (effectiveConfig.vaultAddr) {