otto-cli-agent 0.1.2__tar.gz → 0.1.3__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 (322) hide show
  1. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/PKG-INFO +1 -1
  2. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/README.md +1 -0
  3. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/usage.py +9 -1
  4. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/anthropic_provider.py +10 -0
  5. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/base.py +30 -0
  6. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/gemini_provider.py +4 -0
  7. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/inception_provider.py +35 -16
  8. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/openai_provider.py +10 -0
  9. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/pyproject.toml +1 -1
  10. otto_cli_agent-0.1.3/tests/test_prompt_caching.py +108 -0
  11. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/uv.lock +1 -1
  12. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.gitattributes +0 -0
  13. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.github/workflows/publish.yml +0 -0
  17. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.github/workflows/tests.yml +0 -0
  18. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.gitignore +0 -0
  19. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/.python-version +0 -0
  20. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/CODE_OF_CONDUCT.md +0 -0
  21. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/CONTRIBUTING.md +0 -0
  22. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/LICENSE +0 -0
  23. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/SECURITY.md +0 -0
  24. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/README.md +0 -0
  25. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/__init__.py +0 -0
  26. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/README.md +0 -0
  27. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/__init__.py +0 -0
  28. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/art.py +0 -0
  29. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/chat.py +0 -0
  30. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/clipboard.py +0 -0
  31. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/context.py +0 -0
  32. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/doctor.py +0 -0
  33. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/errors.py +0 -0
  34. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/eval.py +0 -0
  35. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/eval_claw.py +0 -0
  36. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/eval_compaction.py +0 -0
  37. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/eval_hle.py +0 -0
  38. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/eval_memory.py +0 -0
  39. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/eval_swe.py +0 -0
  40. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/lessons.py +0 -0
  41. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/main.py +0 -0
  42. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/modals.py +0 -0
  43. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/models.py +0 -0
  44. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/output.py +0 -0
  45. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/route.py +0 -0
  46. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/serve.py +0 -0
  47. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/sessions.py +0 -0
  48. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/setup_screen.py +0 -0
  49. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/shell.py +0 -0
  50. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/tui.py +0 -0
  51. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/ui.py +0 -0
  52. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/cli/usage_panel.py +0 -0
  53. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/config/README.md +0 -0
  54. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/config/__init__.py +0 -0
  55. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/config/envfile.py +0 -0
  56. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/config/home.py +0 -0
  57. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/embed.py +0 -0
  58. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/README.md +0 -0
  59. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/__init__.py +0 -0
  60. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/claw_bench.py +0 -0
  61. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/compaction_bench.py +0 -0
  62. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/data/README.md +0 -0
  63. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/data/claw/README.md +0 -0
  64. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/data/claw/llm_judge-gemini.patch +0 -0
  65. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/data/claw/otto.yaml +0 -0
  66. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/failures.py +0 -0
  67. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/README.md +0 -0
  68. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/code_01.json +0 -0
  69. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/code_02.json +0 -0
  70. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/code_03.json +0 -0
  71. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/code_04.json +0 -0
  72. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/code_05.json +0 -0
  73. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/code_06.json +0 -0
  74. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/math_01.json +0 -0
  75. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/math_02.json +0 -0
  76. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/math_03.json +0 -0
  77. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/math_04.json +0 -0
  78. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/math_05.json +0 -0
  79. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/math_06.json +0 -0
  80. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_gcp_01.json +0 -0
  81. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_ksp_01.json +0 -0
  82. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_math_binpacking_01.json +0 -0
  83. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_math_clique_01.json +0 -0
  84. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_math_setcover_01.json +0 -0
  85. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_math_subsetsum_01.json +0 -0
  86. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_tsp_01.json +0 -0
  87. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/golden/nphard_tsp_02.json +0 -0
  88. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/hle_bench.py +0 -0
  89. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/langfuse_sync.py +0 -0
  90. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/memory_bench.py +0 -0
  91. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/runner.py +0 -0
  92. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/single_agent.py +0 -0
  93. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/swe_bench.py +0 -0
  94. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/eval/terminal_bench.py +0 -0
  95. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/README.md +0 -0
  96. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/__init__.py +0 -0
  97. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/embeddings.py +0 -0
  98. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/hashing.py +0 -0
  99. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/lessons.py +0 -0
  100. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/queue.py +0 -0
  101. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/retrieval.py +0 -0
  102. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/session.py +0 -0
  103. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/sessions.py +0 -0
  104. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/store.py +0 -0
  105. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/tokens.py +0 -0
  106. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/memory/wiring.py +0 -0
  107. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/README.md +0 -0
  108. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/__init__.py +0 -0
  109. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/assets/app_notes/com.android.settings.md +0 -0
  110. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/assets/app_notes/in.amazon.mShop.android.shopping.md +0 -0
  111. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/assets/guard_pages.json +0 -0
  112. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/assets/guard_rules.json +0 -0
  113. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/backend.py +0 -0
  114. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/digest.py +0 -0
  115. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/guard.py +0 -0
  116. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/notes.py +0 -0
  117. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/phone/tools.py +0 -0
  118. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/README.md +0 -0
  119. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/__init__.py +0 -0
  120. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/_python_session_shim.py +0 -0
  121. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/browsing.py +0 -0
  122. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/budget.py +0 -0
  123. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/codemap.py +0 -0
  124. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/evidence.py +0 -0
  125. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/execution.py +0 -0
  126. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/modes.py +0 -0
  127. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/native.py +0 -0
  128. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/nodes.py +0 -0
  129. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/pricing.py +0 -0
  130. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/profile.py +0 -0
  131. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/progress.py +0 -0
  132. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/python_session.py +0 -0
  133. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/rag.py +0 -0
  134. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/research.py +0 -0
  135. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/run.py +0 -0
  136. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/screen.py +0 -0
  137. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/state.py +0 -0
  138. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/toolkit.py +0 -0
  139. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/tools.py +0 -0
  140. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/tracing.py +0 -0
  141. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/vision.py +0 -0
  142. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/walkthrough.py +0 -0
  143. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/pipeline/workspace.py +0 -0
  144. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/README.md +0 -0
  145. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/__init__.py +0 -0
  146. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/automap.py +0 -0
  147. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/health.py +0 -0
  148. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/README.md +0 -0
  149. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/__init__.py +0 -0
  150. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/custom.py +0 -0
  151. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/retired.py +0 -0
  152. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/llm_provider/temperature.py +0 -0
  153. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/mapping.py +0 -0
  154. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/outcomes.py +0 -0
  155. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/overrides.py +0 -0
  156. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/reload.py +0 -0
  157. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/router.py +0 -0
  158. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/router/setup.py +0 -0
  159. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/server/README.md +0 -0
  160. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/server/__init__.py +0 -0
  161. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/server/app.py +0 -0
  162. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/server/protocol.py +0 -0
  163. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/agent/server/proxy.py +0 -0
  164. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/containers/README.md +0 -0
  165. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/containers/otto-desktop/Dockerfile +0 -0
  166. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/containers/otto-desktop/README.md +0 -0
  167. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/containers/otto-desktop/start-desktop.sh +0 -0
  168. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/debug_pipeline.py +0 -0
  169. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/HISTORY.md +0 -0
  170. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/README.md +0 -0
  171. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/RESEARCH.md +0 -0
  172. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/_config.yml +0 -0
  173. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/design/README.md +0 -0
  174. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/design/tiered-memory.md +0 -0
  175. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/index.md +0 -0
  176. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/codebase-code-map.png +0 -0
  177. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/exercise-todo-cli.png +0 -0
  178. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/otto-demo.gif +0 -0
  179. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/palette-doctor-route.png +0 -0
  180. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/run-trace-and-judge.png +0 -0
  181. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/session-resumed-thinking.png +0 -0
  182. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/sessions-picker.png +0 -0
  183. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/setup-screen.png +0 -0
  184. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/media/social-preview.png +0 -0
  185. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/docs/pypi.md +0 -0
  186. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/scripts/phone_page_fixture.py +0 -0
  187. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/scripts/phone_page_manifest.py +0 -0
  188. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/README.md +0 -0
  189. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/conftest.py +0 -0
  190. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_address_form_pincode.json +0 -0
  191. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_address_step.json +0 -0
  192. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_buy_now_sheet.json +0 -0
  193. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_cart.json +0 -0
  194. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_cart_pens.json +0 -0
  195. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_checkout_payment.json +0 -0
  196. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_checkout_payment_scrolled.json +0 -0
  197. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_checkout_upsell.json +0 -0
  198. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_filters.json +0 -0
  199. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_home.json +0 -0
  200. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_order_review.json +0 -0
  201. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_order_review_scrolled.json +0 -0
  202. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_product.json +0 -0
  203. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_product_buybox.json +0 -0
  204. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_product_ids.json +0 -0
  205. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_results_after_review.json +0 -0
  206. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_results_offers.json +0 -0
  207. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_results_search.json +0 -0
  208. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_results_sorted.json +0 -0
  209. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_search_suggestions.json +0 -0
  210. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_sign_in.json +0 -0
  211. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_sort_price.json +0 -0
  212. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_sort_sheet.json +0 -0
  213. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/amazon_upi_pin_screen.json +0 -0
  214. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/bank_pin_numpw.json +0 -0
  215. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/blinkit_cart_pay.json +0 -0
  216. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/blinkit_search.json +0 -0
  217. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/chat_with_otp.json +0 -0
  218. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/phonepe_upi_pin.json +0 -0
  219. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/settings_display.json +0 -0
  220. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/shop_onboarding_continue.json +0 -0
  221. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/shop_order_summary_continue.json +0 -0
  222. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/shop_otp_boxes.json +0 -0
  223. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/shop_otp_caption.json +0 -0
  224. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/fixtures/phone_pages/shop_qty_total_pay.json +0 -0
  225. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/phone_fakes.py +0 -0
  226. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_agent_loop.py +0 -0
  227. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_art.py +0 -0
  228. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_ask_user_node.py +0 -0
  229. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_automap.py +0 -0
  230. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_background_distil.py +0 -0
  231. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_browsing.py +0 -0
  232. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_budget.py +0 -0
  233. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_call_budget.py +0 -0
  234. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_chat_fast_path.py +0 -0
  235. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_claw_bench.py +0 -0
  236. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_clipboard.py +0 -0
  237. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_codemap.py +0 -0
  238. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_compaction_bench.py +0 -0
  239. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_custom_endpoints.py +0 -0
  240. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_diffusion_retry.py +0 -0
  241. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_embed.py +0 -0
  242. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_embedding_backends.py +0 -0
  243. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_envfile.py +0 -0
  244. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_eval_honesty.py +0 -0
  245. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_eval_runner.py +0 -0
  246. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_evaluator_node.py +0 -0
  247. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_evicted_context.py +0 -0
  248. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_evidence.py +0 -0
  249. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_failures.py +0 -0
  250. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_health.py +0 -0
  251. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_hle_bench.py +0 -0
  252. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_home.py +0 -0
  253. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_inception_provider.py +0 -0
  254. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_inception_stream.py +0 -0
  255. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_langfuse_sync.py +0 -0
  256. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_lessons.py +0 -0
  257. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_lessons_cli.py +0 -0
  258. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_mapping.py +0 -0
  259. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_bench.py +0 -0
  260. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_embeddings.py +0 -0
  261. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_hashing.py +0 -0
  262. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_queue.py +0 -0
  263. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_retrieval.py +0 -0
  264. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_session.py +0 -0
  265. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_store.py +0 -0
  266. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_tokens.py +0 -0
  267. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_memory_wiring.py +0 -0
  268. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_model_policies.py +0 -0
  269. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_modes.py +0 -0
  270. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_native.py +0 -0
  271. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_no_import_shadowing.py +0 -0
  272. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_optional_vendor.py +0 -0
  273. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_output.py +0 -0
  274. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_overrides.py +0 -0
  275. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_app_notes.py +0 -0
  276. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_backend.py +0 -0
  277. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_call_budget.py +0 -0
  278. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_decision.py +0 -0
  279. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_digest.py +0 -0
  280. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_do.py +0 -0
  281. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_element_ids.py +0 -0
  282. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_fold.py +0 -0
  283. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_guard.py +0 -0
  284. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_learned_notes.py +0 -0
  285. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_page_corpus.py +0 -0
  286. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_prompt.py +0 -0
  287. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_seats.py +0 -0
  288. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_shopping_digest.py +0 -0
  289. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_phone_tools.py +0 -0
  290. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_pipeline_nodes.py +0 -0
  291. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_pipeline_run.py +0 -0
  292. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_profile.py +0 -0
  293. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_progress.py +0 -0
  294. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_prompt_tool_sync.py +0 -0
  295. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_provider_connection_detail.py +0 -0
  296. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_python_session.py +0 -0
  297. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_reload.py +0 -0
  298. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_research_router.py +0 -0
  299. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_research_workflow.py +0 -0
  300. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_retrieval_method.py +0 -0
  301. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_router.py +0 -0
  302. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_run_scoped_toolkit.py +0 -0
  303. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_screen.py +0 -0
  304. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_seat_outcomes.py +0 -0
  305. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_seed_lessons.py +0 -0
  306. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_serve.py +0 -0
  307. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_sessions.py +0 -0
  308. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_setup.py +0 -0
  309. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_swe_bench.py +0 -0
  310. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_temperature_learning.py +0 -0
  311. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_terminal_bench_adapter.py +0 -0
  312. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_tool_loop.py +0 -0
  313. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_tools_stubs.py +0 -0
  314. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_tracing.py +0 -0
  315. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_tui.py +0 -0
  316. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_tui_progress.py +0 -0
  317. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_tui_setup.py +0 -0
  318. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_usage.py +0 -0
  319. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_vision_tool.py +0 -0
  320. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_walkthrough.py +0 -0
  321. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_workspace_session.py +0 -0
  322. {otto_cli_agent-0.1.2 → otto_cli_agent-0.1.3}/tests/test_workspace_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: otto-cli-agent
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Otto: a terminal AI agent built around measured results -- one agent loop, a rubric-first evaluator, tiered memory, multi-vendor routing, and six benchmark harnesses.
5
5
  Project-URL: Repository, https://github.com/siddharth23P/otto_agent
