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.js CHANGED
@@ -853,7 +853,32 @@ function parseConfig(raw) {
853
853
  };
854
854
  let embeddingProvider;
855
855
  let embeddingModel = void 0;
856
- if (isValidProvider(input.embeddingProvider)) {
856
+ let customProvider = void 0;
857
+ if (input.embeddingProvider === "custom") {
858
+ embeddingProvider = "custom";
859
+ const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
860
+ 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) {
861
+ customProvider = {
862
+ baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
863
+ model: rawCustom.model,
864
+ dimensions: rawCustom.dimensions,
865
+ apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
866
+ maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
867
+ timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
868
+ concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
869
+ requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
870
+ };
871
+ if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
872
+ console.warn(
873
+ `[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".`
874
+ );
875
+ }
876
+ } else {
877
+ throw new Error(
878
+ "embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
879
+ );
880
+ }
881
+ } else if (isValidProvider(input.embeddingProvider)) {
857
882
  embeddingProvider = input.embeddingProvider;
858
883
  if (input.embeddingModel) {
859
884
  embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
@@ -864,6 +889,7 @@ function parseConfig(raw) {
864
889
  return {
865
890
  embeddingProvider,
866
891
  embeddingModel,
892
+ customProvider,
867
893
  scope: isValidScope(input.scope) ? input.scope : "project",
868
894
  include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
869
895
  exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
@@ -2029,12 +2055,39 @@ function getProviderDisplayName(provider) {
2029
2055
  return "Google (Gemini)";
2030
2056
  case "ollama":
2031
2057
  return "Ollama (Local)";
2058
+ case "custom":
2059
+ return "Custom (OpenAI-compatible)";
2032
2060
  default:
2033
2061
  return provider;
2034
2062
  }
2035
2063
  }
2064
+ function createCustomProviderInfo(config) {
2065
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
2066
+ return {
2067
+ provider: "custom",
2068
+ credentials: {
2069
+ provider: "custom",
2070
+ baseUrl,
2071
+ apiKey: config.apiKey
2072
+ },
2073
+ modelInfo: {
2074
+ provider: "custom",
2075
+ model: config.model,
2076
+ dimensions: config.dimensions,
2077
+ maxTokens: config.maxTokens ?? 8192,
2078
+ costPer1MTokens: 0,
2079
+ timeoutMs: config.timeoutMs ?? 3e4
2080
+ }
2081
+ };
2082
+ }
2036
2083
 
2037
2084
  // src/embeddings/provider.ts
2085
+ var CustomProviderNonRetryableError = class extends Error {
2086
+ constructor(message) {
2087
+ super(message);
2088
+ this.name = "CustomProviderNonRetryableError";
2089
+ }
2090
+ };
2038
2091
  function createEmbeddingProvider(configuredProviderInfo) {
2039
2092
  switch (configuredProviderInfo.provider) {
2040
2093
  case "github-copilot":
@@ -2045,6 +2098,8 @@ function createEmbeddingProvider(configuredProviderInfo) {
2045
2098
  return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2046
2099
  case "ollama":
2047
2100
  return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2101
+ case "custom":
2102
+ return new CustomEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
2048
2103
  default: {
2049
2104
  const _exhaustive = configuredProviderInfo;
2050
2105
  throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
@@ -2279,6 +2334,90 @@ var OllamaEmbeddingProvider = class {
2279
2334
  return this.modelInfo;
2280
2335
  }
2281
2336
  };
2337
+ var CustomEmbeddingProvider = class {
2338
+ constructor(credentials, modelInfo) {
2339
+ this.credentials = credentials;
2340
+ this.modelInfo = modelInfo;
2341
+ }
2342
+ async embedQuery(query) {
2343
+ const result = await this.embedBatch([query]);
2344
+ return {
2345
+ embedding: result.embeddings[0],
2346
+ tokensUsed: result.totalTokensUsed
2347
+ };
2348
+ }
2349
+ async embedDocument(document) {
2350
+ const result = await this.embedBatch([document]);
2351
+ return {
2352
+ embedding: result.embeddings[0],
2353
+ tokensUsed: result.totalTokensUsed
2354
+ };
2355
+ }
2356
+ async embedBatch(texts) {
2357
+ const headers = {
2358
+ "Content-Type": "application/json"
2359
+ };
2360
+ if (this.credentials.apiKey) {
2361
+ headers["Authorization"] = `Bearer ${this.credentials.apiKey}`;
2362
+ }
2363
+ const baseUrl = this.credentials.baseUrl ?? "";
2364
+ const timeoutMs = this.modelInfo.timeoutMs;
2365
+ const controller = new AbortController();
2366
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2367
+ let response;
2368
+ try {
2369
+ response = await fetch(`${baseUrl}/embeddings`, {
2370
+ method: "POST",
2371
+ headers,
2372
+ body: JSON.stringify({
2373
+ model: this.modelInfo.model,
2374
+ input: texts
2375
+ }),
2376
+ signal: controller.signal
2377
+ });
2378
+ } catch (error) {
2379
+ if (error instanceof Error && error.name === "AbortError") {
2380
+ throw new Error(`Custom embedding API request timed out after ${timeoutMs}ms for ${baseUrl}/embeddings`);
2381
+ }
2382
+ throw error;
2383
+ } finally {
2384
+ clearTimeout(timeout);
2385
+ }
2386
+ if (!response.ok) {
2387
+ const errorText = await response.text();
2388
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2389
+ throw new CustomProviderNonRetryableError(`Custom embedding API error (non-retryable): ${response.status} - ${errorText}`);
2390
+ }
2391
+ throw new Error(`Custom embedding API error: ${response.status} - ${errorText}`);
2392
+ }
2393
+ const data = await response.json();
2394
+ if (data.data && Array.isArray(data.data)) {
2395
+ if (data.data.length > 0) {
2396
+ const actualDims = data.data[0].embedding.length;
2397
+ if (actualDims !== this.modelInfo.dimensions) {
2398
+ throw new Error(
2399
+ `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.`
2400
+ );
2401
+ }
2402
+ }
2403
+ if (data.data.length !== texts.length) {
2404
+ throw new Error(
2405
+ `Embedding count mismatch: sent ${texts.length} texts but received ${data.data.length} embeddings. The custom embedding server may not support batch input.`
2406
+ );
2407
+ }
2408
+ return {
2409
+ embeddings: data.data.map((d) => d.embedding),
2410
+ // Rough estimate: ~4 chars per token. Used as fallback when the server
2411
+ // doesn't return usage.total_tokens (e.g. llama.cpp, some vLLM configs).
2412
+ totalTokensUsed: data.usage?.total_tokens ?? texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0)
2413
+ };
2414
+ }
2415
+ throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
2416
+ }
2417
+ getModelInfo() {
2418
+ return this.modelInfo;
2419
+ }
2420
+ };
2282
2421
 
2283
2422
  // src/utils/files.ts
2284
2423
  var import_ignore = __toESM(require_ignore(), 1);
@@ -3463,19 +3602,33 @@ var Indexer = class {
3463
3602
  return { concurrency: 5, intervalMs: 200, minRetryMs: 1e3, maxRetryMs: 3e4 };
3464
3603
  case "ollama":
3465
3604
  return { concurrency: 5, intervalMs: 0, minRetryMs: 500, maxRetryMs: 5e3 };
3605
+ case "custom": {
3606
+ const customConfig = this.config.customProvider;
3607
+ return {
3608
+ concurrency: customConfig?.concurrency ?? 3,
3609
+ intervalMs: customConfig?.requestIntervalMs ?? 1e3,
3610
+ minRetryMs: 1e3,
3611
+ maxRetryMs: 3e4
3612
+ };
3613
+ }
3466
3614
  default:
3467
3615
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
3468
3616
  }
3469
3617
  }
3470
3618
  async initialize() {
3471
- if (this.config.embeddingProvider === "auto") {
3619
+ if (this.config.embeddingProvider === "custom") {
3620
+ if (!this.config.customProvider) {
3621
+ throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
3622
+ }
3623
+ this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
3624
+ } else if (this.config.embeddingProvider === "auto") {
3472
3625
  this.configuredProviderInfo = await tryDetectProvider();
3473
3626
  } else {
3474
3627
  this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
3475
3628
  }
3476
3629
  if (!this.configuredProviderInfo) {
3477
3630
  throw new Error(
3478
- "No embedding provider available. Configure GitHub, OpenAI, Google, or Ollama."
3631
+ "No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
3479
3632
  );
3480
3633
  }
3481
3634
  this.logger.info("Initializing indexer", {
@@ -3485,9 +3638,6 @@ var Indexer = class {
3485
3638
  });
3486
3639
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
3487
3640
  await fsPromises2.mkdir(this.indexPath, { recursive: true });
3488
- if (this.checkForInterruptedIndexing()) {
3489
- await this.recoverFromInterruptedIndexing();
3490
- }
3491
3641
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
3492
3642
  const storePath = path5.join(this.indexPath, "vectors");
3493
3643
  this.store = new VectorStore(storePath, dimensions);
@@ -3508,6 +3658,9 @@ var Indexer = class {
3508
3658
  const dbPath = path5.join(this.indexPath, "codebase.db");
3509
3659
  const dbIsNew = !existsSync4(dbPath);
3510
3660
  this.database = new Database(dbPath);
3661
+ if (this.checkForInterruptedIndexing()) {
3662
+ await this.recoverFromInterruptedIndexing();
3663
+ }
3511
3664
  if (dbIsNew && this.store.count() > 0) {
3512
3665
  this.migrateFromLegacyIndex();
3513
3666
  }
@@ -3928,6 +4081,7 @@ var Indexer = class {
3928
4081
  minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
3929
4082
  maxTimeout: providerRateLimits.maxRetryMs,
3930
4083
  factor: 2,
4084
+ shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
3931
4085
  onFailedAttempt: (error) => {
3932
4086
  const message = getErrorMessage(error);
3933
4087
  if (isRateLimitError(error)) {