acdev 1.0.7 → 1.0.8

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.7",
3
+ "version": "1.0.8",
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
@@ -129,6 +129,73 @@ let settingsSaving = false;
129
129
  let settingsTab = 'ticket';
130
130
  /** Sentinel: no model selected (jobs blocked until user picks one). */
131
131
  const NO_MODEL = '-';
132
+ const OPENROUTER_AGENT_MODEL_PREFIX = 'anthropic/';
133
+ /** @type {string} */
134
+ let modelSourceHintBase = '';
135
+ /**
136
+ * @param {string} modelId
137
+ * @returns {boolean}
138
+ */
139
+ function isOpenRouterAgentCompatibleModel(modelId) {
140
+ if (!modelId || !String(modelId).includes('/')) return false;
141
+ return String(modelId).trim().toLowerCase().startsWith(OPENROUTER_AGENT_MODEL_PREFIX);
142
+ }
143
+
144
+ /**
145
+ * @param {HTMLElement} btn
146
+ * @param {{ id: string, agentCompatible?: boolean }} model
147
+ */
148
+ function appendModelCompatibilityBadge(btn, model) {
149
+ if (currentLlmProviderSelection() !== 'openrouter') return;
150
+ const compatible =
151
+ model.agentCompatible ?? isOpenRouterAgentCompatibleModel(model.id);
152
+ const badge = document.createElement('span');
153
+ badge.className = compatible
154
+ ? 'model-picker-badge model-picker-badge--agent'
155
+ : 'model-picker-badge model-picker-badge--reference';
156
+ badge.textContent = compatible ? 'Agent' : 'May not work';
157
+ badge.title = compatible
158
+ ? 'Supported for acdev agent jobs (anthropic/* via Claude Agent SDK)'
159
+ : 'Listed for reference; agent jobs require anthropic/* models';
160
+ btn.appendChild(badge);
161
+ }
162
+
163
+ /**
164
+ * @param {HTMLElement} btn
165
+ * @param {{ id: string, label?: string, name?: string, agentCompatible?: boolean }} model
166
+ */
167
+ function populateModelPickerOption(btn, model) {
168
+ const titleRow = document.createElement('span');
169
+ titleRow.className = 'model-picker-option-title-row';
170
+
171
+ const title = document.createElement('span');
172
+ title.className = 'model-picker-option-title';
173
+ title.textContent = modelPickerLabel(model);
174
+ titleRow.appendChild(title);
175
+ appendModelCompatibilityBadge(titleRow, model);
176
+ btn.appendChild(titleRow);
177
+
178
+ if (model.id !== modelPickerLabel(model)) {
179
+ const sub = document.createElement('span');
180
+ sub.className = 'model-picker-option-id';
181
+ sub.textContent = model.id;
182
+ btn.appendChild(sub);
183
+ }
184
+ }
185
+
186
+ function refreshModelSourceHint() {
187
+ if (!els.settingsModelHint) return;
188
+ const provider = currentLlmProviderSelection();
189
+ let text = modelSourceHintBase;
190
+ if (
191
+ provider === 'openrouter' &&
192
+ !isNoModelSelection(currentModel) &&
193
+ !isOpenRouterAgentCompatibleModel(currentModel)
194
+ ) {
195
+ text += ` Selected model "${currentModel}" may not work for agent jobs — choose an anthropic/* model to run jobs.`;
196
+ }
197
+ els.settingsModelHint.textContent = text;
198
+ }
132
199
  /** @type {string} */
133
200
  let currentModel = 'claude-sonnet-5';
134
201
  /** @type {Array<{id:string,label?:string,name?:string}>} */
@@ -3012,6 +3079,7 @@ function updateModelLabel() {
3012
3079
  els.modelCombined.title = combined;
3013
3080
  }
3014
3081
  syncAgentPickerDisplay();
3082
+ refreshModelSourceHint();
3015
3083
  }
3016
3084
 
3017
3085
  /**
@@ -3019,8 +3087,10 @@ function updateModelLabel() {
3019
3087
  * @returns {Array<{id:string,label?:string,name?:string}>}
3020
3088
  */
3021
3089
  function getAvailableModels(provider = currentLlmProviderSelection()) {
3022
- if (availableModelsProvider === provider) return availableModels;
3023
- return [];
3090
+ if (availableModelsProvider === provider && availableModels.length) {
3091
+ return availableModels;
3092
+ }
3093
+ return defaultModelsForProvider(provider);
3024
3094
  }
3025
3095
 
