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.
- package/LICENSE +0 -0
- package/README.md +212 -74
- package/dist/config/index.d.ts +14 -2
- package/dist/config/index.js +77 -2
- package/dist/config/providers/catalog.d.ts +27 -0
- package/dist/config/providers/catalog.js +130 -0
- package/dist/config/providers/file.d.ts +4 -2
- package/dist/config/providers/file.js +198 -32
- package/dist/config/providers/paths.d.ts +2 -0
- package/dist/config/providers/paths.js +4 -0
- package/dist/config/providers/providers.default.d.ts +3 -0
- package/dist/config/providers/providers.default.js +12 -0
- package/dist/config/providers/request.js +21 -9
- package/dist/config/types.d.ts +25 -2
- package/dist/config/types.js +22 -7
- package/dist/config/writer.d.ts +8 -2
- package/dist/config/writer.js +59 -12
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/summarizer/providers/anthropic.d.ts +2 -1
- package/dist/summarizer/providers/anthropic.js +3 -2
- package/dist/summarizer/providers/index.js +19 -35
- package/dist/summarizer/providers/models.d.ts +11 -0
- package/dist/summarizer/providers/models.js +113 -0
- package/dist/summarizer/providers/openai.d.ts +2 -1
- package/dist/summarizer/providers/openai.js +2 -1
- package/dist/summarizer/providers/types.d.ts +1 -6
- package/package.json +1 -1
- package/dist/summarizer/providers/gemini.d.ts +0 -9
- package/dist/summarizer/providers/gemini.js +0 -79
- package/dist/summarizer/providers/ollama.d.ts +0 -9
- package/dist/summarizer/providers/ollama.js +0 -23
- package/dist/summarizer/providers/openRouter.d.ts +0 -9
- package/dist/summarizer/providers/openRouter.js +0 -19
package/dist/config/writer.js
CHANGED
|
@@ -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
|
-
...(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 "
|
|
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("
|
|
21
|
-
return new AnthropicClient(apiKey, model);
|
|
22
|
-
|
|
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
|
|
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,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
|
-
}
|