Skip to content
Open
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
60 changes: 60 additions & 0 deletions src/memory/embeddings.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined> = {};

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');
});
});
9 changes: 5 additions & 4 deletions src/memory/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,21 +29,21 @@ function withTimeout<T>(promise: Promise<T>, 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') {
return 'ollama';
}

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) {
Expand Down
70 changes: 70 additions & 0 deletions src/tools/registry.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined> = {};

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');
});
});
10 changes: 5 additions & 5 deletions src/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
}

Expand All @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions src/tools/search/x-search.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<void> {
Expand Down
Loading