shotgun-sh 0.1.16.dev1__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.
- shotgun/agents/common.py +4 -5
- shotgun/agents/config/constants.py +21 -5
- shotgun/agents/config/manager.py +171 -63
- shotgun/agents/config/models.py +65 -84
- shotgun/agents/config/provider.py +174 -85
- shotgun/agents/history/compaction.py +1 -1
- shotgun/agents/history/history_processors.py +18 -9
- shotgun/agents/history/token_counting/__init__.py +31 -0
- shotgun/agents/history/token_counting/anthropic.py +89 -0
- shotgun/agents/history/token_counting/base.py +67 -0
- shotgun/agents/history/token_counting/openai.py +80 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
- shotgun/agents/history/token_counting/utils.py +147 -0
- shotgun/agents/history/token_estimation.py +12 -12
- shotgun/agents/llm.py +62 -0
- shotgun/agents/models.py +2 -2
- shotgun/agents/tools/web_search/__init__.py +42 -15
- shotgun/agents/tools/web_search/anthropic.py +54 -50
- shotgun/agents/tools/web_search/gemini.py +31 -20
- shotgun/agents/tools/web_search/openai.py +4 -4
- shotgun/build_constants.py +2 -2
- shotgun/cli/config.py +28 -57
- shotgun/cli/models.py +2 -2
- shotgun/codebase/models.py +4 -4
- shotgun/llm_proxy/__init__.py +16 -0
- shotgun/llm_proxy/clients.py +39 -0
- shotgun/llm_proxy/constants.py +8 -0
- shotgun/main.py +6 -0
- shotgun/posthog_telemetry.py +5 -3
- shotgun/tui/app.py +7 -3
- shotgun/tui/screens/chat.py +15 -10
- shotgun/tui/screens/chat_screen/command_providers.py +118 -11
- shotgun/tui/screens/chat_screen/history.py +3 -1
- shotgun/tui/screens/model_picker.py +327 -0
- shotgun/tui/screens/provider_config.py +57 -26
- shotgun/utils/env_utils.py +12 -0
- {shotgun_sh-0.1.16.dev1.dist-info → shotgun_sh-0.2.0.dist-info}/METADATA +2 -2
- {shotgun_sh-0.1.16.dev1.dist-info → shotgun_sh-0.2.0.dist-info}/RECORD +42 -31
- shotgun/agents/history/token_counting.py +0 -429
- {shotgun_sh-0.1.16.dev1.dist-info → shotgun_sh-0.2.0.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.16.dev1.dist-info → shotgun_sh-0.2.0.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.16.dev1.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
|
|
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.
|
|
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
|
|
34
|
+
logger.debug("📡 Executing Anthropic web search with prompt: %s", query)
|
|
31
35
|
|
|
32
|
-
# Get
|
|
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
|
-
|
|
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
|
|
50
|
+
# Use the Messages API with web search tool
|
|
45
51
|
try:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
print("
|
|
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
|
-
|
|
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.
|
|
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
|
|
36
|
+
# Get model configuration (supports both Shotgun and BYOK)
|
|
33
37
|
try:
|
|
34
|
-
model_config = get_provider_model(
|
|
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
|
-
|
|
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
|
-
#
|
|
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 =
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
response = await shotgun_model_request(
|
|
62
|
+
model_config=model_config,
|
|
63
|
+
messages=messages,
|
|
64
|
+
model_settings=ModelSettings(
|
|
64
65
|
temperature=0.3,
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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/build_constants.py
CHANGED
|
@@ -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 = '
|
|
16
|
-
LOGFIRE_TOKEN = '
|
|
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
|
-
|
|
47
|
+
config_manager.initialize()
|
|
47
48
|
|
|
48
|
-
# Ask for
|
|
49
|
+
# Ask for provider
|
|
49
50
|
provider_choices = ["openai", "anthropic", "google"]
|
|
50
|
-
console.print("Choose your
|
|
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
|
-
|
|
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
|
|
110
|
-
if api_key is None
|
|
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
|
-
#
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
shotgun/codebase/models.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Data models for codebase service."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from enum import
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""PostHog analytics setup for Shotgun."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|