6
6
  Project-URL: Issues, https://github.com/siddharth23P/otto_agent/issues
@@ -138,6 +138,7 @@ one interpreter per run), `OTTO_PYTHON_SESSION_MEMORY_MB` (that interpreter's
138
138
  address-space ceiling on Linux, default 8192, 0 for none),
139
139
  `OTTO_EMBEDDING_MODEL` (`provider:model`; local BGE is the floor),
140
140
  `OTTO_MODEL_PRICES` (a JSON file that overrides the price table),
141
+ `OTTO_PROMPT_CACHE=0` (turn off input-token caching, on by default for all four vendors),
141
142
  `OTTO_IGNORE_ROUTES=1` (use the shipped routing table untouched; evals do),
142
143
  `OTTO_BROWSER_PYTHON` (an interpreter with Playwright and `pyte`, which
143
144
  enables the browser and terminal tools), `OTTO_HOME` (where state lives,
@@ -139,7 +139,13 @@ class UsageLedger:
139
139
  details = usage.get("input_token_details")
140
140
  if isinstance(details, Mapping):
141
141
  entry.cached_input_tokens += _int(details.get("cache_read"))
142
- entry.cache_write_tokens += _int(details.get("cache_creation"))
142
+ # langchain-anthropic reports a write by its TTL (ephemeral_5m/_1h_input_tokens) and then
143
+ # leaves cache_creation at 0; either shape is a write. Priced at the 5-minute write rate,
144
+ # the TTL otto asks for.
145
+ entry.cache_write_tokens += max(
146
+ _int(details.get("cache_creation")),
147
+ _int(details.get("ephemeral_5m_input_tokens")) + _int(details.get("ephemeral_1h_input_tokens")),
148
+ )
143
149
  entry.reported = entry.reported or got
144
150
 
145
151
  @property
@@ -192,6 +198,7 @@ class UsageLedger:
192
198
  "input_tokens": self.input_tokens,
193
199
  "output_tokens": self.output_tokens,
194
200
  "cached_input_tokens": self.cached_input_tokens,
201
+ "cache_write_tokens": sum(m.cache_write_tokens for m in self.by_model.values()),
195
202
  "total_tokens": self.total_tokens,
196
203
  "cost": self.cost,
197
204
  "fully_priced": self.fully_priced,
@@ -202,6 +209,7 @@ class UsageLedger:
202
209
  "input_tokens": m.input_tokens,
203
210
  "output_tokens": m.output_tokens,
204
211
  "cached_input_tokens": m.cached_input_tokens,
212
+ "cache_write_tokens": m.cache_write_tokens,
205
213
  "total_tokens": m.total_tokens,
206
214
  "reported": m.reported,
207
215
  "cost": m.cost,
@@ -33,8 +33,14 @@ from agent.router.llm_provider.base import (
33
33
  ProviderError,
34
34
  ProviderUnavailable,
35
35
  connection_detail,
36
+ prompt_caching,
37
+ with_model_kwargs,
36
38
  )
37
39
 
40
+ #: The request-level breakpoint for automatic prompt caching (5-minute TTL: an agent loop
41
+ #: calls again well inside it, and a 1-hour write costs 2x input instead of 1.25x).
42
+ PROMPT_CACHE = {"type": "ephemeral"}
43
+
38
44
  __all__ = ["AnthropicProvider"]
39
45
 
40
46
 
@@ -126,4 +132,8 @@ class AnthropicProvider(BaseProvider):
126
132
  def chat_model(self, model_id: str, **kwargs: Any) -> BaseChatModel:
127
133
  if self._base_url:
128
134
  kwargs.setdefault("base_url", self._base_url)
135
+ if prompt_caching():
136
+ # Automatic caching (base.py PROMPT_CACHE_ENV): ChatAnthropic puts model_kwargs at the
137
+ # top level of the request, where the direct API reads `cache_control`.
138
+ kwargs = with_model_kwargs(kwargs, cache_control=dict(PROMPT_CACHE))
129
139
  return ChatAnthropic(model=model_id, api_key=self._api_key, **kwargs)
@@ -184,6 +184,36 @@ class AuthError(ProviderError):
184
184
  """Key missing, malformed, or rejected by the vendor."""
185
185
 
186
186
 
187
+ #: Prompt (input-token) caching, on unless OTTO_PROMPT_CACHE is 0/false/off.
188
+ #: What "on" means differs by vendor, because their caches do:
189
+ #: anthropic opt-in: top-level `cache_control` (automatic caching) -- the
190
+ #: breakpoint follows the last block, so an agent loop re-reads its
191
+ #: whole transcript at 0.1x input and writes only the new part.
192
+ #: openai automatic for prefixes over 1024 tokens; `prompt_cache_key`
193
+ #: routes a model's calls to the same cache.
194
+ #: gemini implicit for 2.5 and later -- nothing to send; the discount
195
+ #: arrives as `cached_content_token_count`, already counted.
196
+ #: inception automatic on Mercury; the provider now reports its
197
+ #: `cached_input_tokens` so the ledger prices them.
198
+ PROMPT_CACHE_ENV = "OTTO_PROMPT_CACHE"
199
+
200
+
201
+ def prompt_caching() -> bool:
202
+ return os.environ.get(PROMPT_CACHE_ENV, "1").strip().lower() not in ("0", "false", "off", "no")
203
+
204
+
205
+ def with_model_kwargs(kwargs: dict[str, Any], **defaults: Any) -> dict[str, Any]:
206
+ """`kwargs` with `defaults` merged under its `model_kwargs`; a route that set one of them wins,
207
+ and a route that set it to None removes it."""
208
+ extra = dict(kwargs.pop("model_kwargs", None) or {})
209
+ for key, value in defaults.items():
210
+ extra.setdefault(key, value)
211
+ extra = {k: v for k, v in extra.items() if v is not None}
212
+ if extra:
213
+ kwargs["model_kwargs"] = extra
214
+ return kwargs
215
+
216
+
187
217
  def connection_detail(exc: BaseException) -> str:
188
218
  """A vendor SDK's message for a failed connection, with what failed underneath it.
189
219
 
@@ -117,6 +117,10 @@ class GeminiProvider(BaseProvider):
117
117
  return found
118
118
 
119
119
  def chat_model(self, model_id: str, **kwargs: Any) -> BaseChatModel:
120
+ # Prompt caching is implicit on Gemini 2.5 and later: a repeated prefix is billed at the
121
+ # cached rate with nothing sent, and ChatGoogleGenerativeAI reports it as
122
+ # input_token_details.cache_read (base.py PROMPT_CACHE_ENV). Explicit `cached_content`
123
+ # caches are not used -- they are stored, and billed, per hour.
120
124
  return ChatGoogleGenerativeAI(
121
125
  model=model_id, google_api_key=self._api_key, **kwargs
122
126
  )
@@ -98,6 +98,28 @@ def _field(obj: Any, key: str) -> Any:
98
98
  return getattr(obj, key, None)
99
99
 
100
100
 
101
+ def _cached_tokens(u: Any) -> int | None:
102
+ """Mercury's cache hits: `prompt_tokens_details.cached_tokens` (what the API sends, as OpenAI
103
+ does) or the SDK's own `cached_input_tokens`."""
104
+ direct = _field(u, "cached_input_tokens")
105
+ if direct:
106
+ return direct
107
+ details = _field(u, "prompt_tokens_details")
108
+ if details is None and isinstance(getattr(u, "model_extra", None), Mapping):
109
+ details = u.model_extra.get("prompt_tokens_details")
110
+ return _field(details, "cached_tokens")
111
+
112
+
113
+ def _usage_metadata(input_tokens: int, output_tokens: int, total_tokens: int, cached: Any) -> dict:
114
+ """LangChain's usage shape. Mercury caches repeated prefixes by itself and reports the hits as
115
+ `cached_input_tokens`; they go to input_token_details.cache_read, where the ledger prices them
116
+ (agent/pipeline/usage.py)."""
117
+ metadata: dict = {"input_tokens": input_tokens, "output_tokens": output_tokens, "total_tokens": total_tokens}
118
+ if isinstance(cached, int) and cached > 0:
119
+ metadata["input_token_details"] = {"cache_read": cached}
120
+ return metadata
121
+
122
+
101
123
  def _usage(u: Any) -> dict[str, int] | None:
