@agentforge-io/llm-langchain 0.1.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
1
  export { OpenAIProvider, type OpenAIProviderOptions, } from './providers/openai-provider';
2
+ export { GeminiProvider, type GeminiProviderOptions, } from './providers/gemini-provider';
3
+ export { GroqProvider, type GroqProviderOptions, } from './providers/groq-provider';
package/dist/index.js CHANGED
@@ -16,6 +16,10 @@
16
16
  // The host wires which provider runs per tenant; this package only ships
17
17
  // the adapters, not the resolution logic.
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.OpenAIProvider = void 0;
19
+ exports.GroqProvider = exports.GeminiProvider = exports.OpenAIProvider = void 0;
20
20
  var openai_provider_1 = require("./providers/openai-provider");
21
21
  Object.defineProperty(exports, "OpenAIProvider", { enumerable: true, get: function () { return openai_provider_1.OpenAIProvider; } });
22
+ var gemini_provider_1 = require("./providers/gemini-provider");
23
+ Object.defineProperty(exports, "GeminiProvider", { enumerable: true, get: function () { return gemini_provider_1.GeminiProvider; } });
24
+ var groq_provider_1 = require("./providers/groq-provider");
25
+ Object.defineProperty(exports, "GroqProvider", { enumerable: true, get: function () { return groq_provider_1.GroqProvider; } });
@@ -0,0 +1,16 @@
1
+ import type { LLMProvider, LLMProviderCapabilities, LLMStreamEvent, LLMStreamParams } from '@agentforge-io/core/ai';
2
+ export interface GeminiProviderOptions {
3
+ apiKey: string;
4
+ /** Provider id surfaced to the platform resolver. Defaults to `'gemini'`. */
5
+ id?: string;
6
+ /** Human-readable label. Defaults to `'Google Gemini'`. */
7
+ displayName?: string;
8
+ }
9
+ export declare class GeminiProvider implements LLMProvider {
10
+ readonly id: string;
11
+ readonly displayName: string;
12
+ readonly capabilities: LLMProviderCapabilities;
13
+ private readonly apiKey;
14
+ constructor(opts: GeminiProviderOptions);
15
+ stream(params: LLMStreamParams): AsyncGenerator<LLMStreamEvent>;
16
+ }
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ // ─── Gemini provider (via LangChain) ────────────────────────────────────────
3
+ //
4
+ // Adapts `@langchain/google-genai`'s `ChatGoogleGenerativeAI` to the
5
+ // framework-free `LLMProvider` contract. Mirrors `OpenAIProvider`'s shape
6
+ // so the rest of the system (registry, resolver, agent runner) treats
7
+ // every provider uniformly.
8
+ //
9
+ // Notable differences from OpenAI:
10
+ // - Gemini's tool-calling event shape uses `tool_calls` on the final
11
+ // AIMessage rather than `tool_call_chunks` arriving incrementally.
12
+ // LangChain normalises both into a similar surface but we read the
13
+ // final tool_calls off the LAST chunk's `.tool_calls` array.
14
+ // - Gemini's stop reasons are STOP / MAX_TOKENS / SAFETY / RECITATION /
15
+ // OTHER. We collapse SAFETY/RECITATION/OTHER into `end_turn` because
16
+ // none of them carry runner-actionable semantics — the model just
17
+ // stopped early. The platform's logging layer reads the raw reason
18
+ // separately for telemetry.
19
+ //
20
+ // Like `OpenAIProvider`, this class doesn't load LangChain modules until
21
+ // `stream()` is called — `ChatGoogleGenerativeAI` is constructed per turn
22
+ // (the model id varies). Safe because the HTTP client is stateless.
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.GeminiProvider = void 0;
25
+ const google_genai_1 = require("@langchain/google-genai");
26
+ const messages_1 = require("@langchain/core/messages");
27
+ class GeminiProvider {
28
+ constructor(opts) {
29
+ this.capabilities = {
30
+ supportsTools: true,
31
+ supportsStreaming: true,
32
+ // Gemini accepts `temperature` on every modern (1.5+) family.
33
+ supportsTemperature: true,
34
+ // Gemini DOES allow multiple function calls per turn — they arrive
35
+ // in the same response's `functionCalls` array, which LangChain
36
+ // surfaces as `tool_calls`.
37
+ supportsParallelTools: true,
38
+ };
39
+ this.id = opts.id ?? 'gemini';
40
+ this.displayName = opts.displayName ?? 'Google Gemini';
41
+ this.apiKey = opts.apiKey;
42
+ }
43
+ async *stream(params) {
44
+ const llm = new google_genai_1.ChatGoogleGenerativeAI({
45
+ apiKey: this.apiKey,
46
+ model: params.model,
47
+ temperature: params.temperature,
48
+ maxOutputTokens: params.maxTokens,
49
+ streaming: true,
50
+ });
51
+ const bound = params.tools && params.tools.length > 0
52
+ ? llm.bindTools(params.tools.map(toLangChainTool))
53
+ : llm;
54
+ const messages = toLangChainMessages(params.systemPrompt, params.messages);
55
+ let textBuffer = '';
56
+ // Gemini emits tool_calls as a finalised list on the closing
57
+ // chunk (not incremental like OpenAI). We track the last chunk's
58
+ // `tool_calls` and parse them at end-of-stream.
59
+ let finalToolCalls = [];
60
+ let usageInput = 0;
61
+ let usageOutput = 0;
62
+ let stopReason = 'end_turn';
63
+ const stream = await bound.stream(messages);
64
+ for await (const chunk of stream) {
65
+ const text = typeof chunk.content === 'string'
66
+ ? chunk.content
67
+ : chunk.content
68
+ .map((c) => typeof c === 'string'
69
+ ? c
70
+ : 'text' in c
71
+ ? c.text
72
+ : '')
73
+ .join('');
74
+ if (text) {
75
+ textBuffer += text;
76
+ yield { type: 'text_delta', delta: text };
77
+ }
78
+ const chunkToolCalls = chunk
79
+ .tool_calls;
80
+ if (chunkToolCalls && chunkToolCalls.length > 0) {
81
+ finalToolCalls = chunkToolCalls;
82
+ }
83
+ const usageMeta = chunk.usage_metadata;
84
+ if (usageMeta) {
85
+ usageInput = usageMeta.input_tokens ?? usageInput;
86
+ usageOutput = usageMeta.output_tokens ?? usageOutput;
87
+ }
88
+ const finishReason = chunk
89
+ .response_metadata?.finish_reason ??
90
+ chunk.finish_reason;
91
+ if (finishReason) {
92
+ stopReason = normalizeFinishReason(finishReason);
93
+ }
94
+ }
95
+ const finalContent = [];
96
+ if (textBuffer) {
97
+ finalContent.push({ type: 'text', text: textBuffer });
98
+ }
99
+ for (const call of finalToolCalls) {
100
+ const id = call.id ?? `call_${Math.random().toString(36).slice(2, 10)}`;
101
+ yield {
102
+ type: 'tool_use_start',
103
+ toolUseId: id,
104
+ toolName: call.name,
105
+ input: call.args,
106
+ };
107
+ finalContent.push({
108
+ type: 'tool_use',
109
+ id,
110
+ name: call.name,
111
+ input: call.args,
112
+ });
113
+ }
114
+ if (finalToolCalls.length > 0 && stopReason !== 'max_tokens') {
115
+ stopReason = 'tool_use';
116
+ }
117
+ yield {
118
+ type: 'usage_delta',
119
+ usage: {
120
+ inputTokens: usageInput,
121
+ outputTokens: usageOutput,
122
+ totalTokens: usageInput + usageOutput,
123
+ },
124
+ };
125
+ yield {
126
+ type: 'message_stop',
127
+ stopReason,
128
+ content: finalContent,
129
+ };
130
+ }
131
+ }
132
+ exports.GeminiProvider = GeminiProvider;
133
+ // ─── Translation helpers ────────────────────────────────────────────────────
134
+ function toLangChainTool(t) {
135
+ return {
136
+ type: 'function',
137
+ function: {
138
+ name: t.name,
139
+ description: t.description,
140
+ parameters: t.input_schema,
141
+ },
142
+ };
143
+ }
144
+ function toLangChainMessages(systemPrompt, messages) {
145
+ const out = [];
146
+ if (systemPrompt) {
147
+ out.push(new messages_1.SystemMessage(systemPrompt));
148
+ }
149
+ for (const m of messages) {
150
+ if (typeof m.content === 'string') {
151
+ out.push(m.role === 'user'
152
+ ? new messages_1.HumanMessage(m.content)
153
+ : new messages_1.AIMessage(m.content));
154
+ continue;
155
+ }
156
+ if (m.role === 'user') {
157
+ const textParts = [];
158
+ for (const block of m.content) {
159
+ if (block.type === 'text') {
160
+ textParts.push(block.text);
161
+ }
162
+ else if (block.type === 'tool_result') {
163
+ if (textParts.length > 0) {
164
+ out.push(new messages_1.HumanMessage(textParts.join('\n')));
165
+ textParts.length = 0;
166
+ }
167
+ out.push(new messages_1.ToolMessage({
168
+ tool_call_id: block.tool_use_id,
169
+ content: block.content,
170
+ status: block.is_error ? 'error' : 'success',
171
+ }));
172
+ }
173
+ }
174
+ if (textParts.length > 0) {
175
+ out.push(new messages_1.HumanMessage(textParts.join('\n')));
176
+ }
177
+ }
178
+ else {
179
+ const textParts = [];
180
+ const toolCalls = [];
181
+ for (const block of m.content) {
182
+ if (block.type === 'text')
183
+ textParts.push(block.text);
184
+ else if (block.type === 'tool_use')
185
+ toolCalls.push({
186
+ id: block.id,
187
+ name: block.name,
188
+ args: block.input,
189
+ type: 'tool_call',
190
+ });
191
+ }
192
+ out.push(new messages_1.AIMessage({
193
+ content: textParts.join('\n'),
194
+ tool_calls: toolCalls,
195
+ }));
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+ function normalizeFinishReason(raw) {
201
+ switch (raw.toUpperCase()) {
202
+ case 'TOOL_CALLS':
203
+ case 'FUNCTION_CALL':
204
+ return 'tool_use';
205
+ case 'MAX_TOKENS':
206
+ case 'LENGTH':
207
+ return 'max_tokens';
208
+ case 'STOP':
209
+ default:
210
+ return 'end_turn';
211
+ }
212
+ }
@@ -0,0 +1,15 @@
1
+ import { OpenAIProvider, type OpenAIProviderOptions } from './openai-provider';
2
+ export interface GroqProviderOptions extends Omit<OpenAIProviderOptions, 'baseURL' | 'organization'> {
3
+ /** Override the default base URL. Only useful for a proxy/test setup;
4
+ * production points at https://api.groq.com/openai/v1. */
5
+ baseURL?: string;
6
+ }
7
+ /**
8
+ * Groq inference provider. Same wire format as OpenAI Chat Completions,
9
+ * just at a different endpoint. The class extends OpenAIProvider so we
10
+ * inherit the entire stream-event translation pipeline — only the
11
+ * default base URL changes.
12
+ */
13
+ export declare class GroqProvider extends OpenAIProvider {
14
+ constructor(opts: GroqProviderOptions);
15
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ // ─── Groq provider ──────────────────────────────────────────────────────────
3
+ //
4
+ // Groq runs OpenAI-compatible inference on their own LPU hardware — same
5
+ // wire format as OpenAI's `/v1/chat/completions`, just at
6
+ // `https://api.groq.com/openai/v1`. So this whole provider is the OpenAI
7
+ // adapter with the base URL pinned + a different id/displayName for
8
+ // telemetry attribution.
9
+ //
10
+ // Worth a separate class (not just an env override on OpenAIProvider)
11
+ // because:
12
+ // - Groq's free tier is the killer feature; the platform wants to
13
+ // attribute spend / quota separately in the dashboard
14
+ // - The model catalog is DIFFERENT (Llama 3, Mixtral, Gemma, etc.) —
15
+ // hardcoding them in the platform registration would lie about
16
+ // which provider answered the turn
17
+ // - Capability flags can diverge: some Groq-hosted models don't
18
+ // expose tool calling even though OpenAI's catalog does
19
+ //
20
+ // Don't confuse with Grok (xAI). They sound alike but:
21
+ // - Grok = xAI's own model family ("grok-2", "grok-2-mini"), served
22
+ // at api.x.ai. Uses GrokProvider in the platform.
23
+ // - Groq = inference service serving open-weight models. Uses THIS
24
+ // provider in the platform.
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.GroqProvider = void 0;
27
+ const openai_provider_1 = require("./openai-provider");
28
+ /**
29
+ * Groq inference provider. Same wire format as OpenAI Chat Completions,
30
+ * just at a different endpoint. The class extends OpenAIProvider so we
31
+ * inherit the entire stream-event translation pipeline — only the
32
+ * default base URL changes.
33
+ */
34
+ class GroqProvider extends openai_provider_1.OpenAIProvider {
35
+ constructor(opts) {
36
+ super({
37
+ apiKey: opts.apiKey,
38
+ baseURL: opts.baseURL ?? 'https://api.groq.com/openai/v1',
39
+ id: opts.id ?? 'groq',
40
+ displayName: opts.displayName ?? 'Groq',
41
+ });
42
+ }
43
+ }
44
+ exports.GroqProvider = GroqProvider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/llm-langchain",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "LangChain-backed LLM providers (OpenAI, Grok, Gemini) implementing the framework-free `LLMProvider` contract from @agentforge-io/core. Drop-in replacements for AnthropicProvider — same stream events, same tool schema, no changes to the agent runner.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",