@doist/doistbot-cli 1.0.3 → 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 (65) hide show
  1. package/dist/actions/auth.js +23 -23
  2. package/dist/actions/doctor.js +3 -5
  3. package/dist/actions/review-usage.js +6 -1
  4. package/dist/actions/review.js +7 -11
  5. package/dist/auth.js +17 -17
  6. package/dist/config.js +14 -9
  7. package/dist/env.js +1 -2
  8. package/dist/file-store.js +1 -1
  9. package/dist/index.js +19 -1
  10. package/dist/terminal.js +0 -7
  11. package/package.json +5 -5
  12. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  13. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
  14. package/sandbox/dist/core/bot-logins.js +0 -13
  15. package/sandbox/dist/core/datadog-metrics.js +0 -2
  16. package/sandbox/dist/core/pi.js +16 -13
  17. package/sandbox/dist/providers/config.js +1 -1
  18. package/sandbox/dist/providers/credentials.js +30 -0
  19. package/sandbox/dist/providers/gemini.js +4 -10
  20. package/sandbox/dist/providers/helpers.js +6 -21
  21. package/sandbox/dist/providers/index.js +0 -3
  22. package/sandbox/dist/providers/openai.js +1 -8
  23. package/sandbox/dist/providers/pi-review.js +16 -54
  24. package/sandbox/dist/providers/schemas.js +0 -39
  25. package/sandbox/dist/tasks/chat/chat.js +2 -1
  26. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  27. package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
  28. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  29. package/sandbox/dist/tasks/issue-triage/context.js +1 -2
  30. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
  31. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  32. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  33. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  34. package/sandbox/dist/tasks/review/conversation-context.js +13 -7
  35. package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
  36. package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
  37. package/sandbox/dist/tasks/review/engines/shared.js +11 -59
  38. package/sandbox/dist/tasks/review/local-git.js +3 -1
  39. package/sandbox/dist/tasks/review/local.js +4 -29
  40. package/sandbox/dist/tasks/review/prompt.js +2 -49
  41. package/sandbox/dist/tasks/review/review-summary.js +82 -0
  42. package/sandbox/dist/tasks/review/review.js +23 -57
  43. package/sandbox/dist/tasks/review/runner.js +35 -51
  44. package/sandbox/dist/tasks/review/summary-model.js +11 -2
  45. package/sandbox/dist/tasks/review/thinking-level.js +0 -2
  46. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  47. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
  48. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  49. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  50. package/sandbox/dist/providers/anthropic.js +0 -30
  51. package/sandbox/dist/tasks/review/author-format.js +0 -14
  52. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  53. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  54. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  55. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  56. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  57. package/sandbox/dist/tasks/review/council/types.js +0 -4
  58. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  59. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  60. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  61. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  62. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  63. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  64. package/sandbox/review-prompt.md +0 -209
  65. package/sandbox/vote-prompt.md +0 -69
@@ -4,26 +4,26 @@ import { tmpdir } from 'os';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
7
- gemini: 'google',
7
+ gemini: 'openrouter',
8
8
  openai: 'openai',
9
- anthropic: 'anthropic',
10
- };
11
- const PI_API_KEY_ENV_VAR_BY_PROVIDER = {
12
- gemini: 'GEMINI_API_KEY',
13
- openai: 'OPENAI_API_KEY',
14
- anthropic: 'ANTHROPIC_API_KEY',
15
9
  };
16
10
  export const DEFAULT_PI_MODELS = {
17
11
  gemini: 'gemini-3.1-pro-preview',
18
12
  openai: 'gpt-5.4',
19
- anthropic: 'claude-opus-4-6',
13
+ };
14
+ export const DEFAULT_OPENROUTER_MODELS = {
15
+ gemini: 'google/gemini-3.1-pro-preview',
20
16
  };
21
17
  export const MAX_PI_THINKING_BY_PROVIDER = {
22
18
  gemini: 'high',
23
19
  openai: 'xhigh',
24
- anthropic: 'xhigh',
25
20
  };
