From 55f3999d695ab2f7f30c106923f660da052edbae Mon Sep 17 00:00:00 2001 From: Zavian <36817799+Zavianx@users.noreply.github.com> Date: Tue, 12 May 2026 19:22:53 +0800 Subject: [PATCH] fix: ignore placeholder API keys for optional tools --- src/memory/embeddings.test.ts | 60 ++++++++++++++++++++++++++++++ src/memory/embeddings.ts | 9 +++-- src/tools/registry.test.ts | 70 +++++++++++++++++++++++++++++++++++ src/tools/registry.ts | 10 ++--- src/tools/search/x-search.ts | 7 +++- 5 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 src/memory/embeddings.test.ts create mode 100644 src/tools/registry.test.ts diff --git a/src/memory/embeddings.test.ts b/src/memory/embeddings.test.ts new file mode 100644 index 000000000..9a7988bdc --- /dev/null +++ b/src/memory/embeddings.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createEmbeddingClient } from './embeddings.js'; + +const API_KEY_ENV_VARS = [ + 'OPENAI_API_KEY', + 'GOOGLE_API_KEY', + 'OLLAMA_BASE_URL', +]; + +describe('createEmbeddingClient', () => { + const originalCwd = process.cwd(); + let tempDir = ''; + let originalEnv: Record = {}; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'dexter-embeddings-')); + process.chdir(tempDir); + originalEnv = {}; + + for (const key of API_KEY_ENV_VARS) { + originalEnv[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + process.chdir(originalCwd); + + for (const key of API_KEY_ENV_VARS) { + const original = originalEnv[key]; + if (original === undefined) { + delete process.env[key]; + } else { + process.env[key] = original; + } + } + + rmSync(tempDir, { recursive: true, force: true }); + }); + + test('does not create an OpenAI embedding client from a placeholder key', () => { + process.env.OPENAI_API_KEY = 'your-openai-api-key'; + + const client = createEmbeddingClient({ provider: 'openai' }); + + expect(client).toBeNull(); + }); + + test('auto provider ignores placeholder OpenAI key and uses Gemini when configured', () => { + process.env.OPENAI_API_KEY = 'your-openai-api-key'; + process.env.GOOGLE_API_KEY = 'google-test-key'; + + const client = createEmbeddingClient({ provider: 'auto' }); + + expect(client?.provider).toBe('gemini'); + }); +}); diff --git a/src/memory/embeddings.ts b/src/memory/embeddings.ts index 5ff2e2ac7..5c16f7e6a 100644 --- a/src/memory/embeddings.ts +++ b/src/memory/embeddings.ts @@ -2,6 +2,7 @@ import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai'; import { OllamaEmbeddings } from '@langchain/ollama'; import { OpenAIEmbeddings } from '@langchain/openai'; import type { EmbeddingProviderId, MemoryEmbeddingClient } from './types.js'; +import { checkApiKeyExists } from '../utils/env.js'; const DEFAULT_OPENAI_MODEL = 'text-embedding-3-small'; const DEFAULT_GEMINI_MODEL = 'gemini-embedding-001'; @@ -28,10 +29,10 @@ function withTimeout(promise: Promise, ms: number, message: string): Promi } function resolveProvider(preferred: EmbeddingProviderId): ResolvedProvider | null { - if (preferred === 'openai' && process.env.OPENAI_API_KEY) { + if (preferred === 'openai' && checkApiKeyExists('OPENAI_API_KEY')) { return 'openai'; } - if (preferred === 'gemini' && process.env.GOOGLE_API_KEY) { + if (preferred === 'gemini' && checkApiKeyExists('GOOGLE_API_KEY')) { return 'gemini'; } if (preferred === 'ollama') { @@ -39,10 +40,10 @@ function resolveProvider(preferred: EmbeddingProviderId): ResolvedProvider | nul } if (preferred === 'auto') { - if (process.env.OPENAI_API_KEY) { + if (checkApiKeyExists('OPENAI_API_KEY')) { return 'openai'; } - if (process.env.GOOGLE_API_KEY) { + if (checkApiKeyExists('GOOGLE_API_KEY')) { return 'gemini'; } if (process.env.OLLAMA_BASE_URL) { diff --git a/src/tools/registry.test.ts b/src/tools/registry.test.ts new file mode 100644 index 000000000..9014f7a31 --- /dev/null +++ b/src/tools/registry.test.ts @@ -0,0 +1,70 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { getToolRegistry } from './registry.js'; + +const API_KEY_ENV_VARS = [ + 'EXASEARCH_API_KEY', + 'PERPLEXITY_API_KEY', + 'TAVILY_API_KEY', + 'X_BEARER_TOKEN', +]; + +function registeredToolNames(): string[] { + return getToolRegistry('gpt-5.5').map((tool) => tool.name); +} + +describe('getToolRegistry', () => { + const originalCwd = process.cwd(); + let tempDir = ''; + let originalEnv: Record = {}; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'dexter-tool-registry-')); + process.chdir(tempDir); + originalEnv = {}; + + for (const key of API_KEY_ENV_VARS) { + originalEnv[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + process.chdir(originalCwd); + + for (const key of API_KEY_ENV_VARS) { + const original = originalEnv[key]; + if (original === undefined) { + delete process.env[key]; + } else { + process.env[key] = original; + } + } + + rmSync(tempDir, { recursive: true, force: true }); + }); + + test('does not register search tools for env.example placeholder keys', () => { + process.env.EXASEARCH_API_KEY = 'your-exa-api-key'; + process.env.PERPLEXITY_API_KEY = 'your-perplexity-api-key'; + process.env.TAVILY_API_KEY = 'your-tavily-api-key'; + process.env.X_BEARER_TOKEN = 'your-X-bearer-token'; + + const names = registeredToolNames(); + + expect(names).not.toContain('web_search'); + expect(names).not.toContain('x_search'); + }); + + test('registers search tools when usable keys are configured', () => { + process.env.TAVILY_API_KEY = 'tvly-test-key'; + process.env.X_BEARER_TOKEN = 'x-test-token'; + + const names = registeredToolNames(); + + expect(names).toContain('web_search'); + expect(names).toContain('x_search'); + }); +}); diff --git a/src/tools/registry.ts b/src/tools/registry.ts index 52460c7be..dbca4c65b 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -3,7 +3,7 @@ import { createGetFinancials, createGetMarketData, createReadFilings, createScre import { exaSearch, perplexitySearch, tavilySearch, WEB_SEARCH_DESCRIPTION, xSearchTool, X_SEARCH_DESCRIPTION } from './search/index.js'; import { createWebSearchTool, type WebSearchProvider } from './search/web-search.js'; import { getSetting } from '../utils/config.js'; -import type { SearchProviderId } from '../utils/env.js'; +import { checkApiKeyExists, checkApiKeyForSearchProvider, type SearchProviderId } from '../utils/env.js'; import { skillTool, SKILL_TOOL_DESCRIPTION } from './skill.js'; import { webFetchTool, WEB_FETCH_DESCRIPTION } from './fetch/web-fetch.js'; import { browserTool, BROWSER_DESCRIPTION } from './browser/browser.js'; @@ -147,13 +147,13 @@ export function getToolRegistry(model: string): RegisteredTool[] { // Build web_search as a fallback chain over whichever providers have keys configured. // The user's preferred provider (set via /search) is tried first; the others act as fallbacks. const allWebSearchProviders: WebSearchProvider[] = []; - if (process.env.EXASEARCH_API_KEY) { + if (checkApiKeyForSearchProvider('exa')) { allWebSearchProviders.push({ id: 'exa', name: 'Exa', tool: exaSearch }); } - if (process.env.PERPLEXITY_API_KEY) { + if (checkApiKeyForSearchProvider('perplexity')) { allWebSearchProviders.push({ id: 'perplexity', name: 'Perplexity', tool: perplexitySearch }); } - if (process.env.TAVILY_API_KEY) { + if (checkApiKeyForSearchProvider('tavily')) { allWebSearchProviders.push({ id: 'tavily', name: 'Tavily', tool: tavilySearch }); } @@ -175,7 +175,7 @@ export function getToolRegistry(model: string): RegisteredTool[] { }); } - if (process.env.X_BEARER_TOKEN) { + if (checkApiKeyExists('X_BEARER_TOKEN')) { tools.push({ name: 'x_search', tool: xSearchTool, diff --git a/src/tools/search/x-search.ts b/src/tools/search/x-search.ts index 01a360c9f..c4e8e46f0 100644 --- a/src/tools/search/x-search.ts +++ b/src/tools/search/x-search.ts @@ -1,6 +1,7 @@ import { DynamicStructuredTool } from '@langchain/core/tools'; import { z } from 'zod'; import { formatToolResult } from '../types.js'; +import { checkApiKeyExists } from '@/utils/env'; const X_API_BASE = 'https://api.x.com/2'; const RATE_DELAY_MS = 350; // Delay between pagination requests to reduce rate-limit risk @@ -36,8 +37,10 @@ interface RawXResponse { function getBearerToken(): string { const token = process.env.X_BEARER_TOKEN; - if (!token) throw new Error('X_BEARER_TOKEN is not set'); - return token; + if (!checkApiKeyExists('X_BEARER_TOKEN') || !token) { + throw new Error('X_BEARER_TOKEN is not set'); + } + return token.trim(); } async function sleep(ms: number): Promise {