@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.
- package/dist/actions/auth.js +23 -23
- package/dist/actions/doctor.js +3 -5
- package/dist/actions/review.js +5 -4
- package/dist/auth.js +17 -17
- package/dist/config.js +14 -9
- package/dist/env.js +1 -2
- package/dist/file-store.js +1 -1
- package/dist/terminal.js +0 -7
- package/package.json +5 -5
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/datadog-metrics.js +0 -2
- package/sandbox/dist/core/pi.js +16 -13
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +30 -0
- package/sandbox/dist/providers/gemini.js +4 -10
- package/sandbox/dist/providers/helpers.js +6 -21
- package/sandbox/dist/providers/index.js +0 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/pi-review.js +16 -54
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +2 -1
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/context.js +1 -2
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/conversation-context.js +13 -7
- package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
- package/sandbox/dist/tasks/review/engines/shared.js +11 -59
- package/sandbox/dist/tasks/review/local.js +4 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +82 -0
- package/sandbox/dist/tasks/review/review.js +23 -57
- package/sandbox/dist/tasks/review/runner.js +35 -51
- package/sandbox/dist/tasks/review/summary-model.js +11 -2
- package/sandbox/dist/tasks/review/thinking-level.js +0 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- 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
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
37
|
+
function emitReviewModelProgress(provider, event, config) {
|
|
48
38
|
config.onProgress?.({
|
|
49
39
|
...event,
|
|
50
40
|
provider,
|
|
51
41
|
});
|
|
52
|
-
logReviewModelProgress(provider, event
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
136
|
-
|
|
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
|
-
*
|
|
11
|
-
* The proxy injects DD auth headers so
|
|
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
|
|
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
|
|
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',
|
|
18
|
-
...(apiKeys?.openai ? [{ provider: '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,
|
|
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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
121
|
-
|
|
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:
|
|
127
|
+
model: DEDUPE_MODEL_BY_PI_PROVIDER.openrouter,
|
|
124
128
|
});
|
|
125
|
-
throw new Error('Multi-focus dedupe requires a Gemini
|
|
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:
|
|
130
|
-
|
|
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,
|