shotgun-sh 0.2.3.dev2__py3-none-any.whl → 0.2.11.dev1__py3-none-any.whl

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 (107) hide show
  1. shotgun/agents/agent_manager.py +524 -58
  2. shotgun/agents/common.py +62 -62
  3. shotgun/agents/config/constants.py +0 -6
  4. shotgun/agents/config/manager.py +14 -3
  5. shotgun/agents/config/models.py +16 -0
  6. shotgun/agents/config/provider.py +68 -13
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +493 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +125 -2
  13. shotgun/agents/conversation_manager.py +24 -2
  14. shotgun/agents/export.py +4 -5
  15. shotgun/agents/history/compaction.py +9 -4
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +14 -2
  18. shotgun/agents/history/token_counting/anthropic.py +32 -10
  19. shotgun/agents/models.py +50 -2
  20. shotgun/agents/plan.py +4 -5
  21. shotgun/agents/research.py +4 -5
  22. shotgun/agents/specify.py +4 -5
  23. shotgun/agents/tasks.py +4 -5
  24. shotgun/agents/tools/__init__.py +0 -2
  25. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  27. shotgun/agents/tools/codebase/file_read.py +6 -0
  28. shotgun/agents/tools/codebase/query_graph.py +6 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  30. shotgun/agents/tools/file_management.py +71 -9
  31. shotgun/agents/tools/registry.py +217 -0
  32. shotgun/agents/tools/web_search/__init__.py +24 -12
  33. shotgun/agents/tools/web_search/anthropic.py +24 -3
  34. shotgun/agents/tools/web_search/gemini.py +22 -10
  35. shotgun/agents/tools/web_search/openai.py +21 -12
  36. shotgun/api_endpoints.py +7 -3
  37. shotgun/build_constants.py +1 -1
  38. shotgun/cli/clear.py +52 -0
  39. shotgun/cli/compact.py +186 -0
  40. shotgun/cli/context.py +111 -0
  41. shotgun/cli/models.py +1 -0
  42. shotgun/cli/update.py +16 -2
  43. shotgun/codebase/core/manager.py +10 -1
  44. shotgun/llm_proxy/__init__.py +5 -2
  45. shotgun/llm_proxy/clients.py +12 -7
  46. shotgun/logging_config.py +8 -10
  47. shotgun/main.py +70 -10
  48. shotgun/posthog_telemetry.py +9 -3
  49. shotgun/prompts/agents/export.j2 +18 -1
  50. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  51. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  52. shotgun/prompts/agents/plan.j2 +1 -1
  53. shotgun/prompts/agents/research.j2 +1 -1
  54. shotgun/prompts/agents/specify.j2 +270 -3
  55. shotgun/prompts/agents/state/system_state.j2 +4 -0
  56. shotgun/prompts/agents/tasks.j2 +1 -1
  57. shotgun/prompts/loader.py +2 -2
  58. shotgun/prompts/tools/web_search.j2 +14 -0
  59. shotgun/sentry_telemetry.py +4 -15
  60. shotgun/settings.py +238 -0
  61. shotgun/telemetry.py +15 -32
  62. shotgun/tui/app.py +203 -9
  63. shotgun/tui/commands/__init__.py +1 -1
  64. shotgun/tui/components/context_indicator.py +136 -0
  65. shotgun/tui/components/mode_indicator.py +70 -0
  66. shotgun/tui/components/status_bar.py +48 -0
  67. shotgun/tui/containers.py +93 -0
  68. shotgun/tui/dependencies.py +39 -0
  69. shotgun/tui/protocols.py +45 -0
  70. shotgun/tui/screens/chat/__init__.py +5 -0
  71. shotgun/tui/screens/chat/chat.tcss +54 -0
  72. shotgun/tui/screens/chat/chat_screen.py +1110 -0
  73. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  74. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  75. shotgun/tui/screens/chat/help_text.py +39 -0
  76. shotgun/tui/screens/chat/prompt_history.py +48 -0
  77. shotgun/tui/screens/chat.tcss +11 -0
  78. shotgun/tui/screens/chat_screen/command_providers.py +68 -2
  79. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  80. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  81. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  82. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  83. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  84. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  85. shotgun/tui/screens/confirmation_dialog.py +151 -0
  86. shotgun/tui/screens/model_picker.py +30 -6
  87. shotgun/tui/screens/pipx_migration.py +153 -0
  88. shotgun/tui/screens/welcome.py +24 -5
  89. shotgun/tui/services/__init__.py +5 -0
  90. shotgun/tui/services/conversation_service.py +182 -0
  91. shotgun/tui/state/__init__.py +7 -0
  92. shotgun/tui/state/processing_state.py +185 -0
  93. shotgun/tui/widgets/__init__.py +5 -0
  94. shotgun/tui/widgets/widget_coordinator.py +247 -0
  95. shotgun/utils/datetime_utils.py +77 -0
  96. shotgun/utils/file_system_utils.py +3 -2
  97. shotgun/utils/update_checker.py +69 -14
  98. shotgun_sh-0.2.11.dev1.dist-info/METADATA +129 -0
  99. shotgun_sh-0.2.11.dev1.dist-info/RECORD +190 -0
  100. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/entry_points.txt +1 -0
  101. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/licenses/LICENSE +1 -1
  102. shotgun/agents/tools/user_interaction.py +0 -37
  103. shotgun/tui/screens/chat.py +0 -804
  104. shotgun/tui/screens/chat_screen/history.py +0 -352
  105. shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
  106. shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
  107. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/WHEEL +0 -0
