costlayers 0.8.11 → 0.8.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # CostLayers CLI
2
2
 
3
- CostLayers helps teams reduce wasted AI coding-agent spend by creating a compact repo context pack, an OpenAI-compatible gateway, and a savings dashboard.
3
+ CostLayers helps coding-agent users stop paying for repeated repo context. API users can route model calls through the gateway for invoice savings. ChatGPT-login Codex users get a usage-stretch meter that shows how much repeated context was avoided.
4
4
 
5
5
  ## Install
6
6
 
@@ -30,8 +30,31 @@ npx -y costlayers@latest start --email you@example.com -- codex
30
30
  This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
31
31
  so it can avoid repeated broad repo exploration. It also writes
32
32
  `~/.codex/costlayers.config.toml` and runs Codex as
33
- `codex --profile costlayers`, enabling gateway metering and redacted Codex token
34
- telemetry for ChatGPT-login sessions.
33
+ `codex --profile costlayers`, enabling redacted Codex telemetry for
34
+ ChatGPT-login sessions while preserving Codex's native model provider.
35
+
36
+ This mode is for getting more useful work out of the same ChatGPT/Codex plan.
37
+ It does not reduce a flat ChatGPT subscription invoice.
38
+
39
+ ## Invoice Savings Mode
40
+
41
+ To reduce an OpenAI Platform invoice, route API-key billed model calls through
42
+ the CostLayers gateway. This requires an OpenAI Platform API key with Responses
43
+ API write permission:
44
+
45
+ ```bash
46
+ export OPENAI_API_KEY=sk-proj-...
47
+ npx -y costlayers@latest start --email you@example.com --codex-proxy -- codex
48
+ ```
49
+
50
+ ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
51
+ Platform invoice savings because it is not billed through your Platform API key.
52
+
53
+ ## Which Mode Should I Use?
54
+
55
+ - ChatGPT-login Codex: use `start -- codex` to reduce repeated repo context and stretch usage limits.
56
+ - OpenAI Platform API billing: use `--codex-proxy` with `OPENAI_API_KEY` for invoice-backed savings.
57
+ - Other OpenAI-compatible clients: point the client at the CostLayers gateway URL and check `costlayers gateway report`.
35
58
 
36
59
  To install only the Codex profile after signup:
37
60
 
@@ -50,9 +73,10 @@ costlayers gateway report
50
73
  costlayers dashboard
