coderag-ai 0.3.3__tar.gz → 0.3.4__tar.gz

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.
Files changed (140) hide show
  1. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/PKG-INFO +1 -1
  2. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/pyproject.toml +1 -1
  3. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/api/app.py +8 -0
  4. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/api/dashboard.py +5 -0
  5. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/api/schemas.py +6 -0
  6. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/cli/main.py +7 -1
  7. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/core/config.py +7 -0
  8. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase13_measure.py +25 -0
  9. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/.env.example +0 -0
  10. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/.gitignore +0 -0
  11. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/CODE_OF_CONDUCT.md +0 -0
  12. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/CONTRIBUTING.md +0 -0
  13. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/Dockerfile +0 -0
  14. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/LICENSE +0 -0
  15. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/Makefile +0 -0
  16. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/NOTICE +0 -0
  17. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/README.md +0 -0
  18. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/SECURITY.md +0 -0
  19. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/alembic.ini +0 -0
  20. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docker/entrypoint.sh +0 -0
  21. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docker-compose.yml +0 -0
  22. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adding-language.md +0 -0
  23. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/001-postgres-pgvector.md +0 -0
  24. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/002-structural-chunks.md +0 -0
  25. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/003-hybrid-search.md +0 -0
  26. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/004-tree-sitter.md +0 -0
  27. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/005-llm-provider-independence.md +0 -0
  28. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/006-repository-authorization.md +0 -0
  29. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/adr/007-token-budgeting.md +0 -0
  30. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/architecture.md +0 -0
  31. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/claude-code.md +0 -0
  32. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/evaluation.md +0 -0
  33. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/install-macos.md +0 -0
  34. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/licenses.md +0 -0
  35. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/llm-providers.md +0 -0
  36. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/retrieval.md +0 -0
  37. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/security.md +0 -0
  38. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/docs/vscode-extension.md +0 -0
  39. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/__init__.py +0 -0
  40. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/api.py +0 -0
  41. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/auth.py +0 -0
  42. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/checkout_service.py +0 -0
  43. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/models.py +0 -0
  44. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/payment_repository.py +0 -0
  45. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/payment_service.py +0 -0
  46. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/retry_policy.py +0 -0
  47. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/payments/users.py +0 -0
  48. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/tests/test_payment_service.py +0 -0
  49. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/demo-repository/tests/test_retry_policy.py +0 -0
  50. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/examples/eval/eval_dataset.json +0 -0
  51. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/scripts/gen_initial_migration.py +0 -0
  52. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/__init__.py +0 -0
  53. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/analyzers/__init__.py +0 -0
  54. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/analyzers/base.py +0 -0
  55. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/analyzers/flake8_adapter.py +0 -0
  56. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/analyzers/pylint_adapter.py +0 -0
  57. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/analyzers/workflow.py +0 -0
  58. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/api/__init__.py +0 -0
  59. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/cli/__init__.py +0 -0
  60. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/context/__init__.py +0 -0
  61. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/context/builder.py +0 -0
  62. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/context/package.py +0 -0
  63. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/core/__init__.py +0 -0
  64. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/core/logging.py +0 -0
  65. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/core/text.py +0 -0
  66. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/core/tokens.py +0 -0
  67. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/__init__.py +0 -0
  68. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/base.py +0 -0
  69. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/migrate.py +0 -0
  70. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/migrations/env.py +0 -0
  71. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/migrations/script.py.mako +0 -0
  72. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/migrations/versions/.gitkeep +0 -0
  73. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/migrations/versions/9a973932b94d_measure_baseline_tokens.py +0 -0
  74. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/migrations/versions/c8a40feeda41_initial_schema.py +0 -0
  75. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/db/models.py +0 -0
  76. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/embeddings/__init__.py +0 -0
  77. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/embeddings/base.py +0 -0
  78. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/embeddings/hashing.py +0 -0
  79. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/embeddings/pipeline.py +0 -0
  80. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/embeddings/registry.py +0 -0
  81. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/embeddings/sentence_transformer.py +0 -0
  82. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/evaluation/__init__.py +0 -0
  83. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/evaluation/datasets.py +0 -0
  84. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/evaluation/harness.py +0 -0
  85. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/git/__init__.py +0 -0
  86. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/git/repo.py +0 -0
  87. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/indexing/__init__.py +0 -0
  88. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/indexing/ignore.py +0 -0
  89. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/indexing/indexer.py +0 -0
  90. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/llm/__init__.py +0 -0
  91. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/llm/anthropic.py +0 -0
  92. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/llm/base.py +0 -0
  93. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/llm/null.py +0 -0
  94. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/llm/registry.py +0 -0
  95. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/localdb.py +0 -0
  96. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/mcp_server.py +0 -0
  97. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/parsing/__init__.py +0 -0
  98. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/parsing/base.py +0 -0
  99. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/parsing/python.py +0 -0
  100. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/parsing/registry.py +0 -0
  101. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/__init__.py +0 -0
  102. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/base.py +0 -0
  103. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/engine.py +0 -0
  104. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/fusion.py +0 -0
  105. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/graph.py +0 -0
  106. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/lexical.py +0 -0
  107. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/reranking.py +0 -0
  108. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/semantic.py +0 -0
  109. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/retrieval/symbol.py +0 -0
  110. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/security/__init__.py +0 -0
  111. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/security/authz.py +0 -0
  112. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/security/secrets.py +0 -0
  113. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/service.py +0 -0
  114. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/src/coderag/telemetry.py +0 -0
  115. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/__init__.py +0 -0
  116. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/conftest.py +0 -0
  117. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase0_db.py +0 -0
  118. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase0_migrations.py +0 -0
  119. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase10_analyzers.py +0 -0
  120. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase11_dashboard.py +0 -0
  121. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase12_mcp.py +0 -0
  122. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase14_localdb.py +0 -0
  123. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase15_migrate.py +0 -0
  124. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase1_indexer.py +0 -0
  125. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase1_parser.py +0 -0
  126. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase2_cli.py +0 -0
  127. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase2_retrieval.py +0 -0
  128. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase3_embeddings.py +0 -0
  129. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase3_semantic.py +0 -0
  130. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase3_sentence_transformer.py +0 -0
  131. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase4_graph.py +0 -0
  132. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase5_context_db.py +0 -0
  133. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase5_context_unit.py +0 -0
  134. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase6_api.py +0 -0
  135. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase6_ask.py +0 -0
  136. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase6_llm.py +0 -0
  137. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase7_eval.py +0 -0
  138. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase8_incremental.py +0 -0
  139. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_phase9_rerank.py +0 -0
  140. {coderag_ai-0.3.3 → coderag_ai-0.3.4}/tests/test_security_secrets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderag-ai
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Token-efficient, structure-aware Code Intelligence + RAG for private source repositories.
5
5
  Project-URL: Homepage, https://github.com/galacticsurfer/coderag
