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.
- shotgun/agents/agent_manager.py +524 -58
- shotgun/agents/common.py +62 -62
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +14 -3
- shotgun/agents/config/models.py +16 -0
- shotgun/agents/config/provider.py +68 -13
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +493 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +24 -2
- shotgun/agents/export.py +4 -5
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +14 -2
- shotgun/agents/history/token_counting/anthropic.py +32 -10
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +4 -5
- shotgun/agents/research.py +4 -5
- shotgun/agents/specify.py +4 -5
- shotgun/agents/tasks.py +4 -5
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +6 -0
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +71 -9
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +24 -12
- shotgun/agents/tools/web_search/anthropic.py +24 -3
- shotgun/agents/tools/web_search/gemini.py +22 -10
- shotgun/agents/tools/web_search/openai.py +21 -12
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +1 -1
- shotgun/cli/clear.py +52 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/context.py +111 -0
- shotgun/cli/models.py +1 -0
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/manager.py +10 -1
- shotgun/llm_proxy/__init__.py +5 -2
- shotgun/llm_proxy/clients.py +12 -7
- shotgun/logging_config.py +8 -10
- shotgun/main.py +70 -10
- shotgun/posthog_telemetry.py +9 -3
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +4 -15
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +15 -32
- shotgun/tui/app.py +203 -9
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +136 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +93 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1110 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +39 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +68 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/model_picker.py +30 -6
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/welcome.py +24 -5
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +182 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +247 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/file_system_utils.py +3 -2
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dev1.dist-info/METADATA +129 -0
- shotgun_sh-0.2.11.dev1.dist-info/RECORD +190 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -352
- shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
- shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
- {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,
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
#
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
#
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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,
|
|
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[
|
|
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
|
shotgun/agents/config/manager.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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,
|
|
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:
|
shotgun/agents/config/models.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
#
|
|
149
|
-
|
|
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
|
|
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-
|
|
224
|
-
model_name = requested_model if requested_model else ModelName.
|
|
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
|
+
]
|