51
74
  ```
52
75
 
53
- Metered dashboard savings only increase when model API traffic is routed through
54
- the CostLayers gateway URL. Codex-metered dashboard savings increase when Codex
55
- uses the CostLayers profile or emits token telemetry to the CostLayers meter.
76
+ Metered gateway savings increase when model API traffic is routed through the
77
+ CostLayers gateway URL with a provider API key. Codex-metered dashboard savings
78
+ increase when Codex uses the CostLayers profile or emits token telemetry to the
79
+ CostLayers meter.
56
80
  For ChatGPT-login Codex, dollar savings are modeled from observed Codex token
57
81
  events because the ChatGPT plan invoice is not an OpenAI Platform API invoice.
58
82
 
package/bin/agentspend.js CHANGED
@@ -9,7 +9,8 @@ const https = require("https");
9
9
  const os = require("os");
10
10
  const { spawnSync } = require("child_process");
11
11
 
12
- const VERSION = "0.8.11";
12
+ const VERSION = "0.8.14";
13
+ const INSTALL_SPEC = "costlayers@latest";
13
14
  const DEFAULT_RUNS_PER_WEEK = 20;
14
15
  const WEEKS_PER_MONTH = 4.33;
15
16
  const DEFAULT_EXCLUDES = new Set([
@@ -51,9 +52,9 @@ CostLayers ${VERSION}
51
52
  Usage:
52
53
  costlayers init [--repo <path>]
53
54
  costlayers scan [--repo <path>] [--price-per-1m <usd>] [--tasks <n>] [--runs-per-week <n>]
54
- costlayers start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [--runs-per-week <n>] [-- <agent command>]
55
+ costlayers start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [--runs-per-week <n>] [--codex-proxy] [-- <agent command>]
55
56
  costlayers signup [--email <email>] [--engine-url <url>]
56
- costlayers codex-profile [--repo <path>]
57
+ costlayers codex-profile [--repo <path>] [--codex-proxy]
57
58
  costlayers connect --engine-url <url> [--api-key <key>]
58
59
  costlayers gateway start [--provider-url <url>] [--api-key-env <name>] [--mode measure|reduce]
59
60
  costlayers gateway report
@@ -122,7 +123,7 @@ function guardRepoRoot(repo, args) {
122
123
  "",
123
124
  "Run it inside a project folder instead:",
124
125
  " cd path/to/your-repo",
125
- " npx -y costlayers@latest start --email you@example.com",
126
+ ` npx -y ${INSTALL_SPEC} start --email you@example.com`,
126
127
  "",
127
128
  "Or pass --repo path/to/your-repo from anywhere.",
128
129
  "If you really intend to scan your whole home directory, add --allow-home.",
@@ -506,6 +507,7 @@ function printSavingsSummary(report) {
506
507
  const projection = savingsProjection(report);
507
508
  const avoided = Number(report.tokens_avoided_per_repeated_task || 0);
508
509
  process.stdout.write(`\n${avoided > 0 ? "CostLayers found repeated context waste" : "CostLayers built your repo context pack"}\n`);
510
+ process.stdout.write(` Evidence: context estimate from local repo scan\n`);
509
511
  process.stdout.write(` Tokens avoided per repeated task: ${formatInt(report.tokens_avoided_per_repeated_task)}\n`);
510
512
  if (avoided > 0) {
511
513
  process.stdout.write(` Before vs after context: ${formatInt(report.baseline_broad_read_tokens)} -> ${formatInt(report.context_pack_tokens)} tokens (${report.estimated_reduction_percent}% less)\n`);
@@ -513,8 +515,9 @@ function printSavingsSummary(report) {
513
515
  process.stdout.write(` No repeated-context reduction found yet for this small/simple repo.\n`);
514
516
  process.stdout.write(` Context pack size: ${formatInt(report.context_pack_tokens)} tokens from ${formatInt(report.source_tokens_indexed)} indexed source tokens\n`);
515
517
  }
516
- process.stdout.write(` Modeled savings per ${formatInt(report.repeated_tasks_modeled)} repeated tasks: ${formatUsd(report.estimated_usd_saved)}\n`);
517
- process.stdout.write(` Projected at ${formatInt(projection.runsPerWeek)} agent runs/week: ${formatUsd(projection.weeklyUsd)}/week, ${formatUsd(projection.monthlyUsd)}/month\n`);
518
+ process.stdout.write(` Estimated waste value per ${formatInt(report.repeated_tasks_modeled)} repeated tasks: ${formatUsd(report.estimated_usd_saved)}\n`);
519
+ process.stdout.write(` Usage-stretch estimate at ${formatInt(projection.runsPerWeek)} agent runs/week: ${formatUsd(projection.weeklyUsd)}/week, ${formatUsd(projection.monthlyUsd)}/month\n`);
520
+ process.stdout.write(` Invoice savings require API traffic through CostLayers invoice mode.\n`);
518
521
  process.stdout.write(` Source tokens indexed: ${formatInt(report.source_tokens_indexed)}\n`);
519
522
  process.stdout.write(` Compact repo pack: ${formatInt(report.context_pack_tokens)} tokens\n`);
520
523
  }
@@ -523,22 +526,58 @@ function codexHomeDir() {
523
526
  return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
524
527
  }
525
528
 
526
- function profileTomlString(connection) {
529
+ function codexProxyEnabled(args = {}) {
530
+ return Boolean(args["codex-proxy"] || args["proxy-codex"]);
531
+ }
532
+
533
+ function codexProxyApiKeyEnv(args = {}) {
534
+ return String(args["api-key-env"] || "OPENAI_API_KEY");
535
+ }
536
+
537
+ function assertCodexProxyApiKey(args = {}) {
538
+ if (!codexProxyEnabled(args)) return;
539
+ const keyEnv = codexProxyApiKeyEnv(args);
540
+ if (process.env[keyEnv]) return;
541
+ process.stderr.write([
542
+ `CostLayers invoice mode needs ${keyEnv} in this shell.`,
543
+ "",
544
+ "Set an OpenAI Platform API key with Responses API write permission, then rerun:",
545
+ ` export ${keyEnv}=sk-proj-...`,
546
+ ` npx -y ${INSTALL_SPEC} start --email you@example.com --codex-proxy -- codex`,
547
+ "",
548
+ "ChatGPT-login Codex can be metered, but it cannot produce provider invoice savings because there is no per-request Platform invoice to reduce.",
549
+ ""
550
+ ].join("\n"));
551
+ process.exit(2);
552
+ }
553
+
554
+ function profileTomlString(connection, args = {}) {
527
555
  const gateway = String(connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace(/\/+$/, "");
528
556
  const baseUrl = `${gateway}/v1`;
529
557
  const engineUrl = String(connection.engine_url || "https://costlayers.com/engine").replace(/\/+$/, "");
530
- return [
531
- "# Generated by CostLayers. This profile routes Codex through the CostLayers meter.",
558
+ const apiKeyEnv = codexProxyApiKeyEnv(args);
559
+ const lines = [
560
+ "# Generated by CostLayers. This profile sends Codex telemetry to the CostLayers meter.",
532
561
  "# Keep this file private because it contains your keyed CostLayers endpoint.",
533
- "",
562
+ "# Default mode preserves Codex's native model provider/auth, which works with ChatGPT-login Codex.",
563
+ ""
564
+ ];
565
+ if (codexProxyEnabled(args)) {
566
+ lines.push(
567
+ "# Proxy mode routes Codex model calls through the CostLayers gateway.",
568
+ `# It uses ${apiKeyEnv} from your shell and requires OpenAI Platform Responses API write scope.`,
569
+ "",
534
570
  'model_provider = "costlayers"',
