@aigne/gemini 0.14.2-beta.8 → 0.14.2-beta.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.2-beta.9](https://github.com/AIGNE-io/aigne-framework/compare/gemini-v0.14.2-beta.8...gemini-v0.14.2-beta.9) (2025-10-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **models:** auto retry when got emtpy response from gemini ([#636](https://github.com/AIGNE-io/aigne-framework/issues/636)) ([9367cef](https://github.com/AIGNE-io/aigne-framework/commit/9367cef49ea4c0c87b8a36b454deb2efaee6886f))
9
+ * **models:** enhance gemini model tool use with status fields ([#634](https://github.com/AIGNE-io/aigne-framework/issues/634)) ([067b175](https://github.com/AIGNE-io/aigne-framework/commit/067b175c8e31bb5b1a6d0fc5a5cfb2d070d8d709))
10
+
11
+
12
+ ### Dependencies
13
+
14
+ * The following workspace dependencies were updated
15
+ * dependencies
16
+ * @aigne/core bumped to 1.63.0-beta.9
17
+ * devDependencies
18
+ * @aigne/test-utils bumped to 0.5.55-beta.9
19
+
3
20
  ## [0.14.2-beta.8](https://github.com/AIGNE-io/aigne-framework/compare/gemini-v0.14.2-beta.7...gemini-v0.14.2-beta.8) (2025-10-16)
4
21
 
5
22
 
@@ -1,13 +1,21 @@
1
- import { type AgentInvokeOptions, type AgentProcessResult, type ChatModelInput, type ChatModelOutput } from "@aigne/core";
1
+ import { type AgentProcessResult, ChatModel, type ChatModelInput, type ChatModelOptions, type ChatModelOutput } from "@aigne/core";
2
2
  import { type PromiseOrValue } from "@aigne/core/utils/type-utils.js";
3
- import { OpenAIChatModel, type OpenAIChatModelOptions } from "@aigne/openai";
4
- import { GoogleGenAI } from "@google/genai";
3
+ import { GoogleGenAI, type GoogleGenAIOptions } from "@google/genai";
4
+ export interface GeminiChatModelOptions extends ChatModelOptions {
5
+ /**
6
+ * API key for Gemini API
7
+ *
8
+ * If not provided, will look for GEMINI_API_KEY or GOOGLE_API_KEY in environment variables
9
+ */
10
+ apiKey?: string;
11
+ /**
12
+ * Optional client options for the Gemini SDK
13
+ */
14
+ clientOptions?: Partial<GoogleGenAIOptions>;
15
+ }
5
16
  /**
6
17
  * Implementation of the ChatModel interface for Google's Gemini API
7
18
  *
8
- * This model uses OpenAI-compatible API format to interact with Google's Gemini models,
9
- * providing access to models like Gemini 1.5 and Gemini 2.0.
10
- *
11
19
  * @example
12
20
  * Here's how to create and use a Gemini chat model:
13
21
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model}
@@ -16,19 +24,20 @@ import { GoogleGenAI } from "@google/genai";
16
24
  * Here's an example with streaming response:
17
25
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model-streaming}
18
26
  */
19
- export declare class GeminiChatModel extends OpenAIChatModel {
20
- constructor(options?: OpenAIChatModelOptions);
27
+ export declare class GeminiChatModel extends ChatModel {
28
+ options?: GeminiChatModelOptions | undefined;
29
+ constructor(options?: GeminiChatModelOptions | undefined);
21
30
  protected apiKeyEnvName: string;
22
- protected supportsToolsUseWithJsonSchema: boolean;
23
- protected supportsParallelToolCalls: boolean;
24
- protected supportsToolStreaming: boolean;
25
- protected optionalFieldMode: "optional";
26
31
  protected _googleClient?: GoogleGenAI;
27
32
  get googleClient(): GoogleGenAI;
28
- process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
29
- private handleImageModelProcessing;
33
+ get credential(): {
34
+ apiKey: string | undefined;
35
+ model: string;
36
+ };
37
+ get modelOptions(): Omit<import("@aigne/core").ChatModelInputOptions, "model"> | undefined;
38
+ process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
39
+ private processInput;
30
40
  private buildConfig;
31
41
  private buildTools;
32
42
  private buildContents;
33
- getRunMessages(input: ChatModelInput): ReturnType<OpenAIChatModel["getRunMessages"]>;
34
43
  }
@@ -2,18 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GeminiChatModel = void 0;
4
4
  const core_1 = require("@aigne/core");
5
+ const logger_js_1 = require("@aigne/core/utils/logger.js");
5
6
  const type_utils_js_1 = require("@aigne/core/utils/type-utils.js");
6
- const openai_1 = require("@aigne/openai");
7
7
  const uuid_1 = require("@aigne/uuid");
8
8
  const genai_1 = require("@google/genai");
9
- const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai";
10
9
  const GEMINI_DEFAULT_CHAT_MODEL = "gemini-2.0-flash";
10
+ const OUTPUT_JSON_FUNCTION_NAME = "output_json";
11
11
  /**
12
12
  * Implementation of the ChatModel interface for Google's Gemini API
13
13
  *
14
- * This model uses OpenAI-compatible API format to interact with Google's Gemini models,
15
- * providing access to models like Gemini 1.5 and Gemini 2.0.
16
- *
17
14
  * @example
18
15
  * Here's how to create and use a Gemini chat model:
19
16
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model}
@@ -22,19 +19,16 @@ const GEMINI_DEFAULT_CHAT_MODEL = "gemini-2.0-flash";
22
19
  * Here's an example with streaming response:
23
20
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model-streaming}
24
21
  */
25
- class GeminiChatModel extends openai_1.OpenAIChatModel {
22
+ class GeminiChatModel extends core_1.ChatModel {
23
+ options;
26
24
  constructor(options) {
27
25
  super({
28
26
  ...options,
29
27
  model: options?.model || GEMINI_DEFAULT_CHAT_MODEL,
30
- baseURL: options?.baseURL || GEMINI_BASE_URL,
31
28
  });
29
+ this.options = options;
32
30
  }
33
31
  apiKeyEnvName = "GEMINI_API_KEY";
34
- supportsToolsUseWithJsonSchema = false;
35
- supportsParallelToolCalls = false;
36
- supportsToolStreaming = false;
37
- optionalFieldMode = "optional";
38
32
  _googleClient;
39
33
  get googleClient() {
40
34
  if (this._googleClient)
@@ -42,20 +36,33 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
42
36
  const { apiKey } = this.credential;
43
37
  if (!apiKey)
44
38
  throw new Error(`${this.name} requires an API key. Please provide it via \`options.apiKey\`, or set the \`${this.apiKeyEnvName}\` environment variable`);
45
- this._googleClient ??= new genai_1.GoogleGenAI({ apiKey });
39
+ this._googleClient ??= new genai_1.GoogleGenAI({
40
+ apiKey,
41
+ ...this.options?.clientOptions,
42
+ });
46
43
  return this._googleClient;
47
44
  }
48
- process(input, options) {
49
- const model = input.modelOptions?.model || this.credential.model;
50
- if (!model.includes("image"))
51
- return super.process(input, options);
52
- return this.handleImageModelProcessing(input);
45
+ get credential() {
46
+ const apiKey = this.options?.apiKey ||
47
+ process.env[this.apiKeyEnvName] ||
48
+ process.env.GEMINI_API_KEY ||
49
+ process.env.GOOGLE_API_KEY;
50
+ return {
51
+ apiKey,
52
+ model: this.options?.model || GEMINI_DEFAULT_CHAT_MODEL,
53
+ };
53
54
  }
54
- async *handleImageModelProcessing(input) {
55
+ get modelOptions() {
56
+ return this.options?.modelOptions;
57
+ }
58
+ process(input) {
59
+ return this.processInput(input);
60
+ }
61
+ async *processInput(input) {
55
62
  const model = input.modelOptions?.model || this.credential.model;
56
63
  const { contents, config } = await this.buildContents(input);
57
64
  const parameters = {
58
- model: model,
65
+ model,
59
66
  contents,
60
67
  config: {
61
68
  responseModalities: input.modelOptions?.modalities,
@@ -64,7 +71,6 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
64
71
  frequencyPenalty: input.modelOptions?.frequencyPenalty || this.modelOptions?.frequencyPenalty,
65
72
  presencePenalty: input.modelOptions?.presencePenalty || this.modelOptions?.presencePenalty,
66
73
  ...config,
67
- ...(await this.buildTools(input)),
68
74
  ...(await this.buildConfig(input)),
69
75
  },
70
76
  };
@@ -77,6 +83,7 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
77
83
  const files = [];
78
84
  const toolCalls = [];
79
85
  let text = "";
86
+ let json;
80
87
  for await (const chunk of response) {
81
88
  if (!responseModel && chunk.modelVersion) {
82
89
  responseModel = chunk.modelVersion;
@@ -100,15 +107,20 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
100
107
  });
101
108
  }
102
109
  if (part.functionCall?.name) {
103
- toolCalls.push({
104
- id: part.functionCall.id || (0, uuid_1.v7)(),
105
- type: "function",
106
- function: {
107
- name: part.functionCall.name,
108
- arguments: part.functionCall.args || {},
109
- },
110
- });
111
- yield { delta: { json: { toolCalls } } };
110
+ if (part.functionCall.name === OUTPUT_JSON_FUNCTION_NAME) {
111
+ json = part.functionCall.args;
112
+ }
113
+ else {
114
+ toolCalls.push({
115
+ id: part.functionCall.id || (0, uuid_1.v7)(),
116
+ type: "function",
117
+ function: {
118
+ name: part.functionCall.name,
119
+ arguments: part.functionCall.args || {},
120
+ },
121
+ });
122
+ yield { delta: { json: { toolCalls } } };
123
+ }
112
124
  }
113
125
  }
114
126
  }
@@ -119,15 +131,51 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
119
131
  }
120
132
  }
121
133
  if (input.responseFormat?.type === "json_schema") {
122
- yield { delta: { json: { json: (0, core_1.safeParseJSON)(text) } } };
134
+ if (json) {
135
+ yield { delta: { json: { json } } };
136
+ }
137
+ else if (text) {
138
+ yield { delta: { json: { json: (0, core_1.safeParseJSON)(text) } } };
139
+ }
140
+ else {
141
+ // NOTE: Trigger retry of chat model
142
+ throw new core_1.StructuredOutputError("No JSON response from the model");
143
+ }
144
+ }
145
+ else if (!toolCalls.length) {
146
+ if (!text) {
147
+ logger_js_1.logger.error("No text response from the model", parameters);
148
+ // NOTE: Trigger retry of chat model
149
+ throw new core_1.StructuredOutputError("No text response from the model");
150
+ }
123
151
  }
124
- yield { delta: { json: { usage, files } } };
152
+ yield { delta: { json: { usage, files: files.length ? files : undefined } } };
125
153
  }
126
154
  async buildConfig(input) {
127
155
  const config = {};
156
+ const { tools, toolConfig } = await this.buildTools(input);
157
+ config.tools = tools;
158
+ config.toolConfig = toolConfig;
128
159
  if (input.responseFormat?.type === "json_schema") {
129
- config.responseJsonSchema = input.responseFormat.jsonSchema.schema;
130
- config.responseMimeType = "application/json";
160
+ if (config.tools?.length) {
161
+ config.tools.push({
162
+ functionDeclarations: [
163
+ {
164
+ name: OUTPUT_JSON_FUNCTION_NAME,
165
+ description: "Output the final response in JSON format",
166
+ parametersJsonSchema: input.responseFormat.jsonSchema.schema,
167
+ },
168
+ ],
169
+ });
170
+ config.toolConfig = {
171
+ ...config.toolConfig,
172
+ functionCallingConfig: { mode: genai_1.FunctionCallingConfigMode.ANY },
173
+ };
174
+ }
175
+ else {
176
+ config.responseJsonSchema = input.responseFormat.jsonSchema.schema;
177
+ config.responseMimeType = "application/json";
178
+ }
131
179
  }
132
180
  return config;
133
181
  }
@@ -178,7 +226,7 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
178
226
  return;
179
227
  }
180
228
  const content = {
181
- role: msg.role === "agent" ? "model" : "user",
229
+ role: msg.role === "agent" ? "model" : msg.role === "user" ? "user" : undefined,
182
230
  };
183
231
  if (msg.toolCalls) {
184
232
  content.parts = msg.toolCalls.map((call) => ({
@@ -195,12 +243,31 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
195
243
  .find((c) => c?.id === msg.toolCallId);
196
244
  if (!call)
197
245
  throw new Error(`Tool call not found: ${msg.toolCallId}`);
246
+ const output = JSON.parse(msg.content);
247
+ const isError = "error" in output && Boolean(input.error);
248
+ const response = {
249
+ tool: call.function.name,
250
+ };
251
+ // NOTE: base on the documentation of gemini api, the content should include `output` field for successful result or `error` field for failed result,
252
+ // and base on the actual test, add a tool field presenting the tool name can improve the LLM understanding that which tool is called.
253
+ if (isError) {
254
+ Object.assign(response, { status: "error" }, output);
255
+ }
256
+ else {
257
+ Object.assign(response, { status: "success" });
258
+ if ("output" in output) {
259
+ Object.assign(response, output);
260
+ }
261
+ else {
262
+ Object.assign(response, { output });
263
+ }
264
+ }
198
265
  content.parts = [
199
266
  {
200
267
  functionResponse: {
201
268
  id: msg.toolCallId,
202
269
  name: call.function.name,
203
- response: JSON.parse(msg.content),
270
+ response,
204
271
  },
205
272
  },
206
273
  ];
@@ -224,24 +291,17 @@ class GeminiChatModel extends openai_1.OpenAIChatModel {
224
291
  }
225
292
  return content;
226
293
  }))).filter(type_utils_js_1.isNonNullable);
294
+ if (!result.contents.length && systemParts.length) {
295
+ const system = systemParts.pop();
296
+ if (system) {
297
+ result.contents.push({ role: "user", parts: [system] });
298
+ }
299
+ }
227
300
  if (systemParts.length) {
228
301
  result.config ??= {};
229
302
  result.config.systemInstruction = systemParts;
230
303
  }
231
304
  return result;
232
305
  }
233
- async getRunMessages(input) {
234
- const messages = await super.getRunMessages(input);
235
- if (!messages.some((i) => i.role === "user")) {
236
- for (const msg of messages) {
237
- if (msg.role === "system") {
238
- // Ensure the last message is from the user
239
- msg.role = "user";
240
- break;
241
- }
242
- }
243
- }
244
- return messages;
245
- }
246
306
  }
