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/index.cjs CHANGED
@@ -857,7 +857,32 @@ function parseConfig(raw) {
857
857
  };
858
858
  let embeddingProvider;
859
859
  let embeddingModel = void 0;
860
- if (isValidProvider(input.embeddingProvider)) {
860
+ let customProvider = void 0;
861
+ if (input.embeddingProvider === "custom") {
862
+ embeddingProvider = "custom";
863
+ const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
864
+ 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) {
865
+ customProvider = {
866
+ baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
867
+ model: rawCustom.model,
868
+ dimensions: rawCustom.dimensions,
869
+ apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
870
+ maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
871
+ timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
872
+ concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
873
+ requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
874
+ };
875
+ if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
876
+ console.warn(
877
+ `[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".`
878
+ );
879
+ }
880
+ } else {
881
+ throw new Error(
882
+ "embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
883
+ );
884
+ }
885
+ } else if (isValidProvider(input.embeddingProvider)) {
861
886
  embeddingProvider = input.embeddingProvider;
862
887
  if (input.embeddingModel) {
863
888
  embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
@@ -868,6 +893,7 @@ function parseConfig(raw) {
868
893
  return {
869
894
  embeddingProvider,
870
895
  embeddingModel,
896
+ customProvider,
871
897
  scope: isValidScope(input.scope) ? input.scope : "project",
872
898
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
873
899
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
@@ -2033,12 +2059,39 @@ function getProviderDisplayName(provider) {
2033
2059
  return "Google (Gemini)";
2034
2060
  case "ollama":
2035
2061
  return "Ollama (Local)";
2062
+ case "custom":
2063
+ return "Custom (OpenAI-compatible)";
2036
2064
  default:
2037
2065
  return provider;
2038
2066
  }
2039
2067
  }
2068
+ function createCustomProviderInfo(config) {
2069
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
2070
+ return {
2071
+ provider: "custom",
2072
+ credentials: {
2073
+ provider: "custom",
2074
+ baseUrl,
2075
+ apiKey: config.apiKey
2076
+ },
2077
+ modelInfo: {
2078
+ provider: "custom",
2079
+ model: config.model,
2080
+ dimensions: config.dimensions,
2081
+ maxTokens: config.maxTokens ?? 8192,
2082
+ costPer1MTokens: 0,
2083
+ timeoutMs: config.timeoutMs ?? 3e4
2084
+ }
2085
+ };
2086
+ }
2040
2087
 
2041
2088
  // src/embeddings/provider.ts
2089
+ var CustomProviderNonRetryableError = class extends Error {
2090
+ constructor(message) {
2091
+ super(message);
2092
+ this.name = "CustomProviderNonRetryableError";
2093
+ }
2094
+ };
2042
2095
  function createEmbeddingProvider(configuredProviderInfo) {
2043
2096
  switch (configuredProviderInfo.provider) {
2044
2097
  case "github-copilot":
@@ -2049,6 +2102,8 @@ function createEmbeddingProvider(configuredProviderInfo) {
2049
2102
  return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2050
2103
  case "ollama":
2051
2104
  return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2105
+ case "custom":
2106
+ return new CustomEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2052
2107
  default: {
2053
2108
  const _exhaustive = configuredProviderInfo;
2054
2109
  throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
@@ -2283,6 +2338,90 @@ var OllamaEmbeddingProvider = class {
2283
2338
  return this.modelInfo;
2284
2339
  }
2285
2340
  };
2341
+ var CustomEmbeddingProvider = class {
2342
+ constructor(credentials, modelInfo) {
2343
+ this.credentials = credentials;
2344
+ this.modelInfo = modelInfo;
2345
+ }
2346
+ async embedQuery(query) {
2347
+ const result = await this.embedBatch([query]);
2348
+ return {
2349
+ embedding: result.embeddings[0],
2350
+ tokensUsed: result.totalTokensUsed
2351
+ };
2352
+ }
2353
+ async embedDocument(document) {
2354
+ const result = await this.embedBatch([document]);
2355
+ return {
2356
+ embedding: result.embeddings[0],
2357
+ tokensUsed: result.totalTokensUsed
2358
+ };
2359
+ }
2360
+ async embedBatch(texts) {
2361
+ const headers = {
2362
+ "Content-Type": "application/json"
2363
+ };
2364
+ if (this.credentials.apiKey) {
2365
+ headers["Authorization"] = `Bearer ${this.credentials.apiKey}`;
2366
+ }
2367
+ const baseUrl = this.credentials.baseUrl ?? "";
2368
+ const timeoutMs = this.modelInfo.timeoutMs;
2369
+ const controller = new AbortController();
2370
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2371
+ let response;
2372
+ try {
2373
+ response = await fetch(`${baseUrl}/embeddings`, {
2374
+ method: "POST",
2375
+ headers,
2376
+ body: JSON.stringify({
2377
+ model: this.modelInfo.model,
2378
+ input: texts
2379
+ }),
2380
+ signal: controller.signal
2381
+ });
2382
+ } catch (error) {
2383
+ if (error instanceof Error && error.name === "AbortError") {
2384
+ throw new Error(`Custom embedding API request timed out after ${timeoutMs}ms for ${baseUrl}/embeddings`);
2385
+ }
2386
+ throw error;
2387
+ } finally {
2388
+ clearTimeout(timeout);
2389
+ }
2390
+ if (!response.ok) {
2391
+ const errorText = await response.text();
2392
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2393
+ throw new CustomProviderNonRetryableError(`Custom embedding API error (non-retryable): ${response.status} - ${errorText}`);
2394
+ }
2395
+ throw new Error(`Custom embedding API error: ${response.status} - ${errorText}`);
2396
+ }
2397
+ const data = await response.json();
2398
+ if (data.data && Array.isArray(data.data)) {
2399
+ if (data.data.length > 0) {
2400
+ const actualDims = data.data[0].embedding.length;
2401
+ if (actualDims !== this.modelInfo.dimensions) {
2402
+ throw new Error(
2403
+ `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.`
2404
+ );
2405
+ }
2406
+ }
2407
+ if (data.data.length !== texts.length) {
2408
+ throw new Error(
2409
+ `Embedding count mismatch: sent ${texts.length} texts but received ${data.data.length} embeddings. The custom embedding server may not support batch input.`
2410
+ );
2411
+ }
2412
+ return {
2413
+ embeddings: data.data.map((d) => d.embedding),
2414
+ // Rough estimate: ~4 chars per token. Used as fallback when the server
2415
+ // doesn't return usage.total_tokens (e.g. llama.cpp, some vLLM configs).
2416
+ totalTokensUsed: data.usage?.total_tokens ?? texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0)
2417
+ };
2418
+ }
2419
+ throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
2420
+ }
2421
+ getModelInfo() {
2422
+ return this.modelInfo;
2423
+ }
2424
+ };
2286
2425
 
2287
2426
  // src/utils/files.ts
2288
2427
  var import_ignore = __toESM(require_ignore(), 1);
@@ -3468,19 +3607,33 @@ var Indexer = class {
3468
3607
  return { concurrency: 5, intervalMs: 200, minRetryMs: 1e3, maxRetryMs: 3e4 };
3469
3608
  case "ollama":
3470
3609
  return { concurrency: 5, intervalMs: 0, minRetryMs: 500, maxRetryMs: 5e3 };
3610
+ case "custom": {
3611
+ const customConfig = this.config.customProvider;
3612
+ return {
3613
+ concurrency: customConfig?.concurrency ?? 3,
3614
+ intervalMs: customConfig?.requestIntervalMs ?? 1e3,
3615
+ minRetryMs: 1e3,
3616
+ maxRetryMs: 3e4
3617
+ };
3618
+ }
3471
3619
  default:
3472
3620
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
3473
3621
  }
3474
3622
  }
3475
3623
  async initialize() {
3476
- if (this.config.embeddingProvider === "auto") {
3624
+ if (this.config.embeddingProvider === "custom") {
3625
+ if (!this.config.customProvider) {
3626
+ throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
3627
+ }
3628
+ this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
3629
+ } else if (this.config.embeddingProvider === "auto") {
3477
3630
  this.configuredProviderInfo = await tryDetectProvider();
3478
3631
  } else {
3479
3632
  this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
3480
3633
  }
3481
3634
  if (!this.configuredProviderInfo) {
3482
3635
  throw new Error(
3483
- "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
3636
+ "No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
3484
3637
  );
3485
3638
  }
3486
3639
  this.logger.info("Initializing indexer", {
@@ -3490,9 +3643,6 @@ var Indexer = class {
3490
3643
  });
3491
3644
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
3492
3645
  await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
3493
- if (this.checkForInterruptedIndexing()) {
3494
- await this.recoverFromInterruptedIndexing();
3495
- }
3496
3646
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
3497
3647
  const storePath = path5.join(this.indexPath, "vectors");
3498
3648
  this.store = new VectorStore(storePath, dimensions);
@@ -3513,6 +3663,9 @@ var Indexer = class {
3513
3663
  const dbPath = path5.join(this.indexPath, "codebase.db");
3514
3664
  const dbIsNew = !(0, import_fs4.existsSync)(dbPath);
3515
3665
  this.database = new Database(dbPath);
3666
+ if (this.checkForInterruptedIndexing()) {
3667
+ await this.recoverFromInterruptedIndexing();
3668
+ }
3516
3669
  if (dbIsNew && this.store.count() > 0) {
3517
3670
  this.migrateFromLegacyIndex();
3518
3671
  }
@@ -3933,6 +4086,7 @@ var Indexer = class {
3933
4086
  minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
3934
4087
  maxTimeout: providerRateLimits.maxRetryMs,
3935
4088
  factor: 2,
4089
+ shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
3936
4090
  onFailedAttempt: (error) => {
3937
4091
  const message = getErrorMessage(error);
3938
4092
  if (isRateLimitError(error)) {