3026
3096
  /**
@@ -3281,16 +3351,7 @@ function renderModelComboboxList(instance, selectedId = currentModel) {
3281
3351
  btn.classList.add('is-active');
3282
3352
  }
3283
3353
 
3284
- const title = document.createElement('span');
3285
- title.textContent = modelPickerLabel(m);
3286
- btn.appendChild(title);
3287
-
3288
- if (m.id !== modelPickerLabel(m)) {
3289
- const sub = document.createElement('span');
3290
- sub.className = 'model-picker-option-id';
3291
- sub.textContent = m.id;
3292
- btn.appendChild(sub);
3293
- }
3354
+ populateModelPickerOption(btn, m);
3294
3355
 
3295
3356
  btn.addEventListener('click', () => {
3296
3357
  instance.onSelect(m.id);
@@ -3477,16 +3538,7 @@ function renderAgentPickerModelList(instance, selectedId = currentModel) {
3477
3538
  btn.classList.add('is-active');
3478
3539
  }
3479
3540
 
3480
- const title = document.createElement('span');
3481
- title.textContent = modelPickerLabel(m);
3482
- btn.appendChild(title);
3483
-
3484
- if (m.id !== modelPickerLabel(m)) {
3485
- const sub = document.createElement('span');
3486
- sub.className = 'model-picker-option-id';
3487
- sub.textContent = m.id;
3488
- btn.appendChild(sub);
3489
- }
3541
+ populateModelPickerOption(btn, m);
3490
3542
 
3491
3543
  btn.addEventListener('click', () => {
3492
3544
  instance.onSelect(m.id);
@@ -3637,9 +3689,16 @@ function lastModelForProvider(provider) {
3637
3689
  function defaultModelsForProvider(provider = 'claude') {
3638
3690
  if (provider === 'openrouter') {
3639
3691
  return [
3640
- { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
3641
- { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
3642
- { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
3692
+ { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)', agentCompatible: true },
3693
+ { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)', agentCompatible: true },
3694
+ { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)', agentCompatible: true },
3695
+ { id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)', agentCompatible: true },
3696
+ { id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)', agentCompatible: false },
3697
+ { id: 'openai/gpt-4o-mini', label: 'GPT-4o Mini (OpenAI)', agentCompatible: false },
3698
+ { id: 'google/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro Preview (Google)', agentCompatible: false },
3699
+ { id: 'qwen/qwen3-235b-a22b', label: 'Qwen3 235B (Qwen)', agentCompatible: false },
3700
+ { id: 'deepseek/deepseek-chat-v3-0324', label: 'DeepSeek Chat V3 (DeepSeek)', agentCompatible: false },
3701
+ { id: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B Instruct (Meta)', agentCompatible: false },
3643
3702
  ];
3644
3703
  }
3645
3704
  return [
@@ -3660,22 +3719,23 @@ function updateModelSourceHint(source, provider = 'claude') {
3660
3719
  if (!els.settingsModelHint) return;
3661
3720
  const name = provider === 'openrouter' ? 'OpenRouter' : 'Claude';
3662
3721
  if (source === 'anthropic') {
3663
- els.settingsModelHint.textContent =
3722
+ modelSourceHintBase =
3664
3723
  `${name} model for agent runs. List loaded from Anthropic Models API.`;
3665
3724
  } else if (source === 'openrouter') {
3666
- els.settingsModelHint.textContent =
3667
- 'OpenRouter model for agent runs (anthropic/* only Claude Agent SDK uses Anthropic-compatible API). List loaded from OpenRouter Models API.';
3725
+ modelSourceHintBase =
3726
+ 'OpenRouter models for agent runs. Full catalog from OpenRouter Models API. Agent jobs require anthropic/* models (marked “Agent” in the list).';
3668
3727
  } else if (source === 'fallback') {
3669
3728
  if (provider === 'openrouter') {
3670
- els.settingsModelHint.textContent =
3671
- 'OpenRouter model for agent runs (anthropic/* only). Showing curated Anthropic models (live list unavailable — set OPENROUTER_API_KEY in Authentication).';
3729
+ modelSourceHintBase =
3730
+ 'OpenRouter models for agent runs. Showing curated catalog (live list unavailable — set OPENROUTER_API_KEY in Authentication). Agent jobs require anthropic/* models.';
3672
3731
  } else {
3673
- els.settingsModelHint.textContent =
3732
+ modelSourceHintBase =
3674
3733
  'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
3675
3734
  }
3676
3735
  } else {
3677
- els.settingsModelHint.textContent = `${name} model for agent runs.`;
3736
+ modelSourceHintBase = `${name} model for agent runs.`;
3678
3737
  }
3738
+ refreshModelSourceHint();
3679
3739
  }
3680
3740
 
3681
3741
  /**
@@ -3721,7 +3781,7 @@ function syncLlmProviderSelects(provider) {
3721
3781
  if (els.settingsLlmProviderHint) {
3722
3782
  els.settingsLlmProviderHint.textContent =
3723
3783
  value === 'openrouter'
3724
- ? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY). Use anthropic/* models only.'
3784
+ ? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY). All models are listed; agent jobs need anthropic/* models.'
3725
3785
  : 'Agent runs use Claude via Anthropic API, OAuth token, or claude auth login.';
3726
3786
  }
3727
3787
  }
@@ -3732,13 +3792,16 @@ function syncLlmProviderSelects(provider) {
3732
3792
  function populateSettingsModels(data) {
3733
3793
  const provider = data?.provider || currentLlmProviderSelection();
3734
3794
  const cached = getAvailableModels(provider);
3735
- const models = Array.isArray(data?.models)
3795
+ let models = Array.isArray(data?.models)
3736
3796
  ? data.models
3737
3797
  : data?.reconcileProvider
3738
3798
  ? []
3739
3799
  : cached.length
3740
3800
  ? cached
3741
3801
  : defaultModelsForProvider(provider);
3802
+ if (!models.length && data?.source) {
3803
+ models = defaultModelsForProvider(provider);
3804
+ }
3742
3805
  setAvailableModels(provider, models);
3743
3806
 
3744
3807
  if (data?.reconcileProvider) {
@@ -3758,10 +3821,9 @@ function populateSettingsModels(data) {
3758
3821
  updateModelLabel();
3759
3822
  }
3760
3823
 
3761
- function modelsForFetchFallback(provider, reconcileProvider) {
3762
- if (reconcileProvider) return [];
3824
+ function modelsForFetchFallback(provider) {
3763
3825
  const cached = getAvailableModels(provider);
3764
- if (cached.length) return cached;
3826
+ if (availableModelsProvider === provider && cached.length) return cached;
3765
3827
  return defaultModelsForProvider(provider);
3766
3828
  }
3767
3829
 
@@ -3779,7 +3841,7 @@ async function fetchModels(opts = {}) {
3779
3841
  const res = await fetch(`/api/models?${query.toString()}`);
3780
3842
  if (!res.ok) {
3781
3843
  populateSettingsModels({
3782
- models: modelsForFetchFallback(provider, reconcileProvider),
3844
+ models: modelsForFetchFallback(provider),
3783
3845
  selected: currentModel,
3784
3846
  provider,
3785
3847
  reconcileProvider,
@@ -3797,7 +3859,7 @@ async function fetchModels(opts = {}) {
3797
3859
  } catch (err) {
3798
3860
  console.error('Failed to fetch models:', err);
3799
3861
  populateSettingsModels({
3800
- models: modelsForFetchFallback(provider, reconcileProvider),
3862
+ models: modelsForFetchFallback(provider),
3801
3863
  selected: currentModel,
3802
3864
  provider,
3803
3865
  reconcileProvider,
@@ -4379,11 +4441,13 @@ function applyConfigSnapshot(data) {
4379
4441
  async function fetchConfig() {
4380
4442
  try {
4381
4443
  const res = await fetch('/api/config');
4382
- if (!res.ok) return;
4444
+ if (!res.ok) return null;
4383
4445
  const data = await readJson(res);
4384
4446
  applyConfigSnapshot(data);
4447
+ return data;
4385
4448
  } catch (err) {
4386
4449
  console.error('Failed to fetch config:', err);
4450
+ return null;
4387
4451
  }
4388
4452
  }
4389
4453
 
@@ -5031,7 +5095,10 @@ els.logJobFilter.addEventListener('change', () => {
5031
5095
  });
5032
5096
 
5033
5097
  applyTheme(loadTheme());
5034
- fetchConfig();
5035
- fetchModels();
5098
+ void (async () => {
5099
+ const cfg = await fetchConfig();
5100
+ const provider = cfg?.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
5101
+ await fetchModels({ provider });
5102
+ })();
5036
5103
  fetchJobs();
5037
5104
  setInterval(fetchJobs, 3000);
package/public/styles.css CHANGED
@@ -2298,6 +2298,40 @@ body.diff-fs-open {
2298
2298
  cursor: pointer;
2299
2299
  }
2300
2300
 
2301
+ .model-picker-option-title-row {
2302
+ display: flex;
2303
+ align-items: center;
2304
+ justify-content: space-between;
2305
+ gap: 8px;
2306
+ }
2307
+
2308
+ .model-picker-option-title {
2309
+ min-width: 0;
2310
+ overflow: hidden;
2311
+ text-overflow: ellipsis;
2312
+ white-space: nowrap;
2313
+ }
2314
+
2315
+ .model-picker-badge {
2316
+ flex-shrink: 0;
2317
+ padding: 1px 6px;
2318
+ border-radius: 999px;
2319
+ font-size: 10px;
2320
+ font-weight: 600;
2321
+ letter-spacing: 0.02em;
2322
+ text-transform: uppercase;
2323
+ }
2324
+
2325
+ .model-picker-badge--agent {
2326
+ color: var(--primary);
2327
+ background: color-mix(in srgb, var(--primary) 14%, transparent);
2328
+ }
2329
+
2330
+ .model-picker-badge--reference {
2331
+ color: var(--text-muted);
2332
+ background: color-mix(in srgb, var(--text-muted) 12%, transparent);
2333
+ }
2334
+
2301
2335
  .model-picker-option:hover,
2302
2336
  .model-picker-option.is-active {
2303
2337
  background: var(--surface-2);
package/src/models.js CHANGED
@@ -45,18 +45,23 @@ export const MODEL_OPTIONS = CLAUDE_MODEL_OPTIONS;
45
45
  export const DEFAULT_MODEL = 'claude-sonnet-5';
46
46
 
47
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.
48
+ * OpenRouter slugs commonly used with acdev. Curated list is merged with the
49
+ * live OpenRouter catalog; anthropic/* entries are agent-compatible.
51
50
  */
