acdev 1.0.5 → 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/.acdev/.env.example +3 -0
- package/README.md +25 -13
- package/bin/acdev.js +20 -10
- package/package.json +1 -1
- package/public/app.js +1166 -124
- package/public/index.html +154 -17
- package/public/styles.css +590 -33
- package/src/agent.js +127 -64
- package/src/config.js +181 -11
- package/src/models.js +296 -39
- package/src/openrouter-auth.js +37 -0
- package/src/server.js +77 -17
package/src/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ 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';
|
|
5
6
|
import { extractUsageFromResult } from './usage.js';
|
|
6
7
|
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
@@ -396,6 +397,66 @@ async function fetchIssueTitle(issueUrl, worktreePath) {
|
|
|
396
397
|
*/
|
|
397
398
|
export const DISALLOWED_AGENT_TOOLS = ['Task', 'TaskOutput', 'AskUserQuestion'];
|
|
398
399
|
|
|
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
|
+
|
|
399
460
|
/**
|
|
400
461
|
* Prefer the first successful SDK result. A known SDK bug can emit a follow-up
|
|
401
462
|
* `error_during_execution` right after success when async subagents finish.
|
|
@@ -422,76 +483,78 @@ export function mergeAgentResult(current, incoming) {
|
|
|
422
483
|
* }} params
|
|
423
484
|
*/
|
|
424
485
|
async function runAgentQuery({ prompt, worktreePath, config, onEvent, queryFn = query }) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
+
}
|
|
461
524
|
}
|
|
462
525
|
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
// Aborting after success can surface as an iterator/abort error.
|
|
528
|
+
if (resultMessage?.subtype === 'success') return;
|
|
529
|
+
throw err;
|
|
463
530
|
}
|
|
464
|
-
}
|
|
465
|
-
// Aborting after success can surface as an iterator/abort error.
|
|
466
|
-
if (resultMessage?.subtype === 'success') return;
|
|
467
|
-
throw err;
|
|
468
|
-
}
|
|
469
|
-
};
|
|
531
|
+
};
|
|
470
532
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
533
|
+
let timeoutId;
|
|
534
|
+
try {
|
|
535
|
+
await Promise.race([
|
|
536
|
+
runLoop(),
|
|
537
|
+
new Promise((_, reject) => {
|
|
538
|
+
timeoutId = setTimeout(() => {
|
|
539
|
+
abortController.abort();
|
|
540
|
+
reject(new Error(`Agent timed out after ${timeoutMs}ms`));
|
|
541
|
+
}, timeoutMs);
|
|
542
|
+
}),
|
|
543
|
+
]);
|
|
544
|
+
} finally {
|
|
545
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
546
|
+
}
|
|
485
547
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
+
}
|
|
490
552
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
553
|
+
const resultText = resultMessage.result ?? '';
|
|
554
|
+
const meta = extractPrMetadata(resultText);
|
|
555
|
+
const usage = extractUsageFromResult(resultMessage);
|
|
556
|
+
return { resultText, meta, usage };
|
|
557
|
+
});
|
|
495
558
|
}
|
|
496
559
|
|
|
497
560
|
function stubAgentResult(onEvent, title, body) {
|
package/src/config.js
CHANGED
|
@@ -6,12 +6,20 @@ import { checkGhAuth, githubTokenFromEnv, originRemoteInfo } from './gh-auth.js'
|
|
|
6
6
|
import { normalizeJiraBaseUrl } from './jira.js';
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_MODEL,
|
|
9
|
-
|
|
9
|
+
DEFAULT_OPENROUTER_MODEL,
|
|
10
|
+
LLM_PROVIDERS,
|
|
11
|
+
NO_MODEL,
|
|
10
12
|
isValidModelId,
|
|
13
|
+
isNoModel,
|
|
14
|
+
isValidLlmProvider,
|
|
15
|
+
defaultModelForProvider,
|
|
16
|
+
isModelIdForProvider,
|
|
17
|
+
curatedModelOptions,
|
|
11
18
|
} from './models.js';
|
|
19
|
+
import { checkOpenRouterAuth } from './openrouter-auth.js';
|
|
12
20
|
import { dataDir } from './paths.js';
|
|
13
21
|
|
|
14
|
-
export { DEFAULT_MODEL, MODEL_OPTIONS } from './models.js';
|
|
22
|
+
export { DEFAULT_MODEL, DEFAULT_OPENROUTER_MODEL, CLAUDE_MODEL_OPTIONS, MODEL_OPTIONS, OPENROUTER_MODEL_OPTIONS, LLM_PROVIDERS, NO_MODEL } from './models.js';
|
|
15
23
|
|
|
16
24
|
/** Tool names the agent may be granted via config. */
|
|
17
25
|
export const KNOWN_TOOLS = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
|
|
@@ -61,7 +69,10 @@ const DEFAULTS = {
|
|
|
61
69
|
maxAgentTurns: 30,
|
|
62
70
|
allowedTools: [...KNOWN_TOOLS],
|
|
63
71
|
agentTimeoutMs: 900_000,
|
|
72
|
+
llmProvider: /** @type {'claude' | 'openrouter'} */ ('claude'),
|
|
64
73
|
model: DEFAULT_MODEL,
|
|
74
|
+
/** Last valid model selection per LLM provider (for restore on provider switch). */
|
|
75
|
+
lastModelsByProvider: /** @type {Record<string, string>} */ ({}),
|
|
65
76
|
ticketSource: /** @type {'github' | 'jira'} */ ('github'),
|
|
66
77
|
jiraBaseUrl: '',
|
|
67
78
|
/** PR body phrase for Jira tickets, e.g. "Relates to PROJ-123". */
|
|
@@ -70,6 +81,79 @@ const DEFAULTS = {
|
|
|
70
81
|
githubRules: structuredClone(DEFAULT_GITHUB_RULES),
|
|
71
82
|
};
|
|
72
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @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]
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
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
|
+
) {
|
|
152
|
+
return stored.trim();
|
|
153
|
+
}
|
|
154
|
+
return NO_MODEL;
|
|
155
|
+
}
|
|
156
|
+
|
|
73
157
|
/**
|
|
74
158
|
* @param {string} prefix e.g. jiraRules / githubRules
|
|
75
159
|
* @param {unknown} raw
|
|
@@ -210,7 +294,9 @@ function persistable(config) {
|
|
|
210
294
|
maxAgentTurns: config.maxAgentTurns,
|
|
211
295
|
allowedTools: config.allowedTools,
|
|
212
296
|
agentTimeoutMs: config.agentTimeoutMs,
|
|
297
|
+
llmProvider: isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude',
|
|
213
298
|
model: config.model,
|
|
299
|
+
lastModelsByProvider: normalizeLastModelsByProvider(config.lastModelsByProvider),
|
|
214
300
|
ticketSource: config.ticketSource,
|
|
215
301
|
jiraBaseUrl: config.jiraBaseUrl || '',
|
|
216
302
|
jiraPrLinkPhrase: config.jiraPrLinkPhrase || 'Relates to',
|
|
@@ -250,9 +336,11 @@ export function loadConfig(repoRoot) {
|
|
|
250
336
|
const initial = {
|
|
251
337
|
...DEFAULTS,
|
|
252
338
|
allowedTools: [...KNOWN_TOOLS],
|
|
339
|
+
lastModelsByProvider: {},
|
|
253
340
|
jiraRules: structuredClone(DEFAULT_JIRA_RULES),
|
|
254
341
|
githubRules: structuredClone(DEFAULT_GITHUB_RULES),
|
|
255
342
|
};
|
|
343
|
+
rememberModelForProvider(initial, 'claude', initial.model);
|
|
256
344
|
fs.writeFileSync(configPath, JSON.stringify(persistable(initial), null, 2) + '\n', 'utf8');
|
|
257
345
|
return initial;
|
|
258
346
|
}
|
|
@@ -262,17 +350,31 @@ export function loadConfig(repoRoot) {
|
|
|
262
350
|
...DEFAULTS,
|
|
263
351
|
...raw,
|
|
264
352
|
allowedTools: raw.allowedTools ?? [...KNOWN_TOOLS],
|
|
353
|
+
lastModelsByProvider: normalizeLastModelsByProvider(raw.lastModelsByProvider),
|
|
265
354
|
jiraRules: normalizeJiraRules(raw.jiraRules),
|
|
266
355
|
githubRules: normalizeGithubRules(raw.githubRules),
|
|
267
356
|
};
|
|
268
357
|
|
|
269
|
-
if (
|
|
270
|
-
config.model
|
|
358
|
+
if (isNoModel(config.model)) {
|
|
359
|
+
if (String(config.model).trim() !== NO_MODEL) {
|
|
360
|
+
config.model = NO_MODEL;
|
|
361
|
+
saveConfig(repoRoot, persistable(config));
|
|
362
|
+
} else {
|
|
363
|
+
config.model = NO_MODEL;
|
|
364
|
+
}
|
|
365
|
+
} else if (!isValidModelId(config.model)) {
|
|
366
|
+
config.model = defaultModelForProvider(
|
|
367
|
+
isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude'
|
|
368
|
+
);
|
|
271
369
|
saveConfig(repoRoot, persistable(config));
|
|
272
370
|
} else {
|
|
273
371
|
config.model = String(config.model).trim();
|
|
274
372
|
}
|
|
275
373
|
|
|
374
|
+
if (!isValidLlmProvider(config.llmProvider)) {
|
|
375
|
+
config.llmProvider = 'claude';
|
|
376
|
+
}
|
|
377
|
+
|
|
276
378
|
if (!ALLOWED_TICKET_SOURCES.has(config.ticketSource)) {
|
|
277
379
|
config.ticketSource = 'github';
|
|
278
380
|
}
|
|
@@ -285,6 +387,11 @@ export function loadConfig(repoRoot) {
|
|
|
285
387
|
config.jiraPrLinkPhrase = 'Relates to';
|
|
286
388
|
}
|
|
287
389
|
|
|
390
|
+
const loadedProvider = isValidLlmProvider(config.llmProvider)
|
|
391
|
+
? config.llmProvider
|
|
392
|
+
: 'claude';
|
|
393
|
+
rememberModelForProvider(config, loadedProvider, config.model);
|
|
394
|
+
|
|
288
395
|
return config;
|
|
289
396
|
}
|
|
290
397
|
|
|
@@ -297,13 +404,57 @@ export function loadConfig(repoRoot) {
|
|
|
297
404
|
* @returns {typeof DEFAULTS}
|
|
298
405
|
*/
|
|
299
406
|
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
|
+
|
|
300
424
|
if (patch.model != null) {
|
|
301
|
-
if (
|
|
425
|
+
if (isNoModel(patch.model)) {
|
|
426
|
+
config.model = NO_MODEL;
|
|
427
|
+
} else if (!isValidModelId(patch.model)) {
|
|
302
428
|
throw new Error(
|
|
303
|
-
`Invalid model "${patch.model}". Expected a non-empty
|
|
429
|
+
`Invalid model "${patch.model}". Expected a non-empty model id or "${NO_MODEL}" for no selection`
|
|
430
|
+
);
|
|
431
|
+
} else {
|
|
432
|
+
config.model = String(patch.model).trim();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (patch.llmProvider !== undefined) {
|
|
437
|
+
if (!isValidLlmProvider(patch.llmProvider)) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Invalid llmProvider "${patch.llmProvider}". Allowed: ${LLM_PROVIDERS.join(', ')}`
|
|
304
440
|
);
|
|
305
441
|
}
|
|
306
|
-
|
|
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);
|
|
307
458
|
}
|
|
308
459
|
|
|
309
460
|
if (patch.baseBranch !== undefined) {
|
|
@@ -394,12 +545,18 @@ export function updateConfig(repoRoot, config, patch) {
|
|
|
394
545
|
* stubAgent?: boolean,
|
|
395
546
|
* ghAuth?: import('./gh-auth.js').GhAuthResult,
|
|
396
547
|
* claudeAuth?: import('./claude-auth.js').ClaudeAuthResult,
|
|
548
|
+
* openrouterAuth?: import('./openrouter-auth.js').OpenRouterAuthResult,
|
|
397
549
|
* }} [opts]
|
|
398
550
|
*/
|
|
399
551
|
export function publicConfig(config, opts = {}) {
|
|
400
|
-
const
|
|
401
|
-
?
|
|
402
|
-
:
|
|
552
|
+
const llmProvider = isValidLlmProvider(config.llmProvider)
|
|
553
|
+
? config.llmProvider
|
|
554
|
+
: 'claude';
|
|
555
|
+
const model = isNoModel(config.model)
|
|
556
|
+
? NO_MODEL
|
|
557
|
+
: isValidModelId(config.model)
|
|
558
|
+
? String(config.model).trim()
|
|
559
|
+
: defaultModelForProvider(llmProvider);
|
|
403
560
|
const repoName = opts.repoRoot ? path.basename(opts.repoRoot) : undefined;
|
|
404
561
|
const ticketSource = ALLOWED_TICKET_SOURCES.has(config.ticketSource)
|
|
405
562
|
? config.ticketSource
|
|
@@ -424,13 +581,19 @@ export function publicConfig(config, opts = {}) {
|
|
|
424
581
|
|
|
425
582
|
const ghAuth = opts.ghAuth ?? checkGhAuth();
|
|
426
583
|
const claudeAuth = opts.claudeAuth ?? checkClaudeAuth();
|
|
584
|
+
const openrouterAuth = opts.openrouterAuth ?? checkOpenRouterAuth();
|
|
427
585
|
const origin = originRemoteInfo(opts.repoRoot);
|
|
428
586
|
const ghTokenMask = maskSecret(githubTokenFromEnv());
|
|
429
587
|
const anthropicMask = maskSecret(process.env.ANTHROPIC_API_KEY);
|
|
430
588
|
const claudeOauthMask = maskSecret(process.env.CLAUDE_CODE_OAUTH_TOKEN);
|
|
589
|
+
const openrouterMask = maskSecret(process.env.OPENROUTER_API_KEY);
|
|
590
|
+
|
|
591
|
+
const curatedModels = curatedModelOptions(llmProvider);
|
|
431
592
|
|
|
432
593
|
return {
|
|
594
|
+
llmProvider,
|
|
433
595
|
model,
|
|
596
|
+
lastModelsByProvider: normalizeLastModelsByProvider(config.lastModelsByProvider),
|
|
434
597
|
baseBranch: config.baseBranch,
|
|
435
598
|
testCommand: config.testCommand ?? null,
|
|
436
599
|
maxAgentTurns: config.maxAgentTurns,
|
|
@@ -438,7 +601,7 @@ export function publicConfig(config, opts = {}) {
|
|
|
438
601
|
allowedTools: Array.isArray(config.allowedTools)
|
|
439
602
|
? [...config.allowedTools]
|
|
440
603
|
: [...KNOWN_TOOLS],
|
|
441
|
-
models:
|
|
604
|
+
models: curatedModels,
|
|
442
605
|
knownTools: KNOWN_TOOLS,
|
|
443
606
|
ticketSource,
|
|
444
607
|
jiraBaseUrl,
|
|
@@ -462,6 +625,13 @@ export function publicConfig(config, opts = {}) {
|
|
|
462
625
|
anthropicApiKeyMasked: anthropicMask.masked,
|
|
463
626
|
claudeOauthTokenSet: claudeOauthMask.set,
|
|
464
627
|
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,
|
|
465
635
|
stubAgent: opts.stubAgent === true,
|
|
466
636
|
...(repoName ? { repoName } : {}),
|
|
467
637
|
};
|