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/public/index.html CHANGED
@@ -155,10 +155,6 @@
155
155
 
156
156
  <div class="enqueue-actions">
157
157
  <div class="agent-picker" id="overview-agent-picker" aria-label="Agent">
158
- <select id="overview-llm-provider" class="sr-only" aria-hidden="true" tabindex="-1">
159
- <option value="claude">Claude</option>
160
- <option value="openrouter">OpenRouter</option>
161
- </select>
162
158
  <input type="hidden" id="overview-model" value="">
163
159
  <button
164
160
  type="button"
@@ -171,15 +167,11 @@
171
167
  <span class="agent-pill-icon agent-pill-icon--claude" id="overview-agent-icon" aria-hidden="true"></span>
172
168
  <span class="agent-pill-body">
173
169
  <span class="agent-pill-label">Agent</span>
174
- <span class="agent-pill-value" id="overview-agent-value">Claude • —</span>
170
+ <span class="agent-pill-value" id="overview-agent-value">—</span>
175
171
  </span>
176
172
  <svg class="agent-pill-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
177
173
  </button>
178
174
  <div class="agent-picker-panel" id="overview-agent-panel" role="dialog" aria-label="Choose agent model" aria-hidden="true">
179
- <div class="agent-picker-tabs" role="tablist" aria-label="Provider">
180
- <button type="button" class="agent-picker-tab is-active" data-provider="claude" role="tab" aria-selected="true">Claude</button>
181
- <button type="button" class="agent-picker-tab" data-provider="openrouter" role="tab" aria-selected="false">OpenRouter</button>
182
- </div>
183
175
  <label class="sr-only" for="overview-agent-search">Search models</label>
184
176
  <input
185
177
  id="overview-agent-search"
@@ -538,21 +530,6 @@
538
530
  </div>
539
531
  </div>
540
532
  </div>
541
-
542
- <div class="card settings-card">
543
- <div class="rules-heading">OpenRouter authentication</div>
544
- <p id="settings-openrouter-status" class="auth-status" role="status">Checking OpenRouter auth…</p>
545
- <div class="settings-grid">
546
- <div class="settings-field settings-field-full">
547
- <label class="field-label" for="settings-openrouter-key">OpenRouter API key</label>
548
- <input id="settings-openrouter-key" class="input" type="password" name="openrouterApiKey" autocomplete="new-password" placeholder="Leave blank to keep existing">
549
- <button type="button" class="auth-clear hidden" id="settings-openrouter-key-clear">Clear saved API key</button>
550
- <p class="field-hint" id="settings-openrouter-key-hint">
551
- Required when LLM provider is OpenRouter. Get a key at <a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">openrouter.ai/keys</a>. Stored as <code>OPENROUTER_API_KEY</code> in <code>.acdev/.env</code>.
552
- </p>
553
- </div>
554
- </div>
555
- </div>
556
533
  </div>
557
534
  </div>
558
535
 
@@ -649,15 +626,6 @@
649
626
  </p>
650
627
  <div class="card settings-card">
651
628
  <div class="settings-grid">
652
- <div class="settings-field">
653
- <label class="field-label" for="settings-llm-provider">LLM provider</label>
654
- <select id="settings-llm-provider" class="input" name="llmProvider">
655
- <option value="claude">Claude (Anthropic / Claude Code)</option>
656
- <option value="openrouter">OpenRouter</option>
657
- </select>
658
- <p class="field-hint" id="settings-llm-provider-hint">Agent runs use the selected provider and model below.</p>
659
- </div>
660
-
661
629
  <div class="settings-field">
662
630
  <label class="field-label" for="settings-base-branch">Base branch</label>
663
631
  <input id="settings-base-branch" class="input" type="text" name="baseBranch" autocomplete="off" required>
package/public/styles.css CHANGED
@@ -556,10 +556,6 @@ a { color: var(--primary); text-underline-offset: 3px; }
556
556
  background: linear-gradient(145deg, #e8a87c 0%, #d97706 100%);
557
557
  }
558
558
 
