@doist/doistbot-cli 1.0.4 → 1.0.5

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.
Files changed (62) hide show
  1. package/dist/actions/auth.js +23 -23
  2. package/dist/actions/doctor.js +3 -5
  3. package/dist/actions/review.js +5 -4
  4. package/dist/auth.js +17 -17
  5. package/dist/config.js +14 -9
  6. package/dist/env.js +1 -2
  7. package/dist/file-store.js +1 -1
  8. package/dist/terminal.js +0 -7
  9. package/package.json +5 -5
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
  12. package/sandbox/dist/core/bot-logins.js +0 -13
  13. package/sandbox/dist/core/datadog-metrics.js +0 -2
  14. package/sandbox/dist/core/pi.js +16 -13
  15. package/sandbox/dist/providers/config.js +1 -1
  16. package/sandbox/dist/providers/credentials.js +30 -0
  17. package/sandbox/dist/providers/gemini.js +4 -10
  18. package/sandbox/dist/providers/helpers.js +6 -21
  19. package/sandbox/dist/providers/index.js +0 -3
  20. package/sandbox/dist/providers/openai.js +1 -8
  21. package/sandbox/dist/providers/pi-review.js +16 -54
  22. package/sandbox/dist/providers/schemas.js +0 -39
  23. package/sandbox/dist/tasks/chat/chat.js +2 -1
  24. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  25. package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
  26. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  27. package/sandbox/dist/tasks/issue-triage/context.js +1 -2
  28. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
  29. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  30. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  31. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  32. package/sandbox/dist/tasks/review/conversation-context.js +13 -7
  33. package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
  34. package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
  35. package/sandbox/dist/tasks/review/engines/shared.js +11 -59
  36. package/sandbox/dist/tasks/review/local.js +4 -29
  37. package/sandbox/dist/tasks/review/prompt.js +2 -49
  38. package/sandbox/dist/tasks/review/review-summary.js +82 -0
  39. package/sandbox/dist/tasks/review/review.js +23 -57
  40. package/sandbox/dist/tasks/review/runner.js +35 -51
  41. package/sandbox/dist/tasks/review/summary-model.js +11 -2
  42. package/sandbox/dist/tasks/review/thinking-level.js +0 -2
  43. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  44. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
  45. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  46. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  47. package/sandbox/dist/providers/anthropic.js +0 -30
  48. package/sandbox/dist/tasks/review/author-format.js +0 -14
  49. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  50. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  51. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  52. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  53. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  54. package/sandbox/dist/tasks/review/council/types.js +0 -4
  55. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  56. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  57. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  58. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  59. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  60. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  61. package/sandbox/review-prompt.md +0 -209
  62. package/sandbox/vote-prompt.md +0 -69
@@ -2,29 +2,23 @@
2
2
  * Gemini provider for the multi-model code review system.
3
3
  * Uses the Pi harness instead of the Gemini CLI.
4
4
  */
5
- import { DEFAULT_PI_MODELS } from '../core/pi.js';
5
+ import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
6
6
  import { resolveProviderTimeoutMs } from './config.js';
7
7
  import { extractJsonFromResponse } from './helpers.js';
8
8
  import { runPiReviewProvider } from './pi-review.js';
