fix: unify live model auth gating

This commit is contained in:
Peter Steinberger
2026-03-24 04:28:13 +00:00
parent e28e520379
commit e864421d83
6 changed files with 64 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isLiveTestEnabled } from "./live-test-helpers.js";
import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "./live-test-helpers.js";
describe("isLiveTestEnabled", () => {
it("treats LIVE and OPENCLAW_LIVE_TEST as shared live gates", () => {
@@ -13,3 +13,11 @@ describe("isLiveTestEnabled", () => {
expect(isLiveTestEnabled(["MINIMAX_LIVE_TEST"], { MINIMAX_LIVE_TEST: "0" })).toBe(false);
});
});
describe("isLiveProfileKeyModeEnabled", () => {
it("only enables profile-key mode for the dedicated flag", () => {
expect(isLiveProfileKeyModeEnabled({ OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS: "1" })).toBe(true);
expect(isLiveProfileKeyModeEnabled({ OPENCLAW_LIVE_TEST: "1" })).toBe(false);
expect(isLiveProfileKeyModeEnabled({ LIVE: "1" })).toBe(false);
});
});

View File

@@ -11,6 +11,10 @@ export function isLiveTestEnabled(
);
}
export function isLiveProfileKeyModeEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
return isTruthyEnvValue(env.OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS);
}
export function createSingleUserPromptMessage(content = LIVE_OK_PROMPT) {
return [
{

View File

@@ -9,7 +9,7 @@ import {
isAnthropicRateLimitError,
} from "./live-auth-keys.js";
import { isModernModelRef } from "./live-model-filter.js";
import { isLiveTestEnabled } from "./live-test-helpers.js";
import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "./live-test-helpers.js";
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
import { shouldSuppressBuiltInModel } from "./model-suppression.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
@@ -18,7 +18,7 @@ import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
const LIVE = isLiveTestEnabled();
const DIRECT_ENABLED = Boolean(process.env.OPENCLAW_LIVE_MODELS?.trim());
const REQUIRE_PROFILE_KEYS = isLiveTestEnabled(["OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS"]);
const REQUIRE_PROFILE_KEYS = isLiveProfileKeyModeEnabled();
const LIVE_HEARTBEAT_MS = Math.max(1_000, toInt(process.env.OPENCLAW_LIVE_HEARTBEAT_MS, 30_000));
const describeLive = LIVE ? describe : describe.skip;

View File

@@ -149,4 +149,31 @@ describe("discoverAuthStorage", () => {
}
});
});
it("includes env-backed provider auth when no auth profile exists", async () => {
await withAgentDir(async (agentDir) => {
const previous = process.env.MISTRAL_API_KEY;
process.env.MISTRAL_API_KEY = "mistral-env-test-key";
try {
saveAuthProfileStore(
{
version: 1,
profiles: {},
},
agentDir,
);
const authStorage = discoverAuthStorage(agentDir);
expect(authStorage.hasAuth("mistral")).toBe(true);
await expect(authStorage.getApiKey("mistral")).resolves.toBe("mistral-env-test-key");
} finally {
if (previous === undefined) {
delete process.env.MISTRAL_API_KEY;
} else {
process.env.MISTRAL_API_KEY = previous;
}
}
});
});
});

View File

@@ -6,6 +6,8 @@ import type {
ModelRegistry as PiModelRegistry,
} from "@mariozechner/pi-coding-agent";
import { ensureAuthProfileStore } from "./auth-profiles.js";
import { PROVIDER_ENV_API_KEY_CANDIDATES } from "./model-auth-env-vars.js";
import { resolveEnvApiKey } from "./model-auth-env.js";
import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js";
const PiAuthStorageClass = PiCodingAgent.AuthStorage;
@@ -136,7 +138,24 @@ function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCred
function resolvePiCredentials(agentDir: string): PiCredentialMap {
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
return resolvePiCredentialMapFromStore(store);
const credentials = resolvePiCredentialMapFromStore(store);
// pi-coding-agent hides providers from its registry when auth storage lacks
// a matching credential entry. Mirror env-backed provider auth here so
// live/model discovery sees the same providers runtime auth can use.
for (const provider of Object.keys(PROVIDER_ENV_API_KEY_CANDIDATES)) {
if (credentials[provider]) {
continue;
}
const resolved = resolveEnvApiKey(provider);
if (!resolved?.apiKey) {
continue;
}
credentials[provider] = {
type: "api_key",
key: resolved.apiKey,
};
}
return credentials;
}
// Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed).

View File

@@ -19,7 +19,7 @@ import {
} from "../agents/live-auth-keys.js";
import { isModelNotFoundErrorMessage } from "../agents/live-model-errors.js";
import { isModernModelRef } from "../agents/live-model-filter.js";
import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "../agents/live-test-helpers.js";
import { getApiKeyForModel } from "../agents/model-auth.js";
import { normalizeGoogleModelId } from "../agents/model-id-normalization.js";
import { shouldSuppressBuiltInModel } from "../agents/model-suppression.js";
@@ -44,7 +44,7 @@ import { startGatewayServer } from "./server.js";
import { loadSessionEntry, readSessionMessages } from "./session-utils.js";
const ZAI_FALLBACK = isTruthyEnvValue(process.env.OPENCLAW_LIVE_GATEWAY_ZAI_FALLBACK);
const REQUIRE_PROFILE_KEYS = isTruthyEnvValue(process.env.OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS);
const REQUIRE_PROFILE_KEYS = isLiveProfileKeyModeEnabled();
const PROVIDERS = parseFilter(process.env.OPENCLAW_LIVE_GATEWAY_PROVIDERS);
const THINKING_LEVEL = "high";
const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;