52
51
  export const OPENROUTER_AGENT_MODEL_PREFIX = 'anthropic/';
53
52
 
54
- /** Curated OpenRouter model ids used as defaults + fallback (agent-compatible only). */
53
+ /** Curated OpenRouter model ids used as defaults + fallback when the live API is unavailable. */
55
54
  export const OPENROUTER_MODEL_OPTIONS = [
56
55
  { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
57
56
  { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
58
57
  { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
59
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)' },
60
65
  ];
61
66
 
62
67
  export const DEFAULT_OPENROUTER_MODEL = 'anthropic/claude-sonnet-4';
@@ -76,7 +81,7 @@ const ANTHROPIC_VERSION = '2023-06-01';
76
81
  const CACHE_TTL_MS = 5 * 60_000;
77
82
  const KEYCHAIN_SERVICE = 'Claude Code-credentials';
78
83
 
79
- /** @typedef {{ id: string, name?: string, label?: string }} ModelOption */
84
+ /** @typedef {{ id: string, name?: string, label?: string, agentCompatible?: boolean }} ModelOption */
80
85
  /** @typedef {'anthropic' | 'openrouter' | 'fallback'} ModelsSource */
81
86
  /** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource, provider?: LlmProvider }} ModelsListResult */
82
87
 
@@ -206,11 +211,36 @@ export function isOpenRouterAgentCompatibleModel(modelId) {
206
211
  }
207
212
 
208
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
228
+ * @returns {ModelOption[]}
229
+ */
230
+ function annotateOpenRouterModelsCompatibility(models) {
231
+ return models.map(annotateOpenRouterModelCompatibility);
232
+ }
233
+
234
+ /**
235
+ * If the live list is empty, fall back to curated options. Listing is not
236
+ * filtered to anthropic/* — runtime enqueue still validates agent compatibility.
209
237
  * @param {ModelOption[]} models
238
+ * @param {ModelOption[]} curated
210
239
  * @returns {ModelOption[]}
211
240
  */
212
- function filterOpenRouterAgentModels(models) {
213
- return models.filter((m) => m?.id && isOpenRouterAgentCompatibleModel(m.id));
241
+ function ensureOpenRouterModels(models, curated) {
242
+ const list = models.length > 0 ? models : curated;
243
+ return annotateOpenRouterModelsCompatibility(list);
214
244
  }
215
245
 
216
246
  /**
@@ -593,7 +623,7 @@ async function listModelsForProvider(provider, opts = {}) {
593
623
  const live = filterModelsForProvider(liveRaw, provider);
594
624
  let models = filterModelsForProvider(mergeModelLists(curated, live), provider);
595
625
  if (provider === 'openrouter') {
596
- models = filterOpenRouterAgentModels(models);
626
+ models = ensureOpenRouterModels(models, curated);
597
627
  }
598
628
  const selected = reconcileModelForProvider(models, selectedRaw);
599
629
  const source = /** @type {ModelsSource} */ (provider === 'openrouter' ? 'openrouter' : 'anthropic');
@@ -603,7 +633,7 @@ async function listModelsForProvider(provider, opts = {}) {
603
633
  } catch {
604
634
  let models = curated;
605
635
  if (provider === 'openrouter') {
606
- models = filterOpenRouterAgentModels(models);
636
+ models = ensureOpenRouterModels(models, curated);
607
637
  }
608
638
  const selected = reconcileModelForProvider(models, selectedRaw);
609
639
  const result = { models, source: /** @type {ModelsSource} */ ('fallback') };