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
shotgun/agents/common.py
CHANGED
|
@@ -18,7 +18,7 @@ from pydantic_ai.messages import (
|
|
|
18
18
|
ModelRequest,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
from shotgun.agents.config import ProviderType,
|
|
21
|
+
from shotgun.agents.config import ProviderType, get_provider_model
|
|
22
22
|
from shotgun.agents.models import AgentType
|
|
23
23
|
from shotgun.logging_config import get_logger
|
|
24
24
|
from shotgun.prompts import PromptLoader
|
|
@@ -115,14 +115,13 @@ def create_base_agent(
|
|
|
115
115
|
"""
|
|
116
116
|
ensure_shotgun_directory_exists()
|
|
117
117
|
|
|
118
|
-
# Get configured model or fall back to
|
|
118
|
+
# Get configured model or fall back to first available provider
|
|
119
119
|
try:
|
|
120
120
|
model_config = get_provider_model(provider)
|
|
121
|
-
|
|
122
|
-
provider_name = provider or config_manager.load().default_provider
|
|
121
|
+
provider_name = model_config.provider
|
|
123
122
|
logger.debug(
|
|
124
123
|
"🤖 Creating agent with configured %s model: %s",
|
|
125
|
-
provider_name.upper(),
|
|
124
|
+
provider_name.value.upper(),
|
|
126
125
|
model_config.name,
|
|
127
126
|
)
|
|
128
127
|
# Use the Model instance directly (has API key baked in)
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
"""Configuration constants for Shotgun agents."""
|
|
2
2
|
|
|
3
|
+
from enum import StrEnum, auto
|
|
4
|
+
|
|
3
5
|
# Field names
|
|
4
6
|
API_KEY_FIELD = "api_key"
|
|
5
|
-
DEFAULT_PROVIDER_FIELD = "default_provider"
|
|
6
7
|
USER_ID_FIELD = "user_id"
|
|
7
8
|
CONFIG_VERSION_FIELD = "config_version"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
class ConfigSection(StrEnum):
|
|
12
|
+
"""Configuration file section names (JSON keys)."""
|
|
13
|
+
|
|
14
|
+
OPENAI = auto()
|
|
15
|
+
ANTHROPIC = auto()
|
|
16
|
+
GOOGLE = auto()
|
|
17
|
+
SHOTGUN = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Backwards compatibility - deprecated
|
|
21
|
+
OPENAI_PROVIDER = ConfigSection.OPENAI.value
|
|
22
|
+
ANTHROPIC_PROVIDER = ConfigSection.ANTHROPIC.value
|
|
23
|
+
GOOGLE_PROVIDER = ConfigSection.GOOGLE.value
|
|
24
|
+
SHOTGUN_PROVIDER = ConfigSection.SHOTGUN.value
|
|
13
25
|
|
|
14
26
|
# Environment variable names
|
|
15
27
|
OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
|
|
16
28
|
ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
|
|
17
29
|
GEMINI_API_KEY_ENV = "GEMINI_API_KEY"
|
|
30
|
+
SHOTGUN_API_KEY_ENV = "SHOTGUN_API_KEY"
|
|
31
|
+
|
|
32
|
+
# Token limits
|
|
33
|
+
MEDIUM_TEXT_8K_TOKENS = 8192 # Default max_tokens for web search requests
|
shotgun/agents/config/manager.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Configuration manager for Shotgun CLI."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import os
|
|
5
4
|
import uuid
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Any
|
|
@@ -12,18 +11,24 @@ from shotgun.logging_config import get_logger
|
|
|
12
11
|
from shotgun.utils import get_shotgun_home
|
|
13
12
|
|
|
14
13
|
from .constants import (
|
|
15
|
-
ANTHROPIC_API_KEY_ENV,
|
|
16
|
-
ANTHROPIC_PROVIDER,
|
|
17
14
|
API_KEY_FIELD,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
ConfigSection,
|
|
16
|
+
)
|
|
17
|
+
from .models import (
|
|
18
|
+
AnthropicConfig,
|
|
19
|
+
GoogleConfig,
|
|
20
|
+
ModelName,
|
|
21
|
+
OpenAIConfig,
|
|
22
|
+
ProviderType,
|
|
23
|
+
ShotgunAccountConfig,
|
|
24
|
+
ShotgunConfig,
|
|
22
25
|
)
|
|
23
|
-
from .models import ProviderType, ShotgunConfig
|
|
24
26
|
|
|
25
27
|
logger = get_logger(__name__)
|
|
26
28
|
|
|
29
|
+
# Type alias for provider configuration objects
|
|
30
|
+
ProviderConfig = OpenAIConfig | AnthropicConfig | GoogleConfig | ShotgunAccountConfig
|
|
31
|
+
|
|
27
32
|
|
|
28
33
|
class ConfigManager:
|
|
29
34
|
"""Manager for Shotgun configuration."""
|
|
@@ -41,13 +46,16 @@ class ConfigManager:
|
|
|
41
46
|
|
|
42
47
|
self._config: ShotgunConfig | None = None
|
|
43
48
|
|
|
44
|
-
def load(self) -> ShotgunConfig:
|
|
49
|
+
def load(self, force_reload: bool = True) -> ShotgunConfig:
|
|
45
50
|
"""Load configuration from file.
|
|
46
51
|
|
|
52
|
+
Args:
|
|
53
|
+
force_reload: If True, reload from disk even if cached (default: True)
|
|
54
|
+
|
|
47
55
|
Returns:
|
|
48
56
|
ShotgunConfig: Loaded configuration or default config if file doesn't exist
|
|
49
57
|
"""
|
|
50
|
-
if self._config is not None:
|
|
58
|
+
if self._config is not None and not force_reload:
|
|
51
59
|
return self._config
|
|
52
60
|
|
|
53
61
|
if not self.config_path.exists():
|
|
@@ -69,20 +77,56 @@ class ConfigManager:
|
|
|
69
77
|
self._config = ShotgunConfig.model_validate(data)
|
|
70
78
|
logger.debug("Configuration loaded successfully from %s", self.config_path)
|
|
71
79
|
|
|
72
|
-
#
|
|
73
|
-
if not self.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
# Validate selected_model if in BYOK mode (no Shotgun key)
|
|
81
|
+
if not self._provider_has_api_key(self._config.shotgun):
|
|
82
|
+
should_save = False
|
|
83
|
+
|
|
84
|
+
# If selected_model is set, verify its provider has a key
|
|
85
|
+
if self._config.selected_model:
|
|
86
|
+
from .models import MODEL_SPECS
|
|
87
|
+
|
|
88
|
+
if self._config.selected_model in MODEL_SPECS:
|
|
89
|
+
spec = MODEL_SPECS[self._config.selected_model]
|
|
90
|
+
if not self.has_provider_key(spec.provider):
|
|
91
|
+
logger.info(
|
|
92
|
+
"Selected model %s provider has no API key, finding available model",
|
|
93
|
+
self._config.selected_model.value,
|
|
94
|
+
)
|
|
95
|
+
self._config.selected_model = None
|
|
96
|
+
should_save = True
|
|
97
|
+
else:
|
|
78
98
|
logger.info(
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
provider.value,
|
|
99
|
+
"Selected model %s not found in MODEL_SPECS, resetting",
|
|
100
|
+
self._config.selected_model.value,
|
|
82
101
|
)
|
|
83
|
-
self._config.
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
self._config.selected_model = None
|
|
103
|
+
should_save = True
|
|
104
|
+
|
|
105
|
+
# If no selected_model or it was invalid, find first available model
|
|
106
|
+
if not self._config.selected_model:
|
|
107
|
+
for provider in ProviderType:
|
|
108
|
+
if self.has_provider_key(provider):
|
|
109
|
+
# Set to that provider's default model
|
|
110
|
+
from .models import MODEL_SPECS, ModelName
|
|
111
|
+
|
|
112
|
+
# Find default model for this provider
|
|
113
|
+
provider_models = {
|
|
114
|
+
ProviderType.OPENAI: ModelName.GPT_5,
|
|
115
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
|
|
116
|
+
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if provider in provider_models:
|
|
120
|
+
self._config.selected_model = provider_models[provider]
|
|
121
|
+
logger.info(
|
|
122
|
+
"Set selected_model to %s (first available provider)",
|
|
123
|
+
self._config.selected_model.value,
|
|
124
|
+
)
|
|
125
|
+
should_save = True
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if should_save:
|
|
129
|
+
self.save(self._config)
|
|
86
130
|
|
|
87
131
|
return self._config
|
|
88
132
|
|
|
@@ -107,7 +151,6 @@ class ConfigManager:
|
|
|
107
151
|
# Create a new config with generated user_id
|
|
108
152
|
config = ShotgunConfig(
|
|
109
153
|
user_id=str(uuid.uuid4()),
|
|
110
|
-
config_version=1,
|
|
111
154
|
)
|
|
112
155
|
|
|
113
156
|
# Ensure directory exists
|
|
@@ -136,8 +179,13 @@ class ConfigManager:
|
|
|
136
179
|
**kwargs: Configuration fields to update (only api_key supported)
|
|
137
180
|
"""
|
|
138
181
|
config = self.load()
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
|
|
183
|
+
# Get provider config and check if it's shotgun
|
|
184
|
+
provider_config, is_shotgun = self._get_provider_config_and_type(
|
|
185
|
+
config, provider
|
|
186
|
+
)
|
|
187
|
+
# For non-shotgun providers, we need the enum for default provider logic
|
|
188
|
+
provider_enum = None if is_shotgun else self._ensure_provider_enum(provider)
|
|
141
189
|
|
|
142
190
|
# Only support api_key updates
|
|
143
191
|
if API_KEY_FIELD in kwargs:
|
|
@@ -152,50 +200,65 @@ class ConfigManager:
|
|
|
152
200
|
raise ValueError(f"Unsupported configuration fields: {unsupported_fields}")
|
|
153
201
|
|
|
154
202
|
# If no other providers have keys configured and we just added one,
|
|
155
|
-
# set
|
|
156
|
-
if API_KEY_FIELD in kwargs and api_key_value is not None:
|
|
203
|
+
# set selected_model to that provider's default model (only for LLM providers, not shotgun)
|
|
204
|
+
if not is_shotgun and API_KEY_FIELD in kwargs and api_key_value is not None:
|
|
205
|
+
# provider_enum is guaranteed to be non-None here since is_shotgun is False
|
|
206
|
+
if provider_enum is None:
|
|
207
|
+
raise RuntimeError("Provider enum should not be None for LLM providers")
|
|
157
208
|
other_providers = [p for p in ProviderType if p != provider_enum]
|
|
158
209
|
has_other_keys = any(self.has_provider_key(p) for p in other_providers)
|
|
159
210
|
if not has_other_keys:
|
|
160
|
-
|
|
211
|
+
# Set selected_model to this provider's default model
|
|
212
|
+
from .models import ModelName
|
|
213
|
+
|
|
214
|
+
provider_models = {
|
|
215
|
+
ProviderType.OPENAI: ModelName.GPT_5,
|
|
216
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
|
|
217
|
+
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
218
|
+
}
|
|
219
|
+
if provider_enum in provider_models:
|
|
220
|
+
config.selected_model = provider_models[provider_enum]
|
|
161
221
|
|
|
162
222
|
self.save(config)
|
|
163
223
|
|
|
164
224
|
def clear_provider_key(self, provider: ProviderType | str) -> None:
|
|
165
|
-
"""Remove the API key for the given provider."""
|
|
225
|
+
"""Remove the API key for the given provider (LLM provider or shotgun)."""
|
|
166
226
|
config = self.load()
|
|
167
|
-
|
|
168
|
-
|
|
227
|
+
|
|
228
|
+
# Get provider config (shotgun or LLM provider)
|
|
229
|
+
provider_config, _ = self._get_provider_config_and_type(config, provider)
|
|
230
|
+
|
|
169
231
|
provider_config.api_key = None
|
|
170
232
|
self.save(config)
|
|
171
233
|
|
|
234
|
+
def update_selected_model(self, model_name: "ModelName") -> None:
|
|
235
|
+
"""Update the selected model.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
model_name: Model to select
|
|
239
|
+
"""
|
|
240
|
+
config = self.load()
|
|
241
|
+
config.selected_model = model_name
|
|
242
|
+
self.save(config)
|
|
243
|
+
|
|
172
244
|
def has_provider_key(self, provider: ProviderType | str) -> bool:
|
|
173
245
|
"""Check if the given provider has a non-empty API key configured.
|
|
174
246
|
|
|
175
|
-
This checks
|
|
247
|
+
This checks only the configuration file.
|
|
176
248
|
"""
|
|
177
|
-
|
|
249
|
+
# Use force_reload=False to avoid infinite loop when called from load()
|
|
250
|
+
config = self.load(force_reload=False)
|
|
178
251
|
provider_enum = self._ensure_provider_enum(provider)
|
|
179
252
|
provider_config = self._get_provider_config(config, provider_enum)
|
|
180
253
|
|
|
181
|
-
|
|
182
|
-
if self._provider_has_api_key(provider_config):
|
|
183
|
-
return True
|
|
184
|
-
|
|
185
|
-
# Check environment variable
|
|
186
|
-
if provider_enum == ProviderType.OPENAI:
|
|
187
|
-
return bool(os.getenv(OPENAI_API_KEY_ENV))
|
|
188
|
-
elif provider_enum == ProviderType.ANTHROPIC:
|
|
189
|
-
return bool(os.getenv(ANTHROPIC_API_KEY_ENV))
|
|
190
|
-
elif provider_enum == ProviderType.GOOGLE:
|
|
191
|
-
return bool(os.getenv(GEMINI_API_KEY_ENV))
|
|
192
|
-
|
|
193
|
-
return False
|
|
254
|
+
return self._provider_has_api_key(provider_config)
|
|
194
255
|
|
|
195
256
|
def has_any_provider_key(self) -> bool:
|
|
196
257
|
"""Determine whether any provider has a configured API key."""
|
|
197
|
-
|
|
198
|
-
|
|
258
|
+
# Use force_reload=False to avoid infinite loop when called from load()
|
|
259
|
+
config = self.load(force_reload=False)
|
|
260
|
+
# Check LLM provider keys (BYOK)
|
|
261
|
+
has_llm_key = any(
|
|
199
262
|
self._provider_has_api_key(self._get_provider_config(config, provider))
|
|
200
263
|
for provider in (
|
|
201
264
|
ProviderType.OPENAI,
|
|
@@ -203,6 +266,9 @@ class ConfigManager:
|
|
|
203
266
|
ProviderType.GOOGLE,
|
|
204
267
|
)
|
|
205
268
|
)
|
|
269
|
+
# Also check Shotgun Account key
|
|
270
|
+
has_shotgun_key = self._provider_has_api_key(config.shotgun)
|
|
271
|
+
return has_llm_key or has_shotgun_key
|
|
206
272
|
|
|
207
273
|
def initialize(self) -> ShotgunConfig:
|
|
208
274
|
"""Initialize configuration with defaults and save to file.
|
|
@@ -213,7 +279,6 @@ class ConfigManager:
|
|
|
213
279
|
# Generate unique user ID for new config
|
|
214
280
|
config = ShotgunConfig(
|
|
215
281
|
user_id=str(uuid.uuid4()),
|
|
216
|
-
config_version=1,
|
|
217
282
|
)
|
|
218
283
|
self.save(config)
|
|
219
284
|
logger.info(
|
|
@@ -225,26 +290,26 @@ class ConfigManager:
|
|
|
225
290
|
|
|
226
291
|
def _convert_secrets_to_secretstr(self, data: dict[str, Any]) -> None:
|
|
227
292
|
"""Convert plain text secrets in data to SecretStr objects."""
|
|
228
|
-
for
|
|
229
|
-
if
|
|
293
|
+
for section in ConfigSection:
|
|
294
|
+
if section.value in data and isinstance(data[section.value], dict):
|
|
230
295
|
if (
|
|
231
|
-
API_KEY_FIELD in data[
|
|
232
|
-
and data[
|
|
296
|
+
API_KEY_FIELD in data[section.value]
|
|
297
|
+
and data[section.value][API_KEY_FIELD] is not None
|
|
233
298
|
):
|
|
234
|
-
data[
|
|
235
|
-
data[
|
|
299
|
+
data[section.value][API_KEY_FIELD] = SecretStr(
|
|
300
|
+
data[section.value][API_KEY_FIELD]
|
|
236
301
|
)
|
|
237
302
|
|
|
238
303
|
def _convert_secretstr_to_plain(self, data: dict[str, Any]) -> None:
|
|
239
304
|
"""Convert SecretStr objects in data to plain text for JSON serialization."""
|
|
240
|
-
for
|
|
241
|
-
if
|
|
305
|
+
for section in ConfigSection:
|
|
306
|
+
if section.value in data and isinstance(data[section.value], dict):
|
|
242
307
|
if (
|
|
243
|
-
API_KEY_FIELD in data[
|
|
244
|
-
and data[
|
|
308
|
+
API_KEY_FIELD in data[section.value]
|
|
309
|
+
and data[section.value][API_KEY_FIELD] is not None
|
|
245
310
|
):
|
|
246
|
-
if hasattr(data[
|
|
247
|
-
data[
|
|
311
|
+
if hasattr(data[section.value][API_KEY_FIELD], "get_secret_value"):
|
|
312
|
+
data[section.value][API_KEY_FIELD] = data[section.value][
|
|
248
313
|
API_KEY_FIELD
|
|
249
314
|
].get_secret_value()
|
|
250
315
|
|
|
@@ -279,6 +344,38 @@ class ConfigManager:
|
|
|
279
344
|
|
|
280
345
|
return bool(value.strip())
|
|
281
346
|
|
|
347
|
+
def _is_shotgun_provider(self, provider: ProviderType | str) -> bool:
|
|
348
|
+
"""Check if provider string represents Shotgun Account.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
provider: Provider type or string
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
True if provider is shotgun account
|
|
355
|
+
"""
|
|
356
|
+
return (
|
|
357
|
+
isinstance(provider, str)
|
|
358
|
+
and provider.lower() == ConfigSection.SHOTGUN.value
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _get_provider_config_and_type(
|
|
362
|
+
self, config: ShotgunConfig, provider: ProviderType | str
|
|
363
|
+
) -> tuple[ProviderConfig, bool]:
|
|
364
|
+
"""Get provider config, handling shotgun as special case.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
config: Shotgun configuration
|
|
368
|
+
provider: Provider type or string
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Tuple of (provider_config, is_shotgun)
|
|
372
|
+
"""
|
|
373
|
+
if self._is_shotgun_provider(provider):
|
|
374
|
+
return (config.shotgun, True)
|
|
375
|
+
|
|
376
|
+
provider_enum = self._ensure_provider_enum(provider)
|
|
377
|
+
return (self._get_provider_config(config, provider_enum), False)
|
|
378
|
+
|
|
282
379
|
def get_user_id(self) -> str:
|
|
283
380
|
"""Get the user ID from configuration.
|
|
284
381
|
|
|
@@ -289,6 +386,17 @@ class ConfigManager:
|
|
|
289
386
|
return config.user_id
|
|
290
387
|
|
|
291
388
|
|
|
389
|
+
# Global singleton instance
|
|
390
|
+
_config_manager_instance: ConfigManager | None = None
|
|
391
|
+
|
|
392
|
+
|
|
292
393
|
def get_config_manager() -> ConfigManager:
|
|
293
|
-
"""Get the global ConfigManager instance.
|
|
294
|
-
|
|
394
|
+
"""Get the global singleton ConfigManager instance.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
The singleton ConfigManager instance
|
|
398
|
+
"""
|
|
399
|
+
global _config_manager_instance
|
|
400
|
+
if _config_manager_instance is None:
|
|
401
|
+
_config_manager_instance = ConfigManager()
|
|
402
|
+
return _config_manager_instance
|
shotgun/agents/config/models.py
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
"""Pydantic models for configuration."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
4
|
-
from typing import Any
|
|
3
|
+
from enum import StrEnum
|
|
5
4
|
|
|
6
5
|
from pydantic import BaseModel, Field, PrivateAttr, SecretStr
|
|
7
|
-
from pydantic_ai.direct import model_request
|
|
8
|
-
from pydantic_ai.messages import ModelMessage, ModelResponse
|
|
9
6
|
from pydantic_ai.models import Model
|
|
10
|
-
from pydantic_ai.settings import ModelSettings
|
|
11
7
|
|
|
12
8
|
|
|
13
|
-
class ProviderType(
|
|
9
|
+
class ProviderType(StrEnum):
|
|
14
10
|
"""Provider types for AI services."""
|
|
15
11
|
|
|
16
12
|
OPENAI = "openai"
|
|
@@ -18,20 +14,42 @@ class ProviderType(str, Enum):
|
|
|
18
14
|
GOOGLE = "google"
|
|
19
15
|
|
|
20
16
|
|
|
17
|
+
class KeyProvider(StrEnum):
|
|
18
|
+
"""Authentication method for accessing AI models."""
|
|
19
|
+
|
|
20
|
+
BYOK = "byok" # Bring Your Own Key (individual provider keys)
|
|
21
|
+
SHOTGUN = "shotgun" # Shotgun Account (unified LiteLLM proxy)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModelName(StrEnum):
|
|
25
|
+
"""Available AI model names."""
|
|
26
|
+
|
|
27
|
+
GPT_5 = "gpt-5"
|
|
28
|
+
GPT_5_MINI = "gpt-5-mini"
|
|
29
|
+
CLAUDE_OPUS_4_1 = "claude-opus-4-1"
|
|
30
|
+
CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
|
|
31
|
+
GEMINI_2_5_PRO = "gemini-2.5-pro"
|
|
32
|
+
GEMINI_2_5_FLASH = "gemini-2.5-flash"
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
class ModelSpec(BaseModel):
|
|
22
36
|
"""Static specification for a model - just metadata."""
|
|
23
37
|
|
|
24
|
-
name:
|
|
38
|
+
name: ModelName # Model identifier
|
|
25
39
|
provider: ProviderType
|
|
26
40
|
max_input_tokens: int
|
|
27
41
|
max_output_tokens: int
|
|
42
|
+
litellm_proxy_model_name: (
|
|
43
|
+
str # LiteLLM format (e.g., "openai/gpt-5", "gemini/gemini-2-pro")
|
|
44
|
+
)
|
|
28
45
|
|
|
29
46
|
|
|
30
47
|
class ModelConfig(BaseModel):
|
|
31
48
|
"""A fully configured model with API key and settings."""
|
|
32
49
|
|
|
33
|
-
name:
|
|
34
|
-
provider: ProviderType
|
|
50
|
+
name: ModelName # Model identifier
|
|
51
|
+
provider: ProviderType # Actual LLM provider (openai, anthropic, google)
|
|
52
|
+
key_provider: KeyProvider # Authentication method (byok or shotgun)
|
|
35
53
|
max_input_tokens: int
|
|
36
54
|
max_output_tokens: int
|
|
37
55
|
api_key: str
|
|
@@ -47,7 +65,7 @@ class ModelConfig(BaseModel):
|
|
|
47
65
|
from .provider import get_or_create_model
|
|
48
66
|
|
|
49
67
|
self._model_instance = get_or_create_model(
|
|
50
|
-
self.provider, self.name, self.api_key
|
|
68
|
+
self.provider, self.key_provider, self.name, self.api_key
|
|
51
69
|
)
|
|
52
70
|
return self._model_instance
|
|
53
71
|
|
|
@@ -61,54 +79,50 @@ class ModelConfig(BaseModel):
|
|
|
61
79
|
}
|
|
62
80
|
return f"{provider_prefix[self.provider]}:{self.name}"
|
|
63
81
|
|
|
64
|
-
def get_model_settings(self, max_tokens: int | None = None) -> ModelSettings:
|
|
65
|
-
"""Get ModelSettings with optional token override.
|
|
66
|
-
|
|
67
|
-
This provides flexibility for specific use cases that need different
|
|
68
|
-
token limits while defaulting to maximum utilization.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
max_tokens: Optional override for max_tokens. If None, uses max_output_tokens
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
ModelSettings configured with specified or maximum tokens
|
|
75
|
-
"""
|
|
76
|
-
return ModelSettings(
|
|
77
|
-
max_tokens=max_tokens if max_tokens is not None else self.max_output_tokens
|
|
78
|
-
)
|
|
79
|
-
|
|
80
82
|
|
|
81
83
|
# Model specifications registry (static metadata)
|
|
82
|
-
MODEL_SPECS: dict[
|
|
83
|
-
|
|
84
|
-
name=
|
|
84
|
+
MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
85
|
+
ModelName.GPT_5: ModelSpec(
|
|
86
|
+
name=ModelName.GPT_5,
|
|
85
87
|
provider=ProviderType.OPENAI,
|
|
86
88
|
max_input_tokens=400_000,
|
|
87
89
|
max_output_tokens=128_000,
|
|
90
|
+
litellm_proxy_model_name="openai/gpt-5",
|
|
88
91
|
),
|
|
89
|
-
|
|
90
|
-
name=
|
|
92
|
+
ModelName.GPT_5_MINI: ModelSpec(
|
|
93
|
+
name=ModelName.GPT_5_MINI,
|
|
91
94
|
provider=ProviderType.OPENAI,
|
|
92
|
-
max_input_tokens=
|
|
93
|
-
max_output_tokens=
|
|
95
|
+
max_input_tokens=400_000,
|
|
96
|
+
max_output_tokens=128_000,
|
|
97
|
+
litellm_proxy_model_name="openai/gpt-5-mini",
|
|
94
98
|
),
|
|
95
|
-
|
|
96
|
-
name=
|
|
99
|
+
ModelName.CLAUDE_OPUS_4_1: ModelSpec(
|
|
100
|
+
name=ModelName.CLAUDE_OPUS_4_1,
|
|
97
101
|
provider=ProviderType.ANTHROPIC,
|
|
98
102
|
max_input_tokens=200_000,
|
|
99
103
|
max_output_tokens=32_000,
|
|
104
|
+
litellm_proxy_model_name="anthropic/claude-opus-4-1",
|
|
100
105
|
),
|
|
101
|
-
|
|
102
|
-
name=
|
|
106
|
+
ModelName.CLAUDE_SONNET_4_5: ModelSpec(
|
|
107
|
+
name=ModelName.CLAUDE_SONNET_4_5,
|
|
103
108
|
provider=ProviderType.ANTHROPIC,
|
|
104
109
|
max_input_tokens=200_000,
|
|
105
|
-
max_output_tokens=
|
|
110
|
+
max_output_tokens=16_000,
|
|
111
|
+
litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
|
|
112
|
+
),
|
|
113
|
+
ModelName.GEMINI_2_5_PRO: ModelSpec(
|
|
114
|
+
name=ModelName.GEMINI_2_5_PRO,
|
|
115
|
+
provider=ProviderType.GOOGLE,
|
|
116
|
+
max_input_tokens=1_000_000,
|
|
117
|
+
max_output_tokens=64_000,
|
|
118
|
+
litellm_proxy_model_name="gemini/gemini-2.5-pro",
|
|
106
119
|
),
|
|
107
|
-
|
|
108
|
-
name=
|
|
120
|
+
ModelName.GEMINI_2_5_FLASH: ModelSpec(
|
|
121
|
+
name=ModelName.GEMINI_2_5_FLASH,
|
|
109
122
|
provider=ProviderType.GOOGLE,
|
|
110
123
|
max_input_tokens=1_000_000,
|
|
111
124
|
max_output_tokens=64_000,
|
|
125
|
+
litellm_proxy_model_name="gemini/gemini-2.5-flash",
|
|
112
126
|
),
|
|
113
127
|
}
|
|
114
128
|
|
|
@@ -131,55 +145,22 @@ class GoogleConfig(BaseModel):
|
|
|
131
145
|
api_key: SecretStr | None = None
|
|
132
146
|
|
|
133
147
|
|
|
148
|
+
class ShotgunAccountConfig(BaseModel):
|
|
149
|
+
"""Configuration for Shotgun Account (LiteLLM proxy)."""
|
|
150
|
+
|
|
151
|
+
api_key: SecretStr | None = None
|
|
152
|
+
|
|
153
|
+
|
|
134
154
|
class ShotgunConfig(BaseModel):
|
|
135
155
|
"""Main configuration for Shotgun CLI."""
|
|
136
156
|
|
|
137
157
|
openai: OpenAIConfig = Field(default_factory=OpenAIConfig)
|
|
138
158
|
anthropic: AnthropicConfig = Field(default_factory=AnthropicConfig)
|
|
139
159
|
google: GoogleConfig = Field(default_factory=GoogleConfig)
|
|
140
|
-
|
|
141
|
-
|
|
160
|
+
shotgun: ShotgunAccountConfig = Field(default_factory=ShotgunAccountConfig)
|
|
161
|
+
selected_model: ModelName | None = Field(
|
|
162
|
+
default=None,
|
|
163
|
+
description="User-selected model",
|
|
142
164
|
)
|
|
143
165
|
user_id: str = Field(description="Unique anonymous user identifier")
|
|
144
|
-
config_version: int = Field(default=
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
async def shotgun_model_request(
|
|
148
|
-
model_config: ModelConfig,
|
|
149
|
-
messages: list[ModelMessage],
|
|
150
|
-
max_tokens: int | None = None,
|
|
151
|
-
**kwargs: Any,
|
|
152
|
-
) -> ModelResponse:
|
|
153
|
-
"""Model request wrapper that uses full token capacity by default.
|
|
154
|
-
|
|
155
|
-
This wrapper ensures all LLM calls in Shotgun use the maximum available
|
|
156
|
-
token capacity of each model, improving response quality and completeness.
|
|
157
|
-
The most common issue this fixes is truncated summaries that were cut off
|
|
158
|
-
at default token limits (e.g., 4096 for Claude models).
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
model_config: ModelConfig instance with model settings and API key
|
|
162
|
-
messages: Messages to send to the model
|
|
163
|
-
max_tokens: Optional override for max_tokens. If None, uses model's max_output_tokens
|
|
164
|
-
**kwargs: Additional arguments passed to model_request
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
ModelResponse from the model
|
|
168
|
-
|
|
169
|
-
Example:
|
|
170
|
-
# Uses full token capacity (e.g., 4096 for Claude, 128k for GPT-5)
|
|
171
|
-
response = await shotgun_model_request(model_config, messages)
|
|
172
|
-
|
|
173
|
-
# Override for specific use case
|
|
174
|
-
response = await shotgun_model_request(model_config, messages, max_tokens=1000)
|
|
175
|
-
"""
|
|
176
|
-
# Get properly configured ModelSettings with maximum or overridden token limit
|
|
177
|
-
model_settings = model_config.get_model_settings(max_tokens)
|
|
178
|
-
|
|
179
|
-
# Make the model request with full token utilization
|
|
180
|
-
return await model_request(
|
|
181
|
-
model=model_config.model_instance,
|
|
182
|
-
messages=messages,
|
|
183
|
-
model_settings=model_settings,
|
|
184
|
-
**kwargs,
|
|
185
|
-
)
|
|
166
|
+
config_version: int = Field(default=2, description="Configuration schema version")
|