claude-code-conductor 2.20.0__tar.gz → 2.21.0__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 (192) hide show
  1. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/session_stop.py +17 -0
  2. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/CHANGELOG.md +34 -0
  3. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/PKG-INFO +1 -1
  4. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/__init__.py +1 -1
  5. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_tier.py +26 -1
  6. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/db.py +247 -0
  7. claude_code_conductor-2.21.0/src/c3/migrations/002_agent_cost_runs.sql +41 -0
  8. claude_code_conductor-2.21.0/src/c3/pricing.py +157 -0
  9. claude_code_conductor-2.21.0/src/c3/usage_ingester.py +388 -0
  10. claude_code_conductor-2.21.0/tests/fixtures/usage/README.md +18 -0
  11. claude_code_conductor-2.21.0/tests/fixtures/usage/mainline.jsonl +5 -0
  12. claude_code_conductor-2.21.0/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +3 -0
  13. claude_code_conductor-2.21.0/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +1 -0
  14. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_session_start.py +11 -5
  15. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_tier.py +90 -0
  16. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_db.py +158 -4
  17. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_migrate.py +64 -0
  18. claude_code_conductor-2.21.0/tests/test_pricing.py +225 -0
  19. claude_code_conductor-2.21.0/tests/test_usage_ingester.py +529 -0
  20. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/CLAUDE.md +0 -0
  21. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/architect.md +0 -0
  22. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/code-reviewer.md +0 -0
  23. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/developer.md +0 -0
  24. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/doc-writer.md +0 -0
  25. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/interviewer.md +0 -0
  26. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/planner.md +0 -0
  27. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/project-setup.md +0 -0
  28. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/security-reviewer.md +0 -0
  29. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/systematic-debugger.md +0 -0
  30. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/tester.md +0 -0
  31. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/wt_developer.md +0 -0
  32. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/wt_systematic-debugger.md +0 -0
  33. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/agents/wt_tester.md +0 -0
  34. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/breaking-changes.txt +0 -0
  35. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/deletions.txt +0 -0
  36. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/docs/config-policy.md +0 -0
  37. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/docs/parallel-agents-setup.md +0 -0
  38. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/docs/platform-adapters.md +0 -0
  39. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/docs/settings.json.md +0 -0
  40. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/_hook_utils.py +0 -0
  41. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/check_agent_invocation.py +0 -0
  42. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/consolidate_memory.py +0 -0
  43. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/permission_handler.py +0 -0
  44. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/permission_handler_toast.py +0 -0
  45. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/planner_check.py +0 -0
  46. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/post_tool.py +0 -0
  47. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/pre_compact.py +0 -0
  48. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/pre_tool.py +0 -0
  49. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/recall_inject.py +0 -0
  50. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/restore_session.py +0 -0
  51. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/select_tier.py +0 -0
  52. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/session_start.py +0 -0
  53. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/session_utils.py +0 -0
  54. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/statusline.py +0 -0
  55. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/stop.py +0 -0
  56. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/hooks/worktree_guard.py +0 -0
  57. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/memory/.gitkeep +0 -0
  58. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/permission_rules.json +0 -0
  59. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/rules/promoted/index.md +0 -0
  60. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/settings.json +0 -0
  61. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/codex-review/SKILL.md +0 -0
  62. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
  63. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
  64. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
  65. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
  66. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
  67. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/scripts/record_tier_outcome.py +0 -0
  68. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
  69. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/develop/SKILL.md +0 -0
  70. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/doc/SKILL.md +0 -0
  71. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/extract-lib/SKILL.md +0 -0
  72. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/init-session/SKILL.md +0 -0
  73. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/mcp-config/SKILL.md +0 -0
  74. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/parallel-agents/SKILL.md +0 -0
  75. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/pattern-status/SKILL.md +0 -0
  76. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
  77. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/recall/SKILL.md +0 -0
  78. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
  79. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  80. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/review-phase/SKILL.md +0 -0
  81. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/setup/SKILL.md +0 -0
  82. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/setup/reference.md +0 -0
  83. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
  84. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
  85. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/skills/start/SKILL.md +0 -0
  86. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.claude/state/.gitkeep +0 -0
  87. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/.gitignore +0 -0
  88. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSE +0 -0
  89. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSES/chroma-hnswlib-LICENSE +0 -0
  90. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSES/chroma-hnswlib-NOTICE +0 -0
  91. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSES/fastembed-LICENSE +0 -0
  92. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSES/fastembed-NOTICE +0 -0
  93. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSES/onnxruntime-LICENSE +0 -0
  94. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
  95. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/README.md +0 -0
  96. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/hatch_build.py +0 -0
  97. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/pyproject.toml +0 -0
  98. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/__main__.py +0 -0
  99. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/_excludes.py +0 -0
  100. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/_terminal.py +0 -0
  101. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/adapters.py +0 -0
  102. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli.py +0 -0
  103. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_ask.py +0 -0
  104. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_doctor.py +0 -0
  105. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_init.py +0 -0
  106. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_list.py +0 -0
  107. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_plan.py +0 -0
  108. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_recall.py +0 -0
  109. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/cli_update.py +0 -0
  110. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/embedding.py +0 -0
  111. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/mcp_server.py +0 -0
  112. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/migrate.py +0 -0
  113. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/migrations/001_initial.sql +0 -0
  114. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/migrations/README.md +0 -0
  115. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/migrations/__init__.py +0 -0
  116. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/paths.py +0 -0
  117. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/plan_validator.py +0 -0
  118. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/platforms.py +0 -0
  119. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/question.py +0 -0
  120. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/recall_chunker.py +0 -0
  121. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/src/c3/recall_index.py +0 -0
  122. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/__init__.py +0 -0
  123. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/conftest.py +0 -0
  124. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/__init__.py +0 -0
  125. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_check_agent_invocation.py +0 -0
  126. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_consolidate_memory.py +0 -0
  127. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_hook_utils.py +0 -0
  128. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_permission_handler.py +0 -0
  129. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_permission_handler_toast.py +0 -0
  130. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  131. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_planner_check.py +0 -0
  132. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_planner_check_dev.py +0 -0
  133. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_post_tool.py +0 -0
  134. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_pre_tool.py +0 -0
  135. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_recall_inject.py +0 -0
  136. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_record_review_decision.py +0 -0
  137. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_record_tier_outcome.py +0 -0
  138. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_restore_session.py +0 -0
  139. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_review_hint_inject.py +0 -0
  140. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_select_tier.py +0 -0
  141. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_select_tier_escalation.py +0 -0
  142. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_session_stop.py +0 -0
  143. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_session_utils.py +0 -0
  144. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  145. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_similarity_boost.py +0 -0
  146. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_statusline.py +0 -0
  147. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_statusline_template_sync.py +0 -0
  148. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_sync_check.py +0 -0
  149. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/hooks/test_template_guard.py +0 -0
  150. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/__init__.py +0 -0
  151. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/_skill_helpers.py +0 -0
  152. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
  153. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_init_session_no_task_type.py +0 -0
  154. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_planner_lightweight.py +0 -0
  155. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_recall_skill.py +0 -0
  156. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
  157. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_setup_templates.py +0 -0
  158. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
  159. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_start_skill_new_flow.py +0 -0
  160. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
  161. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_adapters.py +0 -0
  162. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_ask.py +0 -0
  163. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_entry.py +0 -0
  164. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_init.py +0 -0
  165. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_list.py +0 -0
  166. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_plan.py +0 -0
  167. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_recall.py +0 -0
  168. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_update_breaking_changes.py +0 -0
  169. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_cli_update_deletions.py +0 -0
  170. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_docstring_consistency.py +0 -0
  171. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_embedding.py +0 -0
  172. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_excludes.py +0 -0
  173. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_extract_breaking_changes.py +0 -0
  174. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_mcp_server_elicit.py +0 -0
  175. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_paths.py +0 -0
  176. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_plan_validator.py +0 -0
  177. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_pre_compact.py +0 -0
  178. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_pre_tool_hook.py +0 -0
  179. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_precompact_additional.py +0 -0
  180. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_precompact_toctou_fixes.py +0 -0
  181. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_recall_chunker.py +0 -0
  182. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_recall_index.py +0 -0
  183. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_references_migration.py +0 -0
  184. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_session_utils_additional.py +0 -0
  185. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_skill_no_builtin_conflict.py +0 -0
  186. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_statusline.py +0 -0
  187. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_stop_additional.py +0 -0
  188. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_stop_hook.py +0 -0
  189. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_stop_precompact_fixes.py +0 -0
  190. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_sync_template_stop.py +0 -0
  191. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_template_pre_tool_hook.py +0 -0
  192. {claude_code_conductor-2.20.0 → claude_code_conductor-2.21.0}/tests/test_worktree_guard.py +0 -0
