From 4b382b4183d5f15c527f65e606630c1040463436 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 03:04:12 +0000 Subject: [PATCH] fix: update README for OpenRouter API compatibility and add tests for LLM configuration --- README.md | 2 +- app/config.js | 12 +++--- app/config.test.js | 101 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 app/config.test.js diff --git a/README.md b/README.md index 9574270..9af31e9 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ jobs: - name: AI Code Review uses: https://gitea.jsc.idv.tw/jiantw83/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} with: - OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # OpenRouter 使用 OpenAI 相容介面,以 OPENAI_API_KEY 傳入 OPENAI_BASE_URL: https://openrouter.ai/api/v1 permissions: contents: write diff --git a/app/config.js b/app/config.js index 4a5fc7b..d852576 100644 --- a/app/config.js +++ b/app/config.js @@ -14,12 +14,12 @@ export function getLLMConfig() { ['claude', process.env.CLAUDE_API_KEY, process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com/v1', process.env.CLAUDE_MODEL || 'claude-3-haiku-20240307'], ['gemini', process.env.GEMINI_API_KEY, process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], ['ollama', 'ollama', process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], - ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.OPENAI_MODEL || 'amazon-q'], - ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.OPENAI_MODEL || 'kilo-default'], - ['roo', process.env.ROO_API_KEY, process.env.ROO_BASE_URL || 'https://api.roocode.com/v1', process.env.OPENAI_MODEL || 'roo-default'], - ['cline', process.env.CLINE_API_KEY, process.env.CLINE_BASE_URL || 'https://api.cline.dev/v1', process.env.OPENAI_MODEL || 'cline-default'], - ['continue', process.env.CONTINUE_API_KEY, process.env.CONTINUE_BASE_URL || 'https://api.continue.dev/v1', process.env.OPENAI_MODEL || 'continue-default'], - ['kade', process.env.KADE_API_KEY, process.env.KADE_BASE_URL || 'https://api.kade.dev/v1', process.env.OPENAI_MODEL || 'kade-default'], + ['amazonq', process.env.AMAZONQ_API_KEY, process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'], + ['kilo', process.env.KILO_API_KEY, process.env.KILO_BASE_URL || 'https://api.kilocode.com/v1', process.env.KILO_MODEL || 'kilo-default'], + ['roo', process.env.ROO_API_KEY, process.env.ROO_BASE_URL || 'https://api.roocode.com/v1', process.env.ROO_MODEL || 'roo-default'], + ['cline', process.env.CLINE_API_KEY, process.env.CLINE_BASE_URL || 'https://api.cline.dev/v1', process.env.CLINE_MODEL || 'cline-default'], + ['continue', process.env.CONTINUE_API_KEY, process.env.CONTINUE_BASE_URL || 'https://api.continue.dev/v1', process.env.CONTINUE_MODEL || 'continue-default'], + ['kade', process.env.KADE_API_KEY, process.env.KADE_BASE_URL || 'https://api.kade.dev/v1', process.env.KADE_MODEL || 'kade-default'], ]; for (const [provider, key, baseURL, model] of checks) { if (key && baseURL) return { provider, apiKey: key, baseURL, model }; diff --git a/app/config.test.js b/app/config.test.js new file mode 100644 index 0000000..b757cec --- /dev/null +++ b/app/config.test.js @@ -0,0 +1,101 @@ +import { describe, it, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { getLLMConfig } from './config.js'; + +const ENV_KEYS = [ + 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', + 'CLAUDE_API_KEY', 'CLAUDE_BASE_URL', 'CLAUDE_MODEL', + 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', + 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', + 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', +]; + +let saved = {}; +beforeEach(() => { + saved = {}; + for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; } +}); +afterEach(() => { + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } +}); + +describe('getLLMConfig', () => { + it('returns null provider when no env vars set', () => { + const cfg = getLLMConfig(); + assert.equal(cfg.provider, null); + assert.equal(cfg.apiKey, null); + }); + + it('detects openai with defaults', () => { + process.env.OPENAI_API_KEY = 'sk-test'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'openai'); + assert.equal(cfg.apiKey, 'sk-test'); + assert.equal(cfg.baseURL, 'https://api.openai.com/v1'); + assert.equal(cfg.model, 'gpt-4o-mini'); + }); + + it('detects openai with custom base url and model', () => { + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1'; + process.env.OPENAI_MODEL = 'gpt-4o'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'openai'); + assert.equal(cfg.baseURL, 'https://openrouter.ai/api/v1'); + assert.equal(cfg.model, 'gpt-4o'); + }); + + it('detects gemini with defaults', () => { + process.env.GEMINI_API_KEY = 'gemini-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'gemini'); + assert.equal(cfg.model, 'gemini-2.5-flash'); + }); + + it('detects gemini with custom model', () => { + process.env.GEMINI_API_KEY = 'gemini-key'; + process.env.GEMINI_MODEL = 'gemini-2.0-flash'; + const cfg = getLLMConfig(); + assert.equal(cfg.model, 'gemini-2.0-flash'); + }); + + it('detects claude with defaults', () => { + process.env.CLAUDE_API_KEY = 'claude-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'claude'); + assert.equal(cfg.model, 'claude-3-haiku-20240307'); + }); + + it('detects amazonq with its own model env', () => { + process.env.AMAZONQ_API_KEY = 'aq-key'; + process.env.AMAZONQ_MODEL = 'my-amazon-model'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'amazonq'); + assert.equal(cfg.model, 'my-amazon-model'); + }); + + it('openai takes priority over gemini when both set', () => { + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.GEMINI_API_KEY = 'gemini-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'openai'); + }); + + it('empty string api key is treated as not set', () => { + process.env.OPENAI_API_KEY = ''; + process.env.GEMINI_API_KEY = 'gemini-key'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'gemini'); + }); + + it('detects ollama without api key', () => { + process.env.OLLAMA_BASE_URL = 'http://localhost:11434'; + process.env.OLLAMA_MODEL = 'llama3'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'ollama'); + assert.equal(cfg.model, 'llama3'); + }); +});