shotgun-sh 0.1.16.dev2__py3-none-any.whl → 0.2.0__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 (43) hide show
  1. shotgun/agents/common.py +4 -5
  2. shotgun/agents/config/constants.py +21 -5
  3. shotgun/agents/config/manager.py +171 -63
  4. shotgun/agents/config/models.py +65 -84
  5. shotgun/agents/config/provider.py +174 -85
  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 +54 -50
  20. shotgun/agents/tools/web_search/gemini.py +31 -20
  21. shotgun/agents/tools/web_search/openai.py +4 -4
  22. shotgun/build_constants.py +2 -2
  23. shotgun/cli/config.py +28 -57
  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 +7 -3
  32. shotgun/tui/screens/chat.py +2 -8
  33. shotgun/tui/screens/chat_screen/command_providers.py +118 -11
  34. shotgun/tui/screens/chat_screen/history.py +3 -1
  35. shotgun/tui/screens/model_picker.py +327 -0
  36. shotgun/tui/screens/provider_config.py +57 -26
  37. shotgun/utils/env_utils.py +12 -0
  38. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.0.dist-info}/METADATA +2 -2
  39. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.0.dist-info}/RECORD +42 -31
  40. shotgun/agents/history/token_counting.py +0 -429
  41. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.0.dist-info}/WHEEL +0 -0
  42. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.0.dist-info}/entry_points.txt +0 -0
  43. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.0.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,9 +92,8 @@ 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
- import os
94
97
  import sys
95
98
 
96
99
  from shotgun.logging_config import setup_logger
@@ -110,24 +113,23 @@ def main() -> None:
110
113
  # Join all arguments as the search query
111
114
  query = " ".join(sys.argv[1:])
112
115
 
113
- print("🔍 Testing Anthropic Web Search with streaming")
116
+ print("🔍 Testing Anthropic Web Search")
114
117
  print(f"📝 Query: {query}")
115
118
  print("=" * 60)
116
119
 
117
120
  # Check if API key is available
118
- if not (
119
- os.getenv("ANTHROPIC_API_KEY")
120
- or (
121
- callable(get_provider_model)
122
- and get_provider_model(ProviderType.ANTHROPIC).api_key
123
- )
124
- ):
125
- print(" Error: ANTHROPIC_API_KEY environment variable not set")
126
- print(" Please set it with: export ANTHROPIC_API_KEY=your_key_here")
121
+ try:
122
+ if callable(get_provider_model):
123
+ model_config = get_provider_model(ProviderType.ANTHROPIC)
124
+ if not model_config.api_key:
125
+ raise ValueError("No API key configured")
126
+ except (ValueError, Exception):
127
+ print("❌ Error: Anthropic API key not configured")
128
+ print(" Please set it in your config file")
127
129
  sys.exit(1)
128
130
 
129
131
  try:
130
- result = anthropic_web_search_tool(query)
132
+ result = await anthropic_web_search_tool(query)
131
133
  print(f"✅ Search completed! Result length: {len(result)} characters")
132
134
  print("=" * 60)
133
135
  print("📄 RESULTS:")
@@ -141,4 +143,6 @@ def main() -> None:
141
143
 
142
144
 
143
145
  if __name__ == "__main__":
144
- main()
146
+ import asyncio
147
+
148
+ 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}]}
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
12
12
  POSTHOG_PROJECT_ID = '191396'
13
13
 
14
14
  # Logfire configuration embedded at build time (only for dev builds)
15
- LOGFIRE_ENABLED = 'true'
16
- LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
15
+ LOGFIRE_ENABLED = ''
16
+ LOGFIRE_TOKEN = ''
17
17
 
18
18
  # Build metadata
19
19
  BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
shotgun/cli/config.py CHANGED
@@ -9,6 +9,7 @@ from rich.table import Table
9
9
 
10
10
  from shotgun.agents.config import ProviderType, get_config_manager
11
11
  from shotgun.logging_config import get_logger
12
+ from shotgun.utils.env_utils import is_shotgun_account_enabled
12
13
 
13
14
  logger = get_logger(__name__)
14
15
  console = Console()
