@planningo/duul 1.0.0
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/LICENSE +21 -0
- package/README.ko.md +438 -0
- package/README.md +463 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +18 -0
- package/build/prompts/code-review-system.d.ts +9 -0
- package/build/prompts/code-review-system.js +116 -0
- package/build/prompts/execution-partition-system.d.ts +11 -0
- package/build/prompts/execution-partition-system.js +76 -0
- package/build/prompts/plan-review-system.d.ts +29 -0
- package/build/prompts/plan-review-system.js +175 -0
- package/build/schemas/code-review.d.ts +514 -0
- package/build/schemas/code-review.js +175 -0
- package/build/schemas/common.d.ts +118 -0
- package/build/schemas/common.js +64 -0
- package/build/schemas/execution-partition.d.ts +597 -0
- package/build/schemas/execution-partition.js +107 -0
- package/build/schemas/plan-review.d.ts +523 -0
- package/build/schemas/plan-review.js +175 -0
- package/build/services/filesystem-tools.d.ts +6 -0
- package/build/services/filesystem-tools.js +39 -0
- package/build/services/filesystem.d.ts +69 -0
- package/build/services/filesystem.js +609 -0
- package/build/services/pricing.d.ts +8 -0
- package/build/services/pricing.js +105 -0
- package/build/services/providers/anthropic.d.ts +28 -0
- package/build/services/providers/anthropic.js +431 -0
- package/build/services/providers/google.d.ts +28 -0
- package/build/services/providers/google.js +358 -0
- package/build/services/providers/openai.d.ts +22 -0
- package/build/services/providers/openai.js +395 -0
- package/build/services/providers/types.d.ts +82 -0
- package/build/services/providers/types.js +1 -0
- package/build/services/review-gates.d.ts +83 -0
- package/build/services/review-gates.js +200 -0
- package/build/services/review-limits.d.ts +36 -0
- package/build/services/review-limits.js +65 -0
- package/build/services/reviewer.d.ts +30 -0
- package/build/services/reviewer.js +243 -0
- package/build/services/usage-logger.d.ts +2 -0
- package/build/services/usage-logger.js +42 -0
- package/build/tools/code-review.d.ts +2 -0
- package/build/tools/code-review.js +178 -0
- package/build/tools/execution-partition.d.ts +2 -0
- package/build/tools/execution-partition.js +146 -0
- package/build/tools/plan-review.d.ts +2 -0
- package/build/tools/plan-review.js +183 -0
- package/package.json +65 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { validateProjectRoot } from '../filesystem.js';
|
|
2
|
+
import { executeFilesystemTool } from '../filesystem-tools.js';
|
|
3
|
+
import { estimateCost } from '../pricing.js';
|
|
4
|
+
const MAX_INPUT_CHARS = 400_000;
|
|
5
|
+
const MAX_TOOL_ROUNDS = 10;
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
const MAX_REPEAT_CALLS = 3;
|
|
8
|
+
const LINKED_PATH_HINT = ' To access a linked root, prefix with "linked:<index>:<path>" (e.g. "linked:0:src/types.ts").';
|
|
9
|
+
/**
|
|
10
|
+
* Google/Gemini tool definitions using functionDeclarations format.
|
|
11
|
+
*/
|
|
12
|
+
const GOOGLE_TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
functionDeclarations: [
|
|
15
|
+
{
|
|
16
|
+
name: 'read_file',
|
|
17
|
+
description: 'Read a file from the project being reviewed. Use this to examine type definitions, ' +
|
|
18
|
+
'data models, interfaces, utility functions, and any code you need for thorough review context. ' +
|
|
19
|
+
'Returns file contents as text.' + LINKED_PATH_HINT,
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'OBJECT',
|
|
22
|
+
properties: { path: { type: 'STRING', description: 'Relative path from project root, or "linked:<index>:<path>" for linked roots' } },
|
|
23
|
+
required: ['path'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'list_directory',
|
|
28
|
+
description: 'List files and directories at a path in the project. Use this to understand project structure.' + LINKED_PATH_HINT,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: 'OBJECT',
|
|
31
|
+
properties: { path: { type: 'STRING', description: 'Relative path from project root. Use "." for root.' } },
|
|
32
|
+
required: ['path'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'search_in_files',
|
|
37
|
+
description: 'Search for a pattern across project files. Uses ripgrep when available, falls back to git grep. ' +
|
|
38
|
+
'PREFER this over read_file when you need to find specific symbols, keywords, or patterns.',
|
|
39
|
+
parameters: {
|
|
40
|
+
type: 'OBJECT',
|
|
41
|
+
properties: {
|
|
42
|
+
query: { type: 'STRING', description: 'Search pattern (literal string or regex)' },
|
|
43
|
+
paths: { type: 'ARRAY', items: { type: 'STRING' }, description: 'Optional: restrict search to these relative paths' },
|
|
44
|
+
glob: { type: 'STRING', description: 'Optional: glob pattern to filter files' },
|
|
45
|
+
},
|
|
46
|
+
required: ['query'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'read_file_range',
|
|
51
|
+
description: 'Read a specific line range from a file. PREFER this over read_file for large files. Max 200 lines per call.' + LINKED_PATH_HINT,
|
|
52
|
+
parameters: {
|
|
53
|
+
type: 'OBJECT',
|
|
54
|
+
properties: {
|
|
55
|
+
path: { type: 'STRING', description: 'Relative path from project root' },
|
|
56
|
+
start_line: { type: 'NUMBER', description: 'First line to read (1-based)' },
|
|
57
|
+
end_line: { type: 'NUMBER', description: 'Last line to read (1-based, inclusive)' },
|
|
58
|
+
},
|
|
59
|
+
required: ['path', 'start_line', 'end_line'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'stat_file',
|
|
64
|
+
description: 'Get file metadata: size, type, and modification time.' + LINKED_PATH_HINT,
|
|
65
|
+
parameters: {
|
|
66
|
+
type: 'OBJECT',
|
|
67
|
+
properties: { path: { type: 'STRING', description: 'Relative path from project root' } },
|
|
68
|
+
required: ['path'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'read_json',
|
|
73
|
+
description: 'Read a JSON file, optionally extracting a value at a JSON pointer path.' + LINKED_PATH_HINT,
|
|
74
|
+
parameters: {
|
|
75
|
+
type: 'OBJECT',
|
|
76
|
+
properties: {
|
|
77
|
+
path: { type: 'STRING', description: 'Relative path to a JSON file' },
|
|
78
|
+
json_pointer: { type: 'STRING', description: 'Optional: JSON pointer (e.g. "/dependencies")' },
|
|
79
|
+
},
|
|
80
|
+
required: ['path'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'list_tracked_files',
|
|
85
|
+
description: 'List git-tracked files, optionally filtered by a directory prefix.',
|
|
86
|
+
parameters: {
|
|
87
|
+
type: 'OBJECT',
|
|
88
|
+
properties: { prefix: { type: 'STRING', description: 'Optional: directory prefix to filter' } },
|
|
89
|
+
required: [],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'get_git_diff',
|
|
94
|
+
description: 'Get git diff output for the workspace. Use this to see exactly what changed — ' +
|
|
95
|
+
'PREFER this over reading full files when reviewing modifications. ' +
|
|
96
|
+
'Returns unified diff format. Defaults to comparing against HEAD (current workspace changes). Also includes untracked new files.',
|
|
97
|
+
parameters: {
|
|
98
|
+
type: 'OBJECT',
|
|
99
|
+
properties: {
|
|
100
|
+
base: { type: 'STRING', description: 'Base ref to diff against. Defaults to HEAD.' },
|
|
101
|
+
paths: { type: 'ARRAY', items: { type: 'STRING' }, description: 'Optional: restrict diff to these relative paths' },
|
|
102
|
+
},
|
|
103
|
+
required: [],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const FULL_READ_TOOLS = new Set(['read_file', 'get_git_diff']);
|
|
110
|
+
const SEARCH_ONLY_TOOLS = new Set(['search_in_files', 'list_tracked_files', 'stat_file']);
|
|
111
|
+
/**
|
|
112
|
+
* Google provider — uses Gemini models via the Generative Language API.
|
|
113
|
+
*
|
|
114
|
+
* Capabilities:
|
|
115
|
+
* - Tool calling via native functionDeclarations
|
|
116
|
+
* - JSON mode (responseMimeType: "application/json") when no tools active
|
|
117
|
+
* - No previous_response_id
|
|
118
|
+
*/
|
|
119
|
+
export class GoogleProvider {
|
|
120
|
+
name = 'google';
|
|
121
|
+
capabilities = {
|
|
122
|
+
structuredOutputs: false,
|
|
123
|
+
toolCalling: true,
|
|
124
|
+
previousResponseId: false,
|
|
125
|
+
jsonSchemaStrict: false,
|
|
126
|
+
};
|
|
127
|
+
apiKey;
|
|
128
|
+
baseUrl;
|
|
129
|
+
model;
|
|
130
|
+
temperature;
|
|
131
|
+
topP;
|
|
132
|
+
constructor(config) {
|
|
133
|
+
const apiKey = config?.apiKey ?? process.env.GOOGLE_API_KEY;
|
|
134
|
+
if (!apiKey) {
|
|
135
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
136
|
+
}
|
|
137
|
+
this.apiKey = apiKey;
|
|
138
|
+
this.baseUrl = config?.baseUrl ?? 'https://generativelanguage.googleapis.com';
|
|
139
|
+
this.model = config?.model ?? 'gemini-3.1-pro-preview';
|
|
140
|
+
this.temperature = config?.temperature ?? 0.2;
|
|
141
|
+
this.topP = config?.topP ?? 0.1;
|
|
142
|
+
}
|
|
143
|
+
async review(options) {
|
|
144
|
+
const { systemPrompt, userMessage, outputSchema, workspaceScope } = options;
|
|
145
|
+
const effectiveRoot = workspaceScope?.root ?? null;
|
|
146
|
+
if (effectiveRoot && !workspaceScope) {
|
|
147
|
+
validateProjectRoot(effectiveRoot);
|
|
148
|
+
}
|
|
149
|
+
const enhancedSystem = `${systemPrompt}\n\n## Output Format\nYou MUST respond with ONLY a valid JSON object. No markdown, no explanation, no code blocks — only the JSON object.`;
|
|
150
|
+
const tools = effectiveRoot ? GOOGLE_TOOLS : undefined;
|
|
151
|
+
let allUsedTools = [];
|
|
152
|
+
// Accumulate token usage
|
|
153
|
+
let totalInputTokens = 0;
|
|
154
|
+
let totalOutputTokens = 0;
|
|
155
|
+
let apiCallCount = 0;
|
|
156
|
+
const accumulateUsage = (body) => {
|
|
157
|
+
apiCallCount++;
|
|
158
|
+
if (body.usageMetadata) {
|
|
159
|
+
totalInputTokens += body.usageMetadata.promptTokenCount ?? 0;
|
|
160
|
+
totalOutputTokens += body.usageMetadata.candidatesTokenCount ?? 0;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const buildUsage = () => ({
|
|
164
|
+
input_tokens: totalInputTokens,
|
|
165
|
+
output_tokens: totalOutputTokens,
|
|
166
|
+
total_tokens: totalInputTokens + totalOutputTokens,
|
|
167
|
+
api_calls: apiCallCount,
|
|
168
|
+
provider: 'google',
|
|
169
|
+
model: this.model,
|
|
170
|
+
estimated_cost_usd: estimateCost(this.model, totalInputTokens, totalOutputTokens),
|
|
171
|
+
});
|
|
172
|
+
const contents = [
|
|
173
|
+
{ role: 'user', parts: [{ text: userMessage }] },
|
|
174
|
+
];
|
|
175
|
+
let body = await this.apiCallWithRetry(enhancedSystem, contents, tools);
|
|
176
|
+
accumulateUsage(body);
|
|
177
|
+
console.error(`[duul] Gemini response received, model=${this.model} provider=google`);
|
|
178
|
+
// Agentic tool-calling loop
|
|
179
|
+
if (effectiveRoot) {
|
|
180
|
+
const toolReadBudget = MAX_INPUT_CHARS - (enhancedSystem.length + userMessage.length);
|
|
181
|
+
let accumulatedToolChars = 0;
|
|
182
|
+
const getStrategyLevel = () => {
|
|
183
|
+
const ratio = accumulatedToolChars / toolReadBudget;
|
|
184
|
+
if (ratio < 0.5)
|
|
185
|
+
return 0;
|
|
186
|
+
if (ratio < 0.8)
|
|
187
|
+
return 1;
|
|
188
|
+
if (ratio < 1.0)
|
|
189
|
+
return 2;
|
|
190
|
+
return 3;
|
|
191
|
+
};
|
|
192
|
+
const isToolAllowed = (toolName, level) => {
|
|
193
|
+
if (level >= 3)
|
|
194
|
+
return false;
|
|
195
|
+
if (level >= 2)
|
|
196
|
+
return SEARCH_ONLY_TOOLS.has(toolName);
|
|
197
|
+
if (level >= 1)
|
|
198
|
+
return !FULL_READ_TOOLS.has(toolName);
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
const budgetMessage = (toolName, level) => {
|
|
202
|
+
if (level >= 3)
|
|
203
|
+
return 'Budget exhausted. You must produce your final review verdict now.';
|
|
204
|
+
if (level >= 2)
|
|
205
|
+
return `[Budget Level 2] Only search/stat tools allowed. "${toolName}" blocked.`;
|
|
206
|
+
if (level >= 1)
|
|
207
|
+
return `[Budget Level 1] Full file reads blocked. "${toolName}" not allowed.`;
|
|
208
|
+
return '';
|
|
209
|
+
};
|
|
210
|
+
const toolCache = new Map();
|
|
211
|
+
const callCounts = new Map();
|
|
212
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
213
|
+
const parts = body.candidates?.[0]?.content?.parts ?? [];
|
|
214
|
+
const functionCalls = parts.filter((p) => 'functionCall' in p);
|
|
215
|
+
if (functionCalls.length === 0)
|
|
216
|
+
break;
|
|
217
|
+
const strategyLevel = getStrategyLevel();
|
|
218
|
+
console.error(`[duul] Tool round ${round + 1}: ${functionCalls.length} call(s), budget ${accumulatedToolChars}/${toolReadBudget} (level ${strategyLevel})`);
|
|
219
|
+
// Append model's function call parts
|
|
220
|
+
contents.push({ role: 'model', parts: parts });
|
|
221
|
+
const responseParts = [];
|
|
222
|
+
for (const call of functionCalls) {
|
|
223
|
+
const { name, args: rawArgs } = call.functionCall;
|
|
224
|
+
const args = rawArgs ?? {};
|
|
225
|
+
const cacheKey = `${name}:${JSON.stringify(args)}`;
|
|
226
|
+
const argSummary = (args.path ?? args.query ?? args.prefix ?? '');
|
|
227
|
+
const count = (callCounts.get(cacheKey) ?? 0) + 1;
|
|
228
|
+
callCounts.set(cacheKey, count);
|
|
229
|
+
if (count > MAX_REPEAT_CALLS) {
|
|
230
|
+
responseParts.push({ functionResponse: { name, response: { output: 'You have already read this content multiple times. Use the context you have.' } } });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (toolCache.has(cacheKey)) {
|
|
234
|
+
responseParts.push({ functionResponse: { name, response: { output: toolCache.get(cacheKey) } } });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const currentLevel = getStrategyLevel();
|
|
238
|
+
if (!isToolAllowed(name, currentLevel)) {
|
|
239
|
+
responseParts.push({ functionResponse: { name, response: { output: budgetMessage(name, currentLevel) } } });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const result = await executeFilesystemTool(effectiveRoot, name, args, workspaceScope);
|
|
243
|
+
toolCache.set(cacheKey, result);
|
|
244
|
+
allUsedTools.push(`${name}(${argSummary})`);
|
|
245
|
+
accumulatedToolChars += result.length;
|
|
246
|
+
console.error(`[duul] ${name}(${argSummary}) -> ${result.length} chars (total: ${accumulatedToolChars}/${toolReadBudget}, level ${getStrategyLevel()})`);
|
|
247
|
+
responseParts.push({ functionResponse: { name, response: { output: result } } });
|
|
248
|
+
}
|
|
249
|
+
contents.push({ role: 'user', parts: responseParts });
|
|
250
|
+
body = await this.apiCallWithRetry(enhancedSystem, contents, tools);
|
|
251
|
+
accumulateUsage(body);
|
|
252
|
+
console.error(`[duul] Gemini response (after tool round ${round + 1})`);
|
|
253
|
+
// Force verdict if budget exhausted
|
|
254
|
+
if (getStrategyLevel() >= 3) {
|
|
255
|
+
const pendingParts = (body.candidates?.[0]?.content?.parts ?? []).filter((p) => 'functionCall' in p);
|
|
256
|
+
if (pendingParts.length > 0) {
|
|
257
|
+
contents.push({ role: 'model', parts: body.candidates?.[0]?.content?.parts ?? [] });
|
|
258
|
+
const stopParts = pendingParts.map((p) => ({
|
|
259
|
+
functionResponse: { name: p.functionCall.name, response: { output: 'No more file reads allowed. Produce your final verdict now.' } },
|
|
260
|
+
}));
|
|
261
|
+
contents.push({ role: 'user', parts: stopParts });
|
|
262
|
+
body = await this.apiCallWithRetry(enhancedSystem, contents, tools);
|
|
263
|
+
accumulateUsage(body);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Handle pending tool calls after loop exhaustion
|
|
269
|
+
const pendingParts = (body.candidates?.[0]?.content?.parts ?? []).filter((p) => 'functionCall' in p);
|
|
270
|
+
if (pendingParts.length > 0) {
|
|
271
|
+
contents.push({ role: 'model', parts: body.candidates?.[0]?.content?.parts ?? [] });
|
|
272
|
+
const stopParts = pendingParts.map((p) => ({
|
|
273
|
+
functionResponse: { name: p.functionCall.name, response: { output: 'Tool call limit reached. Produce your final verdict now.' } },
|
|
274
|
+
}));
|
|
275
|
+
contents.push({ role: 'user', parts: stopParts });
|
|
276
|
+
body = await this.apiCallWithRetry(enhancedSystem, contents, tools);
|
|
277
|
+
accumulateUsage(body);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const usage = buildUsage();
|
|
281
|
+
const costStr = usage.estimated_cost_usd !== null ? ` (~$${usage.estimated_cost_usd.toFixed(4)})` : '';
|
|
282
|
+
console.error(`[duul] Token usage: ${usage.input_tokens} in + ${usage.output_tokens} out = ${usage.total_tokens} total (${usage.api_calls} API calls)${costStr}`);
|
|
283
|
+
// Extract text content and parse JSON
|
|
284
|
+
const textPart = (body.candidates?.[0]?.content?.parts ?? []).find((p) => 'text' in p);
|
|
285
|
+
if (!textPart?.text) {
|
|
286
|
+
if (options.createFallback) {
|
|
287
|
+
const reason = 'budget';
|
|
288
|
+
const fallback = options.createFallback(reason, allUsedTools);
|
|
289
|
+
console.error(`[duul] Returning structured fallback (reason: ${reason}).`);
|
|
290
|
+
const reviewId = `google-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
291
|
+
return { parsed: fallback, reviewId, usage };
|
|
292
|
+
}
|
|
293
|
+
throw new Error('Google returned no text content');
|
|
294
|
+
}
|
|
295
|
+
const jsonStr = extractJson(textPart.text);
|
|
296
|
+
const parsed = outputSchema.parse(JSON.parse(jsonStr));
|
|
297
|
+
const reviewId = `google-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
298
|
+
return { parsed, reviewId, usage };
|
|
299
|
+
}
|
|
300
|
+
async apiCallWithRetry(system, contents, tools) {
|
|
301
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const timeout = setTimeout(() => controller.abort(), 120_000);
|
|
304
|
+
try {
|
|
305
|
+
const url = `${this.baseUrl}/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
306
|
+
const response = await fetch(url, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify({
|
|
310
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
311
|
+
contents,
|
|
312
|
+
generationConfig: {
|
|
313
|
+
temperature: this.temperature,
|
|
314
|
+
topP: this.topP,
|
|
315
|
+
maxOutputTokens: 16384,
|
|
316
|
+
// Only use JSON mode when no tools are active (tools produce function calls, not JSON)
|
|
317
|
+
...(tools ? {} : { responseMimeType: 'application/json' }),
|
|
318
|
+
},
|
|
319
|
+
...(tools ? { tools } : {}),
|
|
320
|
+
}),
|
|
321
|
+
signal: controller.signal,
|
|
322
|
+
});
|
|
323
|
+
clearTimeout(timeout);
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
const status = response.status;
|
|
326
|
+
if ((status === 429 || status >= 500) && attempt < MAX_RETRIES - 1) {
|
|
327
|
+
const delay = 1000 * Math.pow(2, attempt);
|
|
328
|
+
console.error(`[duul] Google retry ${attempt + 1}/${MAX_RETRIES} after ${delay}ms (status ${status})`);
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
throw new Error(`Google API error: ${status} ${response.statusText}`);
|
|
333
|
+
}
|
|
334
|
+
return await response.json();
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
clearTimeout(timeout);
|
|
338
|
+
if (attempt < MAX_RETRIES - 1 && error instanceof Error && error.name === 'AbortError') {
|
|
339
|
+
const delay = 1000 * Math.pow(2, attempt);
|
|
340
|
+
console.error(`[duul] Google retry ${attempt + 1}/${MAX_RETRIES} after ${delay}ms (timeout)`);
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
throw new Error('Unreachable: exhausted retries');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function extractJson(text) {
|
|
351
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
352
|
+
if (codeBlockMatch)
|
|
353
|
+
return codeBlockMatch[1].trim();
|
|
354
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
355
|
+
if (jsonMatch)
|
|
356
|
+
return jsonMatch[0];
|
|
357
|
+
return text.trim();
|
|
358
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { ReviewerProvider, ReviewCallOptions, ReviewCallResult, ProviderCapabilities } from './types.js';
|
|
3
|
+
export declare class OpenAIProvider implements ReviewerProvider {
|
|
4
|
+
readonly name = "openai";
|
|
5
|
+
readonly capabilities: ProviderCapabilities;
|
|
6
|
+
private client;
|
|
7
|
+
private model;
|
|
8
|
+
private temperature;
|
|
9
|
+
private topP;
|
|
10
|
+
constructor(config?: {
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
temperature?: number;
|
|
15
|
+
topP?: number;
|
|
16
|
+
});
|
|
17
|
+
review<T extends z.ZodType>(options: ReviewCallOptions<T>): Promise<ReviewCallResult<z.infer<T>>>;
|
|
18
|
+
private apiCallWithRetry;
|
|
19
|
+
private extractStructuredOutput;
|
|
20
|
+
private hasPendingFunctionCalls;
|
|
21
|
+
private getFunctionCalls;
|
|
22
|
+
}
|