26
- const PI_PROVIDER_IDS = new Set(['google', 'openai', 'openai-codex', 'anthropic']);
21
+ const PI_API_KEY_ENV_VAR_BY_PI_PROVIDER = {
22
+ openai: 'OPENAI_API_KEY',
23
+ 'openai-codex': 'OPENAI_API_KEY',
24
+ openrouter: 'OPENROUTER_API_KEY',
25
+ };
26
+ const PI_PROVIDER_IDS = new Set(['openai', 'openai-codex', 'openrouter']);
27
27
  export const PI_AUTH_API_KEY_PREFIX = 'pi-auth://';
28
28
  const DOISTBOT_PI_SYSTEM_PROMPT = 'You are running inside doistbot in a non-interactive environment. Inspect the repository, configuration, documentation, and relevant history when they can improve accuracy. Use available tools proactively to gather evidence, avoid guessing when the workspace can answer the question, and follow the task prompt exactly.';
29
29
  const PI_PROGRESS_HEARTBEAT_MS = 30_000;
@@ -62,7 +62,9 @@ function canExecuteNode(candidate) {
62
62
  }
63
63
  function getPiModelSpec({ provider, piProviderId, modelName, thinkingLevel, }) {
64
64
  const providerId = piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
65
- const resolvedModelName = modelName ?? DEFAULT_PI_MODELS[provider];
65
+ const resolvedModelName = modelName ??
66
+ (providerId === 'openrouter' ? DEFAULT_OPENROUTER_MODELS[provider] : undefined) ??
67
+ DEFAULT_PI_MODELS[provider];
66
68
  return `${providerId}/${resolvedModelName}:${thinkingLevel}`;
67
69
  }
68
70
  function parsePiAuthProviderId(apiKey) {
@@ -222,9 +224,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
222
224
  async invoke({ prompt, modelName, timeoutMs = 5 * 60 * 1000, images, }) {
223
225
  const startedAt = Date.now();
224
226
  const piAuthProviderId = parsePiAuthProviderId(apiKey);
227
+ const resolvedPiProviderId = piAuthProviderId ?? piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
225
228
  const modelSpec = getPiModelSpec({
226
229
  provider,
227
- piProviderId: piAuthProviderId ?? piProviderId,
230
+ piProviderId: resolvedPiProviderId,
228
231
  modelName,
229
232
  thinkingLevel: thinkingLevel ?? MAX_PI_THINKING_BY_PROVIDER[provider],
230
233
  });
@@ -253,7 +256,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
253
256
  GIT_TERMINAL_PROMPT: '0',
254
257
  ...extraEnv,
255
258
  };
256
- const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PROVIDER[provider];
259
+ const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
257
260
  if (piAuthProviderId) {
258
261
  delete childEnv[apiKeyEnvVar];
259
262
  }
@@ -15,7 +15,7 @@ export function resolveProviderTimeoutMs(explicitTimeoutMs) {
15
15
  explicitTimeoutMs > 0) {
16
16
  return explicitTimeoutMs;
17
17
  }
18
- const sharedTimeout = parseTimeoutMs(process.env.COUNCIL_MODEL_TIMEOUT_MS);
18
+ const sharedTimeout = parseTimeoutMs(process.env.REVIEW_MODEL_TIMEOUT_MS);
19
19
  if (sharedTimeout != null) {
20
20
  return sharedTimeout;
21
21
  }
@@ -0,0 +1,30 @@
1
+ export function resolveReviewProviderCredential(credential) {
2
+ if (typeof credential === 'string') {
3
+ const apiKey = credential.trim();
4
+ return apiKey ? { apiKey } : undefined;
5
+ }
6
+ if (!credential) {
7
+ return undefined;
8
+ }
9
+ const apiKey = credential.apiKey.trim();
10
+ if (!apiKey) {
11
+ return undefined;
12
+ }
13
+ return {
14
+ apiKey,
15
+ ...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
16
+ ...(credential.modelName ? { modelName: credential.modelName } : {}),
17
+ };
18
+ }
19
+ export function hasReviewProviderCredential(credential) {
20
+ return resolveReviewProviderCredential(credential) !== undefined;
21
+ }
22
+ export function getReviewProviderCredential(apiKeys, provider) {
23
+ return resolveReviewProviderCredential(apiKeys[provider]);
24
+ }
25
+ export function createOpenRouterCredential(apiKey) {
26
+ return {
27
+ apiKey,
28
+ piProviderId: 'openrouter',
29
+ };
30
+ }
@@ -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.`;