559
- .agent-pill-icon--openrouter {
560
- background: linear-gradient(145deg, #7c8cf5 0%, #5b4fe8 100%);
561
- }
562
-
563
559
  .agent-pill-body {
564
560
  flex: 1;
565
561
  min-width: 0;
@@ -2298,6 +2294,13 @@ body.diff-fs-open {
2298
2294
  cursor: pointer;
2299
2295
  }
2300
2296
 
2297
+ .model-picker-option-title {
2298
+ min-width: 0;
2299
+ overflow: hidden;
2300
+ text-overflow: ellipsis;
2301
+ white-space: nowrap;
2302
+ }
2303
+
2301
2304
  .model-picker-option:hover,
2302
2305
  .model-picker-option.is-active {
2303
2306
  background: var(--surface-2);
package/src/agent.js CHANGED
@@ -2,7 +2,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
4
  import { ensureNoAiAttributionSettings, getIssueTitle } from './git.js';
5
- import { isValidLlmProvider } from './models.js';
6
5
  import { extractUsageFromResult } from './usage.js';
7
6
 
8
7
  const execFileAsync = promisify(execFile);
@@ -397,66 +396,6 @@ async function fetchIssueTitle(issueUrl, worktreePath) {
397
396
  */
398
397
  export const DISALLOWED_AGENT_TOOLS = ['Task', 'TaskOutput', 'AskUserQuestion'];
399
398
 
400
- const OPENROUTER_ANTHROPIC_BASE_URL = 'https://openrouter.ai/api';
401
-
402
- /**
403
- * Run an agent query with provider-specific Anthropic env overrides.
404
- * OpenRouter uses Claude Agent SDK via Anthropic-compatible "skin" routing.
405
- * @template T
406
- * @param {object} config
407
- * @param {() => Promise<T>} fn
408
- * @returns {Promise<T>}
409
- */
410
- export async function withLlmProviderEnv(config, fn) {
411
- const provider = isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude';
412
- if (provider !== 'openrouter') {
413
- return fn();
414
- }
415
-
416
- const apiKey = (process.env.OPENROUTER_API_KEY || '').trim();
417
- if (!apiKey) {
418
- throw new Error(
419
- 'OpenRouter is not authenticated. Add OPENROUTER_API_KEY in Settings → Authentication.'
420
- );
421
- }
422
-
423
- /** @type {Record<string, string | undefined>} */
424
- const saved = {
425
- ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
426
- ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
427
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
428
- HTTP_REFERER: process.env.HTTP_REFERER,
429
- X_TITLE: process.env.X_TITLE,
430
- CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS,
431
- };
432
-
433
- process.env.ANTHROPIC_BASE_URL = OPENROUTER_ANTHROPIC_BASE_URL;
434
- process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
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
- }
440
- if (!process.env.HTTP_REFERER) {
441
- process.env.HTTP_REFERER = 'https://github.com/acdev';
442
- }
443
- if (!process.env.X_TITLE) {
444
- process.env.X_TITLE = 'acdev';
445
- }
446
-
447
- try {
448
- return await fn();
449
- } finally {
450
- for (const [key, value] of Object.entries(saved)) {
451
- if (value === undefined) {
452
- delete process.env[key];
453
- } else {
454
- process.env[key] = value;
455
- }
456
- }
457
- }
458
- }
459
-
460
399
  /**
461
400
  * Prefer the first successful SDK result. A known SDK bug can emit a follow-up
462
401
  * `error_during_execution` right after success when async subagents finish.
@@ -483,78 +422,76 @@ export function mergeAgentResult(current, incoming) {
483
422
  * }} params
484
423
  */
485
424
  async function runAgentQuery({ prompt, worktreePath, config, onEvent, queryFn = query }) {
486
- return withLlmProviderEnv(config, async () => {
487
- const timeoutMs = config.agentTimeoutMs ?? 900_000;
488
- let lastMessage = null;
489
- let resultMessage = null;
490
- const abortController = new AbortController();
491
-
492
- // Claude Agent SDK defaults to injecting Co-Authored-By trailers into commit
493
- // instructions unless attribution is cleared via Claude settings.
494
- ensureNoAiAttributionSettings(worktreePath);
495
-
496
- const runLoop = async () => {
497
- try {
498
- for await (const message of queryFn({
499
- // String prompt = single-turn SDK input (still uses stream-json transport).
500
- prompt,
501
- options: {
502
- cwd: worktreePath,
503
- allowedTools: config.allowedTools,
504
- disallowedTools: DISALLOWED_AGENT_TOOLS,
505
- permissionMode: 'acceptEdits',
506
- maxTurns: config.maxAgentTurns,
507
- model: config.model || 'claude-sonnet-5',
508
- // Load only local settings so our empty attribution wins without
509
- // pulling in unrelated user settings.
510
- settingSources: ['local'],
511
- abortController,
512
- },
513
- })) {
514
- onEvent(message);
515
- lastMessage = message;
516
- if (message?.type === 'result') {
517
- resultMessage = mergeAgentResult(resultMessage, message);
518
- // First success is authoritative — stop before SDK async-task teardown
519
- // can overwrite it with a spurious streaming-mode error.
520
- if (message.subtype === 'success') {
521
- abortController.abort();
522
- break;
523
- }
524
- }
525
- }
526
- } catch (err) {
527
- // Aborting after success can surface as an iterator/abort error.
528
- if (resultMessage?.subtype === 'success') return;
529
- throw err;
530
- }
531
- };
425
+ const timeoutMs = config.agentTimeoutMs ?? 900_000;
426
+ let lastMessage = null;
427
+ let resultMessage = null;
428
+ const abortController = new AbortController();
429
+
430
+ // Claude Agent SDK defaults to injecting Co-Authored-By trailers into commit
431
+ // instructions unless attribution is cleared via Claude settings.
432
+ ensureNoAiAttributionSettings(worktreePath);
532
433
 
533
- let timeoutId;
434
+ const runLoop = async () => {
534
435
  try {
535
- await Promise.race([
536
- runLoop(),
537
- new Promise((_, reject) => {
538
- timeoutId = setTimeout(() => {
436
+ for await (const message of queryFn({
437
+ // String prompt = single-turn SDK input (still uses stream-json transport).
438
+ prompt,
439
+ options: {
440
+ cwd: worktreePath,
441
+ allowedTools: config.allowedTools,
442
+ disallowedTools: DISALLOWED_AGENT_TOOLS,
443
+ permissionMode: 'acceptEdits',
444
+ maxTurns: config.maxAgentTurns,
445
+ model: config.model || 'claude-sonnet-5',
446
+ // Load only local settings so our empty attribution wins without
447
+ // pulling in unrelated user settings.
448
+ settingSources: ['local'],
449
+ abortController,
450
+ },
451
+ })) {
452
+ onEvent(message);
453
+ lastMessage = message;
454
+ if (message?.type === 'result') {
455
+ resultMessage = mergeAgentResult(resultMessage, message);
456
+ // First success is authoritative — stop before SDK async-task teardown
457
+ // can overwrite it with a spurious streaming-mode error.
458
+ if (message.subtype === 'success') {
539
459
  abortController.abort();
540
- reject(new Error(`Agent timed out after ${timeoutMs}ms`));
541
- }, timeoutMs);
542
- }),
543
- ]);
544
- } finally {
545
- if (timeoutId) clearTimeout(timeoutId);
460
+ break;
461
+ }
462
+ }
463
+ }
464
+ } catch (err) {
465
+ // Aborting after success can surface as an iterator/abort error.
466
+ if (resultMessage?.subtype === 'success') return;
467
+ throw err;
546
468
  }
469
+ };
547
470
 
548
- if (!resultMessage || resultMessage.subtype !== 'success') {
549
- const detail = lastMessage ? JSON.stringify(lastMessage) : 'no result received';
550
- throw new Error(`Agent did not complete successfully: ${detail}`);
551
- }
471
+ let timeoutId;
472
+ try {
473
+ await Promise.race([
474
+ runLoop(),
475
+ new Promise((_, reject) => {
476
+ timeoutId = setTimeout(() => {
477
+ abortController.abort();
478
+ reject(new Error(`Agent timed out after ${timeoutMs}ms`));
479
+ }, timeoutMs);
480
+ }),
481
+ ]);
482
+ } finally {
483
+ if (timeoutId) clearTimeout(timeoutId);
484
+ }
552
485
 
553
- const resultText = resultMessage.result ?? '';
554
- const meta = extractPrMetadata(resultText);
555
- const usage = extractUsageFromResult(resultMessage);
556
- return { resultText, meta, usage };
557
- });
486
+ if (!resultMessage || resultMessage.subtype !== 'success') {
487
+ const detail = lastMessage ? JSON.stringify(lastMessage) : 'no result received';
488
+ throw new Error(`Agent did not complete successfully: ${detail}`);
489
+ }
490
+
491
+ const resultText = resultMessage.result ?? '';
492
+ const meta = extractPrMetadata(resultText);
493
+ const usage = extractUsageFromResult(resultMessage);
494
+ return { resultText, meta, usage };
558
495
  }
559
496
 
560
497
  function stubAgentResult(onEvent, title, body) {
package/src/config.js CHANGED
@@ -6,20 +6,15 @@ import { checkGhAuth, githubTokenFromEnv, originRemoteInfo } from './gh-auth.js'
6
6
  import { normalizeJiraBaseUrl } from './jira.js';
7
7
  import {
8
8
  DEFAULT_MODEL,
9
- DEFAULT_OPENROUTER_MODEL,
10
- LLM_PROVIDERS,
11
9
  NO_MODEL,
12
10
  isValidModelId,
13
11
  isNoModel,
14
- isValidLlmProvider,
15
- defaultModelForProvider,
16
- isModelIdForProvider,
12
+ isClaudeCatalogId,
17
13
  curatedModelOptions,
18
14
  } from './models.js';
19
- import { checkOpenRouterAuth } from './openrouter-auth.js';
20
15
  import { dataDir } from './paths.js';
21
16
 
22
- export { DEFAULT_MODEL, DEFAULT_OPENROUTER_MODEL, CLAUDE_MODEL_OPTIONS, MODEL_OPTIONS, OPENROUTER_MODEL_OPTIONS, LLM_PROVIDERS, NO_MODEL } from './models.js';
17
+ export { DEFAULT_MODEL, CLAUDE_MODEL_OPTIONS, MODEL_OPTIONS, NO_MODEL } from './models.js';
23
18
 
24
19
  /** Tool names the agent may be granted via config. */
25
20
  export const KNOWN_TOOLS = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
@@ -69,10 +64,7 @@ const DEFAULTS = {
69
64
  maxAgentTurns: 30,
70
65
  allowedTools: [...KNOWN_TOOLS],
71
66
  agentTimeoutMs: 900_000,
72
- llmProvider: /** @type {'claude' | 'openrouter'} */ ('claude'),
73
67
  model: DEFAULT_MODEL,
74
- /** Last valid model selection per LLM provider (for restore on provider switch). */
75
- lastModelsByProvider: /** @type {Record<string, string>} */ ({}),
76
68
  ticketSource: /** @type {'github' | 'jira'} */ ('github'),
77
69
  jiraBaseUrl: '',
78
70
  /** PR body phrase for Jira tickets, e.g. "Relates to PROJ-123". */
@@ -82,76 +74,29 @@ const DEFAULTS = {
82
74
  };
83
75
 
84
76
  /**
77
+ * Migrate legacy OpenRouter model ids to a Claude model.
85
78
  * @param {unknown} raw
86
- * @returns {Record<string, string>}
87
- */
88
- function normalizeLastModelsByProvider(raw) {
89
- /** @type {Record<string, string>} */
90
- const out = {};
91
- if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
92
- return out;
93
- }
94
- for (const provider of LLM_PROVIDERS) {
95
- const value = /** @type {Record<string, unknown>} */ (raw)[provider];
96
- if (typeof value !== 'string' || isNoModel(value) || !isValidModelId(value)) {
97
- continue;
98
- }
99
- const id = value.trim();
100
- if (isModelIdForProvider(id, provider)) {
101
- out[provider] = id;
102
- }
103
- }
104
- return out;
105
- }
106
-
107
- /**
108
- * @param {object} config
109
- * @param {'claude' | 'openrouter'} provider
110
- * @param {string} model
111
- */
112
- function rememberModelForProvider(config, provider, model) {
113
- if (!isValidLlmProvider(provider)) return;
114
- if (isNoModel(model) || !isValidModelId(model)) return;
115
- const id = String(model).trim();
116
- if (!isModelIdForProvider(id, provider)) return;
117
- if (
118
- !config.lastModelsByProvider ||
119
- typeof config.lastModelsByProvider !== 'object' ||
120
- config.lastModelsByProvider === DEFAULTS.lastModelsByProvider
121
- ) {
122
- config.lastModelsByProvider = {
123
- ...(config.lastModelsByProvider &&
124
- typeof config.lastModelsByProvider === 'object' &&
125
- config.lastModelsByProvider !== DEFAULTS.lastModelsByProvider
126
- ? config.lastModelsByProvider
127
- : {}),
128
- };
129
- }
130
- config.lastModelsByProvider[provider] = id;
131
- }
132
-
133
- /**
134
- * Pick model for a provider: preferred if in catalog, else last stored, else no selection.
135
- * @param {object} config
136
- * @param {'claude' | 'openrouter'} provider
137
- * @param {string} [preferredModel]
79
+ * @param {string} currentModel
138
80
  * @returns {string}
139
81
  */
140
- function resolveModelForProvider(config, provider, preferredModel) {
141
- const candidate =
142
- preferredModel != null ? preferredModel : config.model;
143
- if (!isNoModel(candidate) && isModelIdForProvider(candidate, provider)) {
144
- return String(candidate).trim();
145
- }
146
- const stored = config.lastModelsByProvider?.[provider];
147
- if (
148
- typeof stored === 'string' &&
149
- !isNoModel(stored) &&
150
- isModelIdForProvider(stored, provider)
151
- ) {
82
+ function migrateLegacyModel(raw, currentModel) {
83
+ if (isNoModel(currentModel)) return NO_MODEL;
84
+ if (isClaudeCatalogId(currentModel)) return String(currentModel).trim();
85
+
86
+ const stored =
87
+ raw != null &&
88
+ typeof raw === 'object' &&
89
+ !Array.isArray(raw) &&
90
+ typeof /** @type {Record<string, unknown>} */ (raw).lastModelsByProvider === 'object'
91
+ ? /** @type {Record<string, unknown>} */ (
92
+ /** @type {Record<string, unknown>} */ (raw).lastModelsByProvider
93
+ ).claude
94
+ : null;
95
+
96
+ if (typeof stored === 'string' && isClaudeCatalogId(stored)) {
152
97
  return stored.trim();
153
98
  }
154
- return NO_MODEL;
99
+ return DEFAULT_MODEL;
155
100
  }
156
101
 
157
102
  /**
@@ -294,9 +239,7 @@ function persistable(config) {
294
239
  maxAgentTurns: config.maxAgentTurns,
295
240
  allowedTools: config.allowedTools,
296
241
  agentTimeoutMs: config.agentTimeoutMs,
297
- llmProvider: isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude',
298
242
  model: config.model,
299
- lastModelsByProvider: normalizeLastModelsByProvider(config.lastModelsByProvider),
300
243
  ticketSource: config.ticketSource,
301
244
  jiraBaseUrl: config.jiraBaseUrl || '',
302
245
  jiraPrLinkPhrase: config.jiraPrLinkPhrase || 'Relates to',
@@ -336,11 +279,9 @@ export function loadConfig(repoRoot) {
336
279
  const initial = {
337
280
  ...DEFAULTS,
338
281
  allowedTools: [...KNOWN_TOOLS],
339
- lastModelsByProvider: {},
340
282
  jiraRules: structuredClone(DEFAULT_JIRA_RULES),
341
283
  githubRules: structuredClone(DEFAULT_GITHUB_RULES),
342
284
  };
343
- rememberModelForProvider(initial, 'claude', initial.model);
344
285
  fs.writeFileSync(configPath, JSON.stringify(persistable(initial), null, 2) + '\n', 'utf8');
345
286
  return initial;
346
287
  }
@@ -350,29 +291,34 @@ export function loadConfig(repoRoot) {
350
291
  ...DEFAULTS,
351
292
  ...raw,
352
293
  allowedTools: raw.allowedTools ?? [...KNOWN_TOOLS],
353
- lastModelsByProvider: normalizeLastModelsByProvider(raw.lastModelsByProvider),
354
294
  jiraRules: normalizeJiraRules(raw.jiraRules),
355
295
  githubRules: normalizeGithubRules(raw.githubRules),
356
296
  };
357
297
 
298
+ let needsSave = false;
299
+
358
300
  if (isNoModel(config.model)) {
359
301
  if (String(config.model).trim() !== NO_MODEL) {
360
302
  config.model = NO_MODEL;
361
- saveConfig(repoRoot, persistable(config));
303
+ needsSave = true;
362
304
  } else {
363
305
  config.model = NO_MODEL;
364
306
  }
365
307
  } else if (!isValidModelId(config.model)) {
366
- config.model = defaultModelForProvider(
367
- isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude'
368
- );
369
- saveConfig(repoRoot, persistable(config));
308
+ config.model = DEFAULT_MODEL;
309
+ needsSave = true;
370
310
  } else {
371
- config.model = String(config.model).trim();
311
+ const migrated = migrateLegacyModel(raw, config.model);
312
+ if (migrated !== String(config.model).trim()) {
313
+ config.model = migrated;
314
+ needsSave = true;
315
+ } else {
316
+ config.model = String(config.model).trim();
317
+ }
372
318
  }
373
319
 
374
- if (!isValidLlmProvider(config.llmProvider)) {
375
- config.llmProvider = 'claude';
320
+ if (raw.llmProvider === 'openrouter' || raw.lastModelsByProvider != null) {
321
+ needsSave = true;
376
322
  }
377
323
 
378
324
  if (!ALLOWED_TICKET_SOURCES.has(config.ticketSource)) {
@@ -387,10 +333,9 @@ export function loadConfig(repoRoot) {
387
333
  config.jiraPrLinkPhrase = 'Relates to';
388
334
  }
389
335
 
390
- const loadedProvider = isValidLlmProvider(config.llmProvider)
391
- ? config.llmProvider
392
- : 'claude';
393
- rememberModelForProvider(config, loadedProvider, config.model);
336
+ if (needsSave) {
337
+ saveConfig(repoRoot, persistable(config));
338
+ }
394
339
 
395
340
  return config;
396
341
  }
@@ -404,23 +349,6 @@ export function loadConfig(repoRoot) {
404
349
  * @returns {typeof DEFAULTS}
405
350
  */
406
351
  export function updateConfig(repoRoot, config, patch) {
407
- if (!config.lastModelsByProvider || typeof config.lastModelsByProvider !== 'object') {
408
- config.lastModelsByProvider = normalizeLastModelsByProvider(config.lastModelsByProvider);
409
- }
410
-
411
- const prevProvider = isValidLlmProvider(config.llmProvider)
412
- ? config.llmProvider
413
- : 'claude';
414
- let providerChanged = false;
415
- const willChangeProvider =
416
- patch.llmProvider !== undefined &&
417
- isValidLlmProvider(patch.llmProvider) &&
418
- patch.llmProvider !== prevProvider;
419
-
420
- if (willChangeProvider) {
421
- rememberModelForProvider(config, prevProvider, config.model);
422
- }
423
-
424
352
  if (patch.model != null) {
425
353
  if (isNoModel(patch.model)) {
426
354
  config.model = NO_MODEL;
@@ -428,33 +356,13 @@ export function updateConfig(repoRoot, config, patch) {
428
356
  throw new Error(
429
357
  `Invalid model "${patch.model}". Expected a non-empty model id or "${NO_MODEL}" for no selection`
430
358
  );
431
- } else {
432
- config.model = String(patch.model).trim();
433
- }
434
- }
435
-
436
- if (patch.llmProvider !== undefined) {
437
- if (!isValidLlmProvider(patch.llmProvider)) {
359
+ } else if (!isClaudeCatalogId(String(patch.model).trim())) {
438
360
  throw new Error(
439
- `Invalid llmProvider "${patch.llmProvider}". Allowed: ${LLM_PROVIDERS.join(', ')}`
361
+ `Invalid model "${patch.model}". Expected a Claude model id (no provider/ prefix)`
440
362
  );
363
+ } else {
364
+ config.model = String(patch.model).trim();
441
365
  }
442
- providerChanged = willChangeProvider;
443
- config.llmProvider = patch.llmProvider;
444
- }
445
-
446
- const currentProvider = isValidLlmProvider(config.llmProvider)
447
- ? config.llmProvider
448
- : 'claude';
449
-
450
- if (providerChanged) {
451
- config.model = resolveModelForProvider(config, currentProvider, config.model);
452
- }
453
-
454
- if (patch.model != null && !providerChanged) {
455
- rememberModelForProvider(config, currentProvider, config.model);
456
- } else if (providerChanged && !isNoModel(config.model)) {
457
- rememberModelForProvider(config, currentProvider, config.model);
458
366
  }
459
367
 
460
368
  if (patch.baseBranch !== undefined) {
@@ -545,18 +453,14 @@ export function updateConfig(repoRoot, config, patch) {
545
453
  * stubAgent?: boolean,
546
454
  * ghAuth?: import('./gh-auth.js').GhAuthResult,
547
455
  * claudeAuth?: import('./claude-auth.js').ClaudeAuthResult,
548
- * openrouterAuth?: import('./openrouter-auth.js').OpenRouterAuthResult,
549
456
  * }} [opts]
550
457
  */
551
458
  export function publicConfig(config, opts = {}) {
552
- const llmProvider = isValidLlmProvider(config.llmProvider)
553
- ? config.llmProvider
554
- : 'claude';
555
459
  const model = isNoModel(config.model)
556
460
  ? NO_MODEL
557
- : isValidModelId(config.model)
461
+ : isValidModelId(config.model) && isClaudeCatalogId(config.model)
558
462
  ? String(config.model).trim()
559
- : defaultModelForProvider(llmProvider);
463
+ : DEFAULT_MODEL;
560
464
  const repoName = opts.repoRoot ? path.basename(opts.repoRoot) : undefined;
561
465
  const ticketSource = ALLOWED_TICKET_SOURCES.has(config.ticketSource)
562
466
  ? config.ticketSource
@@ -581,19 +485,15 @@ export function publicConfig(config, opts = {}) {
581
485
 
582
486
  const ghAuth = opts.ghAuth ?? checkGhAuth();
583
487
  const claudeAuth = opts.claudeAuth ?? checkClaudeAuth();
584
- const openrouterAuth = opts.openrouterAuth ?? checkOpenRouterAuth();
585
488
  const origin = originRemoteInfo(opts.repoRoot);
586
489
  const ghTokenMask = maskSecret(githubTokenFromEnv());
587
490
  const anthropicMask = maskSecret(process.env.ANTHROPIC_API_KEY);
588
491
  const claudeOauthMask = maskSecret(process.env.CLAUDE_CODE_OAUTH_TOKEN);
589
- const openrouterMask = maskSecret(process.env.OPENROUTER_API_KEY);
590
492
 
591
- const curatedModels = curatedModelOptions(llmProvider);
493
+ const curatedModels = curatedModelOptions();
592
494
 
593
495
  return {
594
- llmProvider,
595
496
  model,
596
- lastModelsByProvider: normalizeLastModelsByProvider(config.lastModelsByProvider),
597
497
  baseBranch: config.baseBranch,
598
498
  testCommand: config.testCommand ?? null,
599
499
  maxAgentTurns: config.maxAgentTurns,
@@ -625,13 +525,7 @@ export function publicConfig(config, opts = {}) {
625
525
  anthropicApiKeyMasked: anthropicMask.masked,
626
526
  claudeOauthTokenSet: claudeOauthMask.set,
627
527
  claudeOauthTokenMasked: claudeOauthMask.masked,
628
- openrouterAuthOk: openrouterAuth.ok === true,
629
- openrouterApiKeySet: openrouterMask.set,
630
- openrouterApiKeyMasked: openrouterMask.masked,
631
- llmAuthOk:
632
- llmProvider === 'openrouter'
633
- ? openrouterAuth.ok === true
634
- : claudeAuth.ok === true,
528
+ llmAuthOk: claudeAuth.ok === true,
635
529
  stubAgent: opts.stubAgent === true,
636
530
  ...(repoName ? { repoName } : {}),
637
531
  };