6
6
  Project-URL: Repository, https://github.com/galacticsurfer/coderag
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
7
7
  # command are both `coderag`. Plain "coderag" is taken on PyPI by an unrelated project,
8
8
  # and "code-rag" is rejected as too similar (PyPI collapses -/_/. when comparing names).
9
9
  name = "coderag-ai"
10
- version = "0.3.3"
10
+ version = "0.3.4"
11
11
  description = "Token-efficient, structure-aware Code Intelligence + RAG for private source repositories."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
@@ -286,6 +286,10 @@ def metrics(session: Session = Depends(get_session)) -> s.MetricsOut:
286
286
  ) or 0)
287
287
  saved_vs_files = base_sum - base_ctx
288
288
  reduction_vs_files = round(100.0 * saved_vs_files / base_sum, 1) if base_sum else 0.0
289
+ from coderag.core.config import get_settings
290
+
291
+ _s = get_settings()
292
+ pin, pout = _s.price_input_per_mtok, _s.price_output_per_mtok
289
293
  return s.MetricsOut(
290
294
  repositories=count(Repository), symbols=count(Symbol),
291
295
  embeddings=count(SymbolEmbedding), relationships=count(SymbolRelationship),
@@ -297,6 +301,10 @@ def metrics(session: Session = Depends(get_session)) -> s.MetricsOut:
297
301
  total_tokens_saved=saved, avg_token_reduction_percent=reduction,
298
302
  total_baseline_tokens=base_sum, total_saved_vs_files=saved_vs_files,
299
303
  reduction_vs_files_percent=reduction_vs_files,
304
+ price_input_per_mtok=pin, price_output_per_mtok=pout,
305
+ cost_saved_vs_files_usd=round(saved_vs_files / 1e6 * pin, 4),
306
+ cost_context_sent_usd=round(int(ctx_sum) / 1e6 * pin, 4),
307
+ cost_llm_usd=round(int(in_tok) / 1e6 * pin + int(out_tok) / 1e6 * pout, 4),
300
308
  )
301
309
 
302
310
 
@@ -109,6 +109,7 @@ DASHBOARD_HTML = r"""<!doctype html>
109
109
  </header>
110
110
 
111
111
  <section class="kpis" id="kpis"></section>
112
+ <p style="font-size:12px;color:var(--muted);margin:-14px 0 22px">Dollar figures are estimates at the configured per-million prices (CODERAG_PRICE_INPUT_PER_MTOK / _OUTPUT_PER_MTOK) — not billing data. On a flat-rate Claude plan, savings show up as headroom rather than a refund.</p>
112
113
 
113
114
  <section class="card">
114
115
  <h2>Context vs. saved — recent queries</h2>
@@ -143,6 +144,7 @@ DASHBOARD_HTML = r"""<!doctype html>
143
144
  <script>
144
145
  const n = x => (x==null? "—" : Number(x).toLocaleString());
145
146
  const pct = x => (x==null? "—" : Number(x).toFixed(1) + "%");
