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/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
- /** @typedef {'claude' | 'openrouter'} LlmProvider */
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' | 'openrouter' | 'fallback'} ModelsSource */
81
- /** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource, provider?: LlmProvider }} ModelsListResult */
58
+ /** @typedef {'anthropic' | 'fallback'} ModelsSource */
59
+ /** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource }} ModelsListResult */
82
60
 
83
- /** @type {Map<LlmProvider, { expiresAt: number, result: Omit<ModelsListResult, 'selected' | 'provider'> }>} */
84
- const cacheByProvider = new Map();
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
- cacheByProvider.clear();
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 direct path ids: Anthropic API ids and Claude Code aliases (no provider/ prefix).
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
- * OpenRouter slugs use provider/model form (always includes `/`).
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(provider) {
260
- return provider === 'openrouter'
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 a provider's curated catalog.
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, provider) {
162
+ export function isModelInCuratedCatalog(modelId) {
272
163
  if (isNoModel(modelId)) return false;
273
164
  const id = String(modelId).trim();
274
- return curatedModelOptions(provider).some((m) => m.id === id);
165
+ return curatedModelOptions().some((m) => m.id === id);
275
166
  }
276
167
 
277
168
  /**
278
- * Whether a model id is valid for a provider (format-based, not curated-only).
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, provider) {
173
+ export function isModelIdForProvider(modelId) {
285
174
  if (isNoModel(modelId) || !isValidModelId(modelId)) return false;
286
- const id = String(modelId).trim();
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 this provider's catalog.
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 filterModelsForProvider(models, provider) {
297
- const keep =
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 for one provider.
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 provider model catalog.
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
- * @param {Response} res
513
- * @returns {Promise<ModelOption[]>}
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 listModelsForProvider(provider, opts = {}) {
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(provider);
415
+ const curated = curatedModelOptions();
587
416
 
588
417
  try {
589
- const liveRaw =
590
- provider === 'openrouter'
591
- ? await fetchOpenRouterModels()
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 source = /** @type {ModelsSource} */ (provider === 'openrouter' ? 'openrouter' : 'anthropic');
600
- const result = { models, source };
601
- cacheByProvider.set(provider, { expiresAt: now + CACHE_TTL_MS, result });
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
- let models = curated;
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
- cacheByProvider.set(provider, { expiresAt: now + 30_000, result });
611
- return { ...result, selected, provider };
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 { checkOpenRouterAuth } from './openrouter-auth.js';
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
- let message = err instanceof Error ? err.message : String(err);
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 provider = currentLlmProvider();
246
- if (provider === 'openrouter') {
247
- const openrouter = doCheckOpenRouterAuth();
248
- if (!openrouter.ok) {
249
- return {
250
- status: 400,
251
- error:
252
- 'OpenRouter is not authenticated. Add an API key in Settings → Authentication, or start with --stub-agent.',
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 provider =
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);
@@ -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
- }