247
307
  exports.GeminiChatModel = GeminiChatModel;
@@ -1,13 +1,21 @@
1
- import { type AgentInvokeOptions, type AgentProcessResult, type ChatModelInput, type ChatModelOutput } from "@aigne/core";
1
+ import { type AgentProcessResult, ChatModel, type ChatModelInput, type ChatModelOptions, type ChatModelOutput } from "@aigne/core";
2
2
  import { type PromiseOrValue } from "@aigne/core/utils/type-utils.js";
3
- import { OpenAIChatModel, type OpenAIChatModelOptions } from "@aigne/openai";
4
- import { GoogleGenAI } from "@google/genai";
3
+ import { GoogleGenAI, type GoogleGenAIOptions } from "@google/genai";
4
+ export interface GeminiChatModelOptions extends ChatModelOptions {
5
+ /**
6
+ * API key for Gemini API
7
+ *
8
+ * If not provided, will look for GEMINI_API_KEY or GOOGLE_API_KEY in environment variables
9
+ */
10
+ apiKey?: string;
11
+ /**
12
+ * Optional client options for the Gemini SDK
13
+ */
14
+ clientOptions?: Partial<GoogleGenAIOptions>;
15
+ }
5
16
  /**
6
17
  * Implementation of the ChatModel interface for Google's Gemini API
7
18
  *
8
- * This model uses OpenAI-compatible API format to interact with Google's Gemini models,
9
- * providing access to models like Gemini 1.5 and Gemini 2.0.
10
- *
11
19
  * @example
12
20
  * Here's how to create and use a Gemini chat model:
13
21
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model}
@@ -16,19 +24,20 @@ import { GoogleGenAI } from "@google/genai";
16
24
  * Here's an example with streaming response:
17
25
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model-streaming}
18
26
  */