@@ -89,6 +89,23 @@ def main() -> int:
89
89
  except Exception as e:
90
90
  print(f"[session_stop:consolidate_memory] failed: {e}", file=sys.stderr)
91
91
 
92
+ # Phase 3: usage_ingester — セッションログからコスト集計データを ingestion
93
+ # worktree session では起動しない(stop.run の is_worktree 判定と整合)
94
+ # 安全側動作: session_utils ロード失敗時や is_worktree 判定失敗時は ingester をスキップする。
95
+ # worktree 判定不能で誤起動するより skip を優先(except Exception で捕捉 → 早期終了)。
96
+ try:
97
+ session_utils_module = _load_module("session_utils")
98
+ if not session_utils_module.is_worktree(os.getcwd()):
99
+ session_id = payload.get("session_id")
100
+ transcript_path = payload.get("transcript_path")
101
+ if session_id and transcript_path:
102
+ from pathlib import Path as _Path # noqa: PLC0415
103
+ from c3.usage_ingester import ingest_session # noqa: PLC0415
104
+ project_dir = _Path(transcript_path).parent
105
+ ingest_session(session_id=session_id, project_dir=project_dir)
106
+ except Exception as e:
107
+ print(f"[session_stop:usage_ingester] failed: {type(e).__name__}", file=sys.stderr)
108
+
92
109
  return 0
