Skip to content

Commit dbabd4d

Browse files
committed
feat: add Gemini LLM provider
1 parent 9842768 commit dbabd4d

16 files changed

+408
-50
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A GitHub Action that scans your codebase for inline TODOs, FIXMEs, and BUG comme
77
## 🚀 Features
88

99
- ✅ Detects `TODO`, `FIXME`, `BUG`, and `HACK` comments
10-
- ✅ Supports multiple languages: `.ts`, `.js`, `.py`, `.go`, `.html`, etc.
10+
- ✅ Supports many languages: `.ts`, `.js`, `.py`, `.go`, `.c`, `.cpp`, `.rs`, `.html`, `.yaml`, etc.
1111
- ✅ Skips common directories like `node_modules`, `dist`, and `coverage`
1212
- ✅ Extracts metadata like `priority`, `due`, etc.
1313
- ✅ Parses structured tags (`@assignee`, `#module`, `key=value`)
@@ -17,6 +17,8 @@ A GitHub Action that scans your codebase for inline TODOs, FIXMEs, and BUG comme
1717
- ✅ Supports custom label colors and descriptions via JSON config
1818
- ✅ Custom templates for issue titles and bodies
1919
- ✅ LLM-powered issue title and body generation
20+
- ✅ Automatic retry logic for OpenAI API calls
21+
- ✅ Supports multiple LLM providers: OpenAI or Gemini
2022
- ✅ Command-line interface for local usage
2123
- ✅ Optional Jira synchronization
2224

