@agentforge-io/llm-langchain 0.1.0 → 0.2.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,2 @@
1
1
  export { OpenAIProvider, type OpenAIProviderOptions, } from './providers/openai-provider';
2
+ export { GeminiProvider, type GeminiProviderOptions, } from './providers/gemini-provider';
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@
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.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; } });
@@ -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
+ }
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.2.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",