Skip to content
Closed
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
11 changes: 10 additions & 1 deletion src/services/worker/OpenRouterAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class OpenRouterAgent {
// Get OpenRouter configuration
const { apiKey, model, siteUrl, appName } = this.getOpenRouterConfig();

if (!apiKey) {
if (!apiKey || !apiKey.trim()) {
throw new Error('OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.');
}

Expand Down Expand Up @@ -370,6 +370,15 @@ export class OpenRouterAgent {
estimatedTokens
});

// Validate API key before making the request to produce a clear error
// instead of a cryptic 401 with "Bearer " (empty token)
if (!apiKey || !apiKey.trim()) {
throw new Error(
'OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in ~/.claude-mem/settings.json ' +
'or set OPENROUTER_API_KEY in ~/.claude-mem/.env'
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const response = await fetch(OPENROUTER_API_URL, {
method: 'POST',
headers: {
Expand Down
19 changes: 17 additions & 2 deletions src/shared/EnvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,23 @@ export const ENV_FILE_PATH = join(DATA_DIR, '.env');
// All other env vars (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, system vars, etc.)
// are passed through to avoid breaking CLI authentication, proxies, and platform features.
const BLOCKED_ENV_VARS = [
'ANTHROPIC_API_KEY', // Issue #733: Prevent auto-discovery from project .env files
'CLAUDECODE', // Prevent "cannot be launched inside another Claude Code session" error
'ANTHROPIC_API_KEY', // Issue #733: Prevent auto-discovery from project .env files
'ANTHROPIC_AUTH_TOKEN', // Strip ambient values so managed credentials take precedence
'CLAUDECODE', // Prevent "cannot be launched inside another Claude Code session" error
];

// Credential keys that claude-mem manages
export const MANAGED_CREDENTIAL_KEYS = [
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'GEMINI_API_KEY',
'OPENROUTER_API_KEY',
];

export interface ClaudeMemEnv {
// Credentials (optional - empty means use CLI billing for Claude)
ANTHROPIC_API_KEY?: string;
ANTHROPIC_AUTH_TOKEN?: string;
ANTHROPIC_BASE_URL?: string;
GEMINI_API_KEY?: string;
OPENROUTER_API_KEY?: string;
Expand Down Expand Up @@ -116,6 +119,7 @@ export function loadClaudeMemEnv(): ClaudeMemEnv {
// Only return managed credential keys
const result: ClaudeMemEnv = {};
if (parsed.ANTHROPIC_API_KEY) result.ANTHROPIC_API_KEY = parsed.ANTHROPIC_API_KEY;
if (parsed.ANTHROPIC_AUTH_TOKEN) result.ANTHROPIC_AUTH_TOKEN = parsed.ANTHROPIC_AUTH_TOKEN;
if (parsed.ANTHROPIC_BASE_URL) result.ANTHROPIC_BASE_URL = parsed.ANTHROPIC_BASE_URL;
if (parsed.GEMINI_API_KEY) result.GEMINI_API_KEY = parsed.GEMINI_API_KEY;
if (parsed.OPENROUTER_API_KEY) result.OPENROUTER_API_KEY = parsed.OPENROUTER_API_KEY;
Expand Down Expand Up @@ -156,6 +160,13 @@ export function saveClaudeMemEnv(env: ClaudeMemEnv): void {
delete updated.ANTHROPIC_API_KEY;
}
}
if (env.ANTHROPIC_AUTH_TOKEN !== undefined) {
if (env.ANTHROPIC_AUTH_TOKEN) {
updated.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_AUTH_TOKEN;
} else {
delete updated.ANTHROPIC_AUTH_TOKEN;
}
}
if (env.ANTHROPIC_BASE_URL !== undefined) {
if (env.ANTHROPIC_BASE_URL) {
updated.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL;
Expand Down Expand Up @@ -226,6 +237,10 @@ export function buildIsolatedEnv(includeCredentials: boolean = true): Record<str
if (credentials.ANTHROPIC_API_KEY) {
isolatedEnv.ANTHROPIC_API_KEY = credentials.ANTHROPIC_API_KEY;
}
// Forward ANTHROPIC_AUTH_TOKEN if configured in claude-mem's .env
if (credentials.ANTHROPIC_AUTH_TOKEN) {
isolatedEnv.ANTHROPIC_AUTH_TOKEN = credentials.ANTHROPIC_AUTH_TOKEN;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Override ANTHROPIC_BASE_URL from .env if configured
// This ensures the SDK subprocess uses a stable API endpoint instead of
// inheriting a dynamic local proxy port that may become stale
Expand Down
144 changes: 140 additions & 4 deletions src/shared/SettingsDefaultsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export interface SettingsDefaults {
CLAUDE_MEM_CHROMA_DATABASE: string;
}

/** Prevents repeated console.log/warn on every get('CLAUDE_MEM_MODEL') call */
const loggedProviderMappings = new Set<string>();

export class SettingsDefaultsManager {
/**
* Default values for all settings
Expand Down Expand Up @@ -149,6 +152,123 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_CHROMA_DATABASE: 'default_database',
};

/**
* Cloud provider model ID mappings.
* When CLAUDE_CODE_USE_BEDROCK or CLAUDE_CODE_USE_VERTEX are set,
* the default Anthropic model identifier must be translated to the
* provider-specific format.
*/
private static readonly BEDROCK_MODEL_MAP: Record<string, string> = {
'claude-sonnet-4-6': 'anthropic.claude-sonnet-4-6-v1:0',
'claude-sonnet-4-5': 'anthropic.claude-sonnet-4-5-v1:0',
'claude-haiku-3-5': 'anthropic.claude-haiku-3-5-v1:0',
};

private static readonly VERTEX_MODEL_MAP: Record<string, string> = {
'claude-sonnet-4-6': 'claude-sonnet-4-6@20250514',
'claude-sonnet-4-5': 'claude-sonnet-4-5@20250514',
'claude-haiku-3-5': 'claude-haiku-3-5@20250514',
};

/**
* Detect active cloud provider from environment variables.
* Returns 'bedrock', 'vertex', or null for direct Anthropic API.
*/
static detectCloudProvider(): 'bedrock' | 'vertex' | null {
if (process.env.CLAUDE_CODE_USE_BEDROCK === '1' || process.env.CLAUDE_CODE_USE_BEDROCK === 'true') {
return 'bedrock';
}
if (process.env.CLAUDE_CODE_USE_VERTEX === '1' || process.env.CLAUDE_CODE_USE_VERTEX === 'true') {
return 'vertex';
}
return null;
}

/**
* Map a model identifier to its cloud-provider-specific equivalent.
* If the user has explicitly set CLAUDE_MEM_MODEL via env or settings, that value
* is used as-is (the user knows their provider format).
* Otherwise, when a cloud provider is detected, the default model ID is translated.
* If no mapping exists, a warning is logged suggesting the user set CLAUDE_MEM_MODEL.
*/
static resolveModelForCloudProvider(modelId: string): string {
const provider = this.detectCloudProvider();
if (!provider) return modelId;

// If the user explicitly set CLAUDE_MEM_MODEL, trust their value
if (process.env.CLAUDE_MEM_MODEL) return modelId;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Only remap the model when it matches the hardcoded default.
// If the user explicitly configured a different model in settings.json,
// they know their provider format — don't override their choice.
if (modelId !== this.DEFAULTS.CLAUDE_MEM_MODEL) return modelId;

const map = provider === 'bedrock' ? this.BEDROCK_MODEL_MAP : this.VERTEX_MODEL_MAP;
const mapped = map[modelId];

const logKey = `${provider}|${modelId}`;

if (mapped) {
if (!loggedProviderMappings.has(logKey)) {
loggedProviderMappings.add(logKey);
console.log(`[SETTINGS] Cloud provider "${provider}" detected — mapping model "${modelId}" → "${mapped}"`);
}
return mapped;
}

// Model not in our mapping table — warn and pass through
if (!loggedProviderMappings.has(logKey)) {
loggedProviderMappings.add(logKey);
console.warn(
`[SETTINGS] Cloud provider "${provider}" detected but no mapping for model "${modelId}". ` +
`Set CLAUDE_MEM_MODEL to your provider-specific model ID (e.g. ${provider === 'bedrock' ? '"anthropic.claude-sonnet-4-6-v1:0"' : '"claude-sonnet-4-6@20250514"'}).`
);
}
return modelId;
}

/**
* Check cloud provider auth readiness at startup.
* When Vertex AI is detected, OAuth/ANTHROPIC_API_KEY validation should be skipped
* because Vertex uses Google Cloud credentials instead.
* Logs a warning if the expected credentials for the detected provider are missing.
*/
static validateCloudProviderAuth(settings: SettingsDefaults): void {
const provider = this.detectCloudProvider();
if (!provider) return;

if (provider === 'vertex') {
// Vertex AI uses Google Cloud ADC — OAuth token and ANTHROPIC_API_KEY are not required
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS && !process.env.GOOGLE_CLOUD_PROJECT) {
console.warn(
'[SETTINGS] Vertex AI detected (CLAUDE_CODE_USE_VERTEX) but neither GOOGLE_APPLICATION_CREDENTIALS ' +
'nor GOOGLE_CLOUD_PROJECT is set. Vertex auth may fail. Run "gcloud auth application-default login" ' +
'or set GOOGLE_APPLICATION_CREDENTIALS to your service account key.'
);
}
// Override auth method: CLI OAuth is irrelevant for Vertex
if (settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD === 'cli') {
settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD = 'api';
console.log('[SETTINGS] Vertex AI detected — auth method overridden from "cli" to "api" (Google ADC used instead of Anthropic OAuth)');
}
}

if (provider === 'bedrock') {
// Bedrock uses AWS IAM — OAuth token and ANTHROPIC_API_KEY are not required
if (!process.env.AWS_ACCESS_KEY_ID && !process.env.AWS_PROFILE) {
console.warn(
'[SETTINGS] Bedrock detected (CLAUDE_CODE_USE_BEDROCK) but no AWS credentials found. ' +
'Set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, and AWS_REGION.'
);
}
// Override auth method: CLI OAuth is irrelevant for Bedrock
if (settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD === 'cli') {
settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD = 'api';
console.log('[SETTINGS] Bedrock detected — auth method overridden from "cli" to "api" (AWS IAM used instead of Anthropic OAuth)');
}
}
}

/**
* Get all defaults as an object
*/
Expand All @@ -165,7 +285,12 @@ export class SettingsDefaultsManager {
* respects environment variable overrides that were previously ignored.
*/
static get(key: keyof SettingsDefaults): string {
return process.env[key] ?? this.DEFAULTS[key];
const value = process.env[key] ?? this.DEFAULTS[key];
// Resolve cloud-provider-specific model ID when reading the model setting
if (key === 'CLAUDE_MEM_MODEL') {
return this.resolveModelForCloudProvider(value);
}
return value;
Comment on lines 287 to +293
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Console log spam on every get('CLAUDE_MEM_MODEL') call

resolveModelForCloudProvider unconditionally calls console.log/console.warn each time it runs, and get('CLAUDE_MEM_MODEL') is the hot path for reading any model setting. Every call throughout the application's lifecycle will emit the mapping message (or warning), flooding the console rather than logging once at startup.

Consider logging only when loadFromFile resolves the model (startup), or use a boolean flag to log the mapping/warning only once.

Fix in Claude Code

}

/**
Expand Down Expand Up @@ -225,7 +350,10 @@ export class SettingsDefaultsManager {
console.warn('[SETTINGS] Failed to create settings file, using in-memory defaults:', settingsPath, error);
}
// Still apply env var overrides even when file doesn't exist
return this.applyEnvOverrides(defaults);
const final = this.applyEnvOverrides(defaults);
final.CLAUDE_MEM_MODEL = this.resolveModelForCloudProvider(final.CLAUDE_MEM_MODEL);
this.validateCloudProviderAuth(final);
return final;
}

const settingsData = readFileSync(settingsPath, 'utf-8');
Expand Down Expand Up @@ -256,11 +384,19 @@ export class SettingsDefaultsManager {
}

// Apply environment variable overrides (highest priority)
return this.applyEnvOverrides(result);
const final = this.applyEnvOverrides(result);
// Resolve cloud-provider-specific model ID
final.CLAUDE_MEM_MODEL = this.resolveModelForCloudProvider(final.CLAUDE_MEM_MODEL);
// Validate cloud provider auth and adjust auth method if needed
this.validateCloudProviderAuth(final);
return final;
} catch (error) {
console.warn('[SETTINGS] Failed to load settings, using defaults:', settingsPath, error);
// Still apply env var overrides even on error
return this.applyEnvOverrides(this.getAllDefaults());
const final = this.applyEnvOverrides(this.getAllDefaults());
final.CLAUDE_MEM_MODEL = this.resolveModelForCloudProvider(final.CLAUDE_MEM_MODEL);
this.validateCloudProviderAuth(final);
return final;
}
}
}
17 changes: 17 additions & 0 deletions src/supervisor/env-sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ export const ENV_EXACT_MATCHES = new Set([
export const ENV_PRESERVE = new Set([
'CLAUDE_CODE_OAUTH_TOKEN',
'CLAUDE_CODE_GIT_BASH_PATH',
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
]);

/** Cloud provider credential vars that must be forwarded to SDK subprocesses */
export const CLOUD_PROVIDER_ENV_VARS = new Set([
// AWS (Bedrock)
'AWS_REGION',
'AWS_DEFAULT_REGION',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_SESSION_TOKEN',
'AWS_PROFILE',
// Google Cloud (Vertex AI)
'GOOGLE_APPLICATION_CREDENTIALS',
'GOOGLE_CLOUD_PROJECT',
]);

export function sanitizeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
Expand All @@ -18,6 +34,7 @@ export function sanitizeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.Proces
for (const [key, value] of Object.entries(env)) {
if (value === undefined) continue;
if (ENV_PRESERVE.has(key)) { sanitized[key] = value; continue; }
if (CLOUD_PROVIDER_ENV_VARS.has(key)) { sanitized[key] = value; continue; }
if (ENV_EXACT_MATCHES.has(key)) continue;
if (ENV_PREFIXES.some(prefix => key.startsWith(prefix))) continue;
sanitized[key] = value;
Expand Down
Loading