shotgun/agents/common.py CHANGED
@@ -1,14 +1,11 @@
1
1
  """Common utilities for agent creation and management."""
2
2
 
3
- import asyncio
4
3
  from collections.abc import Callable
5
4
  from pathlib import Path
6
5
  from typing import Any
7
6
 
8
7
  from pydantic_ai import (
9
8
  Agent,
10
- DeferredToolRequests,
11
- DeferredToolResults,
12
9
  RunContext,
13
10
  UsageLimits,
14
11
  )
@@ -19,20 +16,19 @@ from pydantic_ai.messages import (
19
16
  )
20
17
 
21
18
  from shotgun.agents.config import ProviderType, get_provider_model
22
- from shotgun.agents.models import AgentType
19
+ from shotgun.agents.models import AgentResponse, AgentType
23
20
  from shotgun.logging_config import get_logger
24
21
  from shotgun.prompts import PromptLoader
25
22
  from shotgun.sdk.services import get_codebase_service
26
23
  from shotgun.utils import ensure_shotgun_directory_exists
24
+ from shotgun.utils.datetime_utils import get_datetime_context
27
25
  from shotgun.utils.file_system_utils import get_shotgun_base_path
28
26
 
29
27
  from .history import token_limit_compactor
30
- from .history.compaction import apply_persistent_compaction
31
28
  from .messages import AgentSystemPrompt, SystemStatusPrompt
32
29
  from .models import AgentDeps, AgentRuntimeOptions, PipelineConfigEntry
