Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions src/server/deployers/__tests__/k8s-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -945,7 +945,7 @@ describe("model config generation", () => {
const rendered = buildOpenClawConfig(config, "gateway-token") as {
models?: {
providers?: Record<string, {
apiKey?: unknown;
apiKey?: { source?: string; provider?: string; id?: string };
baseUrl?: string;
api?: string;
}>;
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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<string, { apiKey?: { source?: string; id?: string } }>;
};
};

expect(rendered.models?.providers?.endpoint?.apiKey).toEqual({
source: "env",
provider: "default",
id: "MODEL_ENDPOINT_API_KEY",
});
});
});
26 changes: 26 additions & 0 deletions src/server/deployers/__tests__/k8s-manifests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
3 changes: 2 additions & 1 deletion src/server/deployers/k8s-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -705,7 +706,7 @@ function attachSecretHandlingConfig(ocConfig: Record<string, unknown>, 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) {
Expand Down
7 changes: 6 additions & 1 deletion src/server/deployers/k8s-manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down
5 changes: 4 additions & 1 deletion src/server/deployers/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
buildVaultSecretProviderConfig,
buildConfiguredAgentModelCatalog,
CUSTOM_ENDPOINT_PROVIDER,
KEYLESS_ENDPOINT_PLACEHOLDER,
GOOGLE_BASE_URL,
GOOGLE_PROVIDER,
OPENAI_BASE_URL,
Expand Down Expand Up @@ -823,7 +824,7 @@ function attachSecretHandlingConfig(ocConfig: Record<string, unknown>, 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;
Expand Down Expand Up @@ -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) {
Expand Down