llm-cli-gateway 2.10.0 → 2.11.1
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/CHANGELOG.md +75 -1
- package/README.md +46 -14
- package/dist/acp/event-normalizer.d.ts +42 -0
- package/dist/acp/event-normalizer.js +71 -0
- package/dist/acp/flight-redaction.d.ts +25 -0
- package/dist/acp/flight-redaction.js +40 -0
- package/dist/acp/host-services.d.ts +16 -0
- package/dist/acp/host-services.js +29 -0
- package/dist/acp/permission-bridge.d.ts +15 -0
- package/dist/acp/permission-bridge.js +90 -0
- package/dist/acp/process-manager.js +7 -1
- package/dist/acp/provider-registry.d.ts +1 -1
- package/dist/acp/provider-registry.js +18 -5
- package/dist/acp/runtime.d.ts +35 -0
- package/dist/acp/runtime.js +125 -0
- package/dist/acp/session-map.d.ts +42 -0
- package/dist/acp/session-map.js +67 -0
- package/dist/acp/smoke-harness.d.ts +28 -0
- package/dist/acp/smoke-harness.js +90 -0
- package/dist/api-http.d.ts +18 -0
- package/dist/api-http.js +122 -0
- package/dist/api-provider.d.ts +83 -0
- package/dist/api-provider.js +258 -0
- package/dist/api-request.d.ts +30 -0
- package/dist/api-request.js +51 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +6 -7
- package/dist/async-job-manager.d.ts +19 -4
- package/dist/async-job-manager.js +211 -35
- package/dist/claude-mcp-config.d.ts +2 -2
- package/dist/claude-mcp-config.js +42 -52
- package/dist/cli-updater.js +16 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +93 -35
- package/dist/doctor.d.ts +1 -1
- package/dist/flight-recorder.d.ts +1 -0
- package/dist/flight-recorder.js +11 -0
- package/dist/index.d.ts +56 -5
- package/dist/index.js +639 -38
- package/dist/job-store.d.ts +15 -0
- package/dist/job-store.js +39 -5
- package/dist/mcp-registry.d.ts +17 -0
- package/dist/mcp-registry.js +5 -0
- package/dist/metrics.js +7 -2
- package/dist/model-registry.js +11 -0
- package/dist/prompt-parts.d.ts +6 -6
- package/dist/provider-login-guidance.js +21 -0
- package/dist/provider-status.js +4 -1
- package/dist/provider-tool-capabilities.d.ts +8 -3
- package/dist/provider-tool-capabilities.js +107 -17
- package/dist/request-helpers.d.ts +6 -6
- package/dist/request-helpers.js +1 -4
- package/dist/session-manager-pg.js +2 -9
- package/dist/session-manager.d.ts +9 -4
- package/dist/session-manager.js +13 -4
- package/dist/upstream-contracts.js +184 -24
- package/dist/validation-normalizer.d.ts +2 -2
- package/dist/validation-orchestrator.d.ts +2 -0
- package/dist/validation-orchestrator.js +28 -7
- package/dist/validation-tools.d.ts +61 -0
- package/dist/validation-tools.js +36 -21
- package/migrations/005_provider_type_open_api_names.sql +28 -0
- package/npm-shrinkwrap.json +6 -5
- package/package.json +12 -9
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createCircuitBreaker, withRetry } from "./retry.js";
|
|
2
|
+
import { logWarn, noopLogger } from "./logger.js";
|
|
3
|
+
import { ApiHttpError, buildEndpointUrl, isHttpTransient, postJson, DEFAULT_API_TIMEOUT_MS, } from "./api-http.js";
|
|
4
|
+
function numberOrUndefined(value) {
|
|
5
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
6
|
+
}
|
|
7
|
+
function firstNumber(...candidates) {
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
const n = numberOrUndefined(candidate);
|
|
10
|
+
if (n !== undefined)
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
export class OpenAiCompatibleProvider {
|
|
16
|
+
name;
|
|
17
|
+
kind = "openai-compatible";
|
|
18
|
+
constructor(name) {
|
|
19
|
+
this.name = name;
|
|
20
|
+
}
|
|
21
|
+
endpointUrl(baseUrl) {
|
|
22
|
+
return buildEndpointUrl(baseUrl, "chat/completions");
|
|
23
|
+
}
|
|
24
|
+
buildBody(req) {
|
|
25
|
+
const body = {
|
|
26
|
+
model: req.model,
|
|
27
|
+
messages: req.messages.map(m => ({ role: m.role, content: m.content })),
|
|
28
|
+
};
|
|
29
|
+
if (req.maxOutputTokens !== undefined)
|
|
30
|
+
body.max_tokens = req.maxOutputTokens;
|
|
31
|
+
if (req.temperature !== undefined)
|
|
32
|
+
body.temperature = req.temperature;
|
|
33
|
+
if (req.topP !== undefined)
|
|
34
|
+
body.top_p = req.topP;
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
parseResult(httpStatus, body) {
|
|
38
|
+
const parsed = JSON.parse(body);
|
|
39
|
+
const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : undefined;
|
|
40
|
+
const text = typeof choice?.message?.content === "string" ? choice.message.content : "";
|
|
41
|
+
const usage = parsed?.usage ?? {};
|
|
42
|
+
return {
|
|
43
|
+
model: typeof parsed?.model === "string" ? parsed.model : "unknown",
|
|
44
|
+
text,
|
|
45
|
+
usage: {
|
|
46
|
+
inputTokens: firstNumber(usage.prompt_tokens, usage.input_tokens),
|
|
47
|
+
outputTokens: firstNumber(usage.completion_tokens, usage.output_tokens),
|
|
48
|
+
cacheReadTokens: firstNumber(usage?.prompt_tokens_details?.cached_tokens),
|
|
49
|
+
raw: usage,
|
|
50
|
+
},
|
|
51
|
+
raw: parsed,
|
|
52
|
+
httpStatus,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
authHeaders(apiKey) {
|
|
56
|
+
return apiKey ? { authorization: `Bearer ${apiKey}` } : {};
|
|
57
|
+
}
|
|
58
|
+
isTransient(err) {
|
|
59
|
+
return isHttpTransient(err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
63
|
+
const DEFAULT_ANTHROPIC_MAX_TOKENS = 4096;
|
|
64
|
+
export class AnthropicProvider {
|
|
65
|
+
name;
|
|
66
|
+
anthropicVersion;
|
|
67
|
+
kind = "anthropic";
|
|
68
|
+
constructor(name, anthropicVersion = DEFAULT_ANTHROPIC_VERSION) {
|
|
69
|
+
this.name = name;
|
|
70
|
+
this.anthropicVersion = anthropicVersion;
|
|
71
|
+
}
|
|
72
|
+
endpointUrl(baseUrl) {
|
|
73
|
+
return buildEndpointUrl(baseUrl, "messages");
|
|
74
|
+
}
|
|
75
|
+
buildBody(req) {
|
|
76
|
+
const system = req.messages
|
|
77
|
+
.filter(m => m.role === "system")
|
|
78
|
+
.map(m => m.content)
|
|
79
|
+
.join("\n\n");
|
|
80
|
+
const messages = req.messages
|
|
81
|
+
.filter(m => m.role !== "system")
|
|
82
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
83
|
+
const body = {
|
|
84
|
+
model: req.model,
|
|
85
|
+
messages,
|
|
86
|
+
max_tokens: req.maxOutputTokens ?? DEFAULT_ANTHROPIC_MAX_TOKENS,
|
|
87
|
+
};
|
|
88
|
+
if (system.length > 0)
|
|
89
|
+
body.system = system;
|
|
90
|
+
if (req.temperature !== undefined)
|
|
91
|
+
body.temperature = req.temperature;
|
|
92
|
+
if (req.topP !== undefined)
|
|
93
|
+
body.top_p = req.topP;
|
|
94
|
+
return body;
|
|
95
|
+
}
|
|
96
|
+
parseResult(httpStatus, body) {
|
|
97
|
+
const parsed = JSON.parse(body);
|
|
98
|
+
const blocks = Array.isArray(parsed?.content) ? parsed.content : [];
|
|
99
|
+
const text = blocks
|
|
100
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
101
|
+
.map((b) => b.text)
|
|
102
|
+
.join("");
|
|
103
|
+
const usage = parsed?.usage ?? {};
|
|
104
|
+
return {
|
|
105
|
+
model: typeof parsed?.model === "string" ? parsed.model : "unknown",
|
|
106
|
+
text,
|
|
107
|
+
usage: {
|
|
108
|
+
inputTokens: firstNumber(usage.input_tokens),
|
|
109
|
+
outputTokens: firstNumber(usage.output_tokens),
|
|
110
|
+
cacheReadTokens: firstNumber(usage.cache_read_input_tokens),
|
|
111
|
+
raw: usage,
|
|
112
|
+
},
|
|
113
|
+
raw: parsed,
|
|
114
|
+
httpStatus,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
authHeaders(apiKey) {
|
|
118
|
+
return { "x-api-key": apiKey, "anthropic-version": this.anthropicVersion };
|
|
119
|
+
}
|
|
120
|
+
isTransient(err) {
|
|
121
|
+
return isHttpTransient(err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function normalizeXaiCostUsd(usage) {
|
|
125
|
+
const ticks = usage?.cost_in_usd_ticks;
|
|
126
|
+
if (typeof ticks === "number" && Number.isFinite(ticks))
|
|
127
|
+
return ticks / 10_000_000_000;
|
|
128
|
+
const nanos = usage?.cost_in_nano_usd;
|
|
129
|
+
if (typeof nanos === "number" && Number.isFinite(nanos))
|
|
130
|
+
return nanos / 1_000_000_000;
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
function extractXaiResponseText(parsed) {
|
|
134
|
+
const output = Array.isArray(parsed?.output) ? parsed.output : [];
|
|
135
|
+
const chunks = [];
|
|
136
|
+
for (const item of output) {
|
|
137
|
+
if (item?.type !== "message" || !Array.isArray(item.content))
|
|
138
|
+
continue;
|
|
139
|
+
for (const content of item.content) {
|
|
140
|
+
if ((content?.type === "output_text" || content?.type === "text") &&
|
|
141
|
+
typeof content.text === "string") {
|
|
142
|
+
chunks.push(content.text);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (chunks.length > 0)
|
|
147
|
+
return chunks.join("");
|
|
148
|
+
if (typeof parsed?.output_text === "string")
|
|
149
|
+
return parsed.output_text;
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
export class XaiResponsesProvider {
|
|
153
|
+
name;
|
|
154
|
+
kind = "xai-responses";
|
|
155
|
+
constructor(name) {
|
|
156
|
+
this.name = name;
|
|
157
|
+
}
|
|
158
|
+
endpointUrl(baseUrl) {
|
|
159
|
+
return buildEndpointUrl(baseUrl, "responses");
|
|
160
|
+
}
|
|
161
|
+
buildBody(req) {
|
|
162
|
+
const instructions = req.messages
|
|
163
|
+
.filter(m => m.role === "system")
|
|
164
|
+
.map(m => m.content)
|
|
165
|
+
.join("\n\n");
|
|
166
|
+
const input = req.messages
|
|
167
|
+
.filter(m => m.role !== "system")
|
|
168
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
169
|
+
const body = { model: req.model, input, store: true };
|
|
170
|
+
if (instructions.length > 0)
|
|
171
|
+
body.instructions = instructions;
|
|
172
|
+
if (req.previousResponseId)
|
|
173
|
+
body.previous_response_id = req.previousResponseId;
|
|
174
|
+
if (req.maxOutputTokens !== undefined)
|
|
175
|
+
body.max_output_tokens = req.maxOutputTokens;
|
|
176
|
+
if (req.temperature !== undefined)
|
|
177
|
+
body.temperature = req.temperature;
|
|
178
|
+
if (req.topP !== undefined)
|
|
179
|
+
body.top_p = req.topP;
|
|
180
|
+
if (req.reasoningEffort !== undefined)
|
|
181
|
+
body.reasoning = { effort: req.reasoningEffort };
|
|
182
|
+
return body;
|
|
183
|
+
}
|
|
184
|
+
parseResult(httpStatus, body) {
|
|
185
|
+
const parsed = JSON.parse(body);
|
|
186
|
+
const usage = parsed?.usage ?? {};
|
|
187
|
+
return {
|
|
188
|
+
responseId: typeof parsed?.id === "string" ? parsed.id : null,
|
|
189
|
+
model: typeof parsed?.model === "string" ? parsed.model : "unknown",
|
|
190
|
+
text: extractXaiResponseText(parsed),
|
|
191
|
+
usage: {
|
|
192
|
+
inputTokens: firstNumber(usage.input_tokens, usage.prompt_tokens),
|
|
193
|
+
outputTokens: firstNumber(usage.output_tokens, usage.completion_tokens),
|
|
194
|
+
cacheReadTokens: firstNumber(usage?.input_tokens_details?.cached_tokens, usage?.prompt_tokens_details?.cached_tokens),
|
|
195
|
+
costUsd: normalizeXaiCostUsd(usage),
|
|
196
|
+
raw: usage,
|
|
197
|
+
},
|
|
198
|
+
raw: parsed,
|
|
199
|
+
httpStatus,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
authHeaders(apiKey) {
|
|
203
|
+
return { authorization: `Bearer ${apiKey}` };
|
|
204
|
+
}
|
|
205
|
+
isTransient(err) {
|
|
206
|
+
return isHttpTransient(err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function createApiProvider(name, kind) {
|
|
210
|
+
switch (kind) {
|
|
211
|
+
case "openai-compatible":
|
|
212
|
+
return new OpenAiCompatibleProvider(name);
|
|
213
|
+
case "anthropic":
|
|
214
|
+
return new AnthropicProvider(name);
|
|
215
|
+
case "xai-responses":
|
|
216
|
+
return new XaiResponsesProvider(name);
|
|
217
|
+
default: {
|
|
218
|
+
const exhaustive = kind;
|
|
219
|
+
throw new Error(`Unknown api provider kind: ${String(exhaustive)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const breakers = new Map();
|
|
224
|
+
function getProviderBreaker(name, logger) {
|
|
225
|
+
let breaker = breakers.get(name);
|
|
226
|
+
if (!breaker) {
|
|
227
|
+
breaker = createCircuitBreaker({
|
|
228
|
+
failureThreshold: 3,
|
|
229
|
+
resetTimeout: 60_000,
|
|
230
|
+
onStateChange: state => logWarn(logger, `[api:${name}] circuit breaker state changed to ${state}`),
|
|
231
|
+
});
|
|
232
|
+
breakers.set(name, breaker);
|
|
233
|
+
}
|
|
234
|
+
return breaker;
|
|
235
|
+
}
|
|
236
|
+
export function resetApiProviderBreakers() {
|
|
237
|
+
breakers.clear();
|
|
238
|
+
}
|
|
239
|
+
export function apiProviderBreakerState(name) {
|
|
240
|
+
return breakers.get(name)?.state ?? "CLOSED";
|
|
241
|
+
}
|
|
242
|
+
export async function runApiRequest(provider, req, logger = noopLogger, opts = {}) {
|
|
243
|
+
const url = provider.endpointUrl(req.baseUrl);
|
|
244
|
+
const body = provider.buildBody(req);
|
|
245
|
+
const headers = provider.authHeaders(req.apiKey);
|
|
246
|
+
const timeoutMs = req.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
|
|
247
|
+
const response = await withRetry(() => postJson(url, body, headers, timeoutMs, undefined, opts.signal), getProviderBreaker(provider.name, logger), {
|
|
248
|
+
initialDelay: 1_000,
|
|
249
|
+
maxDelay: 30_000,
|
|
250
|
+
factor: 2,
|
|
251
|
+
isTransient: err => provider.isTransient(err),
|
|
252
|
+
onRetry: (error, attempt, delay) => {
|
|
253
|
+
logWarn(logger, `[api:${provider.name}] transient failure on attempt ${attempt}; retrying in ${delay}ms: ${error.message}`);
|
|
254
|
+
},
|
|
255
|
+
}, logger);
|
|
256
|
+
return provider.parseResult(response.status, response.text);
|
|
257
|
+
}
|
|
258
|
+
export { ApiHttpError };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ApiProviderRuntime } from "./config.js";
|
|
2
|
+
import type { ApiChatMessage, ApiRequest } from "./api-provider.js";
|
|
3
|
+
export interface PrepareApiRequestParams {
|
|
4
|
+
prompt: string;
|
|
5
|
+
system?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
maxOutputTokens?: number;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
topP?: number;
|
|
10
|
+
reasoningEffort?: "none" | "low" | "medium" | "high";
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
previousResponseId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ApiProviderCatalogEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
providerKind: "api";
|
|
17
|
+
kind: ApiProviderRuntime["kind"];
|
|
18
|
+
defaultModel: string;
|
|
19
|
+
models: string[] | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function apiProviderCatalogEntry(runtime: ApiProviderRuntime): ApiProviderCatalogEntry;
|
|
22
|
+
export declare class ApiModelNotAllowedError extends Error {
|
|
23
|
+
readonly provider: string;
|
|
24
|
+
readonly model: string;
|
|
25
|
+
readonly allowed: readonly string[];
|
|
26
|
+
constructor(provider: string, model: string, allowed: readonly string[]);
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveApiModel(runtime: ApiProviderRuntime, requested?: string): string;
|
|
29
|
+
export declare function assembleApiMessages(prompt: string, system?: string): ApiChatMessage[];
|
|
30
|
+
export declare function prepareApiRequest(runtime: ApiProviderRuntime, params: PrepareApiRequestParams): ApiRequest;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function apiProviderCatalogEntry(runtime) {
|
|
2
|
+
return {
|
|
3
|
+
name: runtime.name,
|
|
4
|
+
providerKind: "api",
|
|
5
|
+
kind: runtime.kind,
|
|
6
|
+
defaultModel: runtime.defaultModel,
|
|
7
|
+
models: runtime.models ?? null,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export class ApiModelNotAllowedError extends Error {
|
|
11
|
+
provider;
|
|
12
|
+
model;
|
|
13
|
+
allowed;
|
|
14
|
+
constructor(provider, model, allowed) {
|
|
15
|
+
super(`Model "${model}" is not in the allowlist for provider "${provider}". Allowed: ${allowed.join(", ")}.`);
|
|
16
|
+
this.provider = provider;
|
|
17
|
+
this.model = model;
|
|
18
|
+
this.allowed = allowed;
|
|
19
|
+
this.name = "ApiModelNotAllowedError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function resolveApiModel(runtime, requested) {
|
|
23
|
+
if (!requested)
|
|
24
|
+
return runtime.defaultModel;
|
|
25
|
+
if (runtime.models && runtime.models.length > 0 && !runtime.models.includes(requested)) {
|
|
26
|
+
throw new ApiModelNotAllowedError(runtime.name, requested, runtime.models);
|
|
27
|
+
}
|
|
28
|
+
return requested;
|
|
29
|
+
}
|
|
30
|
+
export function assembleApiMessages(prompt, system) {
|
|
31
|
+
const messages = [];
|
|
32
|
+
if (system && system.trim().length > 0) {
|
|
33
|
+
messages.push({ role: "system", content: system });
|
|
34
|
+
}
|
|
35
|
+
messages.push({ role: "user", content: prompt });
|
|
36
|
+
return messages;
|
|
37
|
+
}
|
|
38
|
+
export function prepareApiRequest(runtime, params) {
|
|
39
|
+
return {
|
|
40
|
+
baseUrl: runtime.baseUrl,
|
|
41
|
+
apiKey: runtime.apiKey,
|
|
42
|
+
model: resolveApiModel(runtime, params.model),
|
|
43
|
+
messages: assembleApiMessages(params.prompt, params.system),
|
|
44
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
45
|
+
temperature: params.temperature,
|
|
46
|
+
topP: params.topP,
|
|
47
|
+
reasoningEffort: params.reasoningEffort,
|
|
48
|
+
timeoutMs: params.timeoutMs,
|
|
49
|
+
previousResponseId: params.previousResponseId,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { Logger } from "./logger.js";
|
|
|
2
2
|
import type { ReviewIntegrityResult } from "./review-integrity.js";
|
|
3
3
|
export type ApprovalPolicy = "strict" | "balanced" | "permissive";
|
|
4
4
|
export type ApprovalStrategy = "legacy" | "mcp_managed";
|
|
5
|
-
export type ApprovalCli = "claude" | "codex" | "gemini" | "grok" | "mistral";
|
|
5
|
+
export type ApprovalCli = "claude" | "codex" | "gemini" | "grok" | "mistral" | "devin";
|
|
6
6
|
export type ApprovalStatus = "approved" | "denied";
|
|
7
7
|
export interface ApprovalRequest {
|
|
8
8
|
cli: ApprovalCli;
|
package/dist/approval-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { noopLogger } from "./logger.js";
|
|
6
|
+
import { INTERNAL_MCP_REGISTRY } from "./mcp-registry.js";
|
|
6
7
|
import { isReviewContext } from "./review-integrity.js";
|
|
7
8
|
function parsePolicy(policy) {
|
|
8
9
|
if (policy) {
|
|
@@ -60,13 +61,11 @@ export class ApprovalManager {
|
|
|
60
61
|
score += 2;
|
|
61
62
|
reasons.push("Request combines full permission bypass with full-auto execution");
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
score += 1;
|
|
69
|
-
reasons.push("Request enables documentation retrieval MCP (ref_tools)");
|
|
64
|
+
for (const [name, entry] of Object.entries(INTERNAL_MCP_REGISTRY)) {
|
|
65
|
+
if (entry.approval && request.requestedMcpServers.includes(name)) {
|
|
66
|
+
score += entry.approval.score;
|
|
67
|
+
reasons.push(entry.approval.reason);
|
|
68
|
+
}
|
|
70
69
|
}
|
|
71
70
|
if (request.allowedTools && request.allowedTools.length === 0) {
|
|
72
71
|
const promptIsReview = isReviewContext(request.prompt);
|
|
@@ -2,7 +2,9 @@ import type { Logger } from "./logger.js";
|
|
|
2
2
|
import { type JobHealth } from "./process-monitor.js";
|
|
3
3
|
import { JobStore } from "./job-store.js";
|
|
4
4
|
import { type FlightRecorderLike } from "./flight-recorder.js";
|
|
5
|
-
|
|
5
|
+
import { type ApiProvider, type ApiRequest } from "./api-provider.js";
|
|
6
|
+
export type LlmCli = "claude" | "codex" | "gemini" | "grok" | "mistral" | "devin";
|
|
7
|
+
export type JobProvider = LlmCli | (string & {});
|
|
6
8
|
export type AsyncJobStatus = "running" | "completed" | "failed" | "canceled" | "orphaned";
|
|
7
9
|
export interface AsyncJobFlightRecorderEntry {
|
|
8
10
|
model: string;
|
|
@@ -22,7 +24,7 @@ export type AsyncJobUsageExtractor = (stdout: string) => {
|
|
|
22
24
|
};
|
|
23
25
|
export interface AsyncJobSnapshot {
|
|
24
26
|
id: string;
|
|
25
|
-
cli:
|
|
27
|
+
cli: JobProvider;
|
|
26
28
|
status: AsyncJobStatus;
|
|
27
29
|
startedAt: string;
|
|
28
30
|
finishedAt: string | null;
|
|
@@ -66,13 +68,26 @@ export declare class AsyncJobManager {
|
|
|
66
68
|
private processMonitor;
|
|
67
69
|
private store;
|
|
68
70
|
private flightRecorder;
|
|
69
|
-
constructor(logger?: Logger, onJobComplete?: ((cli:
|
|
71
|
+
constructor(logger?: Logger, onJobComplete?: ((cli: JobProvider, durationMs: number, success: boolean) => void) | undefined, store?: JobStore | null, flightRecorder?: FlightRecorderLike);
|
|
70
72
|
private buildOrphanFlightResult;
|
|
71
73
|
checkStalledJobs(now?: number): void;
|
|
72
74
|
hasStore(): boolean;
|
|
73
75
|
private emitMetrics;
|
|
74
76
|
private evictCompletedJobs;
|
|
75
77
|
private buildRequestKey;
|
|
78
|
+
private buildHttpRequestKey;
|
|
79
|
+
private tryReuseDedupedJob;
|
|
80
|
+
startHttpJob(params: {
|
|
81
|
+
provider: ApiProvider;
|
|
82
|
+
apiRequest: ApiRequest;
|
|
83
|
+
correlationId: string;
|
|
84
|
+
forceRefresh?: boolean;
|
|
85
|
+
onComplete?: () => void;
|
|
86
|
+
writeFlightStart?: boolean;
|
|
87
|
+
flightRecorderEntry?: AsyncJobFlightRecorderEntry;
|
|
88
|
+
extractUsage?: AsyncJobUsageExtractor;
|
|
89
|
+
}): StartJobOutcome;
|
|
90
|
+
private finalizeHttpJob;
|
|
76
91
|
private fireOnComplete;
|
|
77
92
|
private writeFlightComplete;
|
|
78
93
|
private safeExtractUsage;
|
|
@@ -105,7 +120,7 @@ export declare class AsyncJobManager {
|
|
|
105
120
|
jobs: JobHealth[];
|
|
106
121
|
};
|
|
107
122
|
getJobOutputFormat(jobId: string): string | undefined;
|
|
108
|
-
getJobCli(jobId: string):
|
|
123
|
+
getJobCli(jobId: string): JobProvider | undefined;
|
|
109
124
|
private snapshot;
|
|
110
125
|
private appendOutput;
|
|
111
126
|
}
|