@@ -55,8 +57,12 @@ jobs:
5557
with:
5658
repo-token: ${{ secrets.GITHUB_TOKEN }}
5759
limit: 5
60+
llm: true
61+
llm-provider: openai # or 'gemini'
5862
```
5963
64+
Set `OPENAI_API_KEY` or `GEMINI_API_KEY` secrets based on your chosen provider.
65+
6066
### 2. Run the CLI locally
6167

6268
Use the bundled command-line interface to scan a directory on your machine and

action.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ inputs:
3939
description: Use LLM to generate issue titles and bodies
4040
default: 'false'
4141

42+
llm-provider:
43+
required: false
44+
description: LLM provider to use (`openai` or `gemini`)
45+
default: openai
46+
4247
openai-api-key:
4348
required: false
4449
description: 'OpenAI API key used when `llm` is true'
@@ -48,6 +53,15 @@ inputs:
4853
description: OpenAI model to use (e.g., `gpt-3.5-turbo`, `gpt-4`)
4954
default: gpt-3.5-turbo
5055

56+
gemini-api-key:
57+
required: false
58+
description: Gemini API key used when `llm-provider` is `gemini`
59+
60+
gemini-model:
61+
required: false
62+
description: Gemini model to use (e.g., `gemini-1.5-pro`)
63+
default: gemini-1.5-pro
64+
5165
sync-to-jira:
5266
required: false
5367
default: 'false'

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"dependencies": {
2727
"@actions/core": "^1.10.0",
2828
"@actions/github": "^6.0.1",
29+
"@google/genai": "^1.4.0",
2930
"@jest/globals": "^29.7.0",
3031
"@octokit/rest": "^21.1.1",
3132
"@types/jest": "^29.5.14",

src/ActionMain.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,20 @@ async function run(): Promise<void> {
2323
const workspace = process.env.GITHUB_WORKSPACE || '.';
2424

2525
// LLM support
26-
process.env.OPENAI_API_KEY = core.getInput('openai-api-key') || process.env.OPENAI_API_KEY;
26+
const llmProvider = core.getInput('llm-provider') || 'openai';
27+
process.env.LLM_PROVIDER = llmProvider;
28+
if (llmProvider === 'gemini') {
29+
process.env.GEMINI_API_KEY = core.getInput('gemini-api-key') || process.env.GEMINI_API_KEY;
30+
} else {
31+
process.env.OPENAI_API_KEY = core.getInput('openai-api-key') || process.env.OPENAI_API_KEY;
32+
}
2733
const useLLM = core.getInput('llm') === 'true';
28-
if (useLLM && !process.env.OPENAI_API_KEY) {
34+
if (useLLM && llmProvider === 'openai' && !process.env.OPENAI_API_KEY) {
2935
core.warning('⚠️ LLM is enabled, but OPENAI_API_KEY is not set.');
3036
}
37+
if (useLLM && llmProvider === 'gemini' && !process.env.GEMINI_API_KEY) {
38+
core.warning('⚠️ LLM is enabled, but GEMINI_API_KEY is not set.');
39+
}
3140

3241
const useStructured = core.getInput('structured') === 'true';
3342

src/core/llm/generateIssueContent.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// src/core/llm/generateIssueContent.ts
22
import { TodoItem } from '../../parser/types';
3-
import OpenAI from 'openai';
43
import * as core from '@actions/core';
4+
import { chatCompletionWithRetry } from './llmClient';
55

6-
const openai = new OpenAI({
7-
apiKey: core.getInput('openai-api-key'), // correto agora
8-
});
9-
10-
const model = core.getInput('openai-model') || 'gpt-3.5-turbo';
6+
const provider = core.getInput('llm-provider') || 'openai';
7+
const model =
8+
provider === 'gemini'
9+
? core.getInput('gemini-model') || 'gemini-1.5-pro'
10+
: core.getInput('openai-model') || 'gpt-3.5-turbo';
1111

1212
export async function generateIssueTitleAndBodyLLM(todo: TodoItem): Promise<{ title: string; body: string }> {
1313
const prompt = `
@@ -27,17 +27,20 @@ TITLE: <title>
2727
BODY:
2828
<detailed body>
2929
`;
30-
// 👇 Adiciona aqui
31-
core.debug(`[DEBUG] OpenAI key starts with: ${process.env.OPENAI_API_KEY?.slice(0, 5)}`);
30+
core.debug(`[DEBUG] LLM provider: ${provider}`);
31+
if (provider === 'openai') {
32+
core.debug(`[DEBUG] OpenAI key starts with: ${process.env.OPENAI_API_KEY?.slice(0, 5)}`);
33+
} else {
34+
core.debug(`[DEBUG] Gemini key starts with: ${process.env.GEMINI_API_KEY?.slice(0, 5)}`);
35+
}
3236
core.debug(`[DEBUG] Using model: ${model}`);
33-
core.debug('[DEBUG] Sending prompt to OpenAI...');
37+
core.debug('[DEBUG] Sending prompt to LLM...');
3438
try {
35-
const response = await openai.chat.completions.create({
39+
const response = await chatCompletionWithRetry({
3640
model,
3741
messages: [{ role: 'user', content: prompt }],
3842
temperature: 0.4,
3943
});
40-
// TODO(priority=high): improve retry logic for API errors
4144
const result = response.choices[0].message?.content || '';
4245
const match = result.match(/TITLE:\s*(.+?)\s*BODY:\s*([\s\S]*)/i);
4346

@@ -48,7 +51,7 @@ try {
4851
const [, title, body] = match;
4952
return { title: title.trim(), body: body.trim() };
5053
} catch (err: any) {
51-
console.error('[ERROR] OpenAI call failed:', err);
54+
console.error('[ERROR] LLM call failed:', err);
5255
throw err;
5356
}
5457
}

src/core/llm/llmClient.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// src/core/llm/openaiClient.ts
2+
import OpenAI from 'openai';
3+
import { GoogleGenAI } from '@google/genai';
4+
import * as core from '@actions/core';
5+
6+
const provider = core.getInput('llm-provider') || 'openai';
7+
8+
export const openai = new OpenAI({
9+
apiKey: core.getInput('openai-api-key'),
10+
});
11+
12+
export const gemini = new GoogleGenAI({
13+
apiKey: core.getInput('gemini-api-key'),
14+
});
15+
16+
/**
17+
* Wraps `openai.chat.completions.create` with simple retry logic.
18+
* Retries on failure with exponential backoff.
19+
*
20+
* @param params Parameters forwarded to OpenAI
21+
* @param maxRetries Maximum number of attempts before throwing
22+
*/
23+
export interface ChatCompletionParams {
24+
model: string;
25+
messages: { role: string; content: string }[];
26+
temperature?: number;
27+
}
28+
29+
export async function chatCompletionWithRetry(
30+
params: ChatCompletionParams,
31+
maxRetries = 3
32+
): Promise<{ choices: { message: { content: string } }[] }> {
33+
let attempt = 0;
34+
for (;;) {
35+
try {
36+
if (provider === 'gemini') {
37+
const prompt = params.messages.map(m => m.content).join('\n');
38+
const response = await gemini.models.generateContent({
39+
model: params.model,
40+
contents: prompt,
41+
generationConfig: { temperature: params.temperature },
42+
} as any);
43+
return { choices: [{ message: { content: (response as any).text } }] } as any;
44+
}
45+
return (await openai.chat.completions.create(params as any)) as any;
46+
} catch (err) {
47+
attempt++;
48+
if (attempt > maxRetries) throw err;
49+
const delay = Math.min(1000 * 2 ** attempt, 5000);
50+
core.warning(`LLM request failed (attempt ${attempt}). Retrying in ${delay}ms...`);
51+
await new Promise(res => setTimeout(res, delay));
52+
}
53+
}
54+
}

src/core/llm/openaiClient.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/parser/extractTodos.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { TodoItem } from './types';
44
import { normalizeTag } from '../utils/isTextFile';
55

66
const COMMENT_PATTERNS = [
7-
{ ext: ['.ts', '.js', '.java', '.go'], pattern: /^\s*\/\/\s*(.*)$/ },
8-
{ ext: ['.py', '.sh', '.rb'], pattern: /^\s*#\s*(.*)$/ },
7+
{ ext: ['.ts', '.js', '.java', '.go', '.c', '.cpp', '.cs', '.rs', '.php', '.h', '.hpp'], pattern: /^\s*\/\/\s*(.*)$/ },
8+
{ ext: ['.py', '.sh', '.rb', '.yaml', '.yml'], pattern: /^\s*#\s*(.*)$/ },
99
{ ext: ['.html', '.xml'], pattern: /<!--\s*(.*?)\s*-->/ }
1010
];
1111

src/parser/extractTodosFromContent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { TodoItem } from './types';
44
import { normalizeTag } from '../utils/isTextFile';
55

66
const COMMENT_PATTERNS = [
7-
{ ext: ['.ts', '.js', '.java', '.go'], pattern: /^\s*\/\/\s*(.*)$/ },
8-
{ ext: ['.py', '.sh', '.rb'], pattern: /^\s*#\s*(.*)$/ },
7+
{ ext: ['.ts', '.js', '.java', '.go', '.c', '.cpp', '.cs', '.rs', '.php', '.h', '.hpp'], pattern: /^\s*\/\/\s*(.*)$/ },
8+
{ ext: ['.py', '.sh', '.rb', '.yaml', '.yml'], pattern: /^\s*#\s*(.*)$/ },
99
{ ext: ['.html', '.xml'], pattern: /<!--\s*(.*?)\s*-->/ }
1010
];
1111

src/parser/extractTodosFromDir.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import { extractTodosFromFile } from './extractTodos';
44
import { TodoItem } from './types';
55

6-
const SUPPORTED_EXTENSIONS = ['.ts', '.js', '.py', '.go', '.java', '.rb', '.sh', '.html', '.xml'];
6+
const SUPPORTED_EXTENSIONS = ['.ts', '.js', '.py', '.go', '.java', '.rb', '.sh', '.c', '.cpp', '.cs', '.rs', '.php', '.h', '.hpp', '.html', '.xml', '.yaml', '.yml'];
77
const IGNORED_DIRS = ['node_modules', 'dist', 'coverage'];
88

99
export function extractTodosFromDir(dirPath: string): TodoItem[] {

0 commit comments

Comments
 (0)