102
124
  """Vendor usage payload -> Langfuse's usage_details key names."""
103
125
  if u is None:
@@ -110,11 +132,10 @@ def _usage(u: Any) -> dict[str, int] | None:
110
132
  details = {k: v for k, v in details.items() if v is not None}
111
133
  if not details:
112
134
  return None
113
- for attr, key in (("cached_input_tokens", "cached_input"),
114
- ("reasoning_tokens", "reasoning")):
115
- value = _field(u, attr)
116
- if value:
117
- details[key] = value
135
+ if cached := _cached_tokens(u):
136
+ details["cached_input"] = cached
137
+ if reasoning := _field(u, "reasoning_tokens"):
138
+ details["reasoning"] = reasoning
118
139
  return details
119
140
 
120
141
 
@@ -380,11 +401,10 @@ class ChatInception(BaseChatModel):
380
401
  "finish_reason": choice.finish_reason,
381
402
  "warning": completion.warning,
382
403
  },
383
- usage_metadata={
384
- "input_tokens": usage.prompt_tokens,
385
- "output_tokens": usage.completion_tokens,
386
- "total_tokens": usage.total_tokens,
387
- },
404
+ usage_metadata=_usage_metadata(
405
+ usage.prompt_tokens, usage.completion_tokens, usage.total_tokens,
406
+ _cached_tokens(usage),
407
+ ),
388
408
  )
