@compaction/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -12
- package/dist/cli/commands/activity.d.ts +8 -0
- package/dist/cli/commands/activity.js +35 -0
- package/dist/cli/commands/apply-context.js +68 -0
- package/dist/cli/commands/billing-delta.js +12 -1
- package/dist/cli/commands/capture-claude-code.d.ts +44 -0
- package/dist/cli/commands/capture-claude-code.js +206 -0
- package/dist/cli/commands/capture.js +283 -1
- package/dist/cli/commands/compact.js +9 -5
- package/dist/cli/commands/dev.d.ts +17 -0
- package/dist/cli/commands/dev.js +131 -0
- package/dist/cli/commands/gateway.d.ts +31 -0
- package/dist/cli/commands/gateway.js +312 -0
- package/dist/cli/commands/hooks.d.ts +5 -0
- package/dist/cli/commands/hooks.js +181 -0
- package/dist/cli/commands/init.js +422 -17
- package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
- package/dist/cli/commands/input-compaction-ab.js +125 -0
- package/dist/cli/commands/optimize-hosted.d.ts +54 -0
- package/dist/cli/commands/optimize-hosted.js +123 -0
- package/dist/cli/commands/optimize.js +8 -0
- package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
- package/dist/cli/commands/output-shaping-ab.js +132 -0
- package/dist/cli/commands/output-shaping.d.ts +7 -0
- package/dist/cli/commands/output-shaping.js +43 -0
- package/dist/cli/commands/policies.d.ts +2 -0
- package/dist/cli/commands/policies.js +80 -0
- package/dist/cli/commands/precall.d.ts +19 -0
- package/dist/cli/commands/precall.js +436 -0
- package/dist/cli/commands/recommend.js +1 -1
- package/dist/cli/commands/run.js +297 -12
- package/dist/cli/commands/upgrade-status.d.ts +42 -0
- package/dist/cli/commands/upgrade-status.js +152 -0
- package/dist/cli/cursor-export-read.d.ts +13 -0
- package/dist/cli/cursor-export-read.js +53 -0
- package/dist/cli/cursor-live-preflight.d.ts +19 -0
- package/dist/cli/cursor-live-preflight.js +46 -0
- package/dist/cli/index.js +36 -2
- package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
- package/dist/cli/onboarding/GatewayTui.js +113 -0
- package/dist/cli/onboarding/model.d.ts +32 -1
- package/dist/cli/onboarding/model.js +27 -2
- package/dist/cli/terminal-logo.d.ts +27 -0
- package/dist/cli/terminal-logo.js +49 -0
- package/dist/core/activity-event.d.ts +107 -0
- package/dist/core/activity-event.js +182 -0
- package/dist/core/activity-store.d.ts +63 -0
- package/dist/core/activity-store.js +254 -0
- package/dist/core/activity-view.d.ts +76 -0
- package/dist/core/activity-view.js +120 -0
- package/dist/core/api-client/index.d.ts +3 -1
- package/dist/core/api-client/index.js +4 -0
- package/dist/core/api-client/payload.d.ts +5 -1
- package/dist/core/api-client/payload.js +8 -6
- package/dist/core/api-client/persisted-config.d.ts +57 -0
- package/dist/core/api-client/persisted-config.js +156 -0
- package/dist/core/api-client/tool.d.ts +29 -0
- package/dist/core/api-client/tool.js +50 -0
- package/dist/core/api-client/types.d.ts +15 -4
- package/dist/core/auto-apply-ask.d.ts +58 -0
- package/dist/core/auto-apply-ask.js +105 -0
- package/dist/core/auto-apply-gates.d.ts +76 -0
- package/dist/core/auto-apply-gates.js +113 -0
- package/dist/core/before-call-activity.d.ts +35 -0
- package/dist/core/before-call-activity.js +103 -0
- package/dist/core/before-call-recovery.d.ts +19 -0
- package/dist/core/before-call-recovery.js +46 -0
- package/dist/core/before-call-stdin.d.ts +59 -0
- package/dist/core/before-call-stdin.js +78 -0
- package/dist/core/before-call.d.ts +149 -0
- package/dist/core/before-call.js +358 -0
- package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
- package/dist/core/billing-delta/billing-delta-record.js +26 -0
- package/dist/core/capture-record.d.ts +40 -0
- package/dist/core/capture-record.js +55 -0
- package/dist/core/claude-code-before-call.d.ts +37 -0
- package/dist/core/claude-code-before-call.js +120 -0
- package/dist/core/claude-code-connect.d.ts +31 -0
- package/dist/core/claude-code-connect.js +87 -0
- package/dist/core/claude-code-hook-record.d.ts +71 -0
- package/dist/core/claude-code-hook-record.js +0 -0
- package/dist/core/claude-code-hooks.d.ts +77 -0
- package/dist/core/claude-code-hooks.js +141 -0
- package/dist/core/codex-capture.d.ts +45 -0
- package/dist/core/codex-capture.js +204 -0
- package/dist/core/command-runner.js +5 -1
- package/dist/core/compaction-artifacts.d.ts +51 -0
- package/dist/core/compaction-artifacts.js +242 -0
- package/dist/core/compactor.d.ts +9 -0
- package/dist/core/compactor.js +95 -0
- package/dist/core/cross-surface-event.d.ts +306 -0
- package/dist/core/cross-surface-event.js +330 -0
- package/dist/core/cursor-capture.d.ts +54 -0
- package/dist/core/cursor-capture.js +215 -0
- package/dist/core/cursor-preflight-probe.d.ts +8 -0
- package/dist/core/cursor-preflight-probe.js +88 -0
- package/dist/core/cursor-preflight.d.ts +86 -0
- package/dist/core/cursor-preflight.js +126 -0
- package/dist/core/gateway/apply-activation.d.ts +39 -0
- package/dist/core/gateway/apply-activation.js +84 -0
- package/dist/core/gateway/apply-policy.d.ts +64 -0
- package/dist/core/gateway/apply-policy.js +221 -0
- package/dist/core/gateway/apply-receipt.d.ts +36 -0
- package/dist/core/gateway/apply-receipt.js +75 -0
- package/dist/core/gateway/cache-proof.d.ts +45 -0
- package/dist/core/gateway/cache-proof.js +65 -0
- package/dist/core/gateway/configure.d.ts +50 -0
- package/dist/core/gateway/configure.js +169 -0
- package/dist/core/gateway/openai-usage.d.ts +56 -0
- package/dist/core/gateway/openai-usage.js +128 -0
- package/dist/core/gateway/receipt.d.ts +138 -0
- package/dist/core/gateway/receipt.js +120 -0
- package/dist/core/gateway/recovery.d.ts +23 -0
- package/dist/core/gateway/recovery.js +68 -0
- package/dist/core/gateway/server.d.ts +51 -0
- package/dist/core/gateway/server.js +276 -0
- package/dist/core/gateway/status.d.ts +45 -0
- package/dist/core/gateway/status.js +109 -0
- package/dist/core/hook-usage-aggregate.d.ts +47 -0
- package/dist/core/hook-usage-aggregate.js +161 -0
- package/dist/core/input-compaction-ab.d.ts +111 -0
- package/dist/core/input-compaction-ab.js +158 -0
- package/dist/core/local-run-record.d.ts +109 -0
- package/dist/core/local-run-record.js +223 -0
- package/dist/core/output-shaping-ab.d.ts +140 -0
- package/dist/core/output-shaping-ab.js +146 -0
- package/dist/core/output-shaping-attach.d.ts +31 -0
- package/dist/core/output-shaping-attach.js +57 -0
- package/dist/core/output-shaping.d.ts +56 -0
- package/dist/core/output-shaping.js +89 -0
- package/dist/core/policy-middleware.d.ts +121 -0
- package/dist/core/policy-middleware.js +919 -0
- package/dist/core/policy-preferences.d.ts +99 -0
- package/dist/core/policy-preferences.js +232 -0
- package/dist/core/run-aggregator.d.ts +11 -1
- package/dist/core/run-aggregator.js +29 -2
- package/dist/core/run-flow-report.d.ts +82 -0
- package/dist/core/run-flow-report.js +71 -0
- package/dist/core/safety-report.js +8 -1
- package/dist/core/shim-capture-bridge.d.ts +32 -0
- package/dist/core/shim-capture-bridge.js +88 -0
- package/dist/core/skill-injection-policy.d.ts +72 -0
- package/dist/core/skill-injection-policy.js +183 -0
- package/dist/core/spend-attribution.js +1 -1
- package/dist/core/token-accounting.d.ts +1 -1
- package/dist/core/tool-shim.d.ts +129 -0
- package/dist/core/tool-shim.js +447 -0
- package/dist/core/trace-parser.d.ts +13 -13
- package/dist/core/trace-parser.js +6 -6
- package/dist/core/types.d.ts +12 -2
- package/dist/core/waste-detector.d.ts +20 -0
- package/dist/core/waste-detector.js +160 -6
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway project configure (PUBLIC CLI core). Charter run-17.1.
|
|
3
|
+
*
|
|
4
|
+
* Approval-gated: this module only ever PLANS a change (detect the project, propose adding an
|
|
5
|
+
* OPENAI_BASE_URL pointing at the local gateway, and render an exact diff). It writes NOTHING unless the
|
|
6
|
+
* caller explicitly applies the plan, and even then it creates a `.bak` backup first and refuses to
|
|
7
|
+
* overwrite an existing provider base URL without an explicit force. No auto-write, ever.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
/** The env var we set to route an OpenAI-compatible client through the gateway. */
|
|
12
|
+
export const CONFIGURE_ENV_VAR = "OPENAI_BASE_URL";
|
|
13
|
+
const ENV_CANDIDATES = [".env", ".env.local"];
|
|
14
|
+
const BASE_URL_VARS = ["OPENAI_BASE_URL", "OPENAI_API_BASE"];
|
|
15
|
+
function readIfExists(p) {
|
|
16
|
+
try {
|
|
17
|
+
return fs.readFileSync(p, "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Find an existing base-URL assignment in a dotenv-style body. Returns the var + value, or null. */
|
|
24
|
+
function findBaseUrlAssignment(body) {
|
|
25
|
+
for (const line of body.split(/\r?\n/)) {
|
|
26
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
27
|
+
if (!m)
|
|
28
|
+
continue;
|
|
29
|
+
if (BASE_URL_VARS.includes(m[1])) {
|
|
30
|
+
return { var: m[1], value: m[2].replace(/^["']|["']$/g, "") };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Detect the project's OpenAI-config surface WITHOUT modifying anything. */
|
|
36
|
+
export function detectProject(cwd) {
|
|
37
|
+
const envFiles = [];
|
|
38
|
+
let existingBaseUrl;
|
|
39
|
+
for (const name of ENV_CANDIDATES) {
|
|
40
|
+
const body = readIfExists(path.join(cwd, name));
|
|
41
|
+
if (body === null)
|
|
42
|
+
continue;
|
|
43
|
+
envFiles.push(name);
|
|
44
|
+
if (!existingBaseUrl) {
|
|
45
|
+
const found = findBaseUrlAssignment(body);
|
|
46
|
+
if (found)
|
|
47
|
+
existingBaseUrl = { file: name, var: found.var, value: found.value };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let hasPackageJson = false;
|
|
51
|
+
let usesOpenAiSdk = false;
|
|
52
|
+
const pkgBody = readIfExists(path.join(cwd, "package.json"));
|
|
53
|
+
if (pkgBody !== null) {
|
|
54
|
+
hasPackageJson = true;
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(pkgBody);
|
|
57
|
+
usesOpenAiSdk = Boolean(pkg.dependencies?.openai || pkg.devDependencies?.openai);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* malformed package.json — leave usesOpenAiSdk false */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { envFiles, hasPackageJson, usesOpenAiSdk, ...(existingBaseUrl ? { existingBaseUrl } : {}) };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a configure plan for pointing this project's OpenAI client at `baseUrl` (the gateway `/v1` URL).
|
|
67
|
+
* Never writes. If a base URL is already configured, the plan action is "conflict" and apply requires force.
|
|
68
|
+
*/
|
|
69
|
+
export function planGatewayConfigure(cwd, baseUrl) {
|
|
70
|
+
const detection = detectProject(cwd);
|
|
71
|
+
const targetFile = detection.envFiles.includes(".env.local")
|
|
72
|
+
? ".env.local"
|
|
73
|
+
: detection.envFiles.includes(".env")
|
|
74
|
+
? ".env"
|
|
75
|
+
: ".env";
|
|
76
|
+
const targetPath = path.join(cwd, targetFile);
|
|
77
|
+
const existing = readIfExists(targetPath);
|
|
78
|
+
const newLine = `${CONFIGURE_ENV_VAR}=${baseUrl}`;
|
|
79
|
+
let action;
|
|
80
|
+
let diff;
|
|
81
|
+
let note;
|
|
82
|
+
if (detection.existingBaseUrl) {
|
|
83
|
+
action = "conflict";
|
|
84
|
+
diff =
|
|
85
|
+
`- ${detection.existingBaseUrl.var}=${detection.existingBaseUrl.value} (in ${detection.existingBaseUrl.file})\n` +
|
|
86
|
+
`+ ${newLine}`;
|
|
87
|
+
note =
|
|
88
|
+
`${detection.existingBaseUrl.file} already sets ${detection.existingBaseUrl.var}. ` +
|
|
89
|
+
"Compaction will NOT overwrite an existing provider base URL without explicit confirmation (--force).";
|
|
90
|
+
}
|
|
91
|
+
else if (existing === null) {
|
|
92
|
+
action = "create";
|
|
93
|
+
diff = `+ ${newLine}`;
|
|
94
|
+
note = `${targetFile} does not exist yet. Apply would create it with just the gateway base URL line.`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
action = "append";
|
|
98
|
+
diff = ` (existing ${targetFile} unchanged)\n+ ${newLine}`;
|
|
99
|
+
note = `Apply would append one line to ${targetFile} (a .bak backup is written first).`;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
targetFile,
|
|
103
|
+
action,
|
|
104
|
+
baseUrl,
|
|
105
|
+
diff,
|
|
106
|
+
...(existing !== null ? { backupFile: `${targetFile}.bak` } : {}),
|
|
107
|
+
detection,
|
|
108
|
+
note
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Apply a configure plan: write a `.bak` backup (when the target exists), then create/append the base-URL
|
|
113
|
+
* line. Refuses a "conflict" plan unless `force` is set. This is the ONLY function here that writes.
|
|
114
|
+
*/
|
|
115
|
+
export function applyGatewayConfigure(cwd, plan, force = false) {
|
|
116
|
+
if (plan.action === "conflict" && !force) {
|
|
117
|
+
return { wrote: false, targetFile: plan.targetFile, reason: "existing provider base URL present; re-run with --force to overwrite" };
|
|
118
|
+
}
|
|
119
|
+
const targetPath = path.join(cwd, plan.targetFile);
|
|
120
|
+
const existing = readIfExists(targetPath);
|
|
121
|
+
const newLine = `${CONFIGURE_ENV_VAR}=${plan.baseUrl}`;
|
|
122
|
+
let backupFile;
|
|
123
|
+
if (existing !== null) {
|
|
124
|
+
backupFile = `${plan.targetFile}.bak`;
|
|
125
|
+
fs.writeFileSync(path.join(cwd, backupFile), existing, "utf8");
|
|
126
|
+
}
|
|
127
|
+
let next;
|
|
128
|
+
if (plan.action === "conflict" && existing !== null) {
|
|
129
|
+
// Replace the existing base-URL assignment line in place.
|
|
130
|
+
next = existing
|
|
131
|
+
.split(/\r?\n/)
|
|
132
|
+
.map((line) => {
|
|
133
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=/);
|
|
134
|
+
return m && BASE_URL_VARS.includes(m[1]) ? newLine : line;
|
|
135
|
+
})
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
else if (existing !== null) {
|
|
139
|
+
next = existing.endsWith("\n") ? `${existing}${newLine}\n` : `${existing}\n${newLine}\n`;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
next = `${newLine}\n`;
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(targetPath, next, "utf8");
|
|
145
|
+
return { wrote: true, targetFile: plan.targetFile, ...(backupFile ? { backupFile } : {}) };
|
|
146
|
+
}
|
|
147
|
+
/** Render a configure plan as plain content-free lines (for the CLI preview). */
|
|
148
|
+
export function formatConfigurePlan(plan) {
|
|
149
|
+
const lines = [];
|
|
150
|
+
lines.push("compaction gateway configure — proposed change (nothing is written yet)");
|
|
151
|
+
lines.push(` target file: ${plan.targetFile} (${plan.action})`);
|
|
152
|
+
lines.push(` detected: ${describeDetection(plan.detection)}`);
|
|
153
|
+
lines.push(" diff:");
|
|
154
|
+
for (const d of plan.diff.split("\n"))
|
|
155
|
+
lines.push(` ${d}`);
|
|
156
|
+
lines.push(` note: ${plan.note}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(" to apply: compaction gateway configure --apply" + (plan.action === "conflict" ? " --force" : ""));
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
161
|
+
function describeDetection(d) {
|
|
162
|
+
const bits = [];
|
|
163
|
+
bits.push(d.envFiles.length ? `env files: ${d.envFiles.join(", ")}` : "no .env files");
|
|
164
|
+
bits.push(d.hasPackageJson ? (d.usesOpenAiSdk ? "openai SDK in package.json" : "package.json (no openai dep)") : "no package.json");
|
|
165
|
+
if (d.existingBaseUrl)
|
|
166
|
+
bits.push(`existing ${d.existingBaseUrl.var} in ${d.existingBaseUrl.file}`);
|
|
167
|
+
return bits.join("; ");
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=configure.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI usage parsing for the Compaction Gateway record mode (PUBLIC CLI/SDK code — engine-free,
|
|
3
|
+
* ships in the npm package). Charter 2026-07-04-run-15 — the first gateway implementation.
|
|
4
|
+
*
|
|
5
|
+
* This module is PURE and CONTENT-FREE by construction: it takes a response body (a non-streaming JSON
|
|
6
|
+
* body, or the tail of an SSE stream) and extracts ONLY the token-usage COUNTS + the model label. It
|
|
7
|
+
* never returns, stores, or logs any message / completion / tool text — only the numbers the provider
|
|
8
|
+
* itself reports in its `usage` object, plus the model name (metadata, not content).
|
|
9
|
+
*
|
|
10
|
+
* OpenAI usage shape (Chat Completions / Responses):
|
|
11
|
+
* usage.prompt_tokens — total input tokens the provider billed for
|
|
12
|
+
* usage.prompt_tokens_details.cached_tokens — the portion served from the provider prompt cache
|
|
13
|
+
* usage.completion_tokens — output tokens
|
|
14
|
+
* usage.completion_tokens_details.reasoning_tokens — reasoning/output-detail tokens (o-series), where present
|
|
15
|
+
*
|
|
16
|
+
* Honesty: a field is reported ONLY when the provider actually returned it. A missing axis is
|
|
17
|
+
* `unavailable` with a reason — NEVER a silent zero. `billed_fresh_input_tokens = prompt - cached`
|
|
18
|
+
* (the input the provider billed at full rate) is computed ONLY when BOTH prompt and cached are present.
|
|
19
|
+
*/
|
|
20
|
+
/** A normalized, content-free token breakdown from a provider `usage` object. */
|
|
21
|
+
export interface OpenAiUsageBreakdown {
|
|
22
|
+
/** True when a `usage` object with a numeric `prompt_tokens` (or output) was found. */
|
|
23
|
+
present: boolean;
|
|
24
|
+
/** Total input tokens the provider reports (usage.prompt_tokens), when present. */
|
|
25
|
+
promptInputTokens?: number;
|
|
26
|
+
/** Input tokens served from the provider prompt cache (prompt_tokens_details.cached_tokens), when present. */
|
|
27
|
+
cachedInputTokens?: number;
|
|
28
|
+
/** Fresh/billed-at-full-rate input tokens = prompt - cached (only when BOTH are present). */
|
|
29
|
+
billedFreshInputTokens?: number;
|
|
30
|
+
/** Output tokens (usage.completion_tokens), when present. */
|
|
31
|
+
outputTokens?: number;
|
|
32
|
+
/** Reasoning/output-detail tokens (completion_tokens_details.reasoning_tokens), when present. */
|
|
33
|
+
reasoningTokens?: number;
|
|
34
|
+
/** The model label the provider echoed (metadata, NOT content), when present. */
|
|
35
|
+
model?: string;
|
|
36
|
+
/** Honest reason the usage is unavailable, when `present` is false. */
|
|
37
|
+
unavailableReason?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract the content-free usage breakdown from an already-parsed OpenAI response object. Handles BOTH
|
|
41
|
+
* OpenAI APIs (their usage field names differ):
|
|
42
|
+
* - Chat Completions: `usage.prompt_tokens` / `completion_tokens` / `prompt_tokens_details.cached_tokens`
|
|
43
|
+
* / `completion_tokens_details.reasoning_tokens`.
|
|
44
|
+
* - Responses API: `usage.input_tokens` / `output_tokens` / `input_tokens_details.cached_tokens`
|
|
45
|
+
* / `output_tokens_details.reasoning_tokens`. Its SSE events wrap the payload under `response`
|
|
46
|
+
* (`{"type":"response.completed","response":{usage,model}}`), so we also look one level down.
|
|
47
|
+
*/
|
|
48
|
+
export declare function usageFromResponseObject(obj: unknown): OpenAiUsageBreakdown;
|
|
49
|
+
/**
|
|
50
|
+
* Extract usage from a raw response body string. Handles BOTH shapes byte-safely (it only READS a copy):
|
|
51
|
+
* - non-streaming JSON: `JSON.parse(body).usage`.
|
|
52
|
+
* - streaming SSE: the LAST `data:` line carrying a `usage` object (present only when the caller sent
|
|
53
|
+
* `stream_options.include_usage: true`). The `[DONE]` sentinel is ignored.
|
|
54
|
+
* Any parse failure → `present: false` with an honest reason (never throws; the proxy stays byte-safe).
|
|
55
|
+
*/
|
|
56
|
+
export declare function usageFromResponseBody(body: string): OpenAiUsageBreakdown;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI usage parsing for the Compaction Gateway record mode (PUBLIC CLI/SDK code — engine-free,
|
|
3
|
+
* ships in the npm package). Charter 2026-07-04-run-15 — the first gateway implementation.
|
|
4
|
+
*
|
|
5
|
+
* This module is PURE and CONTENT-FREE by construction: it takes a response body (a non-streaming JSON
|
|
6
|
+
* body, or the tail of an SSE stream) and extracts ONLY the token-usage COUNTS + the model label. It
|
|
7
|
+
* never returns, stores, or logs any message / completion / tool text — only the numbers the provider
|
|
8
|
+
* itself reports in its `usage` object, plus the model name (metadata, not content).
|
|
9
|
+
*
|
|
10
|
+
* OpenAI usage shape (Chat Completions / Responses):
|
|
11
|
+
* usage.prompt_tokens — total input tokens the provider billed for
|
|
12
|
+
* usage.prompt_tokens_details.cached_tokens — the portion served from the provider prompt cache
|
|
13
|
+
* usage.completion_tokens — output tokens
|
|
14
|
+
* usage.completion_tokens_details.reasoning_tokens — reasoning/output-detail tokens (o-series), where present
|
|
15
|
+
*
|
|
16
|
+
* Honesty: a field is reported ONLY when the provider actually returned it. A missing axis is
|
|
17
|
+
* `unavailable` with a reason — NEVER a silent zero. `billed_fresh_input_tokens = prompt - cached`
|
|
18
|
+
* (the input the provider billed at full rate) is computed ONLY when BOTH prompt and cached are present.
|
|
19
|
+
*/
|
|
20
|
+
function num(value) {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
function obj_(value) {
|
|
24
|
+
return value && typeof value === "object" ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extract the content-free usage breakdown from an already-parsed OpenAI response object. Handles BOTH
|
|
28
|
+
* OpenAI APIs (their usage field names differ):
|
|
29
|
+
* - Chat Completions: `usage.prompt_tokens` / `completion_tokens` / `prompt_tokens_details.cached_tokens`
|
|
30
|
+
* / `completion_tokens_details.reasoning_tokens`.
|
|
31
|
+
* - Responses API: `usage.input_tokens` / `output_tokens` / `input_tokens_details.cached_tokens`
|
|
32
|
+
* / `output_tokens_details.reasoning_tokens`. Its SSE events wrap the payload under `response`
|
|
33
|
+
* (`{"type":"response.completed","response":{usage,model}}`), so we also look one level down.
|
|
34
|
+
*/
|
|
35
|
+
export function usageFromResponseObject(obj) {
|
|
36
|
+
if (!obj || typeof obj !== "object") {
|
|
37
|
+
return { present: false, unavailableReason: "response body was not a JSON object" };
|
|
38
|
+
}
|
|
39
|
+
const root = obj;
|
|
40
|
+
// The Responses API SSE stream nests the completed response under `response`.
|
|
41
|
+
const nested = obj_(root.response);
|
|
42
|
+
const usage = obj_(root.usage) ?? obj_(nested?.usage);
|
|
43
|
+
const model = typeof root.model === "string" ? root.model : typeof nested?.model === "string" ? nested.model : undefined;
|
|
44
|
+
if (!usage) {
|
|
45
|
+
return {
|
|
46
|
+
present: false,
|
|
47
|
+
...(model ? { model } : {}),
|
|
48
|
+
unavailableReason: "the provider response carried no usage object (e.g. a streamed response without usage/include_usage)"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Accept both APIs' field names (Chat Completions ?? Responses API).
|
|
52
|
+
const promptInputTokens = num(usage.prompt_tokens) ?? num(usage.input_tokens);
|
|
53
|
+
const outputTokens = num(usage.completion_tokens) ?? num(usage.output_tokens);
|
|
54
|
+
const cachedInputTokens = num(obj_(usage.prompt_tokens_details)?.cached_tokens) ?? num(obj_(usage.input_tokens_details)?.cached_tokens);
|
|
55
|
+
const reasoningTokens = num(obj_(usage.completion_tokens_details)?.reasoning_tokens) ?? num(obj_(usage.output_tokens_details)?.reasoning_tokens);
|
|
56
|
+
if (promptInputTokens === undefined && outputTokens === undefined) {
|
|
57
|
+
return {
|
|
58
|
+
present: false,
|
|
59
|
+
...(model ? { model } : {}),
|
|
60
|
+
unavailableReason: "the usage object carried no numeric input/prompt or output/completion tokens"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const billedFreshInputTokens = promptInputTokens !== undefined && cachedInputTokens !== undefined
|
|
64
|
+
? Math.max(0, promptInputTokens - cachedInputTokens)
|
|
65
|
+
: undefined;
|
|
66
|
+
return {
|
|
67
|
+
present: true,
|
|
68
|
+
...(promptInputTokens !== undefined ? { promptInputTokens } : {}),
|
|
69
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
70
|
+
...(billedFreshInputTokens !== undefined ? { billedFreshInputTokens } : {}),
|
|
71
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
72
|
+
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
73
|
+
...(model ? { model } : {})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract usage from a raw response body string. Handles BOTH shapes byte-safely (it only READS a copy):
|
|
78
|
+
* - non-streaming JSON: `JSON.parse(body).usage`.
|
|
79
|
+
* - streaming SSE: the LAST `data:` line carrying a `usage` object (present only when the caller sent
|
|
80
|
+
* `stream_options.include_usage: true`). The `[DONE]` sentinel is ignored.
|
|
81
|
+
* Any parse failure → `present: false` with an honest reason (never throws; the proxy stays byte-safe).
|
|
82
|
+
*/
|
|
83
|
+
export function usageFromResponseBody(body) {
|
|
84
|
+
const trimmed = body.trim();
|
|
85
|
+
if (trimmed === "")
|
|
86
|
+
return { present: false, unavailableReason: "empty response body" };
|
|
87
|
+
// Non-streaming JSON first.
|
|
88
|
+
if (trimmed.startsWith("{")) {
|
|
89
|
+
try {
|
|
90
|
+
return usageFromResponseObject(JSON.parse(trimmed));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { present: false, unavailableReason: "response body was not parseable JSON (may be truncated)" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Streaming SSE: scan for the LAST `data:` payload that parses and carries a usage object.
|
|
97
|
+
let best;
|
|
98
|
+
let lastModel;
|
|
99
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
100
|
+
const s = line.trim();
|
|
101
|
+
if (!s.startsWith("data:"))
|
|
102
|
+
continue;
|
|
103
|
+
const payload = s.slice("data:".length).trim();
|
|
104
|
+
if (payload === "" || payload === "[DONE]")
|
|
105
|
+
continue;
|
|
106
|
+
let obj;
|
|
107
|
+
try {
|
|
108
|
+
obj = JSON.parse(payload);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue; // a partial/truncated SSE line — skip it
|
|
112
|
+
}
|
|
113
|
+
if (obj && typeof obj === "object" && typeof obj.model === "string") {
|
|
114
|
+
lastModel = obj.model;
|
|
115
|
+
}
|
|
116
|
+
const b = usageFromResponseObject(obj);
|
|
117
|
+
if (b.present)
|
|
118
|
+
best = b; // keep the latest present usage (OpenAI emits it in the final chunk)
|
|
119
|
+
}
|
|
120
|
+
if (best)
|
|
121
|
+
return best;
|
|
122
|
+
return {
|
|
123
|
+
present: false,
|
|
124
|
+
...(lastModel ? { model: lastModel } : {}),
|
|
125
|
+
unavailableReason: "streamed response carried no usage chunk (send stream_options.include_usage:true to get one)"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=openai-usage.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { OpenAiUsageBreakdown } from "./openai-usage.js";
|
|
2
|
+
/** The gateway modes (run-15 implements ONLY `record`). */
|
|
3
|
+
export type GatewayMode = "record" | "cache" | "apply";
|
|
4
|
+
/** Per-axis honesty label — mirrors the cross-surface `token_source` discipline. */
|
|
5
|
+
export type SourceLabel = "provider-reported" | "local-estimate" | "unavailable";
|
|
6
|
+
/** The default local-only receipts location (gitignored — never committed, never uploaded). */
|
|
7
|
+
export declare const DEFAULT_GATEWAY_RECEIPTS_DIR = ".compaction/gateway";
|
|
8
|
+
export declare const GATEWAY_RECEIPTS_FILE = "receipts.jsonl";
|
|
9
|
+
/**
|
|
10
|
+
* The honest, content-free claim-boundary label carried on every record-mode receipt.
|
|
11
|
+
*
|
|
12
|
+
* CLAIM BOUNDARY (founder-set, run-15): a provider-backed **fresh/billed input-token reduction** MAY be
|
|
13
|
+
* displayed when the provider reports cached input tokens (the provider served part of the input from its
|
|
14
|
+
* prompt cache, so fewer input tokens were billed fresh — with model-visible bytes unchanged). What is
|
|
15
|
+
* NOT claimed on this path: any model-visible input reduction, any output-token reduction, or any
|
|
16
|
+
* cost-savings figure (cost is unavailable — the provider reports tokens, not billing).
|
|
17
|
+
*/
|
|
18
|
+
export declare const GATEWAY_RECORD_LABEL: string;
|
|
19
|
+
/** A provider-backed fresh/billed input-token reduction, or an honest unavailable (never a zero savings). */
|
|
20
|
+
export interface FreshBilledInputReduction {
|
|
21
|
+
available: boolean;
|
|
22
|
+
/** Percent of input tokens the provider served from cache (= cached / prompt · 100), when available. */
|
|
23
|
+
pct?: number;
|
|
24
|
+
/** Honest basis (available) or reason (unavailable). */
|
|
25
|
+
note: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compute the provider-backed fresh/billed input reduction. Available ONLY when the provider reported
|
|
29
|
+
* BOTH prompt input tokens (> 0) and cached input tokens (> 0): the cached portion was NOT billed as
|
|
30
|
+
* fresh input, so fresh/billed input is reduced by `cached / prompt`. Otherwise unavailable-with-reason —
|
|
31
|
+
* NEVER a zero "savings".
|
|
32
|
+
*/
|
|
33
|
+
export declare function freshBilledInputReduction(usage: {
|
|
34
|
+
promptInputTokens?: number;
|
|
35
|
+
cachedInputTokens?: number;
|
|
36
|
+
}): FreshBilledInputReduction;
|
|
37
|
+
/** The compact display string per the claim boundary (allowed forms only). */
|
|
38
|
+
export declare function formatFreshBilledInputReduction(r: FreshBilledInputReduction): string;
|
|
39
|
+
export interface GatewayReceipt {
|
|
40
|
+
/** Content-free unique id (random UUID — never derived from content). */
|
|
41
|
+
receipt_id: string;
|
|
42
|
+
/** ISO timestamp the receipt was recorded. */
|
|
43
|
+
captured_at: string;
|
|
44
|
+
provider: string;
|
|
45
|
+
/** Model label the provider echoed, or "unknown" (never inferred). */
|
|
46
|
+
model: string;
|
|
47
|
+
/** The request path (e.g. `/v1/chat/completions`) — an endpoint label, not content. */
|
|
48
|
+
endpoint: string;
|
|
49
|
+
mode: GatewayMode;
|
|
50
|
+
/** The upstream HTTP status code. */
|
|
51
|
+
upstream_status: number;
|
|
52
|
+
/** record mode NEVER changes model-visible bytes. Always false in run-15. */
|
|
53
|
+
model_visible_bytes_changed: boolean;
|
|
54
|
+
/** Per-axis token COUNTS (only present axes ride here — never a silent zero). */
|
|
55
|
+
tokens: {
|
|
56
|
+
prompt_input?: number;
|
|
57
|
+
cached_input?: number;
|
|
58
|
+
billed_fresh_input?: number;
|
|
59
|
+
output?: number;
|
|
60
|
+
reasoning?: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Provider-backed fresh/billed input-token reduction (allowed claim): available ONLY when the provider
|
|
64
|
+
* reported cached input tokens; otherwise unavailable-with-reason (never a zero savings). This is the
|
|
65
|
+
* ONLY reduction this record-mode receipt asserts — never model-visible input, output, or cost.
|
|
66
|
+
*/
|
|
67
|
+
fresh_billed_input_reduction: FreshBilledInputReduction;
|
|
68
|
+
/** Where the token counts came from. */
|
|
69
|
+
token_source: SourceLabel;
|
|
70
|
+
/** Where the cache-token count came from (provider-reported only when the response exposed cached_tokens). */
|
|
71
|
+
cache_source: SourceLabel;
|
|
72
|
+
/** Cost is always unavailable on this path (provider reports tokens, not billing). */
|
|
73
|
+
cost_source: "unavailable";
|
|
74
|
+
/** Honest per-axis reasons for any unavailable axis (never a bare "unavailable"). */
|
|
75
|
+
reasons: {
|
|
76
|
+
token?: string;
|
|
77
|
+
cache?: string;
|
|
78
|
+
cost: string;
|
|
79
|
+
};
|
|
80
|
+
/** Single request → run-scoped, never generalized. */
|
|
81
|
+
claim_scope: "run-scoped";
|
|
82
|
+
/** record mode applies nothing → nothing to approve. Apply/dry-run receipts carry the explicit source. */
|
|
83
|
+
approval_status: "not-required" | "explicit-mode" | "explicit-header" | "explicit-dry-run";
|
|
84
|
+
/** Local-only by default; nothing is uploaded. */
|
|
85
|
+
sync_status: "local-only";
|
|
86
|
+
content_uploaded: false;
|
|
87
|
+
/**
|
|
88
|
+
* Optional CLIENT-SET proof-run id (an opaque grouping label, NOT content) — present only when the
|
|
89
|
+
* client sent an `x-compaction-proof-run` header. The gateway only READS it (never injects/mutates the
|
|
90
|
+
* request), and it is used purely to pair receipts of a manual proof run. Content-free by construction.
|
|
91
|
+
*/
|
|
92
|
+
proof_run_id?: string;
|
|
93
|
+
/** The honest content-free label (never upgraded to a saving/cost claim). */
|
|
94
|
+
label: string;
|
|
95
|
+
/** The deterministic policy that ran (e.g. `deterministic-dedupe`). */
|
|
96
|
+
policy?: string;
|
|
97
|
+
/** True ONLY when apply actually changed the request body (before/after evidence exists via recovery). */
|
|
98
|
+
request_mutated?: boolean;
|
|
99
|
+
/** The gateway NEVER changes the response. Always false when present. */
|
|
100
|
+
response_mutated?: boolean;
|
|
101
|
+
/** dry-run: a safe mutation was possible but NOT applied (the original was forwarded unchanged). */
|
|
102
|
+
candidate_available?: boolean;
|
|
103
|
+
/** LOCAL-ESTIMATE (chars/4) model-visible input over the safe text fields — never provider-reported. */
|
|
104
|
+
estimated_input_tokens_before?: number;
|
|
105
|
+
estimated_input_tokens_after?: number;
|
|
106
|
+
/** Estimated model-visible input reduction percent over the safe fields (apply/dry-run candidate). */
|
|
107
|
+
estimated_model_visible_input_reduction_percent?: number;
|
|
108
|
+
/** Where the before/after ESTIMATES came from (always local-estimate — chars/4). */
|
|
109
|
+
token_source_before?: SourceLabel;
|
|
110
|
+
/** Where the AFTER token counts came from (provider-reported when the call returned usage, else local). */
|
|
111
|
+
token_source_after?: SourceLabel;
|
|
112
|
+
/** Pointer to the locally-retained original request body (content lives in the recovery store, not here). */
|
|
113
|
+
recovery_id?: string;
|
|
114
|
+
/** Why apply did NOT change the request (fail-closed shape, conflict, or no safe duplicate found). */
|
|
115
|
+
fail_closed_reason?: string;
|
|
116
|
+
/** The honest apply claim string (only asserts a model-visible reduction when request_mutated is true). */
|
|
117
|
+
apply_label?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build ONE content-free gateway receipt from the request metadata + the provider's reported usage.
|
|
121
|
+
* `now`/`id` are injectable for deterministic tests. CONTENT-FREE: only `usage`'s numeric fields, the
|
|
122
|
+
* model label, the endpoint path, and the status ride on the receipt.
|
|
123
|
+
*/
|
|
124
|
+
export declare function buildGatewayReceipt(params: {
|
|
125
|
+
provider: string;
|
|
126
|
+
endpoint: string;
|
|
127
|
+
mode: GatewayMode;
|
|
128
|
+
upstreamStatus: number;
|
|
129
|
+
usage: OpenAiUsageBreakdown;
|
|
130
|
+
/** Model from the request (content-free metadata), used only if the response did not echo one. */
|
|
131
|
+
requestModel?: string;
|
|
132
|
+
/** Optional client-set proof-run id (from an `x-compaction-proof-run` header) — an opaque grouping label. */
|
|
133
|
+
proofRunId?: string;
|
|
134
|
+
now?: () => string;
|
|
135
|
+
id?: () => string;
|
|
136
|
+
}): GatewayReceipt;
|
|
137
|
+
/** Append a receipt to the local-only JSONL store under `<cwd>/.compaction/gateway/`. Content-free. */
|
|
138
|
+
export declare function appendGatewayReceipt(receipt: GatewayReceipt, cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction Gateway RECEIPT — the content-free ground-truth record of one gateway pass (PUBLIC CLI/SDK
|
|
3
|
+
* code — engine-free). Charter 2026-07-04-run-15. Per the canonical architecture
|
|
4
|
+
* (`docs/design/compaction-gateway-architecture.md` §C.2), a receipt carries ONLY counts / structure /
|
|
5
|
+
* labels — never messages, completions, tool outputs, or any request/response content.
|
|
6
|
+
*
|
|
7
|
+
* Run-15 ships **record mode** only: the gateway forwards the request and response byte-for-byte and
|
|
8
|
+
* records this receipt. So `mode: "record"` and `model_visible_bytes_changed: false` always. There is NO
|
|
9
|
+
* optimization and therefore NO savings claim on this receipt — it DESCRIBES the provider's own reported
|
|
10
|
+
* token/cache usage honestly; it never asserts a reduction. Cost is always `unavailable` on this path
|
|
11
|
+
* (the provider reports tokens, not a billing figure).
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { mkdir, appendFile } from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
/** The default local-only receipts location (gitignored — never committed, never uploaded). */
|
|
17
|
+
export const DEFAULT_GATEWAY_RECEIPTS_DIR = ".compaction/gateway";
|
|
18
|
+
export const GATEWAY_RECEIPTS_FILE = "receipts.jsonl";
|
|
19
|
+
/**
|
|
20
|
+
* The honest, content-free claim-boundary label carried on every record-mode receipt.
|
|
21
|
+
*
|
|
22
|
+
* CLAIM BOUNDARY (founder-set, run-15): a provider-backed **fresh/billed input-token reduction** MAY be
|
|
23
|
+
* displayed when the provider reports cached input tokens (the provider served part of the input from its
|
|
24
|
+
* prompt cache, so fewer input tokens were billed fresh — with model-visible bytes unchanged). What is
|
|
25
|
+
* NOT claimed on this path: any model-visible input reduction, any output-token reduction, or any
|
|
26
|
+
* cost-savings figure (cost is unavailable — the provider reports tokens, not billing).
|
|
27
|
+
*/
|
|
28
|
+
export const GATEWAY_RECORD_LABEL = "record mode: request and response forwarded BYTE-FOR-BYTE (model-visible bytes unchanged). Provider-backed " +
|
|
29
|
+
"fresh/billed input reduction may be displayed when the provider reports cached input tokens; no model-visible " +
|
|
30
|
+
"input reduction, output-token reduction, or cost-savings claim is made (cost is unavailable on this path).";
|
|
31
|
+
/**
|
|
32
|
+
* Compute the provider-backed fresh/billed input reduction. Available ONLY when the provider reported
|
|
33
|
+
* BOTH prompt input tokens (> 0) and cached input tokens (> 0): the cached portion was NOT billed as
|
|
34
|
+
* fresh input, so fresh/billed input is reduced by `cached / prompt`. Otherwise unavailable-with-reason —
|
|
35
|
+
* NEVER a zero "savings".
|
|
36
|
+
*/
|
|
37
|
+
export function freshBilledInputReduction(usage) {
|
|
38
|
+
const prompt = usage.promptInputTokens;
|
|
39
|
+
const cached = usage.cachedInputTokens;
|
|
40
|
+
if (prompt !== undefined && prompt > 0 && cached !== undefined && cached > 0) {
|
|
41
|
+
const pct = Math.round((cached / prompt) * 1000) / 10; // one decimal place
|
|
42
|
+
return {
|
|
43
|
+
available: true,
|
|
44
|
+
pct,
|
|
45
|
+
note: "provider-reported cached input tokens → fresh/billed input reduced; model-visible bytes unchanged"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
available: false,
|
|
50
|
+
note: cached === undefined || cached === 0
|
|
51
|
+
? "the provider reported no cached input tokens for this request — fresh/billed input reduction unavailable"
|
|
52
|
+
: "the provider reported no prompt input tokens — fresh/billed input reduction unavailable"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The compact display string per the claim boundary (allowed forms only). */
|
|
56
|
+
export function formatFreshBilledInputReduction(r) {
|
|
57
|
+
return r.available ? `-${r.pct}% fresh/billed input` : "fresh/billed input reduction: unavailable";
|
|
58
|
+
}
|
|
59
|
+
const COST_UNAVAILABLE_REASON = "the OpenAI response reports token usage but no cost or billing figure; no cost data exists on the gateway record path";
|
|
60
|
+
/**
|
|
61
|
+
* Build ONE content-free gateway receipt from the request metadata + the provider's reported usage.
|
|
62
|
+
* `now`/`id` are injectable for deterministic tests. CONTENT-FREE: only `usage`'s numeric fields, the
|
|
63
|
+
* model label, the endpoint path, and the status ride on the receipt.
|
|
64
|
+
*/
|
|
65
|
+
export function buildGatewayReceipt(params) {
|
|
66
|
+
const now = params.now ?? (() => new Date().toISOString());
|
|
67
|
+
const id = params.id ?? (() => randomUUID());
|
|
68
|
+
const u = params.usage;
|
|
69
|
+
const model = u.model ?? params.requestModel ?? "unknown";
|
|
70
|
+
const tokens = {
|
|
71
|
+
...(u.promptInputTokens !== undefined ? { prompt_input: u.promptInputTokens } : {}),
|
|
72
|
+
...(u.cachedInputTokens !== undefined ? { cached_input: u.cachedInputTokens } : {}),
|
|
73
|
+
...(u.billedFreshInputTokens !== undefined ? { billed_fresh_input: u.billedFreshInputTokens } : {}),
|
|
74
|
+
...(u.outputTokens !== undefined ? { output: u.outputTokens } : {}),
|
|
75
|
+
...(u.reasoningTokens !== undefined ? { reasoning: u.reasoningTokens } : {})
|
|
76
|
+
};
|
|
77
|
+
const tokenSource = u.present ? "provider-reported" : "unavailable";
|
|
78
|
+
const cacheSource = u.cachedInputTokens !== undefined ? "provider-reported" : "unavailable";
|
|
79
|
+
const reduction = freshBilledInputReduction({
|
|
80
|
+
...(u.promptInputTokens !== undefined ? { promptInputTokens: u.promptInputTokens } : {}),
|
|
81
|
+
...(u.cachedInputTokens !== undefined ? { cachedInputTokens: u.cachedInputTokens } : {})
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
receipt_id: id(),
|
|
85
|
+
captured_at: now(),
|
|
86
|
+
provider: params.provider,
|
|
87
|
+
model,
|
|
88
|
+
endpoint: params.endpoint,
|
|
89
|
+
mode: params.mode,
|
|
90
|
+
upstream_status: params.upstreamStatus,
|
|
91
|
+
model_visible_bytes_changed: false,
|
|
92
|
+
tokens,
|
|
93
|
+
fresh_billed_input_reduction: reduction,
|
|
94
|
+
token_source: tokenSource,
|
|
95
|
+
cache_source: cacheSource,
|
|
96
|
+
cost_source: "unavailable",
|
|
97
|
+
reasons: {
|
|
98
|
+
...(u.present ? {} : { token: u.unavailableReason ?? "no usage reported by the provider" }),
|
|
99
|
+
...(cacheSource === "unavailable"
|
|
100
|
+
? { cache: "the provider response did not report cached input tokens (prompt_tokens_details.cached_tokens) for this request" }
|
|
101
|
+
: {}),
|
|
102
|
+
cost: COST_UNAVAILABLE_REASON
|
|
103
|
+
},
|
|
104
|
+
claim_scope: "run-scoped",
|
|
105
|
+
approval_status: "not-required",
|
|
106
|
+
sync_status: "local-only",
|
|
107
|
+
content_uploaded: false,
|
|
108
|
+
...(params.proofRunId ? { proof_run_id: params.proofRunId } : {}),
|
|
109
|
+
label: GATEWAY_RECORD_LABEL
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Append a receipt to the local-only JSONL store under `<cwd>/.compaction/gateway/`. Content-free. */
|
|
113
|
+
export async function appendGatewayReceipt(receipt, cwd = process.cwd()) {
|
|
114
|
+
const dir = path.join(cwd, DEFAULT_GATEWAY_RECEIPTS_DIR);
|
|
115
|
+
await mkdir(dir, { recursive: true });
|
|
116
|
+
const file = path.join(dir, GATEWAY_RECEIPTS_FILE);
|
|
117
|
+
await appendFile(file, `${JSON.stringify(receipt)}\n`, "utf8");
|
|
118
|
+
return file;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=receipt.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const GATEWAY_RECOVERY_DIR = ".compaction/gateway/recovery";
|
|
2
|
+
export interface RecoveryRecord {
|
|
3
|
+
recovery_id: string;
|
|
4
|
+
captured_at: string;
|
|
5
|
+
endpoint: string;
|
|
6
|
+
policy: string;
|
|
7
|
+
/** The EXACT original request body (pre-mutation). Local-only; the receipt never carries this. */
|
|
8
|
+
original_body: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Persist the original request body for recovery and return a fresh `recovery_id`. Best-effort restrictive
|
|
12
|
+
* permissions (0700 dir, 0600 file). Synchronous + fail-safe: throws only if the write genuinely fails
|
|
13
|
+
* (the caller treats a failure as "do NOT mutate" — apply must never proceed without a saved original).
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveOriginalForRecovery(cwd: string, params: {
|
|
16
|
+
endpoint: string;
|
|
17
|
+
policy: string;
|
|
18
|
+
originalBody: string;
|
|
19
|
+
now?: () => string;
|
|
20
|
+
id?: () => string;
|
|
21
|
+
}): string;
|
|
22
|
+
/** Read a recovery record back by id, or null when it does not exist / cannot be parsed. */
|
|
23
|
+
export declare function readRecovery(cwd: string, recoveryId: string): RecoveryRecord | null;
|