9
9
  export class GeminiProvider {
10
10
  name = 'gemini';
11
- async review(prompt, config, logOptions) {
11
+ async review(prompt, config) {
12
+ const modelName = config.modelName ?? DEFAULT_OPENROUTER_MODELS.gemini ?? DEFAULT_PI_MODELS.gemini;
12
13
  return await runPiReviewProvider({
13
14
  provider: this.name,
14
- modelName: config.modelName ?? DEFAULT_PI_MODELS.gemini,
15
+ modelName,
15
16
  prompt,
16
17
  config: {
17
18
  ...config,
18
19
  timeout: resolveProviderTimeoutMs(config.timeout),
19
20
  },
20
- prompts: config.jsonSchema
21
- ? [
22
- prompt,
23
- `${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
24
- ]
25
- : [prompt],
26
21
  parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
27
- logOptions,
28
22
  });
29
23
  }
30
24
  }
@@ -3,8 +3,9 @@
3
3
  */
4
4
  import { randomUUID } from 'crypto';
5
5
  import { logger } from '../core/logging.js';
6
- import { isBatchVoteResponse, isRawReviewResult, isVoteResponse } from './schemas.js';
6
+ import { isRawReviewResult } from './schemas.js';
7
7
  const RAW_MODEL_LOG_CHUNK_CHARS = 8000;
8
+ const REVIEW_LOG_ENGINE = 'multi_focus';
8
9
  function redactSensitiveStrings(content) {
9
10
  return content
10
11
  .replace(/(sk-[A-Za-z0-9_-]{10,})/g, '[REDACTED_OPENAI_KEY]')
@@ -34,7 +35,7 @@ export function buildRawModelOutputEntries(content) {
34
35
  originalLength: content.length,
35
36
  }));
36
37
  }
37
- export function logRawModelOutput({ model, engine = 'council', stage, success, status, durationMs, stdout, stderr, }) {
38
+ export function logRawModelOutput({ model, stage, success, status, durationMs, stdout, stderr, }) {
38
39
  const streams = [
39
40
  { name: 'stdout', value: stdout },
40
41
  { name: 'stderr', value: stderr },
@@ -48,7 +49,7 @@ export function logRawModelOutput({ model, engine = 'council', stage, success, s
48
49
  for (const entry of entries) {
49
50
  logger.info('Review model raw output', {
50
51
  model,
51
- engine,
52
+ engine: REVIEW_LOG_ENGINE,
52
53
  stage,
53
54
  success,
54
55
  status: status ?? null,
@@ -95,30 +96,14 @@ export function parseRawFindings(comments, modelName) {
95
96
  /**
96
97
  * Normalizes structured model output into a single provider response shape.
97
98
  */
98
- export function normalizeParsedModelOutput(parsed, modelName, structuredOutputKind = 'auto') {
99
+ export function normalizeParsedModelOutput(parsed, modelName) {
99
100
  if (isRawReviewResult(parsed)) {
100
101
  return {
101
102
  summary: parsed.summary,
102
103
  findings: parseRawFindings(parsed.comments, modelName),
103
104
  };
104
105
  }
105
- if ((structuredOutputKind === 'auto' || structuredOutputKind === 'vote') &&
106
- isVoteResponse(parsed)) {
107
- return {
108
- summary: '',
109
- findings: [],
110
- structuredOutput: parsed,
111
- };
112
- }
113
- if ((structuredOutputKind === 'auto' || structuredOutputKind === 'batch-vote') &&
114
- isBatchVoteResponse(parsed)) {
115
- return {
116
- summary: '',
117
- findings: [],
118
- structuredOutput: parsed,
119
- };
120
- }
121
- throw new Error('Model output did not match review or vote schema');
106
+ throw new Error('Model output did not match review schema');
122
107
  }
123
108
  function extractFencedJsonCandidates(content) {
124
109
  const matches = content.match(/```json\s*([\s\S]*?)\s*```/gi);
@@ -1,17 +1,14 @@
1
1
  /**
2
2
  * Provider factory and exports for the multi-model code review system.
3
3
  */
4
- import { AnthropicProvider } from './anthropic.js';
5
4
  import { GeminiProvider } from './gemini.js';
6
5
  import { OpenAIProvider } from './openai.js';
7
- export { AnthropicProvider } from './anthropic.js';
8
6
  export { GeminiProvider } from './gemini.js';
9
7
  export { OpenAIProvider } from './openai.js';
10
8
  export * from './types.js';
11
9
  const providers = {
12
10
  gemini: new GeminiProvider(),
13
11
  openai: new OpenAIProvider(),
14
- anthropic: new AnthropicProvider(),
15
12
  };
16
13
  /**
17
14
  * Gets a provider instance by name.
@@ -8,7 +8,7 @@ import { extractJsonFromResponse } from './helpers.js';
8
8
  import { runPiReviewProvider } from './pi-review.js';
9
9
  export class OpenAIProvider {
10
10
  name = 'openai';
11
- async review(prompt, config, logOptions) {
11
+ async review(prompt, config) {
12
12
  return await runPiReviewProvider({
13
13
  provider: this.name,
14
14
  modelName: config.modelName ?? DEFAULT_PI_MODELS.openai,
@@ -17,14 +17,7 @@ export class OpenAIProvider {
17
17
  ...config,
18
18
  timeout: resolveProviderTimeoutMs(config.timeout),
19
19
  },
20
- prompts: config.jsonSchema
21
- ? [
22
- prompt,
23
- `${prompt}\n\nReturn only JSON that matches the provided schema. Do not include analysis text.`,
24
- ]
25
- : [prompt],
26
20
  parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
27
- logOptions,
28
21
  });
29
22
  }
30
23
  }
@@ -3,20 +3,10 @@ import { createPiInvoker } from '../core/pi.js';
3
3
  import { logPiProgressEvent } from '../core/pi-progress-logging.js';
4
4
  import { WORKSPACE_DIR } from '../core/shared.js';
5
5
  import { logRawModelOutput, normalizeParsedModelOutput } from './helpers.js';
6
- import { BATCH_VOTE_SCHEMA, VOTE_SCHEMA } from './schemas.js';
7
6
  const VERBOSE_PI_PROGRESS_LOGS = process.env.DOISTBOT_VERBOSE_PI_PROGRESS === 'true';
8
7
  const VERBOSE_PI_RAW_OUTPUT_LOGS = process.env.DOISTBOT_VERBOSE_PI_RAW_OUTPUT === 'true';
8
+ const REVIEW_LOG_ENGINE = 'multi_focus';
9
9
  const REVIEW_PI_SYSTEM_PROMPT = 'You are running inside doistbot in a non-interactive environment. Stay tightly focused on the review task in the user prompt. Read only the files that are directly relevant to the changed code, the explicitly named guidance files, and any immediately connected definitions needed to verify a concrete finding. Do not broadly explore the repository, inspect unrelated modules, or search history unless it is necessary to validate a specific review concern. Stop gathering context once you have enough evidence to answer the review task accurately.';
10
- const DEFAULT_REVIEW_LOG_ENGINE = 'council';
11
- function getStructuredOutputKind(jsonSchema) {
12
- if (jsonSchema === BATCH_VOTE_SCHEMA) {
13
- return 'batch-vote';
14
- }
15
- if (jsonSchema === VOTE_SCHEMA) {
16
- return 'vote';
17
- }
18
- return 'auto';
19
- }
20
10
  function buildModelFailureResult({ model, error, durationMs, timedOut = false, }) {
21
11
  return {
22
12
  model,
@@ -27,7 +17,7 @@ function buildModelFailureResult({ model, error, durationMs, timedOut = false, }
27
17
  durationMs,
28
18
  };
29
19
  }
30
- function logReviewModelProgress(model, event, engine) {
20
+ function logReviewModelProgress(model, event) {
31
21
  logPiProgressEvent({
32
22
  event,
33
23
  messages: {
@@ -39,17 +29,17 @@ function logReviewModelProgress(model, event, engine) {
39
29
  toolUpdate: 'Review model tool still running',
40
30
  toolEnd: 'Review model tool finished',
41
31
  },
42
- meta: { model, engine },
32
+ meta: { model, engine: REVIEW_LOG_ENGINE },
43
33
  logResponding: VERBOSE_PI_PROGRESS_LOGS,
44
34
  logTools: VERBOSE_PI_PROGRESS_LOGS,
45
35
  });
46
36
  }
47
- function emitReviewModelProgress(provider, event, config, engine) {
37
+ function emitReviewModelProgress(provider, event, config) {
48
38
  config.onProgress?.({
49
39
  ...event,
50
40
  provider,
51
41
  });
52
- logReviewModelProgress(provider, event, engine);
42
+ logReviewModelProgress(provider, event);
53
43
  }
54
44
  function logReviewRawModelOutput(args) {
55
45
  if (!VERBOSE_PI_RAW_OUTPUT_LOGS) {
@@ -57,16 +47,15 @@ function logReviewRawModelOutput(args) {
57
47
  }
58
48
  logRawModelOutput(args);
59
49
  }
60
- export async function runPiReviewProvider({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, logOptions, }) {
50
+ export async function runPiReviewProvider({ provider, modelName, prompt, config, prompts = [prompt], parseOutput, }) {
61
51
  const startTime = Date.now();
62
52
  const timeoutMs = config.timeout ?? 0;
63
- const engine = logOptions?.engine ?? DEFAULT_REVIEW_LOG_ENGINE;
64
53
  let stdout = '';
65
54
  let stderr = '';
66
55
  let status = null;
67
56
  logger.info('Review model request', {
68
57
  model: provider,
69
- engine,
58
+ engine: REVIEW_LOG_ENGINE,
70
59
  promptLength: prompt.length,
71
60
  modelName,
72
61
  timeoutMs,
@@ -75,9 +64,10 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
75
64
  const invoker = createPiInvoker({
76
65
  provider,
77
66
  apiKey: config.apiKey,
67
+ piProviderId: config.piProviderId,
78
68
  cwd: config.workspaceDir ?? WORKSPACE_DIR,
79
69
  onProgress: (event) => {
80
- emitReviewModelProgress(provider, event, config, engine);
70
+ emitReviewModelProgress(provider, event, config);
81
71
  },
82
72
  capabilityPreset: 'read-only',
83
73
  thinkingLevel: config.thinkingLevel,
@@ -95,7 +85,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
95
85
  status = result.ok ? 0 : null;
96
86
  logger.info('Review model tool usage', {
97
87
  model: provider,
98
- engine,
88
+ engine: REVIEW_LOG_ENGINE,
99
89
  attempt: attempt + 1,
100
90
  attemptsTotal: prompts.length,
101
91
  attemptDurationMs: result.durationMs,
@@ -104,7 +94,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
104
94
  if (!result.ok && result.timedOut) {
105
95
  logReviewRawModelOutput({
106
96
  model: provider,
107
- engine,
108
97
  stage: 'timeout',
109
98
  success: false,
110
99
  status,
@@ -115,7 +104,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
115
104
  const error = result.error || `Pi ${provider} request timed out after ${timeoutMs}ms`;
116
105
  logger.error('Review model failed', {
117
106
  model: provider,
118
- engine,
107
+ engine: REVIEW_LOG_ENGINE,
119
108
  error,
120
109
  durationMs,
121
110
  });
@@ -129,7 +118,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
129
118
  if (!result.ok) {
130
119
  logReviewRawModelOutput({
131
120
  model: provider,
132
- engine,
133
121
  stage: 'non-zero-exit',
134
122
  success: false,
135
123
  status,
@@ -140,7 +128,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
140
128
  const error = `Pi ${provider} request failed: ${result.error.slice(0, 500)}`;
141
129
  logger.error('Review model failed', {
142
130
  model: provider,
143
- engine,
131
+ engine: REVIEW_LOG_ENGINE,
144
132
  error,
145
133
  durationMs,
146
134
  });
@@ -154,7 +142,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
154
142
  if (!output.trim()) {
155
143
  logReviewRawModelOutput({
156
144
  model: provider,
157
- engine,
158
145
  stage: 'empty-output',
159
146
  success: false,
160
147
  status,
@@ -169,10 +156,10 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
169
156
  });
170
157
  }
171
158
  try {
172
- const normalized = normalizeParsedModelOutput(parseOutput(output), provider, getStructuredOutputKind(config.jsonSchema));
159
+ const normalized = normalizeParsedModelOutput(parseOutput(output), provider);
173
160
  logger.info('Review model response', {
174
161
  model: provider,
175
- engine,
162
+ engine: REVIEW_LOG_ENGINE,
176
163
  findingsCount: normalized.findings.length,
177
164
  durationMs,
178
165
  parseMethod: 'extract_json',
@@ -180,13 +167,12 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
180
167
  if (VERBOSE_PI_RAW_OUTPUT_LOGS) {
181
168
  logger.info('Review model findings', {
182
169
  model: provider,
183
- engine,
170
+ engine: REVIEW_LOG_ENGINE,
184
171
  findings: normalized.findings,
185
172
  });
186
173
  }
187
174
  logReviewRawModelOutput({
188
175
  model: provider,
189
- engine,
190
176
  stage: 'success',
191
177
  success: true,
192
178
  status,
@@ -198,35 +184,12 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
198
184
  model: provider,
199
185
  summary: normalized.summary,
200
186
  findings: normalized.findings,
201
- structuredOutput: normalized.structuredOutput,
202
187
  durationMs,
203
188
  };
204
189
  }
205
190
  catch (error) {
206
- if (config.jsonSchema && attempt < prompts.length - 1) {
207
- const retryError = error instanceof Error ? error.message : String(error);
208
- logReviewRawModelOutput({
209
- model: provider,
210
- engine,
211
- stage: 'schema-retry',
212
- success: false,
213
- status,
214
- durationMs,
215
- stdout,
216
- stderr,
217
- });
218
- logger.warn('Review model schema retry', {
219
- model: provider,
220
- engine,
221
- durationMs,
222
- attempt: attempt + 1,
223
- error: retryError,
224
- });
225
- continue;
226
- }
227
191
  logReviewRawModelOutput({
228
192
  model: provider,
229
- engine,
230
193
  stage: 'parse-error',
231
194
  success: false,
232
195
  status,
@@ -248,7 +211,6 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
248
211
  const errorMessage = error instanceof Error ? error.message : String(error);
249
212
  logReviewRawModelOutput({
250
213
  model: provider,
251
- engine,
252
214
  stage: 'exception',
253
215
  success: false,
254
216
  status,
@@ -258,7 +220,7 @@ export async function runPiReviewProvider({ provider, modelName, prompt, config,
258
220
  });
259
221
  logger.error('Review model failed', {
260
222
  model: provider,
261
- engine,
223
+ engine: REVIEW_LOG_ENGINE,
262
224
  error: errorMessage,
263
225
  durationMs,
264
226
  });
@@ -21,37 +21,6 @@ export const REVIEW_SCHEMA = {
21
21
  },
22
22
  required: ['summary', 'comments'],
23
23
  };
24
- export const VOTE_SCHEMA = {
25
- type: 'object',
26
- additionalProperties: false,
27
- properties: {
28
- approve: { type: 'boolean' },
29
- confidence: { type: 'number', minimum: 0, maximum: 1 },
30
- reasoning: { type: 'string', minLength: 1 },
31
- },
32
- required: ['approve', 'confidence', 'reasoning'],
33
- };
34
- export const BATCH_VOTE_SCHEMA = {
35
- type: 'object',
36
- additionalProperties: false,
37
- properties: {
38
- votes: {
39
- type: 'array',
40
- items: {
41
- type: 'object',
42
- additionalProperties: false,
43
- properties: {
44
- findingId: { type: 'string', minLength: 1 },
45
- approve: { type: 'boolean' },
46
- confidence: { type: 'number', minimum: 0, maximum: 1 },
47
- reasoning: { type: 'string', minLength: 1 },
48
- },
49
- required: ['findingId', 'approve', 'confidence', 'reasoning'],
50
- },
51
- },
52
- },
53
- required: ['votes'],
54
- };
55
24
  export const DEDUPE_SCHEMA = {
56
25
  type: 'object',
57
26
  additionalProperties: false,
@@ -76,18 +45,10 @@ export const DEDUPE_SCHEMA = {
76
45
  };
77
46
  const ajv = new Ajv();
78
47
  const validateRawReviewResult = ajv.compile(REVIEW_SCHEMA);
79
- const validateVoteResponse = ajv.compile(VOTE_SCHEMA);
80
- const validateBatchVoteResponse = ajv.compile(BATCH_VOTE_SCHEMA);
81
48
  const validateDedupeResponse = ajv.compile(DEDUPE_SCHEMA);
82
49
  export function isRawReviewResult(value) {
83
50
  return validateRawReviewResult(value);
84
51
  }
85
- export function isVoteResponse(value) {
86
- return validateVoteResponse(value);
87
- }
88
- export function isBatchVoteResponse(value) {
89
- return validateBatchVoteResponse(value);
90
- }
91
52
  export function isDedupeResponse(value) {
92
53
  return validateDedupeResponse(value);
93
54
  }
@@ -120,7 +120,7 @@ async function gatherIssueCommentContext(octokit, owner, repo, prNumber, comment
120
120
  };
121
121
  }
122
122
  export async function invokeChatModel(prompt) {
123
- const apiKey = process.env.GEMINI_API_KEY?.trim();
123
+ const apiKey = process.env.OPENROUTER_API_KEY?.trim();
124
124
  if (!apiKey) {
125
125
  logger.warn('Chat model API key missing');
126
126
  return null;
@@ -128,6 +128,7 @@ export async function invokeChatModel(prompt) {
128
128
  const result = await invokePiPrompt({
129
129
  provider: 'gemini',
130
130
  apiKey,
131
+ piProviderId: 'openrouter',
131
132
  prompt,
132
133
  cwd: WORKSPACE_DIR,
133
134
  timeoutMs: CHAT_TIMEOUT_MS,
@@ -11,7 +11,7 @@ export function parseSummarizeContext(env = process.env) {
11
11
  appId: requiredEnv('APP_ID', env),
12
12
  privateKey: requiredEnv('PRIVATE_KEY', env),
13
13
  githubToken: requiredEnv('GITHUB_TOKEN', env),
14
- geminiApiKey: requiredEnv('GEMINI_API_KEY', env),
14
+ openRouterApiKey: requiredEnv('OPENROUTER_API_KEY', env),
15
15
  commentId,
16
16
  reactionId,
17
17
  deliveryId: env.DELIVERY_ID,
@@ -1,10 +1,12 @@
1
1
  import { logger } from '../../core/logging.js';
2
2
  import { buildPiImageContent, createPiInvoker } from '../../core/pi.js';
3
3
  const ISSUE_SUMMARIZE_TIMEOUT_MS = 5 * 60 * 1000;
4
+ const ISSUE_SUMMARIZE_MULTIMODAL_MODEL = 'google/gemini-2.5-pro';
4
5
  export function createIssueSummarizeModel(apiKey) {
5
6
  return createPiInvoker({
6
7
  provider: 'gemini',
7
8
  apiKey,
9
+ piProviderId: 'openrouter',
8
10
  cwd: process.cwd(),
9
11
  capabilityPreset: 'read-only',
10
12
  });
@@ -43,7 +45,7 @@ export async function invokeIssueSummarizeModel({ model, prompt, mediaImages, })
43
45
  .join('\n\n');
44
46
  try {
45
47
  const result = await model.invoke({
46
- modelName: 'gemini-2.5-pro',
48
+ modelName: ISSUE_SUMMARIZE_MULTIMODAL_MODEL,
47
49
  prompt: `${prompt}\n\n<media_context>\n${mediaContext}\n</media_context>`,
48
50
  timeoutMs: ISSUE_SUMMARIZE_TIMEOUT_MS,
49
51
  images: mediaImages.map((mediaImage) => buildPiImageContent({
@@ -39,7 +39,7 @@ function summarizeMediaInspectionStats(mediaInspections) {
39
39
  async function summarizeIssue({ ctx, issue, entries, mediaReferences, mediaInspections, }) {
40
40
  const template = loadPromptTemplate();
41
41
  const threadPrompt = prepareThreadPrompt(entries);
42
- const summarizeModel = createIssueSummarizeModel(ctx.geminiApiKey);
42
+ const summarizeModel = createIssueSummarizeModel(ctx.openRouterApiKey);
43
43
  logger.info('Prepared summarize thread prompt', {
44
44
  entryCount: entries.length,
45
45
  chunked: threadPrompt.chunked,
@@ -35,7 +35,6 @@ export function parseIssueTriageContext(env = process.env) {
35
35
  dryRun: env.DRY_RUN?.toLowerCase() === 'true',
36
36
  triggerType,
37
37
  enableAutoFixOverride: parseOptionalBoolean(env.ENABLE_AUTO_FIX),
38
- geminiApiKey: env.GEMINI_API_KEY?.trim() || undefined,
39
- anthropicApiKey: env.ANTHROPIC_API_KEY?.trim() || undefined,
38
+ openRouterApiKey: env.OPENROUTER_API_KEY?.trim() || undefined,
40
39
  };
41
40
  }
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../../core/logging.js';
2
+ import { createOpenRouterCredential } from '../../providers/credentials.js';
2
3
  import { DefaultLoopEffects } from './fix-loop/effects.js';
3
4
  import { runFixLoop } from './fix-loop/runners.js';
4
5
  import { attemptFix, buildFixAssessmentSkipReason, shouldAttemptFix } from './fix-attempt.js';
@@ -132,9 +133,8 @@ export async function runConfiguredFix(input) {
132
133
  pushToken: input.ctx.githubToken,
133
134
  apiKeys: {
134
135
  openai: input.ctx.openaiApiKey,
135
- ...(input.ctx.geminiApiKey !== undefined && { gemini: input.ctx.geminiApiKey }),
136
- ...(input.ctx.anthropicApiKey !== undefined && {
137
- anthropic: input.ctx.anthropicApiKey,
136
+ ...(input.ctx.openRouterApiKey !== undefined && {
137
+ gemini: createOpenRouterCredential(input.ctx.openRouterApiKey),
138
138
  }),
139
139
  },
140
140
  };
@@ -7,8 +7,8 @@ const DD_HOST = 'api.datadoghq.com';
7
7
  * Starts a localhost HTTP proxy that forwards requests to the Datadog API,
8
8
  * scrubs PII from responses, and logs each query for auditability.
9
9
  *
10
- * Claude hits http://localhost:{port}/api/v2/... instead of the DD API directly.
11
- * The proxy injects DD auth headers so Claude never sees the API keys.
10
+ * The agent hits http://localhost:{port}/api/v2/... instead of the DD API directly.
11
+ * The proxy injects DD auth headers so the agent never sees the API keys.
12
12
  */
13
13
  export function startDdProxy({ port, datadogApiKey, datadogAppKey }) {
14
14
  return new Promise((resolve, reject) => {
@@ -49,7 +49,7 @@ export async function runInvestigation(params) {
49
49
  });
50
50
  // Ensure output directory exists
51
51
  fs.mkdirSync(params.outputDir, { recursive: true });
52
- // Start the DD proxy — Claude queries localhost, proxy injects auth and scrubs PII from responses
52
+ // Start the DD proxy so the agent queries localhost; the proxy injects auth and scrubs PII.
53
53
  const proxy = await startDdProxy({
54
54
  port: DD_PROXY_PORT,
55
55
  datadogApiKey: params.datadogApiKey,
@@ -441,7 +441,7 @@ export function buildSnapshotComment({ analysisContent, csvContent, worklogConte
441
441
  '_Links expire in 7 days. Use `@doistbot /refresh-s3-urls` to generate new links._',
442
442
  ].join('\n');
443
443
  const csvPreview = buildCsvPreview(csvContent);
444
- // Always prepend the marker — Claude's output won't contain it
444
+ // Always prepend the marker; model output won't contain it.
445
445
  const analysis = analysisContent
446
446
  ? `${LOG_SNAPSHOT_MARKER}\n${analysisContent}`
447
447
  : `${LOG_SNAPSHOT_MARKER}\n## Datadog Log Snapshot\n\n**${totalLogs} log entries** captured from Datadog.`;
@@ -3,6 +3,7 @@ import { fetchIssueComments } from '../../core/github-issues.js';
3
3
  import { logger } from '../../core/logging.js';
4
4
  import { invokePiPrompt } from '../../core/pi.js';
5
5
  import { WORKSPACE_DIR } from '../../core/shared.js';
6
+ import { resolveReviewProviderCredential } from '../../providers/credentials.js';
6
7
  import { stripReviewFeedbackFooter } from './review-feedback.js';
7
8
  // Roughly 6k tokens: large enough for substantive PR discussions while still
8
9
  // keeping conversation context secondary to the diff.
@@ -14,11 +15,8 @@ const GITHUB_ACTIONS_BOT_LOGIN = 'github-actions[bot]';
14
15
  const MASKED_PR_AUTHOR_LABEL = 'PR author';
15
16
  function getSummaryProviders(apiKeys) {
16
17
  return [
17
- ...(apiKeys?.gemini ? [{ provider: 'gemini', apiKey: apiKeys.gemini }] : []),
18
- ...(apiKeys?.openai ? [{ provider: 'openai', apiKey: apiKeys.openai }] : []),
19
- ...(apiKeys?.anthropic
20
- ? [{ provider: 'anthropic', apiKey: apiKeys.anthropic }]
21
- : []),
18
+ ...(apiKeys?.gemini ? [{ provider: 'gemini', credential: apiKeys.gemini }] : []),
19
+ ...(apiKeys?.openai ? [{ provider: 'openai', credential: apiKeys.openai }] : []),
22
20
  ];
23
21
  }
24
22
  function isRelevantBotLogin(login) {
@@ -253,7 +251,7 @@ If a section has no relevant information, use "- None."
253
251
  ${rawContext}
254
252
  </pr_conversation>`;
255
253
  }
256
- async function summarizeConversationContextWithModel({ provider, apiKey, rawContext, workspaceDir, }) {
254
+ async function summarizeConversationContextWithModel({ provider, credential, rawContext, workspaceDir, }) {
257
255
  const startedAt = Date.now();
258
256
  logger.info('PR conversation summarization started', {
259
257
  task: 'review',
@@ -261,9 +259,17 @@ async function summarizeConversationContextWithModel({ provider, apiKey, rawCont
261
259
  raw_chars: rawContext.length,
262
260
  });
263
261
  try {
262
+ const resolvedCredential = resolveReviewProviderCredential(credential);
263
+ if (!resolvedCredential) {
264
+ return null;
265
+ }
264
266
  const result = await invokePiPrompt({
265
267
  provider,
266
- apiKey,
268
+ apiKey: resolvedCredential.apiKey,
269
+ ...(resolvedCredential.piProviderId
270
+ ? { piProviderId: resolvedCredential.piProviderId }
271
+ : {}),
272
+ ...(resolvedCredential.modelName ? { modelName: resolvedCredential.modelName } : {}),
267
273
  prompt: buildSummaryPrompt(rawContext),
268
274
  cwd: workspaceDir ?? WORKSPACE_DIR,
269
275
  timeoutMs: CONVERSATION_SUMMARY_TIMEOUT_MS,
@@ -1,11 +1,14 @@
1
1
  import { logger } from '../../../core/logging.js';
2
2
  import { invokePiPrompt } from '../../../core/pi.js';
3
3
  import { logPiProgressEvent } from '../../../core/pi-progress-logging.js';
4
+ import { resolveReviewProviderCredential } from '../../../providers/credentials.js';
4
5
  import { extractJsonFromResponse } from '../../../providers/helpers.js';
5
6
  import { isDedupeResponse } from '../../../providers/schemas.js';
6
7
  const DEDUPE_TIMEOUT_MS = 5 * 60 * 1000;
7
8
  const DEDUPE_PROVIDER = 'gemini';
8
- const DEDUPE_MODEL_NAME = 'gemini-3-flash-preview';
9
+ const DEDUPE_MODEL_BY_PI_PROVIDER = {
10
+ openrouter: 'google/gemini-3-flash-preview',
11
+ };
9
12
  const DEDUPE_THINKING_LEVEL = 'medium';
10
13
  const DEDUPE_MAX_ATTEMPTS = 2;
11
14
  const DEDUPE_RETRY_DELAY_MS = 1_000;
@@ -106,28 +109,31 @@ export function applyDedupeGroups(findings, groups) {
106
109
  });
107
110
  return findings.filter((finding) => !dropped.has(finding.id));
108
111
  }
109
- export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiApiKey, }) {
112
+ export async function dedupeMultiFocusFindings({ findings, availableModels, workspaceDir, geminiCredential, }) {
110
113
  if (findings.length <= 1) {
111
114
  return findings;
112
115
  }
113
116
  if (!availableModels.includes(DEDUPE_PROVIDER)) {
114
117
  logger.error('Multi-focus dedupe unavailable because Gemini is not enabled', {
115
118
  provider: DEDUPE_PROVIDER,
116
- model: DEDUPE_MODEL_NAME,
119
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
117
120
  });
118
121
  throw new Error('Multi-focus dedupe requires Gemini to be enabled');
119
122
  }
120
- if (!geminiApiKey) {
121
- logger.error('Multi-focus dedupe unavailable because Gemini API key is unavailable', {
123
+ const credential = resolveReviewProviderCredential(geminiCredential);
124
+ if (!credential) {
125
+ logger.error('Multi-focus dedupe unavailable because Gemini review credential is unavailable', {
122
126
  provider: DEDUPE_PROVIDER,
123
- model: DEDUPE_MODEL_NAME,
127
+ model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
124
128
  });
125
- throw new Error('Multi-focus dedupe requires a Gemini API key');
129
+ throw new Error('Multi-focus dedupe requires a Gemini review credential');
126
130
  }
131
+ const piProviderId = credential.piProviderId ?? 'openrouter';
127
132
  const providerConfig = {
128
133
  provider: DEDUPE_PROVIDER,
129
- apiKey: geminiApiKey,
130
- modelName: DEDUPE_MODEL_NAME,
134
+ apiKey: credential.apiKey,
135
+ ...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
136
+ modelName: DEDUPE_MODEL_BY_PI_PROVIDER[piProviderId] ?? DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
131
137
  };
132
138
  for (let attempt = 1; attempt <= DEDUPE_MAX_ATTEMPTS; attempt += 1) {
133
139
  let result;
@@ -135,6 +141,9 @@ export async function dedupeMultiFocusFindings({ findings, availableModels, work
135
141
  result = await invokePiPrompt({
136
142
  provider: providerConfig.provider,
137
143
  apiKey: providerConfig.apiKey,
144
+ ...(providerConfig.piProviderId
145
+ ? { piProviderId: providerConfig.piProviderId }
146
+ : {}),
138
147
  prompt: buildDedupePrompt(findings),
139
148
  cwd: workspaceDir,
140
149
  timeoutMs: DEDUPE_TIMEOUT_MS,