93
110
 
94
111
 
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.21.0] - 2026-05-25
4
+
5
+ **tier-routing コスト統合(データ収集基盤)**: Claude Code セッションログ(`~/.claude/projects/<slug>/<session>.jsonl` + subagent jsonl)を読み込み、モデル単価で USD 換算して c3.db に蓄積するデータ収集基盤を整備する。将来の cost-aware routing(v2.22.0)の土台。
6
+
7
+ **スコープ注記**: 本リリースはデータ収集基盤のみ。cost-aware routing と tier_bandit のコスト列は v2.22.0 予定。
8
+
9
+ ### 機能追加
10
+
11
+ - **`src/c3/pricing.py`(新規)**: Claude API モデルの USD/MTok 単価から token コストを計算する純関数モジュール。
12
+ `resolve_tier(model)`・`compute_cost_usd(...) -> tuple[float, bool]`・`known_models()` を提供。
13
+ Opus は世代で単価が 3 倍異なる(4.1/4=$15 系、4.5/4.6/4.7=$5 系)ため、具体パターン優先マッチ → tier 部分一致 fallback の 2 段構成を採用。
14
+ 単価は 2026-05-25 公式取得値(出典 URL を docstring に明記)。
15
+
16
+ - **`src/c3/migrations/002_agent_cost_runs.sql`(新規)**: `agent_cost_runs` テーブル・`usage_ingest_state` テーブル・インデックスを追加する migration。
17
+ PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」の集約設計。
18
+ 既存 event-based `agent_runs`(001)は一切変更しない。
19
+
20
+ - **`src/c3/usage_ingester.py`(新規)**: セッションログ取り込みモジュール。
21
+ 公開 API `ingest_session(*, session_id, project_dir, db_path=None) -> IngestResult`。
22
+ mainline / subagent jsonl を走査し、model 単位でトークンを合算して `insert_agent_cost_run` で upsert する。
23
+ session_id UUID validate・パストラバーサル防止・symlink スキップ・例外 type 名のみログ(SR-R-001 準拠)。
24
+
25
+ - **`.claude/hooks/session_stop.py` Phase 3 追加**: セッション終了時に `ingest_session` を呼ぶ Phase 3 を追加。
26
+ worktree session では起動しない。例外握りつぶしで exit 0 を維持する。
27
+
28
+ - **`c3 tier stats` の Agent 別コスト集計セクション追加**: `_collect_snapshot()` に `read_agent_cost_summary()` を追加し、human / JSON 両出力に `agent_cost` セクションを表示する。
29
+ mainline 行には「(マクロ集計・tier 学習対象外)」を明示。0 件のときは「(コストデータ未収集)」を表示。
30
+
31
+ ### 変更
32
+
33
+ - **`src/c3/db.py`**: 4 ヘルパー追加(`insert_agent_cost_run` / `read_agent_cost_summary` / `get_ingest_offset` / `set_ingest_offset`)。既存 tier ヘルパー規約(DB 不在で静かに False/0/[]・WAL・busy_timeout)に準拠。
34
+
35
+ - **`src/c3/__init__.py`**: `__version__` を `"2.20.0"` から `"2.21.0"` に更新。
36
+
3
37
  ## [2.20.0] - 2026-05-25
4
38
 
5
39
  **SQLite schema migration 枠組み導入**: `.claude/hooks/schema.sql` の「冪等 DDL 一発実行」から、`src/c3/migrations/` の「連番 NNN_xxx.sql migration runner」へ移行する基盤を整備する。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-conductor
3
- Version: 2.20.0
3
+ Version: 2.21.0
4
4
  Summary: Multi-agent orchestration framework for Claude Code with Codex/Cursor adapters (C3)
5
5
  Project-URL: Homepage, https://github.com/satoh-y-0323/claude-code-conductor
