shotgun-sh 0.1.15.dev2__py3-none-any.whl → 0.2.1.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 (40) hide show
  1. shotgun/agents/common.py +4 -5
  2. shotgun/agents/config/constants.py +21 -5
  3. shotgun/agents/config/manager.py +147 -39
  4. shotgun/agents/config/models.py +59 -86
  5. shotgun/agents/config/provider.py +164 -61
  6. shotgun/agents/history/compaction.py +1 -1
  7. shotgun/agents/history/history_processors.py +18 -9
  8. shotgun/agents/history/token_counting/__init__.py +31 -0
  9. shotgun/agents/history/token_counting/anthropic.py +89 -0
  10. shotgun/agents/history/token_counting/base.py +67 -0
  11. shotgun/agents/history/token_counting/openai.py +80 -0
  12. shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
  13. shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
  14. shotgun/agents/history/token_counting/utils.py +147 -0
  15. shotgun/agents/history/token_estimation.py +12 -12
  16. shotgun/agents/llm.py +62 -0
  17. shotgun/agents/models.py +2 -2
  18. shotgun/agents/tools/web_search/__init__.py +42 -15
  19. shotgun/agents/tools/web_search/anthropic.py +46 -40
  20. shotgun/agents/tools/web_search/gemini.py +31 -20
  21. shotgun/agents/tools/web_search/openai.py +4 -4
  22. shotgun/cli/config.py +14 -55
  23. shotgun/cli/feedback.py +1 -1
  24. shotgun/cli/models.py +2 -2
  25. shotgun/codebase/models.py +4 -4
  26. shotgun/llm_proxy/__init__.py +16 -0
  27. shotgun/llm_proxy/clients.py +39 -0
  28. shotgun/llm_proxy/constants.py +8 -0
  29. shotgun/main.py +6 -0
  30. shotgun/posthog_telemetry.py +5 -3
  31. shotgun/tui/app.py +3 -1
  32. shotgun/tui/screens/chat_screen/command_providers.py +20 -0
  33. shotgun/tui/screens/model_picker.py +214 -0
  34. shotgun/tui/screens/provider_config.py +39 -26
  35. {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/METADATA +2 -2
  36. {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/RECORD +39 -28
  37. shotgun/agents/history/token_counting.py +0 -429
  38. {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/WHEEL +0 -0
  39. {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/entry_points.txt +0 -0
  40. {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -1,20 +1,24 @@
1
1
  """Anthropic web search tool implementation."""
2
2
 
3
- import anthropic
4
3
  from opentelemetry import trace
4
+ from pydantic_ai.messages import ModelMessage, ModelRequest, TextPart
5
+ from pydantic_ai.settings import ModelSettings
5
6
 
6
7
  from shotgun.agents.config import get_provider_model
8
+ from shotgun.agents.config.constants import MEDIUM_TEXT_8K_TOKENS
7
9
  from shotgun.agents.config.models import ProviderType
10
+ from shotgun.agents.llm import shotgun_model_request
8
11
  from shotgun.logging_config import get_logger
9
12
 
10
13
  logger = get_logger(__name__)
11
14
 
12
15
 
13
- def anthropic_web_search_tool(query: str) -> str:
14
- """Perform a web search using Anthropic's Claude API with streaming.
16
+ async def anthropic_web_search_tool(query: str) -> str:
17
+ """Perform a web search using Anthropic's Claude API.
15
18
 
16
19
  This tool uses Anthropic's web search capabilities to find current information
17
- about the given query. Results are streamed for faster response times.
20
+ about the given query. Works with both Shotgun API keys (via LiteLLM proxy)
21
+ and direct Anthropic API keys (BYOK).
18
22
 
19
23
  Args:
20
24
  query: The search query
@@ -27,49 +31,49 @@ def anthropic_web_search_tool(query: str) -> str:
27
31
  span = trace.get_current_span()
28
32
  span.set_attribute("input.value", f"**Query:** {query}\n")
29
33
 
30
- logger.debug("📡 Executing Anthropic web search with streaming prompt: %s", query)
34
+ logger.debug("📡 Executing Anthropic web search with prompt: %s", query)
31
35
 
32
- # Get API key from centralized configuration
36
+ # Get model configuration (supports both Shotgun and BYOK)
33
37
  try:
34
38
  model_config = get_provider_model(ProviderType.ANTHROPIC)
35
- api_key = model_config.api_key
36
39
  except ValueError as e:
37
40
  error_msg = f"Anthropic API key not configured: {str(e)}"
38
41
  logger.error("❌ %s", error_msg)
39
42
  span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
40
43
  return error_msg
41
44
 
42
- client = anthropic.Anthropic(api_key=api_key)
45
+ # Build the request messages
46
+ messages: list[ModelMessage] = [
47
+ ModelRequest.user_text_prompt(f"Search for: {query}")
48
+ ]
43
49
 
44
- # Use the Messages API with web search tool and streaming
50
+ # Use the Messages API with web search tool
45
51
  try:
46
- result_text = ""
47
-
48
- with client.messages.stream(
49
- model="claude-3-5-sonnet-latest",
50
- max_tokens=8192, # Maximum for Claude 3.5 Sonnet
51
- messages=[{"role": "user", "content": f"Search for: {query}"}],
52
- tools=[
53
- {
54
- "type": "web_search_20250305",
55
- "name": "web_search",
56
- }
57
- ],
58
- tool_choice={"type": "tool", "name": "web_search"},
59
- ) as stream:
60
- logger.debug("🌊 Started streaming Anthropic web search response")
61
-
62
- for event in stream:
63
- if event.type == "content_block_delta":
64
- if hasattr(event.delta, "text"):
65
- result_text += event.delta.text
66
- elif event.type == "message_start":
67
- logger.debug("🚀 Streaming started")
68
- elif event.type == "message_stop":
69
- logger.debug("✅ Streaming completed")
70
-
71
- if not result_text:
72
- result_text = "No content returned from search"
52
+ response = await shotgun_model_request(
53
+ model_config=model_config,
54
+ messages=messages,
55
+ model_settings=ModelSettings(
56
+ max_tokens=MEDIUM_TEXT_8K_TOKENS,
57
+ # Enable Anthropic web search tool
58
+ extra_body={
59
+ "tools": [
60
+ {
61
+ "type": "web_search_20250305",
62
+ "name": "web_search",
63
+ }
64
+ ],
65
+ "tool_choice": {"type": "tool", "name": "web_search"},
66
+ },
67
+ ),
68
+ )
69
+
70
+ # Extract text from response
71
+ result_text = "No content returned from search"
72
+ if response.parts:
73
+ for part in response.parts:
74
+ if isinstance(part, TextPart):
75
+ result_text = part.content
76
+ break
73
77
 
74
78
  logger.debug("📄 Anthropic web search result: %d characters", len(result_text))
75
79
  logger.debug(
@@ -88,7 +92,7 @@ def anthropic_web_search_tool(query: str) -> str:
88
92
  return error_msg
89
93
 
90
94
 
91
- def main() -> None:
95
+ async def main() -> None:
92
96
  """Main function for testing the Anthropic web search tool."""
93
97
  import os
94
98
  import sys
@@ -110,7 +114,7 @@ def main() -> None:
110
114
  # Join all arguments as the search query
111
115
  query = " ".join(sys.argv[1:])
112
116
 
113
- print("🔍 Testing Anthropic Web Search with streaming")
117
+ print("🔍 Testing Anthropic Web Search")
114
118
  print(f"📝 Query: {query}")
115
119
  print("=" * 60)
116
120
 
@@ -127,7 +131,7 @@ def main() -> None:
127
131
  sys.exit(1)
128
132
 
129
133
  try:
130
- result = anthropic_web_search_tool(query)
134
+ result = await anthropic_web_search_tool(query)
131
135
  print(f"✅ Search completed! Result length: {len(result)} characters")
132
136
  print("=" * 60)
133
137
  print("📄 RESULTS:")
@@ -141,4 +145,6 @@ def main() -> None:
141
145
 
142
146
 
143
147
  if __name__ == "__main__":
144
- main()
148
+ import asyncio
149
+
150
+ asyncio.run(main())
@@ -1,20 +1,24 @@
1
1
  """Gemini web search tool implementation."""
2
2
 
3
- import google.generativeai as genai
4
3
  from opentelemetry import trace
4
+ from pydantic_ai.messages import ModelMessage, ModelRequest
5
+ from pydantic_ai.settings import ModelSettings
5
6
 
6
7
  from shotgun.agents.config import get_provider_model
7
- from shotgun.agents.config.models import ProviderType
8
+ from shotgun.agents.config.constants import MEDIUM_TEXT_8K_TOKENS
9
+ from shotgun.agents.config.models import ModelName
10
+ from shotgun.agents.llm import shotgun_model_request
8
11
  from shotgun.logging_config import get_logger
9
12
 
10
13
  logger = get_logger(__name__)
11
14
 
12
15
 
13
- def gemini_web_search_tool(query: str) -> str:
16
+ async def gemini_web_search_tool(query: str) -> str:
14
17
  """Perform a web search using Google's Gemini API with grounding.
15
18
 
16
19
  This tool uses Gemini's Google Search grounding to find current information
17
- about the given query.
20
+ about the given query. Works with both Shotgun API keys (via LiteLLM proxy)
21
+ and direct Gemini API keys (BYOK).
18
22
 
19
23
  Args:
20
24
  query: The search query
@@ -29,23 +33,16 @@ def gemini_web_search_tool(query: str) -> str:
29
33
 
30
34
  logger.debug("📡 Executing Gemini web search with prompt: %s", query)
31
35
 
32
- # Get API key from centralized configuration
36
+ # Get model configuration (supports both Shotgun and BYOK)
33
37
  try:
34
- model_config = get_provider_model(ProviderType.GOOGLE)
35
- api_key = model_config.api_key
38
+ model_config = get_provider_model(ModelName.GEMINI_2_5_FLASH)
36
39
  except ValueError as e:
37
40
  error_msg = f"Gemini API key not configured: {str(e)}"
38
41
  logger.error("❌ %s", error_msg)
39
42
  span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
40
43
  return error_msg
41
44
 
42
- genai.configure(api_key=api_key) # type: ignore[attr-defined]
43
-
44
- # Create model without built-in tools to avoid conflict with Pydantic AI
45
- # Using prompt-based search approach instead
46
- model = genai.GenerativeModel("gemini-2.5-pro") # type: ignore[attr-defined]
47
-
48
- # Create a search-optimized prompt that leverages Gemini's knowledge
45
+ # Create a search-optimized prompt
49
46
  search_prompt = f"""Please provide current and accurate information about the following query:
50
47
 
51
48
  Query: {query}
@@ -56,17 +53,31 @@ Instructions:
56
53
  - Focus on current and recent information
57
54
  - Be specific and accurate in your response"""
58
55
 
59
- # Generate response using the model's knowledge
56
+ # Build the request messages
57
+ messages: list[ModelMessage] = [ModelRequest.user_text_prompt(search_prompt)]
58
+
59
+ # Generate response using Pydantic AI with Google Search grounding
60
60
  try:
61
- response = model.generate_content(
62
- search_prompt,
63
- generation_config=genai.GenerationConfig( # type: ignore[attr-defined]
61
+ response = await shotgun_model_request(
62
+ model_config=model_config,
63
+ messages=messages,
64
+ model_settings=ModelSettings(
64
65
  temperature=0.3,
65
- max_output_tokens=8192,
66
+ max_tokens=MEDIUM_TEXT_8K_TOKENS,
67
+ # Enable Google Search grounding for Gemini
68
+ extra_body={"tools": [{"googleSearch": {}}]},
66
69
  ),
67
70
  )
68
71
 
69
- result_text = response.text or "No content returned from search"
72
+ # Extract text from response
73
+ from pydantic_ai.messages import TextPart
74
+
75
+ result_text = "No content returned from search"
76
+ if response.parts:
77
+ for part in response.parts:
78
+ if isinstance(part, TextPart):
79
+ result_text = part.content
80
+ break
70
81
 
71
82
  logger.debug("📄 Gemini web search result: %d characters", len(result_text))
72
83
  logger.debug(
@@ -1,6 +1,6 @@
1
1
  """OpenAI web search tool implementation."""
2
2
 
3
- from openai import OpenAI
3
+ from openai import AsyncOpenAI
4
4
  from opentelemetry import trace
5
5
 
6
6
  from shotgun.agents.config import get_provider_model
@@ -10,7 +10,7 @@ from shotgun.logging_config import get_logger
10
10
  logger = get_logger(__name__)
11
11
 
12
12
 
13
- def openai_web_search_tool(query: str) -> str:
13
+ async def openai_web_search_tool(query: str) -> str:
14
14
  """Perform a web search and return results.
15
15
 
16
16
  This tool uses OpenAI's web search capabilities to find current information
@@ -54,8 +54,8 @@ Instructions:
54
54
  ALWAYS PROVIDE THE SOURCES (urls) TO BACK UP THE INFORMATION YOU PROVIDE.
55
55
  """
56
56
 
57
- client = OpenAI(api_key=api_key)
58
- response = client.responses.create( # type: ignore[call-overload]
57
+ client = AsyncOpenAI(api_key=api_key)
58
+ response = await client.responses.create( # type: ignore[call-overload]
59
59
  model="gpt-5-mini",
60
60
  input=[
61
61
  {"role": "user", "content": [{"type": "input_text", "text": prompt}]}
shotgun/cli/config.py CHANGED
@@ -43,11 +43,11 @@ def init(
43
43
  console.print()
44
44
 
45
45
  # Initialize with defaults
46
- config = config_manager.initialize()
46
+ config_manager.initialize()
47
47
 
48
- # Ask for default provider
48
+ # Ask for provider
49
49
  provider_choices = ["openai", "anthropic", "google"]
50
- console.print("Choose your default AI provider:")
50
+ console.print("Choose your AI provider:")
51
51
  for i, provider in enumerate(provider_choices, 1):
52
52
  console.print(f" {i}. {provider}")
53
53
 
@@ -55,7 +55,7 @@ def init(
55
55
  try:
56
56
  choice = typer.prompt("Enter choice (1-3)", type=int)
57
57
  if 1 <= choice <= 3:
58
- config.default_provider = ProviderType(provider_choices[choice - 1])
58
+ provider = ProviderType(provider_choices[choice - 1])
59
59
  break
60
60
  else:
61
61
  console.print(
@@ -65,7 +65,6 @@ def init(
65
65
  console.print("❌ Please enter a valid number.", style="red")
66
66
 
67
67
  # Ask for API key for the selected provider
68
- provider = config.default_provider
69
68
  console.print(f"\n🔑 Setting up {provider.upper()} API key...")
70
69
 
71
70
  api_key = typer.prompt(
@@ -75,9 +74,9 @@ def init(
75
74
  )
76
75
 
77
76
  if api_key:
77
+ # update_provider will automatically set selected_model for first provider
78
78
  config_manager.update_provider(provider, api_key=api_key)
79
79
 
80
- config_manager.save()
81
80
  console.print(
82
81
  f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
83
82
  )
@@ -98,16 +97,12 @@ def set(
98
97
  str | None,
99
98
  typer.Option("--api-key", "-k", help="API key for the provider"),
100
99
  ] = None,
101
- default: Annotated[
102
- bool,
103
- typer.Option("--default", "-d", help="Set this provider as default"),
104
- ] = False,
105
100
  ) -> None:
106
101
  """Set configuration for a specific provider."""
107
102
  config_manager = get_config_manager()
108
103
 
109
- # If no API key provided via option and not just setting default, prompt for it
110
- if api_key is None and not default:
104
+ # If no API key provided via option, prompt for it
105
+ if api_key is None:
111
106
  api_key = typer.prompt(
112
107
  f"Enter your {provider.upper()} API key",
113
108
  hide_input=True,
@@ -118,11 +113,6 @@ def set(
118
113
  if api_key:
119
114
  config_manager.update_provider(provider, api_key=api_key)
120
115
 
121
- if default:
122
- config = config_manager.load()
123
- config.default_provider = provider
124
- config_manager.save(config)
125
-
126
116
  console.print(f"✅ Configuration updated for {provider}")
127
117
 
128
118
  except Exception as e:
@@ -130,41 +120,6 @@ def set(
130
120
  raise typer.Exit(1) from e
131
121
 
132
122
 
133
- @app.command()
134
- def set_default(
135
- provider: Annotated[
136
- ProviderType,
137
- typer.Argument(
138
- help="AI provider to set as default (openai, anthropic, google)"
139
- ),
140
- ],
141
- ) -> None:
142
- """Set the default AI provider without modifying API keys."""
143
- config_manager = get_config_manager()
144
-
145
- try:
146
- config = config_manager.load()
147
-
148
- # Check if the provider has an API key configured
149
- provider_config = getattr(config, provider.value)
150
- if not provider_config.api_key:
151
- console.print(
152
- f"⚠️ Warning: {provider.upper()} does not have an API key configured.",
153
- style="yellow",
154
- )
155
- console.print(f"Use 'shotgun config set {provider}' to configure it.")
156
-
157
- # Set as default
158
- config.default_provider = provider
159
- config_manager.save(config)
160
-
161
- console.print(f"✅ Default provider set to: {provider}")
162
-
163
- except Exception as e:
164
- console.print(f"❌ Failed to set default provider: {e}", style="red")
165
- raise typer.Exit(1) from e
166
-
167
-
168
123
  @app.command()
169
124
  def get(
170
125
  provider: Annotated[
@@ -201,8 +156,9 @@ def _show_full_config(config: Any) -> None:
201
156
  table.add_column("Setting", style="cyan")
202
157
  table.add_column("Value", style="white")
203
158
 
204
- # Default provider
205
- table.add_row("Default Provider", f"[bold]{config.default_provider}[/bold]")
159
+ # Selected model
160
+ selected_model = config.selected_model or "None (will auto-detect)"
161
+ table.add_row("Selected Model", f"[bold]{selected_model}[/bold]")
206
162
  table.add_row("", "") # Separator
207
163
 
208
164
  # Provider configurations
@@ -210,6 +166,7 @@ def _show_full_config(config: Any) -> None:
210
166
  ("OpenAI", config.openai),
211
167
  ("Anthropic", config.anthropic),
212
168
  ("Google", config.google),
169
+ ("Shotgun Account", config.shotgun),
213
170
  ]:
214
171
  table.add_row(f"[bold]{provider_name}[/bold]", "")
215
172
 
@@ -231,6 +188,8 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
231
188
  provider_config = config.anthropic
232
189
  elif provider_str == "google":
233
190
  provider_config = config.google
191
+ elif provider_str == "shotgun":
192
+ provider_config = config.shotgun
234
193
  else:
235
194
  console.print(f"❌ Unknown provider: {provider}", style="red")
236
195
  return
@@ -248,7 +207,7 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
248
207
 
249
208
  def _mask_secrets(data: dict[str, Any]) -> None:
250
209
  """Mask secrets in configuration data."""
251
- for provider in ["openai", "anthropic", "google"]:
210
+ for provider in ["openai", "anthropic", "google", "shotgun"]:
252
211
  if provider in data and isinstance(data[provider], dict):
253
212
  if "api_key" in data[provider] and data[provider]["api_key"]:
254
213
  data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
shotgun/cli/feedback.py CHANGED
@@ -43,4 +43,4 @@ def send_feedback(
43
43
 
44
44
  submit_feedback_survey(feedback)
45
45
 
46
- console.print("Feedback sent. Thank you!")
46
+ console.print("Feedback sent. Thank you!")
shotgun/cli/models.py CHANGED
@@ -1,9 +1,9 @@
1
1
  """Common models for CLI commands."""
2
2
 
3
- from enum import Enum
3
+ from enum import StrEnum
4
4
 
5
5
 
6
- class OutputFormat(str, Enum):
6
+ class OutputFormat(StrEnum):
7
7
  """Output format options for CLI commands."""
8
8
 
9
9
  TEXT = "text"
@@ -1,13 +1,13 @@
1
1
  """Data models for codebase service."""
2
2
 
3
3
  from collections.abc import Callable
4
- from enum import Enum
4
+ from enum import StrEnum
5
5
  from typing import Any
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
9
9
 
10
- class GraphStatus(str, Enum):
10
+ class GraphStatus(StrEnum):
11
11
  """Status of a code knowledge graph."""
12
12
 
13
13
  READY = "READY" # Graph is ready for queries
@@ -16,14 +16,14 @@ class GraphStatus(str, Enum):
16
16
  ERROR = "ERROR" # Last operation failed
17
17
 
18
18
 
19
- class QueryType(str, Enum):
19
+ class QueryType(StrEnum):
20
20
  """Type of query being executed."""
21
21
 
22
22
  NATURAL_LANGUAGE = "natural_language"
23
23
  CYPHER = "cypher"
24
24
 
25
25
 
26
- class ProgressPhase(str, Enum):
26
+ class ProgressPhase(StrEnum):
27
27
  """Phase of codebase indexing progress."""
28
28
 
29
29
  STRUCTURE = "structure" # Identifying packages and folders
@@ -0,0 +1,16 @@
1
+ """LiteLLM proxy client utilities and configuration."""
2
+
3
+ from .clients import create_anthropic_proxy_client, create_litellm_provider
4
+ from .constants import (
5
+ LITELLM_PROXY_ANTHROPIC_BASE,
6
+ LITELLM_PROXY_BASE_URL,
7
+ LITELLM_PROXY_OPENAI_BASE,
8
+ )
9
+
10
+ __all__ = [
11
+ "LITELLM_PROXY_BASE_URL",
12
+ "LITELLM_PROXY_ANTHROPIC_BASE",
13
+ "LITELLM_PROXY_OPENAI_BASE",
14
+ "create_litellm_provider",
15
+ "create_anthropic_proxy_client",
16
+ ]
@@ -0,0 +1,39 @@
1
+ """Client creation utilities for LiteLLM proxy."""
2
+
3
+ from anthropic import Anthropic
4
+ from pydantic_ai.providers.litellm import LiteLLMProvider
5
+
6
+ from .constants import LITELLM_PROXY_ANTHROPIC_BASE, LITELLM_PROXY_BASE_URL
7
+
8
+
9
+ def create_litellm_provider(api_key: str) -> LiteLLMProvider:
10
+ """Create LiteLLM provider for Shotgun Account.
11
+
12
+ Args:
13
+ api_key: Shotgun API key
14
+
15
+ Returns:
16
+ Configured LiteLLM provider pointing to Shotgun's proxy
17
+ """
18
+ return LiteLLMProvider(
19
+ api_base=LITELLM_PROXY_BASE_URL,
20
+ api_key=api_key,
21
+ )
22
+
23
+
24
+ def create_anthropic_proxy_client(api_key: str) -> Anthropic:
25
+ """Create Anthropic client configured for LiteLLM proxy.
26
+
27
+ This client will proxy token counting requests through the
28
+ LiteLLM proxy to Anthropic's actual token counting API.
29
+
30
+ Args:
31
+ api_key: Shotgun API key
32
+
33
+ Returns:
34
+ Anthropic client configured to use LiteLLM proxy
35
+ """
36
+ return Anthropic(
37
+ api_key=api_key,
38
+ base_url=LITELLM_PROXY_ANTHROPIC_BASE,
39
+ )
@@ -0,0 +1,8 @@
1
+ """LiteLLM proxy constants and configuration."""
2
+
3
+ # Shotgun's LiteLLM proxy base URL
4
+ LITELLM_PROXY_BASE_URL = "https://litellm-701197220809.us-east1.run.app"
5
+
6
+ # Provider-specific endpoints
7
+ LITELLM_PROXY_ANTHROPIC_BASE = f"{LITELLM_PROXY_BASE_URL}/anthropic"
8
+ LITELLM_PROXY_OPENAI_BASE = LITELLM_PROXY_BASE_URL
shotgun/main.py CHANGED
@@ -1,5 +1,11 @@
1
1
  """Main CLI application for shotgun."""
2
2
 
3
+ # NOTE: These are before we import any Google library to stop the noisy gRPC logs.
4
+ import os # noqa: I001
5
+
6
+ os.environ["GRPC_VERBOSITY"] = "ERROR"
7
+ os.environ["GLOG_minloglevel"] = "2"
8
+
3
9
  import logging
4
10
 
5
11
  # CRITICAL: Add NullHandler to root logger before ANY other imports.
@@ -1,6 +1,6 @@
1
1
  """PostHog analytics setup for Shotgun."""
2
2
 
3
- from enum import Enum
3
+ from enum import StrEnum
4
4
  from typing import Any
5
5
 
6
6
  import posthog
@@ -137,7 +137,7 @@ def shutdown() -> None:
137
137
  _posthog_client = None
138
138
 
139
139
 
140
- class FeedbackKind(str, Enum):
140
+ class FeedbackKind(StrEnum):
141
141
  BUG = "bug"
142
142
  FEATURE = "feature"
143
143
  OTHER = "other"
@@ -178,7 +178,9 @@ def submit_feedback_survey(feedback: Feedback) -> None:
178
178
  ],
179
179
  f"$survey_response_{Q_KIND_ID}": feedback.kind,
180
180
  f"$survey_response_{Q_DESCRIPTION_ID}": feedback.description,
181
- "provider": config.default_provider.value,
181
+ "selected_model": config.selected_model.value
182
+ if config.selected_model
183
+ else None,
182
184
  "config_version": config.config_version,
183
185
  "last_10_messages": last_10_messages, # last 10 messages
184
186
  },
shotgun/tui/app.py CHANGED
@@ -14,6 +14,7 @@ from shotgun.utils.update_checker import perform_auto_update_async
14
14
  from .screens.chat import ChatScreen
15
15
  from .screens.directory_setup import DirectorySetupScreen
16
16
  from .screens.feedback import FeedbackScreen
17
+ from .screens.model_picker import ModelPickerScreen
17
18
  from .screens.provider_config import ProviderConfigScreen
18
19
 
19
20
  logger = get_logger(__name__)
@@ -23,6 +24,7 @@ class ShotgunApp(App[None]):
23
24
  SCREENS = {
24
25
  "chat": ChatScreen,
25
26
  "provider_config": ProviderConfigScreen,
27
+ "model_picker": ModelPickerScreen,
26
28
  "directory_setup": DirectorySetupScreen,
27
29
  "feedback": FeedbackScreen,
28
30
  }
@@ -106,7 +108,7 @@ class ShotgunApp(App[None]):
106
108
  def handle_feedback(feedback: Feedback | None) -> None:
107
109
  if feedback is not None:
108
110
  submit_feedback_survey(feedback)
109
- self.notify("Feedback sent. Thank you!")
111
+ self.notify("Feedback sent. Thank you!")
110
112
 
111
113
  self.push_screen("feedback", callback=handle_feedback)
112
114
 
@@ -109,15 +109,25 @@ class ProviderSetupProvider(Provider):
109
109
  """Show the provider configuration screen."""
110
110
  self.chat_screen.app.push_screen("provider_config")
111
111
 
112
+ def open_model_picker(self) -> None:
113
+ """Show the model picker screen."""
114
+ self.chat_screen.app.push_screen("model_picker")
115
+
112
116
  async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
113
117
  yield DiscoveryHit(
114
118
  "Open Provider Setup",
115
119
  self.open_provider_config,
116
120
  help="⚙️ Manage API keys for available providers",
117
121
  )
122
+ yield DiscoveryHit(
123
+ "Select AI Model",
124
+ self.open_model_picker,
125
+ help="🤖 Choose which AI model to use",
126
+ )
118
127
 
119
128
  async def search(self, query: str) -> AsyncGenerator[Hit, None]:
120
129
  matcher = self.matcher(query)
130
+
121
131
  title = "Open Provider Setup"
122
132
  score = matcher.match(title)
123
133
  if score > 0:
@@ -128,6 +138,16 @@ class ProviderSetupProvider(Provider):
128
138
  help="⚙️ Manage API keys for available providers",
129
139
  )
130
140
 
141
+ title = "Select AI Model"
142
+ score = matcher.match(title)
143
+ if score > 0:
144
+ yield Hit(
145
+ score,
146
+ matcher.highlight(title),
147
+ self.open_model_picker,
148
+ help="🤖 Choose which AI model to use",
149
+ )
150
+
131
151
 
132
152
  class CodebaseCommandProvider(Provider):
133
153
  """Command palette entries for codebase management."""