@@ -43,11 +44,11 @@ def init(
43
44
  console.print()
44
45
 
45
46
  # Initialize with defaults
46
- config = config_manager.initialize()
47
+ config_manager.initialize()
47
48
 
48
- # Ask for default provider
49
+ # Ask for provider
49
50
  provider_choices = ["openai", "anthropic", "google"]
50
- console.print("Choose your default AI provider:")
51
+ console.print("Choose your AI provider:")
51
52
  for i, provider in enumerate(provider_choices, 1):
52
53
  console.print(f" {i}. {provider}")
53
54
 
@@ -55,7 +56,7 @@ def init(
55
56
  try:
56
57
  choice = typer.prompt("Enter choice (1-3)", type=int)
57
58
  if 1 <= choice <= 3:
58
- config.default_provider = ProviderType(provider_choices[choice - 1])
59
+ provider = ProviderType(provider_choices[choice - 1])
59
60
  break
60
61
  else:
61
62
  console.print(
@@ -65,7 +66,6 @@ def init(
65
66
  console.print("❌ Please enter a valid number.", style="red")
66
67
 
67
68
  # Ask for API key for the selected provider
68
- provider = config.default_provider
69
69
  console.print(f"\n🔑 Setting up {provider.upper()} API key...")
70
70
 
71
71
  api_key = typer.prompt(
@@ -75,9 +75,9 @@ def init(
75
75
  )
76
76
 
77
77
  if api_key:
78
+ # update_provider will automatically set selected_model for first provider
78
79
  config_manager.update_provider(provider, api_key=api_key)
79
80
 
80
- config_manager.save()
81
81
  console.print(
82
82
  f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
83
83
  )
@@ -98,16 +98,12 @@ def set(
98
98
  str | None,
99
99
  typer.Option("--api-key", "-k", help="API key for the provider"),
100
100
  ] = None,
101
- default: Annotated[
102
- bool,
103
- typer.Option("--default", "-d", help="Set this provider as default"),
104
- ] = False,
105
101
  ) -> None:
106
102
  """Set configuration for a specific provider."""
107
103
  config_manager = get_config_manager()
108
104
 
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:
105
+ # If no API key provided via option, prompt for it
106
+ if api_key is None:
111
107
  api_key = typer.prompt(
112
108
  f"Enter your {provider.upper()} API key",
113
109
  hide_input=True,
@@ -118,11 +114,6 @@ def set(
118
114
  if api_key:
119
115
  config_manager.update_provider(provider, api_key=api_key)
120
116
 
121
- if default:
122
- config = config_manager.load()
123
- config.default_provider = provider
124
- config_manager.save(config)
125
-
126
117
  console.print(f"✅ Configuration updated for {provider}")
127
118
 
128
119
  except Exception as e:
@@ -130,41 +121,6 @@ def set(
130
121
  raise typer.Exit(1) from e
131
122
 
132
123
 
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
124
  @app.command()
169
125
  def get(
170
126
  provider: Annotated[
@@ -201,16 +157,23 @@ def _show_full_config(config: Any) -> None:
201
157
  table.add_column("Setting", style="cyan")
202
158
  table.add_column("Value", style="white")
203
159
 
204
- # Default provider
205
- table.add_row("Default Provider", f"[bold]{config.default_provider}[/bold]")
160
+ # Selected model
161
+ selected_model = config.selected_model or "None (will auto-detect)"
162
+ table.add_row("Selected Model", f"[bold]{selected_model}[/bold]")
206
163
  table.add_row("", "") # Separator
207
164
 
208
165
  # Provider configurations
209
- for provider_name, provider_config in [
166
+ providers_to_show = [
210
167
  ("OpenAI", config.openai),
211
168
  ("Anthropic", config.anthropic),
212
169
  ("Google", config.google),
213
- ]:
170
+ ]
171
+
172
+ # Only show Shotgun Account if feature flag is enabled
173
+ if is_shotgun_account_enabled():
174
+ providers_to_show.append(("Shotgun Account", config.shotgun))
175
+
176
+ for provider_name, provider_config in providers_to_show:
214
177
  table.add_row(f"[bold]{provider_name}[/bold]", "")
215
178
 
216
179
  # API Key
@@ -231,6 +194,8 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
231
194
  provider_config = config.anthropic
232
195
  elif provider_str == "google":
233
196
  provider_config = config.google
197
+ elif provider_str == "shotgun":
198
+ provider_config = config.shotgun
234
199
  else:
235
200
  console.print(f"❌ Unknown provider: {provider}", style="red")
236
201
  return
@@ -248,7 +213,13 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
248
213
 
249
214
  def _mask_secrets(data: dict[str, Any]) -> None:
250
215
  """Mask secrets in configuration data."""
251
- for provider in ["openai", "anthropic", "google"]:
216
+ providers = ["openai", "anthropic", "google"]
217
+
218
+ # Only mask shotgun if feature flag is enabled
219
+ if is_shotgun_account_enabled():
220
+ providers.append("shotgun")
221
+
222
+ for provider in providers:
252
223
  if provider in data and isinstance(data[provider], dict):
253
224
  if "api_key" in data[provider] and data[provider]["api_key"]:
254
225
  data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
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
  }
@@ -62,7 +64,8 @@ class ShotgunApp(App[None]):
62
64
  return
63
65
 
64
66
  self.push_screen(
65
- "provider_config", callback=lambda _arg: self.refresh_startup_screen()
67
+ ProviderConfigScreen(),
68
+ callback=lambda _arg: self.refresh_startup_screen(),
66
69
  )
67
70
  return
68
71
 
@@ -71,7 +74,8 @@ class ShotgunApp(App[None]):
71
74
  return
72
75
 
73
76
  self.push_screen(
74
- "directory_setup", callback=lambda _arg: self.refresh_startup_screen()
77
+ DirectorySetupScreen(),
78
+ callback=lambda _arg: self.refresh_startup_screen(),
75
79
  )
76
80
  return
77
81
 
@@ -108,7 +112,7 @@ class ShotgunApp(App[None]):
108
112
  submit_feedback_survey(feedback)
109
113
  self.notify("Feedback sent. Thank you!")
110
114
 
111
- self.push_screen("feedback", callback=handle_feedback)
115
+ self.push_screen(FeedbackScreen(), callback=handle_feedback)
112
116
 
113
117
 
114
118
  def run(no_update_check: bool = False, continue_session: bool = False) -> None: