bitbucket-gemini-action 1.0.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/.claude/settings.local.json +8 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +150 -0
- package/README.md +375 -0
- package/bitbucket-pipelines.yml +95 -0
- package/bun.lock +227 -0
- package/dist/prepare.js +7111 -0
- package/examples/bitbucket-pipelines-full.yml +157 -0
- package/examples/bitbucket-pipelines-minimal.yml +22 -0
- package/package.json +33 -0
- package/src/bitbucket/api/client.ts +406 -0
- package/src/bitbucket/context.ts +196 -0
- package/src/bitbucket/data/fetcher.ts +195 -0
- package/src/bitbucket/data/formatter.ts +221 -0
- package/src/bitbucket/operations/comments.ts +236 -0
- package/src/bitbucket/types.ts +262 -0
- package/src/bitbucket/validation/permissions.ts +154 -0
- package/src/bitbucket/validation/trigger.ts +175 -0
- package/src/entrypoints/execute.ts +349 -0
- package/src/entrypoints/prepare.ts +216 -0
- package/src/gemini/client.ts +263 -0
- package/src/gemini/presets.ts +2130 -0
- package/src/gemini/prompts.ts +331 -0
- package/src/gemini/tools.ts +226 -0
- package/src/index.ts +71 -0
- package/src/modes/agent/index.ts +119 -0
- package/src/modes/registry.ts +118 -0
- package/src/modes/tag/index.ts +172 -0
- package/src/modes/types.ts +95 -0
- package/src/utils/env.ts +190 -0
- package/src/utils/retry.ts +149 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare Entrypoint
|
|
3
|
+
* Phase 1: Initialize context, validate triggers, prepare for execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseBitbucketContext } from "../bitbucket/context.js";
|
|
7
|
+
import { createBitbucketClientFromEnv } from "../bitbucket/api/client.js";
|
|
8
|
+
import { detectMode } from "../modes/registry.js";
|
|
9
|
+
import { validateTrigger } from "../bitbucket/validation/trigger.js";
|
|
10
|
+
import { validateActor } from "../bitbucket/validation/permissions.js";
|
|
11
|
+
import { fetchPRData } from "../bitbucket/data/fetcher.js";
|
|
12
|
+
import { formatPRContext } from "../bitbucket/data/formatter.js";
|
|
13
|
+
import { getEnvConfig, getPipelineUrl, getEnvBool } from "../utils/env.js";
|
|
14
|
+
import type { ParsedBitbucketContext } from "../bitbucket/types.js";
|
|
15
|
+
import type { ModeName } from "../modes/types.js";
|
|
16
|
+
|
|
17
|
+
export interface PrepareResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
shouldContinue: boolean;
|
|
20
|
+
context?: ParsedBitbucketContext;
|
|
21
|
+
prompt?: string;
|
|
22
|
+
systemPrompt?: string;
|
|
23
|
+
trackingCommentId?: number;
|
|
24
|
+
prContext?: ReturnType<typeof formatPRContext>;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Main prepare function
|
|
30
|
+
*/
|
|
31
|
+
export async function prepare(): Promise<PrepareResult> {
|
|
32
|
+
console.log("🚀 Starting Bitbucket Gemini Action - Prepare Phase");
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Load configuration
|
|
36
|
+
const config = getEnvConfig();
|
|
37
|
+
console.log(`📋 Workspace: ${config.BITBUCKET_WORKSPACE}`);
|
|
38
|
+
console.log(`📋 Repository: ${config.BITBUCKET_REPO_SLUG}`);
|
|
39
|
+
console.log(`📋 Trigger Phrase: ${config.TRIGGER_PHRASE}`);
|
|
40
|
+
|
|
41
|
+
// Parse Bitbucket context
|
|
42
|
+
const context = parseBitbucketContext();
|
|
43
|
+
console.log(`📋 Event Type: ${context.eventType}`);
|
|
44
|
+
console.log(`📋 Actor: ${context.actor.display_name}`);
|
|
45
|
+
|
|
46
|
+
if (context.entityNumber) {
|
|
47
|
+
console.log(`📋 PR Number: ${context.entityNumber}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create Bitbucket client
|
|
51
|
+
const client = createBitbucketClientFromEnv();
|
|
52
|
+
|
|
53
|
+
// Validate trigger
|
|
54
|
+
const triggerResult = validateTrigger(context, {
|
|
55
|
+
triggerPhrase: config.TRIGGER_PHRASE,
|
|
56
|
+
prompt: config.PROMPT,
|
|
57
|
+
botAccountId: config.BOT_ACCOUNT_ID,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!triggerResult.shouldTrigger) {
|
|
61
|
+
console.log(`⏭️ Skipping: ${triggerResult.reason}`);
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
shouldContinue: false,
|
|
65
|
+
error: triggerResult.reason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`✅ Trigger validated: ${triggerResult.reason}`);
|
|
70
|
+
|
|
71
|
+
// Validate actor permissions
|
|
72
|
+
const actorResult = await validateActor(
|
|
73
|
+
client,
|
|
74
|
+
context.workspace,
|
|
75
|
+
context.repoSlug,
|
|
76
|
+
context.actor,
|
|
77
|
+
{
|
|
78
|
+
requireWritePermission: true,
|
|
79
|
+
allowBots: false,
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!actorResult.valid) {
|
|
84
|
+
console.log(`⏭️ Skipping: ${actorResult.reason}`);
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
shouldContinue: false,
|
|
88
|
+
error: actorResult.reason,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log("✅ Actor validation passed");
|
|
93
|
+
|
|
94
|
+
// Detect mode
|
|
95
|
+
const { mode, reason } = detectMode(context, {
|
|
96
|
+
triggerPhrase: config.TRIGGER_PHRASE,
|
|
97
|
+
prompt: config.PROMPT,
|
|
98
|
+
mode: config.MODE as ModeName | undefined,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log(`📋 Mode: ${mode.name} (${reason})`);
|
|
102
|
+
|
|
103
|
+
// Prepare mode context
|
|
104
|
+
const createTrackingComment = getEnvBool("CREATE_TRACKING_COMMENT", true);
|
|
105
|
+
const modeResult = await mode.prepare({
|
|
106
|
+
context,
|
|
107
|
+
client,
|
|
108
|
+
triggerPhrase: config.TRIGGER_PHRASE,
|
|
109
|
+
prompt: config.PROMPT,
|
|
110
|
+
createTrackingComment,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!modeResult.success) {
|
|
114
|
+
console.error(`❌ Mode preparation failed: ${modeResult.error}`);
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
shouldContinue: false,
|
|
118
|
+
context,
|
|
119
|
+
error: modeResult.error,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fetch PR data if in PR context
|
|
124
|
+
let prContext: ReturnType<typeof formatPRContext> | undefined;
|
|
125
|
+
if (context.isPR && context.entityNumber) {
|
|
126
|
+
console.log("📥 Fetching PR data...");
|
|
127
|
+
const prData = await fetchPRData(client, context, {
|
|
128
|
+
includeComments: true,
|
|
129
|
+
includeDiff: true,
|
|
130
|
+
includeCommits: true,
|
|
131
|
+
filterCommentsAfter: context.triggerTimestamp,
|
|
132
|
+
});
|
|
133
|
+
prContext = formatPRContext(prData);
|
|
134
|
+
console.log(`📥 Fetched ${prContext.files.length} changed files`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log("✅ Prepare phase completed successfully");
|
|
138
|
+
|
|
139
|
+
// Output results for next phase
|
|
140
|
+
outputResults({
|
|
141
|
+
containsTrigger: true,
|
|
142
|
+
mode: mode.name,
|
|
143
|
+
trackingCommentId: modeResult.trackingCommentId,
|
|
144
|
+
prompt: triggerResult.userMessage || config.PROMPT || "",
|
|
145
|
+
systemPrompt: modeResult.modeContext.systemPrompt,
|
|
146
|
+
tools: modeResult.modeContext.tools,
|
|
147
|
+
pipelineUrl: getPipelineUrl(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
shouldContinue: true,
|
|
153
|
+
context,
|
|
154
|
+
prompt: triggerResult.userMessage || config.PROMPT,
|
|
155
|
+
systemPrompt: modeResult.modeContext.systemPrompt,
|
|
156
|
+
trackingCommentId: modeResult.trackingCommentId,
|
|
157
|
+
prContext,
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
161
|
+
console.error(`❌ Prepare phase failed: ${message}`);
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
shouldContinue: false,
|
|
165
|
+
error: message,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Output results for pipeline consumption
|
|
172
|
+
*/
|
|
173
|
+
function outputResults(results: {
|
|
174
|
+
containsTrigger: boolean;
|
|
175
|
+
mode: string;
|
|
176
|
+
trackingCommentId?: number;
|
|
177
|
+
prompt: string;
|
|
178
|
+
systemPrompt: string;
|
|
179
|
+
tools: string[];
|
|
180
|
+
pipelineUrl?: string;
|
|
181
|
+
}) {
|
|
182
|
+
// Write to files for pipeline use
|
|
183
|
+
const fs = require("fs");
|
|
184
|
+
const outputDir = process.env.BITBUCKET_CLONE_DIR || ".";
|
|
185
|
+
|
|
186
|
+
const outputPath = `${outputDir}/.gemini-action-output.json`;
|
|
187
|
+
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
188
|
+
console.log(`📝 Output written to ${outputPath}`);
|
|
189
|
+
|
|
190
|
+
// Also set environment variables for immediate use
|
|
191
|
+
console.log(`::set-output name=contains_trigger::${results.containsTrigger}`);
|
|
192
|
+
console.log(`::set-output name=mode::${results.mode}`);
|
|
193
|
+
if (results.trackingCommentId) {
|
|
194
|
+
console.log(
|
|
195
|
+
`::set-output name=tracking_comment_id::${results.trackingCommentId}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Run if executed directly
|
|
201
|
+
if (import.meta.main) {
|
|
202
|
+
prepare()
|
|
203
|
+
.then((result) => {
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
if (!result.shouldContinue) {
|
|
208
|
+
console.log("ℹ️ No action needed");
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.catch((error) => {
|
|
213
|
+
console.error("Fatal error:", error);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini API Client
|
|
3
|
+
* Wrapper for @google/generative-ai SDK
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
GoogleGenerativeAI,
|
|
8
|
+
GenerativeModel,
|
|
9
|
+
GenerationConfig,
|
|
10
|
+
Content,
|
|
11
|
+
Part,
|
|
12
|
+
FunctionDeclaration,
|
|
13
|
+
Tool,
|
|
14
|
+
FunctionCallingMode,
|
|
15
|
+
type GenerateContentResult,
|
|
16
|
+
} from "@google/generative-ai";
|
|
17
|
+
|
|
18
|
+
export interface GeminiClientOptions {
|
|
19
|
+
apiKey: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
generationConfig?: GenerationConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GeminiMessage {
|
|
25
|
+
role: "user" | "model";
|
|
26
|
+
parts: Part[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GeminiToolCall {
|
|
30
|
+
name: string;
|
|
31
|
+
args: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GeminiResponse {
|
|
35
|
+
text: string;
|
|
36
|
+
toolCalls?: GeminiToolCall[];
|
|
37
|
+
finishReason: string;
|
|
38
|
+
usage?: {
|
|
39
|
+
promptTokenCount: number;
|
|
40
|
+
candidatesTokenCount: number;
|
|
41
|
+
totalTokenCount: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_MODEL = "gemini-2.0-flash";
|
|
46
|
+
|
|
47
|
+
const DEFAULT_GENERATION_CONFIG: GenerationConfig = {
|
|
48
|
+
temperature: 0.7,
|
|
49
|
+
topP: 0.95,
|
|
50
|
+
topK: 40,
|
|
51
|
+
maxOutputTokens: 8192,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export class GeminiClient {
|
|
55
|
+
private genAI: GoogleGenerativeAI;
|
|
56
|
+
private model: GenerativeModel;
|
|
57
|
+
private modelName: string;
|
|
58
|
+
private generationConfig: GenerationConfig;
|
|
59
|
+
|
|
60
|
+
constructor(options: GeminiClientOptions) {
|
|
61
|
+
this.genAI = new GoogleGenerativeAI(options.apiKey);
|
|
62
|
+
this.modelName = options.model || DEFAULT_MODEL;
|
|
63
|
+
this.generationConfig = {
|
|
64
|
+
...DEFAULT_GENERATION_CONFIG,
|
|
65
|
+
...options.generationConfig,
|
|
66
|
+
};
|
|
67
|
+
this.model = this.genAI.getGenerativeModel({
|
|
68
|
+
model: this.modelName,
|
|
69
|
+
generationConfig: this.generationConfig,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate content with a simple prompt
|
|
75
|
+
*/
|
|
76
|
+
async generateContent(prompt: string): Promise<GeminiResponse> {
|
|
77
|
+
const result = await this.model.generateContent(prompt);
|
|
78
|
+
return this.parseResponse(result);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate content with chat history
|
|
83
|
+
*/
|
|
84
|
+
async generateWithHistory(
|
|
85
|
+
history: GeminiMessage[],
|
|
86
|
+
prompt: string
|
|
87
|
+
): Promise<GeminiResponse> {
|
|
88
|
+
const chat = this.model.startChat({
|
|
89
|
+
history: history.map((msg) => ({
|
|
90
|
+
role: msg.role,
|
|
91
|
+
parts: msg.parts,
|
|
92
|
+
})),
|
|
93
|
+
generationConfig: this.generationConfig,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = await chat.sendMessage(prompt);
|
|
97
|
+
return this.parseResponse(result);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate content with system instruction
|
|
102
|
+
*/
|
|
103
|
+
async generateWithSystemPrompt(
|
|
104
|
+
systemPrompt: string,
|
|
105
|
+
userPrompt: string
|
|
106
|
+
): Promise<GeminiResponse> {
|
|
107
|
+
const modelWithSystem = this.genAI.getGenerativeModel({
|
|
108
|
+
model: this.modelName,
|
|
109
|
+
systemInstruction: systemPrompt,
|
|
110
|
+
generationConfig: this.generationConfig,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await modelWithSystem.generateContent(userPrompt);
|
|
114
|
+
return this.parseResponse(result);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate content with function calling (tools)
|
|
119
|
+
*/
|
|
120
|
+
async generateWithTools(
|
|
121
|
+
prompt: string,
|
|
122
|
+
tools: FunctionDeclaration[],
|
|
123
|
+
systemPrompt?: string
|
|
124
|
+
): Promise<GeminiResponse> {
|
|
125
|
+
const toolConfig: Tool[] = [
|
|
126
|
+
{
|
|
127
|
+
functionDeclarations: tools,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const modelWithTools = this.genAI.getGenerativeModel({
|
|
132
|
+
model: this.modelName,
|
|
133
|
+
systemInstruction: systemPrompt,
|
|
134
|
+
generationConfig: this.generationConfig,
|
|
135
|
+
tools: toolConfig,
|
|
136
|
+
toolConfig: {
|
|
137
|
+
functionCallingConfig: {
|
|
138
|
+
mode: FunctionCallingMode.AUTO,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await modelWithTools.generateContent(prompt);
|
|
144
|
+
return this.parseResponse(result);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Start a chat session for multi-turn conversation
|
|
149
|
+
*/
|
|
150
|
+
startChat(options?: {
|
|
151
|
+
history?: Content[];
|
|
152
|
+
systemInstruction?: string;
|
|
153
|
+
tools?: FunctionDeclaration[];
|
|
154
|
+
}) {
|
|
155
|
+
const modelConfig: {
|
|
156
|
+
model: string;
|
|
157
|
+
generationConfig: GenerationConfig;
|
|
158
|
+
systemInstruction?: string;
|
|
159
|
+
tools?: Tool[];
|
|
160
|
+
} = {
|
|
161
|
+
model: this.modelName,
|
|
162
|
+
generationConfig: this.generationConfig,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (options?.systemInstruction) {
|
|
166
|
+
modelConfig.systemInstruction = options.systemInstruction;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (options?.tools) {
|
|
170
|
+
modelConfig.tools = [
|
|
171
|
+
{
|
|
172
|
+
functionDeclarations: options.tools,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const model = this.genAI.getGenerativeModel(modelConfig);
|
|
178
|
+
|
|
179
|
+
return model.startChat({
|
|
180
|
+
history: options?.history || [],
|
|
181
|
+
generationConfig: this.generationConfig,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Count tokens for a given content
|
|
187
|
+
*/
|
|
188
|
+
async countTokens(content: string | Part[]): Promise<number> {
|
|
189
|
+
const result = await this.model.countTokens(content);
|
|
190
|
+
return result.totalTokens;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private parseResponse(result: GenerateContentResult): GeminiResponse {
|
|
194
|
+
const response = result.response;
|
|
195
|
+
const candidate = response.candidates?.[0];
|
|
196
|
+
|
|
197
|
+
if (!candidate) {
|
|
198
|
+
throw new GeminiApiError("No response candidates returned", "NO_CANDIDATES");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const text =
|
|
202
|
+
candidate.content?.parts
|
|
203
|
+
?.filter((part): part is { text: string } => "text" in part)
|
|
204
|
+
.map((part) => part.text)
|
|
205
|
+
.join("") || "";
|
|
206
|
+
|
|
207
|
+
// Extract function calls if present
|
|
208
|
+
const toolCalls = candidate.content?.parts
|
|
209
|
+
?.filter(
|
|
210
|
+
(part): part is { functionCall: { name: string; args: Record<string, unknown> } } =>
|
|
211
|
+
"functionCall" in part
|
|
212
|
+
)
|
|
213
|
+
.map((part) => ({
|
|
214
|
+
name: part.functionCall.name,
|
|
215
|
+
args: part.functionCall.args,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
text,
|
|
220
|
+
toolCalls: toolCalls?.length ? toolCalls : undefined,
|
|
221
|
+
finishReason: candidate.finishReason || "UNKNOWN",
|
|
222
|
+
usage: response.usageMetadata
|
|
223
|
+
? {
|
|
224
|
+
promptTokenCount: response.usageMetadata.promptTokenCount || 0,
|
|
225
|
+
candidatesTokenCount:
|
|
226
|
+
response.usageMetadata.candidatesTokenCount || 0,
|
|
227
|
+
totalTokenCount: response.usageMetadata.totalTokenCount || 0,
|
|
228
|
+
}
|
|
229
|
+
: undefined,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export class GeminiApiError extends Error {
|
|
235
|
+
constructor(
|
|
236
|
+
message: string,
|
|
237
|
+
public code: string,
|
|
238
|
+
public details?: unknown
|
|
239
|
+
) {
|
|
240
|
+
super(message);
|
|
241
|
+
this.name = "GeminiApiError";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create Gemini client from environment variables
|
|
247
|
+
*/
|
|
248
|
+
export function createGeminiClientFromEnv(): GeminiClient {
|
|
249
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
250
|
+
|
|
251
|
+
if (!apiKey) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_API_KEY environment variable"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const model = process.env.GEMINI_MODEL || DEFAULT_MODEL;
|
|
258
|
+
|
|
259
|
+
return new GeminiClient({
|
|
260
|
+
apiKey,
|
|
261
|
+
model,
|
|
262
|
+
});
|
|
263
|
+
}
|