@offerpilot/axiomruntime 0.0.2 → 0.0.4
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 +20 -29
- package/dist/cli/commands/image.js +75 -0
- package/dist/cli/commands/telegram.js +4 -3
- package/dist/cli/index.js +11 -2
- package/dist/core/context/context-service.js +3 -3
- package/dist/core/images/image-generation-service.js +258 -0
- package/dist/core/integrations/codex-provider-config.js +3 -2
- package/dist/core/runner/engine-registry.js +0 -2
- package/dist/core/runner/openai-usage-http.js +55 -12
- package/dist/core/runner/openai-usage-proxy.js +1 -0
- package/dist/core/runner/openai-usage-recording.js +38 -6
- package/dist/core/runner/openai-usage-server.js +14 -6
- package/dist/core/runner/openai-usage-summary.js +18 -0
- package/dist/core/runner/tool-runner.js +88 -41
- package/dist/telegram/engine/claude-engine.js +36 -34
- package/dist/telegram/engine/codex-engine.js +18 -46
- package/dist/telegram/engine/engine-utils.js +33 -17
- package/docs/USAGE.html +24 -2
- package/package.json +1 -1
- package/docs/README.md +0 -98
|
@@ -59,15 +59,47 @@ export async function recordUsageFromResponse(input) {
|
|
|
59
59
|
}
|
|
60
60
|
function extractBestUsageCandidate(text, contentType, fallbackModel) {
|
|
61
61
|
const values = contentType.includes("text/event-stream") ? parseSseValues(text) : parseJsonValues(text);
|
|
62
|
-
|
|
62
|
+
const candidates = [];
|
|
63
63
|
for (const value of values) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
candidates.push(...collectUsageCandidates(value, fallbackModel));
|
|
65
|
+
}
|
|
66
|
+
return mergeUsageCandidates(candidates, fallbackModel);
|
|
67
|
+
}
|
|
68
|
+
function mergeUsageCandidates(candidates, fallbackModel) {
|
|
69
|
+
if (!candidates.length)
|
|
70
|
+
return null;
|
|
71
|
+
let inputTokens = 0;
|
|
72
|
+
let outputTokens = 0;
|
|
73
|
+
let totalTokens = 0;
|
|
74
|
+
let cacheReadInputTokens;
|
|
75
|
+
let model = fallbackModel;
|
|
76
|
+
let modelCandidateTokens = -1;
|
|
77
|
+
let costUsd;
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
inputTokens = Math.max(inputTokens, candidate.usage.inputTokens);
|
|
80
|
+
outputTokens = Math.max(outputTokens, candidate.usage.outputTokens);
|
|
81
|
+
totalTokens = Math.max(totalTokens, candidate.usage.totalTokens);
|
|
82
|
+
if (candidate.usage.cacheReadInputTokens !== undefined) {
|
|
83
|
+
cacheReadInputTokens = Math.max(cacheReadInputTokens ?? 0, candidate.usage.cacheReadInputTokens);
|
|
84
|
+
}
|
|
85
|
+
if (candidate.usage.totalTokens >= modelCandidateTokens) {
|
|
86
|
+
model = candidate.model;
|
|
87
|
+
modelCandidateTokens = candidate.usage.totalTokens;
|
|
88
|
+
}
|
|
89
|
+
if (candidate.costUsd !== undefined) {
|
|
90
|
+
costUsd = Math.max(costUsd ?? 0, candidate.costUsd);
|
|
68
91
|
}
|
|
69
92
|
}
|
|
70
|
-
return
|
|
93
|
+
return {
|
|
94
|
+
model,
|
|
95
|
+
usage: {
|
|
96
|
+
inputTokens,
|
|
97
|
+
outputTokens,
|
|
98
|
+
totalTokens: Math.max(totalTokens, inputTokens + outputTokens),
|
|
99
|
+
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {})
|
|
100
|
+
},
|
|
101
|
+
...(costUsd !== undefined ? { costUsd } : {})
|
|
102
|
+
};
|
|
71
103
|
}
|
|
72
104
|
function parseJsonValues(text) {
|
|
73
105
|
const trimmed = text.trim();
|
|
@@ -8,7 +8,8 @@ import { getToolBaseUrl } from "./tool-runner.js";
|
|
|
8
8
|
const DEFAULT_UPSTREAM_HEADERS_TIMEOUT_MS = 45_000;
|
|
9
9
|
const MAX_CONSECUTIVE_HEADERS_TIMEOUTS = 2;
|
|
10
10
|
export async function startOpenAiUsageProxy(input) {
|
|
11
|
-
const
|
|
11
|
+
const engine = input.engine ?? (input.tool === "claude" ? "claude" : "codex");
|
|
12
|
+
const upstreamBaseUrl = getToolBaseUrl(engine, input.provider);
|
|
12
13
|
const upstreamHeadersTimeoutMs = normalizePositiveTimeout(input.upstreamHeadersTimeoutMs, DEFAULT_UPSTREAM_HEADERS_TIMEOUT_MS);
|
|
13
14
|
// Codex already retries failed Responses requests. Give those retries fresh
|
|
14
15
|
// upstream connections so a stale gateway keep-alive socket cannot poison an
|
|
@@ -50,7 +51,7 @@ export async function startOpenAiUsageProxy(input) {
|
|
|
50
51
|
requestModel = extractRequestModel(requestBody) ?? input.model;
|
|
51
52
|
requestBytes = requestBody.length;
|
|
52
53
|
requestShape = summarizeRequestShape(requestBody);
|
|
53
|
-
const responsesStream = isResponsesStreamRequest(req.url ?? "/", requestBody);
|
|
54
|
+
const responsesStream = engine === "codex" && isResponsesStreamRequest(req.url ?? "/", requestBody);
|
|
54
55
|
void writeLog({
|
|
55
56
|
category: "usage",
|
|
56
57
|
action: "openai_usage_proxy_request",
|
|
@@ -62,7 +63,9 @@ export async function startOpenAiUsageProxy(input) {
|
|
|
62
63
|
path: new URL(req.url ?? "/", "http://127.0.0.1").pathname
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
|
-
const upstreamUrl = buildUpstreamUrl(upstreamBaseUrl, req.url ?? "/"
|
|
66
|
+
const upstreamUrl = buildUpstreamUrl(upstreamBaseUrl, req.url ?? "/", {
|
|
67
|
+
stripV1Prefix: engine === "codex"
|
|
68
|
+
});
|
|
66
69
|
upstreamPath = new URL(upstreamUrl).pathname;
|
|
67
70
|
if (consecutiveHeadersTimeouts >= MAX_CONSECUTIVE_HEADERS_TIMEOUTS) {
|
|
68
71
|
await writeLog({
|
|
@@ -87,7 +90,10 @@ export async function startOpenAiUsageProxy(input) {
|
|
|
87
90
|
}
|
|
88
91
|
const upstreamResponse = await fetch(upstreamUrl, {
|
|
89
92
|
method: req.method,
|
|
90
|
-
headers: buildUpstreamHeaders(req.headers, input.provider.apiKey
|
|
93
|
+
headers: buildUpstreamHeaders(req.headers, input.provider.apiKey, {
|
|
94
|
+
includeAnthropicApiKey: engine === "claude",
|
|
95
|
+
includeAuthorization: engine !== "claude"
|
|
96
|
+
}),
|
|
91
97
|
body: requestBody.length && methodMayHaveBody(req.method) ? requestBody : undefined,
|
|
92
98
|
dispatcher: upstreamDispatcher
|
|
93
99
|
});
|
|
@@ -150,7 +156,9 @@ export async function startOpenAiUsageProxy(input) {
|
|
|
150
156
|
return;
|
|
151
157
|
}
|
|
152
158
|
writeResponseHead(res, upstreamResponse);
|
|
153
|
-
const captured = await streamResponse(upstreamResponse, res
|
|
159
|
+
const captured = await streamResponse(upstreamResponse, res, {
|
|
160
|
+
maxCaptureBytes: input.tool === "image" ? 32 * 1024 * 1024 : undefined
|
|
161
|
+
});
|
|
154
162
|
const recordedUsage = await recordUsageFromResponse({
|
|
155
163
|
text: captured.toString("utf8"),
|
|
156
164
|
contentType: upstreamResponse.headers.get("content-type") ?? "",
|
|
@@ -208,7 +216,7 @@ export async function startOpenAiUsageProxy(input) {
|
|
|
208
216
|
throw new Error("Failed to start AI Gateway OpenAI proxy.");
|
|
209
217
|
}
|
|
210
218
|
return {
|
|
211
|
-
baseUrl: `http://127.0.0.1:${address.port}/v1`,
|
|
219
|
+
baseUrl: `http://127.0.0.1:${address.port}${engine === "codex" ? "/v1" : ""}`,
|
|
212
220
|
async close() {
|
|
213
221
|
const closeServer = server.listening
|
|
214
222
|
? new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function sumKnownProxyCosts(records) {
|
|
2
|
+
const knownCosts = records
|
|
3
|
+
.map((record) => record.costUsd)
|
|
4
|
+
.filter((cost) => typeof cost === "number" && Number.isFinite(cost) && cost >= 0);
|
|
5
|
+
if (!knownCosts.length)
|
|
6
|
+
return undefined;
|
|
7
|
+
return Number(knownCosts.reduce((sum, cost) => sum + cost, 0).toFixed(8));
|
|
8
|
+
}
|
|
9
|
+
export function sumProxyUsage(records) {
|
|
10
|
+
if (!records.length)
|
|
11
|
+
return undefined;
|
|
12
|
+
return records.reduce((sum, record) => ({
|
|
13
|
+
inputTokens: sum.inputTokens + record.usage.inputTokens,
|
|
14
|
+
outputTokens: sum.outputTokens + record.usage.outputTokens,
|
|
15
|
+
totalTokens: sum.totalTokens + record.usage.totalTokens,
|
|
16
|
+
cacheReadInputTokens: (sum.cacheReadInputTokens ?? 0) + (record.usage.cacheReadInputTokens ?? 0)
|
|
17
|
+
}), { inputTokens: 0, outputTokens: 0, totalTokens: 0, cacheReadInputTokens: 0 });
|
|
18
|
+
}
|
|
@@ -7,6 +7,7 @@ import { writeLog } from "../logs/log-service.js";
|
|
|
7
7
|
import { isRecord } from "../utils/is-record.js";
|
|
8
8
|
import { resolveToolCommand } from "./command-resolver.js";
|
|
9
9
|
import { getRunnerEngine } from "./engine-registry.js";
|
|
10
|
+
import { startOpenAiUsageProxy } from "./openai-usage-proxy.js";
|
|
10
11
|
import { CLAUDE_ROOT_PERMISSION_WARNING, resolveClaudePermissionMode } from "./claude-permission-policy.js";
|
|
11
12
|
import { finishSession, startSession } from "../sessions/session-service.js";
|
|
12
13
|
export async function runTool(tool, provider, model, options = {}) {
|
|
@@ -21,55 +22,101 @@ export async function runTool(tool, provider, model, options = {}) {
|
|
|
21
22
|
console.warn(`Warning: ${launchPolicy.warning}`);
|
|
22
23
|
}
|
|
23
24
|
const effectiveProvider = launchPolicy.provider;
|
|
24
|
-
const
|
|
25
|
-
const temporaryFiles = [];
|
|
26
|
-
const claudeSettingsPath = engine.requiresSettingsFile
|
|
27
|
-
? writeTemporaryClaudeSettings(effectiveProvider, { permissionMode: launchPolicy.claudeSettingsPermissionMode })
|
|
28
|
-
: null;
|
|
29
|
-
if (claudeSettingsPath)
|
|
30
|
-
temporaryFiles.push(claudeSettingsPath);
|
|
31
|
-
const args = buildRunnerArgs(tool, effectiveProvider, model, claudeSettingsPath);
|
|
32
|
-
const effectiveBaseUrl = getToolBaseUrl(tool, effectiveProvider);
|
|
33
|
-
await writeLog({
|
|
34
|
-
category: "usage",
|
|
35
|
-
action: "tool_spawn",
|
|
36
|
-
message: `Spawning ${tool}: ${provider.name} / ${model}`,
|
|
37
|
-
metadata: {
|
|
38
|
-
tool,
|
|
39
|
-
provider: provider.name,
|
|
40
|
-
model,
|
|
41
|
-
mode: effectiveProvider.mode,
|
|
42
|
-
configuredMode: provider.mode,
|
|
43
|
-
permissionConstraint: launchPolicy.reason,
|
|
44
|
-
baseUrl: effectiveBaseUrl,
|
|
45
|
-
configuredBaseUrl: provider.baseUrl,
|
|
46
|
-
apiKeyFingerprint: getSecretFingerprint(provider.apiKey),
|
|
47
|
-
command,
|
|
48
|
-
args,
|
|
49
|
-
authInjection: engine.authInjection
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
const session = await startSession({
|
|
53
|
-
tool,
|
|
25
|
+
const usageProxy = await startOpenAiUsageProxy({
|
|
54
26
|
provider: effectiveProvider,
|
|
55
27
|
model,
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
tool,
|
|
29
|
+
engine: tool
|
|
58
30
|
});
|
|
31
|
+
const temporaryFiles = [];
|
|
32
|
+
const setup = await (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const routedProvider = { ...effectiveProvider, baseUrl: usageProxy.baseUrl };
|
|
35
|
+
const env = buildRunnerEnv(tool, routedProvider, model);
|
|
36
|
+
const claudeSettingsPath = engine.requiresSettingsFile
|
|
37
|
+
? writeTemporaryClaudeSettings(routedProvider, { permissionMode: launchPolicy.claudeSettingsPermissionMode })
|
|
38
|
+
: null;
|
|
39
|
+
if (claudeSettingsPath)
|
|
40
|
+
temporaryFiles.push(claudeSettingsPath);
|
|
41
|
+
const args = buildRunnerArgs(tool, routedProvider, model, claudeSettingsPath);
|
|
42
|
+
const effectiveBaseUrl = getToolBaseUrl(tool, effectiveProvider);
|
|
43
|
+
await writeLog({
|
|
44
|
+
category: "usage",
|
|
45
|
+
action: "tool_spawn",
|
|
46
|
+
message: `Spawning ${tool}: ${provider.name} / ${model}`,
|
|
47
|
+
metadata: {
|
|
48
|
+
tool,
|
|
49
|
+
provider: provider.name,
|
|
50
|
+
model,
|
|
51
|
+
mode: effectiveProvider.mode,
|
|
52
|
+
configuredMode: provider.mode,
|
|
53
|
+
permissionConstraint: launchPolicy.reason,
|
|
54
|
+
baseUrl: effectiveBaseUrl,
|
|
55
|
+
transportBaseUrl: usageProxy.baseUrl,
|
|
56
|
+
configuredBaseUrl: provider.baseUrl,
|
|
57
|
+
apiKeyFingerprint: getSecretFingerprint(provider.apiKey),
|
|
58
|
+
command,
|
|
59
|
+
args,
|
|
60
|
+
authInjection: engine.authInjection
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const session = await startSession({
|
|
64
|
+
tool,
|
|
65
|
+
provider: effectiveProvider,
|
|
66
|
+
model,
|
|
67
|
+
projectPath: process.cwd(),
|
|
68
|
+
fallbackFrom: options.fallbackFrom ?? null
|
|
69
|
+
});
|
|
70
|
+
return { args, env, session };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
cleanupTemporaryFiles(temporaryFiles);
|
|
74
|
+
await usageProxy.close().catch(() => undefined);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
const { args, env, session } = setup;
|
|
59
79
|
return new Promise((resolve, reject) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
let finished = false;
|
|
81
|
+
const finish = async (exitCode, error) => {
|
|
82
|
+
if (finished)
|
|
83
|
+
return;
|
|
84
|
+
finished = true;
|
|
65
85
|
cleanupTemporaryFiles(temporaryFiles);
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
await usageProxy.close().catch(async (closeError) => {
|
|
87
|
+
await writeLog({
|
|
88
|
+
level: "warn",
|
|
89
|
+
category: "usage",
|
|
90
|
+
action: "usage_proxy_close_failed",
|
|
91
|
+
message: `Failed to close usage proxy for ${tool}: ${closeError instanceof Error ? closeError.message : String(closeError)}`,
|
|
92
|
+
metadata: { tool, provider: provider.name, model }
|
|
93
|
+
}).catch(() => undefined);
|
|
94
|
+
});
|
|
95
|
+
await finishSession(session.id, exitCode).catch(() => undefined);
|
|
96
|
+
if (error) {
|
|
97
|
+
reject(error);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
resolve(exitCode);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
let child;
|
|
104
|
+
try {
|
|
105
|
+
child = spawn(command, args, {
|
|
106
|
+
stdio: "inherit",
|
|
107
|
+
env
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
void finish(1, new Error(`Failed to start ${command}. Run \`ai doctor\` to check local CLI installation. ${error instanceof Error ? error.message : String(error)}`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
child.on("error", (error) => {
|
|
115
|
+
void finish(1, new Error(`Failed to start ${command}. Run \`ai doctor\` to check local CLI installation. ${error.message}`));
|
|
68
116
|
});
|
|
69
117
|
child.on("close", (code) => {
|
|
70
|
-
cleanupTemporaryFiles(temporaryFiles);
|
|
71
118
|
const exitCode = code ?? 0;
|
|
72
|
-
void
|
|
119
|
+
void finish(exitCode);
|
|
73
120
|
});
|
|
74
121
|
});
|
|
75
122
|
}
|
|
@@ -4,6 +4,7 @@ import { requiresDangerousConfirmation } from "../interaction/approval.js";
|
|
|
4
4
|
import { writeTelegramLog } from "../log.js";
|
|
5
5
|
import { isRecord } from "../../core/utils/is-record.js";
|
|
6
6
|
import { CLAUDE_ROOT_PERMISSION_WARNING, resolveClaudePermissionMode } from "../../core/runner/claude-permission-policy.js";
|
|
7
|
+
import { sumKnownProxyCosts, sumProxyUsage } from "../../core/runner/openai-usage-proxy.js";
|
|
7
8
|
export class ClaudeEngine {
|
|
8
9
|
name = "claude";
|
|
9
10
|
processes = new Map();
|
|
@@ -16,42 +17,41 @@ export class ClaudeEngine {
|
|
|
16
17
|
this.controllers.set(params.chatId, controller);
|
|
17
18
|
const signal = params.signal ?? controller.signal;
|
|
18
19
|
const startTime = Date.now();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
try {
|
|
21
|
+
if (permissionMode === "bypassPermissions" && requiresDangerousConfirmation(params.prompt)) {
|
|
22
|
+
const approved = await params.onPermission(createCommandPermissionRequest("Claude prompt", params.prompt));
|
|
23
|
+
if (!approved) {
|
|
24
|
+
throw new Error("Dangerous command denied.");
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
allowedTools: params.allowedTools
|
|
33
|
-
});
|
|
34
|
-
const permissionWarning = runtime.claudePermission?.constrained ? CLAUDE_ROOT_PERMISSION_WARNING : null;
|
|
35
|
-
await params.onProgress?.({ phase: "thinking", message: permissionWarning ?? "Claude started" });
|
|
36
|
-
if (permissionWarning) {
|
|
37
|
-
await writeTelegramLog({
|
|
38
|
-
level: "warn",
|
|
39
|
-
action: "claude_permission_constrained",
|
|
40
|
-
message: permissionWarning,
|
|
41
|
-
metadata: { requestedPermissionMode, effectivePermissionMode: permissionMode, reason: runtime.claudePermission?.reason }
|
|
27
|
+
const args = buildClaudeArgs({
|
|
28
|
+
prompt: params.prompt,
|
|
29
|
+
model: runtime.model,
|
|
30
|
+
settingsPath: runtime.claudeSettingsPath,
|
|
31
|
+
permissionMode,
|
|
32
|
+
resume: params.resume,
|
|
33
|
+
allowedTools: params.allowedTools
|
|
42
34
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
const permissionWarning = runtime.claudePermission?.constrained ? CLAUDE_ROOT_PERMISSION_WARNING : null;
|
|
36
|
+
await params.onProgress?.({ phase: "thinking", message: permissionWarning ?? "Claude started" });
|
|
37
|
+
if (permissionWarning) {
|
|
38
|
+
await writeTelegramLog({
|
|
39
|
+
level: "warn",
|
|
40
|
+
action: "claude_permission_constrained",
|
|
41
|
+
message: permissionWarning,
|
|
42
|
+
metadata: { requestedPermissionMode, effectivePermissionMode: permissionMode, reason: runtime.claudePermission?.reason }
|
|
43
|
+
});
|
|
52
44
|
}
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
await writeTelegramLog({
|
|
46
|
+
action: "claude_start",
|
|
47
|
+
message: `Starting Claude for Telegram chat ${params.chatId}`,
|
|
48
|
+
metadata: {
|
|
49
|
+
...summarizeRuntime(runtime.provider, runtime.model, "claude"),
|
|
50
|
+
cwd: params.cwd,
|
|
51
|
+
requestedPermissionMode,
|
|
52
|
+
effectivePermissionMode: permissionMode
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
55
|
const progressParser = createJsonLineAccumulator((event) => handleClaudeProgressEvent(event, params.onProgress));
|
|
56
56
|
const result = await spawnCapture({
|
|
57
57
|
command: runtime.command,
|
|
@@ -79,6 +79,8 @@ export class ClaudeEngine {
|
|
|
79
79
|
await params.onProgress?.({ phase: "done" });
|
|
80
80
|
return {
|
|
81
81
|
...parsed,
|
|
82
|
+
cost: parsed.cost ?? sumKnownProxyCosts(runtime.usageProxy.getRecords()),
|
|
83
|
+
usage: sumProxyUsage(runtime.usageProxy.getRecords()),
|
|
82
84
|
provider: runtime.provider.name,
|
|
83
85
|
model: runtime.model
|
|
84
86
|
};
|
|
@@ -86,7 +88,7 @@ export class ClaudeEngine {
|
|
|
86
88
|
finally {
|
|
87
89
|
this.processes.delete(params.chatId);
|
|
88
90
|
this.controllers.delete(params.chatId);
|
|
89
|
-
cleanupRuntime(runtime);
|
|
91
|
+
await cleanupRuntime(runtime);
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
abort(chatId) {
|
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { cleanupRuntime, createCommandPermissionRequest, formatEngineTimeout, prepareEngineRuntime, summarizeRuntime } from "./engine-utils.js";
|
|
5
5
|
import { createJsonLineAccumulator, extractJsonLines, spawnCapture, stripAnsi, terminateProcessTree } from "./process-utils.js";
|
|
6
|
-
import {
|
|
6
|
+
import { sumKnownProxyCosts, sumProxyUsage } from "../../core/runner/openai-usage-proxy.js";
|
|
7
7
|
import { requiresDangerousConfirmation } from "../interaction/approval.js";
|
|
8
8
|
import { writeTelegramLog } from "../log.js";
|
|
9
9
|
import { isRecord } from "../../core/utils/is-record.js";
|
|
@@ -18,28 +18,21 @@ export class CodexEngine {
|
|
|
18
18
|
const signal = params.signal ?? controller.signal;
|
|
19
19
|
const startTime = Date.now();
|
|
20
20
|
const permissionMode = params.permissionMode ?? "default";
|
|
21
|
-
if (permissionMode === "bypassPermissions" && requiresDangerousConfirmation(params.prompt)) {
|
|
22
|
-
const approved = await params.onPermission(createCommandPermissionRequest("Codex prompt", params.prompt));
|
|
23
|
-
if (!approved) {
|
|
24
|
-
cleanupRuntime(runtime);
|
|
25
|
-
throw new Error("Dangerous command denied.");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
let usageProxy = null;
|
|
29
|
-
await params.onProgress?.({ phase: "thinking", message: "Codex started" });
|
|
30
|
-
await writeTelegramLog({
|
|
31
|
-
action: "codex_start",
|
|
32
|
-
message: `Starting Codex for Telegram chat ${params.chatId}`,
|
|
33
|
-
metadata: { ...summarizeRuntime(runtime.provider, runtime.model, "codex"), cwd: params.cwd }
|
|
34
|
-
});
|
|
35
21
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
if (permissionMode === "bypassPermissions" && requiresDangerousConfirmation(params.prompt)) {
|
|
23
|
+
const approved = await params.onPermission(createCommandPermissionRequest("Codex prompt", params.prompt));
|
|
24
|
+
if (!approved) {
|
|
25
|
+
throw new Error("Dangerous command denied.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await params.onProgress?.({ phase: "thinking", message: "Codex started" });
|
|
29
|
+
await writeTelegramLog({
|
|
30
|
+
action: "codex_start",
|
|
31
|
+
message: `Starting Codex for Telegram chat ${params.chatId}`,
|
|
32
|
+
metadata: { ...summarizeRuntime(runtime.provider, runtime.model, "codex"), cwd: params.cwd }
|
|
40
33
|
});
|
|
41
34
|
const args = buildCodexArgs(params.prompt, runtime.model, params.cwd, permissionMode, {
|
|
42
|
-
baseUrl: usageProxy.baseUrl
|
|
35
|
+
baseUrl: runtime.usageProxy.baseUrl
|
|
43
36
|
}, params.resume, params.imagePaths);
|
|
44
37
|
const progressParser = createJsonLineAccumulator((event) => handleCodexProgressEvent(event, params.onProgress));
|
|
45
38
|
const result = await spawnCapture({
|
|
@@ -49,8 +42,8 @@ export class CodexEngine {
|
|
|
49
42
|
env: {
|
|
50
43
|
...runtime.env,
|
|
51
44
|
OPENAI_API_KEY: runtime.provider.apiKey,
|
|
52
|
-
OPENAI_BASE_URL: usageProxy.baseUrl,
|
|
53
|
-
AI_GATEWAY_BASE_URL: usageProxy.baseUrl
|
|
45
|
+
OPENAI_BASE_URL: runtime.usageProxy.baseUrl,
|
|
46
|
+
AI_GATEWAY_BASE_URL: runtime.usageProxy.baseUrl
|
|
54
47
|
},
|
|
55
48
|
signal,
|
|
56
49
|
timeoutMs: params.timeoutMs,
|
|
@@ -83,8 +76,8 @@ export class CodexEngine {
|
|
|
83
76
|
return {
|
|
84
77
|
...parsed,
|
|
85
78
|
sessionId,
|
|
86
|
-
cost: sumKnownProxyCosts(usageProxy.getRecords()),
|
|
87
|
-
usage: sumProxyUsage(usageProxy.getRecords()),
|
|
79
|
+
cost: sumKnownProxyCosts(runtime.usageProxy.getRecords()),
|
|
80
|
+
usage: sumProxyUsage(runtime.usageProxy.getRecords()),
|
|
88
81
|
provider: runtime.provider.name,
|
|
89
82
|
model: runtime.model
|
|
90
83
|
};
|
|
@@ -92,8 +85,7 @@ export class CodexEngine {
|
|
|
92
85
|
finally {
|
|
93
86
|
this.processes.delete(params.chatId);
|
|
94
87
|
this.controllers.delete(params.chatId);
|
|
95
|
-
await
|
|
96
|
-
cleanupRuntime(runtime);
|
|
88
|
+
await cleanupRuntime(runtime);
|
|
97
89
|
}
|
|
98
90
|
}
|
|
99
91
|
abort(chatId) {
|
|
@@ -169,8 +161,6 @@ function buildCodexProviderOverrideArgs(providerOverride) {
|
|
|
169
161
|
"-c",
|
|
170
162
|
`model_providers.${providerId}.env_key=${tomlString("OPENAI_API_KEY")}`,
|
|
171
163
|
"-c",
|
|
172
|
-
`model_providers.${providerId}.requires_openai_auth=true`,
|
|
173
|
-
"-c",
|
|
174
164
|
`model_providers.${providerId}.wire_api=${tomlString("responses")}`
|
|
175
165
|
];
|
|
176
166
|
}
|
|
@@ -417,21 +407,3 @@ function readChangedFiles(event) {
|
|
|
417
407
|
return [event.path];
|
|
418
408
|
return [];
|
|
419
409
|
}
|
|
420
|
-
function sumKnownProxyCosts(records) {
|
|
421
|
-
const knownCosts = records
|
|
422
|
-
.map((record) => record.costUsd)
|
|
423
|
-
.filter((cost) => typeof cost === "number" && Number.isFinite(cost) && cost >= 0);
|
|
424
|
-
if (!knownCosts.length)
|
|
425
|
-
return undefined;
|
|
426
|
-
return Number(knownCosts.reduce((sum, cost) => sum + cost, 0).toFixed(8));
|
|
427
|
-
}
|
|
428
|
-
function sumProxyUsage(records) {
|
|
429
|
-
if (!records.length)
|
|
430
|
-
return undefined;
|
|
431
|
-
return records.reduce((sum, record) => ({
|
|
432
|
-
inputTokens: sum.inputTokens + record.usage.inputTokens,
|
|
433
|
-
outputTokens: sum.outputTokens + record.usage.outputTokens,
|
|
434
|
-
totalTokens: sum.totalTokens + record.usage.totalTokens,
|
|
435
|
-
cacheReadInputTokens: (sum.cacheReadInputTokens ?? 0) + (record.usage.cacheReadInputTokens ?? 0)
|
|
436
|
-
}), { inputTokens: 0, outputTokens: 0, totalTokens: 0, cacheReadInputTokens: 0 });
|
|
437
|
-
}
|
|
@@ -3,6 +3,7 @@ import { resolveExactProviderForTool, resolveProviderForTool } from "../../core/
|
|
|
3
3
|
import { buildRunnerEnv, cleanupTemporaryFiles, getToolBaseUrl, writeTemporaryClaudeSettings } from "../../core/runner/tool-runner.js";
|
|
4
4
|
import { resolveClaudePermissionMode } from "../../core/runner/claude-permission-policy.js";
|
|
5
5
|
import { resolveToolCommand } from "../../core/runner/command-resolver.js";
|
|
6
|
+
import { startOpenAiUsageProxy } from "../../core/runner/openai-usage-proxy.js";
|
|
6
7
|
export async function prepareEngineRuntime(tool, providerName, modelName, permissionMode) {
|
|
7
8
|
// Telegram resolves and orders fallback candidates before invoking an
|
|
8
9
|
// engine. Once a candidate is selected, execute it exactly so the engine
|
|
@@ -18,26 +19,41 @@ export async function prepareEngineRuntime(tool, providerName, modelName, permis
|
|
|
18
19
|
? resolveClaudePermissionMode(permissionMode ?? "default")
|
|
19
20
|
: null;
|
|
20
21
|
const effectiveProvider = claudePermission?.constrained ? { ...provider, mode: "ask" } : provider;
|
|
21
|
-
const env = {
|
|
22
|
-
...buildRunnerEnv(tool, effectiveProvider, model),
|
|
23
|
-
AI_GATEWAY_SOURCE: "telegram"
|
|
24
|
-
};
|
|
25
22
|
const temporaryFiles = [];
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const usageProxy = await startOpenAiUsageProxy({
|
|
24
|
+
provider: effectiveProvider,
|
|
25
|
+
model,
|
|
26
|
+
tool: "telegram",
|
|
27
|
+
engine: tool
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const routedProvider = { ...effectiveProvider, baseUrl: usageProxy.baseUrl };
|
|
31
|
+
const env = {
|
|
32
|
+
...buildRunnerEnv(tool, routedProvider, model),
|
|
33
|
+
AI_GATEWAY_SOURCE: "telegram"
|
|
34
|
+
};
|
|
35
|
+
const claudeSettingsPath = tool === "claude"
|
|
36
|
+
? writeTemporaryClaudeSettings(routedProvider, {
|
|
37
|
+
permissionMode: permissionMode === "readOnly"
|
|
38
|
+
? "readOnly"
|
|
39
|
+
: claudePermission?.privileged
|
|
40
|
+
? "safeDefault"
|
|
41
|
+
: "provider"
|
|
42
|
+
})
|
|
43
|
+
: null;
|
|
44
|
+
if (claudeSettingsPath)
|
|
45
|
+
temporaryFiles.push(claudeSettingsPath);
|
|
46
|
+
return { provider, model, env, command, temporaryFiles, claudeSettingsPath, claudePermission, usageProxy };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
cleanupTemporaryFiles(temporaryFiles);
|
|
50
|
+
await usageProxy.close().catch(() => undefined);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
38
53
|
}
|
|
39
|
-
export function cleanupRuntime(runtime) {
|
|
54
|
+
export async function cleanupRuntime(runtime) {
|
|
40
55
|
cleanupTemporaryFiles(runtime.temporaryFiles);
|
|
56
|
+
await runtime.usageProxy.close().catch(() => undefined);
|
|
41
57
|
}
|
|
42
58
|
export function resolveWorkingCommand(tool) {
|
|
43
59
|
return resolveToolCommand(tool);
|
package/docs/USAGE.html
CHANGED
|
@@ -531,6 +531,12 @@ ai setup</pre>
|
|
|
531
531
|
<td>用默认 provider 和默认模型启动 Claude Code 或 Codex。</td>
|
|
532
532
|
<td>不指定时使用 <code>level: 1</code>。</td>
|
|
533
533
|
</tr>
|
|
534
|
+
<tr data-search="image generate image prompt model b64 url">
|
|
535
|
+
<td><span class="group-label">使用</span></td>
|
|
536
|
+
<td><code class="cmd">ai image 51talk-image "一只戴墨镜的猫"</code></td>
|
|
537
|
+
<td>调用已配置的图片 Provider 和默认模型生成图片并保存到当前目录。</td>
|
|
538
|
+
<td>支持 Provider fallback;真实 usage 写入 <code>usage.json</code>。</td>
|
|
539
|
+
</tr>
|
|
534
540
|
<tr data-search="use provider model named">
|
|
535
541
|
<td><span class="group-label">使用</span></td>
|
|
536
542
|
<td><code class="cmd">ai use claude 51talk claude-opus-4-6</code><br /><code class="cmd">ai use codex 51talk gpt-5.5</code></td>
|
|
@@ -632,6 +638,20 @@ ai use claude 51talk
|
|
|
632
638
|
ai use codex 51talk gpt-5.5</pre>
|
|
633
639
|
<p>不指定 provider 时使用 <code>level: 1</code>;不指定模型时使用 provider 默认 <code>model</code>。</p>
|
|
634
640
|
</div>
|
|
641
|
+
<div class="note">
|
|
642
|
+
<h3>生成图片</h3>
|
|
643
|
+
<pre>ai image
|
|
644
|
+
ai image 51talk-image "一只戴墨镜的猫,数字插画"
|
|
645
|
+
ai image 51talk-image gpt-image-2 "一只戴墨镜的猫,数字插画"</pre>
|
|
646
|
+
<ul>
|
|
647
|
+
<li>直接运行 <code>ai image</code> 时,会依次选择 Provider、具体模型并输入提示词。</li>
|
|
648
|
+
<li>第一个参数是 Provider 名;第二个参数匹配模型缓存时作为具体模型,否则从第二个参数开始作为提示词并使用默认模型。</li>
|
|
649
|
+
<li>命令执行前会自动刷新模型缓存;也可以先运行 <code>ai status</code> 检查。指定模型时建议给提示词加引号。</li>
|
|
650
|
+
<li><code>b64_json</code> 会以 <code>0600</code> 权限保存到当前目录并输出绝对路径;远程响应只接受无凭证的 <code>https</code> URL。</li>
|
|
651
|
+
<li>图片请求最长等待 120 秒;超时会自动失败并尝试下一个 Provider。</li>
|
|
652
|
+
<li>请求经过 execution-scoped Transport Proxy,真实 usage 复用唯一 <code>UsageObserver</code> 账本。</li>
|
|
653
|
+
</ul>
|
|
654
|
+
</div>
|
|
635
655
|
<div class="note">
|
|
636
656
|
<h3>运行规则</h3>
|
|
637
657
|
<ul>
|
|
@@ -640,6 +660,7 @@ ai use codex 51talk gpt-5.5</pre>
|
|
|
640
660
|
<li><code>ai use codex</code> 使用本次子进程的 <code>-c model_provider=...</code> 固定所选 provider,并通过 <code>env_key = "OPENAI_API_KEY"</code> 使用 AI Gateway 注入的 Key;不会修改 Codex 全局认证或配置。</li>
|
|
641
661
|
<li>provider 的 <code>mode</code> 为 <code>auto</code> 时,非 root 执行 Claude Code 会追加 <code>--dangerously-skip-permissions</code>;<code>ask</code> 模式不会追加,并会在启动前确认。root 下请求 <code>auto</code> 会被安全约束为默认权限,临时 settings 中继承的 bypass 配置也会被清除。</li>
|
|
642
662
|
<li>OpenAI 兼容地址用于 Codex 时会使用 <code>/v1</code>,用于 Claude 时会去掉 <code>/v1</code>。</li>
|
|
663
|
+
<li>Claude 与 Codex 的本次子进程都会通过 execution-scoped 本地代理转发;原生终端交互不变,provider 返回的真实 usage 会按 provider/model 写入 <code>usage.json</code>。</li>
|
|
643
664
|
<li>日志只记录 Key 指纹,不记录完整 API Key。</li>
|
|
644
665
|
</ul>
|
|
645
666
|
</div>
|
|
@@ -658,6 +679,7 @@ ai set codex proxy off</pre>
|
|
|
658
679
|
<li><code>provider name model-id</code> 显式选择该 provider 已探测的 <code>gpt-*</code> 模型;只给 provider 时自动选最新 GPT。</li>
|
|
659
680
|
<li><code>provider</code> 不带名称时依次交互选择已添加的 provider 和 GPT 模型;没有可用 GPT 时提示运行 <code>ai status</code>,不修改配置。</li>
|
|
660
681
|
<li>自动选择先比较 GPT 数字版本,同版本优先 <code>sol</code>、<code>pro</code>、alias、<code>codex</code>、<code>terra</code>、<code>mini</code>、<code>luna</code>、<code>nano</code>,并优先滚动 alias。API Key 只写入 mode <code>0600</code> 的 <code>auth.json</code>。</li>
|
|
682
|
+
<li>写入 <code>config.toml</code> 前会规范化 <code>base_url</code>:末尾没有 <code>/v1</code> 时自动补上,已有时不重复追加。</li>
|
|
661
683
|
<li>原有 project、MCP、plugin、feature 设置会保留;任一步失败会恢复原 <code>config.toml</code> 和 <code>auth.json</code>。</li>
|
|
662
684
|
<li>切换自定义 provider 会替换当前 ChatGPT token;切回 ChatGPT 时需重新登录。</li>
|
|
663
685
|
<li>成功后 macOS 会重启 <code>Codex.app</code> 或当前名称的 <code>ChatGPT.app</code>;无法自动重启时按提示手动操作。</li>
|
|
@@ -753,7 +775,7 @@ ai telegram status --all</pre>
|
|
|
753
775
|
<li><code>/provider</code> 以按钮列出全部 AI Provider,可编辑名称、Base URL、API Key、默认模型、模式和优先级,二次确认后删除,或从列表底部添加。操作复用 <code>ai provider</code> 的核心 CRUD 和检查逻辑。</li>
|
|
754
776
|
<li>Provider API Key 在详情中只显示“已配置”,不会写入 Bot 应用日志;输入后 Bot 会尽力立即删除该 Telegram 消息。未完成表单在 Bot 重启后需要重新开始。</li>
|
|
755
777
|
<li>Telegram Codex 执行会自动走本地 OpenAI 兼容代理记录 provider usage;<code>/usage</code> 有 provider cost 时直接展示,否则按 <code>model-pricing.json</code> 估算。</li>
|
|
756
|
-
<li>如果本机不能直连 <code>api.telegram.org</code>,设置 <code>proxyUrl</code> 或环境变量 <code>HTTPS_PROXY</code> / <code>ALL_PROXY</code>。</li>
|
|
778
|
+
<li>如果本机不能直连 <code>api.telegram.org</code>,设置 <code>proxyUrl</code> 或环境变量 <code>HTTPS_PROXY</code> / <code>ALL_PROXY</code>。交互式新增或编辑 Bot 时,<code>proxyUrl</code> 输入提示会展示完整 URL 示例;地址请包含协议,例如 <code>http://127.0.0.1:7890</code>。</li>
|
|
757
779
|
</ul>
|
|
758
780
|
</div>
|
|
759
781
|
</details>
|
|
@@ -799,7 +821,7 @@ ai provider list
|
|
|
799
821
|
ai log
|
|
800
822
|
ai log --print
|
|
801
823
|
ai context</pre>
|
|
802
|
-
<p><code>status</code> 会刷新 provider 健康度和模型缓存,展示 Claude/Codex 各自实际 baseUrl、兼容模型、token
|
|
824
|
+
<p><code>status</code> 会刷新 provider 健康度和模型缓存,展示 Claude/Codex 各自实际 baseUrl、兼容模型、token 使用量和使用占比。CLI 与 Telegram 的 Claude/Codex 新请求只要 provider 返回结构化 usage 都会计入;周统计按 provider 精确名称聚合,同一 provider 的不同模型合并,事件仍保留 model 明细。升级前未经过代理的旧历史不会估算补录。</p>
|
|
803
825
|
</div>
|
|
804
826
|
<div class="note">
|
|
805
827
|
<h3>诊断与自动修复</h3>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@offerpilot/axiomruntime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI-first foundation for an enterprise AI Runtime with provider routing and governed execution.",
|
|
5
5
|
"homepage": "https://gitlab.com/linlangli/agent-router",
|
|
6
6
|
"repository": {
|