acdev 1.0.8 → 1.0.9
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/.acdev/.env.example +0 -3
- package/README.md +11 -21
- package/bin/acdev.js +3 -13
- package/package.json +1 -2
- package/public/app.js +45 -385
- package/public/index.html +1 -33
- package/public/styles.css +0 -31
- package/src/agent.js +64 -127
- package/src/config.js +45 -151
- package/src/models.js +35 -256
- package/src/server.js +11 -50
- package/src/openrouter-auth.js +0 -37
package/src/models.js
CHANGED
|
@@ -15,7 +15,6 @@ import { join } from 'node:path';
|
|
|
15
15
|
/**
|
|
16
16
|
* Curated Claude Agent SDK / Claude Code model ids used as defaults + fallback.
|
|
17
17
|
* Prefer documented Code aliases and Anthropic API ids (not invented snapshots).
|
|
18
|
-
* OpenRouter slugs (provider/model) are excluded — those belong in OPENROUTER_MODEL_OPTIONS.
|
|
19
18
|
*/
|
|
20
19
|
export const CLAUDE_MODEL_OPTIONS = [
|
|
21
20
|
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
@@ -44,49 +43,23 @@ export const MODEL_OPTIONS = CLAUDE_MODEL_OPTIONS;
|
|
|
44
43
|
|
|
45
44
|
export const DEFAULT_MODEL = 'claude-sonnet-5';
|
|
46
45
|
|
|
47
|
-
/**
|
|
48
|
-
* OpenRouter slugs commonly used with acdev. Curated list is merged with the
|
|
49
|
-
* live OpenRouter catalog; anthropic/* entries are agent-compatible.
|
|
50
|
-
*/
|
|
51
|
-
export const OPENROUTER_AGENT_MODEL_PREFIX = 'anthropic/';
|
|
52
|
-
|
|
53
|
-
/** Curated OpenRouter model ids used as defaults + fallback when the live API is unavailable. */
|
|
54
|
-
export const OPENROUTER_MODEL_OPTIONS = [
|
|
55
|
-
{ id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
|
|
56
|
-
{ id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
|
|
57
|
-
{ id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
|
|
58
|
-
{ id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)' },
|
|
59
|
-
{ id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)' },
|
|
60
|
-
{ id: 'openai/gpt-4o-mini', label: 'GPT-4o Mini (OpenAI)' },
|
|
61
|
-
{ id: 'google/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro Preview (Google)' },
|
|
62
|
-
{ id: 'qwen/qwen3-235b-a22b', label: 'Qwen3 235B (Qwen)' },
|
|
63
|
-
{ id: 'deepseek/deepseek-chat-v3-0324', label: 'DeepSeek Chat V3 (DeepSeek)' },
|
|
64
|
-
{ id: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B Instruct (Meta)' },
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
export const DEFAULT_OPENROUTER_MODEL = 'anthropic/claude-sonnet-4';
|
|
68
|
-
|
|
69
46
|
/** Config value meaning no model selected (jobs blocked until user picks one). */
|
|
70
47
|
export const NO_MODEL = '-';
|
|
71
48
|
|
|
72
|
-
/**
|
|
73
|
-
export const LLM_PROVIDERS = /** @type {const} */ (['claude', 'openrouter']);
|
|
74
|
-
|
|
75
|
-
/** Loose model id shape accepted by config (Claude aliases, API ids, OpenRouter slugs). */
|
|
49
|
+
/** Claude direct path ids: Anthropic API ids and Claude Code aliases (no `/`). */
|
|
76
50
|
export const MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:\/-]{0,127}$/;
|
|
77
51
|
|
|
78
52
|
const ANTHROPIC_MODELS_URL = 'https://api.anthropic.com/v1/models';
|
|
79
|
-
const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
80
53
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
81
54
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
82
55
|
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
83
56
|
|
|
84
|
-
/** @typedef {{ id: string, name?: string, label?: string
|
|
85
|
-
/** @typedef {'anthropic' | '
|
|
86
|
-
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource
|
|
57
|
+
/** @typedef {{ id: string, name?: string, label?: string }} ModelOption */
|
|
58
|
+
/** @typedef {'anthropic' | 'fallback'} ModelsSource */
|
|
59
|
+
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource }} ModelsListResult */
|
|
87
60
|
|
|
88
|
-
/** @type {
|
|
89
|
-
|
|
61
|
+
/** @type {{ expiresAt: number, result: Omit<ModelsListResult, 'selected'> } | null} */
|
|
62
|
+
let cache = null;
|
|
90
63
|
|
|
91
64
|
/** @type {typeof fetch | null} */
|
|
92
65
|
let fetchImpl = null;
|
|
@@ -125,23 +98,7 @@ export function _resetCredentialsTokenResolver() {
|
|
|
125
98
|
}
|
|
126
99
|
|
|
127
100
|
export function _resetModelsCache() {
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* @param {unknown} value
|
|
133
|
-
* @returns {value is LlmProvider}
|
|
134
|
-
*/
|
|
135
|
-
export function isValidLlmProvider(value) {
|
|
136
|
-
return value === 'claude' || value === 'openrouter';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Default model id for an LLM provider.
|
|
141
|
-
* @param {LlmProvider} [provider]
|
|
142
|
-
*/
|
|
143
|
-
export function defaultModelForProvider(provider = 'claude') {
|
|
144
|
-
return provider === 'openrouter' ? DEFAULT_OPENROUTER_MODEL : DEFAULT_MODEL;
|
|
101
|
+
cache = null;
|
|
145
102
|
}
|
|
146
103
|
|
|
147
104
|
/**
|
|
@@ -181,7 +138,7 @@ function toOption(id, displayName) {
|
|
|
181
138
|
}
|
|
182
139
|
|
|
183
140
|
/**
|
|
184
|
-
* Claude
|
|
141
|
+
* Claude catalog ids: Anthropic API ids and Claude Code aliases (no provider/ prefix).
|
|
185
142
|
* @param {string} id
|
|
186
143
|
* @returns {boolean}
|
|
187
144
|
*/
|
|
@@ -190,147 +147,45 @@ export function isClaudeCatalogId(id) {
|
|
|
190
147
|
}
|
|
191
148
|
|
|
192
149
|
/**
|
|
193
|
-
*
|
|
194
|
-
* @param {string} id
|
|
195
|
-
* @returns {boolean}
|
|
196
|
-
*/
|
|
197
|
-
export function isOpenRouterCatalogId(id) {
|
|
198
|
-
return isValidModelId(id) && String(id).includes('/');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Whether an OpenRouter slug can be used for acdev agent runs.
|
|
203
|
-
* Non-anthropic slugs may appear in OpenRouter's catalog but fail at runtime
|
|
204
|
-
* because the Claude Agent SDK speaks Anthropic Messages API semantics.
|
|
205
|
-
* @param {string} modelId
|
|
206
|
-
* @returns {boolean}
|
|
207
|
-
*/
|
|
208
|
-
export function isOpenRouterAgentCompatibleModel(modelId) {
|
|
209
|
-
if (!isOpenRouterCatalogId(modelId)) return false;
|
|
210
|
-
return String(modelId).trim().toLowerCase().startsWith(OPENROUTER_AGENT_MODEL_PREFIX);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Mark whether each OpenRouter slug is supported for acdev agent jobs.
|
|
215
|
-
* @param {ModelOption} model
|
|
216
|
-
* @returns {ModelOption}
|
|
217
|
-
*/
|
|
218
|
-
export function annotateOpenRouterModelCompatibility(model) {
|
|
219
|
-
if (!model?.id) return model;
|
|
220
|
-
return {
|
|
221
|
-
...model,
|
|
222
|
-
agentCompatible: isOpenRouterAgentCompatibleModel(model.id),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @param {ModelOption[]} models
|
|
150
|
+
* Curated fallback list.
|
|
228
151
|
* @returns {ModelOption[]}
|
|
229
152
|
*/
|
|
230
|
-
function
|
|
231
|
-
return
|
|
153
|
+
export function curatedModelOptions() {
|
|
154
|
+
return CLAUDE_MODEL_OPTIONS.map((m) => ({ ...m }));
|
|
232
155
|
}
|
|
233
156
|
|
|
234
157
|
/**
|
|
235
|
-
*
|
|
236
|
-
* filtered to anthropic/* — runtime enqueue still validates agent compatibility.
|
|
237
|
-
* @param {ModelOption[]} models
|
|
238
|
-
* @param {ModelOption[]} curated
|
|
239
|
-
* @returns {ModelOption[]}
|
|
240
|
-
*/
|
|
241
|
-
function ensureOpenRouterModels(models, curated) {
|
|
242
|
-
const list = models.length > 0 ? models : curated;
|
|
243
|
-
return annotateOpenRouterModelsCompatibility(list);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Improve OpenRouter agent failure messages for the job UI.
|
|
248
|
-
* @param {string} message
|
|
249
|
-
* @param {string} [model]
|
|
250
|
-
* @returns {string}
|
|
251
|
-
*/
|
|
252
|
-
export function enhanceOpenRouterAgentError(message, model) {
|
|
253
|
-
const raw = String(message || '').trim();
|
|
254
|
-
if (!raw) return raw;
|
|
255
|
-
|
|
256
|
-
const id = String(model || '').trim();
|
|
257
|
-
const routingFailure =
|
|
258
|
-
/no allowed providers are available/i.test(raw) ||
|
|
259
|
-
/not allowed by provider/i.test(raw) ||
|
|
260
|
-
/model.*not allowed/i.test(raw);
|
|
261
|
-
|
|
262
|
-
if (id && !isOpenRouterAgentCompatibleModel(id)) {
|
|
263
|
-
return [
|
|
264
|
-
`OpenRouter model "${id}" is not supported for acdev agent runs.`,
|
|
265
|
-
'Agent jobs use the Claude Agent SDK via OpenRouter\'s Anthropic-compatible API.',
|
|
266
|
-
'Choose an anthropic/* model (e.g. anthropic/claude-sonnet-4).',
|
|
267
|
-
raw !== id ? `Provider error: ${raw}` : '',
|
|
268
|
-
]
|
|
269
|
-
.filter(Boolean)
|
|
270
|
-
.join(' ');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (routingFailure) {
|
|
274
|
-
return [
|
|
275
|
-
raw,
|
|
276
|
-
'If you use OpenRouter provider allowlists, clear Settings → Privacy → Providers or add the model\'s upstream provider.',
|
|
277
|
-
'For acdev, anthropic/* models on OpenRouter are the most reliable choice.',
|
|
278
|
-
].join(' ');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return raw;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Curated fallback list for a provider.
|
|
286
|
-
* @param {LlmProvider} provider
|
|
287
|
-
* @returns {ModelOption[]}
|
|
288
|
-
*/
|
|
289
|
-
export function curatedModelOptions(provider) {
|
|
290
|
-
return provider === 'openrouter'
|
|
291
|
-
? OPENROUTER_MODEL_OPTIONS.map((m) => ({ ...m }))
|
|
292
|
-
: CLAUDE_MODEL_OPTIONS.map((m) => ({ ...m }));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Whether a model id belongs in a provider's curated catalog.
|
|
158
|
+
* Whether a model id belongs in the curated catalog.
|
|
297
159
|
* @param {string} modelId
|
|
298
|
-
* @param {LlmProvider} provider
|
|
299
160
|
* @returns {boolean}
|
|
300
161
|
*/
|
|
301
|
-
export function isModelInCuratedCatalog(modelId
|
|
162
|
+
export function isModelInCuratedCatalog(modelId) {
|
|
302
163
|
if (isNoModel(modelId)) return false;
|
|
303
164
|
const id = String(modelId).trim();
|
|
304
|
-
return curatedModelOptions(
|
|
165
|
+
return curatedModelOptions().some((m) => m.id === id);
|
|
305
166
|
}
|
|
306
167
|
|
|
307
168
|
/**
|
|
308
|
-
* Whether a model id is valid for
|
|
309
|
-
* Claude: direct ids without `/`. OpenRouter: provider/model slugs with `/`.
|
|
169
|
+
* Whether a model id is valid for Claude (direct ids without `/`).
|
|
310
170
|
* @param {string} modelId
|
|
311
|
-
* @param {LlmProvider} provider
|
|
312
171
|
* @returns {boolean}
|
|
313
172
|
*/
|
|
314
|
-
export function isModelIdForProvider(modelId
|
|
173
|
+
export function isModelIdForProvider(modelId) {
|
|
315
174
|
if (isNoModel(modelId) || !isValidModelId(modelId)) return false;
|
|
316
|
-
|
|
317
|
-
return provider === 'openrouter' ? isOpenRouterCatalogId(id) : isClaudeCatalogId(id);
|
|
175
|
+
return isClaudeCatalogId(String(modelId).trim());
|
|
318
176
|
}
|
|
319
177
|
|
|
320
178
|
/**
|
|
321
|
-
* Drop ids that do not belong in
|
|
179
|
+
* Drop ids that do not belong in the Claude catalog.
|
|
322
180
|
* @param {ModelOption[]} models
|
|
323
|
-
* @param {LlmProvider} provider
|
|
324
181
|
* @returns {ModelOption[]}
|
|
325
182
|
*/
|
|
326
|
-
function
|
|
327
|
-
|
|
328
|
-
provider === 'openrouter' ? isOpenRouterCatalogId : isClaudeCatalogId;
|
|
329
|
-
return models.filter((m) => m?.id && keep(m.id));
|
|
183
|
+
function filterClaudeModels(models) {
|
|
184
|
+
return models.filter((m) => m?.id && isClaudeCatalogId(m.id));
|
|
330
185
|
}
|
|
331
186
|
|
|
332
187
|
/**
|
|
333
|
-
* Merge curated options (stable order / aliases) with live rows
|
|
188
|
+
* Merge curated options (stable order / aliases) with live rows.
|
|
334
189
|
* Same ids keep curated position but prefer live display names.
|
|
335
190
|
* @param {ModelOption[]} curated
|
|
336
191
|
* @param {ModelOption[]} live
|
|
@@ -365,7 +220,7 @@ function mergeModelLists(curated, live) {
|
|
|
365
220
|
}
|
|
366
221
|
|
|
367
222
|
/**
|
|
368
|
-
* Keep selected only when it exists in the
|
|
223
|
+
* Keep selected only when it exists in the model catalog.
|
|
369
224
|
* @param {ModelOption[]} models
|
|
370
225
|
* @param {string} selected
|
|
371
226
|
* @returns {string}
|
|
@@ -539,69 +394,14 @@ export async function fetchAnthropicModels() {
|
|
|
539
394
|
}
|
|
540
395
|
|
|
541
396
|
/**
|
|
542
|
-
*
|
|
543
|
-
* @
|
|
544
|
-
*/
|
|
545
|
-
async function parseOpenRouterModelsResponse(res) {
|
|
546
|
-
if (!res.ok) {
|
|
547
|
-
throw new Error(`OpenRouter Models API HTTP ${res.status}`);
|
|
548
|
-
}
|
|
549
|
-
const body = await res.json();
|
|
550
|
-
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
551
|
-
/** @type {ModelOption[]} */
|
|
552
|
-
const models = [];
|
|
553
|
-
for (const row of rows) {
|
|
554
|
-
const id = typeof row?.id === 'string' ? row.id.trim() : '';
|
|
555
|
-
if (!isValidModelId(id)) continue;
|
|
556
|
-
const display =
|
|
557
|
-
typeof row.name === 'string'
|
|
558
|
-
? row.name
|
|
559
|
-
: typeof row?.id === 'string'
|
|
560
|
-
? row.id
|
|
561
|
-
: '';
|
|
562
|
-
models.push(toOption(id, display));
|
|
563
|
-
}
|
|
564
|
-
if (models.length === 0) {
|
|
565
|
-
throw new Error('OpenRouter Models API returned no models');
|
|
566
|
-
}
|
|
567
|
-
return models;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Fetch live models from OpenRouter (no cache).
|
|
572
|
-
* @returns {Promise<ModelOption[]>}
|
|
573
|
-
*/
|
|
574
|
-
export async function fetchOpenRouterModels() {
|
|
575
|
-
const env = envResolver();
|
|
576
|
-
const apiKey = (env.OPENROUTER_API_KEY || '').trim();
|
|
577
|
-
if (!apiKey) {
|
|
578
|
-
throw new Error('No OpenRouter API key for models list');
|
|
579
|
-
}
|
|
580
|
-
const doFetch = fetchImpl || globalThis.fetch;
|
|
581
|
-
if (typeof doFetch !== 'function') {
|
|
582
|
-
throw new Error('fetch is not available');
|
|
583
|
-
}
|
|
584
|
-
const res = await doFetch(OPENROUTER_MODELS_URL, {
|
|
585
|
-
method: 'GET',
|
|
586
|
-
headers: {
|
|
587
|
-
Authorization: `Bearer ${apiKey}`,
|
|
588
|
-
'HTTP-Referer': 'https://github.com/acdev',
|
|
589
|
-
'X-Title': 'acdev',
|
|
590
|
-
},
|
|
591
|
-
});
|
|
592
|
-
return parseOpenRouterModelsResponse(res);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* @param {LlmProvider} provider
|
|
597
|
-
* @param {{ selected?: string, force?: boolean }} opts
|
|
397
|
+
* List Claude models for the UI.
|
|
398
|
+
* @param {{ selected?: string, force?: boolean }} [opts]
|
|
598
399
|
* @returns {Promise<ModelsListResult>}
|
|
599
400
|
*/
|
|
600
|
-
async function
|
|
401
|
+
export async function listModels(opts = {}) {
|
|
601
402
|
const selectedRaw = opts.selected;
|
|
602
403
|
const force = opts.force === true;
|
|
603
404
|
const now = Date.now();
|
|
604
|
-
const cache = cacheByProvider.get(provider);
|
|
605
405
|
|
|
606
406
|
if (!force && cache && cache.expiresAt > now) {
|
|
607
407
|
const selected = reconcileModelForProvider(cache.result.models, selectedRaw);
|
|
@@ -609,45 +409,24 @@ async function listModelsForProvider(provider, opts = {}) {
|
|
|
609
409
|
...cache.result,
|
|
610
410
|
models: cache.result.models,
|
|
611
411
|
selected,
|
|
612
|
-
provider,
|
|
613
412
|
};
|
|
614
413
|
}
|
|
615
414
|
|
|
616
|
-
const curated = curatedModelOptions(
|
|
415
|
+
const curated = curatedModelOptions();
|
|
617
416
|
|
|
618
417
|
try {
|
|
619
|
-
const liveRaw =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
: await fetchAnthropicModels();
|
|
623
|
-
const live = filterModelsForProvider(liveRaw, provider);
|
|
624
|
-
let models = filterModelsForProvider(mergeModelLists(curated, live), provider);
|
|
625
|
-
if (provider === 'openrouter') {
|
|
626
|
-
models = ensureOpenRouterModels(models, curated);
|
|
627
|
-
}
|
|
418
|
+
const liveRaw = await fetchAnthropicModels();
|
|
419
|
+
const live = filterClaudeModels(liveRaw);
|
|
420
|
+
const models = filterClaudeModels(mergeModelLists(curated, live));
|
|
628
421
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return { ...result, selected, provider };
|
|
422
|
+
const result = { models, source: /** @type {ModelsSource} */ ('anthropic') };
|
|
423
|
+
cache = { expiresAt: now + CACHE_TTL_MS, result };
|
|
424
|
+
return { ...result, selected };
|
|
633
425
|
} catch {
|
|
634
|
-
|
|
635
|
-
if (provider === 'openrouter') {
|
|
636
|
-
models = ensureOpenRouterModels(models, curated);
|
|
637
|
-
}
|
|
426
|
+
const models = curated;
|
|
638
427
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
639
428
|
const result = { models, source: /** @type {ModelsSource} */ ('fallback') };
|
|
640
|
-
|
|
641
|
-
return { ...result, selected
|
|
429
|
+
cache = { expiresAt: now + 30_000, result };
|
|
430
|
+
return { ...result, selected };
|
|
642
431
|
}
|
|
643
432
|
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* List models for the UI.
|
|
647
|
-
* @param {{ selected?: string, force?: boolean, provider?: LlmProvider }} [opts]
|
|
648
|
-
* @returns {Promise<ModelsListResult>}
|
|
649
|
-
*/
|
|
650
|
-
export async function listModels(opts = {}) {
|
|
651
|
-
const provider = isValidLlmProvider(opts.provider) ? opts.provider : 'claude';
|
|
652
|
-
return listModelsForProvider(provider, opts);
|
|
653
|
-
}
|
package/src/server.js
CHANGED
|
@@ -38,8 +38,7 @@ import { splitIssueUrls } from './urls.js';
|
|
|
38
38
|
import { usageFromLogs, withJobUsage } from './usage.js';
|
|
39
39
|
import { checkGhAuth } from './gh-auth.js';
|
|
40
40
|
import { checkClaudeAuth } from './claude-auth.js';
|
|
41
|
-
import {
|
|
42
|
-
import { isValidLlmProvider, isValidModelId, isNoModel, NO_MODEL, isOpenRouterAgentCompatibleModel, enhanceOpenRouterAgentError } from './models.js';
|
|
41
|
+
import { isValidModelId, isNoModel, NO_MODEL } from './models.js';
|
|
43
42
|
|
|
44
43
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
44
|
|
|
@@ -175,7 +174,6 @@ export function normalizeReviewComments(body) {
|
|
|
175
174
|
* resolveJiraCredentials?: Function,
|
|
176
175
|
* checkGhAuth?: typeof checkGhAuth,
|
|
177
176
|
* checkClaudeAuth?: typeof checkClaudeAuth,
|
|
178
|
-
* checkOpenRouterAuth?: typeof checkOpenRouterAuth,
|
|
179
177
|
* },
|
|
180
178
|
* }} options
|
|
181
179
|
*/
|
|
@@ -191,18 +189,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
191
189
|
const doApplyFileExclusions = deps.applyFileExclusions || applyFileExclusions;
|
|
192
190
|
const doCheckGhAuth = deps.checkGhAuth || checkGhAuth;
|
|
193
191
|
const doCheckClaudeAuth = deps.checkClaudeAuth || checkClaudeAuth;
|
|
194
|
-
const doCheckOpenRouterAuth = deps.checkOpenRouterAuth || checkOpenRouterAuth;
|
|
195
|
-
|
|
196
|
-
function currentLlmProvider() {
|
|
197
|
-
return isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude';
|
|
198
|
-
}
|
|
199
192
|
|
|
200
193
|
function formatAgentJobError(err) {
|
|
201
|
-
|
|
202
|
-
if (currentLlmProvider() === 'openrouter') {
|
|
203
|
-
message = enhanceOpenRouterAgentError(message, config.model);
|
|
204
|
-
}
|
|
205
|
-
return message;
|
|
194
|
+
return err instanceof Error ? err.message : String(err);
|
|
206
195
|
}
|
|
207
196
|
|
|
208
197
|
/**
|
|
@@ -242,35 +231,14 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
242
231
|
};
|
|
243
232
|
}
|
|
244
233
|
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
code: 'openrouter_auth_required',
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
if (!isOpenRouterAgentCompatibleModel(model)) {
|
|
257
|
-
return {
|
|
258
|
-
status: 400,
|
|
259
|
-
error:
|
|
260
|
-
`Model "${model}" is not supported for OpenRouter agent runs. acdev uses the Claude Agent SDK (Anthropic-compatible API). Choose an anthropic/* model such as anthropic/claude-sonnet-4.`,
|
|
261
|
-
code: 'openrouter_model_incompatible',
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
const claude = doCheckClaudeAuth();
|
|
266
|
-
if (!claude.ok) {
|
|
267
|
-
return {
|
|
268
|
-
status: 400,
|
|
269
|
-
error:
|
|
270
|
-
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
271
|
-
code: 'claude_auth_required',
|
|
272
|
-
};
|
|
273
|
-
}
|
|
234
|
+
const claude = doCheckClaudeAuth();
|
|
235
|
+
if (!claude.ok) {
|
|
236
|
+
return {
|
|
237
|
+
status: 400,
|
|
238
|
+
error:
|
|
239
|
+
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
240
|
+
code: 'claude_auth_required',
|
|
241
|
+
};
|
|
274
242
|
}
|
|
275
243
|
}
|
|
276
244
|
|
|
@@ -283,7 +251,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
283
251
|
stubAgent: useStubAgent,
|
|
284
252
|
ghAuth: doCheckGhAuth(),
|
|
285
253
|
claudeAuth: doCheckClaudeAuth(),
|
|
286
|
-
openrouterAuth: doCheckOpenRouterAuth(),
|
|
287
254
|
});
|
|
288
255
|
}
|
|
289
256
|
|
|
@@ -603,11 +570,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
603
570
|
req.query.refresh === '1' ||
|
|
604
571
|
req.query.refresh === 'true' ||
|
|
605
572
|
req.query.force === '1';
|
|
606
|
-
const
|
|
607
|
-
req.query.provider === 'openrouter' || req.query.provider === 'claude'
|
|
608
|
-
? req.query.provider
|
|
609
|
-
: currentLlmProvider();
|
|
610
|
-
const result = await listModels({ selected: config.model, force, provider });
|
|
573
|
+
const result = await listModels({ selected: config.model, force });
|
|
611
574
|
res.json(result);
|
|
612
575
|
} catch (err) {
|
|
613
576
|
res.status(500).json({ error: err.message });
|
|
@@ -631,7 +594,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
631
594
|
applySecretField(envPatch, 'GH_TOKEN', patch.ghToken);
|
|
632
595
|
applySecretField(envPatch, 'ANTHROPIC_API_KEY', patch.anthropicApiKey);
|
|
633
596
|
applySecretField(envPatch, 'CLAUDE_CODE_OAUTH_TOKEN', patch.claudeOauthToken);
|
|
634
|
-
applySecretField(envPatch, 'OPENROUTER_API_KEY', patch.openrouterApiKey);
|
|
635
597
|
if (patch.jiraBaseUrl !== undefined && typeof patch.jiraBaseUrl === 'string') {
|
|
636
598
|
// Also mirror base URL into env for convenience when set via Settings
|
|
637
599
|
const trimmed = patch.jiraBaseUrl.trim();
|
|
@@ -649,7 +611,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
649
611
|
ghToken: _gh,
|
|
650
612
|
anthropicApiKey: _ak,
|
|
651
613
|
claudeOauthToken: _oa,
|
|
652
|
-
openrouterApiKey: _or,
|
|
653
614
|
...configPatch
|
|
654
615
|
} = patch;
|
|
655
616
|
updateConfig(repoRoot, config, configPatch);
|
package/src/openrouter-auth.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/** @typedef {{ ok: true } | { ok: false, reason: 'missing' }} OpenRouterAuthResult */
|
|
2
|
-
|
|
3
|
-
/** @type {() => NodeJS.ProcessEnv} */
|
|
4
|
-
let envResolver = () => process.env;
|
|
5
|
-
|
|
6
|
-
/** @param {() => NodeJS.ProcessEnv} fn */
|
|
7
|
-
export function _setEnvResolver(fn) {
|
|
8
|
-
envResolver = fn;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function _resetEnvResolver() {
|
|
12
|
-
envResolver = () => process.env;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Whether OpenRouter can authenticate agent runs.
|
|
17
|
-
* @returns {OpenRouterAuthResult}
|
|
18
|
-
*/
|
|
19
|
-
export function checkOpenRouterAuth() {
|
|
20
|
-
const key = (envResolver().OPENROUTER_API_KEY || '').trim();
|
|
21
|
-
if (key) return { ok: true };
|
|
22
|
-
return { ok: false, reason: 'missing' };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Human-readable startup warning for a failed {@link checkOpenRouterAuth}.
|
|
27
|
-
* Soft-auth: server still starts so Settings can configure the key.
|
|
28
|
-
* @param {OpenRouterAuthResult} [_result]
|
|
29
|
-
*/
|
|
30
|
-
export function formatOpenRouterAuthError(_result) {
|
|
31
|
-
return [
|
|
32
|
-
'⚠ OpenRouter is not authenticated — server will still start.',
|
|
33
|
-
' Open Settings → Authentication to add your OpenRouter API key',
|
|
34
|
-
' (or set OPENROUTER_API_KEY in `.acdev/.env`).',
|
|
35
|
-
' For UI-only testing without auth: pass `--stub-agent`.',
|
|
36
|
-
].join('\n');
|
|
37
|
-
}
|