shotgun-sh 0.1.16.dev2__tar.gz → 0.2.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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (153) hide show
  1. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/.gitignore +3 -0
  2. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/PKG-INFO +2 -2
  3. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/pyproject.toml +2 -2
  4. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/common.py +4 -5
  5. shotgun_sh-0.2.0/src/shotgun/agents/config/constants.py +33 -0
  6. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/config/manager.py +171 -63
  7. shotgun_sh-0.2.0/src/shotgun/agents/config/models.py +166 -0
  8. shotgun_sh-0.2.0/src/shotgun/agents/config/provider.py +295 -0
  9. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/compaction.py +1 -1
  10. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/history_processors.py +18 -9
  11. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/__init__.py +31 -0
  12. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/anthropic.py +89 -0
  13. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/base.py +67 -0
  14. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/openai.py +80 -0
  15. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
  16. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
  17. shotgun_sh-0.2.0/src/shotgun/agents/history/token_counting/utils.py +147 -0
  18. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/token_estimation.py +12 -12
  19. shotgun_sh-0.2.0/src/shotgun/agents/llm.py +62 -0
  20. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/models.py +2 -2
  21. shotgun_sh-0.2.0/src/shotgun/agents/tools/web_search/__init__.py +87 -0
  22. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/web_search/anthropic.py +54 -50
  23. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/web_search/gemini.py +31 -20
  24. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/web_search/openai.py +4 -4
  25. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/build_constants.py +2 -2
  26. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/config.py +28 -57
  27. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/models.py +2 -2
  28. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/models.py +4 -4
  29. shotgun_sh-0.2.0/src/shotgun/llm_proxy/__init__.py +16 -0
  30. shotgun_sh-0.2.0/src/shotgun/llm_proxy/clients.py +39 -0
  31. shotgun_sh-0.2.0/src/shotgun/llm_proxy/constants.py +8 -0
  32. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/main.py +6 -0
  33. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/posthog_telemetry.py +5 -3
  34. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/app.py +7 -3
  35. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/chat.py +2 -8
  36. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/chat_screen/command_providers.py +118 -11
  37. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/chat_screen/history.py +3 -1
  38. shotgun_sh-0.2.0/src/shotgun/tui/screens/model_picker.py +327 -0
  39. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/provider_config.py +57 -26
  40. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/utils/env_utils.py +12 -0
  41. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/constants.py +0 -17
  42. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/models.py +0 -185
  43. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/provider.py +0 -206
  44. shotgun_sh-0.1.16.dev2/src/shotgun/agents/history/token_counting.py +0 -429
  45. shotgun_sh-0.1.16.dev2/src/shotgun/agents/tools/web_search/__init__.py +0 -60
  46. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/LICENSE +0 -0
  47. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/README.md +0 -0
  48. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/hatch_build.py +0 -0
  49. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/__init__.py +0 -0
  50. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/__init__.py +0 -0
  51. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/agent_manager.py +0 -0
  52. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/config/__init__.py +0 -0
  53. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/conversation_history.py +0 -0
  54. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/conversation_manager.py +0 -0
  55. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/export.py +0 -0
  56. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/__init__.py +0 -0
  57. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/constants.py +0 -0
  58. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/context_extraction.py +0 -0
  59. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/history_building.py +0 -0
  60. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/history/message_utils.py +0 -0
  61. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/messages.py +0 -0
  62. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/plan.py +0 -0
  63. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/research.py +0 -0
  64. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/specify.py +0 -0
  65. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tasks.py +0 -0
  66. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/__init__.py +0 -0
  67. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  68. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  69. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  70. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  71. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/models.py +0 -0
  72. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  73. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  74. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/file_management.py +0 -0
  75. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/user_interaction.py +0 -0
  76. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  77. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/agents/usage_manager.py +0 -0
  78. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/__init__.py +0 -0
  79. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/codebase/__init__.py +0 -0
  80. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/codebase/commands.py +0 -0
  81. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/codebase/models.py +0 -0
  82. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/export.py +0 -0
  83. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/feedback.py +0 -0
  84. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/plan.py +0 -0
  85. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/research.py +0 -0
  86. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/specify.py +0 -0
  87. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/tasks.py +0 -0
  88. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/update.py +0 -0
  89. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/cli/utils.py +0 -0
  90. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/__init__.py +0 -0
  91. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/__init__.py +0 -0
  92. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/change_detector.py +0 -0
  93. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  94. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/cypher_models.py +0 -0
  95. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/ingestor.py +0 -0
  96. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/language_config.py +0 -0
  97. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/manager.py +0 -0
  98. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/nl_query.py +0 -0
  99. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/core/parser_loader.py +0 -0
  100. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/codebase/service.py +0 -0
  101. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/logging_config.py +0 -0
  102. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/__init__.py +0 -0
  103. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/__init__.py +0 -0
  104. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/export.j2 +0 -0
  105. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  106. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  107. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  108. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  109. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/plan.j2 +0 -0
  110. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/research.j2 +0 -0
  111. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/specify.j2 +0 -0
  112. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  113. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  114. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  115. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/__init__.py +0 -0
  116. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  117. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  118. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  119. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  120. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  121. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  122. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/history/__init__.py +0 -0
  123. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  124. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/history/summarization.j2 +0 -0
  125. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/prompts/loader.py +0 -0
  126. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/py.typed +0 -0
  127. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/sdk/__init__.py +0 -0
  128. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/sdk/codebase.py +0 -0
  129. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/sdk/exceptions.py +0 -0
  130. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/sdk/models.py +0 -0
  131. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/sdk/services.py +0 -0
  132. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/sentry_telemetry.py +0 -0
  133. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/telemetry.py +0 -0
  134. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/__init__.py +0 -0
  135. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/commands/__init__.py +0 -0
  136. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/components/prompt_input.py +0 -0
  137. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/components/spinner.py +0 -0
  138. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/components/splash.py +0 -0
  139. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/components/vertical_tail.py +0 -0
  140. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/filtered_codebase_service.py +0 -0
  141. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/chat.tcss +0 -0
  142. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  143. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  144. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/directory_setup.py +0 -0
  145. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/feedback.py +0 -0
  146. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/screens/splash.py +0 -0
  147. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/styles.tcss +0 -0
  148. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/utils/__init__.py +0 -0
  149. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/tui/utils/mode_progress.py +0 -0
  150. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/utils/__init__.py +0 -0
  151. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/utils/file_system_utils.py +0 -0
  152. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/utils/source_detection.py +0 -0
  153. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.0}/src/shotgun/utils/update_checker.py +0 -0
