devlensio 0.4.4 → 0.6.0

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.
Files changed (34) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +212 -74
  3. package/dist/config/index.d.ts +14 -2
  4. package/dist/config/index.js +77 -2
  5. package/dist/config/providers/catalog.d.ts +27 -0
  6. package/dist/config/providers/catalog.js +130 -0
  7. package/dist/config/providers/file.d.ts +4 -2
  8. package/dist/config/providers/file.js +198 -32
  9. package/dist/config/providers/paths.d.ts +2 -0
  10. package/dist/config/providers/paths.js +4 -0
  11. package/dist/config/providers/providers.default.d.ts +3 -0
  12. package/dist/config/providers/providers.default.js +12 -0
  13. package/dist/config/providers/request.js +21 -9
  14. package/dist/config/types.d.ts +25 -2
  15. package/dist/config/types.js +22 -7
  16. package/dist/config/writer.d.ts +8 -2
  17. package/dist/config/writer.js +59 -12
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.js +5 -1
  20. package/dist/summarizer/providers/anthropic.d.ts +2 -1
  21. package/dist/summarizer/providers/anthropic.js +3 -2
  22. package/dist/summarizer/providers/index.js +19 -35
  23. package/dist/summarizer/providers/models.d.ts +11 -0
  24. package/dist/summarizer/providers/models.js +113 -0
  25. package/dist/summarizer/providers/openai.d.ts +2 -1
  26. package/dist/summarizer/providers/openai.js +2 -1
  27. package/dist/summarizer/providers/types.d.ts +1 -6
  28. package/package.json +1 -1
  29. package/dist/summarizer/providers/gemini.d.ts +0 -9
  30. package/dist/summarizer/providers/gemini.js +0 -79
  31. package/dist/summarizer/providers/ollama.d.ts +0 -9
  32. package/dist/summarizer/providers/ollama.js +0 -23
  33. package/dist/summarizer/providers/openRouter.d.ts +0 -9
  34. package/dist/summarizer/providers/openRouter.js +0 -19
@@ -1,6 +1,7 @@
1
1
  //! The concept of local config file should only exist in open Source, and in case of deployment the apis for writing this cofig file should never be exposed.
2
2
  import fs from "fs";
3
3
  import { CONFIG_FILE, CONFIG_DIR } from "./providers/file.js";
4
+ import { makeProviderKey } from "./types.js";
4
5
  function readRawFile() {
5
6
  if (!fs.existsSync(CONFIG_FILE))
6
7
  return {};
@@ -12,10 +13,10 @@ function readRawFile() {
12
13
  return {};
13
14
  }
14
15
  }
15
- // atomicWrite
16
+ // atomicWrite
16
17
  // Writes to a temp file first, then renames to the real path.
17
18
  // If the server crashes mid-write, the old config survives intact.
