@offerpilot/axiomruntime 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 env = buildRunnerEnv(tool, effectiveProvider, model);
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
- projectPath: process.cwd(),
57
- fallbackFrom: options.fallbackFrom ?? null
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
- const child = spawn(command, args, {
61
- stdio: "inherit",
62
- env
63
- });
64
- child.on("error", (error) => {
80
+ let finished = false;
81
+ const finish = async (exitCode, error) => {
82
+ if (finished)
83
+ return;
84
+ finished = true;
65
85
  cleanupTemporaryFiles(temporaryFiles);
66
- void finishSession(session.id, 1);
67
- reject(new Error(`Failed to start ${command}. Run \`ai doctor\` to check local CLI installation. ${error.message}`));
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 finishSession(session.id, exitCode).finally(() => resolve(exitCode));
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
- if (permissionMode === "bypassPermissions" && requiresDangerousConfirmation(params.prompt)) {
20
- const approved = await params.onPermission(createCommandPermissionRequest("Claude prompt", params.prompt));
21
- if (!approved) {
22
- cleanupRuntime(runtime);
23
- throw new Error("Dangerous command denied.");
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
- const args = buildClaudeArgs({
27
- prompt: params.prompt,
28
- model: runtime.model,
29
- settingsPath: runtime.claudeSettingsPath,
30
- permissionMode,
31
- resume: params.resume,
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
- await writeTelegramLog({
45
- action: "claude_start",
46
- message: `Starting Claude for Telegram chat ${params.chatId}`,
47
- metadata: {
48
- ...summarizeRuntime(runtime.provider, runtime.model, "claude"),
49
- cwd: params.cwd,
50
- requestedPermissionMode,
51
- effectivePermissionMode: permissionMode
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
- try {
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 { startOpenAiUsageProxy } from "../../core/runner/openai-usage-proxy.js";
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
- usageProxy = await startOpenAiUsageProxy({
37
- provider: runtime.provider,
38
- model: runtime.model,
39
- tool: "telegram"
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 usageProxy?.close().catch(() => undefined);
96
- cleanupRuntime(runtime);
88
+ await cleanupRuntime(runtime);
97
89
  }
98
90
  }
99
91
  abort(chatId) {
@@ -415,21 +407,3 @@ function readChangedFiles(event) {
415
407
  return [event.path];
416
408
  return [];
417
409
  }
418
- function sumKnownProxyCosts(records) {
419
- const knownCosts = records
420
- .map((record) => record.costUsd)
421
- .filter((cost) => typeof cost === "number" && Number.isFinite(cost) && cost >= 0);
422
- if (!knownCosts.length)
423
- return undefined;
424
- return Number(knownCosts.reduce((sum, cost) => sum + cost, 0).toFixed(8));
425
- }
426
- function sumProxyUsage(records) {
427
- if (!records.length)
428
- return undefined;
429
- return records.reduce((sum, record) => ({
430
- inputTokens: sum.inputTokens + record.usage.inputTokens,
431
- outputTokens: sum.outputTokens + record.usage.outputTokens,
432
- totalTokens: sum.totalTokens + record.usage.totalTokens,
433
- cacheReadInputTokens: (sum.cacheReadInputTokens ?? 0) + (record.usage.cacheReadInputTokens ?? 0)
434
- }), { inputTokens: 0, outputTokens: 0, totalTokens: 0, cacheReadInputTokens: 0 });
435
- }
@@ -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 claudeSettingsPath = tool === "claude"
27
- ? writeTemporaryClaudeSettings(provider, {
28
- permissionMode: permissionMode === "readOnly"
29
- ? "readOnly"
30
- : claudePermission?.privileged
31
- ? "safeDefault"
32
- : "provider"
33
- })
34
- : null;
35
- if (claudeSettingsPath)
36
- temporaryFiles.push(claudeSettingsPath);
37
- return { provider, model, env, command, temporaryFiles, claudeSettingsPath, claudePermission };
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,9 +679,10 @@ 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
- <li>成功后 macOS 会重启 <code>Codex.app</code> 或当前名称的 <code>ChatGPT.app</code>;无法自动重启时按提示手动操作。</li>
685
+ <li>成功后 macOS 会重启 <code>Codex.app</code> 或当前名称的 <code>ChatGPT.app</code>,并清理重启进程继承的 <code>OPENAI_API_KEY</code>,确保客户端读取刚写入的 <code>auth.json</code>;无法自动重启时按提示手动操作。</li>
664
686
  <li><code>proxy</code> 把裸 <code>host:port</code> 规范化为 HTTP origin,只更新 <code>shell_environment_policy.set</code> 的大小写 HTTP(S)/ALL proxy keys;<code>off</code> 清除这些 managed keys,不修改 <code>auth.json</code>。</li>
665
687
  <li>Proxy URL 禁止包含账号密码、path、query 或 fragment,command usage log 会整体脱敏。macOS 会同步当前登录会话的 user launchd environment;失败时恢复原 config 且不重启,注销或重启 macOS 后需重新运行本命令。</li>
666
688
  </ul>
@@ -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 使用量和使用占比。</p>
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@offerpilot/axiomruntime",
3
- "version": "0.0.3",
4
- "description": "CLI-first foundation for an enterprise AI Runtime with provider routing and governed execution.",
3
+ "version": "0.0.5",
4
+ "description": "CLI-first local AI Runtime for isolated multi-provider Claude Code and Codex execution with Telegram remote control.",
5
5
  "homepage": "https://gitlab.com/linlangli/agent-router",
6
6
  "repository": {
7
7
  "type": "git",
package/docs/README.md DELETED
@@ -1,98 +0,0 @@
1
- # AI Runtime 文档总览
2
-
3
- 本项目采用分层文档体系:英文正典(`AGENTS.md`、`AI_RUNTIME_ARCHITECTURE.md`、`CONVENTIONS.md`)定义仓库规则与目标架构;中英混合的 `doc_2.0` 文档层(根文档在 `docs/`,模块文档在 `docs/modules/`)承载产品规范、宪法摘要、模块设计与强制条款;面向用户的中文文档负责操作说明。各层描述的是同一个产品,不是多套路线。
4
-
5
- ## 给用户和中文评审者
6
-
7
- | 文档 | 内容 |
8
- |---|---|
9
- | [项目 README](../README.md) | 产品定位、当前能力、安装与常用入口 |
10
- | [doc_2.0 规范层完整索引](#doc_20-规范层完整索引) | 分层架构、模块文档、规范条款、路线图的统一索引(见下方章节)|
11
- | [使用指南](usage.md) | CLI、Provider、Telegram、会话和诊断操作 |
12
- | [HTML 命令手册](USAGE.html) | 可浏览的命令索引 |
13
- | [整体架构与当前实现快照](00-overview.md) | 分层俯瞰、现状映射与从旧 ARCHITECTURE 迁入的实现快照 |
14
- | [产品架构全景图](architecture-map.md) | 中文架构图、执行主链、学习闭环与权威所有权说明 |
15
- | [自动记忆系统方案](modules/06-memory.md) | Memory、Knowledge、Skill 与学习治理设计(配套 [contracts](contracts.md)、[07-learning-governance](modules/07-learning-governance.md))|
16
- | [Telegram Bot 方案](modules/01-surfaces.md) | Telegram 入口的交互和实现设计 |
17
-
18
- ## 给 AI 和维护者
19
-
20
- 以下文档使用英文,并作为架构与实现的权威来源:
21
-
22
- | 文档 | 内容 |
23
- |---|---|
24
- | [Repository Instructions](../AGENTS.md) | AI 在仓库内工作的规则、文档优先级和交付标准 |
25
- | [Target Architecture](AI_RUNTIME_ARCHITECTURE.md) | Enterprise AI Runtime 目标架构与演进路径 |
26
- | [Development Conventions](CONVENTIONS.md) | 工程、发布、文档同步和模块边界规范 |
27
- | [Portable AI Specification](AI_RUNTIME_PORTABLE_SPEC.md) | 可直接提供给其他 AI 的英文单文件完整上下文;仓库内属于便携快照,不替代分层权威文档 |
28
-
29
- 产品宪法摘要、产品规范与验收标准构成中英混合的 `doc_2.0` 文档层:宪法摘要见 [00-overview.md](00-overview.md),跨模块强制条款(R1–R36 / Q1–Q6)见 [enforcement-decisions.md](enforcement-decisions.md)。
30
-
31
- 除宪法摘要外,正典文档与 `doc_2.0` 规范层都是随开发持续更新的 Living Documents:每个开发任务结束前检查影响,每个里程碑或发布进行完整审计。宪法摘要(见 [00-overview.md](00-overview.md))只在人类明确调整使命、北极星或三条硬不变量时修改。
32
-
33
- ## doc_2.0 规范层完整索引
34
-
35
- > `doc_2.0` 是按**分层架构**重构的文档集:一份项目整体文档 + 每个独立模块一份文档。语言模式为**中英混合**(叙述中文,标识符英文原样保留,以保 AI 识别准确率)。权威顺序与冲突处理见下方「文档冲突处理」。
36
-
37
- ### 起点
38
-
39
- | 文档 | 内容 |
40
- |---|---|
41
- | [00-overview.md](00-overview.md) | ⭐项目整体文档:三条不变量、分层全景、模块清单、现状映射 |
42
- | [glossary.md](glossary.md) | 中英术语表(一致性锁) |
43
- | [contracts.md](contracts.md) | L0 端口契约清单,含统一 Capability Observability envelope、Sink 与授权查询契约 |
44
- | [extraction-readiness.md](extraction-readiness.md) | 模块产品化就绪度判据 |
45
- | [enforcement-decisions.md](enforcement-decisions.md) | ⭐规范约定 + R1–R36 强制实施决策 + Q1–Q6 质量/运维决策(跨模块权威引用源)|
46
- | [roadmap.md](roadmap.md) | Release R0–R3 纵向产品闭环、Phase 0–13 技术计划与贯穿全程可观测性 |
47
- | [testing.md](testing.md) | 测试计划:核心记忆/Telegram/CLI/Auth/检索质量/长期仿真/Memory、Skill 与 Capability 可观测性 |
48
- | [service-level-objectives.md](service-level-objectives.md) | `local/team/enterprise` 的 SLO、RTO、RPO、容量与恢复演练门槛 |
49
- | [usage.md](usage.md) | 用户使用指南:逐命令用法(交互模式/Claude·Codex/Telegram/Provider/会话/日志等)|
50
-
51
- ### 模块文档(每个独立模块单独维护,位于 `modules/`)
52
-
53
- | # | 模块 | 文档 | 状态 |
54
- |---|---|---|---|
55
- | ① | Surfaces | [modules/01-surfaces.md](modules/01-surfaces.md) | ✅ |
56
- | ② | Orchestration | [modules/02-orchestration.md](modules/02-orchestration.md) | ✅ |
57
- | ③ | Capabilities | [modules/03-capabilities.md](modules/03-capabilities.md) | ✅ |
58
- | ④ | Execution Kernel | [modules/04-execution-kernel.md](modules/04-execution-kernel.md) | ✅ |
59
- | ⑤ | Transport Proxy | [modules/05-transport-proxy.md](modules/05-transport-proxy.md) | ✅ |
60
- | ⑥ | Memory | [modules/06-memory.md](modules/06-memory.md) | ✅ |
61
- | ⑦ | Learning & Governance | [modules/07-learning-governance.md](modules/07-learning-governance.md) | ✅ |
62
- | ⑧ | Policy | [modules/08-policy.md](modules/08-policy.md) | ✅ |
63
- | ⑨ | Security | [modules/09-security.md](modules/09-security.md) | ✅ |
64
- | ⑩ | Adapters/Plugins | [modules/10-adapters-plugins.md](modules/10-adapters-plugins.md) | ✅ |
65
- | ⑪ | Storage | [modules/11-storage.md](modules/11-storage.md) | ✅ |
66
-
67
- 每份模块文档固定 11 节:`定位 → 单一职责边界 → 内部构件 → 对外/消费端口 → 依赖 → 数据所有权 → 降级/kill switch → 产品化就绪度 → 现状映射 → 验收标准 → 相关文档`。以 [modules/05-transport-proxy.md](modules/05-transport-proxy.md) 为样板。
68
-
69
- ## 文档冲突处理
70
-
71
- 不同问题只有一个权威归属:
72
-
73
- | 问题 | 唯一权威位置 |
74
- |---|---|
75
- | mission / North Star / 三条硬不变量 | [00-overview.md](00-overview.md) 的宪法摘要;仅人类明确决策可修改 |
76
- | 分层、模块 ownership、依赖方向、插件原则 | [AI_RUNTIME_ARCHITECTURE.md](AI_RUNTIME_ARCHITECTURE.md) + [CONVENTIONS.md](CONVENTIONS.md) |
77
- | canonical identifier、Port、跨进程 schema、错误/版本语义 | [contracts.md](contracts.md) |
78
- | 跨模块产品行为和安全/质量硬门槛 | [enforcement-decisions.md](enforcement-decisions.md) 的 R/Q 条款 |
79
- | 单模块职责、数据所有权、降级和模块验收 | 对应 `modules/*.md` 的正文 1–11 节 |
80
- | 交付顺序与测试覆盖 | [roadmap.md](roadmap.md) + [testing.md](testing.md) |
81
- | 历史实现、迁移代码、旧文档保留材料 | 模块附录,仅作 supporting material,不是独立权威源 |
82
-
83
- 模块附录中的旧字段、代码块、状态说明或实现建议不得覆盖当前 L0 契约、R/Q 条款或模块正文。发现冲突时不能按“更具体”或“更旧”自动取舍:当前任务必须同步修正文档,无法当场修复时显式记录 conflict、owner 和 exit condition。`doc_2.0` 中的 canonical identifier 必须与 `contracts.md` 一致。
84
-
85
- 所有附录标题下必须声明 `Appendix Status`:
86
-
87
- | `Appendix Status` | 含义 | 可否作为新实现依据 |
88
- |---|---|---|
89
- | `CONSTITUTION SUMMARY` | 经人类批准的使命/North Star/硬不变量摘要 | 可以,仅限宪章范围 |
90
- | `SUPPORTING SPEC` | 补充正文的详细行为;不得覆盖正文/L0/RQ | 可以,但先归一化到 canonical contract |
91
- | `CURRENT SNAPSHOT` | 某一时点实现事实 | 不可以直接推导目标设计 |
92
- | `MIGRATION FIXTURE` | legacy schema/样本/兼容输入 | 仅用于 migration 与 compatibility test |
93
- | `DESIGN SOURCE` | 保留的方案、推理或成熟设计来源 | 只能作为 ADR 输入 |
94
- | `DEPRECATED REFERENCE` | 已被新规范取代、等待删除的材料 | 不可以 |
95
-
96
- 缺少状态标签的附录视为 `DEPRECATED REFERENCE`。引用附录时必须同时引用它所服从的正文、contract 或 R/Q 条款。
97
-
98
- 宪法摘要只能由人类明确决策后修改。代码、单次模型输出或局部功能文档不能反向改变项目定位。