shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.2.17__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.
- shotgun/agents/agent_manager.py +354 -46
- shotgun/agents/common.py +14 -8
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +66 -35
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +33 -5
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -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 +2 -0
- shotgun/agents/conversation_manager.py +35 -19
- shotgun/agents/export.py +2 -2
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/history_processors.py +113 -5
- shotgun/agents/history/token_counting/anthropic.py +17 -1
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -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 +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +7 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +3 -3
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +37 -28
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +243 -43
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -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 +1254 -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 +40 -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 +78 -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 +115 -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/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +14 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.17.dist-info/METADATA +465 -0
- shotgun_sh-0.2.17.dist-info/RECORD +194 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
shotgun/agents/config/manager.py
CHANGED
|
@@ -5,6 +5,8 @@ import uuid
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
import aiofiles
|
|
9
|
+
import aiofiles.os
|
|
8
10
|
from pydantic import SecretStr
|
|
9
11
|
|
|
10
12
|
from shotgun.logging_config import get_logger
|
|
@@ -48,7 +50,7 @@ class ConfigManager:
|
|
|
48
50
|
|
|
49
51
|
self._config: ShotgunConfig | None = None
|
|
50
52
|
|
|
51
|
-
def load(self, force_reload: bool = True) -> ShotgunConfig:
|
|
53
|
+
async def load(self, force_reload: bool = True) -> ShotgunConfig:
|
|
52
54
|
"""Load configuration from file.
|
|
53
55
|
|
|
54
56
|
Args:
|
|
@@ -60,18 +62,19 @@ class ConfigManager:
|
|
|
60
62
|
if self._config is not None and not force_reload:
|
|
61
63
|
return self._config
|
|
62
64
|
|
|
63
|
-
if not
|
|
65
|
+
if not await aiofiles.os.path.exists(self.config_path):
|
|
64
66
|
logger.info(
|
|
65
67
|
"Configuration file not found, creating new config at: %s",
|
|
66
68
|
self.config_path,
|
|
67
69
|
)
|
|
68
70
|
# Create new config with generated shotgun_instance_id
|
|
69
|
-
self._config = self.initialize()
|
|
71
|
+
self._config = await self.initialize()
|
|
70
72
|
return self._config
|
|
71
73
|
|
|
72
74
|
try:
|
|
73
|
-
with open(self.config_path, encoding="utf-8") as f:
|
|
74
|
-
|
|
75
|
+
async with aiofiles.open(self.config_path, encoding="utf-8") as f:
|
|
76
|
+
content = await f.read()
|
|
77
|
+
data = json.loads(content)
|
|
75
78
|
|
|
76
79
|
# Migration: Rename user_id to shotgun_instance_id (config v2 -> v3)
|
|
77
80
|
if "user_id" in data and SHOTGUN_INSTANCE_ID_FIELD not in data:
|
|
@@ -101,6 +104,12 @@ class ConfigManager:
|
|
|
101
104
|
"Existing BYOK user detected: set shown_welcome_screen=False to show welcome screen"
|
|
102
105
|
)
|
|
103
106
|
|
|
107
|
+
# Migration: Add marketing config for v3 -> v4
|
|
108
|
+
if "marketing" not in data:
|
|
109
|
+
data["marketing"] = {"messages": {}}
|
|
110
|
+
data["config_version"] = 4
|
|
111
|
+
logger.info("Migrated config v3->v4: added marketing configuration")
|
|
112
|
+
|
|
104
113
|
# Convert plain text secrets to SecretStr objects
|
|
105
114
|
self._convert_secrets_to_secretstr(data)
|
|
106
115
|
|
|
@@ -117,7 +126,7 @@ class ConfigManager:
|
|
|
117
126
|
|
|
118
127
|
if self._config.selected_model in MODEL_SPECS:
|
|
119
128
|
spec = MODEL_SPECS[self._config.selected_model]
|
|
120
|
-
if not self.has_provider_key(spec.provider):
|
|
129
|
+
if not await self.has_provider_key(spec.provider):
|
|
121
130
|
logger.info(
|
|
122
131
|
"Selected model %s provider has no API key, finding available model",
|
|
123
132
|
self._config.selected_model.value,
|
|
@@ -135,14 +144,14 @@ class ConfigManager:
|
|
|
135
144
|
# If no selected_model or it was invalid, find first available model
|
|
136
145
|
if not self._config.selected_model:
|
|
137
146
|
for provider in ProviderType:
|
|
138
|
-
if self.has_provider_key(provider):
|
|
147
|
+
if await self.has_provider_key(provider):
|
|
139
148
|
# Set to that provider's default model
|
|
140
149
|
from .models import MODEL_SPECS, ModelName
|
|
141
150
|
|
|
142
151
|
# Find default model for this provider
|
|
143
152
|
provider_models = {
|
|
144
153
|
ProviderType.OPENAI: ModelName.GPT_5,
|
|
145
|
-
ProviderType.ANTHROPIC: ModelName.
|
|
154
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_HAIKU_4_5,
|
|
146
155
|
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -156,7 +165,7 @@ class ConfigManager:
|
|
|
156
165
|
break
|
|
157
166
|
|
|
158
167
|
if should_save:
|
|
159
|
-
self.save(self._config)
|
|
168
|
+
await self.save(self._config)
|
|
160
169
|
|
|
161
170
|
return self._config
|
|
162
171
|
|
|
@@ -165,10 +174,10 @@ class ConfigManager:
|
|
|
165
174
|
"Failed to load configuration from %s: %s", self.config_path, e
|
|
166
175
|
)
|
|
167
176
|
logger.info("Creating new configuration with generated shotgun_instance_id")
|
|
168
|
-
self._config = self.initialize()
|
|
177
|
+
self._config = await self.initialize()
|
|
169
178
|
return self._config
|
|
170
179
|
|
|
171
|
-
def save(self, config: ShotgunConfig | None = None) -> None:
|
|
180
|
+
async def save(self, config: ShotgunConfig | None = None) -> None:
|
|
172
181
|
"""Save configuration to file.
|
|
173
182
|
|
|
174
183
|
Args:
|
|
@@ -184,15 +193,17 @@ class ConfigManager:
|
|
|
184
193
|
)
|
|
185
194
|
|
|
186
195
|
# Ensure directory exists
|
|
187
|
-
self.config_path.parent
|
|
196
|
+
await aiofiles.os.makedirs(self.config_path.parent, exist_ok=True)
|
|
188
197
|
|
|
189
198
|
try:
|
|
190
199
|
# Convert SecretStr to plain text for JSON serialization
|
|
191
200
|
data = config.model_dump()
|
|
192
201
|
self._convert_secretstr_to_plain(data)
|
|
202
|
+
self._convert_datetime_to_isoformat(data)
|
|
193
203
|
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
json_content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
205
|
+
async with aiofiles.open(self.config_path, "w", encoding="utf-8") as f:
|
|
206
|
+
await f.write(json_content)
|
|
196
207
|
|
|
197
208
|
logger.debug("Configuration saved to %s", self.config_path)
|
|
198
209
|
self._config = config
|
|
@@ -201,14 +212,16 @@ class ConfigManager:
|
|
|
201
212
|
logger.error("Failed to save configuration to %s: %s", self.config_path, e)
|
|
202
213
|
raise
|
|
203
214
|
|
|
204
|
-
def update_provider(
|
|
215
|
+
async def update_provider(
|
|
216
|
+
self, provider: ProviderType | str, **kwargs: Any
|
|
217
|
+
) -> None:
|
|
205
218
|
"""Update provider configuration.
|
|
206
219
|
|
|
207
220
|
Args:
|
|
208
221
|
provider: Provider to update
|
|
209
222
|
**kwargs: Configuration fields to update (only api_key supported)
|
|
210
223
|
"""
|
|
211
|
-
config = self.load()
|
|
224
|
+
config = await self.load()
|
|
212
225
|
|
|
213
226
|
# Get provider config and check if it's shotgun
|
|
214
227
|
provider_config, is_shotgun = self._get_provider_config_and_type(
|
|
@@ -243,7 +256,7 @@ class ConfigManager:
|
|
|
243
256
|
|
|
244
257
|
provider_models = {
|
|
245
258
|
ProviderType.OPENAI: ModelName.GPT_5,
|
|
246
|
-
ProviderType.ANTHROPIC: ModelName.
|
|
259
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_HAIKU_4_5,
|
|
247
260
|
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
248
261
|
}
|
|
249
262
|
if provider_enum in provider_models:
|
|
@@ -253,11 +266,11 @@ class ConfigManager:
|
|
|
253
266
|
# This prevents the welcome screen from showing again after user has made their choice
|
|
254
267
|
config.shown_welcome_screen = True
|
|
255
268
|
|
|
256
|
-
self.save(config)
|
|
269
|
+
await self.save(config)
|
|
257
270
|
|
|
258
|
-
def clear_provider_key(self, provider: ProviderType | str) -> None:
|
|
271
|
+
async def clear_provider_key(self, provider: ProviderType | str) -> None:
|
|
259
272
|
"""Remove the API key for the given provider (LLM provider or shotgun)."""
|
|
260
|
-
config = self.load()
|
|
273
|
+
config = await self.load()
|
|
261
274
|
|
|
262
275
|
# Get provider config (shotgun or LLM provider)
|
|
263
276
|
provider_config, is_shotgun = self._get_provider_config_and_type(
|
|
@@ -270,34 +283,34 @@ class ConfigManager:
|
|
|
270
283
|
if is_shotgun and isinstance(provider_config, ShotgunAccountConfig):
|
|
271
284
|
provider_config.supabase_jwt = None
|
|
272
285
|
|
|
273
|
-
self.save(config)
|
|
286
|
+
await self.save(config)
|
|
274
287
|
|
|
275
|
-
def update_selected_model(self, model_name: "ModelName") -> None:
|
|
288
|
+
async def update_selected_model(self, model_name: "ModelName") -> None:
|
|
276
289
|
"""Update the selected model.
|
|
277
290
|
|
|
278
291
|
Args:
|
|
279
292
|
model_name: Model to select
|
|
280
293
|
"""
|
|
281
|
-
config = self.load()
|
|
294
|
+
config = await self.load()
|
|
282
295
|
config.selected_model = model_name
|
|
283
|
-
self.save(config)
|
|
296
|
+
await self.save(config)
|
|
284
297
|
|
|
285
|
-
def has_provider_key(self, provider: ProviderType | str) -> bool:
|
|
298
|
+
async def has_provider_key(self, provider: ProviderType | str) -> bool:
|
|
286
299
|
"""Check if the given provider has a non-empty API key configured.
|
|
287
300
|
|
|
288
301
|
This checks only the configuration file.
|
|
289
302
|
"""
|
|
290
303
|
# Use force_reload=False to avoid infinite loop when called from load()
|
|
291
|
-
config = self.load(force_reload=False)
|
|
304
|
+
config = await self.load(force_reload=False)
|
|
292
305
|
provider_enum = self._ensure_provider_enum(provider)
|
|
293
306
|
provider_config = self._get_provider_config(config, provider_enum)
|
|
294
307
|
|
|
295
308
|
return self._provider_has_api_key(provider_config)
|
|
296
309
|
|
|
297
|
-
def has_any_provider_key(self) -> bool:
|
|
310
|
+
async def has_any_provider_key(self) -> bool:
|
|
298
311
|
"""Determine whether any provider has a configured API key."""
|
|
299
312
|
# Use force_reload=False to avoid infinite loop when called from load()
|
|
300
|
-
config = self.load(force_reload=False)
|
|
313
|
+
config = await self.load(force_reload=False)
|
|
301
314
|
# Check LLM provider keys (BYOK)
|
|
302
315
|
has_llm_key = any(
|
|
303
316
|
self._provider_has_api_key(self._get_provider_config(config, provider))
|
|
@@ -311,7 +324,7 @@ class ConfigManager:
|
|
|
311
324
|
has_shotgun_key = self._provider_has_api_key(config.shotgun)
|
|
312
325
|
return has_llm_key or has_shotgun_key
|
|
313
326
|
|
|
314
|
-
def initialize(self) -> ShotgunConfig:
|
|
327
|
+
async def initialize(self) -> ShotgunConfig:
|
|
315
328
|
"""Initialize configuration with defaults and save to file.
|
|
316
329
|
|
|
317
330
|
Returns:
|
|
@@ -321,7 +334,7 @@ class ConfigManager:
|
|
|
321
334
|
config = ShotgunConfig(
|
|
322
335
|
shotgun_instance_id=str(uuid.uuid4()),
|
|
323
336
|
)
|
|
324
|
-
self.save(config)
|
|
337
|
+
await self.save(config)
|
|
325
338
|
logger.info(
|
|
326
339
|
"Configuration initialized at %s with shotgun_instance_id: %s",
|
|
327
340
|
self.config_path,
|
|
@@ -377,6 +390,24 @@ class ConfigManager:
|
|
|
377
390
|
SUPABASE_JWT_FIELD
|
|
378
391
|
].get_secret_value()
|
|
379
392
|
|
|
393
|
+
def _convert_datetime_to_isoformat(self, data: dict[str, Any]) -> None:
|
|
394
|
+
"""Convert datetime objects in data to ISO8601 format strings for JSON serialization."""
|
|
395
|
+
from datetime import datetime
|
|
396
|
+
|
|
397
|
+
def convert_dict(d: dict[str, Any]) -> None:
|
|
398
|
+
"""Recursively convert datetime objects in a dict."""
|
|
399
|
+
for key, value in d.items():
|
|
400
|
+
if isinstance(value, datetime):
|
|
401
|
+
d[key] = value.isoformat()
|
|
402
|
+
elif isinstance(value, dict):
|
|
403
|
+
convert_dict(value)
|
|
404
|
+
elif isinstance(value, list):
|
|
405
|
+
for item in value:
|
|
406
|
+
if isinstance(item, dict):
|
|
407
|
+
convert_dict(item)
|
|
408
|
+
|
|
409
|
+
convert_dict(data)
|
|
410
|
+
|
|
380
411
|
def _ensure_provider_enum(self, provider: ProviderType | str) -> ProviderType:
|
|
381
412
|
"""Normalize provider values to ProviderType enum."""
|
|
382
413
|
return (
|
|
@@ -440,16 +471,16 @@ class ConfigManager:
|
|
|
440
471
|
provider_enum = self._ensure_provider_enum(provider)
|
|
441
472
|
return (self._get_provider_config(config, provider_enum), False)
|
|
442
473
|
|
|
443
|
-
def get_shotgun_instance_id(self) -> str:
|
|
474
|
+
async def get_shotgun_instance_id(self) -> str:
|
|
444
475
|
"""Get the shotgun instance ID from configuration.
|
|
445
476
|
|
|
446
477
|
Returns:
|
|
447
478
|
The unique shotgun instance ID string
|
|
448
479
|
"""
|
|
449
|
-
config = self.load()
|
|
480
|
+
config = await self.load()
|
|
450
481
|
return config.shotgun_instance_id
|
|
451
482
|
|
|
452
|
-
def update_shotgun_account(
|
|
483
|
+
async def update_shotgun_account(
|
|
453
484
|
self, api_key: str | None = None, supabase_jwt: str | None = None
|
|
454
485
|
) -> None:
|
|
455
486
|
"""Update Shotgun Account configuration.
|
|
@@ -458,7 +489,7 @@ class ConfigManager:
|
|
|
458
489
|
api_key: LiteLLM proxy API key (optional)
|
|
459
490
|
supabase_jwt: Supabase authentication JWT (optional)
|
|
460
491
|
"""
|
|
461
|
-
config = self.load()
|
|
492
|
+
config = await self.load()
|
|
462
493
|
|
|
463
494
|
if api_key is not None:
|
|
464
495
|
config.shotgun.api_key = SecretStr(api_key) if api_key else None
|
|
@@ -468,7 +499,7 @@ class ConfigManager:
|
|
|
468
499
|
SecretStr(supabase_jwt) if supabase_jwt else None
|
|
469
500
|
)
|
|
470
501
|
|
|
471
|
-
self.save(config)
|
|
502
|
+
await self.save(config)
|
|
472
503
|
logger.info("Updated Shotgun Account configuration")
|
|
473
504
|
|
|
474
505
|
|
shotgun/agents/config/models.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Pydantic models for configuration."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
from enum import StrEnum
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field, PrivateAttr, SecretStr
|
|
@@ -28,6 +29,7 @@ class ModelName(StrEnum):
|
|
|
28
29
|
GPT_5_MINI = "gpt-5-mini"
|
|
29
30
|
CLAUDE_OPUS_4_1 = "claude-opus-4-1"
|
|
30
31
|
CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
|
|
32
|
+
CLAUDE_HAIKU_4_5 = "claude-haiku-4-5"
|
|
31
33
|
GEMINI_2_5_PRO = "gemini-2.5-pro"
|
|
32
34
|
GEMINI_2_5_FLASH = "gemini-2.5-flash"
|
|
33
35
|
|
|
@@ -42,6 +44,7 @@ class ModelSpec(BaseModel):
|
|
|
42
44
|
litellm_proxy_model_name: (
|
|
43
45
|
str # LiteLLM format (e.g., "openai/gpt-5", "gemini/gemini-2-pro")
|
|
44
46
|
)
|
|
47
|
+
short_name: str # Display name for UI (e.g., "Sonnet 4.5", "GPT-5")
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
class ModelConfig(BaseModel):
|
|
@@ -88,6 +91,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
88
91
|
max_input_tokens=400_000,
|
|
89
92
|
max_output_tokens=128_000,
|
|
90
93
|
litellm_proxy_model_name="openai/gpt-5",
|
|
94
|
+
short_name="GPT-5",
|
|
91
95
|
),
|
|
92
96
|
ModelName.GPT_5_MINI: ModelSpec(
|
|
93
97
|
name=ModelName.GPT_5_MINI,
|
|
@@ -95,6 +99,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
95
99
|
max_input_tokens=400_000,
|
|
96
100
|
max_output_tokens=128_000,
|
|
97
101
|
litellm_proxy_model_name="openai/gpt-5-mini",
|
|
102
|
+
short_name="GPT-5 Mini",
|
|
98
103
|
),
|
|
99
104
|
ModelName.CLAUDE_OPUS_4_1: ModelSpec(
|
|
100
105
|
name=ModelName.CLAUDE_OPUS_4_1,
|
|
@@ -102,6 +107,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
102
107
|
max_input_tokens=200_000,
|
|
103
108
|
max_output_tokens=32_000,
|
|
104
109
|
litellm_proxy_model_name="anthropic/claude-opus-4-1",
|
|
110
|
+
short_name="Opus 4.1",
|
|
105
111
|
),
|
|
106
112
|
ModelName.CLAUDE_SONNET_4_5: ModelSpec(
|
|
107
113
|
name=ModelName.CLAUDE_SONNET_4_5,
|
|
@@ -109,6 +115,15 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
109
115
|
max_input_tokens=200_000,
|
|
110
116
|
max_output_tokens=16_000,
|
|
111
117
|
litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
|
|
118
|
+
short_name="Sonnet 4.5",
|
|
119
|
+
),
|
|
120
|
+
ModelName.CLAUDE_HAIKU_4_5: ModelSpec(
|
|
121
|
+
name=ModelName.CLAUDE_HAIKU_4_5,
|
|
122
|
+
provider=ProviderType.ANTHROPIC,
|
|
123
|
+
max_input_tokens=200_000,
|
|
124
|
+
max_output_tokens=64_000,
|
|
125
|
+
litellm_proxy_model_name="anthropic/claude-haiku-4-5",
|
|
126
|
+
short_name="Haiku 4.5",
|
|
112
127
|
),
|
|
113
128
|
ModelName.GEMINI_2_5_PRO: ModelSpec(
|
|
114
129
|
name=ModelName.GEMINI_2_5_PRO,
|
|
@@ -116,6 +131,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
116
131
|
max_input_tokens=1_000_000,
|
|
117
132
|
max_output_tokens=64_000,
|
|
118
133
|
litellm_proxy_model_name="gemini/gemini-2.5-pro",
|
|
134
|
+
short_name="Gemini 2.5 Pro",
|
|
119
135
|
),
|
|
120
136
|
ModelName.GEMINI_2_5_FLASH: ModelSpec(
|
|
121
137
|
name=ModelName.GEMINI_2_5_FLASH,
|
|
@@ -123,6 +139,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
123
139
|
max_input_tokens=1_000_000,
|
|
124
140
|
max_output_tokens=64_000,
|
|
125
141
|
litellm_proxy_model_name="gemini/gemini-2.5-flash",
|
|
142
|
+
short_name="Gemini 2.5 Flash",
|
|
126
143
|
),
|
|
127
144
|
}
|
|
128
145
|
|
|
@@ -154,6 +171,21 @@ class ShotgunAccountConfig(BaseModel):
|
|
|
154
171
|
)
|
|
155
172
|
|
|
156
173
|
|
|
174
|
+
class MarketingMessageRecord(BaseModel):
|
|
175
|
+
"""Record of when a marketing message was shown to the user."""
|
|
176
|
+
|
|
177
|
+
shown_at: datetime = Field(description="Timestamp when the message was shown")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MarketingConfig(BaseModel):
|
|
181
|
+
"""Configuration for marketing messages shown to users."""
|
|
182
|
+
|
|
183
|
+
messages: dict[str, MarketingMessageRecord] = Field(
|
|
184
|
+
default_factory=dict,
|
|
185
|
+
description="Tracking which marketing messages have been shown. Key is message ID (e.g., 'github_star_v1')",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
157
189
|
class ShotgunConfig(BaseModel):
|
|
158
190
|
"""Main configuration for Shotgun CLI."""
|
|
159
191
|
|
|
@@ -168,8 +200,16 @@ class ShotgunConfig(BaseModel):
|
|
|
168
200
|
shotgun_instance_id: str = Field(
|
|
169
201
|
description="Unique shotgun instance identifier (also used for anonymous telemetry)",
|
|
170
202
|
)
|
|
171
|
-
config_version: int = Field(default=
|
|
203
|
+
config_version: int = Field(default=4, description="Configuration schema version")
|
|
172
204
|
shown_welcome_screen: bool = Field(
|
|
173
205
|
default=False,
|
|
174
206
|
description="Whether the welcome screen has been shown to the user",
|
|
175
207
|
)
|
|
208
|
+
shown_onboarding_popup: datetime | None = Field(
|
|
209
|
+
default=None,
|
|
210
|
+
description="Timestamp when the onboarding popup was shown to the user (ISO8601 format)",
|
|
211
|
+
)
|
|
212
|
+
marketing: MarketingConfig = Field(
|
|
213
|
+
default_factory=MarketingConfig,
|
|
214
|
+
description="Marketing messages configuration and tracking",
|
|
215
|
+
)
|
|
@@ -32,6 +32,34 @@ logger = get_logger(__name__)
|
|
|
32
32
|
_model_cache: dict[tuple[ProviderType, KeyProvider, ModelName, str], Model] = {}
|
|
33
33
|
|
|
34
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
|
+
|
|
35
63
|
def get_or_create_model(
|
|
36
64
|
provider: ProviderType,
|
|
37
65
|
key_provider: "KeyProvider",
|
|
@@ -142,7 +170,7 @@ def get_or_create_model(
|
|
|
142
170
|
return _model_cache[cache_key]
|
|
143
171
|
|
|
144
172
|
|
|
145
|
-
def get_provider_model(
|
|
173
|
+
async def get_provider_model(
|
|
146
174
|
provider_or_model: ProviderType | ModelName | None = None,
|
|
147
175
|
) -> ModelConfig:
|
|
148
176
|
"""Get a fully configured ModelConfig with API key and Model instance.
|
|
@@ -161,7 +189,7 @@ def get_provider_model(
|
|
|
161
189
|
"""
|
|
162
190
|
config_manager = get_config_manager()
|
|
163
191
|
# Use cached config for read-only access (performance)
|
|
164
|
-
config = config_manager.load(force_reload=False)
|
|
192
|
+
config = await config_manager.load(force_reload=False)
|
|
165
193
|
|
|
166
194
|
# Priority 1: Check if Shotgun key exists - if so, use it for ANY model
|
|
167
195
|
shotgun_api_key = _get_api_key(config.shotgun.api_key)
|
|
@@ -172,7 +200,7 @@ def get_provider_model(
|
|
|
172
200
|
model_name = provider_or_model
|
|
173
201
|
else:
|
|
174
202
|
# No specific model requested - use selected or default
|
|
175
|
-
model_name = config.selected_model or ModelName.
|
|
203
|
+
model_name = config.selected_model or ModelName.GPT_5
|
|
176
204
|
|
|
177
205
|
if model_name not in MODEL_SPECS:
|
|
178
206
|
raise ValueError(f"Model '{model_name.value}' not found")
|
|
@@ -247,8 +275,8 @@ def get_provider_model(
|
|
|
247
275
|
if not api_key:
|
|
248
276
|
raise ValueError("Anthropic API key not configured. Set via config.")
|
|
249
277
|
|
|
250
|
-
# Use requested model or default to claude-
|
|
251
|
-
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
|
|
252
280
|
if model_name not in MODEL_SPECS:
|
|
253
281
|
raise ValueError(f"Model '{model_name.value}' not found")
|
|
254
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
|
+
]
|