allow proxy auto model selection
CI / 1. BUILD (pull_request) Successful in 2s
CI / 2. TEST (pull_request) Successful in 2s
CI / 3. RESULT (pull_request) Successful in 0s

This commit is contained in:
2026-08-07 06:16:45 +00:00
parent 0eb30cf9d4
commit 651e221e90
10 changed files with 96 additions and 41 deletions
+5 -1
View File
@@ -8,6 +8,9 @@ inputs:
comment_token: comment_token:
description: '操作 Gitea Commit API 的 Token' description: '操作 Gitea Commit API 的 Token'
required: false required: false
model:
description: '使用的 AI 模型'
required: false
runs: runs:
using: 'docker' using: 'docker'
image: 'dockerfile' image: 'dockerfile'
@@ -15,4 +18,5 @@ runs:
GITEA_TOKEN: ${{ inputs.token || secrets.TOKEN || gitea.token }} GITEA_TOKEN: ${{ inputs.token || secrets.TOKEN || gitea.token }}
GITEA_COMMENT_TOKEN: ${{ inputs.comment_token || inputs.token || secrets.TOKEN || gitea.token }} GITEA_COMMENT_TOKEN: ${{ inputs.comment_token || inputs.token || secrets.TOKEN || gitea.token }}
CLI_PROXY_API: ${{ vars.CLI_PROXY_API }} CLI_PROXY_API: ${{ vars.CLI_PROXY_API }}
CLI_PROXY_API_KEY: ${{ secrets.CLI_PROXY_API_KEY }} CLI_PROXY_API_KEY: ${{ secrets.CLI_PROXY_API_KEY }}
CLI_PROXY_API_MODEL: ${{ inputs.model || vars.CLI_PROXY_API_MODEL }}
+5 -5
View File
@@ -293,7 +293,7 @@ await axios.get('https://internal-gitea.example/api/v1/user', { httpsAgent });
### getLLMConfig ### getLLMConfig
依環境變數解析並回傳 CLIProxyAPI 設定:`INPUT_CLI_PROXY_API`/`CLI_PROXY_API` 作 base URL(trim 並去尾斜線),`INPUT_MODEL`/`MODEL`/`OPENCODE_MODEL` 依序 fallback 作模型名稱,`INPUT_CLI_PROXY_API_KEY`/`CLI_PROXY_API_KEY` 作金鑰。base URL 無法解析時 `provider`/`baseURL` 回 `null`、`apiKeys` 回空陣列,但 `model`(若有)仍會回傳。 依環境變數解析並回傳 CLIProxyAPI 設定:`INPUT_CLI_PROXY_API`/`CLI_PROXY_API` 作 base URL(trim 並去尾斜線),`INPUT_MODEL`/`CLI_PROXY_API_MODEL`/`MODEL`/`OPENCODE_MODEL` 依序 fallback 作可選模型名稱;若未指定模型,會交由 CLIProxyAPI 自動選擇,`INPUT_CLI_PROXY_API_KEY`/`CLI_PROXY_API_KEY` 作金鑰。base URL 無法解析時 `provider`/`baseURL` 回 `null`、`apiKeys` 回空陣列,但 `model`(若有)仍會回傳。
- 參數:無。 - 參數:無。
- 回傳:`{ provider, apiKeys, baseURL, model, command }`。 - 回傳:`{ provider, apiKeys, baseURL, model, command }`。
@@ -301,9 +301,9 @@ await axios.get('https://internal-gitea.example/api/v1/user', { httpsAgent });
```javascript ```javascript
import { getLLMConfig } from './src/config.js'; import { getLLMConfig } from './src/config.js';
// 環境變數:CLI_PROXY_API=https://proxy.example, MODEL=gpt-4o, CLI_PROXY_API_KEY=sk-xxx // 環境變數:CLI_PROXY_API=https://proxy.example, CLI_PROXY_API_KEY=sk-xxx
const cfg = getLLMConfig(); const cfg = getLLMConfig();
// => { provider: 'cliproxyapi', apiKeys: ['sk-xxx'], baseURL: 'https://proxy.example', model: 'gpt-4o', command: null } // => { provider: 'cliproxyapi', apiKeys: ['sk-xxx'], baseURL: 'https://proxy.example', model: null, command: null }
``` ```
<a id="analyzewithrole"></a> <a id="analyzewithrole"></a>
@@ -972,7 +972,7 @@ extractMeaningfulError('some noise\nERROR: rate limit exceeded\nmore noise');
- 例外:設定缺失、API 呼叫失敗或回應無文字內容時拋出。 - 例外:設定缺失、API 呼叫失敗或回應無文字內容時拋出。
```javascript ```javascript
// 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數(CLI_PROXY_API / MODEL)。 // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數(CLI_PROXY_API;MODEL 可省略)。
import { chat } from './src/llm.js'; import { chat } from './src/llm.js';
const reply = await chat('你是程式碼審查員', '請審查以下 diff:...'); const reply = await chat('你是程式碼審查員', '請審查以下 diff:...');
@@ -1251,7 +1251,7 @@ const result = await fetchLLMModels();
### verifyLLM ### verifyLLM
驗證 LLM proxy 設定可用:確認目前環境可偵測到 CLIProxyAPI 且已解析出 model,額外向模型清單端點確認 proxy 可連線且設定的 model 在可用清單內(不送 prompt)。 驗證 LLM proxy 設定可用:確認目前環境可偵測到 CLIProxyAPI;若有指定 model,額外向模型清單端點確認該 model 在可用清單內(不送 prompt)。未指定 model 時,只要求 proxy 與模型清單端點可連線。
- 參數:`deps.fetchLLMModelsFn`(預設 `fetchLLMModels`)。 - 參數:`deps.fetchLLMModelsFn`(預設 `fetchLLMModels`)。
- 回傳:`Promise<{ok:true, provider, command, model, models?} | {ok:false, provider?, command?, model?, error}>`。 - 回傳:`Promise<{ok:true, provider, command, model, models?} | {ok:false, provider?, command?, model?, error}>`。
+5 -3
View File
@@ -65,8 +65,10 @@ export const getOpenCodeHttpsAgent = getInsecureHttpsAgent;
* 依環境變數解析並回傳 CLIProxyAPI 設定。 * 依環境變數解析並回傳 CLIProxyAPI 設定。
* *
* 優先讀取 `INPUT_CLI_PROXY_API` / `CLI_PROXY_API` 作為 base URL(會 trim 並移除結尾斜線), * 優先讀取 `INPUT_CLI_PROXY_API` / `CLI_PROXY_API` 作為 base URL(會 trim 並移除結尾斜線),
* `INPUT_MODEL` / `MODEL` / `OPENCODE_MODEL`(依序 fallback,相容舊 OpenCode 設定)作為模型 * `INPUT_MODEL` / `CLI_PROXY_API_MODEL` / `MODEL` / `OPENCODE_MODEL`(依序 fallback,
* 名稱,`INPUT_CLI_PROXY_API_KEY` / `CLI_PROXY_API_KEY` 作為存取金鑰(會 trim)。 * 相容 action input、舊 OpenCode 設定與環境變數)作為可選模型名稱;若未提供,
* 則交由 CLIProxyAPI 自動選擇模型,`INPUT_CLI_PROXY_API_KEY` / `CLI_PROXY_API_KEY`
* 作為存取金鑰(會 trim)。
* *
* 若 base URL 無法解析出任何值,視為沒有可用的 proxy 設定:`provider`/`baseURL` 回傳 `null`、 * 若 base URL 無法解析出任何值,視為沒有可用的 proxy 設定:`provider`/`baseURL` 回傳 `null`、
* `apiKeys` 回傳空陣列,但 `model`(若有解析到)仍會回傳,不會被清空。 * `apiKeys` 回傳空陣列,但 `model`(若有解析到)仍會回傳,不會被清空。
@@ -76,7 +78,7 @@ export const getOpenCodeHttpsAgent = getInsecureHttpsAgent;
*/ */
export function getLLMConfig() { export function getLLMConfig() {
const baseURL = String(process.env.INPUT_CLI_PROXY_API || process.env.CLI_PROXY_API || '').trim().replace(/\/$/, ''); const baseURL = String(process.env.INPUT_CLI_PROXY_API || process.env.CLI_PROXY_API || '').trim().replace(/\/$/, '');
const model = process.env.INPUT_MODEL || process.env.MODEL || process.env.OPENCODE_MODEL || ''; const model = process.env.INPUT_MODEL || process.env.CLI_PROXY_API_MODEL || process.env.MODEL || process.env.OPENCODE_MODEL || '';
const apiKey = String(process.env.INPUT_CLI_PROXY_API_KEY || process.env.CLI_PROXY_API_KEY || '').trim(); const apiKey = String(process.env.INPUT_CLI_PROXY_API_KEY || process.env.CLI_PROXY_API_KEY || '').trim();
if (!baseURL) return { provider: null, apiKeys: [], baseURL: null, model: model || null, command: null }; if (!baseURL) return { provider: null, apiKeys: [], baseURL: null, model: model || null, command: null };
+14 -13
View File
@@ -147,8 +147,8 @@ function summarizeApiError(e) {
* 由 axios 直接拋出例外,交由呼叫端(chat())攔截並摘要。僅使用 * 由 axios 直接拋出例外,交由呼叫端(chat())攔截並摘要。僅使用
* `apiKeys` 陣列的第一個元素,不會輪替其他金鑰。 * `apiKeys` 陣列的第一個元素,不會輪替其他金鑰。
* *
* @param {{provider: string, baseURL: string, apiKeys: string[], model: string}} cfg - 連線設定; * @param {{provider: string, baseURL: string, apiKeys: string[], model?: string|null}} cfg - 連線設定;
* 僅使用 `apiKeys[0]`。 * 僅使用 `apiKeys[0]`;`model` 可省略,省略時交由 CLIProxyAPI 自動選擇。
* @param {string} prompt - 送給 API 的完整 prompt 內容,會作為 user 訊息內容; * @param {string} prompt - 送給 API 的完整 prompt 內容,會作為 user 訊息內容;
* HTTP 層的 system 訊息為固定的通用指示,與 prompt 內可能內嵌的 `<system>` 內容無關。 * HTTP 層的 system 訊息為固定的通用指示,與 prompt 內可能內嵌的 `<system>` 內容無關。
* @returns {Promise<any>} API 回應的原始資料物件(`resp.data`),並非純文字; * @returns {Promise<any>} API 回應的原始資料物件(`resp.data`),並非純文字;
@@ -164,17 +164,18 @@ async function runProxyAPI({ provider, baseURL, apiKeys, model }, prompt) {
const maxBuffer = Number(process.env.AI_ASSISTANT_MAX_BUFFER || 20 * 1024 * 1024); const maxBuffer = Number(process.env.AI_ASSISTANT_MAX_BUFFER || 20 * 1024 * 1024);
const root = String(baseURL || '').trim().replace(/\/$/, ''); const root = String(baseURL || '').trim().replace(/\/$/, '');
const apiKey = Array.isArray(apiKeys) ? apiKeys[0] : ''; const apiKey = Array.isArray(apiKeys) ? apiKeys[0] : '';
const body = {
messages: [
{ role: 'system', content: '請依照以下系統指示處理使用者內容,並只輸出要求的最終結果。' },
{ role: 'user', content: prompt },
],
temperature: 0,
stream: false,
};
if (model) body.model = model;
const resp = await axios.post( const resp = await axios.post(
`${root}/v1/chat/completions`, `${root}/v1/chat/completions`,
{ body,
model,
messages: [
{ role: 'system', content: '請依照以下系統指示處理使用者內容,並只輸出要求的最終結果。' },
{ role: 'user', content: prompt },
],
temperature: 0,
stream: false,
},
{ {
timeout, timeout,
maxBodyLength: maxBuffer, maxBodyLength: maxBuffer,
@@ -209,9 +210,9 @@ async function runProxyAPI({ provider, baseURL, apiKeys, model }, prompt) {
export async function chat(systemPrompt, userContent) { export async function chat(systemPrompt, userContent) {
const cfg = getLLMConfig(); const cfg = getLLMConfig();
const { provider, baseURL, model } = cfg; const { provider, baseURL, model } = cfg;
if (!provider || !baseURL || !model) throw new Error('未偵測到可用的 CLIProxyAPI 設定,請確認 CLI_PROXY_API 與 MODEL'); if (!provider || !baseURL) throw new Error('未偵測到可用的 CLIProxyAPI 設定,請確認 CLI_PROXY_API');
line(`[LLM] provider=${provider} baseURL=${baseURL} model=${model}`); line(`[LLM] provider=${provider} baseURL=${baseURL} model=${model || 'auto'}`);
try { try {
const data = await runProxyAPI(cfg, buildPrompt(systemPrompt, userContent)); const data = await runProxyAPI(cfg, buildPrompt(systemPrompt, userContent));
+6 -5
View File
@@ -37,7 +37,7 @@ const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';
* - Step3 自動提交檢查:偵測上輪 bot `[failure]`(exit 1)或本次為 bot 自動提交(exit 0 跳過)。 * - Step3 自動提交檢查:偵測上輪 bot `[failure]`(exit 1)或本次為 bot 自動提交(exit 0 跳過)。
* - Step4 PR 對話收斂:關閉未解決 comment 並將 finding 分流為已修復 / 誤報 / 仍成立(失敗則降級繼續)。 * - Step4 PR 對話收斂:關閉未解決 comment 並將 finding 分流為已修復 / 誤報 / 仍成立(失敗則降級繼續)。
* - Step5 角色分析:載入角色、取 PR diff,平行產生 findings 並補齊缺漏行號; * - Step5 角色分析:載入角色、取 PR diff,平行產生 findings 並補齊缺漏行號;
* 未設定 API Key 或取 diff 失敗 exit 1,diff 為空 exit 0。 * 未設定 CLIProxyAPI 或取 diff 失敗 exit 1,diff 為空 exit 0。
* - Step6 合併去重:舊 findings + 對話收斂結果 + 新 findings → 語意去重並排序。 * - Step6 合併去重:舊 findings + 對話收斂結果 + 新 findings → 語意去重並排序。
* - Step7 過濾:套用排除規則 + 防守方 AI 誤報裁決。 * - Step7 過濾:套用排除規則 + 防守方 AI 誤報裁決。
* - Step8 發布:寫入 findings、組裝使用量,發布 Gitea Review(失敗則降級繼續)。 * - Step8 發布:寫入 findings、組裝使用量,發布 Gitea Review(失敗則降級繼續)。
@@ -119,9 +119,10 @@ export async function main() {
section('Pipeline 結束'); section('Pipeline 結束');
process.exit(0); process.exit(0);
} }
input(`LLM=${provider}/${model};角色=[${roles.map(r => r.name).join(', ')}];diff=${diff.length} 字元`); const modelLabel = model || 'auto';
input(`LLM=${provider}/${modelLabel};角色=[${roles.map(r => r.name).join(', ')}];diff=${diff.length} 字元`);
try { try {
await postComment(getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`); await postComment(getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${modelLabel}`);
line('角色介紹 comment 已發布'); line('角色介紹 comment 已發布');
} catch (e) { } catch (e) {
warn(`角色介紹 comment 發布失敗(繼續執行): ${e.message}`); warn(`角色介紹 comment 發布失敗(繼續執行): ${e.message}`);
@@ -191,9 +192,9 @@ export async function main() {
const runUsage = getRunUsage(); const runUsage = getRunUsage();
const quota = await fetchAccountQuota(provider, { apiKeys, baseURL }); const quota = await fetchAccountQuota(provider, { apiKeys, baseURL });
const rate = getRateLimit(); const rate = getRateLimit();
const usageSection = formatUsageStats(provider, model, runUsage, quota, rate); const usageSection = formatUsageStats(provider, modelLabel, runUsage, quota, rate);
input(`findings ${filtered.length} 筆(${formatFindingsStatsLine(filtered)})`); input(`findings ${filtered.length} 筆(${formatFindingsStatsLine(filtered)})`);
line(`使用量: ${formatUsageStatsLine(provider, model, runUsage, quota, rate)}`); line(`使用量: ${formatUsageStatsLine(provider, modelLabel, runUsage, quota, rate)}`);
try { try {
await postFindingsReview(filtered, { summaryFindings: filtered, commentFindings: filtered, usageSection }); await postFindingsReview(filtered, { summaryFindings: filtered, commentFindings: filtered, usageSection });
output('Gitea Review 已發布'); output('Gitea Review 已發布');
+6 -10
View File
@@ -165,29 +165,25 @@ export async function fetchLLMModels({
/** /**
* 驗證 LLM proxy 設定可用。 * 驗證 LLM proxy 設定可用。
* *
* 確認目前環境可偵測到 CLIProxyAPI 且已解析出 model;額外向模型清單端點確認 * 確認目前環境可偵測到 CLIProxyAPI;若有明確指定 model,則額外向模型清單端點確認
* proxy 可連線且設定的 model 在可用清單內(不送 prompt)。 * 該 model 在可用清單內(不送 prompt)。當 model 未指定時,只要求 proxy 與模型清單端點可連線。
* @param {object} [deps] - 可注入相依,供測試。 * @param {object} [deps] - 可注入相依,供測試。
* @param {Function} [deps.fetchLLMModelsFn=fetchLLMModels] - proxy 模型清單取得函式。 * @param {Function} [deps.fetchLLMModelsFn=fetchLLMModels] - proxy 模型清單取得函式。
* @returns {Promise< * @returns {Promise<
* {ok: true, provider: string, command: null, model: string, models?: string[]} | * {ok: true, provider: string, command: null, model: string|null, models?: string[]} |
* {ok: false, provider?: string, command?: null, model?: string, error: string} * {ok: false, provider?: string, command?: null, model?: string|null, error: string}
* >} * >}
* 通過時含 provider、command、model(另含 models 清單);未設定 provider 的失敗分支不含 provider。 * 通過時含 provider、command、model(另含 models 清單);未設定 provider 的失敗分支不含 provider。
* @remarks 設定來源為 config.js 的 getLLMConfig()。 * @remarks 設定來源為 config.js 的 getLLMConfig()。
* @remarks 【需人工確認】依目前 getLLMConfig() 的型別標註(`provider: ('cliproxyapi'|null)`),
* `provider` 存在但不是 `'cliproxyapi'` 的分支在目前設定來源下應為不會被觸發的保留分支,
* 但無法從本檔案確認這是刻意保留的向前相容設計、還是尚未清理的死碼,建議與維護者確認。
*/ */
export async function verifyLLM({ fetchLLMModelsFn = fetchLLMModels } = {}) { export async function verifyLLM({ fetchLLMModelsFn = fetchLLMModels } = {}) {
const { provider, command, model } = getLLMConfig(); const { provider, command, model } = getLLMConfig();
if (!provider) return { ok: false, error: '未偵測到可用的 CLIProxyAPI 設定,請確認 CLI_PROXY_API' }; if (!provider) return { ok: false, error: '未偵測到可用的 CLIProxyAPI 設定,請確認 CLI_PROXY_API' };
if (!model) return { ok: false, provider, error: '未設定 MODEL' };
if (provider === 'cliproxyapi') { if (provider === 'cliproxyapi') {
const models = await fetchLLMModelsFn(); const models = await fetchLLMModelsFn();
if (!models.ok) return { ok: false, provider, command, model, error: models.error }; if (!models.ok) return { ok: false, provider, command, model, error: models.error };
if (!models.slugs.includes(model)) { if (model && !models.slugs.includes(model)) {
return { ok: false, provider, command, model, error: `模型 ${model} 不在 CLIProxyAPI 可用清單: [${models.slugs.join(', ')}]` }; return { ok: false, provider, command, model, error: `模型 ${model} 不在 CLIProxyAPI 可用清單: [${models.slugs.join(', ')}]` };
} }
return { ok: true, provider, command, model, models: models.slugs }; return { ok: true, provider, command, model, models: models.slugs };
@@ -255,7 +251,7 @@ export async function runPreflight(workspace = process.env.GITHUB_WORKSPACE || '
error(`LLM 驗證失敗: ${llm.error}`); error(`LLM 驗證失敗: ${llm.error}`);
return false; return false;
} }
ok(`LLM proxy 可用(provider=${llm.provider}, model=${llm.model})`); ok(`LLM proxy 可用(provider=${llm.provider}, model=${llm.model || 'auto'})`);
if (llm.models) line(`模型已確認在可用清單內(共 ${llm.models.length} 個可用模型)`); if (llm.models) line(`模型已確認在可用清單內(共 ${llm.models.length} 個可用模型)`);
result(true, '前置驗證通過'); result(true, '前置驗證通過');
+12 -2
View File
@@ -3,8 +3,8 @@ import assert from 'node:assert/strict';
import { getLLMConfig, getOpenCodeHttpsAgent } from '../config.js'; import { getLLMConfig, getOpenCodeHttpsAgent } from '../config.js';
const ENV_KEYS = [ const ENV_KEYS = [
'CLI_PROXY_API', 'CLI_PROXY_API_KEY', 'INPUT_CLI_PROXY_API', 'INPUT_CLI_PROXY_API_KEY', 'CLI_PROXY_API', 'CLI_PROXY_API_KEY', 'CLI_PROXY_API_MODEL', 'INPUT_CLI_PROXY_API', 'INPUT_CLI_PROXY_API_KEY', 'INPUT_MODEL',
'MODEL', 'OPENCODE_MODEL', 'INPUT_MODEL', 'MODEL', 'OPENCODE_MODEL',
]; ];
let saved = {}; let saved = {};
@@ -52,6 +52,16 @@ describe('getLLMConfig', () => {
assert.equal(cfg.model, 'gpt-5-mini'); assert.equal(cfg.model, 'gpt-5-mini');
}); });
it('uses CLI_PROXY_API_MODEL when INPUT_MODEL is missing', () => {
process.env.CLI_PROXY_API = 'https://proxy.example';
process.env.CLI_PROXY_API_MODEL = 'gpt-5.4-mini';
process.env.MODEL = 'gpt-5.5';
const cfg = getLLMConfig();
assert.equal(cfg.model, 'gpt-5.4-mini');
});
it('returns null provider when CLI_PROXY_API is missing', () => { it('returns null provider when CLI_PROXY_API is missing', () => {
process.env.MODEL = 'gpt-5.5'; process.env.MODEL = 'gpt-5.5';
const cfg = getLLMConfig(); const cfg = getLLMConfig();
+20 -1
View File
@@ -4,7 +4,7 @@ import axios from 'axios';
import { extractBalancedJSON, extractJSONText, extractMeaningfulError, mapWithConcurrency } from '../llm.js'; import { extractBalancedJSON, extractJSONText, extractMeaningfulError, mapWithConcurrency } from '../llm.js';
const ENV_KEYS = [ const ENV_KEYS = [
'CLI_PROXY_API', 'CLI_PROXY_API_KEY', 'MODEL', 'INPUT_MODEL', 'OPENCODE_MODEL', 'CLI_PROXY_API', 'CLI_PROXY_API_KEY', 'CLI_PROXY_API_MODEL', 'MODEL', 'INPUT_MODEL', 'OPENCODE_MODEL',
'AI_ASSISTANT_TIMEOUT_MS', 'AI_ASSISTANT_MAX_BUFFER', 'AI_ASSISTANT_TIMEOUT_MS', 'AI_ASSISTANT_MAX_BUFFER',
]; ];
@@ -57,6 +57,25 @@ describe('chat - CLIProxyAPI', async () => {
assert.equal(capturedOpts.headers.Authorization, 'Bearer secret'); assert.equal(capturedOpts.headers.Authorization, 'Bearer secret');
}); });
it('omits model from the request body when auto selection is allowed', async () => {
process.env.CLI_PROXY_API = 'https://proxy.example';
process.env.CLI_PROXY_API_KEY = 'secret';
let capturedBody;
mock.method(axios, 'post', async (url, body) => {
capturedBody = body;
return {
data: { choices: [{ message: { content: 'cli response' } }] },
headers: {},
};
});
const result = await chat('sys', 'user');
assert.equal(result, 'cli response');
assert.equal(Object.hasOwn(capturedBody, 'model'), false);
});
it('throws an error when the API fails', async () => { it('throws an error when the API fails', async () => {
process.env.CLI_PROXY_API = 'https://proxy.example'; process.env.CLI_PROXY_API = 'https://proxy.example';
process.env.MODEL = 'gpt-5-mini'; process.env.MODEL = 'gpt-5-mini';
+6
View File
@@ -131,6 +131,12 @@ describe('main pipeline', () => {
assert.equal(await runMain(), 0); assert.equal(await runMain(), 0);
}); });
it('無 MODEL 仍可由 Proxy 自動選模並正常走完(exit 0)', async () => {
assert.equal(await runMain({
config: { getLLMConfig: () => ({ provider: 'cliproxyapi', apiKeys: ['secret'], baseURL: 'https://proxy.example', model: null, command: null }) },
}), 0);
});
it('clone 失敗仍繼續、不因 commitAndPush 中斷(無 critical → exit 0)', async () => { it('clone 失敗仍繼續、不因 commitAndPush 中斷(無 critical → exit 0)', async () => {
assert.equal(await runMain({ git: { cloneRepo: () => { throw new Error('clone fail'); } } }), 0); assert.equal(await runMain({ git: { cloneRepo: () => { throw new Error('clone fail'); } } }), 0);
}); });
+17 -1
View File
@@ -4,7 +4,7 @@ import axios from 'axios';
import { checkRequiredEnv, verifyGiteaToken, verifyCommentToken, verifyLLM, fetchLLMModels, runPreflight } from '../preflight.js'; import { checkRequiredEnv, verifyGiteaToken, verifyCommentToken, verifyLLM, fetchLLMModels, runPreflight } from '../preflight.js';
const LLM_ENV_KEYS = [ const LLM_ENV_KEYS = [
'CLI_PROXY_API', 'CLI_PROXY_API_KEY', 'INPUT_CLI_PROXY_API', 'INPUT_CLI_PROXY_API_KEY', 'CLI_PROXY_API', 'CLI_PROXY_API_KEY', 'CLI_PROXY_API_MODEL', 'INPUT_CLI_PROXY_API', 'INPUT_CLI_PROXY_API_KEY',
'MODEL', 'OPENCODE_MODEL', 'INPUT_MODEL', 'MODEL', 'OPENCODE_MODEL', 'INPUT_MODEL',
]; ];
@@ -164,6 +164,22 @@ describe('verifyLLM', () => {
assert.deepEqual(result.models, ['gpt-5.5', 'gpt-5.4-mini']); assert.deepEqual(result.models, ['gpt-5.5', 'gpt-5.4-mini']);
}); });
it('passes when no model is specified and the proxy is reachable', async () => {
clearLLMEnv();
process.env.CLI_PROXY_API = 'https://proxy.example';
process.env.CLI_PROXY_API_KEY = 'secret';
const result = await verifyLLM({
fetchLLMModelsFn: async () => ({ ok: true, slugs: ['gpt-5.5', 'gpt-5.4-mini'] }),
});
assert.equal(result.ok, true);
assert.equal(result.provider, 'cliproxyapi');
assert.equal(result.command, null);
assert.equal(result.model, null);
assert.deepEqual(result.models, ['gpt-5.5', 'gpt-5.4-mini']);
});
it('fails when proxy auth is invalid', async () => { it('fails when proxy auth is invalid', async () => {
clearLLMEnv(); clearLLMEnv();
process.env.CLI_PROXY_API = 'https://proxy.example'; process.env.CLI_PROXY_API = 'https://proxy.example';