535
571
  "",
536
572
  "[model_providers.costlayers]",
537
573
  'name = "CostLayers"',
538
574
  `base_url = ${JSON.stringify(baseUrl)}`,
539
575
  'wire_api = "responses"',
540
- "requires_openai_auth = true",
541
- "",
576
+ `env_key = ${JSON.stringify(apiKeyEnv)}`,
577
+ ""
578
+ );
579
+ }
580
+ lines.push(
542
581
  "[otel]",
543
582
  'environment = "costlayers"',
544
583
  "log_user_prompt = false",
@@ -550,14 +589,15 @@ function profileTomlString(connection) {
550
589
  '[otel.exporter."otlp-http".headers]',
551
590
  `"x-costlayers-key" = ${JSON.stringify(connection.api_key || "")}`,
552
591
  ""
553
- ].join("\n");
592
+ );
593
+ return lines.join("\n");
554
594
  }
555
595
 
556
- function writeCodexProfile(connection) {
596
+ function writeCodexProfile(connection, args = {}) {
557
597
  const dir = codexHomeDir();
558
598
  ensureDir(dir);
559
599
  const file = path.join(dir, "costlayers.config.toml");
560
- fs.writeFileSync(file, profileTomlString(connection), "utf8");
600
+ fs.writeFileSync(file, profileTomlString(connection, args), "utf8");
561
601
  return file;
562
602
  }
563
603
 
@@ -625,9 +665,10 @@ async function signup(repo, args) {
625
665
 
626
666
  async function codexProfile(repo, args) {
627
667
  const connection = await ensureConnection(repo, args);
628
- const profilePath = writeCodexProfile(connection);
668
+ const profilePath = writeCodexProfile(connection, args);
629
669
  process.stdout.write(`CostLayers Codex profile installed\n`);
630
670
  process.stdout.write(`Profile: ${profilePath}\n`);
671
+ process.stdout.write(`Mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode, native Codex provider preserved"}\n`);
631
672
  process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
632
673
  process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
633
674
  process.stdout.write(`Run: codex --profile costlayers\n`);
@@ -747,10 +788,12 @@ async function runAgent(repo, args, argv, options = {}) {
747
788
  process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
748
789
  let commandToRun = command;
749
790
  if (connection && connection.engine_url && isCodexCommand(command)) {
750
- const profilePath = writeCodexProfile(connection);
791
+ assertCodexProxyApiKey(args);
792
+ const profilePath = writeCodexProfile(connection, args);
751
793
  commandToRun = withCostLayersCodexProfile(command);
752
794
  process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
753
795
  process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
796
+ process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
754
797
  process.stdout.write(`Savings dashboard: ${dashboardUrlFromConnection(connection)}\n`);
755
798
  process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
756
799
  }
@@ -827,19 +870,22 @@ async function dashboard(repo, args) {
827
870
  process.stdout.write(`URL: ${dashboardUrl}\n`);
828
871
  process.stdout.write(`status: ${status.status || "unknown"}\n`);
829
872
  process.stdout.write(`plan: ${status.free_beta ? "free beta" : "metered"}\n`);
830
- process.stdout.write(`metered_savings_usd: ${status.metered_savings_usd ?? ""}\n`);
831
- process.stdout.write(`codex_metered_savings_usd: ${status.codex_metered_savings_usd ?? ""}\n`);
873
+ process.stdout.write(`api_invoice_savings_usd: ${status.metered_savings_usd ?? ""}\n`);
874
+ process.stdout.write(`chatgpt_codex_usage_stretch_usd: ${status.codex_metered_savings_usd ?? ""}\n`);
832
875
  process.stdout.write(`codex_meter_total_tokens: ${status.codex_meter_total_tokens ?? 0}\n`);
833
- process.stdout.write(`codex_context_savings_usd: ${status.context_savings_usd ?? ""}\n`);
876
+ process.stdout.write(`context_waste_estimate_usd: ${status.context_savings_usd ?? ""}\n`);
834
877
  process.stdout.write(`context_tokens_avoided_per_task: ${status.context_tokens_avoided_per_task ?? 0}\n`);
835
878
  process.stdout.write(`gateway_requests: ${status.gateway_request_count ?? 0}\n`);
836
879
  process.stdout.write(`blocked_requests: ${status.blocked_request_count ?? 0}\n`);
837
880
  process.stdout.write(`rate_limit_per_minute: ${status.rate_limit_per_minute ?? ""}\n`);
881
+ process.stdout.write(`claim: API invoice savings only count when provider API traffic is routed through CostLayers. ChatGPT-login Codex shows usage stretch, not invoice reduction.\n`);
838
882
  }
839
883
 
840
884
  async function start(repo, args, argv) {
841
885
  const dash = argv.indexOf("--");
842
886
  const command = dash >= 0 ? argv.slice(dash + 1) : [];
887
+ const codexTelemetryRun = command.length > 0 && isCodexCommand(command) && !codexProxyEnabled(args);
888
+ if (command.length > 0 && isCodexCommand(command)) assertCodexProxyApiKey(args);
843
889
  init(repo);
844
890
  process.stdout.write(`Scanning repo: ${repo}\n`);
845
891
  const precomputed = scanToFiles(repo, args);
@@ -855,37 +901,49 @@ async function start(repo, args, argv) {
855
901
  process.stderr.write(`Dashboard sync delayed; local report is still available: ${err.message}\n`);
856
902
  }
857
903
  process.stdout.write(`CostLayers connection ready\n`);
858
- const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
859
- const payload = {
860
- host: args.host || "127.0.0.1",
861
- port: Number(args.port || 8788),
862
- provider_url: providerUrl,
863
- api_key_env: args["api-key-env"] || "OPENAI_API_KEY",
864
- mode: args.mode || "reduce",
865
- dry_run: Boolean(args["dry-run"]),
866
- public_gateway_url: args["public-gateway-url"] || connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)
867
- };
868
- const result = await postJson(`${connection.engine_url}/v1/gateway/start`, payload, connection.api_key);
869
- if (!result.ok) {
870
- process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
871
- process.exit(1);
904
+ let gatewayBaseUrl = connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key);
905
+ if (codexTelemetryRun) {
906
+ process.stdout.write(`ChatGPT-login Codex mode: native Codex provider preserved; model calls are not routed through CostLayers.\n`);
907
+ process.stdout.write(`What users get: less repeated repo context and a usage-stretch meter. This does not reduce a flat ChatGPT subscription invoice.\n`);
908
+ } else {
909
+ const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
910
+ const payload = {
911
+ host: args.host || "127.0.0.1",
912
+ port: Number(args.port || 8788),
913
+ provider_url: providerUrl,
914
+ api_key_env: args["api-key-env"] || "OPENAI_API_KEY",
915
+ mode: args.mode || "reduce",
916
+ dry_run: Boolean(args["dry-run"]),
917
+ public_gateway_url: args["public-gateway-url"] || gatewayBaseUrl
918
+ };
919
+ const result = await postJson(`${connection.engine_url}/v1/gateway/start`, payload, connection.api_key);
920
+ if (!result.ok) {
921
+ process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
922
+ process.exit(1);
923
+ }
924
+ gatewayBaseUrl = result.base_url || gatewayBaseUrl;
925
+ process.stdout.write(`CostLayers gateway ready: ${gatewayBaseUrl}\n`);
926
+ process.stdout.write(`OpenAI-compatible base URL: ${gatewayBaseUrl}\n`);
927
+ if (codexProxyEnabled(args)) {
928
+ process.stdout.write(`API invoice mode: Codex will use ${codexProxyApiKeyEnv(args)} through the CostLayers gateway.\n`);
929
+ }
872
930
  }
873
- process.stdout.write(`CostLayers gateway ready: ${result.base_url}\n`);
874
- process.stdout.write(`OpenAI-compatible base URL: ${result.base_url}\n`);
875
931
  process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
876
932
  process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
877
933
  process.stdout.write(`Plan: free beta\n`);
878
- const profilePath = writeCodexProfile(connection);
934
+ const profilePath = writeCodexProfile(connection, args);
879
935
  process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
936
+ process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
880
937
  if (command.length > 0) {
881
938
  return runAgent(repo, args, argv, { skipSetup: true, precomputed });
882
939
  }
883
940
  process.stdout.write(`\nNext options:\n`);
884
- process.stdout.write(` Use gateway URL in your model client: ${result.base_url}\n`);
885
- process.stdout.write(` Run Codex metered: npx -y costlayers@latest start --email you@example.com -- codex\n`);
941
+ process.stdout.write(` Use gateway URL in your model client: ${gatewayBaseUrl}\n`);
942
+ process.stdout.write(` Run Codex metered: npx -y ${INSTALL_SPEC} start --email you@example.com -- codex\n`);
943
+ process.stdout.write(` Reduce an OpenAI API invoice: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} start --email you@example.com --codex-proxy -- codex\n`);
886
944
  process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
887
- process.stdout.write(` View savings: npx -y costlayers@latest gateway report\n`);
888
- process.stdout.write(` Dashboard: npx -y costlayers@latest dashboard\n`);
945
+ process.stdout.write(` View report: npx -y ${INSTALL_SPEC} gateway report\n`);
946
+ process.stdout.write(` Dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
889
947
  }
890
948
 
891
949
  function doctor() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "costlayers",
3
- "version": "0.8.11",
3
+ "version": "0.8.14",
4
4
  "description": "CostLayers cost control for AI coding agents. Build compact repo context packs, gateway reports, and savings dashboards.",
5
5
  "bin": {
6
6
  "agentspend": "bin/agentspend.js",