acdev 1.0.7 → 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 +80 -353
- package/public/index.html +1 -33
- package/public/styles.css +7 -4
- package/src/agent.js +64 -127
- package/src/config.js +45 -151
- package/src/models.js +34 -225
- 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,44 +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 that work with acdev agent runs.
|
|
49
|
-
* Agent jobs use Claude Agent SDK via OpenRouter's Anthropic-compatible API;
|
|
50
|
-
* only anthropic/* models are supported on that path.
|
|
51
|
-
*/
|
|
52
|
-
export const OPENROUTER_AGENT_MODEL_PREFIX = 'anthropic/';
|
|
53
|
-
|
|
54
|
-
/** Curated OpenRouter model ids used as defaults + fallback (agent-compatible only). */
|
|
55
|
-
export const OPENROUTER_MODEL_OPTIONS = [
|
|
56
|
-
{ id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
|
|
57
|
-
{ id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
|
|
58
|
-
{ id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
|
|
59
|
-
{ id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)' },
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
export const DEFAULT_OPENROUTER_MODEL = 'anthropic/claude-sonnet-4';
|
|
63
|
-
|
|
64
46
|
/** Config value meaning no model selected (jobs blocked until user picks one). */
|
|
65
47
|
export const NO_MODEL = '-';
|
|
66
48
|
|
|
67
|
-
/**
|
|
68
|
-
export const LLM_PROVIDERS = /** @type {const} */ (['claude', 'openrouter']);
|
|
69
|
-
|
|
70
|
-
/** 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 `/`). */
|
|
71
50
|
export const MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:\/-]{0,127}$/;
|
|
72
51
|
|
|
73
52
|
const ANTHROPIC_MODELS_URL = 'https://api.anthropic.com/v1/models';
|
|
74
|
-
const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
75
53
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
76
54
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
77
55
|
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
78
56
|
|
|
79
57
|
/** @typedef {{ id: string, name?: string, label?: string }} ModelOption */
|
|
80
|
-
/** @typedef {'anthropic' | '
|
|
81
|
-
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource
|
|
58
|
+
/** @typedef {'anthropic' | 'fallback'} ModelsSource */
|
|
59
|
+
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource }} ModelsListResult */
|
|
82
60
|
|
|
83
|
-
/** @type {
|
|
84
|
-
|
|
61
|
+
/** @type {{ expiresAt: number, result: Omit<ModelsListResult, 'selected'> } | null} */
|
|
62
|
+
let cache = null;
|
|
85
63
|
|
|
86
64
|
/** @type {typeof fetch | null} */
|
|
87
65
|
let fetchImpl = null;
|
|
@@ -120,23 +98,7 @@ export function _resetCredentialsTokenResolver() {
|
|
|
120
98
|
}
|
|
121
99
|
|
|
122
100
|
export function _resetModelsCache() {
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* @param {unknown} value
|
|
128
|
-
* @returns {value is LlmProvider}
|
|
129
|
-
*/
|
|
130
|
-
export function isValidLlmProvider(value) {
|
|
131
|
-
return value === 'claude' || value === 'openrouter';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Default model id for an LLM provider.
|
|
136
|
-
* @param {LlmProvider} [provider]
|
|
137
|
-
*/
|
|
138
|
-
export function defaultModelForProvider(provider = 'claude') {
|
|
139
|
-
return provider === 'openrouter' ? DEFAULT_OPENROUTER_MODEL : DEFAULT_MODEL;
|
|
101
|
+
cache = null;
|
|
140
102
|
}
|
|
141
103
|
|
|
142
104
|
/**
|
|
@@ -176,7 +138,7 @@ function toOption(id, displayName) {
|
|
|
176
138
|
}
|
|
177
139
|
|
|
178
140
|
/**
|
|
179
|
-
* Claude
|
|
141
|
+
* Claude catalog ids: Anthropic API ids and Claude Code aliases (no provider/ prefix).
|
|
180
142
|
* @param {string} id
|
|
181
143
|
* @returns {boolean}
|
|
182
144
|
*/
|
|
@@ -185,122 +147,45 @@ export function isClaudeCatalogId(id) {
|
|
|
185
147
|
}
|
|
186
148
|
|
|
187
149
|
/**
|
|
188
|
-
*
|
|
189
|
-
* @param {string} id
|
|
190
|
-
* @returns {boolean}
|
|
191
|
-
*/
|
|
192
|
-
export function isOpenRouterCatalogId(id) {
|
|
193
|
-
return isValidModelId(id) && String(id).includes('/');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Whether an OpenRouter slug can be used for acdev agent runs.
|
|
198
|
-
* Non-anthropic slugs may appear in OpenRouter's catalog but fail at runtime
|
|
199
|
-
* because the Claude Agent SDK speaks Anthropic Messages API semantics.
|
|
200
|
-
* @param {string} modelId
|
|
201
|
-
* @returns {boolean}
|
|
202
|
-
*/
|
|
203
|
-
export function isOpenRouterAgentCompatibleModel(modelId) {
|
|
204
|
-
if (!isOpenRouterCatalogId(modelId)) return false;
|
|
205
|
-
return String(modelId).trim().toLowerCase().startsWith(OPENROUTER_AGENT_MODEL_PREFIX);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* @param {ModelOption[]} models
|
|
210
|
-
* @returns {ModelOption[]}
|
|
211
|
-
*/
|
|
212
|
-
function filterOpenRouterAgentModels(models) {
|
|
213
|
-
return models.filter((m) => m?.id && isOpenRouterAgentCompatibleModel(m.id));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Improve OpenRouter agent failure messages for the job UI.
|
|
218
|
-
* @param {string} message
|
|
219
|
-
* @param {string} [model]
|
|
220
|
-
* @returns {string}
|
|
221
|
-
*/
|
|
222
|
-
export function enhanceOpenRouterAgentError(message, model) {
|
|
223
|
-
const raw = String(message || '').trim();
|
|
224
|
-
if (!raw) return raw;
|
|
225
|
-
|
|
226
|
-
const id = String(model || '').trim();
|
|
227
|
-
const routingFailure =
|
|
228
|
-
/no allowed providers are available/i.test(raw) ||
|
|
229
|
-
/not allowed by provider/i.test(raw) ||
|
|
230
|
-
/model.*not allowed/i.test(raw);
|
|
231
|
-
|
|
232
|
-
if (id && !isOpenRouterAgentCompatibleModel(id)) {
|
|
233
|
-
return [
|
|
234
|
-
`OpenRouter model "${id}" is not supported for acdev agent runs.`,
|
|
235
|
-
'Agent jobs use the Claude Agent SDK via OpenRouter\'s Anthropic-compatible API.',
|
|
236
|
-
'Choose an anthropic/* model (e.g. anthropic/claude-sonnet-4).',
|
|
237
|
-
raw !== id ? `Provider error: ${raw}` : '',
|
|
238
|
-
]
|
|
239
|
-
.filter(Boolean)
|
|
240
|
-
.join(' ');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (routingFailure) {
|
|
244
|
-
return [
|
|
245
|
-
raw,
|
|
246
|
-
'If you use OpenRouter provider allowlists, clear Settings → Privacy → Providers or add the model\'s upstream provider.',
|
|
247
|
-
'For acdev, anthropic/* models on OpenRouter are the most reliable choice.',
|
|
248
|
-
].join(' ');
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return raw;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Curated fallback list for a provider.
|
|
256
|
-
* @param {LlmProvider} provider
|
|
150
|
+
* Curated fallback list.
|
|
257
151
|
* @returns {ModelOption[]}
|
|
258
152
|
*/
|
|
259
|
-
export function curatedModelOptions(
|
|
260
|
-
return
|
|
261
|
-
? OPENROUTER_MODEL_OPTIONS.map((m) => ({ ...m }))
|
|
262
|
-
: CLAUDE_MODEL_OPTIONS.map((m) => ({ ...m }));
|
|
153
|
+
export function curatedModelOptions() {
|
|
154
|
+
return CLAUDE_MODEL_OPTIONS.map((m) => ({ ...m }));
|
|
263
155
|
}
|
|
264
156
|
|
|
265
157
|
/**
|
|
266
|
-
* Whether a model id belongs in
|
|
158
|
+
* Whether a model id belongs in the curated catalog.
|
|
267
159
|
* @param {string} modelId
|
|
268
|
-
* @param {LlmProvider} provider
|
|
269
160
|
* @returns {boolean}
|
|
270
161
|
*/
|
|
271
|
-
export function isModelInCuratedCatalog(modelId
|
|
162
|
+
export function isModelInCuratedCatalog(modelId) {
|
|
272
163
|
if (isNoModel(modelId)) return false;
|
|
273
164
|
const id = String(modelId).trim();
|
|
274
|
-
return curatedModelOptions(
|
|
165
|
+
return curatedModelOptions().some((m) => m.id === id);
|
|
275
166
|
}
|
|
276
167
|
|
|
277
168
|
/**
|
|
278
|
-
* Whether a model id is valid for
|
|
279
|
-
* Claude: direct ids without `/`. OpenRouter: provider/model slugs with `/`.
|
|
169
|
+
* Whether a model id is valid for Claude (direct ids without `/`).
|
|
280
170
|
* @param {string} modelId
|
|
281
|
-
* @param {LlmProvider} provider
|
|
282
171
|
* @returns {boolean}
|
|
283
172
|
*/
|
|
284
|
-
export function isModelIdForProvider(modelId
|
|
173
|
+
export function isModelIdForProvider(modelId) {
|
|
285
174
|
if (isNoModel(modelId) || !isValidModelId(modelId)) return false;
|
|
286
|
-
|
|
287
|
-
return provider === 'openrouter' ? isOpenRouterCatalogId(id) : isClaudeCatalogId(id);
|
|
175
|
+
return isClaudeCatalogId(String(modelId).trim());
|
|
288
176
|
}
|
|
289
177
|
|
|
290
178
|
/**
|
|
291
|
-
* Drop ids that do not belong in
|
|
179
|
+
* Drop ids that do not belong in the Claude catalog.
|
|
292
180
|
* @param {ModelOption[]} models
|
|
293
|
-
* @param {LlmProvider} provider
|
|
294
181
|
* @returns {ModelOption[]}
|
|
295
182
|
*/
|
|
296
|
-
function
|
|
297
|
-
|
|
298
|
-
provider === 'openrouter' ? isOpenRouterCatalogId : isClaudeCatalogId;
|
|
299
|
-
return models.filter((m) => m?.id && keep(m.id));
|
|
183
|
+
function filterClaudeModels(models) {
|
|
184
|
+
return models.filter((m) => m?.id && isClaudeCatalogId(m.id));
|
|
300
185
|
}
|
|
301
186
|
|
|
302
187
|
/**
|
|
303
|
-
* Merge curated options (stable order / aliases) with live rows
|
|
188
|
+
* Merge curated options (stable order / aliases) with live rows.
|
|
304
189
|
* Same ids keep curated position but prefer live display names.
|
|
305
190
|
* @param {ModelOption[]} curated
|
|
306
191
|
* @param {ModelOption[]} live
|
|
@@ -335,7 +220,7 @@ function mergeModelLists(curated, live) {
|
|
|
335
220
|
}
|
|
336
221
|
|
|
337
222
|
/**
|
|
338
|
-
* Keep selected only when it exists in the
|
|
223
|
+
* Keep selected only when it exists in the model catalog.
|
|
339
224
|
* @param {ModelOption[]} models
|
|
340
225
|
* @param {string} selected
|
|
341
226
|
* @returns {string}
|
|
@@ -509,69 +394,14 @@ export async function fetchAnthropicModels() {
|
|
|
509
394
|
}
|
|
510
395
|
|
|
511
396
|
/**
|
|
512
|
-
*
|
|
513
|
-
* @
|
|
514
|
-
*/
|
|
515
|
-
async function parseOpenRouterModelsResponse(res) {
|
|
516
|
-
if (!res.ok) {
|
|
517
|
-
throw new Error(`OpenRouter Models API HTTP ${res.status}`);
|
|
518
|
-
}
|
|
519
|
-
const body = await res.json();
|
|
520
|
-
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
521
|
-
/** @type {ModelOption[]} */
|
|
522
|
-
const models = [];
|
|
523
|
-
for (const row of rows) {
|
|
524
|
-
const id = typeof row?.id === 'string' ? row.id.trim() : '';
|
|
525
|
-
if (!isValidModelId(id)) continue;
|
|
526
|
-
const display =
|
|
527
|
-
typeof row.name === 'string'
|
|
528
|
-
? row.name
|
|
529
|
-
: typeof row?.id === 'string'
|
|
530
|
-
? row.id
|
|
531
|
-
: '';
|
|
532
|
-
models.push(toOption(id, display));
|
|
533
|
-
}
|
|
534
|
-
if (models.length === 0) {
|
|
535
|
-
throw new Error('OpenRouter Models API returned no models');
|
|
536
|
-
}
|
|
537
|
-
return models;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Fetch live models from OpenRouter (no cache).
|
|
542
|
-
* @returns {Promise<ModelOption[]>}
|
|
543
|
-
*/
|
|
544
|
-
export async function fetchOpenRouterModels() {
|
|
545
|
-
const env = envResolver();
|
|
546
|
-
const apiKey = (env.OPENROUTER_API_KEY || '').trim();
|
|
547
|
-
if (!apiKey) {
|
|
548
|
-
throw new Error('No OpenRouter API key for models list');
|
|
549
|
-
}
|
|
550
|
-
const doFetch = fetchImpl || globalThis.fetch;
|
|
551
|
-
if (typeof doFetch !== 'function') {
|
|
552
|
-
throw new Error('fetch is not available');
|
|
553
|
-
}
|
|
554
|
-
const res = await doFetch(OPENROUTER_MODELS_URL, {
|
|
555
|
-
method: 'GET',
|
|
556
|
-
headers: {
|
|
557
|
-
Authorization: `Bearer ${apiKey}`,
|
|
558
|
-
'HTTP-Referer': 'https://github.com/acdev',
|
|
559
|
-
'X-Title': 'acdev',
|
|
560
|
-
},
|
|
561
|
-
});
|
|
562
|
-
return parseOpenRouterModelsResponse(res);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* @param {LlmProvider} provider
|
|
567
|
-
* @param {{ selected?: string, force?: boolean }} opts
|
|
397
|
+
* List Claude models for the UI.
|
|
398
|
+
* @param {{ selected?: string, force?: boolean }} [opts]
|
|
568
399
|
* @returns {Promise<ModelsListResult>}
|
|
569
400
|
*/
|
|
570
|
-
async function
|
|
401
|
+
export async function listModels(opts = {}) {
|
|
571
402
|
const selectedRaw = opts.selected;
|
|
572
403
|
const force = opts.force === true;
|
|
573
404
|
const now = Date.now();
|
|
574
|
-
const cache = cacheByProvider.get(provider);
|
|
575
405
|
|
|
576
406
|
if (!force && cache && cache.expiresAt > now) {
|
|
577
407
|
const selected = reconcileModelForProvider(cache.result.models, selectedRaw);
|
|
@@ -579,45 +409,24 @@ async function listModelsForProvider(provider, opts = {}) {
|
|
|
579
409
|
...cache.result,
|
|
580
410
|
models: cache.result.models,
|
|
581
411
|
selected,
|
|
582
|
-
provider,
|
|
583
412
|
};
|
|
584
413
|
}
|
|
585
414
|
|
|
586
|
-
const curated = curatedModelOptions(
|
|
415
|
+
const curated = curatedModelOptions();
|
|
587
416
|
|
|
588
417
|
try {
|
|
589
|
-
const liveRaw =
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
: await fetchAnthropicModels();
|
|
593
|
-
const live = filterModelsForProvider(liveRaw, provider);
|
|
594
|
-
let models = filterModelsForProvider(mergeModelLists(curated, live), provider);
|
|
595
|
-
if (provider === 'openrouter') {
|
|
596
|
-
models = filterOpenRouterAgentModels(models);
|
|
597
|
-
}
|
|
418
|
+
const liveRaw = await fetchAnthropicModels();
|
|
419
|
+
const live = filterClaudeModels(liveRaw);
|
|
420
|
+
const models = filterClaudeModels(mergeModelLists(curated, live));
|
|
598
421
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
return { ...result, selected, provider };
|
|
422
|
+
const result = { models, source: /** @type {ModelsSource} */ ('anthropic') };
|
|
423
|
+
cache = { expiresAt: now + CACHE_TTL_MS, result };
|
|
424
|
+
return { ...result, selected };
|
|
603
425
|
} catch {
|
|
604
|
-
|
|
605
|
-
if (provider === 'openrouter') {
|
|
606
|
-
models = filterOpenRouterAgentModels(models);
|
|
607
|
-
}
|
|
426
|
+
const models = curated;
|
|
608
427
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
609
428
|
const result = { models, source: /** @type {ModelsSource} */ ('fallback') };
|
|
610
|
-
|
|
611
|
-
return { ...result, selected
|
|
429
|
+
cache = { expiresAt: now + 30_000, result };
|
|
430
|
+
return { ...result, selected };
|
|
612
431
|
}
|
|
613
432
|
}
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* List models for the UI.
|
|
617
|
-
* @param {{ selected?: string, force?: boolean, provider?: LlmProvider }} [opts]
|
|
618
|
-
* @returns {Promise<ModelsListResult>}
|
|
619
|
-
*/
|
|
620
|
-
export async function listModels(opts = {}) {
|
|
621
|
-
const provider = isValidLlmProvider(opts.provider) ? opts.provider : 'claude';
|
|
622
|
-
return listModelsForProvider(provider, opts);
|
|
623
|
-
}
|
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
|
-
}
|