33
30
  from .tools import (
34
31
  append_file,
35
- ask_user,
36
32
  codebase_shell,
37
33
  directory_lister,
38
34
  file_read,
@@ -74,12 +70,18 @@ async def add_system_status_message(
74
70
  # Extract table of contents from the agent's markdown file
75
71
  markdown_toc = extract_markdown_toc(deps.agent_mode)
76
72
 
73
+ # Get current datetime with timezone information
74
+ dt_context = get_datetime_context()
75
+
77
76
  system_state = prompt_loader.render(
78
77
  "agents/state/system_state.j2",
79
78
  codebase_understanding_graphs=codebase_understanding_graphs,
80
79
  is_tui_context=deps.is_tui_context,
81
80
  existing_files=existing_files,
82
81
  markdown_toc=markdown_toc,
82
+ current_datetime=dt_context.datetime_formatted,
83
+ timezone_name=dt_context.timezone_name,
84
+ utc_offset=dt_context.utc_offset,
83
85
  )
84
86
 
85
87
  message_history.append(
@@ -99,7 +101,7 @@ def create_base_agent(
99
101
  additional_tools: list[Any] | None = None,
100
102
  provider: ProviderType | None = None,
101
103
  agent_mode: AgentType | None = None,
102
- ) -> tuple[Agent[AgentDeps, str | DeferredToolRequests], AgentDeps]:
104
+ ) -> tuple[Agent[AgentDeps, AgentResponse], AgentDeps]:
103
105
  """Create a base agent with common configuration.
104
106
 
105
107
  Args:
@@ -157,7 +159,7 @@ def create_base_agent(
157
159
 
158
160
  agent = Agent(
159
161
  model,
160
- output_type=[str, DeferredToolRequests],
162
+ output_type=AgentResponse,
161
163
  deps_type=AgentDeps,
162
164
  instrument=True,
163
165
  history_processors=[history_processor],
@@ -172,11 +174,6 @@ def create_base_agent(
172
174
  for tool in additional_tools or []:
173
175
  agent.tool_plain(tool)
174
176
 
175
- # Register interactive tool conditionally based on deps
176
- if deps.interactive_mode:
177
- agent.tool(ask_user)
178
- logger.debug("📞 Interactive mode enabled - ask_user tool registered")
179
-
180
177
  # Register common file management tools (always available)
181
178
  agent.tool(write_file)
182
179
  agent.tool(append_file)
@@ -316,7 +313,9 @@ def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
316
313
  if prior_toc:
317
314
  # Add section with XML tags
318
315
  toc_sections.append(
319
- f'<TABLE_OF_CONTENTS file_name="{prior_file}">\n{prior_toc}\n</TABLE_OF_CONTENTS>'
316
+ f'<TABLE_OF_CONTENTS file_name="{prior_file}">\n'
317
+ f"{prior_toc}\n"
318
+ f"</TABLE_OF_CONTENTS>"
320
319
  )
321
320
 
322
321
  # Extract TOC from own file (full detail)
@@ -327,7 +326,9 @@ def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
327
326
  # Put own file TOC at the beginning with XML tags
328
327
  toc_sections.insert(
329
328
  0,
330
- f'<TABLE_OF_CONTENTS file_name="{config.own_file}">\n{own_toc}\n</TABLE_OF_CONTENTS>',
329
+ f'<TABLE_OF_CONTENTS file_name="{config.own_file}">\n'
330
+ f"{own_toc}\n"
331
+ f"</TABLE_OF_CONTENTS>",
331
332
  )
332
333
 
333
334
  # Combine all sections
@@ -383,23 +384,48 @@ def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
383
384
  relative_path = file_path.relative_to(base_path)
384
385
  existing_files.append(str(relative_path))
385
386
  else:
386
- # For other agents, check both .md file and directory with same name
387
- allowed_file = AGENT_DIRECTORIES[agent_mode]
388
-
389
- # Check for the .md file
390
- md_file_path = base_path / allowed_file
391
- if md_file_path.exists():
392
- existing_files.append(allowed_file)
393
-
394
- # Check for directory with same base name (e.g., research/ for research.md)
395
- base_name = allowed_file.replace(".md", "")
396
- dir_path = base_path / base_name
397
- if dir_path.exists() and dir_path.is_dir():
398
- # List all files in the directory
399
- for file_path in dir_path.rglob("*"):
400
- if file_path.is_file():
401
- relative_path = file_path.relative_to(base_path)
402
- existing_files.append(str(relative_path))
387
+ # For other agents, check files/directories they have access to
388
+ allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
389
+
390
+ # Convert single Path/string to list of Paths for uniform handling
391
+ if isinstance(allowed_paths_raw, str):
392
+ # Special case: "*" means export agent (shouldn't reach here but handle it)
393
+ allowed_paths = (
394
+ [Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
395
+ )
396
+ elif isinstance(allowed_paths_raw, Path):
397
+ allowed_paths = [allowed_paths_raw]
398
+ else:
399
+ # Already a list
400
+ allowed_paths = allowed_paths_raw
401
+
402
+ # Check each allowed path
403
+ for allowed_path in allowed_paths:
404
+ allowed_str = str(allowed_path)
405
+
406
+ # Check if it's a directory (no .md suffix)
407
+ if not allowed_path.suffix or not allowed_str.endswith(".md"):
408
+ # It's a directory - list all files within it
409
+ dir_path = base_path / allowed_str
410
+ if dir_path.exists() and dir_path.is_dir():
411
+ for file_path in dir_path.rglob("*"):
412
+ if file_path.is_file():
413
+ relative_path = file_path.relative_to(base_path)
414
+ existing_files.append(str(relative_path))
415
+ else:
416
+ # It's a file - check if it exists
417
+ file_path = base_path / allowed_str
418
+ if file_path.exists():
419
+ existing_files.append(allowed_str)
420
+
421
+ # Also check for associated directory (e.g., research/ for research.md)
422
+ base_name = allowed_str.replace(".md", "")
423
+ dir_path = base_path / base_name
424
+ if dir_path.exists() and dir_path.is_dir():
425
+ for file_path in dir_path.rglob("*"):
426
+ if file_path.is_file():
427
+ relative_path = file_path.relative_to(base_path)
428
+ existing_files.append(str(relative_path))
403
429
 
404
430
  return existing_files
405
431
 
@@ -469,7 +495,8 @@ async def add_system_prompt_message(
469
495
  message_history = message_history or []
470
496
 
471
497
  # Create a minimal RunContext to call the system prompt function
472
- # We'll pass None for model and usage since they're not used by our system prompt functions
498
+ # We'll pass None for model and usage since they're not used
499
+ # by our system prompt functions
473
500
  context = type(
474
501
  "RunContext", (), {"deps": deps, "retry": 0, "model": None, "usage": None}
475
502
  )()
@@ -493,12 +520,12 @@ async def add_system_prompt_message(
493
520
 
494
521
 
495
522
  async def run_agent(
496
- agent: Agent[AgentDeps, str | DeferredToolRequests],
523
+ agent: Agent[AgentDeps, AgentResponse],
497
524
  prompt: str,
498
525
  deps: AgentDeps,
499
526
  message_history: list[ModelMessage] | None = None,
500
527
  usage_limits: UsageLimits | None = None,
501
- ) -> AgentRunResult[str | DeferredToolRequests]:
528
+ ) -> AgentRunResult[AgentResponse]:
502
529
  # Clear file tracker for new run
503
530
  deps.file_tracker.clear()
504
531
  logger.debug("🔧 Cleared file tracker for new agent run")
@@ -513,33 +540,6 @@ async def run_agent(
513
540
  message_history=message_history,
514
541
  )
515
542
 
516
- # Apply persistent compaction to prevent cascading token growth across CLI commands
517
- messages = await apply_persistent_compaction(result.all_messages(), deps)
518
- while isinstance(result.output, DeferredToolRequests):
519
- logger.info("got deferred tool requests")
520
- await deps.queue.join()
521
- requests = result.output
522
- done, _ = await asyncio.wait(deps.tasks)
523
-
524
- task_results = [task.result() for task in done]
525
- task_results_by_tool_call_id = {
526
- result.tool_call_id: result.answer for result in task_results
527
- }
528
- logger.info("got task results", task_results_by_tool_call_id)
529
- results = DeferredToolResults()
530
- for call in requests.calls:
531
- results.calls[call.tool_call_id] = task_results_by_tool_call_id[
532
- call.tool_call_id
533
- ]
534
- result = await agent.run(
535
- deps=deps,
536
- usage_limits=usage_limits,
537
- message_history=messages,
538
- deferred_tool_results=results,
539
- )
540
- # Apply persistent compaction to prevent cascading token growth in multi-turn loops
541
- messages = await apply_persistent_compaction(result.all_messages(), deps)
542
-
543
543
  # Log file operations summary if any files were modified
544
544
  if deps.file_tracker.operations:
545
545
  summary = deps.file_tracker.format_summary()
@@ -24,11 +24,5 @@ ANTHROPIC_PROVIDER = ConfigSection.ANTHROPIC.value
24
24
  GOOGLE_PROVIDER = ConfigSection.GOOGLE.value
25
25
  SHOTGUN_PROVIDER = ConfigSection.SHOTGUN.value
26
26
 
27
- # Environment variable names
28
- OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
29
- ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
30
- GEMINI_API_KEY_ENV = "GEMINI_API_KEY"
31
- SHOTGUN_API_KEY_ENV = "SHOTGUN_API_KEY"
32
-
33
27
  # Token limits
34
28
  MEDIUM_TEXT_8K_TOKENS = 8192 # Default max_tokens for web search requests
@@ -142,7 +142,7 @@ class ConfigManager:
142
142
  # Find default model for this provider
143
143
  provider_models = {
144
144
  ProviderType.OPENAI: ModelName.GPT_5,
145
- ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
145
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_HAIKU_4_5,
146
146
  ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
147
147
  }
148
148
 
@@ -243,12 +243,16 @@ class ConfigManager:
243
243
 
244
244
  provider_models = {
245
245
  ProviderType.OPENAI: ModelName.GPT_5,
246
- ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
246
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_HAIKU_4_5,
247
247
  ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
248
248
  }
249
249
  if provider_enum in provider_models:
250
250
  config.selected_model = provider_models[provider_enum]
251
251
 
252
+ # Mark welcome screen as shown when BYOK provider is configured
253
+ # This prevents the welcome screen from showing again after user has made their choice
254
+ config.shown_welcome_screen = True
255
+
252
256
  self.save(config)
253
257
 
254
258
  def clear_provider_key(self, provider: ProviderType | str) -> None:
@@ -256,9 +260,16 @@ class ConfigManager:
256
260
  config = self.load()
257
261
 
258
262
  # Get provider config (shotgun or LLM provider)
259
- provider_config, _ = self._get_provider_config_and_type(config, provider)
263
+ provider_config, is_shotgun = self._get_provider_config_and_type(
264
+ config, provider
265
+ )
260
266
 
261
267
  provider_config.api_key = None
268
+
269
+ # For Shotgun Account, also clear the JWT
270
+ if is_shotgun and isinstance(provider_config, ShotgunAccountConfig):
271
+ provider_config.supabase_jwt = None
272
+
262
273
  self.save(config)
263
274
 
264
275
  def update_selected_model(self, model_name: "ModelName") -> None:
@@ -28,6 +28,7 @@ class ModelName(StrEnum):
28
28
  GPT_5_MINI = "gpt-5-mini"
29
29
  CLAUDE_OPUS_4_1 = "claude-opus-4-1"
30
30
  CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
31
+ CLAUDE_HAIKU_4_5 = "claude-haiku-4-5"
31
32
  GEMINI_2_5_PRO = "gemini-2.5-pro"
32
33
  GEMINI_2_5_FLASH = "gemini-2.5-flash"
33
34
 
@@ -42,6 +43,7 @@ class ModelSpec(BaseModel):
42
43
  litellm_proxy_model_name: (
43
44
  str # LiteLLM format (e.g., "openai/gpt-5", "gemini/gemini-2-pro")
44
45
  )
46
+ short_name: str # Display name for UI (e.g., "Sonnet 4.5", "GPT-5")
45
47
 
46
48
 
47
49
  class ModelConfig(BaseModel):
@@ -88,6 +90,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
88
90
  max_input_tokens=400_000,
89
91
  max_output_tokens=128_000,
90
92
  litellm_proxy_model_name="openai/gpt-5",
93
+ short_name="GPT-5",
91
94
  ),
92
95
  ModelName.GPT_5_MINI: ModelSpec(
93
96
  name=ModelName.GPT_5_MINI,
@@ -95,6 +98,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
95
98
  max_input_tokens=400_000,
96
99
  max_output_tokens=128_000,
97
100
  litellm_proxy_model_name="openai/gpt-5-mini",
101
+ short_name="GPT-5 Mini",
98
102
  ),
99
103
  ModelName.CLAUDE_OPUS_4_1: ModelSpec(
100
104
  name=ModelName.CLAUDE_OPUS_4_1,
@@ -102,6 +106,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
102
106
  max_input_tokens=200_000,
103
107
  max_output_tokens=32_000,
104
108
  litellm_proxy_model_name="anthropic/claude-opus-4-1",
109
+ short_name="Opus 4.1",
105
110
  ),
106
111
  ModelName.CLAUDE_SONNET_4_5: ModelSpec(
107
112
  name=ModelName.CLAUDE_SONNET_4_5,
@@ -109,6 +114,15 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
109
114
  max_input_tokens=200_000,
110
115
  max_output_tokens=16_000,
111
116
  litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
117
+ short_name="Sonnet 4.5",
118
+ ),
119
+ ModelName.CLAUDE_HAIKU_4_5: ModelSpec(
120
+ name=ModelName.CLAUDE_HAIKU_4_5,
121
+ provider=ProviderType.ANTHROPIC,
122
+ max_input_tokens=200_000,
123
+ max_output_tokens=64_000,
124
+ litellm_proxy_model_name="anthropic/claude-haiku-4-5",
125
+ short_name="Haiku 4.5",
112
126
  ),
113
127
  ModelName.GEMINI_2_5_PRO: ModelSpec(
114
128
  name=ModelName.GEMINI_2_5_PRO,
@@ -116,6 +130,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
116
130
  max_input_tokens=1_000_000,
117
131
  max_output_tokens=64_000,
118
132
  litellm_proxy_model_name="gemini/gemini-2.5-pro",
133
+ short_name="Gemini 2.5 Pro",
119
134
  ),
120
135
  ModelName.GEMINI_2_5_FLASH: ModelSpec(
121
136
  name=ModelName.GEMINI_2_5_FLASH,
@@ -123,6 +138,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
123
138
  max_input_tokens=1_000_000,
124
139
  max_output_tokens=64_000,
125
140
  litellm_proxy_model_name="gemini/gemini-2.5-flash",
141
+ short_name="Gemini 2.5 Flash",
126
142
  ),
127
143
  }
128
144
 
@@ -10,7 +10,10 @@ from pydantic_ai.providers.google import GoogleProvider
10
10
  from pydantic_ai.providers.openai import OpenAIProvider
11
11
  from pydantic_ai.settings import ModelSettings
12
12
 
13
- from shotgun.llm_proxy import create_litellm_provider
13
+ from shotgun.llm_proxy import (
14
+ create_anthropic_proxy_provider,
15
+ create_litellm_provider,
16
+ )
14
17
  from shotgun.logging_config import get_logger
15
18
 
16
19
  from .manager import get_config_manager
@@ -29,6 +32,34 @@ logger = get_logger(__name__)
29
32
  _model_cache: dict[tuple[ProviderType, KeyProvider, ModelName, str], Model] = {}
30
33
 
31
34
 
35
+ def get_default_model_for_provider(config: ShotgunConfig) -> ModelName:
36
+ """Get the default model based on which provider/account is configured.
37
+
38
+ Checks API keys in priority order and returns appropriate default model.
39
+ Treats Shotgun Account as a provider context.
40
+
41
+ Args:
42
+ config: Shotgun configuration containing API keys
43
+
44
+ Returns:
45
+ Default ModelName for the configured provider/account
46
+ """
47
+ # Priority 1: Shotgun Account
48
+ if _get_api_key(config.shotgun.api_key):
49
+ return ModelName.GPT_5
50
+
51
+ # Priority 2: Individual provider keys
52
+ if _get_api_key(config.anthropic.api_key):
53
+ return ModelName.CLAUDE_HAIKU_4_5
54
+ if _get_api_key(config.openai.api_key):
55
+ return ModelName.GPT_5
56
+ if _get_api_key(config.google.api_key):
57
+ return ModelName.GEMINI_2_5_PRO
58
+
59
+ # Fallback: system-wide default
60
+ return ModelName.CLAUDE_HAIKU_4_5
61
+
62
+
32
63
  def get_or_create_model(
33
64
  provider: ProviderType,
34
65
  key_provider: "KeyProvider",
@@ -72,19 +103,37 @@ def get_or_create_model(
72
103
 
73
104
  # Use LiteLLM proxy for Shotgun Account, native providers for BYOK
74
105
  if key_provider == KeyProvider.SHOTGUN:
75
- # Shotgun Account uses LiteLLM proxy for any model
106
+ # Shotgun Account uses LiteLLM proxy with native model types where possible
76
107
  if model_name in MODEL_SPECS:
77
108
  litellm_model_name = MODEL_SPECS[model_name].litellm_proxy_model_name
78
109
  else:
79
110
  # Fallback for unmapped models
80
111
  litellm_model_name = f"openai/{model_name.value}"
81
112
 
82
- litellm_provider = create_litellm_provider(api_key)
83
- _model_cache[cache_key] = OpenAIChatModel(
84
- litellm_model_name,
85
- provider=litellm_provider,
86
- settings=ModelSettings(max_tokens=max_tokens),
87
- )
113
+ # Use native provider types to preserve API formats and features
114
+ if provider == ProviderType.ANTHROPIC:
115
+ # Anthropic: Use native AnthropicProvider with /anthropic endpoint
116
+ # This preserves Anthropic-specific features like tool_choice
117
+ # Note: Web search for Shotgun Account uses Gemini only (not Anthropic)
118
+ # Note: Anthropic API expects model name without prefix (e.g., "claude-sonnet-4-5")
119
+ anthropic_provider = create_anthropic_proxy_provider(api_key)
120
+ _model_cache[cache_key] = AnthropicModel(
121
+ model_name.value, # Use model name without "anthropic/" prefix
122
+ provider=anthropic_provider,
123
+ settings=AnthropicModelSettings(
124
+ max_tokens=max_tokens,
125
+ timeout=600, # 10 minutes timeout for large responses
126
+ ),
127
+ )
128
+ else:
129
+ # OpenAI and Google: Use LiteLLMProvider (OpenAI-compatible format)
130
+ # Google's GoogleProvider doesn't support base_url, so use LiteLLM
131
+ litellm_provider = create_litellm_provider(api_key)
132
+ _model_cache[cache_key] = OpenAIChatModel(
133
+ litellm_model_name,
134
+ provider=litellm_provider,
135
+ settings=ModelSettings(max_tokens=max_tokens),
136
+ )
88
137
  elif key_provider == KeyProvider.BYOK:
89
138
  # Use native provider implementations with user's API keys
90
139
  if provider == ProviderType.OPENAI:
@@ -145,13 +194,19 @@ def get_provider_model(
145
194
  # Priority 1: Check if Shotgun key exists - if so, use it for ANY model
146
195
  shotgun_api_key = _get_api_key(config.shotgun.api_key)
147
196
  if shotgun_api_key:
148
- # Use selected model or default to claude-sonnet-4-5
149
- model_name = config.selected_model or ModelName.CLAUDE_SONNET_4_5
197
+ # Determine which model to use
198
+ if isinstance(provider_or_model, ModelName):
199
+ # Specific model requested - honor it (e.g., web search tools)
200
+ model_name = provider_or_model
201
+ else:
202
+ # No specific model requested - use selected or default
203
+ model_name = config.selected_model or ModelName.GPT_5
204
+
150
205
  if model_name not in MODEL_SPECS:
151
206
  raise ValueError(f"Model '{model_name.value}' not found")
152
207
  spec = MODEL_SPECS[model_name]
153
208
 
154
- # Use Shotgun Account with selected model (provider = actual LLM provider)
209
+ # Use Shotgun Account with determined model (provider = actual LLM provider)
155
210
  return ModelConfig(
156
211
  name=spec.name,
157
212
  provider=spec.provider, # Actual LLM provider (OPENAI/ANTHROPIC/GOOGLE)
@@ -220,8 +275,8 @@ def get_provider_model(
220
275
  if not api_key:
221
276
  raise ValueError("Anthropic API key not configured. Set via config.")
222
277
 
223
- # Use requested model or default to claude-sonnet-4-5
224
- model_name = requested_model if requested_model else ModelName.CLAUDE_SONNET_4_5
278
+ # Use requested model or default to claude-haiku-4-5
279
+ model_name = requested_model if requested_model else ModelName.CLAUDE_HAIKU_4_5
225
280
  if model_name not in MODEL_SPECS:
226
281
  raise ValueError(f"Model '{model_name.value}' not found")
227
282
  spec = MODEL_SPECS[model_name]
@@ -0,0 +1,28 @@
1
+ """Context analysis module for conversation composition statistics.
2
+
3
+ This module provides tools for analyzing conversation context usage, breaking down
4
+ token consumption by message type and tool category.
5
+ """
6
+
7
+ from .analyzer import ContextAnalyzer
8
+ from .constants import ToolCategory, get_tool_category
9
+ from .formatter import ContextFormatter
10
+ from .models import (
11
+ ContextAnalysis,
12
+ ContextAnalysisOutput,
13
+ ContextCompositionTelemetry,
14
+ MessageTypeStats,
15
+ TokenAllocation,
16
+ )
17
+
18
+ __all__ = [
19
+ "ContextAnalyzer",
20
+ "ContextAnalysis",
21
+ "ContextAnalysisOutput",
22
+ "ContextCompositionTelemetry",
23
+ "ContextFormatter",
24
+ "MessageTypeStats",
25
+ "TokenAllocation",
26
+ "ToolCategory",
27
+ "get_tool_category",
28
+ ]