Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfdefa4ba | |||
| 1492f1b9b4 | |||
| a9ac7857be | |||
| 3f78543159 | |||
| 98eaeb6050 | |||
| ffc9038923 |
@@ -1,9 +1 @@
|
||||
[
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Bard",
|
||||
"location": "app/comments.test.js:172",
|
||||
"suggestion": "此處斷言使用了魔術字串 `/嚴重問題/`,就像樂譜中突然出現的無標記音符,雖能理解,卻少了點優雅與明確。建議將此字串提取為一個具名常數,或至少賦予一個描述性變數,以提升可讀性與未來維護的便利性,讓意圖更加清晰。",
|
||||
"is_new": true
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
@@ -74,6 +74,14 @@ jobs:
|
||||
issues: write
|
||||
```
|
||||
|
||||
OpenAI GPT-5.5 會透過 Responses API 呼叫;設定方式仍使用 `OPENAI_*`:
|
||||
|
||||
```yaml
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: https://api.openai.com/v1
|
||||
OPENAI_MODEL: gpt-5.5
|
||||
```
|
||||
|
||||
### 2. OpenRouter
|
||||
```yaml
|
||||
name: AI
|
||||
@@ -192,7 +200,54 @@ jobs:
|
||||
issues: write
|
||||
```
|
||||
|
||||
### 6. Ollama
|
||||
### 6. OpenCode Server
|
||||
```yaml
|
||||
name: AI
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- master
|
||||
types: [opened, synchronize]
|
||||
jobs:
|
||||
code-review:
|
||||
name: Code Review
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- name: AI Code Review
|
||||
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
|
||||
with:
|
||||
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
|
||||
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
OPENCODE_BASE_URL: http://192.168.3.124:4096
|
||||
OPENCODE_PROVIDER: google
|
||||
OPENCODE_MODEL: gemini-2.5-flash
|
||||
# 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供:
|
||||
# OPENCODE_SERVER_USERNAME: opencode
|
||||
# OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
```
|
||||
|
||||
OpenCode Server 串接方式會呼叫 server root 的 `/session` 與 `/session/{sessionID}/message`,並把模型指定為 `providerID=google`、`modelID=gemini-2.5-flash`。可用的內部 OpenCode server:
|
||||
|
||||
```yaml
|
||||
OPENCODE_BASE_URL: https://opencode.jsc.idv.me
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```yaml
|
||||
OPENCODE_BASE_URL: http://192.168.3.124:4096
|
||||
```
|
||||
|
||||
OpenCode server 本身必須已設定好 `google` provider 與 `gemini-2.5-flash` model;此 action 不會把 Google API key 傳給 OpenCode server。
|
||||
|
||||
### 7. Ollama
|
||||
|
||||
```yaml
|
||||
name: AI
|
||||
|
||||
+22
@@ -79,6 +79,23 @@ inputs:
|
||||
description: 'Amazon Q Base URL'
|
||||
required: false
|
||||
|
||||
# OpenCode Server
|
||||
OPENCODE_BASE_URL:
|
||||
description: 'OpenCode server Base URL'
|
||||
required: false
|
||||
OPENCODE_MODEL:
|
||||
description: 'OpenCode model id'
|
||||
required: false
|
||||
OPENCODE_PROVIDER:
|
||||
description: 'OpenCode server provider id'
|
||||
required: false
|
||||
OPENCODE_SERVER_USERNAME:
|
||||
description: 'OpenCode server Basic Auth username'
|
||||
required: false
|
||||
OPENCODE_SERVER_PASSWORD:
|
||||
description: 'OpenCode server Basic Auth password'
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
@@ -107,3 +124,8 @@ runs:
|
||||
OLLAMA_MODEL: ${{ inputs.OLLAMA_MODEL }}
|
||||
AMAZONQ_API_KEY: ${{ inputs.AMAZONQ_API_KEY }}
|
||||
AMAZONQ_BASE_URL: ${{ inputs.AMAZONQ_BASE_URL }}
|
||||
OPENCODE_BASE_URL: ${{ inputs.OPENCODE_BASE_URL }}
|
||||
OPENCODE_MODEL: ${{ inputs.OPENCODE_MODEL }}
|
||||
OPENCODE_PROVIDER: ${{ inputs.OPENCODE_PROVIDER }}
|
||||
OPENCODE_SERVER_USERNAME: ${{ inputs.OPENCODE_SERVER_USERNAME }}
|
||||
OPENCODE_SERVER_PASSWORD: ${{ inputs.OPENCODE_SERVER_PASSWORD }}
|
||||
|
||||
@@ -163,6 +163,7 @@ describe('postNewCriticalComments', () => {
|
||||
});
|
||||
|
||||
it('handles multiple criticals, posting inline where possible and degrading the rest', async () => {
|
||||
const criticalCommentPattern = /嚴重問題/;
|
||||
const inlineCalls = [];
|
||||
const issueCalls = [];
|
||||
const findings = [
|
||||
@@ -181,6 +182,6 @@ describe('postNewCriticalComments', () => {
|
||||
assert.equal(inlineCalls[0].path, 'app/a.js');
|
||||
assert.equal(inlineCalls[0].line, 10);
|
||||
assert.equal(issueCalls.length, 2);
|
||||
assert.ok(issueCalls.every(b => /嚴重問題/.test(b)));
|
||||
assert.ok(issueCalls.every(b => criticalCommentPattern.test(b)));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export function getLLMConfig() {
|
||||
['gemini', splitKeys(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', splitKeys(process.env.AMAZONQ_API_KEY), process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'],
|
||||
['opencode', ['opencode'], process.env.OPENCODE_BASE_URL, process.env.OPENCODE_MODEL || 'gemini-2.5-flash'],
|
||||
];
|
||||
for (const [provider, apiKeys, baseURL, model] of checks) {
|
||||
if (apiKeys.length > 0 && baseURL) return { provider, apiKeys, baseURL, model };
|
||||
|
||||
@@ -8,6 +8,8 @@ const ENV_KEYS = [
|
||||
'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL',
|
||||
'OLLAMA_BASE_URL', 'OLLAMA_MODEL',
|
||||
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
||||
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
||||
];
|
||||
|
||||
let saved = {};
|
||||
@@ -84,6 +86,24 @@ describe('getLLMConfig', () => {
|
||||
assert.equal(cfg.model, 'my-amazon-model');
|
||||
});
|
||||
|
||||
it('detects opencode server with gemini defaults', () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
const cfg = getLLMConfig();
|
||||
assert.equal(cfg.provider, 'opencode');
|
||||
assert.deepEqual(cfg.apiKeys, ['opencode']);
|
||||
assert.equal(cfg.baseURL, 'http://opencode.local:4096');
|
||||
assert.equal(cfg.model, 'gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('detects opencode server with custom model', () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
process.env.OPENCODE_MODEL = 'google/gemini-2.5-pro';
|
||||
const cfg = getLLMConfig();
|
||||
assert.equal(cfg.provider, 'opencode');
|
||||
assert.equal(cfg.baseURL, 'http://opencode.local:4096');
|
||||
assert.equal(cfg.model, 'google/gemini-2.5-pro');
|
||||
});
|
||||
|
||||
it('openai takes priority over gemini when both set', () => {
|
||||
process.env.OPENAI_API_KEY = 'sk-test';
|
||||
process.env.GEMINI_API_KEY = 'gemini-key';
|
||||
|
||||
+83
-4
@@ -2,6 +2,81 @@ import axios from 'axios';
|
||||
import { getLLMConfig } from './config.js';
|
||||
import { line, error } from './log.js';
|
||||
|
||||
function isOpenAIGpt55(provider, model) {
|
||||
return provider === 'openai' && /^gpt-5\.5(?:-|$)/i.test(model || '');
|
||||
}
|
||||
|
||||
function chatEndpoint(baseURL, provider, model) {
|
||||
const base = baseURL.replace(/\/$/, '');
|
||||
return isOpenAIGpt55(provider, model) ? `${base}/responses` : `${base}/chat/completions`;
|
||||
}
|
||||
|
||||
function chatPayload(provider, model, systemPrompt, userContent) {
|
||||
if (isOpenAIGpt55(provider, model)) {
|
||||
return { model, instructions: systemPrompt, input: userContent, temperature: 0.2 };
|
||||
}
|
||||
return { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 };
|
||||
}
|
||||
|
||||
function extractContent(provider, model, data) {
|
||||
if (!isOpenAIGpt55(provider, model)) return data.choices[0].message.content;
|
||||
if (typeof data.output_text === 'string') return data.output_text;
|
||||
const parts = data.output?.flatMap(item => item.content || []) || [];
|
||||
const text = parts
|
||||
.map(part => {
|
||||
if (typeof part.text === 'string') return part.text;
|
||||
if (typeof part.content === 'string') return part.content;
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
if (text) return text;
|
||||
return data.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
|
||||
function opencodeModelConfig(model) {
|
||||
const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : [process.env.OPENCODE_PROVIDER || 'google', model];
|
||||
return { providerID, modelID };
|
||||
}
|
||||
|
||||
function applyOpenCodeAuth(headers) {
|
||||
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
||||
if (!password) return;
|
||||
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
||||
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
function extractOpenCodeContent(data) {
|
||||
const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || [];
|
||||
return parts
|
||||
.map(part => part.text || part.content || '')
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
}
|
||||
|
||||
async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers) {
|
||||
const base = baseURL.replace(/\/$/, '');
|
||||
const { providerID, modelID } = opencodeModelConfig(model);
|
||||
const session = await axios.post(
|
||||
`${base}/session`,
|
||||
{ title: 'AI Code Review', model: { providerID, id: modelID } },
|
||||
{ headers }
|
||||
);
|
||||
const sessionID = session.data.id || session.data.data?.id;
|
||||
if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id');
|
||||
|
||||
const resp = await axios.post(
|
||||
`${base}/session/${sessionID}/message`,
|
||||
{
|
||||
model: { providerID, modelID },
|
||||
system: systemPrompt,
|
||||
parts: [{ type: 'text', text: userContent }],
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
return extractOpenCodeContent(resp.data);
|
||||
}
|
||||
|
||||
export async function chat(systemPrompt, userContent) {
|
||||
const { provider, apiKeys, baseURL, model } = getLLMConfig();
|
||||
if (!provider) throw new Error('未設定任何 LLM API Key');
|
||||
@@ -13,14 +88,18 @@ export async function chat(systemPrompt, userContent) {
|
||||
|
||||
const shuffled = [...apiKeys].sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < shuffled.length; i++) {
|
||||
if (provider !== 'ollama') headers['Authorization'] = `Bearer ${shuffled[i]}`;
|
||||
if (provider !== 'ollama' && provider !== 'opencode') headers['Authorization'] = `Bearer ${shuffled[i]}`;
|
||||
try {
|
||||
if (provider === 'opencode') {
|
||||
applyOpenCodeAuth(headers);
|
||||
return await chatOpenCode(baseURL, model, systemPrompt, userContent, headers);
|
||||
}
|
||||
const resp = await axios.post(
|
||||
`${baseURL.replace(/\/$/, '')}/chat/completions`,
|
||||
{ model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 },
|
||||
chatEndpoint(baseURL, provider, model),
|
||||
chatPayload(provider, model, systemPrompt, userContent),
|
||||
{ headers }
|
||||
);
|
||||
return resp.data.choices[0].message.content;
|
||||
return extractContent(provider, model, resp.data);
|
||||
} catch (e) {
|
||||
line(`[LLM] key[${i + 1}/${shuffled.length}] 失敗: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ const ENV_KEYS = [
|
||||
'CLAUDE_API_KEY', 'CLAUDE_BASE_URL', 'CLAUDE_MODEL',
|
||||
'OLLAMA_BASE_URL', 'OLLAMA_MODEL',
|
||||
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
||||
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
||||
];
|
||||
|
||||
let saved = {};
|
||||
@@ -125,6 +127,68 @@ describe('chat - key rotation', async () => {
|
||||
await chat('sys', 'user');
|
||||
assert.equal(capturedHeaders['anthropic-version'], '2023-06-01');
|
||||
});
|
||||
|
||||
it('uses OpenCode server session API for opencode', async () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
process.env.OPENCODE_PROVIDER = 'google';
|
||||
process.env.OPENCODE_MODEL = 'gemini-2.5-flash';
|
||||
const calls = [];
|
||||
mock.method(axios, 'post', async (url, payload, opts) => {
|
||||
calls.push({ url, payload, headers: opts.headers });
|
||||
if (url.endsWith('/session')) return { data: { id: 'ses_test' } };
|
||||
return { data: { parts: [{ type: 'text', text: 'opencode response' }] } };
|
||||
});
|
||||
const result = await chat('sys', 'user');
|
||||
assert.equal(result, 'opencode response');
|
||||
assert.equal(calls[0].url, 'http://opencode.local:4096/session');
|
||||
assert.deepEqual(calls[0].payload.model, { providerID: 'google', id: 'gemini-2.5-flash' });
|
||||
assert.equal(calls[1].url, 'http://opencode.local:4096/session/ses_test/message');
|
||||
assert.deepEqual(calls[1].payload.model, { providerID: 'google', modelID: 'gemini-2.5-flash' });
|
||||
assert.equal(calls[1].payload.system, 'sys');
|
||||
assert.deepEqual(calls[1].payload.parts, [{ type: 'text', text: 'user' }]);
|
||||
assert.equal(calls[1].headers['Authorization'], undefined);
|
||||
});
|
||||
|
||||
it('uses Basic Auth for protected OpenCode server', async () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
process.env.OPENCODE_SERVER_USERNAME = 'opencode';
|
||||
process.env.OPENCODE_SERVER_PASSWORD = 'secret';
|
||||
const headers = [];
|
||||
mock.method(axios, 'post', async (url, _payload, opts) => {
|
||||
headers.push(opts.headers);
|
||||
if (url.endsWith('/session')) return { data: { id: 'ses_test' } };
|
||||
return { data: { parts: [{ type: 'text', text: 'ok' }] } };
|
||||
});
|
||||
await chat('sys', 'user');
|
||||
assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`);
|
||||
});
|
||||
|
||||
it('uses Responses API for openai GPT-5.5', async () => {
|
||||
process.env.OPENAI_API_KEY = 'sk-test';
|
||||
process.env.OPENAI_MODEL = 'GPT-5.5';
|
||||
let capturedUrl, capturedPayload;
|
||||
mock.method(axios, 'post', async (url, payload) => {
|
||||
capturedUrl = url;
|
||||
capturedPayload = payload;
|
||||
return { data: { output_text: 'gpt response' } };
|
||||
});
|
||||
const result = await chat('sys', 'user');
|
||||
assert.equal(result, 'gpt response');
|
||||
assert.equal(capturedUrl, 'https://api.openai.com/v1/responses');
|
||||
assert.deepEqual(capturedPayload, { model: 'GPT-5.5', instructions: 'sys', input: 'user', temperature: 0.2 });
|
||||
});
|
||||
|
||||
it('extracts opencode text from message parts', async () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
let calls = 0;
|
||||
mock.method(axios, 'post', async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return { data: { id: 'ses_test' } };
|
||||
return { data: { parts: [{ type: 'text', text: 'hello' }, { type: 'text', text: ' world' }] } };
|
||||
});
|
||||
const result = await chat('sys', 'user');
|
||||
assert.equal(result, 'hello world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chatJSON', async () => {
|
||||
|
||||
+32
-3
@@ -15,6 +15,17 @@ import { step, line, ok, error } from './log.js';
|
||||
const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined;
|
||||
const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`;
|
||||
const giteaHeaders = (token) => ({ Authorization: `token ${token}`, 'Content-Type': 'application/json' });
|
||||
const usesResponsesApi = (provider, model) => provider === 'openai' && /^gpt-5\.5(?:-|$)/i.test(model || '');
|
||||
const opencodeModelConfig = (model) => {
|
||||
const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : [process.env.OPENCODE_PROVIDER || 'google', model];
|
||||
return { providerID, modelID };
|
||||
};
|
||||
const applyOpenCodeAuth = (headers) => {
|
||||
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
||||
if (!password) return;
|
||||
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
||||
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
};
|
||||
|
||||
function giteaErr(e) {
|
||||
const status = e.response?.status;
|
||||
@@ -63,6 +74,7 @@ export async function verifyLLM() {
|
||||
if (!baseURL) return { ok: false, provider, error: `${provider} 缺少 base URL` };
|
||||
|
||||
const base = baseURL.replace(/\/$/, '');
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
|
||||
if (provider === 'ollama') {
|
||||
try {
|
||||
@@ -73,14 +85,31 @@ export async function verifyLLM() {
|
||||
}
|
||||
}
|
||||
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (provider === 'opencode') {
|
||||
const { providerID, modelID } = opencodeModelConfig(model);
|
||||
applyOpenCodeAuth(headers);
|
||||
try {
|
||||
await axios.get(`${base}/global/health`, { headers, timeout: 30000 });
|
||||
const providers = await axios.get(`${base}/config/providers`, { headers, timeout: 30000 });
|
||||
const configuredProvider = providers.data.providers?.find(p => p.id === providerID);
|
||||
if (!configuredProvider) return { ok: false, provider, error: `OpenCode server 未設定 provider=${providerID}` };
|
||||
if (!configuredProvider.models?.[modelID]) return { ok: false, provider, error: `OpenCode server provider=${providerID} 未列出 model=${modelID}` };
|
||||
return { ok: true, provider };
|
||||
} catch (e) {
|
||||
return { ok: false, provider, error: `OpenCode server 驗證失敗: ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'claude') headers['anthropic-version'] = '2023-06-01';
|
||||
const payload = { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 };
|
||||
const endpoint = usesResponsesApi(provider, model) ? `${base}/responses` : `${base}/chat/completions`;
|
||||
const payload = usesResponsesApi(provider, model)
|
||||
? { model, input: 'ping', max_output_tokens: 1, temperature: 0 }
|
||||
: { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 };
|
||||
|
||||
for (let i = 0; i < apiKeys.length; i++) {
|
||||
headers['Authorization'] = `Bearer ${apiKeys[i]}`;
|
||||
try {
|
||||
await axios.post(`${base}/chat/completions`, payload, { headers, timeout: 30000 });
|
||||
await axios.post(endpoint, payload, { headers, timeout: 30000 });
|
||||
return { ok: true, provider, keyIndex: i + 1, total: apiKeys.length };
|
||||
} catch (e) {
|
||||
line(`[preflight] LLM key[${i + 1}/${apiKeys.length}] 驗證失敗: ${e.message}`);
|
||||
|
||||
@@ -9,6 +9,8 @@ const LLM_ENV_KEYS = [
|
||||
'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL',
|
||||
'OLLAMA_BASE_URL', 'OLLAMA_MODEL',
|
||||
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
||||
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
||||
];
|
||||
|
||||
function clearLLMEnv() {
|
||||
@@ -163,6 +165,41 @@ describe('verifyLLM', () => {
|
||||
assert.equal(capturedHeaders['anthropic-version'], '2023-06-01');
|
||||
});
|
||||
|
||||
it('checks opencode server provider and model', async () => {
|
||||
clearLLMEnv();
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
process.env.OPENCODE_PROVIDER = 'google';
|
||||
process.env.OPENCODE_MODEL = 'gemini-2.5-flash';
|
||||
const urls = [];
|
||||
mock.method(axios, 'get', async (url) => {
|
||||
urls.push(url);
|
||||
if (url.endsWith('/global/health')) return { data: { healthy: true, version: '1.17.7' } };
|
||||
return { data: { providers: [{ id: 'google', models: { 'gemini-2.5-flash': { id: 'gemini-2.5-flash' } } }] } };
|
||||
});
|
||||
const result = await verifyLLM();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.provider, 'opencode');
|
||||
assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']);
|
||||
});
|
||||
|
||||
it('checks openai GPT-5.5 with Responses API', async () => {
|
||||
clearLLMEnv();
|
||||
process.env.OPENAI_API_KEY = 'sk-test';
|
||||
process.env.OPENAI_MODEL = 'GPT-5.5';
|
||||
let capturedUrl, capturedPayload;
|
||||
mock.method(axios, 'post', async (url, payload) => {
|
||||
capturedUrl = url;
|
||||
capturedPayload = payload;
|
||||
return { data: { output_text: 'o' } };
|
||||
});
|
||||
const result = await verifyLLM();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.provider, 'openai');
|
||||
assert.equal(capturedUrl, 'https://api.openai.com/v1/responses');
|
||||
assert.equal(capturedPayload.model, 'GPT-5.5');
|
||||
assert.equal(capturedPayload.max_output_tokens, 1);
|
||||
});
|
||||
|
||||
it('checks base URL connectivity for ollama (no key)', async () => {
|
||||
clearLLMEnv();
|
||||
process.env.OLLAMA_BASE_URL = 'http://ollama.local/v1';
|
||||
|
||||
Reference in New Issue
Block a user