@@ -15,6 +15,9 @@ src/shotgun/build_constants.py
15
15
 
16
16
  .DS_Store
17
17
 
18
+ # Tokenizer model files (downloaded on first use)
19
+ *.model
20
+
18
21
  # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
19
22
  # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode
20
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.16.dev2
3
+ Version: 0.2.0
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -23,7 +23,6 @@ Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.11
24
24
  Requires-Dist: anthropic>=0.39.0
25
25
  Requires-Dist: genai-prices>=0.0.27
26
- Requires-Dist: google-generativeai>=0.8.5
27
26
  Requires-Dist: httpx>=0.27.0
28
27
  Requires-Dist: jinja2>=3.1.0
29
28
  Requires-Dist: kuzu>=0.7.0
@@ -33,6 +32,7 @@ Requires-Dist: packaging>=23.0
33
32
  Requires-Dist: posthog>=3.0.0
34
33
  Requires-Dist: pydantic-ai>=0.0.14
35
34
  Requires-Dist: rich>=13.0.0
35
+ Requires-Dist: sentencepiece>=0.2.0
36
36
  Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
37
37
  Requires-Dist: textual-dev>=1.7.0
38
38
  Requires-Dist: textual>=6.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.16.dev2"
3
+ version = "0.2.0"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -42,8 +42,8 @@ dependencies = [
42
42
  "watchdog>=4.0.0",
43
43
  "openai>=1.0.0",
44
44
  "anthropic>=0.39.0",
45
- "google-generativeai>=0.8.5",
46
45
  "tiktoken>=0.7.0",
46
+ "sentencepiece>=0.2.0",
47
47
  "packaging>=23.0",
48
48
  "genai-prices>=0.0.27",
49
49
  ]
@@ -18,7 +18,7 @@ from pydantic_ai.messages import (
18
18
  ModelRequest,
19
19
  )
20
20
 
21
- from shotgun.agents.config import ProviderType, get_config_manager, get_provider_model
21
+ from shotgun.agents.config import ProviderType, get_provider_model
22
22
  from shotgun.agents.models import AgentType
23
23
  from shotgun.logging_config import get_logger
24
24
  from shotgun.prompts import PromptLoader
@@ -115,14 +115,13 @@ def create_base_agent(
115
115
  """
116
116
  ensure_shotgun_directory_exists()
117
117
 
118
- # Get configured model or fall back to hardcoded default
118
+ # Get configured model or fall back to first available provider
119
119
  try:
120
120
  model_config = get_provider_model(provider)
121
- config_manager = get_config_manager()
122
- provider_name = provider or config_manager.load().default_provider
121
+ provider_name = model_config.provider
123
122
  logger.debug(
124
123
  "🤖 Creating agent with configured %s model: %s",
125
- provider_name.upper(),
124
+ provider_name.value.upper(),
126
125
  model_config.name,
127
126
  )
128
127
  # Use the Model instance directly (has API key baked in)
@@ -0,0 +1,33 @@
1
+ """Configuration constants for Shotgun agents."""
2
+
3
+ from enum import StrEnum, auto
4
+
5
+ # Field names
6
+ API_KEY_FIELD = "api_key"
7
+ USER_ID_FIELD = "user_id"
8
+ CONFIG_VERSION_FIELD = "config_version"
9
+
10
+
11
+ class ConfigSection(StrEnum):
12
+ """Configuration file section names (JSON keys)."""
13
+
14
+ OPENAI = auto()
15
+ ANTHROPIC = auto()
16
+ GOOGLE = auto()
17
+ SHOTGUN = auto()
18
+
19
+
20
+ # Backwards compatibility - deprecated
21
+ OPENAI_PROVIDER = ConfigSection.OPENAI.value
22
+ ANTHROPIC_PROVIDER = ConfigSection.ANTHROPIC.value
23
+ GOOGLE_PROVIDER = ConfigSection.GOOGLE.value
24
+ SHOTGUN_PROVIDER = ConfigSection.SHOTGUN.value
25
+
26
+ # Environment variable names
27
+ OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
28
+ ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
29
+ GEMINI_API_KEY_ENV = "GEMINI_API_KEY"
30
+ SHOTGUN_API_KEY_ENV = "SHOTGUN_API_KEY"
31
+
32
+ # Token limits
33
+ MEDIUM_TEXT_8K_TOKENS = 8192 # Default max_tokens for web search requests
@@ -1,7 +1,6 @@
1
1
  """Configuration manager for Shotgun CLI."""
2
2
 
3
3
  import json
4
- import os
5
4
  import uuid
6
5
  from pathlib import Path
7
6
  from typing import Any
@@ -12,18 +11,24 @@ from shotgun.logging_config import get_logger
12
11
  from shotgun.utils import get_shotgun_home
13
12
 
14
13
  from .constants import (
15
- ANTHROPIC_API_KEY_ENV,
16
- ANTHROPIC_PROVIDER,
17
14
  API_KEY_FIELD,
18
- GEMINI_API_KEY_ENV,
19
- GOOGLE_PROVIDER,
20
- OPENAI_API_KEY_ENV,
21
- OPENAI_PROVIDER,
15
+ ConfigSection,
16
+ )
17
+ from .models import (
18
+ AnthropicConfig,
19
+ GoogleConfig,
20
+ ModelName,
21
+ OpenAIConfig,
22
+ ProviderType,
23
+ ShotgunAccountConfig,
24
+ ShotgunConfig,
22
25
  )
23
- from .models import ProviderType, ShotgunConfig
24
26
 
25
27
  logger = get_logger(__name__)
26
28
 
29
+ # Type alias for provider configuration objects
30
+ ProviderConfig = OpenAIConfig | AnthropicConfig | GoogleConfig | ShotgunAccountConfig
31
+
27
32
 
28
33
  class ConfigManager:
29
34
  """Manager for Shotgun configuration."""
@@ -41,13 +46,16 @@ class ConfigManager:
41
46
 
42
47
  self._config: ShotgunConfig | None = None
43
48
 
44
- def load(self) -> ShotgunConfig:
49
+ def load(self, force_reload: bool = True) -> ShotgunConfig:
45
50
  """Load configuration from file.
46
51
 
52
+ Args:
53
+ force_reload: If True, reload from disk even if cached (default: True)
54
+
47
55
  Returns:
48
56
  ShotgunConfig: Loaded configuration or default config if file doesn't exist
49
57
  """
50
- if self._config is not None:
58
+ if self._config is not None and not force_reload:
51
59
  return self._config
52
60
 
53
61
  if not self.config_path.exists():
@@ -69,20 +77,56 @@ class ConfigManager:
69
77
  self._config = ShotgunConfig.model_validate(data)
70
78
  logger.debug("Configuration loaded successfully from %s", self.config_path)
71
79
 
72
- # Check if the default provider has a key, if not find one that does
73
- if not self.has_provider_key(self._config.default_provider):
74
- original_default = self._config.default_provider
75
- # Find first provider with a configured key
76
- for provider in ProviderType:
77
- if self.has_provider_key(provider):
80
+ # Validate selected_model if in BYOK mode (no Shotgun key)
81
+ if not self._provider_has_api_key(self._config.shotgun):
82
+ should_save = False
83
+
84
+ # If selected_model is set, verify its provider has a key
85
+ if self._config.selected_model:
86
+ from .models import MODEL_SPECS
87
+
88
+ if self._config.selected_model in MODEL_SPECS:
89
+ spec = MODEL_SPECS[self._config.selected_model]
90
+ if not self.has_provider_key(spec.provider):
91
+ logger.info(
92
+ "Selected model %s provider has no API key, finding available model",
93
+ self._config.selected_model.value,
94
+ )
95
+ self._config.selected_model = None
96
+ should_save = True
97
+ else:
78
98
  logger.info(
79
- "Default provider %s has no API key, updating to %s",
80
- original_default.value,
81
- provider.value,
99
+ "Selected model %s not found in MODEL_SPECS, resetting",
100
+ self._config.selected_model.value,
82
101
  )
83
- self._config.default_provider = provider
84
- self.save(self._config)
85
- break
102
+ self._config.selected_model = None
103
+ should_save = True
104
+
105
+ # If no selected_model or it was invalid, find first available model
106
+ if not self._config.selected_model:
107
+ for provider in ProviderType:
108
+ if self.has_provider_key(provider):
109
+ # Set to that provider's default model
110
+ from .models import MODEL_SPECS, ModelName
111
+
112
+ # Find default model for this provider
113
+ provider_models = {
114
+ ProviderType.OPENAI: ModelName.GPT_5,
115
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
116
+ ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
117
+ }
118
+
119
+ if provider in provider_models:
120
+ self._config.selected_model = provider_models[provider]
121
+ logger.info(
122
+ "Set selected_model to %s (first available provider)",
123
+ self._config.selected_model.value,
124
+ )
125
+ should_save = True
126
+ break
127
+
128
+ if should_save:
129
+ self.save(self._config)
86
130
 
87
131
  return self._config
88
132
 
@@ -107,7 +151,6 @@ class ConfigManager:
107
151
  # Create a new config with generated user_id
108
152
  config = ShotgunConfig(
109
153
  user_id=str(uuid.uuid4()),
110
- config_version=1,
111
154
  )
112
155
 
113
156
  # Ensure directory exists
@@ -136,8 +179,13 @@ class ConfigManager:
136
179
  **kwargs: Configuration fields to update (only api_key supported)
137
180
  """
138
181
  config = self.load()
139
- provider_enum = self._ensure_provider_enum(provider)
140
- provider_config = self._get_provider_config(config, provider_enum)
182
+
183
+ # Get provider config and check if it's shotgun
184
+ provider_config, is_shotgun = self._get_provider_config_and_type(
185
+ config, provider
186
+ )
187
+ # For non-shotgun providers, we need the enum for default provider logic
188
+ provider_enum = None if is_shotgun else self._ensure_provider_enum(provider)
141
189
 
142
190
  # Only support api_key updates
143
191
  if API_KEY_FIELD in kwargs:
@@ -152,50 +200,65 @@ class ConfigManager:
152
200
  raise ValueError(f"Unsupported configuration fields: {unsupported_fields}")
153
201
 
154
202
  # If no other providers have keys configured and we just added one,
155
- # set this provider as the default
156
- if API_KEY_FIELD in kwargs and api_key_value is not None:
203
+ # set selected_model to that provider's default model (only for LLM providers, not shotgun)
204
+ if not is_shotgun and API_KEY_FIELD in kwargs and api_key_value is not None:
205
+ # provider_enum is guaranteed to be non-None here since is_shotgun is False
206
+ if provider_enum is None:
207
+ raise RuntimeError("Provider enum should not be None for LLM providers")
157
208
  other_providers = [p for p in ProviderType if p != provider_enum]
158
209
  has_other_keys = any(self.has_provider_key(p) for p in other_providers)
159
210
  if not has_other_keys:
160
- config.default_provider = provider_enum
211
+ # Set selected_model to this provider's default model
212
+ from .models import ModelName
213
+
214
+ provider_models = {
215
+ ProviderType.OPENAI: ModelName.GPT_5,
216
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
217
+ ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
218
+ }
219
+ if provider_enum in provider_models:
220
+ config.selected_model = provider_models[provider_enum]
161
221
 
162
222
  self.save(config)
163
223
 
164
224
  def clear_provider_key(self, provider: ProviderType | str) -> None:
165
- """Remove the API key for the given provider."""
225
+ """Remove the API key for the given provider (LLM provider or shotgun)."""
166
226
  config = self.load()
167
- provider_enum = self._ensure_provider_enum(provider)
168
- provider_config = self._get_provider_config(config, provider_enum)
227
+
228
+ # Get provider config (shotgun or LLM provider)
229
+ provider_config, _ = self._get_provider_config_and_type(config, provider)
230
+
169
231
  provider_config.api_key = None
170
232
  self.save(config)
171
233
 
234
+ def update_selected_model(self, model_name: "ModelName") -> None:
235
+ """Update the selected model.
236
+
237
+ Args:
238
+ model_name: Model to select
239
+ """
240
+ config = self.load()
241
+ config.selected_model = model_name
242
+ self.save(config)
243
+
172
244
  def has_provider_key(self, provider: ProviderType | str) -> bool:
173
245
  """Check if the given provider has a non-empty API key configured.
174
246
 
175
- This checks both the configuration file and environment variables.
247
+ This checks only the configuration file.
176
248
  """
177
- config = self.load()
249
+ # Use force_reload=False to avoid infinite loop when called from load()
250
+ config = self.load(force_reload=False)
178
251
  provider_enum = self._ensure_provider_enum(provider)
179
252
  provider_config = self._get_provider_config(config, provider_enum)
180
253
 
181
- # Check config first
182
- if self._provider_has_api_key(provider_config):
183
- return True
184
-
185
- # Check environment variable
186
- if provider_enum == ProviderType.OPENAI:
187
- return bool(os.getenv(OPENAI_API_KEY_ENV))
188
- elif provider_enum == ProviderType.ANTHROPIC:
189
- return bool(os.getenv(ANTHROPIC_API_KEY_ENV))
190
- elif provider_enum == ProviderType.GOOGLE:
191
- return bool(os.getenv(GEMINI_API_KEY_ENV))
192
-
193
- return False
254
+ return self._provider_has_api_key(provider_config)
194
255
 
195
256
  def has_any_provider_key(self) -> bool:
196
257
  """Determine whether any provider has a configured API key."""
197
- config = self.load()
198
- return any(
258
+ # Use force_reload=False to avoid infinite loop when called from load()
259
+ config = self.load(force_reload=False)
260
+ # Check LLM provider keys (BYOK)
261
+ has_llm_key = any(
199
262
  self._provider_has_api_key(self._get_provider_config(config, provider))
200
263
  for provider in (
201
264
  ProviderType.OPENAI,
@@ -203,6 +266,9 @@ class ConfigManager:
203
266
  ProviderType.GOOGLE,
204
267
  )
205
268
  )
269
+ # Also check Shotgun Account key
270
+ has_shotgun_key = self._provider_has_api_key(config.shotgun)
271
+ return has_llm_key or has_shotgun_key
206
272
 
207
273
  def initialize(self) -> ShotgunConfig:
208
274
  """Initialize configuration with defaults and save to file.
@@ -213,7 +279,6 @@ class ConfigManager:
213
279
  # Generate unique user ID for new config
214
280
  config = ShotgunConfig(
215
281
  user_id=str(uuid.uuid4()),
216
- config_version=1,
217
282
  )
218
283
  self.save(config)
219
284
  logger.info(
@@ -225,26 +290,26 @@ class ConfigManager:
225
290
 
226
291
  def _convert_secrets_to_secretstr(self, data: dict[str, Any]) -> None:
227
292
  """Convert plain text secrets in data to SecretStr objects."""
228
- for provider in [OPENAI_PROVIDER, ANTHROPIC_PROVIDER, GOOGLE_PROVIDER]:
229
- if provider in data and isinstance(data[provider], dict):
293
+ for section in ConfigSection:
294
+ if section.value in data and isinstance(data[section.value], dict):
230
295
  if (
231
- API_KEY_FIELD in data[provider]
232
- and data[provider][API_KEY_FIELD] is not None
296
+ API_KEY_FIELD in data[section.value]
297
+ and data[section.value][API_KEY_FIELD] is not None
233
298
  ):
234
- data[provider][API_KEY_FIELD] = SecretStr(
235
- data[provider][API_KEY_FIELD]
299
+ data[section.value][API_KEY_FIELD] = SecretStr(
300
+ data[section.value][API_KEY_FIELD]
236
301
  )
237
302
 
238
303
  def _convert_secretstr_to_plain(self, data: dict[str, Any]) -> None:
239
304
  """Convert SecretStr objects in data to plain text for JSON serialization."""
240
- for provider in [OPENAI_PROVIDER, ANTHROPIC_PROVIDER, GOOGLE_PROVIDER]:
241
- if provider in data and isinstance(data[provider], dict):
305
+ for section in ConfigSection:
306
+ if section.value in data and isinstance(data[section.value], dict):
242
307
  if (
243
- API_KEY_FIELD in data[provider]
244
- and data[provider][API_KEY_FIELD] is not None
308
+ API_KEY_FIELD in data[section.value]
309
+ and data[section.value][API_KEY_FIELD] is not None
245
310
  ):
246
- if hasattr(data[provider][API_KEY_FIELD], "get_secret_value"):
247
- data[provider][API_KEY_FIELD] = data[provider][
311
+ if hasattr(data[section.value][API_KEY_FIELD], "get_secret_value"):
312
+ data[section.value][API_KEY_FIELD] = data[section.value][
248
313
  API_KEY_FIELD
249
314
  ].get_secret_value()
250
315
 
@@ -279,6 +344,38 @@ class ConfigManager:
279
344
 
280
345
  return bool(value.strip())
281
346
 
347
+ def _is_shotgun_provider(self, provider: ProviderType | str) -> bool:
348
+ """Check if provider string represents Shotgun Account.
349
+
350
+ Args:
351
+ provider: Provider type or string
352
+
353
+ Returns:
354
+ True if provider is shotgun account
355
+ """
356
+ return (
357
+ isinstance(provider, str)
358
+ and provider.lower() == ConfigSection.SHOTGUN.value
359
+ )
360
+
361
+ def _get_provider_config_and_type(
362
+ self, config: ShotgunConfig, provider: ProviderType | str
363
+ ) -> tuple[ProviderConfig, bool]:
364
+ """Get provider config, handling shotgun as special case.
365
+
366
+ Args:
367
+ config: Shotgun configuration
368
+ provider: Provider type or string
369
+
370
+ Returns:
371
+ Tuple of (provider_config, is_shotgun)
372
+ """
373
+ if self._is_shotgun_provider(provider):
374
+ return (config.shotgun, True)
375
+
376
+ provider_enum = self._ensure_provider_enum(provider)
377
+ return (self._get_provider_config(config, provider_enum), False)
378
+
282
379
  def get_user_id(self) -> str:
283
380
  """Get the user ID from configuration.
284
381
 
@@ -289,6 +386,17 @@ class ConfigManager:
289
386
  return config.user_id
290
387
 
291
388
 
389
+ # Global singleton instance
390
+ _config_manager_instance: ConfigManager | None = None
391
+
392
+
292
393
  def get_config_manager() -> ConfigManager:
293
- """Get the global ConfigManager instance."""
294
- return ConfigManager()
394
+ """Get the global singleton ConfigManager instance.
395
+
396
+ Returns:
397
+ The singleton ConfigManager instance
398
+ """
399
+ global _config_manager_instance
400
+ if _config_manager_instance is None:
401
+ _config_manager_instance = ConfigManager()
402
+ return _config_manager_instance
@@ -0,0 +1,166 @@
1
+ """Pydantic models for configuration."""
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, Field, PrivateAttr, SecretStr
6
+ from pydantic_ai.models import Model
7
+
8
+
9
+ class ProviderType(StrEnum):
10
+ """Provider types for AI services."""
11
+
12
+ OPENAI = "openai"
13
+ ANTHROPIC = "anthropic"
14
+ GOOGLE = "google"
15
+
16
+
17
+ class KeyProvider(StrEnum):
18
+ """Authentication method for accessing AI models."""
19
+
20
+ BYOK = "byok" # Bring Your Own Key (individual provider keys)
21
+ SHOTGUN = "shotgun" # Shotgun Account (unified LiteLLM proxy)
22
+
23
+
24
+ class ModelName(StrEnum):
25
+ """Available AI model names."""
26
+
27
+ GPT_5 = "gpt-5"
28
+ GPT_5_MINI = "gpt-5-mini"
29
+ CLAUDE_OPUS_4_1 = "claude-opus-4-1"
30
+ CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
31
+ GEMINI_2_5_PRO = "gemini-2.5-pro"
32
+ GEMINI_2_5_FLASH = "gemini-2.5-flash"
33
+
34
+
35
+ class ModelSpec(BaseModel):
36
+ """Static specification for a model - just metadata."""
37
+
38
+ name: ModelName # Model identifier
39
+ provider: ProviderType
40
+ max_input_tokens: int
41
+ max_output_tokens: int
42
+ litellm_proxy_model_name: (
43
+ str # LiteLLM format (e.g., "openai/gpt-5", "gemini/gemini-2-pro")
44
+ )
45
+
46
+
47
+ class ModelConfig(BaseModel):
48
+ """A fully configured model with API key and settings."""
49
+
50
+ name: ModelName # Model identifier
51
+ provider: ProviderType # Actual LLM provider (openai, anthropic, google)
52
+ key_provider: KeyProvider # Authentication method (byok or shotgun)
53
+ max_input_tokens: int
54
+ max_output_tokens: int
55
+ api_key: str
56
+ _model_instance: Model | None = PrivateAttr(default=None)
57
+
58
+ class Config:
59
+ arbitrary_types_allowed = True
60
+
61
+ @property
62
+ def model_instance(self) -> Model:
63
+ """Lazy load the Model instance."""
64
+ if self._model_instance is None:
65
+ from .provider import get_or_create_model
66
+
67
+ self._model_instance = get_or_create_model(
68
+ self.provider, self.key_provider, self.name, self.api_key
69
+ )
70
+ return self._model_instance
71
+
72
+ @property
73
+ def pydantic_model_name(self) -> str:
74
+ """Compute the full Pydantic AI model identifier. For backward compatibility."""
75
+ provider_prefix = {
76
+ ProviderType.OPENAI: "openai",
77
+ ProviderType.ANTHROPIC: "anthropic",
78
+ ProviderType.GOOGLE: "google-gla",
79
+ }
80
+ return f"{provider_prefix[self.provider]}:{self.name}"
81
+
82
+
83
+ # Model specifications registry (static metadata)
84
+ MODEL_SPECS: dict[ModelName, ModelSpec] = {
85
+ ModelName.GPT_5: ModelSpec(
86
+ name=ModelName.GPT_5,
87
+ provider=ProviderType.OPENAI,
88
+ max_input_tokens=400_000,
89
+ max_output_tokens=128_000,
90
+ litellm_proxy_model_name="openai/gpt-5",
91
+ ),
92
+ ModelName.GPT_5_MINI: ModelSpec(
93
+ name=ModelName.GPT_5_MINI,
94
+ provider=ProviderType.OPENAI,
95
+ max_input_tokens=400_000,
96
+ max_output_tokens=128_000,
97
+ litellm_proxy_model_name="openai/gpt-5-mini",
98
+ ),
99
+ ModelName.CLAUDE_OPUS_4_1: ModelSpec(
100
+ name=ModelName.CLAUDE_OPUS_4_1,
101
+ provider=ProviderType.ANTHROPIC,
102
+ max_input_tokens=200_000,
103
+ max_output_tokens=32_000,
104
+ litellm_proxy_model_name="anthropic/claude-opus-4-1",
105
+ ),
106
+ ModelName.CLAUDE_SONNET_4_5: ModelSpec(
107
+ name=ModelName.CLAUDE_SONNET_4_5,
108
+ provider=ProviderType.ANTHROPIC,
109
+ max_input_tokens=200_000,
110
+ max_output_tokens=16_000,
111
+ litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
112
+ ),
113
+ ModelName.GEMINI_2_5_PRO: ModelSpec(
114
+ name=ModelName.GEMINI_2_5_PRO,
115
+ provider=ProviderType.GOOGLE,
116
+ max_input_tokens=1_000_000,
117
+ max_output_tokens=64_000,
118
+ litellm_proxy_model_name="gemini/gemini-2.5-pro",
119
+ ),
120
+ ModelName.GEMINI_2_5_FLASH: ModelSpec(
121
+ name=ModelName.GEMINI_2_5_FLASH,
122
+ provider=ProviderType.GOOGLE,
123
+ max_input_tokens=1_000_000,
124
+ max_output_tokens=64_000,
125
+ litellm_proxy_model_name="gemini/gemini-2.5-flash",
126
+ ),
127
+ }
128
+
129
+
130
+ class OpenAIConfig(BaseModel):
131
+ """Configuration for OpenAI provider."""
132
+
133
+ api_key: SecretStr | None = None
134
+
135
+
136
+ class AnthropicConfig(BaseModel):
137
+ """Configuration for Anthropic provider."""
138
+
139
+ api_key: SecretStr | None = None
140
+
141
+
142
+ class GoogleConfig(BaseModel):
143
+ """Configuration for Google provider."""
144
+
145
+ api_key: SecretStr | None = None
146
+
147
+
148
+ class ShotgunAccountConfig(BaseModel):
149
+ """Configuration for Shotgun Account (LiteLLM proxy)."""
150
+
151
+ api_key: SecretStr | None = None
152
+
153
+
154
+ class ShotgunConfig(BaseModel):
155
+ """Main configuration for Shotgun CLI."""
156
+
157
+ openai: OpenAIConfig = Field(default_factory=OpenAIConfig)
158
+ anthropic: AnthropicConfig = Field(default_factory=AnthropicConfig)
159
+ google: GoogleConfig = Field(default_factory=GoogleConfig)
160
+ shotgun: ShotgunAccountConfig = Field(default_factory=ShotgunAccountConfig)
161
+ selected_model: ModelName | None = Field(
162
+ default=None,
163
+ description="User-selected model",
164
+ )
165
+ user_id: str = Field(description="Unique anonymous user identifier")
166
+ config_version: int = Field(default=2, description="Configuration schema version")