acdev 1.0.6 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acdev",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Local CLI + web UI for running AI agents on GitHub issues via git worktrees",
5
5
  "type": "module",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -3639,8 +3639,7 @@ function defaultModelsForProvider(provider = 'claude') {
3639
3639
  return [
3640
3640
  { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
3641
3641
  { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
3642
- { id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)' },
3643
- { id: 'x-ai/grok-2', label: 'Grok 2 (xAI)' },
3642
+ { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
3644
3643
  ];
3645
3644
  }
3646
3645
  return [
@@ -3665,11 +3664,11 @@ function updateModelSourceHint(source, provider = 'claude') {
3665
3664
  `${name} model for agent runs. List loaded from Anthropic Models API.`;
3666
3665
  } else if (source === 'openrouter') {
3667
3666
  els.settingsModelHint.textContent =
3668
- 'OpenRouter model for agent runs. List loaded from OpenRouter Models API.';
3667
+ 'OpenRouter model for agent runs (anthropic/* only — Claude Agent SDK uses Anthropic-compatible API). List loaded from OpenRouter Models API.';
3669
3668
  } else if (source === 'fallback') {
3670
3669
  if (provider === 'openrouter') {
3671
3670
  els.settingsModelHint.textContent =
3672
- 'OpenRouter model for agent runs. Showing curated models (live list unavailable — set OPENROUTER_API_KEY in Authentication).';
3671
+ 'OpenRouter model for agent runs (anthropic/* only). Showing curated Anthropic models (live list unavailable — set OPENROUTER_API_KEY in Authentication).';
3673
3672
  } else {
3674
3673
  els.settingsModelHint.textContent =
3675
3674
  'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
@@ -3722,7 +3721,7 @@ function syncLlmProviderSelects(provider) {
3722
3721
  if (els.settingsLlmProviderHint) {
3723
3722
  els.settingsLlmProviderHint.textContent =
3724
3723
  value === 'openrouter'
3725
- ? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY in Authentication).'
3724
+ ? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY). Use anthropic/* models only.'
3726
3725
  : 'Agent runs use Claude via Anthropic API, OAuth token, or claude auth login.';
3727
3726
  }
3728
3727
  }
package/src/agent.js CHANGED
@@ -427,11 +427,16 @@ export async function withLlmProviderEnv(config, fn) {
427
427
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
428
428
  HTTP_REFERER: process.env.HTTP_REFERER,
429
429
  X_TITLE: process.env.X_TITLE,
430
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS,
430
431
  };
431
432
 
432
433
  process.env.ANTHROPIC_BASE_URL = OPENROUTER_ANTHROPIC_BASE_URL;
433
434
  process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
434
435
  process.env.ANTHROPIC_API_KEY = '';
436
+ // OpenRouter rejects Anthropic-only beta headers on some models.
437
+ if (!process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) {
438
+ process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = '1';
439
+ }
435
440
  if (!process.env.HTTP_REFERER) {
436
441
  process.env.HTTP_REFERER = 'https://github.com/acdev';
437
442
  }
package/src/models.js CHANGED
@@ -44,17 +44,19 @@ export const MODEL_OPTIONS = CLAUDE_MODEL_OPTIONS;
44
44
 
45
45
  export const DEFAULT_MODEL = 'claude-sonnet-5';
46
46
 
47
- /** Curated OpenRouter model ids used as defaults + fallback. */
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). */
48
55
  export const OPENROUTER_MODEL_OPTIONS = [
49
56
  { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
50
57
  { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
51
58
  { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
52
59
  { id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)' },
53
- { id: 'openai/gpt-4.1', label: 'GPT-4.1 (OpenAI)' },
54
- { id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)' },
55
- { id: 'google/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro (Google)' },
56
- { id: 'x-ai/grok-2', label: 'Grok 2 (xAI)' },
57
- { id: 'x-ai/grok-2-1212', label: 'Grok 2 1212 (xAI)' },
58
60
  ];
59
61
 
60
62
  export const DEFAULT_OPENROUTER_MODEL = 'anthropic/claude-sonnet-4';
@@ -191,6 +193,64 @@ export function isOpenRouterCatalogId(id) {
191
193
  return isValidModelId(id) && String(id).includes('/');
192
194
  }
193
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
+
194
254
  /**
195
255
  * Curated fallback list for a provider.
196
256
  * @param {LlmProvider} provider
@@ -531,14 +591,20 @@ async function listModelsForProvider(provider, opts = {}) {
531
591
  ? await fetchOpenRouterModels()
532
592
  : await fetchAnthropicModels();
533
593
  const live = filterModelsForProvider(liveRaw, provider);
534
- const models = filterModelsForProvider(mergeModelLists(curated, live), provider);
594
+ let models = filterModelsForProvider(mergeModelLists(curated, live), provider);
595
+ if (provider === 'openrouter') {
596
+ models = filterOpenRouterAgentModels(models);
597
+ }
535
598
  const selected = reconcileModelForProvider(models, selectedRaw);
536
599
  const source = /** @type {ModelsSource} */ (provider === 'openrouter' ? 'openrouter' : 'anthropic');
537
600
  const result = { models, source };
538
601
  cacheByProvider.set(provider, { expiresAt: now + CACHE_TTL_MS, result });
539
602
  return { ...result, selected, provider };
540
603
  } catch {
541
- const models = curated;
604
+ let models = curated;
605
+ if (provider === 'openrouter') {
606
+ models = filterOpenRouterAgentModels(models);
607
+ }
542
608
  const selected = reconcileModelForProvider(models, selectedRaw);
543
609
  const result = { models, source: /** @type {ModelsSource} */ ('fallback') };
544
610
  cacheByProvider.set(provider, { expiresAt: now + 30_000, result });
package/src/server.js CHANGED
@@ -39,7 +39,7 @@ import { usageFromLogs, withJobUsage } from './usage.js';
39
39
  import { checkGhAuth } from './gh-auth.js';
40
40
  import { checkClaudeAuth } from './claude-auth.js';
41
41
  import { checkOpenRouterAuth } from './openrouter-auth.js';
42
- import { isValidLlmProvider, isValidModelId, isNoModel, NO_MODEL } from './models.js';
42
+ import { isValidLlmProvider, isValidModelId, isNoModel, NO_MODEL, isOpenRouterAgentCompatibleModel, enhanceOpenRouterAgentError } from './models.js';
43
43
 
44
44
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
45
 
@@ -197,6 +197,14 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
197
197
  return isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude';
198
198
  }
199
199
 
200
+ 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;
206
+ }
207
+
200
208
  /**
201
209
  * Reject enqueue / agent / PR actions when required auth is missing.
202
210
  * @param {{ needGh?: boolean, needLlm?: boolean }} [opts]
@@ -245,6 +253,14 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
245
253
  code: 'openrouter_auth_required',
246
254
  };
247
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
+ }
248
264
  } else {
249
265
  const claude = doCheckClaudeAuth();
250
266
  if (!claude.ok) {
@@ -421,7 +437,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
421
437
  if (usage) patch.usage = usage;
422
438
  setStatus(jobId, 'awaiting_review', patch);
423
439
  } catch (err) {
424
- const message = err instanceof Error ? err.message : String(err);
440
+ const message = formatAgentJobError(err);
425
441
  const current = store.getJob(jobId);
426
442
  if (!current) return;
427
443
  appendLog(current, 'error', message);
@@ -522,7 +538,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
522
538
  if (usage) patch.usage = usage;
523
539
  setStatus(jobId, 'awaiting_review', patch);
524
540
  } catch (err) {
525
- const message = err instanceof Error ? err.message : String(err);
541
+ const message = formatAgentJobError(err);
526
542
  const current = store.getJob(jobId);
527
543
  if (!current) return;
528
544
  appendLog(current, 'error', message);