UNPKG

socraticode

Version:

SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.

138 lines • 6.13 kB
// SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited /** * OpenAI embedding provider. * * Uses the OpenAI Embeddings API (text-embedding-3-small by default). * * Required env: * EMBEDDING_PROVIDER=openai * OPENAI_API_KEY=sk-... * * Optional env: * EMBEDDING_MODEL=text-embedding-3-small (default) * EMBEDDING_DIMENSIONS=1536 (default for text-embedding-3-small) */ import OpenAI from "openai"; import { getEmbeddingConfig } from "./embedding-config.js"; import { logger } from "./logger.js"; // ── Constants ─────────────────────────────────────────────────────────── /** * OpenAI embeddings API accepts up to 2048 inputs per request, * but we use a conservative batch size to stay within rate limits. */ const OPENAI_BATCH_SIZE = 512; /** Max input tokens for OpenAI embedding models (8191 for text-embedding-3-*). */ const OPENAI_MAX_TOKENS = 8191; /** * Conservative chars-per-token estimate for code. * OpenAI tokenizers average ~4 chars/token for English, * but code has more single-char tokens. Using 3.0 for safety. */ const CHARS_PER_TOKEN_ESTIMATE = 3.0; // ── Client management ─────────────────────────────────────────────────── let openaiClient = null; function getClient() { if (!openaiClient) { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OPENAI_API_KEY environment variable is required when using the OpenAI embedding provider. " + "Set it in your MCP config env block."); } openaiClient = new OpenAI({ apiKey }); } return openaiClient; } /** Reset client (for testing). */ export function resetOpenAIClient() { openaiClient = null; } // ── Pre-truncation ────────────────────────────────────────────────────── function pretruncateTexts(texts, contextLength) { if (contextLength <= 0) return texts; const maxChars = Math.floor(contextLength * CHARS_PER_TOKEN_ESTIMATE); return texts.map((t) => (t.length > maxChars ? t.substring(0, maxChars) : t)); } // ── Provider class ────────────────────────────────────────────────────── export class OpenAIEmbeddingProvider { name = "openai"; async ensureReady() { // Validate API key is present getClient(); // Verify connectivity with a minimal request try { const client = getClient(); await client.models.list(); logger.info("OpenAI embedding provider ready", { model: getEmbeddingConfig().embeddingModel, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`OpenAI API is not reachable: ${message}`); } // Cloud providers don't pull models / start containers return { modelPulled: false, containerStarted: false, imagePulled: false }; } async embed(texts) { if (texts.length === 0) return []; const config = getEmbeddingConfig(); const client = getClient(); const truncated = pretruncateTexts(texts, config.embeddingContextLength || OPENAI_MAX_TOKENS); // OpenAI supports batching natively — send all at once if within limits if (truncated.length <= OPENAI_BATCH_SIZE) { return this._embedBatch(client, truncated, config.embeddingModel, config.embeddingDimensions); } // Split into sub-batches const results = []; for (let i = 0; i < truncated.length; i += OPENAI_BATCH_SIZE) { const batch = truncated.slice(i, i + OPENAI_BATCH_SIZE); const embeddings = await this._embedBatch(client, batch, config.embeddingModel, config.embeddingDimensions); results.push(...embeddings); } return results; } async embedSingle(text) { const results = await this.embed([text]); if (results.length === 0) { throw new Error("Embedding failed: no result returned"); } return results[0]; } async healthCheck() { const config = getEmbeddingConfig(); const lines = []; const icon = (ok) => (ok ? "[OK]" : "[MISSING]"); const hasKey = !!process.env.OPENAI_API_KEY; lines.push(`${icon(hasKey)} OpenAI API key: ${hasKey ? "Configured" : "Missing — set OPENAI_API_KEY in your MCP config"}`); if (!hasKey) { return { available: false, modelReady: false, statusLines: lines }; } try { const client = getClient(); await client.models.list(); lines.push(`${icon(true)} OpenAI API: Reachable`); lines.push(`${icon(true)} Embedding model: ${config.embeddingModel}`); return { available: true, modelReady: true, statusLines: lines }; } catch (err) { const message = err instanceof Error ? err.message : String(err); lines.push(`${icon(false)} OpenAI API: ${message}`); return { available: false, modelReady: false, statusLines: lines }; } } async _embedBatch(client, texts, model, dimensions) { const supportsDimensions = model.startsWith("text-embedding-3"); const response = await client.embeddings.create({ model, input: texts, ...(supportsDimensions ? { dimensions } : {}), }); // OpenAI returns embeddings sorted by index, but we ensure order const sorted = response.data.sort((a, b) => a.index - b.index); return sorted.map((d) => d.embedding); } } //# sourceMappingURL=provider-openai.js.map