@offerpilot/axiomruntime 0.0.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/README.md +185 -0
- package/dist/cli/commands/add.js +29 -0
- package/dist/cli/commands/context.js +29 -0
- package/dist/cli/commands/doctor.js +62 -0
- package/dist/cli/commands/edit.js +85 -0
- package/dist/cli/commands/help.js +16 -0
- package/dist/cli/commands/list.js +25 -0
- package/dist/cli/commands/log.js +63 -0
- package/dist/cli/commands/memory.js +241 -0
- package/dist/cli/commands/report.js +35 -0
- package/dist/cli/commands/session.js +74 -0
- package/dist/cli/commands/setup.js +86 -0
- package/dist/cli/commands/status.js +50 -0
- package/dist/cli/commands/telegram.js +701 -0
- package/dist/cli/commands/use.js +108 -0
- package/dist/cli/commands/version.js +12 -0
- package/dist/cli/index.js +276 -0
- package/dist/cli/output/table.js +22 -0
- package/dist/cli/prompts/prompt.js +37 -0
- package/dist/cli/registry.js +16 -0
- package/dist/core/config/cache-store.js +193 -0
- package/dist/core/config/json-store.js +114 -0
- package/dist/core/config/paths.js +85 -0
- package/dist/core/config/providers-store.js +89 -0
- package/dist/core/config/schema.js +60 -0
- package/dist/core/config/session-store.js +30 -0
- package/dist/core/config/usage-store.js +18 -0
- package/dist/core/context/context-service.js +186 -0
- package/dist/core/integrations/integration-state.js +105 -0
- package/dist/core/logs/log-service.js +56 -0
- package/dist/core/memory/embedding-check.js +121 -0
- package/dist/core/memory/memory-config.js +122 -0
- package/dist/core/models/model-discovery.js +430 -0
- package/dist/core/models/model-filter.js +13 -0
- package/dist/core/providers/provider-service.js +212 -0
- package/dist/core/reports/report-service.js +166 -0
- package/dist/core/runner/command-resolver.js +60 -0
- package/dist/core/runner/engine-registry.js +93 -0
- package/dist/core/runner/fallback.js +114 -0
- package/dist/core/runner/openai-usage-http.js +82 -0
- package/dist/core/runner/openai-usage-proxy.js +1 -0
- package/dist/core/runner/openai-usage-recording.js +172 -0
- package/dist/core/runner/openai-usage-responses.js +469 -0
- package/dist/core/runner/openai-usage-server.js +319 -0
- package/dist/core/runner/openai-usage-types.js +1 -0
- package/dist/core/runner/tool-runner.js +138 -0
- package/dist/core/sessions/session-service.js +47 -0
- package/dist/core/status/doctor-service.js +391 -0
- package/dist/core/status/status-service.js +60 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage/pricing.js +113 -0
- package/dist/core/usage/usage-service.js +30 -0
- package/dist/core/utils/is-record.js +3 -0
- package/dist/server/index.js +28 -0
- package/dist/server/runtime-server.js +430 -0
- package/dist/telegram/bot-registry.js +80 -0
- package/dist/telegram/bot.js +128 -0
- package/dist/telegram/config.js +235 -0
- package/dist/telegram/engine/claude-engine.js +240 -0
- package/dist/telegram/engine/codex-engine.js +437 -0
- package/dist/telegram/engine/engine-utils.js +67 -0
- package/dist/telegram/engine/process-utils.js +132 -0
- package/dist/telegram/engine/registry.js +31 -0
- package/dist/telegram/engine/types.js +1 -0
- package/dist/telegram/handler-registry.js +28 -0
- package/dist/telegram/handlers/callback.js +311 -0
- package/dist/telegram/handlers/command.js +272 -0
- package/dist/telegram/handlers/document.js +108 -0
- package/dist/telegram/handlers/memory.js +305 -0
- package/dist/telegram/handlers/message.js +701 -0
- package/dist/telegram/handlers/provider.js +332 -0
- package/dist/telegram/handlers/setup.js +527 -0
- package/dist/telegram/handlers/usage.js +124 -0
- package/dist/telegram/index.js +93 -0
- package/dist/telegram/interaction/approval.js +108 -0
- package/dist/telegram/interaction/command-menu.js +253 -0
- package/dist/telegram/interaction/formatter.js +487 -0
- package/dist/telegram/interaction/keyboards.js +145 -0
- package/dist/telegram/interaction/progress-reporter.js +160 -0
- package/dist/telegram/interaction/prompt-middleware.js +168 -0
- package/dist/telegram/interaction/result-store.js +41 -0
- package/dist/telegram/interaction/token-budget.js +21 -0
- package/dist/telegram/interaction/tool-name.js +41 -0
- package/dist/telegram/lifecycle-registry.js +47 -0
- package/dist/telegram/log.js +46 -0
- package/dist/telegram/memory/memory-inject.js +52 -0
- package/dist/telegram/memory/memory-service.js +413 -0
- package/dist/telegram/memory/memory-store.js +216 -0
- package/dist/telegram/memory/types.js +1 -0
- package/dist/telegram/network-retry.js +22 -0
- package/dist/telegram/network.js +53 -0
- package/dist/telegram/session/manager.js +229 -0
- package/dist/telegram/session/store.js +363 -0
- package/dist/telegram/session/types.js +1 -0
- package/dist/telegram/supervisor.js +57 -0
- package/dist/telegram/templates/messages.js +1 -0
- package/docs/README.md +98 -0
- package/docs/USAGE.html +853 -0
- package/package.json +57 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { Agent, fetch } from "undici";
|
|
3
|
+
import { writeLog } from "../logs/log-service.js";
|
|
4
|
+
import { buildUpstreamHeaders, buildUpstreamUrl, methodMayHaveBody, readRequestBody, streamResponse, writeResponseHead } from "./openai-usage-http.js";
|
|
5
|
+
import { buildChatCompletionsResponsesSse, isResponsesStreamRequest, shouldFallbackResponsesToChat } from "./openai-usage-responses.js";
|
|
6
|
+
import { extractRequestModel, recordUsageFromResponse } from "./openai-usage-recording.js";
|
|
7
|
+
import { getToolBaseUrl } from "./tool-runner.js";
|
|
8
|
+
const DEFAULT_UPSTREAM_HEADERS_TIMEOUT_MS = 45_000;
|
|
9
|
+
const MAX_CONSECUTIVE_HEADERS_TIMEOUTS = 2;
|
|
10
|
+
export async function startOpenAiUsageProxy(input) {
|
|
11
|
+
const upstreamBaseUrl = getToolBaseUrl("codex", input.provider);
|
|
12
|
+
const upstreamHeadersTimeoutMs = normalizePositiveTimeout(input.upstreamHeadersTimeoutMs, DEFAULT_UPSTREAM_HEADERS_TIMEOUT_MS);
|
|
13
|
+
// Codex already retries failed Responses requests. Give those retries fresh
|
|
14
|
+
// upstream connections so a stale gateway keep-alive socket cannot poison an
|
|
15
|
+
// entire turn with immediate fetch failures.
|
|
16
|
+
const upstreamDispatcher = new Agent({
|
|
17
|
+
connections: 4,
|
|
18
|
+
pipelining: 0,
|
|
19
|
+
headersTimeout: upstreamHeadersTimeoutMs,
|
|
20
|
+
bodyTimeout: 0
|
|
21
|
+
});
|
|
22
|
+
const records = [];
|
|
23
|
+
const metrics = createEmptyMetrics();
|
|
24
|
+
let consecutiveHeadersTimeouts = 0;
|
|
25
|
+
const server = http.createServer(async (req, res) => {
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
let requestModel = input.model;
|
|
28
|
+
let requestBytes = 0;
|
|
29
|
+
let requestShape = {};
|
|
30
|
+
let upstreamPath = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
31
|
+
metrics.requestCount += 1;
|
|
32
|
+
let finalized = false;
|
|
33
|
+
const finalizeMetrics = (success, upstreamStatus) => {
|
|
34
|
+
if (finalized)
|
|
35
|
+
return;
|
|
36
|
+
finalized = true;
|
|
37
|
+
if (success) {
|
|
38
|
+
metrics.successCount += 1;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
metrics.failureCount += 1;
|
|
42
|
+
}
|
|
43
|
+
metrics.totalDurationMs += Date.now() - startedAt;
|
|
44
|
+
if (upstreamStatus !== undefined) {
|
|
45
|
+
incrementStatusCount(metrics, upstreamStatus);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const requestBody = await readRequestBody(req);
|
|
50
|
+
requestModel = extractRequestModel(requestBody) ?? input.model;
|
|
51
|
+
requestBytes = requestBody.length;
|
|
52
|
+
requestShape = summarizeRequestShape(requestBody);
|
|
53
|
+
const responsesStream = isResponsesStreamRequest(req.url ?? "/", requestBody);
|
|
54
|
+
void writeLog({
|
|
55
|
+
category: "usage",
|
|
56
|
+
action: "openai_usage_proxy_request",
|
|
57
|
+
message: `Proxying ${input.tool} request for ${input.provider.name} / ${requestModel}`,
|
|
58
|
+
metadata: {
|
|
59
|
+
tool: input.tool,
|
|
60
|
+
provider: input.provider.name,
|
|
61
|
+
model: requestModel,
|
|
62
|
+
path: new URL(req.url ?? "/", "http://127.0.0.1").pathname
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const upstreamUrl = buildUpstreamUrl(upstreamBaseUrl, req.url ?? "/");
|
|
66
|
+
upstreamPath = new URL(upstreamUrl).pathname;
|
|
67
|
+
if (consecutiveHeadersTimeouts >= MAX_CONSECUTIVE_HEADERS_TIMEOUTS) {
|
|
68
|
+
await writeLog({
|
|
69
|
+
level: "warn",
|
|
70
|
+
category: "usage",
|
|
71
|
+
action: "openai_usage_proxy_timeout_short_circuit",
|
|
72
|
+
message: `Skipping another stalled upstream attempt for ${input.provider.name} / ${requestModel}.`,
|
|
73
|
+
metadata: {
|
|
74
|
+
tool: input.tool,
|
|
75
|
+
provider: input.provider.name,
|
|
76
|
+
model: requestModel,
|
|
77
|
+
upstreamPath,
|
|
78
|
+
requestBytes,
|
|
79
|
+
...requestShape,
|
|
80
|
+
timeoutMs: upstreamHeadersTimeoutMs,
|
|
81
|
+
consecutiveTimeouts: consecutiveHeadersTimeouts
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
sendProxyError(res, 504, "AI Gateway timed out waiting for provider response headers.");
|
|
85
|
+
finalizeMetrics(false, "headers_timeout_short_circuit");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const upstreamResponse = await fetch(upstreamUrl, {
|
|
89
|
+
method: req.method,
|
|
90
|
+
headers: buildUpstreamHeaders(req.headers, input.provider.apiKey),
|
|
91
|
+
body: requestBody.length && methodMayHaveBody(req.method) ? requestBody : undefined,
|
|
92
|
+
dispatcher: upstreamDispatcher
|
|
93
|
+
});
|
|
94
|
+
consecutiveHeadersTimeouts = 0;
|
|
95
|
+
// Native Responses is attempted first. Only when the upstream cannot speak
|
|
96
|
+
// the Responses protocol do we translate the request through chat
|
|
97
|
+
// completions; transient/no-channel errors are passed through untouched so
|
|
98
|
+
// provider rotation can react to them.
|
|
99
|
+
if (responsesStream && !isUsableEventStream(upstreamResponse)) {
|
|
100
|
+
const nativeBody = await upstreamResponse.text().catch(() => "");
|
|
101
|
+
const outcome = {
|
|
102
|
+
status: upstreamResponse.status,
|
|
103
|
+
contentType: upstreamResponse.headers.get("content-type") ?? "",
|
|
104
|
+
bodyText: nativeBody
|
|
105
|
+
};
|
|
106
|
+
if (shouldFallbackResponsesToChat(outcome)) {
|
|
107
|
+
void writeLog({
|
|
108
|
+
category: "usage",
|
|
109
|
+
action: "openai_usage_proxy_responses_chat_fallback",
|
|
110
|
+
message: `Responses protocol is unsupported for ${input.provider.name} / ${requestModel}; falling back to chat completions.`,
|
|
111
|
+
metadata: {
|
|
112
|
+
tool: input.tool,
|
|
113
|
+
provider: input.provider.name,
|
|
114
|
+
model: requestModel,
|
|
115
|
+
status: outcome.status
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
await sendChatCompletionsResponsesSse({
|
|
119
|
+
upstreamBaseUrl,
|
|
120
|
+
requestBody,
|
|
121
|
+
apiKey: input.provider.apiKey,
|
|
122
|
+
dispatcher: upstreamDispatcher,
|
|
123
|
+
requestModel,
|
|
124
|
+
provider: input.provider.name,
|
|
125
|
+
tool: input.tool,
|
|
126
|
+
records,
|
|
127
|
+
onUsage: input.onUsage,
|
|
128
|
+
metrics,
|
|
129
|
+
res
|
|
130
|
+
}).then((result) => finalizeMetrics(result.status >= 200 && result.status < 400, result.status));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Not a protocol problem (e.g. 503 no-channel): pass the native error
|
|
134
|
+
// through so the caller and provider rotation see the real cause.
|
|
135
|
+
res.statusCode = outcome.status;
|
|
136
|
+
res.statusMessage = upstreamResponse.statusText;
|
|
137
|
+
res.setHeader("content-type", outcome.contentType || "application/json; charset=utf-8");
|
|
138
|
+
const recordedUsage = await recordUsageFromResponse({
|
|
139
|
+
text: nativeBody,
|
|
140
|
+
contentType: outcome.contentType,
|
|
141
|
+
fallbackModel: requestModel,
|
|
142
|
+
provider: input.provider.name,
|
|
143
|
+
tool: input.tool,
|
|
144
|
+
records,
|
|
145
|
+
onUsage: input.onUsage
|
|
146
|
+
});
|
|
147
|
+
recordUsageMetric(metrics, recordedUsage);
|
|
148
|
+
res.end(nativeBody);
|
|
149
|
+
finalizeMetrics(outcome.status >= 200 && outcome.status < 400, outcome.status);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
writeResponseHead(res, upstreamResponse);
|
|
153
|
+
const captured = await streamResponse(upstreamResponse, res);
|
|
154
|
+
const recordedUsage = await recordUsageFromResponse({
|
|
155
|
+
text: captured.toString("utf8"),
|
|
156
|
+
contentType: upstreamResponse.headers.get("content-type") ?? "",
|
|
157
|
+
fallbackModel: requestModel,
|
|
158
|
+
provider: input.provider.name,
|
|
159
|
+
tool: input.tool,
|
|
160
|
+
records,
|
|
161
|
+
onUsage: input.onUsage
|
|
162
|
+
});
|
|
163
|
+
recordUsageMetric(metrics, recordedUsage);
|
|
164
|
+
res.end();
|
|
165
|
+
finalizeMetrics(upstreamResponse.status >= 200 && upstreamResponse.status < 400, upstreamResponse.status);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const diagnostic = summarizeUpstreamError(error);
|
|
169
|
+
const headersTimedOut = diagnostic.code === "UND_ERR_HEADERS_TIMEOUT";
|
|
170
|
+
consecutiveHeadersTimeouts = headersTimedOut ? consecutiveHeadersTimeouts + 1 : 0;
|
|
171
|
+
await writeLog({
|
|
172
|
+
level: "error",
|
|
173
|
+
category: "usage",
|
|
174
|
+
action: "openai_usage_proxy_upstream_error",
|
|
175
|
+
message: `OpenAI upstream request failed for ${input.provider.name} / ${requestModel}: ${diagnostic.message}`,
|
|
176
|
+
metadata: {
|
|
177
|
+
tool: input.tool,
|
|
178
|
+
provider: input.provider.name,
|
|
179
|
+
model: requestModel,
|
|
180
|
+
upstreamPath,
|
|
181
|
+
requestBytes,
|
|
182
|
+
...requestShape,
|
|
183
|
+
errorName: diagnostic.name,
|
|
184
|
+
errorCode: diagnostic.code,
|
|
185
|
+
errorSyscall: diagnostic.syscall,
|
|
186
|
+
timeoutMs: headersTimedOut ? upstreamHeadersTimeoutMs : null,
|
|
187
|
+
consecutiveTimeouts: headersTimedOut ? consecutiveHeadersTimeouts : 0
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const code = diagnostic.code ? ` (${diagnostic.code})` : "";
|
|
191
|
+
sendProxyError(res, headersTimedOut ? 504 : 502, `AI Gateway proxy failed: ${diagnostic.message}${code}`);
|
|
192
|
+
finalizeMetrics(false, headersTimedOut ? "headers_timeout" : "proxy_error");
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
await new Promise((resolve, reject) => {
|
|
196
|
+
const onListenError = (error) => {
|
|
197
|
+
void upstreamDispatcher.close();
|
|
198
|
+
reject(error);
|
|
199
|
+
};
|
|
200
|
+
server.once("error", onListenError);
|
|
201
|
+
server.listen(0, "127.0.0.1", () => {
|
|
202
|
+
server.off("error", onListenError);
|
|
203
|
+
resolve();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
const address = server.address();
|
|
207
|
+
if (!address || typeof address === "string") {
|
|
208
|
+
throw new Error("Failed to start AI Gateway OpenAI proxy.");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
baseUrl: `http://127.0.0.1:${address.port}/v1`,
|
|
212
|
+
async close() {
|
|
213
|
+
const closeServer = server.listening
|
|
214
|
+
? new Promise((resolve, reject) => {
|
|
215
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
216
|
+
})
|
|
217
|
+
: Promise.resolve();
|
|
218
|
+
await Promise.all([closeServer, upstreamDispatcher.close()]);
|
|
219
|
+
},
|
|
220
|
+
getRecords() {
|
|
221
|
+
return [...records];
|
|
222
|
+
},
|
|
223
|
+
getMetrics() {
|
|
224
|
+
return snapshotMetrics(metrics);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function normalizePositiveTimeout(value, fallback) {
|
|
229
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
230
|
+
? Math.floor(value)
|
|
231
|
+
: fallback;
|
|
232
|
+
}
|
|
233
|
+
function sendProxyError(res, status, message) {
|
|
234
|
+
res.statusCode = status;
|
|
235
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
236
|
+
res.end(JSON.stringify({ error: { message } }));
|
|
237
|
+
}
|
|
238
|
+
function summarizeRequestShape(requestBody) {
|
|
239
|
+
try {
|
|
240
|
+
const body = JSON.parse(requestBody.toString("utf8"));
|
|
241
|
+
return {
|
|
242
|
+
stream: body.stream === true,
|
|
243
|
+
inputItems: Array.isArray(body.input) ? body.input.length : typeof body.input === "string" ? 1 : 0,
|
|
244
|
+
toolCount: Array.isArray(body.tools) ? body.tools.length : 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return { stream: null, inputItems: null, toolCount: null };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function summarizeUpstreamError(error) {
|
|
252
|
+
const top = error instanceof Error ? error : new Error(String(error));
|
|
253
|
+
const cause = top.cause && typeof top.cause === "object" ? top.cause : null;
|
|
254
|
+
return {
|
|
255
|
+
name: cause && typeof cause.name === "string" ? cause.name : top.name,
|
|
256
|
+
message: cause && typeof cause.message === "string" ? cause.message : top.message,
|
|
257
|
+
code: cause && typeof cause.code === "string" ? cause.code : null,
|
|
258
|
+
syscall: cause && typeof cause.syscall === "string" ? cause.syscall : null
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function sendChatCompletionsResponsesSse(input) {
|
|
262
|
+
const response = await buildChatCompletionsResponsesSse({
|
|
263
|
+
upstreamBaseUrl: input.upstreamBaseUrl,
|
|
264
|
+
requestBody: input.requestBody,
|
|
265
|
+
apiKey: input.apiKey,
|
|
266
|
+
dispatcher: input.dispatcher
|
|
267
|
+
});
|
|
268
|
+
input.res.statusCode = response.status;
|
|
269
|
+
input.res.statusMessage = response.statusText;
|
|
270
|
+
input.res.setHeader("content-type", response.contentType);
|
|
271
|
+
const recordedUsage = await recordUsageFromResponse({
|
|
272
|
+
text: response.body.toString("utf8"),
|
|
273
|
+
contentType: response.contentType,
|
|
274
|
+
fallbackModel: input.requestModel,
|
|
275
|
+
provider: input.provider,
|
|
276
|
+
tool: input.tool,
|
|
277
|
+
records: input.records,
|
|
278
|
+
onUsage: input.onUsage
|
|
279
|
+
});
|
|
280
|
+
recordUsageMetric(input.metrics, recordedUsage);
|
|
281
|
+
input.res.end(response.body);
|
|
282
|
+
return { status: response.status };
|
|
283
|
+
}
|
|
284
|
+
function isUsableEventStream(response) {
|
|
285
|
+
if (!response.ok)
|
|
286
|
+
return false;
|
|
287
|
+
return (response.headers.get("content-type") ?? "").toLowerCase().includes("text/event-stream");
|
|
288
|
+
}
|
|
289
|
+
function createEmptyMetrics() {
|
|
290
|
+
return {
|
|
291
|
+
requestCount: 0,
|
|
292
|
+
successCount: 0,
|
|
293
|
+
failureCount: 0,
|
|
294
|
+
noUsageCount: 0,
|
|
295
|
+
usageRecordCount: 0,
|
|
296
|
+
totalDurationMs: 0,
|
|
297
|
+
averageDurationMs: 0,
|
|
298
|
+
upstreamStatusCounts: {}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function recordUsageMetric(metrics, recordedUsage) {
|
|
302
|
+
if (recordedUsage) {
|
|
303
|
+
metrics.usageRecordCount += 1;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
metrics.noUsageCount += 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function incrementStatusCount(metrics, status) {
|
|
310
|
+
const key = String(status);
|
|
311
|
+
metrics.upstreamStatusCounts[key] = (metrics.upstreamStatusCounts[key] ?? 0) + 1;
|
|
312
|
+
}
|
|
313
|
+
function snapshotMetrics(metrics) {
|
|
314
|
+
return {
|
|
315
|
+
...metrics,
|
|
316
|
+
averageDurationMs: metrics.requestCount ? metrics.totalDurationMs / metrics.requestCount : 0,
|
|
317
|
+
upstreamStatusCounts: { ...metrics.upstreamStatusCounts }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { saveLastSelection } from "../config/cache-store.js";
|
|
6
|
+
import { writeLog } from "../logs/log-service.js";
|
|
7
|
+
import { isRecord } from "../utils/is-record.js";
|
|
8
|
+
import { resolveToolCommand } from "./command-resolver.js";
|
|
9
|
+
import { getRunnerEngine } from "./engine-registry.js";
|
|
10
|
+
import { finishSession, startSession } from "../sessions/session-service.js";
|
|
11
|
+
export async function runTool(tool, provider, model, options = {}) {
|
|
12
|
+
await saveLastSelection(tool, provider.name, model);
|
|
13
|
+
const command = resolveToolCommand(tool);
|
|
14
|
+
if (!command) {
|
|
15
|
+
throw new Error(`Failed to find ${tool} CLI. Run \`ai doctor\` to check local CLI installation.`);
|
|
16
|
+
}
|
|
17
|
+
const engine = getRunnerEngine(tool);
|
|
18
|
+
const env = buildRunnerEnv(tool, provider, model);
|
|
19
|
+
const temporaryFiles = [];
|
|
20
|
+
const claudeSettingsPath = engine.requiresSettingsFile ? writeTemporaryClaudeSettings(provider) : null;
|
|
21
|
+
if (claudeSettingsPath)
|
|
22
|
+
temporaryFiles.push(claudeSettingsPath);
|
|
23
|
+
const args = buildRunnerArgs(tool, provider, model, claudeSettingsPath);
|
|
24
|
+
const effectiveBaseUrl = getToolBaseUrl(tool, provider);
|
|
25
|
+
await writeLog({
|
|
26
|
+
category: "usage",
|
|
27
|
+
action: "tool_spawn",
|
|
28
|
+
message: `Spawning ${tool}: ${provider.name} / ${model}`,
|
|
29
|
+
metadata: {
|
|
30
|
+
tool,
|
|
31
|
+
provider: provider.name,
|
|
32
|
+
model,
|
|
33
|
+
mode: provider.mode,
|
|
34
|
+
baseUrl: effectiveBaseUrl,
|
|
35
|
+
configuredBaseUrl: provider.baseUrl,
|
|
36
|
+
apiKeyFingerprint: getSecretFingerprint(provider.apiKey),
|
|
37
|
+
command,
|
|
38
|
+
args,
|
|
39
|
+
authInjection: engine.authInjection
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const session = await startSession({
|
|
43
|
+
tool,
|
|
44
|
+
provider,
|
|
45
|
+
model,
|
|
46
|
+
projectPath: process.cwd(),
|
|
47
|
+
fallbackFrom: options.fallbackFrom ?? null
|
|
48
|
+
});
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const child = spawn(command, args, {
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env
|
|
53
|
+
});
|
|
54
|
+
child.on("error", (error) => {
|
|
55
|
+
cleanupTemporaryFiles(temporaryFiles);
|
|
56
|
+
void finishSession(session.id, 1);
|
|
57
|
+
reject(new Error(`Failed to start ${command}. Run \`ai doctor\` to check local CLI installation. ${error.message}`));
|
|
58
|
+
});
|
|
59
|
+
child.on("close", (code) => {
|
|
60
|
+
cleanupTemporaryFiles(temporaryFiles);
|
|
61
|
+
const exitCode = code ?? 0;
|
|
62
|
+
void finishSession(session.id, exitCode).finally(() => resolve(exitCode));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function getSecretFingerprint(value) {
|
|
67
|
+
if (!value)
|
|
68
|
+
return "(empty)";
|
|
69
|
+
if (value.length <= 12)
|
|
70
|
+
return `len=${value.length}`;
|
|
71
|
+
return `${value.slice(0, 8)}...${value.slice(-6)} len=${value.length}`;
|
|
72
|
+
}
|
|
73
|
+
export function buildRunnerArgs(tool, provider, model, claudeSettingsPath) {
|
|
74
|
+
return getRunnerEngine(tool).buildArgs({ provider, model, settingsPath: claudeSettingsPath });
|
|
75
|
+
}
|
|
76
|
+
const AUTO_PERMISSIONS = {
|
|
77
|
+
defaultMode: "bypassPermissions",
|
|
78
|
+
allow: [
|
|
79
|
+
"Bash",
|
|
80
|
+
"Edit",
|
|
81
|
+
"Write",
|
|
82
|
+
"Read",
|
|
83
|
+
"Glob",
|
|
84
|
+
"Grep",
|
|
85
|
+
"WebFetch",
|
|
86
|
+
"WebSearch",
|
|
87
|
+
"MultiEdit",
|
|
88
|
+
"NotebookEdit"
|
|
89
|
+
],
|
|
90
|
+
deny: []
|
|
91
|
+
};
|
|
92
|
+
const READ_ONLY_PERMISSIONS = {
|
|
93
|
+
defaultMode: "default",
|
|
94
|
+
allow: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
|
|
95
|
+
deny: ["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]
|
|
96
|
+
};
|
|
97
|
+
export function writeTemporaryClaudeSettings(provider, options = {}) {
|
|
98
|
+
const userSettingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
99
|
+
const settings = readClaudeSettings(userSettingsPath);
|
|
100
|
+
const settingsEnv = isRecord(settings.env) ? settings.env : {};
|
|
101
|
+
const baseUrl = getToolBaseUrl("claude", provider);
|
|
102
|
+
settings.env = {
|
|
103
|
+
...settingsEnv,
|
|
104
|
+
ANTHROPIC_API_KEY: provider.apiKey,
|
|
105
|
+
ANTHROPIC_BASE_URL: baseUrl
|
|
106
|
+
};
|
|
107
|
+
if (options.permissionMode === "readOnly") {
|
|
108
|
+
settings.permissions = { ...READ_ONLY_PERMISSIONS };
|
|
109
|
+
}
|
|
110
|
+
else if (provider.mode === "auto") {
|
|
111
|
+
settings.permissions = { ...AUTO_PERMISSIONS };
|
|
112
|
+
}
|
|
113
|
+
const settingsPath = path.join(os.tmpdir(), `ai-gateway-claude-settings-${process.pid}-${Date.now()}.json`);
|
|
114
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
115
|
+
return settingsPath;
|
|
116
|
+
}
|
|
117
|
+
function readClaudeSettings(filePath) {
|
|
118
|
+
if (!fs.existsSync(filePath)) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
122
|
+
}
|
|
123
|
+
export function cleanupTemporaryFiles(filePaths) {
|
|
124
|
+
for (const filePath of filePaths) {
|
|
125
|
+
try {
|
|
126
|
+
fs.unlinkSync(filePath);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Best-effort cleanup for temporary launch settings.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function buildRunnerEnv(tool, provider, model) {
|
|
134
|
+
return getRunnerEngine(tool).buildEnv(provider, model, process.env);
|
|
135
|
+
}
|
|
136
|
+
export function getToolBaseUrl(tool, provider) {
|
|
137
|
+
return getRunnerEngine(tool).normalizeBaseUrl(provider.baseUrl);
|
|
138
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { addSession, clearSessions, readSessions, updateSession } from "../config/session-store.js";
|
|
3
|
+
export async function startSession(input) {
|
|
4
|
+
const now = new Date();
|
|
5
|
+
const session = {
|
|
6
|
+
id: buildSessionId(now),
|
|
7
|
+
tool: input.tool,
|
|
8
|
+
provider: input.provider.name,
|
|
9
|
+
model: input.model,
|
|
10
|
+
mode: input.provider.mode,
|
|
11
|
+
projectPath: input.projectPath,
|
|
12
|
+
startedAt: now.toISOString(),
|
|
13
|
+
endedAt: null,
|
|
14
|
+
status: "running",
|
|
15
|
+
exitCode: null,
|
|
16
|
+
fallbackFrom: input.fallbackFrom ?? null
|
|
17
|
+
};
|
|
18
|
+
await addSession(session);
|
|
19
|
+
return session;
|
|
20
|
+
}
|
|
21
|
+
export async function finishSession(sessionId, exitCode) {
|
|
22
|
+
await updateSession(sessionId, {
|
|
23
|
+
endedAt: new Date().toISOString(),
|
|
24
|
+
status: exitCode === 0 ? "completed" : "failed",
|
|
25
|
+
exitCode
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function listRecentSessions(limit = 20) {
|
|
29
|
+
const file = await readSessions();
|
|
30
|
+
return [...file.sessions]
|
|
31
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
32
|
+
.slice(0, limit);
|
|
33
|
+
}
|
|
34
|
+
export async function getSession(id) {
|
|
35
|
+
const file = await readSessions();
|
|
36
|
+
return file.sessions.find((session) => session.id === id) ?? null;
|
|
37
|
+
}
|
|
38
|
+
export async function getLastSession() {
|
|
39
|
+
return (await listRecentSessions(1))[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
export async function clearAllSessions() {
|
|
42
|
+
return clearSessions();
|
|
43
|
+
}
|
|
44
|
+
function buildSessionId(date) {
|
|
45
|
+
const stamp = date.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "_");
|
|
46
|
+
return `ses_${stamp}_${randomBytes(3).toString("hex")}`;
|
|
47
|
+
}
|