147
+ const usd = x => (x==null? "—" : "$" + Number(x).toLocaleString(undefined,{minimumFractionDigits:2, maximumFractionDigits:2}));
146
148
  const esc = s => (s||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
147
149
 
148
150
  function tile(label, val, unit, hero){
@@ -153,7 +155,10 @@ function tile(label, val, unit, hero){
153
155
  function renderKpis(m){
154
156
  document.getElementById('kpis').innerHTML =
155
157
  tile('Saved vs reading files', n(m.total_saved_vs_files), 'tok', true) +
158
+ tile('Est. $ saved', usd(m.cost_saved_vs_files_usd), '', true) +
156
159
  tile('Reduction vs files', pct(m.reduction_vs_files_percent), '', true) +
160
+ tile('Est. $ context sent', usd(m.cost_context_sent_usd)) +
161
+ tile('Est. $ LLM spend', usd(m.cost_llm_usd)) +
157
162
  tile('Whole-file baseline', n(m.total_baseline_tokens), 'tok') +
158
163
  tile('Tokens saved (budgeting)', n(m.total_tokens_saved)) +
159
164
  tile('Overall reduction', pct(m.avg_token_reduction_percent)) +
@@ -158,6 +158,12 @@ class MetricsOut(BaseModel):
158
158
  total_baseline_tokens: int
159
159
  total_saved_vs_files: int
160
160
  reduction_vs_files_percent: float
161
+ # Indicative USD at the configured per-million prices (estimates, not billing)
162
+ price_input_per_mtok: float
163
+ price_output_per_mtok: float
164
+ cost_saved_vs_files_usd: float
165
+ cost_context_sent_usd: float
166
+ cost_llm_usd: float
161
167
 
162
168
 
163
169
  class QueryRow(BaseModel):
@@ -195,9 +195,15 @@ def context(
195
195
  f"{acct.final_prompt_tokens:,}")
196
196
  console.print(t)
197
197
  if acct.baseline_tokens:
198
+ price = get_settings().price_input_per_mtok
199
+ saved_usd = acct.tokens_saved_vs_files / 1e6 * price
198
200
  console.print(
199
201
  f"[green]saved {acct.tokens_saved_vs_files:,} tokens "
200
- f"({acct.reduction_vs_files}%) vs opening those files[/]"
202
+ f"({acct.reduction_vs_files}%) vs opening those files[/] "
203
+ f"[dim]≈ ${saved_usd:.4f} at ${price:.2f}/M input tokens; "
204
+ f"× 1,000 similar queries ≈ ${saved_usd * 1000:.2f}[/]\n"
205
+ f"[dim]Estimate at configured prices, not billing data. On a "
206
+ f"flat-rate plan this is headroom, not a refund.[/]"
201
207
  )
202
208
  else:
203
209
  console.print("[yellow]No baseline recorded (nothing selected).[/]")
@@ -85,6 +85,13 @@ class Settings(BaseSettings):
85
85
  llm_max_output_tokens: int = 1024
86
86
  llm_timeout_seconds: float = 60.0
87
87
 
88
+ # ---- Cost estimation -------------------------------------------------
89
+ # USD per million tokens, used only to turn token counts into indicative
90
+ # dollar figures. Defaults are Claude Opus 4.8 list prices; override for your
91
+ # model/plan. These are ESTIMATES at configured prices — not billing data.
92
+ price_input_per_mtok: float = 5.00
93
+ price_output_per_mtok: float = 25.00
94
+
88
95
  # ---- Analyzers (Phase 10) ------------------------------------------
89
96
  max_fix_attempts: int = 2
90
97
 
@@ -77,3 +77,28 @@ def test_mcp_context_includes_measurement(db_session, demo_repo):
77
77
  out = coderag_context("retry failed payment", repository="payments")
78
78
  assert "fewer input tokens" in out["measurement"]
79
79
  assert out["token_accounting"]["baseline_tokens"] > 0
80
+
81
+
82
+ def test_metrics_exposes_dollar_estimates(db_session, demo_repo):
83
+ """Token savings are convertible to indicative USD at configured prices."""
84
+ from coderag.core.config import get_settings
85
+
86
+ run_context(db_session, "retry failed payment", "payments")
87
+ db_session.commit()
88
+
89
+ client = TestClient(app)
90
+ m = client.get("/metrics").json()
91
+ s = get_settings()
92
+ assert m["price_input_per_mtok"] == s.price_input_per_mtok
93
+ # dollars must be a faithful function of tokens and the configured price
94
+ expected = round(m["total_saved_vs_files"] / 1e6 * s.price_input_per_mtok, 4)
95
+ assert m["cost_saved_vs_files_usd"] == expected
96
+ assert m["cost_context_sent_usd"] >= 0.0
97
+ assert m["cost_llm_usd"] >= 0.0
98
+
99
+
100
+ def test_price_settings_are_configurable():
101
+ from coderag.core.config import Settings
102
+
103
+ s = Settings(price_input_per_mtok=3.0, price_output_per_mtok=15.0)
104
+ assert s.price_input_per_mtok == 3.0 and s.price_output_per_mtok == 15.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes