@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.
- package/dist/actions/auth.js +23 -23
- package/dist/actions/doctor.js +3 -5
- package/dist/actions/review-usage.js +6 -1
- package/dist/actions/review.js +7 -11
- 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/index.js +19 -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-git.js +3 -1
- 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
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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:
|
|
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 =
|
|
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.
|
|
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
|
|
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.`;
|