@prsense/llm 0.1.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 (72) hide show
  1. package/LICENSE +201 -0
  2. package/dist/events.d.ts +9 -0
  3. package/dist/events.d.ts.map +1 -0
  4. package/dist/events.js +9 -0
  5. package/dist/events.js.map +1 -0
  6. package/dist/helper.d.ts +3 -0
  7. package/dist/helper.d.ts.map +1 -0
  8. package/dist/helper.js +8 -0
  9. package/dist/helper.js.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +9 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/middleware/retry.d.ts +6 -0
  15. package/dist/middleware/retry.d.ts.map +1 -0
  16. package/dist/middleware/retry.js +21 -0
  17. package/dist/middleware/retry.js.map +1 -0
  18. package/dist/middleware/timeout.d.ts +3 -0
  19. package/dist/middleware/timeout.d.ts.map +1 -0
  20. package/dist/middleware/timeout.js +11 -0
  21. package/dist/middleware/timeout.js.map +1 -0
  22. package/dist/providers/anthropic.d.ts +7 -0
  23. package/dist/providers/anthropic.d.ts.map +1 -0
  24. package/dist/providers/anthropic.js +52 -0
  25. package/dist/providers/anthropic.js.map +1 -0
  26. package/dist/providers/claude.d.ts +7 -0
  27. package/dist/providers/claude.d.ts.map +1 -0
  28. package/dist/providers/claude.js +40 -0
  29. package/dist/providers/claude.js.map +1 -0
  30. package/dist/providers/gemini.d.ts +7 -0
  31. package/dist/providers/gemini.d.ts.map +1 -0
  32. package/dist/providers/gemini.js +46 -0
  33. package/dist/providers/gemini.js.map +1 -0
  34. package/dist/providers/google.d.ts +7 -0
  35. package/dist/providers/google.d.ts.map +1 -0
  36. package/dist/providers/google.js +61 -0
  37. package/dist/providers/google.js.map +1 -0
  38. package/dist/providers/ollama.d.ts +3 -0
  39. package/dist/providers/ollama.d.ts.map +1 -0
  40. package/dist/providers/ollama.js +56 -0
  41. package/dist/providers/ollama.js.map +1 -0
  42. package/dist/providers/ollamaEmbeddings.d.ts +6 -0
  43. package/dist/providers/ollamaEmbeddings.d.ts.map +1 -0
  44. package/dist/providers/ollamaEmbeddings.js +48 -0
  45. package/dist/providers/ollamaEmbeddings.js.map +1 -0
  46. package/dist/providers/openai.d.ts +8 -0
  47. package/dist/providers/openai.d.ts.map +1 -0
  48. package/dist/providers/openai.js +49 -0
  49. package/dist/providers/openai.js.map +1 -0
  50. package/dist/providers/openaiEmbeddings.d.ts +6 -0
  51. package/dist/providers/openaiEmbeddings.d.ts.map +1 -0
  52. package/dist/providers/openaiEmbeddings.js +30 -0
  53. package/dist/providers/openaiEmbeddings.js.map +1 -0
  54. package/dist/types.d.ts +62 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +8 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +32 -0
  59. package/src/events.ts +11 -0
  60. package/src/helper.ts +13 -0
  61. package/src/index.ts +8 -0
  62. package/src/middleware/retry.ts +26 -0
  63. package/src/middleware/timeout.ts +13 -0
  64. package/src/providers/anthropic.ts +71 -0
  65. package/src/providers/google.ts +82 -0
  66. package/src/providers/ollama.ts +77 -0
  67. package/src/providers/ollamaEmbeddings.ts +67 -0
  68. package/src/providers/openai.ts +71 -0
  69. package/src/providers/openaiEmbeddings.ts +37 -0
  70. package/src/types.ts +81 -0
  71. package/tsconfig.json +10 -0
  72. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,56 @@