6
6
  Project-URL: Repository, https://github.com/satoh-y-0323/claude-code-conductor
@@ -1,3 +1,3 @@
1
1
  """Claude Code Conductor (C3) - multi-agent orchestration framework for Claude Code."""
2
2
 
3
- __version__ = "2.20.0"
3
+ __version__ = "2.21.0"
@@ -89,7 +89,7 @@ def handle_stats(args: argparse.Namespace) -> int:
89
89
 
90
90
 
91
91
  def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
92
- """DB から tier_bandit / tier_recent_outcomes を読み snapshot dict を返す。"""
92
+ """DB から tier_bandit / tier_recent_outcomes / agent_cost を読み snapshot dict を返す。"""
93
93
  bandit_rows: list[dict[str, Any]] = []
94
94
  total_trials = 0
95
95
 
@@ -114,6 +114,8 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
114
114
  db_path=db_path,
115
115
  )
116
116
 
117
+ agent_cost: list[dict[str, Any]] = c3_db.read_agent_cost_summary(db_path=db_path)
118
+
117
119
  if total_trials < _LEARNING_THRESHOLD:
118
120
  mode = "uniform"
119
121
  else:
@@ -127,6 +129,7 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
127
129
  },
128
130
  "tier_bandit": bandit_rows,
129
131
  "recent_outcomes": recent_outcomes,
132
+ "agent_cost": agent_cost,
130
133
  }
131
134
 
132
135
 
@@ -166,3 +169,25 @@ def _render_human(snapshot: dict[str, Any]) -> None:
166
169
  print("== 学習データ記録チャネル ==")
167
170
  print("記録元: dev-workflow フェーズ E の最終承認時のみ(record_tier_outcome.py)")
168
171
  print("直接指示作業ではデータが溜まりません(設計通り)")