389
409
  return ChatResult(
390
410
  generations=[
@@ -449,12 +469,11 @@ class ChatInception(BaseChatModel):
449
469
  yield ChatGenerationChunk(
450
470
  message=AIMessageChunk(
451
471
  content="",
452
- usage_metadata={
453
- "input_tokens": input_tokens,
454
- "output_tokens": output_tokens,
455
- "total_tokens": _field(usage, "total_tokens")
456
- or input_tokens + output_tokens,
457
- },
472
+ usage_metadata=_usage_metadata(
473
+ input_tokens, output_tokens,
474
+ _field(usage, "total_tokens") or input_tokens + output_tokens,
475
+ _cached_tokens(usage),
476
+ ),
458
477
  )
459
478
  )
460
479
  continue
@@ -31,8 +31,13 @@ from agent.router.llm_provider.base import (
31
31
  ProviderError,
32
32
  ProviderUnavailable,
33
33
  connection_detail,
34
+ prompt_caching,
35
+ with_model_kwargs,
34
36
  )
35
37
 
38
+ #: prompt_cache_key is this plus the model id: one cache per model, shared by its calls.
39
+ PROMPT_CACHE_KEY_PREFIX = "otto:"
40
+
36
41
  __all__ = ["OpenAIProvider"]
37
42
 
38
43
 
@@ -132,6 +137,11 @@ class OpenAIProvider(BaseProvider):
132
137
  # No hand-rolled chat method. Returning a BaseChatModel is what makes
133
138
  # this provider interchangeable inside a LangGraph node and visible to
134
139
  # Langfuse without any extra wiring.
140
+ if prompt_caching() and self._base_url is None:
141
+ # OpenAI caches long prefixes by itself; the key sends a model's calls to the same cache
142
+ # (base.py PROMPT_CACHE_ENV). Only for OpenAI itself: an OpenAI-compatible server
143
+ # (custom.py) may reject a field it does not know.
144
+ kwargs = with_model_kwargs(kwargs, prompt_cache_key=f"{PROMPT_CACHE_KEY_PREFIX}{model_id}")
135
145
  return ChatOpenAI(
136
146
  model=model_id,
137
147
  api_key=self._api_key,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "otto-cli-agent"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Otto: a terminal AI agent built around measured results -- one agent loop, a rubric-first evaluator, tiered memory, multi-vendor routing, and six benchmark harnesses."
5
5
  readme = "docs/pypi.md"
6
6
  license = { file = "LICENSE" }
@@ -0,0 +1,108 @@
1
+ """Input-token caching is on for all four providers (agent/router/llm_provider/base.py
2
+ PROMPT_CACHE_ENV), and what each vendor reports as cached is priced as cached."""
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from agent.pipeline.usage import UsageLedger
8
+ from agent.router.llm_provider import base
9
+ from agent.router.llm_provider.anthropic_provider import PROMPT_CACHE, AnthropicProvider
10
+ from agent.router.llm_provider.inception_provider import _usage_metadata
11
+ from agent.router.llm_provider.openai_provider import OpenAIProvider
12
+
13
+
14
+ def _anthropic(**kw):
15
+ provider = AnthropicProvider.__new__(AnthropicProvider)
16
+ provider._api_key, provider._base_url = "sk-ant-test", kw.get("base_url")
17
+ return provider
18
+
19
+
20
+ def _openai(base_url=None):
21
+ provider = OpenAIProvider.__new__(OpenAIProvider)
22
+ provider._api_key, provider._base_url = "sk-test", base_url
23
+ return provider
24
+
25
+
26
+ def test_anthropic_asks_for_automatic_caching_at_the_top_of_the_request(monkeypatch):
27
+ monkeypatch.delenv(base.PROMPT_CACHE_ENV, raising=False)
28
+ llm = _anthropic().chat_model("claude-haiku-4-5-20251001", max_tokens=64)
29
+ assert llm.model_kwargs["cache_control"] == PROMPT_CACHE
30
+ payload = llm._get_request_payload([("human", "hi")])
31
+ assert payload["cache_control"] == {"type": "ephemeral"}
32
+
33
+
34
+ def test_a_route_keeps_its_own_model_kwargs_and_may_opt_out(monkeypatch):
35
+ monkeypatch.delenv(base.PROMPT_CACHE_ENV, raising=False)
36
+ llm = _anthropic().chat_model("claude-haiku-4-5", model_kwargs={"service_tier": "auto"})
37
+ assert llm.model_kwargs == {"service_tier": "auto", "cache_control": PROMPT_CACHE}
38
+ llm = _anthropic().chat_model("claude-haiku-4-5", model_kwargs={"cache_control": {"type": "ephemeral", "ttl": "1h"}})
39
+ assert llm.model_kwargs["cache_control"]["ttl"] == "1h"
40
+ llm = _anthropic().chat_model("claude-haiku-4-5", model_kwargs={"cache_control": None})
41
+ assert "cache_control" not in llm.model_kwargs
42
+
43
+
44
+ def test_openai_routes_a_models_calls_to_one_cache(monkeypatch):
45
+ monkeypatch.delenv(base.PROMPT_CACHE_ENV, raising=False)
46
+ llm = _openai().chat_model("gpt-5-mini")
47
+ assert llm.model_kwargs["prompt_cache_key"] == "otto:gpt-5-mini"
48
+ assert "prompt_cache_key" in llm._get_request_payload([("human", "hi")])
49
+ # An OpenAI-compatible server is not sent a field it may not know.
50
+ assert "prompt_cache_key" not in _openai("http://localhost:11434/v1").chat_model("llama3").model_kwargs
51
+
52
+
53
+ @pytest.mark.parametrize("value", ["0", "false", "OFF"])
54
+ def test_the_switch_turns_it_off(monkeypatch, value):
55
+ monkeypatch.setenv(base.PROMPT_CACHE_ENV, value)
56
+ assert "cache_control" not in _anthropic().chat_model("claude-haiku-4-5").model_kwargs
57
+ assert "prompt_cache_key" not in _openai().chat_model("gpt-5-mini").model_kwargs
58
+
59
+
60
+ def test_inception_reports_its_cached_tokens():
61
+ assert _usage_metadata(100, 5, 105, 80) == {
62
+ "input_tokens": 100, "output_tokens": 5, "total_tokens": 105,
63
+ "input_token_details": {"cache_read": 80}}
64
+ assert "input_token_details" not in _usage_metadata(100, 5, 105, 0)
65
+ assert "input_token_details" not in _usage_metadata(100, 5, 105, None)
66
+
67
+
68
+ def test_inception_streaming_carries_the_cached_count():
69
+ from tests.test_inception_stream import accumulate, content_chunk, model, usage_chunk
70
+
71
+ llm = model([content_chunk("hi"), usage_chunk(
72
+ {"prompt_tokens": 2000, "completion_tokens": 3, "total_tokens": 2003, "cached_input_tokens": 1800})])
73
+ assert accumulate(llm).usage_metadata["input_token_details"] == {"cache_read": 1800}
74
+
75
+
76
+ def test_cached_reads_and_writes_reach_the_snapshot():
77
+ ledger = UsageLedger()
78
+ ledger.record("claude-haiku-4-5", {
79
+ "input_tokens": 5000, "output_tokens": 10, "total_tokens": 5010,
80
+ "input_token_details": {"cache_read": 4000, "cache_creation": 900}})
81
+ snap = ledger.snapshot()
82
+ assert snap["cached_input_tokens"] == 4000 and snap["cache_write_tokens"] == 900
83
+ assert snap["models"][0]["cache_write_tokens"] == 900
84
+
85
+
86
+ def test_inception_reads_the_cached_count_where_the_api_puts_it():
87
+ from agent.router.llm_provider.inception_provider import _cached_tokens
88
+
89
+ assert _cached_tokens({"prompt_tokens": 9, "prompt_tokens_details": {"cached_tokens": 7}}) == 7
90
+ assert _cached_tokens({"cached_input_tokens": 5, "prompt_tokens_details": {"cached_tokens": 0}}) == 5
91
+ assert not _cached_tokens({"cached_input_tokens": None, "prompt_tokens_details": {"cached_tokens": 0}})
92
+ assert not _cached_tokens({"prompt_tokens": 9})
93
+
94
+
95
+ def test_an_anthropic_write_reported_by_its_ttl_is_priced_as_a_write():
96
+ # The shape langchain-anthropic 1.7 returns for a first cached call (seen live, 2026-09-17).
97
+ ledger = UsageLedger()
98
+ ledger.record("claude-haiku-4-5", {
99
+ "input_tokens": 12203, "output_tokens": 4, "total_tokens": 12207,
100
+ "input_token_details": {"cache_read": 0, "cache_creation": 0,
101
+ "ephemeral_5m_input_tokens": 12200, "ephemeral_1h_input_tokens": 0}})
102
+ assert ledger.snapshot()["cache_write_tokens"] == 12200
103
+ ledger.record("claude-haiku-4-5", {
104
+ "input_tokens": 12203, "output_tokens": 4, "total_tokens": 12207,
105
+ "input_token_details": {"cache_read": 12200, "cache_creation": 0,
106
+ "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0}})
107
+ snap = ledger.snapshot()
108
+ assert snap["cached_input_tokens"] == 12200 and snap["cache_write_tokens"] == 12200
@@ -1842,7 +1842,7 @@ wheels = [
1842
1842
 
1843
1843
  [[package]]
1844
1844
  name = "otto-cli-agent"
1845
- version = "0.1.2"
1845
+ version = "0.1.3"
1846
1846
  source = { editable = "." }
1847
1847
  dependencies = [
1848
1848
  { name = "emoji" },
File without changes