1
+ import fetch from "node-fetch";
2
+ export function createOllamaClient(config) {
3
+ const baseUrl = config.baseUrl ?? "http://localhost:11434";
4
+ return {
5
+ async generate(req) {
6
+ const { prompt } = req;
7
+ const res = await fetch(`${baseUrl}/api/generate`, {
8
+ method: "POST",
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ body: JSON.stringify({
13
+ model: config.model,
14
+ prompt: `
15
+ ${prompt.system}
16
+
17
+ ### REVIEW INPUT START ###
18
+
19
+ ${prompt.user}
20
+
21
+ ### REVIEW INPUT END ###
22
+
23
+ Remember:
24
+ Return ONLY JSON.
25
+ `,
26
+ stream: false,
27
+ options: {
28
+ temperature: config.temperature ?? 0.1,
29
+ top_p: 0.9,
30
+ },
31
+ }),
32
+ });
33
+ if (!res.ok) {
34
+ throw new Error(`Ollama error: ${res.statusText}`);
35
+ }
36
+ const json = (await res.json());
37
+ const usage = json.prompt_eval_count !== undefined && json.eval_count !== undefined
38
+ ? {
39
+ promptTokens: json.prompt_eval_count,
40
+ completionTokens: json.eval_count,
41
+ totalTokens: json.prompt_eval_count + json.eval_count,
42
+ }
43
+ : undefined;
44
+ if (usage) {
45
+ return {
46
+ text: json.response,
47
+ usage,
48
+ };
49
+ }
50
+ return {
51
+ text: json.response,
52
+ };
53
+ },
54
+ };
55
+ }
56
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/providers/ollama.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,YAAY,CAAC;AAe/B,MAAM,UAAU,kBAAkB,CAAC,MAAoB;IACrD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,wBAAwB,CAAC;IAE3D,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,GAAe;YAC5B,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;YAEvB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,eAAe,EAAE;gBACjD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,MAAM,EAAE;EAChB,MAAM,CAAC,MAAM;;;;EAIb,MAAM,CAAC,IAAI;;;;;;CAMZ;oBACS,MAAM,EAAE,KAAK;oBACb,OAAO,EAAE;wBACP,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,GAAG;wBACtC,KAAK,EAAE,GAAG;qBACX;iBACF,CAAC;aACH,CAAC,CAAC;YAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmB,CAAC;YAElD,MAAM,KAAK,GACT,IAAI,CAAC,iBAAiB,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;gBACnE,CAAC,CAAC;oBACE,YAAY,EAAE,IAAI,CAAC,iBAAiB;oBACpC,gBAAgB,EAAE,IAAI,CAAC,UAAU;oBACjC,WAAW,EAAE,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,UAAU;iBACtD;gBACH,CAAC,CAAC,SAAS,CAAC;YAEhB,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO;oBACL,IAAI,EAAE,IAAI,CAAC,QAAQ;oBACnB,KAAK;iBACN,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,IAAI,EAAE,IAAI,CAAC,QAAQ;aACpB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { EmbeddingClient } from "../types.js";
2
+ export declare function createOllamaEmbeddingClient(opts: {
3
+ baseUrl?: string;
4
+ model: string;
5
+ }): EmbeddingClient;
6
+ //# sourceMappingURL=ollamaEmbeddings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ollamaEmbeddings.d.ts","sourceRoot":"","sources":["../../src/providers/ollamaEmbeddings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,eAAe,CA4DlB"}
@@ -0,0 +1,48 @@
1
+ import fetch from "node-fetch";
2
+ export function createOllamaEmbeddingClient(opts) {
3
+ const baseUrl = opts.baseUrl ?? "http://localhost:11434";
4
+ let cachedDimension = null;
5
+ async function detectDimension() {
6
+ if (cachedDimension !== null)
7
+ return cachedDimension;
8
+ const res = await fetch(`${baseUrl}/api/embeddings`, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify({
12
+ model: opts.model,
13
+ prompt: "dimension test",
14
+ }),
15
+ });
16
+ if (!res.ok) {
17
+ throw new Error(`Ollama embedding dimension detection failed: ${res.status}`);
18
+ }
19
+ const json = (await res.json());
20
+ cachedDimension = json.embedding.length;
21
+ return cachedDimension;
22
+ }
23
+ return {
24
+ async embed(texts) {
25
+ const results = [];
26
+ for (const text of texts) {
27
+ const res = await fetch(`${baseUrl}/api/embeddings`, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ },
32
+ body: JSON.stringify({
33
+ model: opts.model,
34
+ prompt: text,
35
+ }),
36
+ });
37
+ if (!res.ok) {
38
+ throw new Error(`Ollama embedding error: ${res.status} ${await res.text()}`);
39
+ }
40
+ const json = (await res.json());
41
+ results.push(json.embedding);
42
+ }
43
+ return results;
44
+ },
45
+ dimension: detectDimension,
46
+ };
47
+ }
48
+ //# sourceMappingURL=ollamaEmbeddings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ollamaEmbeddings.js","sourceRoot":"","sources":["../../src/providers/ollamaEmbeddings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,YAAY,CAAC;AAG/B,MAAM,UAAU,2BAA2B,CAAC,IAG3C;IACC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,wBAAwB,CAAC;IACzD,IAAI,eAAe,GAAkB,IAAI,CAAC;IAC1C,KAAK,UAAU,eAAe;QAC5B,IAAI,eAAe,KAAK,IAAI;YAAE,OAAO,eAAe,CAAC;QAErD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,iBAAiB,EAAE;YACnD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,MAAM,EAAE,gBAAgB;aACzB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,gDAAgD,GAAG,CAAC,MAAM,EAAE,CAC7D,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;QAE3D,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;QACxC,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,KAAe;YACzB,MAAM,OAAO,GAAe,EAAE,CAAC;YAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,iBAAiB,EAAE;oBACnD,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE;wBACP,cAAc,EAAE,kBAAkB;qBACnC;oBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,MAAM,EAAE,IAAI;qBACb,CAAC;iBACH,CAAC,CAAC;gBAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;oBACZ,MAAM,IAAI,KAAK,CACb,2BAA2B,GAAG,CAAC,MAAM,IAAI,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAC5D,CAAC;gBACJ,CAAC;gBAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAE7B,CAAC;gBAEF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC/B,CAAC;YAED,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,SAAS,EAAE,eAAe;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { LlmClient } from "../types.js";
2
+ export declare function createOpenAiClient(config: {
3
+ apiKey: string;
4
+ model: string;
5
+ temperature?: number;
6
+ baseUrl?: string;
7
+ }): LlmClient;
8
+ //# sourceMappingURL=openai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,SAAS,EAKV,MAAM,aAAa,CAAC;AAErB,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,SAAS,CAuDZ"}
@@ -0,0 +1,49 @@
1
+ // packages/llm/src/providers/openai.ts
2
+ import OpenAI from "openai";
3
+ import { LlmError, } from "../types.js";
4
+ export function createOpenAiClient(config) {
5
+ const client = new OpenAI({
6
+ apiKey: config.apiKey,
7
+ baseURL: config.baseUrl,
8
+ });
9
+ return {
10
+ async generate(req) {
11
+ const { prompt } = req;
12
+ const temperature = req.temperature ?? config.temperature ?? 0.05;
13
+ try {
14
+ const request = {
15
+ model: config.model,
16
+ temperature,
17
+ messages: [
18
+ { role: "system", content: prompt.system },
19
+ { role: "user", content: prompt.user },
20
+ ],
21
+ };
22
+ if (req.maxTokens !== undefined) {
23
+ request.max_tokens = req.maxTokens;
24
+ }
25
+ const res = await client.chat.completions.create(request);
26
+ const text = res.choices[0]?.message?.content;
27
+ if (!text) {
28
+ throw new Error("OpenAI returned empty response");
29
+ }
30
+ const usage = res.usage && res.usage.prompt_tokens !== undefined
31
+ ? {
32
+ promptTokens: res.usage.prompt_tokens,
33
+ completionTokens: res.usage.completion_tokens ?? 0,
34
+ totalTokens: res.usage.total_tokens ??
35
+ res.usage.prompt_tokens + (res.usage.completion_tokens ?? 0),
36
+ }
37
+ : undefined;
38
+ if (usage) {
39
+ return { text, usage };
40
+ }
41
+ return { text };
42
+ }
43
+ catch (err) {
44
+ throw new LlmError("OpenAI request failed", err);
45
+ }
46
+ },
47
+ };
48
+ }
49
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAIL,QAAQ,GAET,MAAM,aAAa,CAAC;AAErB,MAAM,UAAU,kBAAkB,CAAC,MAKlC;IACC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,GAAe;YAC5B,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;YAEvB,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC;YAElE,IAAI,CAAC;gBACH,MAAM,OAAO,GAAQ;oBACnB,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,WAAW;oBACX,QAAQ,EAAE;wBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;wBAC1C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE;qBACvC;iBACF,CAAC;gBAEF,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;oBAChC,OAAO,CAAC,UAAU,GAAG,GAAG,CAAC,SAAS,CAAC;gBACrC,CAAC;gBAED,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAE1D,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;gBAE9C,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;gBACpD,CAAC;gBAED,MAAM,KAAK,GACT,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,aAAa,KAAK,SAAS;oBAChD,CAAC,CAAC;wBACE,YAAY,EAAE,GAAG,CAAC,KAAK,CAAC,aAAa;wBACrC,gBAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,iBAAiB,IAAI,CAAC;wBAClD,WAAW,EACT,GAAG,CAAC,KAAK,CAAC,YAAY;4BACtB,GAAG,CAAC,KAAK,CAAC,aAAa,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,IAAI,CAAC,CAAC;qBAC/D;oBACH,CAAC,CAAC,SAAS,CAAC;gBAEhB,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;gBACzB,CAAC;gBAED,OAAO,EAAE,IAAI,EAAE,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,QAAQ,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { EmbeddingClient } from "../types.js";
2
+ export declare function createOpenAiEmbeddingClient(opts: {
3
+ apiKey: string;
4
+ model: string;
5
+ }): EmbeddingClient;
6
+ //# sourceMappingURL=openaiEmbeddings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openaiEmbeddings.d.ts","sourceRoot":"","sources":["../../src/providers/openaiEmbeddings.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,eAAe,CA6BlB"}
@@ -0,0 +1,30 @@
1
+ //packages/llm/src/providers/openaiEmbeddings.ts
2
+ import OpenAI from "openai";
3
+ export function createOpenAiEmbeddingClient(opts) {
4
+ const client = new OpenAI({
5
+ apiKey: opts.apiKey,
6
+ });
7
+ let cachedDimension = null;
8
+ async function embed(texts) {
9
+ const response = await client.embeddings.create({
10
+ model: opts.model,
11
+ input: texts,
12
+ });
13
+ return response.data.map((d) => d.embedding);
14
+ }
15
+ async function detectDimension() {
16
+ if (cachedDimension !== null)
17
+ return cachedDimension;
18
+ const vectors = await embed(["dimension test"]);
19
+ if (!vectors.length || !vectors[0]) {
20
+ throw new Error("Failed to detect embedding dimension: empty response");
21
+ }
22
+ cachedDimension = vectors[0].length;
23
+ return cachedDimension;
24
+ }
25
+ return {
26
+ embed,
27
+ dimension: detectDimension,
28
+ };
29
+ }
30
+ //# sourceMappingURL=openaiEmbeddings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openaiEmbeddings.js","sourceRoot":"","sources":["../../src/providers/openaiEmbeddings.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B,MAAM,UAAU,2BAA2B,CAAC,IAG3C;IACC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;QACxB,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC,CAAC;IACH,IAAI,eAAe,GAAkB,IAAI,CAAC;IAE1C,KAAK,UAAU,KAAK,CAAC,KAAe;QAClC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;YAC9C,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,UAAU,eAAe;QAC5B,IAAI,eAAe,KAAK,IAAI;YAAE,OAAO,eAAe,CAAC;QAErD,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC;QACD,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACpC,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS,EAAE,eAAe;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A minimal, explicit prompt contract.
3
+ * No chat history, no tools, no streaming (yet).
4
+ */
5
+ export type LlmPrompt = {
6
+ system: string;
7
+ user: string;
8
+ };
9
+ export type LlmUsage = {
10
+ promptTokens: number;
11
+ completionTokens: number;
12
+ totalTokens: number;
13
+ };
14
+ /**
15
+ * Raw LLM output.
16
+ * Parsing happens in the engine.
17
+ */
18
+ export type LlmResponse = {
19
+ text: string;
20
+ usage?: LlmUsage;
21
+ };
22
+ export type OllamaConfig = {
23
+ baseUrl?: string;
24
+ model: string;
25
+ temperature?: number;
26
+ };
27
+ export type LlmRequest = {
28
+ prompt: LlmPrompt;
29
+ temperature?: number;
30
+ maxTokens?: number;
31
+ timeoutMs?: number;
32
+ };
33
+ /**
34
+ * LLM port used by the engine.
35
+ * Providers implement this.
36
+ */
37
+ export type LlmClient = {
38
+ generate(req: LlmRequest): Promise<LlmResponse>;
39
+ };
40
+ export type LlmMiddleware = (client: LlmClient) => LlmClient;
41
+ export declare class LlmError extends Error {
42
+ readonly cause?: unknown | undefined;
43
+ constructor(message: string, cause?: unknown | undefined);
44
+ }
45
+ export type ClaudeConfig = {
46
+ apiKey: string;
47
+ model: string;
48
+ };
49
+ export type GeminiConfig = {
50
+ apiKey: string;
51
+ model: string;
52
+ };
53
+ export type OpenAiConfig = {
54
+ apiKey: string;
55
+ model: string;
56
+ baseUrl?: string;
57
+ };
58
+ export type EmbeddingClient = {
59
+ embed(texts: string[]): Promise<number[][]>;
60
+ dimension(): Promise<number>;
61
+ };
62
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,SAAS,CAAC;IAMlB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,SAAS,CAAC;AAE7D,qBAAa,QAAS,SAAQ,KAAK;aAGf,KAAK,CAAC,EAAE,OAAO;gBAD/B,OAAO,EAAE,MAAM,EACC,KAAK,CAAC,EAAE,OAAO,YAAA;CAIlC;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC5C,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9B,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ export class LlmError extends Error {
2
+ cause;
3
+ constructor(message, cause) {
4
+ super(message);
5
+ this.cause = cause;
6
+ }
7
+ }
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAoDA,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGf;IAFlB,YACE,OAAe,EACC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,UAAK,GAAL,KAAK,CAAU;IAGjC,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@prsense/llm",
3
+ "version": "0.1.0",
4
+ "description": "LLM interface and providers for PRsense",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "keywords": [],
15
+ "author": "Navdeep Saini",
16
+ "license": "Apache-2.0",
17
+ "dependencies": {
18
+ "@anthropic-ai/sdk": "^0.71.2",
19
+ "@google/generative-ai": "^0.24.1",
20
+ "node-fetch": "^3.3.2",
21
+ "openai": "^6.16.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^25.0.3",
25
+ "tsx": "^4.21.0",
26
+ "typescript": "^5.9.3"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -b",
30
+ "clean": "rm -rf dist *.tsbuildinfo"
31
+ }
32
+ }
package/src/events.ts ADDED
@@ -0,0 +1,11 @@
1
+ // packages/llm/src/events.ts
2
+
3
+ export const LlmEvents = {
4
+ RequestStarted: "llm.request.started",
5
+ RequestSucceeded: "llm.request.succeeded",
6
+ RequestFailed: "llm.request.failed",
7
+ RequestRetried: "llm.request.retried",
8
+ RequestTimedOut: "llm.request.timed_out",
9
+ } as const;
10
+
11
+ export type LlmEventName = (typeof LlmEvents)[keyof typeof LlmEvents];
package/src/helper.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { LlmClient, LlmMiddleware } from "./types.js";
2
+
3
+ // helper function for composition
4
+ export function composeLlm(
5
+ client: LlmClient,
6
+ ...middlewares: LlmMiddleware[]
7
+ ): LlmClient {
8
+ return middlewares.reduce((acc, mw) => mw(acc), client);
9
+ }
10
+
11
+ // usage
12
+ // const base = createOllamaClient(config)
13
+ // const llm = composeLlm(base, withTimeout(30_000), withRetries({retries: 2}))
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./providers/ollama.js";
3
+ export * from "./providers/openai.js";
4
+ export * from "./providers/anthropic.js";
5
+ export * from "./providers/google.js";
6
+ export * from "./providers/ollamaEmbeddings.js";
7
+ export * from "./providers/openaiEmbeddings.js";
8
+ //TODO add gemini embeddings client if available
@@ -0,0 +1,26 @@
1
+ import type { LlmMiddleware } from "../types.js";
2
+ export function withRetries(opts: {
3
+ retries: number;
4
+ backoffMs?: number;
5
+ }): LlmMiddleware {
6
+ const { retries, backoffMs = 200 } = opts;
7
+
8
+ return (client) => ({
9
+ async generate(req) {
10
+ let lastErr: unknown;
11
+
12
+ for (let i = 0; i <= retries; i++) {
13
+ try {
14
+ return await client.generate(req);
15
+ } catch (err) {
16
+ lastErr = err;
17
+ if (i < retries) {
18
+ await new Promise((r) => setTimeout(r, backoffMs * (i + 1)));
19
+ }
20
+ }
21
+ }
22
+
23
+ throw lastErr;
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,13 @@
1
+ import { LlmMiddleware } from "../types.js";
2
+ export function withTimeout(ms: number): LlmMiddleware {
3
+ return (client) => ({
4
+ async generate(req) {
5
+ return Promise.race([
6
+ client.generate(req),
7
+ new Promise<never>((_, reject) =>
8
+ setTimeout(() => reject(new Error("LLM timeout")), ms),
9
+ ),
10
+ ]);
11
+ },
12
+ });
13
+ }
@@ -0,0 +1,71 @@
1
+ // packages/llm/src/providers/anthropic.ts
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ import type { TextBlock } from "@anthropic-ai/sdk/resources/messages/messages";
4
+ import {
5
+ LlmClient,
6
+ LlmRequest,
7
+ LlmResponse,
8
+ LlmError,
9
+ LlmUsage,
10
+ } from "../types.js";
11
+
12
+ export function createAnthropicClient(config: {
13
+ apiKey: string;
14
+ model: string;
15
+ temperature?: number;
16
+ }): LlmClient {
17
+ const client = new Anthropic({
18
+ apiKey: config.apiKey,
19
+ });
20
+
21
+ return {
22
+ async generate(req: LlmRequest): Promise<LlmResponse> {
23
+ const { prompt, temperature = 0 } = req;
24
+
25
+ try {
26
+ const res = await client.messages.create({
27
+ model: config.model,
28
+ max_tokens: req.maxTokens ?? 4096,
29
+ temperature,
30
+ system: prompt.system,
31
+ messages: [
32
+ {
33
+ role: "user",
34
+ content: prompt.user,
35
+ },
36
+ ],
37
+ });
38
+
39
+ const text = res.content
40
+ .filter((c): c is TextBlock => c.type === "text")
41
+ .map((c) => c.text)
42
+ .join("\n");
43
+
44
+ if (!text) {
45
+ throw new Error("Claude returned empty response");
46
+ }
47
+
48
+ const usage: LlmUsage | undefined =
49
+ res.usage && res.usage.input_tokens !== undefined
50
+ ? {
51
+ promptTokens: res.usage.input_tokens,
52
+ completionTokens: res.usage.output_tokens ?? 0,
53
+ totalTokens:
54
+ res.usage.input_tokens + (res.usage.output_tokens ?? 0),
55
+ }
56
+ : undefined;
57
+
58
+ if (usage) {
59
+ return {
60
+ text,
61
+ usage,
62
+ };
63
+ }
64
+
65
+ return { text };
66
+ } catch (err) {
67
+ throw new LlmError("Claude request failed", err);
68
+ }
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,82 @@
1
+ // packages/llm/src/providers/google.ts
2
+ import {
3
+ GoogleGenerativeAI,
4
+ HarmCategory,
5
+ HarmBlockThreshold,
6
+ } from "@google/generative-ai";
7
+ import { LlmClient, LlmRequest, LlmResponse } from "../types.js";
8
+
9
+ function stripMarkdownJson(text: string): string {
10
+ const trimmed = text.trim();
11
+
12
+ if (!trimmed.startsWith("```")) return trimmed;
13
+
14
+ const lines = trimmed.split("\n");
15
+
16
+ if (lines[0]?.startsWith("```")) lines.shift();
17
+ if (lines.at(-1)?.startsWith("```")) lines.pop();
18
+
19
+ return lines.join("\n").trim();
20
+ }
21
+
22
+ export function createGoogleClient(config: {
23
+ apiKey: string;
24
+ model: string;
25
+ temperature?: number;
26
+ }): LlmClient {
27
+ const genAI = new GoogleGenerativeAI(config.apiKey);
28
+
29
+ return {
30
+ async generate(req: LlmRequest): Promise<LlmResponse> {
31
+ const { prompt, temperature = 0.05 } = req;
32
+
33
+ const model = genAI.getGenerativeModel({
34
+ model: config.model,
35
+ systemInstruction: prompt.system,
36
+ safetySettings: [
37
+ {
38
+ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
39
+ threshold: HarmBlockThreshold.BLOCK_NONE,
40
+ },
41
+ ],
42
+ });
43
+
44
+ const res = await model.generateContent({
45
+ contents: [
46
+ {
47
+ role: "user",
48
+ parts: [{ text: prompt.user }],
49
+ },
50
+ ],
51
+ generationConfig: {
52
+ temperature,
53
+ maxOutputTokens: 1024,
54
+ responseMimeType: "application/json",
55
+ },
56
+ });
57
+
58
+ const candidate = res.response.candidates?.[0];
59
+
60
+ const raw =
61
+ candidate?.content?.parts
62
+ ?.map((p) => ("text" in p ? p.text : ""))
63
+ .join("") ?? "";
64
+
65
+ if (!raw) {
66
+ throw new Error("Gemini returned empty response");
67
+ }
68
+
69
+ const text = stripMarkdownJson(raw);
70
+
71
+ return {
72
+ text,
73
+ usage: {
74
+ promptTokens: res.response.usageMetadata?.promptTokenCount ?? 0,
75
+ completionTokens:
76
+ res.response.usageMetadata?.candidatesTokenCount ?? 0,
77
+ totalTokens: res.response.usageMetadata?.totalTokenCount ?? 0,
78
+ },
79
+ };
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,77 @@
1
+ import fetch from "node-fetch";
2
+ import {
3
+ LlmClient,
4
+ LlmRequest,
5
+ LlmResponse,
6
+ OllamaConfig,
7
+ LlmUsage,
8
+ } from "../types.js";
9
+
10
+ type OllamaResponse = {
11
+ response: string;
12
+ prompt_eval_count?: number;
13
+ eval_count?: number;
14
+ };
15
+
16
+ export function createOllamaClient(config: OllamaConfig): LlmClient {
17
+ const baseUrl = config.baseUrl ?? "http://localhost:11434";
18
+
19
+ return {
20
+ async generate(req: LlmRequest): Promise<LlmResponse> {
21
+ const { prompt } = req;
22
+
23
+ const res = await fetch(`${baseUrl}/api/generate`, {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: JSON.stringify({
29
+ model: config.model,
30
+ prompt: `
31
+ ${prompt.system}
32
+
33
+ ### REVIEW INPUT START ###
34
+
35
+ ${prompt.user}
36
+
37
+ ### REVIEW INPUT END ###
38
+
39
+ Remember:
40
+ Return ONLY JSON.
41
+ `,
42
+ stream: false,
43
+ options: {
44
+ temperature: config.temperature ?? 0.1,
45
+ top_p: 0.9,
46
+ },
47
+ }),
48
+ });
49
+
50
+ if (!res.ok) {
51
+ throw new Error(`Ollama error: ${res.statusText}`);
52
+ }
53
+
54
+ const json = (await res.json()) as OllamaResponse;
55
+
56
+ const usage: LlmUsage | undefined =
57
+ json.prompt_eval_count !== undefined && json.eval_count !== undefined
58
+ ? {
59
+ promptTokens: json.prompt_eval_count,
60
+ completionTokens: json.eval_count,
61
+ totalTokens: json.prompt_eval_count + json.eval_count,
62
+ }
63
+ : undefined;
64
+
65
+ if (usage) {
66
+ return {
67
+ text: json.response,
68
+ usage,
69
+ };
70
+ }
71
+
72
+ return {
73
+ text: json.response,
74
+ };
75
+ },
76
+ };
77
+ }