172
+ print()
173
+
174
+ print("== Agent 別コスト集計(agent_cost_runs) ==")
175
+ agent_cost = snapshot.get("agent_cost", [])
176
+ if not agent_cost:
177
+ print("(コストデータ未収集)")
178
+ else:
179
+ print(
180
+ f"{'agent_type':<16} {'runs':>5} {'total_usd':>10} "
181
+ f"{'in_tok':>9} {'out_tok':>9} {'cache_r':>9} {'cache_w':>9}"
182
+ )
183
+ for row in agent_cost:
184
+ note = " (マクロ集計・tier 学習対象外)" if row["agent_type"] == "mainline" else ""
185
+ print(
186
+ f"{row['agent_type']:<16} {row['runs']:>5} "
187
+ f"${row['total_cost_usd']:>9.4f} "
188
+ f"{row['input_tokens']:>9} {row['output_tokens']:>9} "
189
+ f"{row['cache_read_tokens']:>9} {row['cache_create_tokens']:>9}"
190
+ f"{note}"
191
+ )
192
+ print()
193
+ print("(注: 本リリースはデータ収集基盤のみ。cost-aware routing は v2.22.0 予定)")
@@ -493,3 +493,250 @@ def read_tier_failure_rate(
493
493
 
494
494
  failures = sum(1 for r in rows if r[0] == 0)
495
495
  return failures / sample_count, sample_count
496
+
497
+
498
+ # ---------------------------------------------------------------------------
499
+ # usage-ingester: agent_cost_runs / usage_ingest_state ヘルパー(v2.21.0)
500
+ # ---------------------------------------------------------------------------
501
+
502
+
503
+ def insert_agent_cost_run(
504
+ *,
505
+ session_id: str,
506
+ agent_id: str,
507
+ agent_type: str,
508
+ description: str | None,
509
+ model: str,
510
+ attribution_skill: str | None,
511
+ input_tokens: int,
512
+ output_tokens: int,
513
+ cache_read_tokens: int,
514
+ cache_create_tokens: int,
515
+ total_cost_usd: float,
516
+ db_path: Path | None = None,
517
+ ) -> bool:
518
+ """agent_cost_runs に 1 行 upsert する。
519
+
520
+ PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」。
521
+ 同一 PK が既に存在する場合は全数値列・total_cost_usd・recorded_at を最新合算値に上書き。
522
+
523
+ Args:
524
+ session_id: セッション UUID。
525
+ agent_id: 'agent-<id>' または 'mainline'。
526
+ agent_type: meta.json の agentType / 'mainline'。
527
+ description: meta.json の description(任意)。
528
+ model: message.model 文字列。
529
+ attribution_skill: assistant レコードの attributionSkill(任意)。
530
+ input_tokens: 入力トークン数(jsonl 内合算値)。
531
+ output_tokens: 出力トークン数(jsonl 内合算値)。
532
+ cache_read_tokens: キャッシュ読み込みトークン数(jsonl 内合算値)。
533
+ cache_create_tokens: キャッシュ書き込みトークン数(jsonl 内合算値)。
534
+ total_cost_usd: compute_cost_usd が返した USD コスト。
535
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
536
+
537
+ Returns:
538
+ upsert 成功時 True、DB 不在 / sqlite3.Error 時は静かに False。
539
+ """
540
+ if db_path is None:
541
+ db_path = locate_c3_db()
542
+ if db_path is None:
543
+ return False
544
+
545
+ from datetime import timezone as _tz # noqa: PLC0415
546
+ now_iso = datetime.now(_tz.utc).isoformat(timespec="seconds")
547
+
548
+ try:
549
+ conn = sqlite3.connect(str(db_path))
550
+ try:
551
+ conn.execute("PRAGMA journal_mode=WAL")
552
+ _apply_busy_timeout(conn)
553
+ conn.execute(
554
+ "INSERT INTO agent_cost_runs "
555
+ "(session_id, agent_id, agent_type, description, model, "
556
+ " attribution_skill, input_tokens, output_tokens, "
557
+ " cache_read_tokens, cache_create_tokens, "
558
+ " total_cost_usd, recorded_at) "
559
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
560
+ "ON CONFLICT(session_id, agent_id, model) DO UPDATE SET "
561
+ " agent_type = excluded.agent_type, "
562
+ " description = excluded.description, "
563
+ " attribution_skill = excluded.attribution_skill, "
564
+ " input_tokens = excluded.input_tokens, "
565
+ " output_tokens = excluded.output_tokens, "
566
+ " cache_read_tokens = excluded.cache_read_tokens, "
567
+ " cache_create_tokens = excluded.cache_create_tokens, "
568
+ " total_cost_usd = excluded.total_cost_usd, "
569
+ " recorded_at = excluded.recorded_at",
570
+ (
571
+ session_id, agent_id, agent_type, description, model,
572
+ attribution_skill, input_tokens, output_tokens,
573
+ cache_read_tokens, cache_create_tokens,
574
+ total_cost_usd, now_iso,
575
+ ),
576
+ )
577
+ conn.commit()
578
+ finally:
579
+ conn.close()
580
+ return True
581
+ except Exception as exc: # noqa: BLE001
582
+ logger.warning("failed to insert_agent_cost_run: %s", type(exc).__name__)
583
+ return False
584
+
585
+
586
+ def read_agent_cost_summary(
587
+ *,
588
+ db_path: Path | None = None,
589
+ limit: int = 50,
590
+ ) -> list[dict]:
591
+ """agent_cost_runs を agent_type 別に集計した結果を返す。
592
+
593
+ SELECT agent_type, COUNT(*) runs, SUM(total_cost_usd), SUM(input_tokens),
594
+ SUM(output_tokens), SUM(cache_read_tokens), SUM(cache_create_tokens)
595
+ FROM agent_cost_runs GROUP BY agent_type ORDER BY total_cost_usd DESC LIMIT ?
596
+
597
+ Args:
598
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
599
+ limit: 返す最大件数(デフォルト 50)。
600
+
601
+ Returns:
602
+ 各行を dict にしたリスト。キー:
603
+ ``agent_type`` / ``runs`` / ``total_cost_usd`` /
604
+ ``input_tokens`` / ``output_tokens`` /
605
+ ``cache_read_tokens`` / ``cache_create_tokens``。
606
+ DB 不在 / テーブル不在 / エラー時は空リスト。
607
+ """
608
+ if db_path is None:
609
+ db_path = locate_c3_db()
610
+ if db_path is None:
611
+ return []
612
+
613
+ try:
614
+ conn = sqlite3.connect(str(db_path))
615
+ try:
616
+ # WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
617
+ # (既存 read_recent_outcomes 等の read ヘルパーと同方針 / CR-M-001)
618
+ _apply_busy_timeout(conn)
619
+ rows = conn.execute(
620
+ "SELECT agent_type, "
621
+ " COUNT(*) AS runs, "
622
+ " SUM(total_cost_usd) AS total_cost_usd, "
623
+ " SUM(input_tokens) AS input_tokens, "
624
+ " SUM(output_tokens) AS output_tokens, "
625
+ " SUM(cache_read_tokens) AS cache_read_tokens, "
626
+ " SUM(cache_create_tokens) AS cache_create_tokens "
627
+ "FROM agent_cost_runs "
628
+ "GROUP BY agent_type "
629
+ "ORDER BY total_cost_usd DESC "
630
+ "LIMIT ?",
631
+ (limit,),
632
+ ).fetchall()
633
+ finally:
634
+ conn.close()
635
+ except sqlite3.OperationalError as exc:
636
+ # テーブル不在(no such table)は [] を返す(DB 未初期化でも止めない)
637
+ logger.debug(
638
+ "read_agent_cost_summary: table not found or inaccessible: %s",
639
+ type(exc).__name__,
640
+ )
641
+ return []
642
+ except Exception as exc: # noqa: BLE001
643
+ logger.warning("read_agent_cost_summary: unexpected error: %s", type(exc).__name__)
644
+ return []
645
+
646
+ return [
647
+ {
648
+ "agent_type": row[0],
649
+ "runs": row[1],
650
+ "total_cost_usd": row[2],
651
+ "input_tokens": row[3],
652
+ "output_tokens": row[4],
653
+ "cache_read_tokens": row[5],
654
+ "cache_create_tokens": row[6],
655
+ }
656
+ for row in rows
657
+ ]
658
+
659
+
660
+ def get_ingest_offset(
661
+ file_key: str,
662
+ *,
663
+ db_path: Path | None = None,
664
+ ) -> int:
665
+ """usage_ingest_state から処理済み行数(offset)を返す。
666
+
667
+ Args:
668
+ file_key: '<session>:mainline' / '<session>:agent-<id>'。
669
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
670
+
671
+ Returns:
672
+ 処理済み行数。行・テーブル不在・DB 不在は 0 を返す。
673
+ """
674
+ if db_path is None:
675
+ db_path = locate_c3_db()
676
+ if db_path is None:
677
+ return 0
678
+
679
+ try:
680
+ conn = sqlite3.connect(str(db_path))
681
+ try:
682
+ # WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
683
+ # (既存 read_recent_outcomes 等の read ヘルパーと同方針 / CR-M-001)
684
+ _apply_busy_timeout(conn)
685
+ row = conn.execute(
686
+ "SELECT last_offset FROM usage_ingest_state WHERE file_key = ?",
687
+ (file_key,),
688
+ ).fetchone()
689
+ finally:
690
+ conn.close()
691
+ except Exception as exc: # noqa: BLE001
692
+ logger.debug("get_ingest_offset: %s", type(exc).__name__)
693
+ return 0
694
+
695
+ return row[0] if row is not None else 0
696
+
697
+
698
+ def set_ingest_offset(
699
+ file_key: str,
700
+ offset: int,
701
+ *,
702
+ db_path: Path | None = None,
703
+ ) -> bool:
704
+ """usage_ingest_state に offset を upsert する。
705
+
706
+ Args:
707
+ file_key: '<session>:mainline' / '<session>:agent-<id>'。
708
+ offset: 処理済み行数(= 次回開始行)。
709
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
710
+
711
+ Returns:
712
+ upsert 成功時 True、DB 不在 / エラー時は静かに False。
713
+ """
714
+ if db_path is None:
715
+ db_path = locate_c3_db()
716
+ if db_path is None:
717
+ return False
718
+
719
+ from datetime import timezone as _tz # noqa: PLC0415
720
+ now_iso = datetime.now(_tz.utc).isoformat(timespec="seconds")
721
+
722
+ try:
723
+ conn = sqlite3.connect(str(db_path))
724
+ try:
725
+ conn.execute("PRAGMA journal_mode=WAL")
726
+ _apply_busy_timeout(conn)
727
+ conn.execute(
728
+ "INSERT INTO usage_ingest_state "
729
+ "(file_key, last_offset, last_processed_at) "
730
+ "VALUES (?, ?, ?) "
731
+ "ON CONFLICT(file_key) DO UPDATE SET "
732
+ " last_offset = excluded.last_offset, "
733
+ " last_processed_at = excluded.last_processed_at",
734
+ (file_key, offset, now_iso),
735
+ )
736
+ conn.commit()
737
+ finally:
738
+ conn.close()
739
+ return True
740
+ except Exception as exc: # noqa: BLE001
741
+ logger.warning("failed to set_ingest_offset: %s", type(exc).__name__)
742
+ return False
@@ -0,0 +1,41 @@
1
+ -- C3 SQLite migration 002: agent_cost_runs / usage_ingest_state テーブル追加
2
+ --
3
+ -- セッションログ(~/.claude/projects/<slug>/<session>.jsonl + subagents/)から
4
+ -- モデル別トークン消費を USD 換算して蓄積するコスト収集基盤テーブルを定義する。
5
+ --
6
+ -- PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」。
7
+ -- jsonl 内の複数 requestId は ingester(usage_ingester.py)が合算 upsert する。
8
+ --
9
+ -- usage_ingest_state は jsonl ごとの処理済み行数 offset を管理する。
10
+ -- file_key = '<session>:mainline' / '<session>:agent-<id>'
11
+
12
+ BEGIN;
13
+
14
+ CREATE TABLE IF NOT EXISTS agent_cost_runs (
15
+ session_id TEXT NOT NULL,
16
+ agent_id TEXT NOT NULL, -- 'agent-<id>' / mainline は 'mainline'
17
+ agent_type TEXT NOT NULL, -- meta.json agentType / 'mainline'
18
+ description TEXT,
19
+ model TEXT NOT NULL, -- message.model
20
+ attribution_skill TEXT,
21
+ input_tokens INTEGER NOT NULL DEFAULT 0,
22
+ output_tokens INTEGER NOT NULL DEFAULT 0,
23
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
24
+ cache_create_tokens INTEGER NOT NULL DEFAULT 0,
25
+ total_cost_usd REAL NOT NULL,
26
+ recorded_at TEXT NOT NULL, -- ISO8601 UTC
27
+ PRIMARY KEY (session_id, agent_id, model)
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_agent_cost_runs_agent_type
31
+ ON agent_cost_runs(agent_type, recorded_at);
32
+
33
+ CREATE TABLE IF NOT EXISTS usage_ingest_state (
34
+ file_key TEXT PRIMARY KEY, -- '<session>:mainline' / '<session>:agent-<id>'
35
+ last_offset INTEGER NOT NULL, -- 処理済み行数
36
+ last_processed_at TEXT NOT NULL -- ISO8601 UTC
37
+ );
38
+
39
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES ('002');
40
+
41
+ COMMIT;
@@ -0,0 +1,157 @@
1
+ """Claude API モデルの USD/MTok 単価からトークンコストを計算する純関数モジュール。
2
+
3
+ 出典: https://platform.claude.com/docs/en/about-claude/pricing
4
+ 取得日: 2026-05-25
5
+
6
+ 単価は改定されうるため、メンテ時はこの URL を再確認すること。
7
+ (本モジュールの _PRICING dict と docstring の日付を合わせて更新する)
8
+
9
+ 公開 API:
10
+ - resolve_tier(model) -> str | None
11
+ - compute_cost_usd(...) -> tuple[float, bool]
12
+ - known_models() -> tuple[str, ...]
13
+
14
+ 設計判断(plan-report T1 §2):
15
+ Opus は世代で単価が約 3 倍異なる(4.1/4 = $15 系、4.5/4.6/4.7 = $5 系)ため、
16
+ 単純な "opus" 部分一致では取り違える。
17
+ 「現行世代を列挙マッチ → それ以外の opus は旧世代」の 2 段構成で実装する。
18
+ 具体的には: model に "opus-4-[5-9]" にマッチする場合は現行 ($5 系)、
19
+ それ以外の opus("claude-opus-4-YYYYMMDD" 形式の初代 Opus 4 / 4.1 等)は旧世代 ($15 系)。
20
+ 将来 Opus メジャー更新(5.x 等)が出たら本判定と _PRICING の見直しが必要。
21
+ haiku は "haiku-3" を含む場合は旧世代。
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # 単価表 (USD/MTok) 取得日: 2026-05-25
30
+ # キー = pricing tier 識別子(resolve_pricing_key() が返す文字列)
31
+ # 値 = (input, output, cache_5m_write, cache_read)
32
+ # ---------------------------------------------------------------------------
33
+ # 現行世代(C3 が実際に使うモデル)
34
+ # 旧世代(過去ログ互換・現行 C3 では通常出現しない)
35
+
36
+ # 現行 Opus 世代($5 系): 4.5 / 4.6 / 4.7(将来 4.8/4.9 も同単価想定)
37
+ # それ以外の opus(初代 Opus 4 "claude-opus-4-YYYYMMDD" / 4.1 等)は旧世代 $15 系とする。
38
+ _CURRENT_OPUS_RE = re.compile(r"opus-4-[5-9]")
39
+
40
+ _PRICING: dict[str, tuple[float, float, float, float]] = {
41
+ "opus-4-5": (5.0, 25.0, 6.25, 0.50), # Opus 4.5 / 4.6 / 4.7 共通
42
+ "sonnet-4-5": (3.0, 15.0, 3.75, 0.30), # Sonnet 4.5 / 4.6 共通
43
+ "haiku-4-5": (1.0, 5.0, 1.25, 0.10), # Haiku 4.5
44
+ "opus-4-1": (15.0, 75.0, 18.75, 1.50), # Opus 4.1 / 4(旧世代)
45
+ "haiku-3-5": (0.80, 4.0, 1.00, 0.08), # Haiku 3.5(旧世代)
46
+ }
47
+
48
+
49
+ def resolve_tier(model: str) -> str | None:
50
+ """model 文字列から tier 名を返す(集計・表示用グルーピング)。
51
+
52
+ Args:
53
+ model: message.model 文字列(例: "claude-opus-4-7-20260101")
54
+
55
+ Returns:
56
+ "opus" / "sonnet" / "haiku" のいずれか。
57
+ いずれも含まなければ None。
58
+
59
+ Note:
60
+ 単価の世代振り分けには使わない。世代判定は _resolve_pricing_key() が担当する。
61
+ """
62
+ lower = model.lower()
63
+ if "opus" in lower:
64
+ return "opus"
65
+ if "sonnet" in lower:
66
+ return "sonnet"
67
+ if "haiku" in lower:
68
+ return "haiku"
69
+ return None
70
+
71
+
72
+ def _resolve_pricing_key(model: str) -> str | None:
73
+ """model 文字列を _PRICING キーに解決する(内部関数)。
74
+
75
+ 2 段構成で世代を振り分ける:
76
+ 1. 具体パターン優先マッチ: Opus 旧世代 / Haiku 旧世代
77
+ 2. tier 部分一致 fallback: 現行世代
78
+
79
+ Args:
80
+ model: message.model 文字列
81
+
82
+ Returns:
83
+ _PRICING のキー文字列。既知モデルでなければ None。
84
+ """
85
+ lower = model.lower()
86
+
87
+ # ---- Opus: 世代判定が必要 ----
88
+ if "opus" in lower:
89
+ # 現行世代($5 系: 4.5/4.6/4.7)を列挙マッチ
90
+ # それ以外(初代 Opus 4 "claude-opus-4-YYYYMMDD" / 4.1 等)は旧世代 ($15 系)
91
+ if _CURRENT_OPUS_RE.search(lower):
92
+ return "opus-4-5" # 現行 $5 系
93
+ return "opus-4-1" # 旧世代 $15 系(4.0/4.1/初代 Opus 4 等)
94
+
95
+ # ---- Haiku: 世代判定が必要 ----
96
+ if "haiku" in lower:
97
+ # "haiku-3" を含めば旧世代 ($0.80 系)
98
+ if "haiku-3" in lower:
99
+ return "haiku-3-5"
100
+ # それ以外(haiku-4-5 等)は現行
101
+ return "haiku-4-5"
102
+
103
+ # ---- Sonnet: 世代で単価不変 ($3 系) ----
104
+ if "sonnet" in lower:
105
+ return "sonnet-4-5"
106
+
107
+ return None
108
+
109
+
110
+ def compute_cost_usd(
111
+ *,
112
+ model: str,
113
+ input_tokens: int,
114
+ output_tokens: int,
115
+ cache_read_tokens: int,
116
+ cache_create_tokens: int,
117
+ ) -> tuple[float, bool]:
118
+ """トークン数から USD コストを計算する純関数。
119
+
120
+ Args:
121
+ model: message.model 文字列(例: "claude-opus-4-7-20260101")
122
+ input_tokens: 入力トークン数
123
+ output_tokens: 出力トークン数
124
+ cache_read_tokens: キャッシュ読み込みトークン数
125
+ cache_create_tokens: キャッシュ書き込みトークン数(5 分キャッシュ単価を採用)
126
+
127
+ Returns:
128
+ (cost_usd, known) のタプル。
129
+ - cost_usd: 計算した USD コスト(不明モデルは 0.0)
130
+ - known: 単価が解決できた場合 True、不明モデルは False
131
+
132
+ Note:
133
+ 不明モデルは (0.0, False) を返す。例外も warning も出さない。
134
+ 換算式: tokens / 1_000_000 * unit_price_per_mtok
135
+ """
136
+ key = _resolve_pricing_key(model)
137
+ if key is None:
138
+ return (0.0, False)
139
+
140
+ inp_price, out_price, cache_write_price, cache_read_price = _PRICING[key]
141
+
142
+ cost = (
143
+ input_tokens / 1_000_000 * inp_price
144
+ + output_tokens / 1_000_000 * out_price
145
+ + cache_read_tokens / 1_000_000 * cache_read_price
146
+ + cache_create_tokens / 1_000_000 * cache_write_price
147
+ )
148
+ return (cost, True)
149
+
150
+
151
+ def known_models() -> tuple[str, ...]:
152
+ """単価表のキー一覧を返す(テスト・デバッグ用)。
153
+
154
+ Returns:
155
+ _PRICING のキー文字列のタプル(順序は定義順)。
156
+ """
157
+ return tuple(_PRICING.keys())