acdev 1.0.6 → 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 +1 -1
- package/public/app.js +109 -43
- package/public/styles.css +34 -0
- package/src/agent.js +5 -0
- package/src/models.js +104 -8
- package/src/server.js +19 -3
package/package.json
CHANGED
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
|
|
3023
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,10 +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: '
|
|
3643
|
-
{ id: '
|
|
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 },
|
|
3644
3702
|
];
|
|
3645
3703
|
}
|
|
3646
3704
|
return [
|
|
@@ -3661,22 +3719,23 @@ function updateModelSourceHint(source, provider = 'claude') {
|
|
|
3661
3719
|
if (!els.settingsModelHint) return;
|
|
3662
3720
|
const name = provider === 'openrouter' ? 'OpenRouter' : 'Claude';
|
|
3663
3721
|
if (source === 'anthropic') {
|
|
3664
|
-
|
|
3722
|
+
modelSourceHintBase =
|
|
3665
3723
|
`${name} model for agent runs. List loaded from Anthropic Models API.`;
|
|
3666
3724
|
} else if (source === 'openrouter') {
|
|
3667
|
-
|
|
3668
|
-
'OpenRouter
|
|
3725
|
+
modelSourceHintBase =
|
|
3726
|
+
'OpenRouter models for agent runs. Full catalog from OpenRouter Models API. Agent jobs require anthropic/* models (marked “Agent” in the list).';
|
|
3669
3727
|
} else if (source === 'fallback') {
|
|
3670
3728
|
if (provider === 'openrouter') {
|
|
3671
|
-
|
|
3672
|
-
'OpenRouter
|
|
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.';
|
|
3673
3731
|
} else {
|
|
3674
|
-
|
|
3732
|
+
modelSourceHintBase =
|
|
3675
3733
|
'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
|
|
3676
3734
|
}
|
|
3677
3735
|
} else {
|
|
3678
|
-
|
|
3736
|
+
modelSourceHintBase = `${name} model for agent runs.`;
|
|
3679
3737
|
}
|
|
3738
|
+
refreshModelSourceHint();
|
|
3680
3739
|
}
|
|
3681
3740
|
|
|
3682
3741
|
/**
|
|
@@ -3722,7 +3781,7 @@ function syncLlmProviderSelects(provider) {
|
|
|
3722
3781
|
if (els.settingsLlmProviderHint) {
|
|
3723
3782
|
els.settingsLlmProviderHint.textContent =
|
|
3724
3783
|
value === 'openrouter'
|
|
3725
|
-
? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY
|
|
3784
|
+
? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY). All models are listed; agent jobs need anthropic/* models.'
|
|
3726
3785
|
: 'Agent runs use Claude via Anthropic API, OAuth token, or claude auth login.';
|
|
3727
3786
|
}
|
|
3728
3787
|
}
|
|
@@ -3733,13 +3792,16 @@ function syncLlmProviderSelects(provider) {
|
|
|
3733
3792
|
function populateSettingsModels(data) {
|
|
3734
3793
|
const provider = data?.provider || currentLlmProviderSelection();
|
|
3735
3794
|
const cached = getAvailableModels(provider);
|
|
3736
|
-
|
|
3795
|
+
let models = Array.isArray(data?.models)
|
|
3737
3796
|
? data.models
|
|
3738
3797
|
: data?.reconcileProvider
|
|
3739
3798
|
? []
|
|
3740
3799
|
: cached.length
|
|
3741
3800
|
? cached
|
|
3742
3801
|
: defaultModelsForProvider(provider);
|
|
3802
|
+
if (!models.length && data?.source) {
|
|
3803
|
+
models = defaultModelsForProvider(provider);
|
|
3804
|
+
}
|
|
3743
3805
|
setAvailableModels(provider, models);
|
|
3744
3806
|
|
|
3745
3807
|
if (data?.reconcileProvider) {
|
|
@@ -3759,10 +3821,9 @@ function populateSettingsModels(data) {
|
|
|
3759
3821
|
updateModelLabel();
|
|
3760
3822
|
}
|
|
3761
3823
|
|
|
3762
|
-
function modelsForFetchFallback(provider
|
|
3763
|
-
if (reconcileProvider) return [];
|
|
3824
|
+
function modelsForFetchFallback(provider) {
|
|
3764
3825
|
const cached = getAvailableModels(provider);
|
|
3765
|
-
if (cached.length) return cached;
|
|
3826
|
+
if (availableModelsProvider === provider && cached.length) return cached;
|
|
3766
3827
|
return defaultModelsForProvider(provider);
|
|
3767
3828
|
}
|
|
3768
3829
|
|
|
@@ -3780,7 +3841,7 @@ async function fetchModels(opts = {}) {
|
|
|
3780
3841
|
const res = await fetch(`/api/models?${query.toString()}`);
|
|
3781
3842
|
if (!res.ok) {
|
|
3782
3843
|
populateSettingsModels({
|
|
3783
|
-
models: modelsForFetchFallback(provider
|
|
3844
|
+
models: modelsForFetchFallback(provider),
|
|
3784
3845
|
selected: currentModel,
|
|
3785
3846
|
provider,
|
|
3786
3847
|
reconcileProvider,
|
|
@@ -3798,7 +3859,7 @@ async function fetchModels(opts = {}) {
|
|
|
3798
3859
|
} catch (err) {
|
|
3799
3860
|
console.error('Failed to fetch models:', err);
|
|
3800
3861
|
populateSettingsModels({
|
|
3801
|
-
models: modelsForFetchFallback(provider
|
|
3862
|
+
models: modelsForFetchFallback(provider),
|
|
3802
3863
|
selected: currentModel,
|
|
3803
3864
|
provider,
|
|
3804
3865
|
reconcileProvider,
|
|
@@ -4380,11 +4441,13 @@ function applyConfigSnapshot(data) {
|
|
|
4380
4441
|
async function fetchConfig() {
|
|
4381
4442
|
try {
|
|
4382
4443
|
const res = await fetch('/api/config');
|
|
4383
|
-
if (!res.ok) return;
|
|
4444
|
+
if (!res.ok) return null;
|
|
4384
4445
|
const data = await readJson(res);
|
|
4385
4446
|
applyConfigSnapshot(data);
|
|
4447
|
+
return data;
|
|
4386
4448
|
} catch (err) {
|
|
4387
4449
|
console.error('Failed to fetch config:', err);
|
|
4450
|
+
return null;
|
|
4388
4451
|
}
|
|
4389
4452
|
}
|
|
4390
4453
|
|
|
@@ -5032,7 +5095,10 @@ els.logJobFilter.addEventListener('change', () => {
|
|
|
5032
5095
|
});
|
|
5033
5096
|
|
|
5034
5097
|
applyTheme(loadTheme());
|
|
5035
|
-
|
|
5036
|
-
|
|
5098
|
+
void (async () => {
|
|
5099
|
+
const cfg = await fetchConfig();
|
|
5100
|
+
const provider = cfg?.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
|
|
5101
|
+
await fetchModels({ provider });
|
|
5102
|
+
})();
|
|
5037
5103
|
fetchJobs();
|
|
5038
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/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,24 @@ export const MODEL_OPTIONS = CLAUDE_MODEL_OPTIONS;
|
|
|
44
44
|
|
|
45
45
|
export const DEFAULT_MODEL = 'claude-sonnet-5';
|
|
46
46
|
|
|
47
|
-
/**
|
|
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. */
|
|
48
54
|
export const OPENROUTER_MODEL_OPTIONS = [
|
|
49
55
|
{ id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
|
|
50
56
|
{ id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
|
|
51
57
|
{ id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
|
|
52
58
|
{ id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)' },
|
|
53
|
-
{ id: 'openai/gpt-4.1', label: 'GPT-4.1 (OpenAI)' },
|
|
54
59
|
{ id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)' },
|
|
55
|
-
{ id: '
|
|
56
|
-
{ id: '
|
|
57
|
-
{ id: '
|
|
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)' },
|
|
58
65
|
];
|
|
59
66
|
|
|
60
67
|
export const DEFAULT_OPENROUTER_MODEL = 'anthropic/claude-sonnet-4';
|
|
@@ -74,7 +81,7 @@ const ANTHROPIC_VERSION = '2023-06-01';
|
|
|
74
81
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
75
82
|
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
76
83
|
|
|
77
|
-
/** @typedef {{ id: string, name?: string, label?: string }} ModelOption */
|
|
84
|
+
/** @typedef {{ id: string, name?: string, label?: string, agentCompatible?: boolean }} ModelOption */
|
|
78
85
|
/** @typedef {'anthropic' | 'openrouter' | 'fallback'} ModelsSource */
|
|
79
86
|
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource, provider?: LlmProvider }} ModelsListResult */
|
|
80
87
|
|
|
@@ -191,6 +198,89 @@ export function isOpenRouterCatalogId(id) {
|
|
|
191
198
|
return isValidModelId(id) && String(id).includes('/');
|
|
192
199
|
}
|
|
193
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
|
|
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.
|
|
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
|
+
|
|
194
284
|
/**
|
|
195
285
|
* Curated fallback list for a provider.
|
|
196
286
|
* @param {LlmProvider} provider
|
|
@@ -531,14 +621,20 @@ async function listModelsForProvider(provider, opts = {}) {
|
|
|
531
621
|
? await fetchOpenRouterModels()
|
|
532
622
|
: await fetchAnthropicModels();
|
|
533
623
|
const live = filterModelsForProvider(liveRaw, provider);
|
|
534
|
-
|
|
624
|
+
let models = filterModelsForProvider(mergeModelLists(curated, live), provider);
|
|
625
|
+
if (provider === 'openrouter') {
|
|
626
|
+
models = ensureOpenRouterModels(models, curated);
|
|
627
|
+
}
|
|
535
628
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
536
629
|
const source = /** @type {ModelsSource} */ (provider === 'openrouter' ? 'openrouter' : 'anthropic');
|
|
537
630
|
const result = { models, source };
|
|
538
631
|
cacheByProvider.set(provider, { expiresAt: now + CACHE_TTL_MS, result });
|
|
539
632
|
return { ...result, selected, provider };
|
|
540
633
|
} catch {
|
|
541
|
-
|
|
634
|
+
let models = curated;
|
|
635
|
+
if (provider === 'openrouter') {
|
|
636
|
+
models = ensureOpenRouterModels(models, curated);
|
|
637
|
+
}
|
|
542
638
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
543
639
|
const result = { models, source: /** @type {ModelsSource} */ ('fallback') };
|
|
544
640
|
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 =
|
|
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 =
|
|
541
|
+
const message = formatAgentJobError(err);
|
|
526
542
|
const current = store.getJob(jobId);
|
|
527
543
|
if (!current) return;
|
|
528
544
|
appendLog(current, 'error', message);
|