18
- function atomicWrite(filePath, content) {
19
+ export function atomicWrite(filePath, content) {
19
20
  const tmp = `${filePath}.tmp`;
20
21
  fs.writeFileSync(tmp, content, "utf-8");
21
22
  fs.renameSync(tmp, filePath);
@@ -36,17 +37,62 @@ export function writeConfig(partial) {
36
37
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
37
38
  }
38
39
  const existing = readRawFile();
40
+ // ── Multi-provider summarization upsert ──────────────────────────────────
41
+ let summarizationUpdate;
42
+ if (partial.summarization) {
43
+ const s = partial.summarization;
44
+ const protocol = s.provider ?? "openai";
45
+ const providerName = s.providerName ?? protocol;
46
+ const key = makeProviderKey(protocol, providerName);
47
+ // Read existing multi-provider structure (or build from legacy flat format)
48
+ const rawSum = existing.summarization;
49
+ let storage;
50
+ if (rawSum && typeof rawSum === "object" && rawSum.providers && typeof rawSum.providers === "object" && typeof rawSum.active === "string") {
51
+ // Already multi-provider format
52
+ storage = {
53
+ active: rawSum.active,
54
+ providers: rawSum.providers,
55
+ };
56
+ }
57
+ else {
58
+ // Legacy flat format or first save — create fresh multi-provider storage
59
+ storage = { active: key, providers: {} };
60
+ // If there's a legacy flat config, preserve it as the first entry
61
+ if (rawSum && typeof rawSum.provider === "string") {
62
+ const legacyKey = makeProviderKey(rawSum.provider, rawSum.providerName ?? rawSum.provider);
63
+ storage.providers[legacyKey] = {
64
+ provider: rawSum.provider,
65
+ providerName: rawSum.providerName ?? rawSum.provider,
66
+ model: rawSum.model ?? "",
67
+ apiKey: rawSum.apiKey,
68
+ baseUrl: rawSum.baseUrl,
69
+ batchSize: rawSum.batchSize ?? 50,
70
+ };
71
+ if (legacyKey !== key) {
72
+ storage.active = key; // new entry becomes active
73
+ }
74
+ }
75
+ }
76
+ // Upsert the incoming entry
77
+ const existingEntry = storage.providers[key];
78
+ storage.providers[key] = {
79
+ provider: s.provider ?? existingEntry?.provider ?? "openai",
80
+ providerName: providerName,
81
+ model: s.model ?? existingEntry?.model ?? "",
82
+ apiKey: s.apiKey ?? existingEntry?.apiKey,
83
+ baseUrl: s.baseUrl ?? existingEntry?.baseUrl,
84
+ batchSize: s.batchSize ?? existingEntry?.batchSize ?? 50,
85
+ };
86
+ storage.active = key;
87
+ summarizationUpdate = storage;
88
+ }
39
89
  const updated = {
40
90
  ...existing,
41
- // Only merge blocks that the user actually touched
42
91
  ...(partial.deploymentMode && {
43
92
  deploymentMode: partial.deploymentMode,
44
93
  }),
45
- ...(partial.summarization && {
46
- summarization: {
47
- ...existing.summarization,
48
- ...partial.summarization,
49
- },
94
+ ...(summarizationUpdate && {
95
+ summarization: summarizationUpdate,
50
96
  }),
51
97
  ...(partial.embedding && {
52
98
  embedding: {
@@ -55,10 +101,9 @@ export function writeConfig(partial) {
55
101
  },
56
102
  }),
57
103
  // neo4j: if user sent it, merge. If user sent null explicitly, delete it.
58
- // undefined means "don't touch it"
59
104
  ...(partial.neo4j !== undefined && {
60
105
  neo4j: partial.neo4j === null
61
- ? undefined // user explicitly removed Neo4j config
106
+ ? undefined
62
107
  : {
63
108
  ...existing.neo4j,
64
109
  ...partial.neo4j,
@@ -73,17 +118,19 @@ function maskKey(key) {
73
118
  // Show last 3 characters only — enough to identify which key is set
74
119
  return `...${key.slice(-3)}`;
75
120
  }
76
- // maskConfig
121
+ // maskConfig
77
122
  // Public — called by GET /api/config handler.
123
+ // DevLens is OSS/local: apiKeys are returned in full (the user owns their keys).
78
124
  export function maskConfig(config) {
79
125
  return {
80
126
  deploymentMode: config.deploymentMode,
81
127
  summarization: {
82
128
  provider: config.summarization.provider,
129
+ providerName: config.summarization.providerName,
83
130
  model: config.summarization.model,
84
131
  baseUrl: config.summarization.baseUrl,
85
132
  batchSize: config.summarization.batchSize,
86
- apiKeyHint: maskKey(config.summarization.apiKey),
133
+ apiKey: config.summarization.apiKey, // full key for OSS
87
134
  },
88
135
  embedding: {
89
136
  provider: config.embedding.provider,
package/dist/index.d.ts CHANGED
@@ -9,7 +9,8 @@ export * from "./pipeline/index.js";
9
9
  export * from "./config/types.js";
10
10
  export { queue } from "./jobs/index.js";
11
11
  export { storage } from "./storage/index.js";
12
- export { resolveConfig, initConfig, maskConfig, writeConfig } from "./config/index.js";
12
+ export { resolveConfig, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
13
+ export type { AllProvidersResult } from "./config/index.js";
13
14
  export { readPackageDependencies, categorizeLibrary } from "./graph/thirdPartyLibs.js";
14
15
  export { analyzePipeline } from "./pipeline/index.js";
15
16
  export { runSummarization } from "./summarizer/index.js";
@@ -18,3 +19,7 @@ export { EDGE_LABELS } from "./summarizer/prompts.js";
18
19
  export { CycleGroup, TopologicalResult } from "./summarizer/types.js";
19
20
  export { buildGraphIndex, getBlastRadius, getKHop, getSubgraph, findCycles, } from "./graph/traversal.js";
20
21
  export type { GraphIndex, TraversalHit, TraversalResult, TraversalOpts, SubgraphResult, } from "./graph/traversal.js";
22
+ export { loadCatalog, findProvider, saveProvider, removeProvider, resetUserCatalog, listUserProviders, } from "./config/providers/catalog.js";
23
+ export type { CatalogProvider, CatalogProviderInput } from "./config/providers/catalog.js";
24
+ export { listModels } from "./summarizer/providers/models.js";
25
+ export type { ListModelsOpts } from "./summarizer/providers/models.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export * from "./config/types.js";
11
11
  export { queue } from "./jobs/index.js";
12
12
  export { storage } from "./storage/index.js";
13
13
  // Config helpers
14
- export { resolveConfig, initConfig, maskConfig, writeConfig } from "./config/index.js";
14
+ export { resolveConfig, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
15
15
  // Pre-scan helpers
16
16
  export { readPackageDependencies, categorizeLibrary } from "./graph/thirdPartyLibs.js";
17
17
  // Pipeline & analysis
@@ -21,3 +21,7 @@ export { computeClusters } from "./clustering/index.js";
21
21
  export { EDGE_LABELS } from "./summarizer/prompts.js";
22
22
  // Graph traversal & queries (consumed by the MCP server)
23
23
  export { buildGraphIndex, getBlastRadius, getKHop, getSubgraph, findCycles, } from "./graph/traversal.js";
24
+ // Provider catalog (consumed by DevLens OSS frontend & CLI)
25
+ export { loadCatalog, findProvider, saveProvider, removeProvider, resetUserCatalog, listUserProviders, } from "./config/providers/catalog.js";
26
+ // Dynamic model discovery (consumed by DevLens OSS CLI)
27
+ export { listModels } from "./summarizer/providers/models.js";
@@ -1,9 +1,10 @@
1
1
  import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
2
  export declare class AnthropicClient implements LLMClient {
3
3
  readonly provider: "anthropic";
4
+ readonly providerName?: string;
4
5
  readonly model: string;
5
6
  private client;
6
- constructor(apiKey: string, model: string);
7
+ constructor(apiKey: string, model: string, baseURL?: string, providerName?: string);
7
8
  summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
9
  validateConnection(): Promise<void>;
9
10
  }
@@ -33,10 +33,11 @@ function parseResponse(raw) {
33
33
  };
34
34
  }
35
35
  export class AnthropicClient {
36
- constructor(apiKey, model) {
36
+ constructor(apiKey, model, baseURL, providerName) {
37
37
  this.provider = "anthropic";
38
38
  this.model = model;
39
- this.client = new Anthropic({ apiKey });
39
+ this.providerName = providerName;
40
+ this.client = new Anthropic({ apiKey, ...(baseURL ? { baseURL } : {}) });
40
41
  }
41
42
  async summarize(request) {
42
43
  const systemMessage = request.messages.find(m => m.role === "system");
@@ -1,43 +1,27 @@
1
- // providers/index.ts has one job — given the config, return the right LLMClient instance. It's the factory that the batch loop calls so it never has to know which provider is being used.
1
+ // providers/index.ts has one job — given the config, return the right LLMClient instance.
2
+ // It's the factory that the batch loop calls so it never has to know which provider is being used.
3
+ // The switch has exactly 2 arms: the two wire protocols (openai, anthropic).
4
+ // Brand identity (providerName) is data — resolved from the catalog or user input.
5
+ import { findProvider } from "../../config/providers/catalog.js";
2
6
  import { AnthropicClient } from "./anthropic.js";
3
- import { GeminiClient } from "./gemini.js";
4
- import { OllamaClient } from "./ollama.js";
5
7
  import { OpenAIClient } from "./openai.js";
6
- import { OpenRouterClient } from "./openRouter.js";
7
- // ─── Factory ──────────────────────────────────────────────────────────────────
8
- //
9
- // Single entry point for the batch loop — returns the right LLMClient
10
- // based on config. Caller never imports individual provider classes.
11
- //
12
- // Throws clearly if:
13
- // - provider is unknown
14
- // - apiKey is missing for cloud providers (fail fast before the batch loop)
15
8
  export function createLLMClient(config) {
16
- const { provider, model, apiKey, baseUrl } = config;
9
+ const { provider, providerName, model, apiKey, baseUrl } = config;
10
+ const entry = findProvider(providerName ?? "");
11
+ const needsKey = entry?.requiresKey ?? true; // unknown/custom → require key
12
+ const effectiveBase = baseUrl ?? entry?.baseUrl;
17
13
  switch (provider) {
18
- case "anthropic":
14
+ case "openai": {
15
+ if (needsKey && !apiKey)
16
+ throw new Error(`Provider "${providerName}" requires an API key`);
17
+ return new OpenAIClient(apiKey ?? "ollama", model, effectiveBase, providerName);
18
+ }
19
+ case "anthropic": {
19
20
  if (!apiKey)
20
- throw new Error("Anthropic provider requires an API key");
21
- return new AnthropicClient(apiKey, model);
22
- case "openai":
23
- if (!apiKey)
24
- throw new Error("OpenAI provider requires an API key");
25
- return new OpenAIClient(apiKey, model);
26
- case "openrouter":
27
- if (!apiKey)
28
- throw new Error("OpenRouter provider requires an API key");
29
- return new OpenRouterClient(apiKey, model);
30
- case "gemini":
31
- if (!apiKey)
32
- throw new Error("Gemini provider requires an API key");
33
- return new GeminiClient(apiKey, model);
34
- case "ollama":
35
- return new OllamaClient(model, baseUrl);
36
- case "managed":
37
- // Cloud SaaS only — platform injects the key via request headers.
38
- // Should never reach here in local mode.
39
- throw new Error("Managed provider is only available in cloud mode");
21
+ throw new Error(`Provider "${providerName}" requires an API key`);
22
+ return new AnthropicClient(apiKey, model, effectiveBase, providerName);
23
+ }
40
24
  default:
41
- throw new Error(`Unknown LLM provider: ${provider}`);
25
+ throw new Error(`Unknown provider protocol: ${provider}`);
42
26
  }
43
27
  }
@@ -0,0 +1,11 @@
1
+ export interface ListModelsOpts {
2
+ protocol: "openai" | "anthropic";
3
+ baseUrl: string;
4
+ apiKey?: string;
5
+ }
6
+ /**
7
+ * Fetches the live model list from a provider's models endpoint.
8
+ * Returns deduped (case-insensitive), sorted model ID strings.
9
+ * Throws on auth/network errors with a clear message.
10
+ */
11
+ export declare function listModels(opts: ListModelsOpts): Promise<string[]>;
@@ -0,0 +1,113 @@
1
+ const FETCH_TIMEOUT_MS = 10000;
2
+ /**
3
+ * Fetches the live model list from a provider's models endpoint.
4
+ * Returns deduped (case-insensitive), sorted model ID strings.
5
+ * Throws on auth/network errors with a clear message.
6
+ */
7
+ export async function listModels(opts) {
8
+ if (opts.protocol === "openai") {
9
+ return listOpenAIModels(opts.baseUrl, opts.apiKey);
10
+ }
11
+ return listAnthropicModels(opts.baseUrl, opts.apiKey);
12
+ }
13
+ // OpenAI-style lister
14
+ // GET {baseUrl}/models | Authorization: Bearer {apiKey}
15
+ // Tolerant of URLs with or without /v1 — tries as-is first, retries with /v1 if empty.
16
+ async function listOpenAIModels(baseUrl, apiKey) {
17
+ const headers = {
18
+ "Content-Type": "application/json",
19
+ };
20
+ if (apiKey) {
21
+ headers["Authorization"] = `Bearer ${apiKey}`;
22
+ }
23
+ async function tryFetch(url) {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
26
+ try {
27
+ return await fetch(url, { method: "GET", headers, signal: controller.signal });
28
+ }
29
+ finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+ const cleanBase = baseUrl.replace(/\/+$/, "");
34
+ let res = await tryFetch(`${cleanBase}/models`);
35
+ // If URL has no /v1 segment and the request returned nothing useful, retry with /v1
36
+ if (!res.ok && !cleanBase.includes("/v1")) {
37
+ res = await tryFetch(`${cleanBase}/v1/models`);
38
+ }
39
+ if (res.status === 401) {
40
+ throw new Error("Invalid API key for this provider. Check your key in config.");
41
+ }
42
+ if (!res.ok) {
43
+ throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`);
44
+ }
45
+ const body = (await res.json());
46
+ return normalizeModels((body.data ?? []).map((m) => m.id));
47
+ }
48
+ // Anthropic-style lister
49
+ // GET {baseUrl}/v1/models | x-api-key + anthropic-version
50
+ // Paginated — loops via after_id until has_more === false
51
+ async function listAnthropicModels(baseUrl, apiKey) {
52
+ if (!apiKey) {
53
+ throw new Error("Anthropic-style provider requires an API key to list models.");
54
+ }
55
+ const headers = {
56
+ "x-api-key": apiKey,
57
+ "anthropic-version": "2023-06-01",
58
+ "Content-Type": "application/json",
59
+ };
60
+ const base = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
61
+ const allIds = [];
62
+ let afterId;
63
+ let hasMore = true;
64
+ const controller = new AbortController();
65
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
66
+ try {
67
+ while (hasMore) {
68
+ const url = afterId
69
+ ? `${base}?after_id=${afterId}&limit=1000`
70
+ : `${base}?limit=1000`;
71
+ const res = await fetch(url, {
72
+ method: "GET",
73
+ headers,
74
+ signal: controller.signal,
75
+ });
76
+ if (res.status === 401) {
77
+ throw new Error("Invalid API key for this provider. Check your key in config.");
78
+ }
79
+ if (!res.ok) {
80
+ throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`);
81
+ }
82
+ const body = (await res.json());
83
+ allIds.push(...(body.data ?? []).map((m) => m.id));
84
+ hasMore = body.has_more ?? false;
85
+ afterId = body.last_id;
86
+ }
87
+ return normalizeModels(allIds);
88
+ }
89
+ catch (err) {
90
+ if (err.name === "AbortError") {
91
+ throw new Error(`Timed out fetching models from ${baseUrl} after ${FETCH_TIMEOUT_MS}ms`);
92
+ }
93
+ throw err;
94
+ }
95
+ finally {
96
+ clearTimeout(timer);
97
+ }
98
+ }
99
+ // Normalization
100
+ // Dedupe case-insensitively, sort ascending.
101
+ function normalizeModels(models) {
102
+ const seen = new Set();
103
+ const result = [];
104
+ for (const m of models) {
105
+ const key = m.toLowerCase();
106
+ if (!seen.has(key)) {
107
+ seen.add(key);
108
+ result.push(m);
109
+ }
110
+ }
111
+ result.sort((a, b) => a.localeCompare(b));
112
+ return result;
113
+ }
@@ -1,9 +1,10 @@
1
1
  import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
2
  export declare class OpenAIClient implements LLMClient {
3
3
  readonly provider: "openai";
4
+ readonly providerName?: string;
4
5
  readonly model: string;
5
6
  private client;
6
- constructor(apiKey: string, model: string, baseURL?: string);
7
+ constructor(apiKey: string, model: string, baseURL?: string, providerName?: string);
7
8
  summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
9
  validateConnection(): Promise<void>;
9
10
  }
@@ -29,9 +29,10 @@ function parseResponse(raw) {
29
29
  // Also used as the base for OpenRouter and Ollama —
30
30
  // both expose OpenAI-compatible APIs, just with a different baseURL.
31
31
  export class OpenAIClient {
32
- constructor(apiKey, model, baseURL) {
32
+ constructor(apiKey, model, baseURL, providerName) {
33
33
  this.provider = "openai";
34
34
  this.model = model;
35
+ this.providerName = providerName;
35
36
  this.client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
36
37
  }
37
38
  async summarize(request) {
@@ -20,13 +20,8 @@ export interface NodeSummaryOutput {
20
20
  }
21
21
  export interface LLMClient {
22
22
  readonly provider: LLMProvider;
23
+ readonly providerName?: string;
23
24
  readonly model: string;
24
25
  summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
25
26
  validateConnection(): Promise<void>;
26
27
  }
27
- export type LLMClientFactory = (config: {
28
- provider: LLMProvider;
29
- model: string;
30
- apiKey?: string;
31
- baseUrl?: string;
32
- }) => LLMClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlensio",
3
- "version": "0.4.4",
3
+ "version": "0.6.0",
4
4
  "description": "Codebase intelligence engine for TypeScript/JavaScript/Reactjs/Nextjs repositories.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,9 +0,0 @@
1
- import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
- export declare class GeminiClient implements LLMClient {
3
- readonly provider: "gemini";
4
- readonly model: string;
5
- private ai;
6
- constructor(apiKey: string, model: string);
7
- summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
- validateConnection(): Promise<void>;
9
- }
@@ -1,79 +0,0 @@
1
- import { GoogleGenAI } from "@google/genai";
2
- // ─── Response Parser ──────────────────────────────────────────────────────────
3
- //
4
- // Same XML format as all other providers — consistent across the board.
5
- const VALID_SEVERITIES = new Set(["none", "low", "medium", "high"]);
6
- function parseXmlTag(text, tag) {
7
- const match = text.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
8
- return match ? match[1].trim() : "";
9
- }
10
- function parseResponse(raw) {
11
- const technicalSummary = parseXmlTag(raw, "technical");
12
- const businessSummary = parseXmlTag(raw, "business");
13
- const severityRaw = parseXmlTag(raw, "security_severity").toLowerCase();
14
- const securitySummary = parseXmlTag(raw, "security_summary");
15
- const severity = VALID_SEVERITIES.has(severityRaw)
16
- ? severityRaw
17
- : "none";
18
- return {
19
- technicalSummary: technicalSummary || raw.trim(),
20
- businessSummary: businessSummary || "",
21
- security: {
22
- severity,
23
- summary: severity === "none" ? "" : securitySummary,
24
- },
25
- tokensUsed: 0,
26
- };
27
- }
28
- export class GeminiClient {
29
- constructor(apiKey, model) {
30
- this.provider = "gemini";
31
- this.model = model;
32
- this.ai = new GoogleGenAI({ apiKey });
33
- }
34
- async summarize(request) {
35
- const systemMessage = request.messages.find(m => m.role === "system");
36
- const userMessages = request.messages.filter(m => m.role !== "system");
37
- const contents = userMessages.map(m => ({
38
- role: "user",
39
- parts: [{ text: m.content }],
40
- }));
41
- const response = await this.ai.models.generateContent({
42
- model: this.model,
43
- contents,
44
- config: {
45
- systemInstruction: systemMessage?.content,
46
- temperature: request.temperature ?? 0,
47
- maxOutputTokens: request.maxTokens ?? 2048,
48
- },
49
- });
50
- const raw = response.text ?? "";
51
- const result = parseResponse(raw);
52
- result.tokensUsed =
53
- (response.usageMetadata?.promptTokenCount ?? 0) +
54
- (response.usageMetadata?.candidatesTokenCount ?? 0);
55
- return result;
56
- }
57
- async validateConnection() {
58
- try {
59
- await this.ai.models.generateContent({
60
- model: this.model,
61
- contents: [{ role: "user", parts: [{ text: "hi" }] }],
62
- config: { maxOutputTokens: 10 },
63
- });
64
- }
65
- catch (err) {
66
- const status = err?.status ?? err?.statusCode ?? err?.code;
67
- const message = err?.message ?? "";
68
- if (status === 400 || message.includes("API_KEY_INVALID") || message.includes("invalid api key"))
69
- throw new Error(`Gemini API key is invalid or missing. Check your key in config.`);
70
- if (status === 403 || message.includes("PERMISSION_DENIED"))
71
- throw new Error(`Gemini API key does not have permission to use model "${this.model}".`);
72
- if (status === 404 || message.includes("models/") || message.includes("not found"))
73
- throw new Error(`Gemini model "${this.model}" not found. Check model name in config.`);
74
- if (status === 429 || message.includes("RESOURCE_EXHAUSTED"))
75
- throw new Error(`Gemini rate limit hit during connection check. Try again shortly.`);
76
- throw new Error(`Gemini connection failed: ${message || "unknown error"}`);
77
- }
78
- }
79
- }
@@ -1,9 +0,0 @@
1
- import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
- export declare class OllamaClient implements LLMClient {
3
- readonly provider: "ollama";
4
- readonly model: string;
5
- private inner;
6
- constructor(model: string, baseURL?: string);
7
- summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
- validateConnection(): Promise<void>;
9
- }
@@ -1,23 +0,0 @@
1
- import { OpenAIClient } from "./openai.js";
2
- const OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434";
3
- // ─── OllamaClient ─────────────────────────────────────────────────────────────
4
- //
5
- // Ollama exposes an OpenAI-compatible API at /v1 — wraps OpenAIClient.
6
- // No API key needed for local usage — passes a placeholder to satisfy the SDK.
7
- // baseURL is configurable for users running Ollama on a non-default port or host.
8
- // /v1 is always appended — user-provided baseURL should not include it.
9
- export class OllamaClient {
10
- constructor(model, baseURL) {
11
- this.provider = "ollama";
12
- this.model = model;
13
- const base = (baseURL ?? OLLAMA_DEFAULT_BASE_URL).replace(/\/+$/, "");
14
- this.inner = new OpenAIClient("ollama", // placeholder — Ollama doesn't validate the key
15
- model, `${base}/v1`);
16
- }
17
- summarize(request) {
18
- return this.inner.summarize(request);
19
- }
20
- validateConnection() {
21
- return this.inner.validateConnection();
22
- }
23
- }
@@ -1,9 +0,0 @@
1
- import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
- export declare class OpenRouterClient implements LLMClient {
3
- readonly provider: "openrouter";
4
- readonly model: string;
5
- private inner;
6
- constructor(apiKey: string, model: string);
7
- summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
- validateConnection(): Promise<void>;
9
- }
@@ -1,19 +0,0 @@
1
- import { OpenAIClient } from "./openai.js";
2
- const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
3
- // ─── OpenRouterClient ─────────────────────────────────────────────────────────
4
- //
5
- // OpenRouter exposes an OpenAI-compatible API — no new logic needed.
6
- // Just wraps OpenAIClient with the correct baseURL and overrides provider. (OpenAI actually set the industry standard for the response of the LLMs through API.Everyone follows the OpenAI standard, Aside of course Anthropic, because its anthropic, who even go against pentagon.)
7
- export class OpenRouterClient {
8
- constructor(apiKey, model) {
9
- this.provider = "openrouter";
10
- this.model = model;
11
- this.inner = new OpenAIClient(apiKey, model, OPENROUTER_BASE_URL);
12
- }
13
- summarize(request) {
14
- return this.inner.summarize(request);
15
- }
16
- validateConnection() {
17
- return this.inner.validateConnection();
18
- }
19
- }