19
- export declare class GeminiChatModel extends OpenAIChatModel {
20
- constructor(options?: OpenAIChatModelOptions);
27
+ export declare class GeminiChatModel extends ChatModel {
28
+ options?: GeminiChatModelOptions | undefined;
29
+ constructor(options?: GeminiChatModelOptions | undefined);
21
30
  protected apiKeyEnvName: string;
22
- protected supportsToolsUseWithJsonSchema: boolean;
23
- protected supportsParallelToolCalls: boolean;
24
- protected supportsToolStreaming: boolean;
25
- protected optionalFieldMode: "optional";
26
31
  protected _googleClient?: GoogleGenAI;
27
32
  get googleClient(): GoogleGenAI;
28
- process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
29
- private handleImageModelProcessing;
33
+ get credential(): {
34
+ apiKey: string | undefined;
35
+ model: string;
36
+ };
37
+ get modelOptions(): Omit<import("@aigne/core").ChatModelInputOptions, "model"> | undefined;
38
+ process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
39
+ private processInput;
30
40
  private buildConfig;
31
41
  private buildTools;
32
42
  private buildContents;
33
- getRunMessages(input: ChatModelInput): ReturnType<OpenAIChatModel["getRunMessages"]>;
34
43
  }
@@ -1,13 +1,21 @@
1
- import { type AgentInvokeOptions, type AgentProcessResult, type ChatModelInput, type ChatModelOutput } from "@aigne/core";
1
+ import { type AgentProcessResult, ChatModel, type ChatModelInput, type ChatModelOptions, type ChatModelOutput } from "@aigne/core";
2
2
  import { type PromiseOrValue } from "@aigne/core/utils/type-utils.js";
3
- import { OpenAIChatModel, type OpenAIChatModelOptions } from "@aigne/openai";
4
- import { GoogleGenAI } from "@google/genai";
3
+ import { GoogleGenAI, type GoogleGenAIOptions } from "@google/genai";
4
+ export interface GeminiChatModelOptions extends ChatModelOptions {
5
+ /**
6
+ * API key for Gemini API
7
+ *
8
+ * If not provided, will look for GEMINI_API_KEY or GOOGLE_API_KEY in environment variables
9
+ */
10
+ apiKey?: string;
11
+ /**
12
+ * Optional client options for the Gemini SDK
13
+ */
14
+ clientOptions?: Partial<GoogleGenAIOptions>;
15
+ }
5
16
  /**
6
17
  * Implementation of the ChatModel interface for Google's Gemini API
7
18
  *
8
- * This model uses OpenAI-compatible API format to interact with Google's Gemini models,
9
- * providing access to models like Gemini 1.5 and Gemini 2.0.
10
- *
11
19
  * @example
12
20
  * Here's how to create and use a Gemini chat model:
13
21
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model}
@@ -16,19 +24,20 @@ import { GoogleGenAI } from "@google/genai";
16
24
  * Here's an example with streaming response:
17
25
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model-streaming}
18
26
  */
19
- export declare class GeminiChatModel extends OpenAIChatModel {
20
- constructor(options?: OpenAIChatModelOptions);
27
+ export declare class GeminiChatModel extends ChatModel {
28
+ options?: GeminiChatModelOptions | undefined;
29
+ constructor(options?: GeminiChatModelOptions | undefined);
21
30
  protected apiKeyEnvName: string;
22
- protected supportsToolsUseWithJsonSchema: boolean;
23
- protected supportsParallelToolCalls: boolean;
24
- protected supportsToolStreaming: boolean;
25
- protected optionalFieldMode: "optional";
26
31
  protected _googleClient?: GoogleGenAI;
27
32
  get googleClient(): GoogleGenAI;
28
- process(input: ChatModelInput, options: AgentInvokeOptions): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
29
- private handleImageModelProcessing;
33
+ get credential(): {
34
+ apiKey: string | undefined;
35
+ model: string;
36
+ };
37
+ get modelOptions(): Omit<import("@aigne/core").ChatModelInputOptions, "model"> | undefined;
38
+ process(input: ChatModelInput): PromiseOrValue<AgentProcessResult<ChatModelOutput>>;
39
+ private processInput;
30
40
  private buildConfig;
31
41
  private buildTools;
32
42
  private buildContents;
33
- getRunMessages(input: ChatModelInput): ReturnType<OpenAIChatModel["getRunMessages"]>;
34
43
  }
@@ -1,16 +1,13 @@
1
- import { safeParseJSON, } from "@aigne/core";
1
+ import { ChatModel, StructuredOutputError, safeParseJSON, } from "@aigne/core";
2
+ import { logger } from "@aigne/core/utils/logger.js";
2
3
  import { isNonNullable } from "@aigne/core/utils/type-utils.js";
3
- import { OpenAIChatModel } from "@aigne/openai";
4
4
  import { v7 } from "@aigne/uuid";
5
5
  import { FunctionCallingConfigMode, GoogleGenAI, } from "@google/genai";
6
- const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai";
7
6
  const GEMINI_DEFAULT_CHAT_MODEL = "gemini-2.0-flash";
7
+ const OUTPUT_JSON_FUNCTION_NAME = "output_json";
8
8
  /**
9
9
  * Implementation of the ChatModel interface for Google's Gemini API
10
10
  *
11
- * This model uses OpenAI-compatible API format to interact with Google's Gemini models,
12
- * providing access to models like Gemini 1.5 and Gemini 2.0.
13
- *
14
11
  * @example
15
12
  * Here's how to create and use a Gemini chat model:
16
13
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model}
@@ -19,19 +16,16 @@ const GEMINI_DEFAULT_CHAT_MODEL = "gemini-2.0-flash";
19
16
  * Here's an example with streaming response:
20
17
  * {@includeCode ../test/gemini-chat-model.test.ts#example-gemini-chat-model-streaming}
21
18
  */
22
- export class GeminiChatModel extends OpenAIChatModel {
19
+ export class GeminiChatModel extends ChatModel {
20
+ options;
23
21
  constructor(options) {
24
22
  super({
25
23
  ...options,
26
24
  model: options?.model || GEMINI_DEFAULT_CHAT_MODEL,
27
- baseURL: options?.baseURL || GEMINI_BASE_URL,
28
25
  });
26
+ this.options = options;
29
27
  }
30
28
  apiKeyEnvName = "GEMINI_API_KEY";
31
- supportsToolsUseWithJsonSchema = false;
32
- supportsParallelToolCalls = false;
33
- supportsToolStreaming = false;
34
- optionalFieldMode = "optional";
35
29
  _googleClient;
36
30
  get googleClient() {
37
31
  if (this._googleClient)
@@ -39,20 +33,33 @@ export class GeminiChatModel extends OpenAIChatModel {
39
33
  const { apiKey } = this.credential;
40
34
  if (!apiKey)
41
35
  throw new Error(`${this.name} requires an API key. Please provide it via \`options.apiKey\`, or set the \`${this.apiKeyEnvName}\` environment variable`);
42
- this._googleClient ??= new GoogleGenAI({ apiKey });
36
+ this._googleClient ??= new GoogleGenAI({
37
+ apiKey,
38
+ ...this.options?.clientOptions,
39
+ });
43
40
  return this._googleClient;
44
41
  }
45
- process(input, options) {
46
- const model = input.modelOptions?.model || this.credential.model;
47
- if (!model.includes("image"))
48
- return super.process(input, options);
49
- return this.handleImageModelProcessing(input);
42
+ get credential() {
43
+ const apiKey = this.options?.apiKey ||
44
+ process.env[this.apiKeyEnvName] ||
45
+ process.env.GEMINI_API_KEY ||
46
+ process.env.GOOGLE_API_KEY;
47
+ return {
48
+ apiKey,
49
+ model: this.options?.model || GEMINI_DEFAULT_CHAT_MODEL,
50
+ };
50
51
  }
51
- async *handleImageModelProcessing(input) {
52
+ get modelOptions() {
53
+ return this.options?.modelOptions;
54
+ }
55
+ process(input) {
56
+ return this.processInput(input);
57
+ }
58
+ async *processInput(input) {
52
59
  const model = input.modelOptions?.model || this.credential.model;
53
60
  const { contents, config } = await this.buildContents(input);
54
61
  const parameters = {
55
- model: model,
62
+ model,
56
63
  contents,
57
64
  config: {
58
65
  responseModalities: input.modelOptions?.modalities,
@@ -61,7 +68,6 @@ export class GeminiChatModel extends OpenAIChatModel {
61
68
  frequencyPenalty: input.modelOptions?.frequencyPenalty || this.modelOptions?.frequencyPenalty,
62
69
  presencePenalty: input.modelOptions?.presencePenalty || this.modelOptions?.presencePenalty,
63
70
  ...config,
64
- ...(await this.buildTools(input)),
65
71
  ...(await this.buildConfig(input)),
66
72
  },
67
73
  };
@@ -74,6 +80,7 @@ export class GeminiChatModel extends OpenAIChatModel {
74
80
  const files = [];
75
81
  const toolCalls = [];
76
82
  let text = "";
83
+ let json;
77
84
  for await (const chunk of response) {
78
85
  if (!responseModel && chunk.modelVersion) {
79
86
  responseModel = chunk.modelVersion;
@@ -97,15 +104,20 @@ export class GeminiChatModel extends OpenAIChatModel {
97
104
  });
98
105
  }
99
106
  if (part.functionCall?.name) {
100
- toolCalls.push({
101
- id: part.functionCall.id || v7(),
102
- type: "function",
103
- function: {
104
- name: part.functionCall.name,
105
- arguments: part.functionCall.args || {},
106
- },
107
- });
108
- yield { delta: { json: { toolCalls } } };
107
+ if (part.functionCall.name === OUTPUT_JSON_FUNCTION_NAME) {
108
+ json = part.functionCall.args;
109
+ }
110
+ else {
111
+ toolCalls.push({
112
+ id: part.functionCall.id || v7(),
113
+ type: "function",
114
+ function: {
115
+ name: part.functionCall.name,
116
+ arguments: part.functionCall.args || {},
117
+ },
118
+ });
119
+ yield { delta: { json: { toolCalls } } };
120
+ }
109
121
  }
110
122
  }
111
123
  }
@@ -116,15 +128,51 @@ export class GeminiChatModel extends OpenAIChatModel {
116
128
  }
117
129
  }
118
130
  if (input.responseFormat?.type === "json_schema") {
119
- yield { delta: { json: { json: safeParseJSON(text) } } };
131
+ if (json) {
132
+ yield { delta: { json: { json } } };
133
+ }
134
+ else if (text) {
135
+ yield { delta: { json: { json: safeParseJSON(text) } } };
136
+ }
137
+ else {
138
+ // NOTE: Trigger retry of chat model
139
+ throw new StructuredOutputError("No JSON response from the model");
140
+ }
141
+ }
142
+ else if (!toolCalls.length) {
143
+ if (!text) {
144
+ logger.error("No text response from the model", parameters);
145
+ // NOTE: Trigger retry of chat model
146
+ throw new StructuredOutputError("No text response from the model");
147
+ }
120
148
  }
121
- yield { delta: { json: { usage, files } } };
149
+ yield { delta: { json: { usage, files: files.length ? files : undefined } } };
122
150
  }
123
151
  async buildConfig(input) {
124
152
  const config = {};
153
+ const { tools, toolConfig } = await this.buildTools(input);
154
+ config.tools = tools;
155
+ config.toolConfig = toolConfig;
125
156
  if (input.responseFormat?.type === "json_schema") {
126
- config.responseJsonSchema = input.responseFormat.jsonSchema.schema;
127
- config.responseMimeType = "application/json";
157
+ if (config.tools?.length) {
158
+ config.tools.push({
159
+ functionDeclarations: [
160
+ {
161
+ name: OUTPUT_JSON_FUNCTION_NAME,
162
+ description: "Output the final response in JSON format",
163
+ parametersJsonSchema: input.responseFormat.jsonSchema.schema,
164
+ },
165
+ ],
166
+ });
167
+ config.toolConfig = {
168
+ ...config.toolConfig,
169
+ functionCallingConfig: { mode: FunctionCallingConfigMode.ANY },
170
+ };
171
+ }
172
+ else {
173
+ config.responseJsonSchema = input.responseFormat.jsonSchema.schema;
174
+ config.responseMimeType = "application/json";
175
+ }
128
176
  }
129
177
  return config;
130
178
  }
@@ -175,7 +223,7 @@ export class GeminiChatModel extends OpenAIChatModel {
175
223
  return;
176
224
  }
177
225
  const content = {
178
- role: msg.role === "agent" ? "model" : "user",
226
+ role: msg.role === "agent" ? "model" : msg.role === "user" ? "user" : undefined,
179
227
  };
180
228
  if (msg.toolCalls) {
181
229
  content.parts = msg.toolCalls.map((call) => ({
@@ -192,12 +240,31 @@ export class GeminiChatModel extends OpenAIChatModel {
192
240
  .find((c) => c?.id === msg.toolCallId);
193
241
  if (!call)
194
242
  throw new Error(`Tool call not found: ${msg.toolCallId}`);
243
+ const output = JSON.parse(msg.content);
244
+ const isError = "error" in output && Boolean(input.error);
245
+ const response = {
246
+ tool: call.function.name,
247
+ };
248
+ // NOTE: base on the documentation of gemini api, the content should include `output` field for successful result or `error` field for failed result,
249
+ // and base on the actual test, add a tool field presenting the tool name can improve the LLM understanding that which tool is called.
250
+ if (isError) {
251
+ Object.assign(response, { status: "error" }, output);
252
+ }
253
+ else {
254
+ Object.assign(response, { status: "success" });
255
+ if ("output" in output) {
256
+ Object.assign(response, output);
257
+ }
258
+ else {
259
+ Object.assign(response, { output });
260
+ }
261
+ }
195
262
  content.parts = [
196
263
  {
197
264
  functionResponse: {
198
265
  id: msg.toolCallId,
199
266
  name: call.function.name,
200
- response: JSON.parse(msg.content),
267
+ response,
201
268
  },
202
269
  },
203
270
  ];
@@ -221,23 +288,16 @@ export class GeminiChatModel extends OpenAIChatModel {
221
288
  }
222
289
  return content;
223
290
  }))).filter(isNonNullable);
291
+ if (!result.contents.length && systemParts.length) {
292
+ const system = systemParts.pop();
293
+ if (system) {
294
+ result.contents.push({ role: "user", parts: [system] });
295
+ }
296
+ }
224
297
  if (systemParts.length) {
225
298
  result.config ??= {};
226
299
  result.config.systemInstruction = systemParts;
227
300
  }
228
301
  return result;
229
302
  }
230
- async getRunMessages(input) {
231
- const messages = await super.getRunMessages(input);
232
- if (!messages.some((i) => i.role === "user")) {
233
- for (const msg of messages) {
234
- if (msg.role === "system") {
235
- // Ensure the last message is from the user
236
- msg.role = "user";
237
- break;
238
- }
239
- }
240
- }
241
- return messages;
242
- }
243
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/gemini",
3
- "version": "0.14.2-beta.8",
3
+ "version": "0.14.2-beta.9",
4
4
  "description": "AIGNE Gemini SDK for integrating with Google's Gemini AI models",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -39,7 +39,7 @@
39
39
  "@google/genai": "^1.24.0",
40
40
  "zod": "^3.25.67",
41
41
  "@aigne/platform-helpers": "^0.6.3",
42
- "@aigne/openai": "^0.16.2-beta.8"
42
+ "@aigne/core": "^1.63.0-beta.9"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/bun": "^1.2.22",
@@ -47,8 +47,7 @@
47
47
  "npm-run-all": "^4.1.5",
48
48
  "rimraf": "^6.0.1",
49
49
  "typescript": "^5.9.2",
50
- "@aigne/core": "^1.63.0-beta.8",
51
- "@aigne/test-utils": "^0.5.55-beta.8"
50
+ "@aigne/test-utils": "^0.5.55-beta.9"
52
51
  },
53
52
  "scripts": {
54
53
  "lint": "tsc --noEmit",