opencode-codebase-index 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -854,7 +854,32 @@ function parseConfig(raw) {
854
854
  };
855
855
  let embeddingProvider;
856
856
  let embeddingModel = void 0;
857
- if (isValidProvider(input.embeddingProvider)) {
857
+ let customProvider = void 0;
858
+ if (input.embeddingProvider === "custom") {
859
+ embeddingProvider = "custom";
860
+ const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
861
+ if (rawCustom && typeof rawCustom.baseUrl === "string" && rawCustom.baseUrl.trim().length > 0 && typeof rawCustom.model === "string" && rawCustom.model.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
862
+ customProvider = {
863
+ baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
864
+ model: rawCustom.model,
865
+ dimensions: rawCustom.dimensions,
866
+ apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
867
+ maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
868
+ timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
869
+ concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
870
+ requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
871
+ };
872
+ if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
873
+ console.warn(
874
+ `[codebase-index] Warning: customProvider.baseUrl ("${customProvider.baseUrl}") does not end with an API version path like /v1. The plugin appends /embeddings automatically, so the full URL will be "${customProvider.baseUrl}/embeddings". If your provider expects /v1/embeddings, set baseUrl to "${customProvider.baseUrl}/v1".`
875
+ );
876
+ }
877
+ } else {
878
+ throw new Error(
879
+ "embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
880
+ );
881
+ }
882
+ } else if (isValidProvider(input.embeddingProvider)) {
858
883
  embeddingProvider = input.embeddingProvider;
859
884
  if (input.embeddingModel) {
860
885
  embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
@@ -865,6 +890,7 @@ function parseConfig(raw) {
865
890
  return {
866
891
  embeddingProvider,
867
892
  embeddingModel,
893
+ customProvider,
868
894
  scope: isValidScope(input.scope) ? input.scope : "project",
869
895
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
870
896
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
@@ -2034,12 +2060,39 @@ function getProviderDisplayName(provider) {
2034
2060
  return "Google (Gemini)";
2035
2061
  case "ollama":
2036
2062
  return "Ollama (Local)";
2063
+ case "custom":
2064
+ return "Custom (OpenAI-compatible)";
2037
2065
  default:
2038
2066
  return provider;
2039
2067
  }
2040
2068
  }
2069
+ function createCustomProviderInfo(config) {
2070
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
2071
+ return {
2072
+ provider: "custom",
2073
+ credentials: {
2074
+ provider: "custom",
2075
+ baseUrl,
2076
+ apiKey: config.apiKey
2077
+ },
2078
+ modelInfo: {
2079
+ provider: "custom",
2080
+ model: config.model,
2081
+ dimensions: config.dimensions,
2082
+ maxTokens: config.maxTokens ?? 8192,
2083
+ costPer1MTokens: 0,
2084
+ timeoutMs: config.timeoutMs ?? 3e4
2085
+ }
2086
+ };
2087
+ }
2041
2088
 
2042
2089
  // src/embeddings/provider.ts
2090
+ var CustomProviderNonRetryableError = class extends Error {
2091
+ constructor(message) {
2092
+ super(message);
2093
+ this.name = "CustomProviderNonRetryableError";
2094
+ }
2095
+ };
2043
2096
  function createEmbeddingProvider(configuredProviderInfo) {
2044
2097
  switch (configuredProviderInfo.provider) {
2045
2098
  case "github-copilot":
@@ -2050,6 +2103,8 @@ function createEmbeddingProvider(configuredProviderInfo) {
2050
2103
  return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2051
2104
  case "ollama":
2052
2105
  return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2106
+ case "custom":
2107
+ return new CustomEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2053
2108
  default: {
2054
2109
  const _exhaustive = configuredProviderInfo;
2055
2110
  throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
@@ -2284,6 +2339,90 @@ var OllamaEmbeddingProvider = class {
2284
2339
  return this.modelInfo;
2285
2340
  }
2286
2341
  };
2342
+ var CustomEmbeddingProvider = class {
2343
+ constructor(credentials, modelInfo) {
2344
+ this.credentials = credentials;
2345
+ this.modelInfo = modelInfo;
2346
+ }
2347
+ async embedQuery(query) {
2348
+ const result = await this.embedBatch([query]);
2349
+ return {
2350
+ embedding: result.embeddings[0],
2351
+ tokensUsed: result.totalTokensUsed
2352
+ };
2353
+ }
2354
+ async embedDocument(document) {
2355
+ const result = await this.embedBatch([document]);
2356
+ return {
2357
+ embedding: result.embeddings[0],
2358
+ tokensUsed: result.totalTokensUsed
2359
+ };
2360
+ }
2361
+ async embedBatch(texts) {
2362
+ const headers = {
2363
+ "Content-Type": "application/json"
2364
+ };
2365
+ if (this.credentials.apiKey) {
2366
+ headers["Authorization"] = `Bearer ${this.credentials.apiKey}`;
2367
+ }
2368
+ const baseUrl = this.credentials.baseUrl ?? "";
2369
+ const timeoutMs = this.modelInfo.timeoutMs;
2370
+ const controller = new AbortController();
2371
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2372
+ let response;
2373
+ try {
2374
+ response = await fetch(`${baseUrl}/embeddings`, {
2375
+ method: "POST",
2376
+ headers,
2377
+ body: JSON.stringify({
2378
+ model: this.modelInfo.model,
2379
+ input: texts
2380
+ }),
2381
+ signal: controller.signal
2382
+ });
2383
+ } catch (error) {
2384
+ if (error instanceof Error && error.name === "AbortError") {
2385
+ throw new Error(`Custom embedding API request timed out after ${timeoutMs}ms for ${baseUrl}/embeddings`);
2386
+ }
2387
+ throw error;
2388
+ } finally {
2389
+ clearTimeout(timeout);
2390
+ }
2391
+ if (!response.ok) {
2392
+ const errorText = await response.text();
2393
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2394
+ throw new CustomProviderNonRetryableError(`Custom embedding API error (non-retryable): ${response.status} - ${errorText}`);
2395
+ }
2396
+ throw new Error(`Custom embedding API error: ${response.status} - ${errorText}`);
2397
+ }
2398
+ const data = await response.json();
2399
+ if (data.data && Array.isArray(data.data)) {
2400
+ if (data.data.length > 0) {
2401
+ const actualDims = data.data[0].embedding.length;
2402
+ if (actualDims !== this.modelInfo.dimensions) {
2403
+ throw new Error(
2404
+ `Dimension mismatch: customProvider.dimensions is ${this.modelInfo.dimensions}, but the API returned vectors with ${actualDims} dimensions. Update your config to match the model's actual output dimensions.`
2405
+ );
2406
+ }
2407
+ }
2408
+ if (data.data.length !== texts.length) {
2409
+ throw new Error(
2410
+ `Embedding count mismatch: sent ${texts.length} texts but received ${data.data.length} embeddings. The custom embedding server may not support batch input.`
2411
+ );
2412
+ }
2413
+ return {
2414
+ embeddings: data.data.map((d) => d.embedding),
2415
+ // Rough estimate: ~4 chars per token. Used as fallback when the server
2416
+ // doesn't return usage.total_tokens (e.g. llama.cpp, some vLLM configs).
2417
+ totalTokensUsed: data.usage?.total_tokens ?? texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0)
2418
+ };
2419
+ }
2420
+ throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
2421
+ }
2422
+ getModelInfo() {
2423
+ return this.modelInfo;
2424
+ }
2425
+ };
2287
2426
 
2288
2427
  // src/utils/files.ts
2289
2428
  var import_ignore = __toESM(require_ignore(), 1);
@@ -3420,19 +3559,33 @@ var Indexer = class {
3420
3559
  return { concurrency: 5, intervalMs: 200, minRetryMs: 1e3, maxRetryMs: 3e4 };
3421
3560
  case "ollama":
3422
3561
  return { concurrency: 5, intervalMs: 0, minRetryMs: 500, maxRetryMs: 5e3 };
3562
+ case "custom": {
3563
+ const customConfig = this.config.customProvider;
3564
+ return {
3565
+ concurrency: customConfig?.concurrency ?? 3,
3566
+ intervalMs: customConfig?.requestIntervalMs ?? 1e3,
3567
+ minRetryMs: 1e3,
3568
+ maxRetryMs: 3e4
3569
+ };
3570
+ }
3423
3571
  default:
3424
3572
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
3425
3573
  }
3426
3574
  }
3427
3575
  async initialize() {
3428
- if (this.config.embeddingProvider === "auto") {
3576
+ if (this.config.embeddingProvider === "custom") {
3577
+ if (!this.config.customProvider) {
3578
+ throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
3579
+ }
3580
+ this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
3581
+ } else if (this.config.embeddingProvider === "auto") {
3429
3582
  this.configuredProviderInfo = await tryDetectProvider();
3430
3583
  } else {
3431
3584
  this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
3432
3585
  }
3433
3586
  if (!this.configuredProviderInfo) {
3434
3587
  throw new Error(
3435
- "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
3588
+ "No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
3436
3589
  );
3437
3590
  }
3438
3591
  this.logger.info("Initializing indexer", {
@@ -3442,9 +3595,6 @@ var Indexer = class {
3442
3595
  });
3443
3596
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
3444
3597
  await fsPromises2.mkdir(this.indexPath, { recursive: true });
3445
- if (this.checkForInterruptedIndexing()) {
3446
- await this.recoverFromInterruptedIndexing();
3447
- }
3448
3598
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
3449
3599
  const storePath = path5.join(this.indexPath, "vectors");
3450
3600
  this.store = new VectorStore(storePath, dimensions);
@@ -3465,6 +3615,9 @@ var Indexer = class {
3465
3615
  const dbPath = path5.join(this.indexPath, "codebase.db");
3466
3616
  const dbIsNew = !existsSync4(dbPath);
3467
3617
  this.database = new Database(dbPath);
3618
+ if (this.checkForInterruptedIndexing()) {
3619
+ await this.recoverFromInterruptedIndexing();
3620
+ }
3468
3621
  if (dbIsNew && this.store.count() > 0) {
3469
3622
  this.migrateFromLegacyIndex();
3470
3623
  }
@@ -3885,6 +4038,7 @@ var Indexer = class {
3885
4038
  minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
3886
4039
  maxTimeout: providerRateLimits.maxRetryMs,
3887
4040
  factor: 2,
4041
+ shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
3888
4042
  onFailedAttempt: (error) => {
3889
4043
  const message = getErrorMessage(error);
3890
4044
  if (isRateLimitError(error)) {
@@ -4522,7 +4676,7 @@ var CHUNK_TYPE_ENUM = [
4522
4676
  function createMcpServer(projectRoot, config) {
4523
4677
  const server = new McpServer({
4524
4678
  name: "opencode-codebase-index",
4525
- version: "0.5.0"
4679
+ version: "0.5.1"
4526
4680
  });
4527
4681
  const indexer = new Indexer(projectRoot, config);
4528
4682
  let initialized = false;