shotgun-sh 0.1.16.dev2__py3-none-any.whl → 0.2.1__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 +23 -6
- shotgun/agents/config/manager.py +239 -76
- shotgun/agents/config/models.py +74 -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 +34 -63
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +2 -2
- shotgun/codebase/core/ingestor.py +47 -8
- shotgun/codebase/core/manager.py +7 -3
- 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 +15 -11
- shotgun/sentry_telemetry.py +3 -3
- shotgun/shotgun_web/__init__.py +19 -0
- shotgun/shotgun_web/client.py +138 -0
- shotgun/shotgun_web/constants.py +17 -0
- shotgun/shotgun_web/models.py +47 -0
- shotgun/telemetry.py +7 -4
- shotgun/tui/app.py +26 -8
- shotgun/tui/screens/chat.py +2 -8
- shotgun/tui/screens/chat_screen/command_providers.py +118 -11
- shotgun/tui/screens/chat_screen/history.py +3 -1
- shotgun/tui/screens/feedback.py +2 -2
- shotgun/tui/screens/model_picker.py +327 -0
- shotgun/tui/screens/provider_config.py +118 -28
- shotgun/tui/screens/shotgun_auth.py +295 -0
- shotgun/tui/screens/welcome.py +176 -0
- shotgun/utils/env_utils.py +12 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/METADATA +2 -2
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/RECORD +54 -37
- shotgun/agents/history/token_counting.py +0 -429
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.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,34 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
+
SUPABASE_JWT_FIELD = "supabase_jwt"
|
|
8
|
+
SHOTGUN_INSTANCE_ID_FIELD = "shotgun_instance_id"
|
|
7
9
|
CONFIG_VERSION_FIELD = "config_version"
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
class ConfigSection(StrEnum):
|
|
13
|
+
"""Configuration file section names (JSON keys)."""
|
|
14
|
+
|
|
15
|
+
OPENAI = auto()
|
|
16
|
+
ANTHROPIC = auto()
|
|
17
|
+
GOOGLE = auto()
|
|
18
|
+
SHOTGUN = auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Backwards compatibility - deprecated
|
|
22
|
+
OPENAI_PROVIDER = ConfigSection.OPENAI.value
|
|
23
|
+
ANTHROPIC_PROVIDER = ConfigSection.ANTHROPIC.value
|
|
24
|
+
GOOGLE_PROVIDER = ConfigSection.GOOGLE.value
|
|
25
|
+
SHOTGUN_PROVIDER = ConfigSection.SHOTGUN.value
|
|
13
26
|
|
|
14
27
|
# Environment variable names
|
|
15
28
|
OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
|
|
16
29
|
ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
|
|
17
30
|
GEMINI_API_KEY_ENV = "GEMINI_API_KEY"
|
|
31
|
+
SHOTGUN_API_KEY_ENV = "SHOTGUN_API_KEY"
|
|
32
|
+
|
|
33
|
+
# Token limits
|
|
34
|
+
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,26 @@ 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
|
+
SHOTGUN_INSTANCE_ID_FIELD,
|
|
16
|
+
SUPABASE_JWT_FIELD,
|
|
17
|
+
ConfigSection,
|
|
18
|
+
)
|
|
19
|
+
from .models import (
|
|
20
|
+
AnthropicConfig,
|
|
21
|
+
GoogleConfig,
|
|
22
|
+
ModelName,
|
|
23
|
+
OpenAIConfig,
|
|
24
|
+
ProviderType,
|
|
25
|
+
ShotgunAccountConfig,
|
|
26
|
+
ShotgunConfig,
|
|
22
27
|
)
|
|
23
|
-
from .models import ProviderType, ShotgunConfig
|
|
24
28
|
|
|
25
29
|
logger = get_logger(__name__)
|
|
26
30
|
|
|
31
|
+
# Type alias for provider configuration objects
|
|
32
|
+
ProviderConfig = OpenAIConfig | AnthropicConfig | GoogleConfig | ShotgunAccountConfig
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
class ConfigManager:
|
|
29
36
|
"""Manager for Shotgun configuration."""
|
|
@@ -41,21 +48,24 @@ class ConfigManager:
|
|
|
41
48
|
|
|
42
49
|
self._config: ShotgunConfig | None = None
|
|
43
50
|
|
|
44
|
-
def load(self) -> ShotgunConfig:
|
|
51
|
+
def load(self, force_reload: bool = True) -> ShotgunConfig:
|
|
45
52
|
"""Load configuration from file.
|
|
46
53
|
|
|
54
|
+
Args:
|
|
55
|
+
force_reload: If True, reload from disk even if cached (default: True)
|
|
56
|
+
|
|
47
57
|
Returns:
|
|
48
58
|
ShotgunConfig: Loaded configuration or default config if file doesn't exist
|
|
49
59
|
"""
|
|
50
|
-
if self._config is not None:
|
|
60
|
+
if self._config is not None and not force_reload:
|
|
51
61
|
return self._config
|
|
52
62
|
|
|
53
63
|
if not self.config_path.exists():
|
|
54
64
|
logger.info(
|
|
55
|
-
"Configuration file not found, creating new config
|
|
65
|
+
"Configuration file not found, creating new config at: %s",
|
|
56
66
|
self.config_path,
|
|
57
67
|
)
|
|
58
|
-
# Create new config with generated
|
|
68
|
+
# Create new config with generated shotgun_instance_id
|
|
59
69
|
self._config = self.initialize()
|
|
60
70
|
return self._config
|
|
61
71
|
|
|
@@ -63,26 +73,70 @@ class ConfigManager:
|
|
|
63
73
|
with open(self.config_path, encoding="utf-8") as f:
|
|
64
74
|
data = json.load(f)
|
|
65
75
|
|
|
76
|
+
# Migration: Rename user_id to shotgun_instance_id (config v2 -> v3)
|
|
77
|
+
if "user_id" in data and SHOTGUN_INSTANCE_ID_FIELD not in data:
|
|
78
|
+
data[SHOTGUN_INSTANCE_ID_FIELD] = data.pop("user_id")
|
|
79
|
+
data["config_version"] = 3
|
|
80
|
+
logger.info(
|
|
81
|
+
"Migrated config v2->v3: renamed user_id to shotgun_instance_id"
|
|
82
|
+
)
|
|
83
|
+
|
|
66
84
|
# Convert plain text secrets to SecretStr objects
|
|
67
85
|
self._convert_secrets_to_secretstr(data)
|
|
68
86
|
|
|
69
87
|
self._config = ShotgunConfig.model_validate(data)
|
|
70
88
|
logger.debug("Configuration loaded successfully from %s", self.config_path)
|
|
71
89
|
|
|
72
|
-
#
|
|
73
|
-
if not self.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
# Validate selected_model if in BYOK mode (no Shotgun key)
|
|
91
|
+
if not self._provider_has_api_key(self._config.shotgun):
|
|
92
|
+
should_save = False
|
|
93
|
+
|
|
94
|
+
# If selected_model is set, verify its provider has a key
|
|
95
|
+
if self._config.selected_model:
|
|
96
|
+
from .models import MODEL_SPECS
|
|
97
|
+
|
|
98
|
+
if self._config.selected_model in MODEL_SPECS:
|
|
99
|
+
spec = MODEL_SPECS[self._config.selected_model]
|
|
100
|
+
if not self.has_provider_key(spec.provider):
|
|
101
|
+
logger.info(
|
|
102
|
+
"Selected model %s provider has no API key, finding available model",
|
|
103
|
+
self._config.selected_model.value,
|
|
104
|
+
)
|
|
105
|
+
self._config.selected_model = None
|
|
106
|
+
should_save = True
|
|
107
|
+
else:
|
|
78
108
|
logger.info(
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
provider.value,
|
|
109
|
+
"Selected model %s not found in MODEL_SPECS, resetting",
|
|
110
|
+
self._config.selected_model.value,
|
|
82
111
|
)
|
|
83
|
-
self._config.
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
self._config.selected_model = None
|
|
113
|
+
should_save = True
|
|
114
|
+
|
|
115
|
+
# If no selected_model or it was invalid, find first available model
|
|
116
|
+
if not self._config.selected_model:
|
|
117
|
+
for provider in ProviderType:
|
|
118
|
+
if self.has_provider_key(provider):
|
|
119
|
+
# Set to that provider's default model
|
|
120
|
+
from .models import MODEL_SPECS, ModelName
|
|
121
|
+
|
|
122
|
+
# Find default model for this provider
|
|
123
|
+
provider_models = {
|
|
124
|
+
ProviderType.OPENAI: ModelName.GPT_5,
|
|
125
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
|
|
126
|
+
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if provider in provider_models:
|
|
130
|
+
self._config.selected_model = provider_models[provider]
|
|
131
|
+
logger.info(
|
|
132
|
+
"Set selected_model to %s (first available provider)",
|
|
133
|
+
self._config.selected_model.value,
|
|
134
|
+
)
|
|
135
|
+
should_save = True
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
if should_save:
|
|
139
|
+
self.save(self._config)
|
|
86
140
|
|
|
87
141
|
return self._config
|
|
88
142
|
|
|
@@ -90,7 +144,7 @@ class ConfigManager:
|
|
|
90
144
|
logger.error(
|
|
91
145
|
"Failed to load configuration from %s: %s", self.config_path, e
|
|
92
146
|
)
|
|
93
|
-
logger.info("Creating new configuration with generated
|
|
147
|
+
logger.info("Creating new configuration with generated shotgun_instance_id")
|
|
94
148
|
self._config = self.initialize()
|
|
95
149
|
return self._config
|
|
96
150
|
|
|
@@ -104,10 +158,9 @@ class ConfigManager:
|
|
|
104
158
|
if self._config:
|
|
105
159
|
config = self._config
|
|
106
160
|
else:
|
|
107
|
-
# Create a new config with generated
|
|
161
|
+
# Create a new config with generated shotgun_instance_id
|
|
108
162
|
config = ShotgunConfig(
|
|
109
|
-
|
|
110
|
-
config_version=1,
|
|
163
|
+
shotgun_instance_id=str(uuid.uuid4()),
|
|
111
164
|
)
|
|
112
165
|
|
|
113
166
|
# Ensure directory exists
|
|
@@ -136,8 +189,13 @@ class ConfigManager:
|
|
|
136
189
|
**kwargs: Configuration fields to update (only api_key supported)
|
|
137
190
|
"""
|
|
138
191
|
config = self.load()
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
|
|
193
|
+
# Get provider config and check if it's shotgun
|
|
194
|
+
provider_config, is_shotgun = self._get_provider_config_and_type(
|
|
195
|
+
config, provider
|
|
196
|
+
)
|
|
197
|
+
# For non-shotgun providers, we need the enum for default provider logic
|
|
198
|
+
provider_enum = None if is_shotgun else self._ensure_provider_enum(provider)
|
|
141
199
|
|
|
142
200
|
# Only support api_key updates
|
|
143
201
|
if API_KEY_FIELD in kwargs:
|
|
@@ -152,50 +210,65 @@ class ConfigManager:
|
|
|
152
210
|
raise ValueError(f"Unsupported configuration fields: {unsupported_fields}")
|
|
153
211
|
|
|
154
212
|
# 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:
|
|
213
|
+
# set selected_model to that provider's default model (only for LLM providers, not shotgun)
|
|
214
|
+
if not is_shotgun and API_KEY_FIELD in kwargs and api_key_value is not None:
|
|
215
|
+
# provider_enum is guaranteed to be non-None here since is_shotgun is False
|
|
216
|
+
if provider_enum is None:
|
|
217
|
+
raise RuntimeError("Provider enum should not be None for LLM providers")
|
|
157
218
|
other_providers = [p for p in ProviderType if p != provider_enum]
|
|
158
219
|
has_other_keys = any(self.has_provider_key(p) for p in other_providers)
|
|
159
220
|
if not has_other_keys:
|
|
160
|
-
|
|
221
|
+
# Set selected_model to this provider's default model
|
|
222
|
+
from .models import ModelName
|
|
223
|
+
|
|
224
|
+
provider_models = {
|
|
225
|
+
ProviderType.OPENAI: ModelName.GPT_5,
|
|
226
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
|
|
227
|
+
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
228
|
+
}
|
|
229
|
+
if provider_enum in provider_models:
|
|
230
|
+
config.selected_model = provider_models[provider_enum]
|
|
161
231
|
|
|
162
232
|
self.save(config)
|
|
163
233
|
|
|
164
234
|
def clear_provider_key(self, provider: ProviderType | str) -> None:
|
|
165
|
-
"""Remove the API key for the given provider."""
|
|
235
|
+
"""Remove the API key for the given provider (LLM provider or shotgun)."""
|
|
166
236
|
config = self.load()
|
|
167
|
-
|
|
168
|
-
|
|
237
|
+
|
|
238
|
+
# Get provider config (shotgun or LLM provider)
|
|
239
|
+
provider_config, _ = self._get_provider_config_and_type(config, provider)
|
|
240
|
+
|
|
169
241
|
provider_config.api_key = None
|
|
170
242
|
self.save(config)
|
|
171
243
|
|
|
244
|
+
def update_selected_model(self, model_name: "ModelName") -> None:
|
|
245
|
+
"""Update the selected model.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
model_name: Model to select
|
|
249
|
+
"""
|
|
250
|
+
config = self.load()
|
|
251
|
+
config.selected_model = model_name
|
|
252
|
+
self.save(config)
|
|
253
|
+
|
|
172
254
|
def has_provider_key(self, provider: ProviderType | str) -> bool:
|
|
173
255
|
"""Check if the given provider has a non-empty API key configured.
|
|
174
256
|
|
|
175
|
-
This checks
|
|
257
|
+
This checks only the configuration file.
|
|
176
258
|
"""
|
|
177
|
-
|
|
259
|
+
# Use force_reload=False to avoid infinite loop when called from load()
|
|
260
|
+
config = self.load(force_reload=False)
|
|
178
261
|
provider_enum = self._ensure_provider_enum(provider)
|
|
179
262
|
provider_config = self._get_provider_config(config, provider_enum)
|
|
180
263
|
|
|
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
|
|
264
|
+
return self._provider_has_api_key(provider_config)
|
|
194
265
|
|
|
195
266
|
def has_any_provider_key(self) -> bool:
|
|
196
267
|
"""Determine whether any provider has a configured API key."""
|
|
197
|
-
|
|
198
|
-
|
|
268
|
+
# Use force_reload=False to avoid infinite loop when called from load()
|
|
269
|
+
config = self.load(force_reload=False)
|
|
270
|
+
# Check LLM provider keys (BYOK)
|
|
271
|
+
has_llm_key = any(
|
|
199
272
|
self._provider_has_api_key(self._get_provider_config(config, provider))
|
|
200
273
|
for provider in (
|
|
201
274
|
ProviderType.OPENAI,
|
|
@@ -203,6 +276,9 @@ class ConfigManager:
|
|
|
203
276
|
ProviderType.GOOGLE,
|
|
204
277
|
)
|
|
205
278
|
)
|
|
279
|
+
# Also check Shotgun Account key
|
|
280
|
+
has_shotgun_key = self._provider_has_api_key(config.shotgun)
|
|
281
|
+
return has_llm_key or has_shotgun_key
|
|
206
282
|
|
|
207
283
|
def initialize(self) -> ShotgunConfig:
|
|
208
284
|
"""Initialize configuration with defaults and save to file.
|
|
@@ -210,43 +286,65 @@ class ConfigManager:
|
|
|
210
286
|
Returns:
|
|
211
287
|
Default ShotgunConfig
|
|
212
288
|
"""
|
|
213
|
-
# Generate unique
|
|
289
|
+
# Generate unique shotgun instance ID for new config
|
|
214
290
|
config = ShotgunConfig(
|
|
215
|
-
|
|
216
|
-
config_version=1,
|
|
291
|
+
shotgun_instance_id=str(uuid.uuid4()),
|
|
217
292
|
)
|
|
218
293
|
self.save(config)
|
|
219
294
|
logger.info(
|
|
220
|
-
"Configuration initialized at %s with
|
|
295
|
+
"Configuration initialized at %s with shotgun_instance_id: %s",
|
|
221
296
|
self.config_path,
|
|
222
|
-
config.
|
|
297
|
+
config.shotgun_instance_id,
|
|
223
298
|
)
|
|
224
299
|
return config
|
|
225
300
|
|
|
226
301
|
def _convert_secrets_to_secretstr(self, data: dict[str, Any]) -> None:
|
|
227
302
|
"""Convert plain text secrets in data to SecretStr objects."""
|
|
228
|
-
for
|
|
229
|
-
if
|
|
303
|
+
for section in ConfigSection:
|
|
304
|
+
if section.value in data and isinstance(data[section.value], dict):
|
|
305
|
+
# Convert API key
|
|
230
306
|
if (
|
|
231
|
-
API_KEY_FIELD in data[
|
|
232
|
-
and data[
|
|
307
|
+
API_KEY_FIELD in data[section.value]
|
|
308
|
+
and data[section.value][API_KEY_FIELD] is not None
|
|
233
309
|
):
|
|
234
|
-
data[
|
|
235
|
-
data[
|
|
310
|
+
data[section.value][API_KEY_FIELD] = SecretStr(
|
|
311
|
+
data[section.value][API_KEY_FIELD]
|
|
312
|
+
)
|
|
313
|
+
# Convert supabase JWT (shotgun section only)
|
|
314
|
+
if (
|
|
315
|
+
section == ConfigSection.SHOTGUN
|
|
316
|
+
and SUPABASE_JWT_FIELD in data[section.value]
|
|
317
|
+
and data[section.value][SUPABASE_JWT_FIELD] is not None
|
|
318
|
+
):
|
|
319
|
+
data[section.value][SUPABASE_JWT_FIELD] = SecretStr(
|
|
320
|
+
data[section.value][SUPABASE_JWT_FIELD]
|
|
236
321
|
)
|
|
237
322
|
|
|
238
323
|
def _convert_secretstr_to_plain(self, data: dict[str, Any]) -> None:
|
|
239
324
|
"""Convert SecretStr objects in data to plain text for JSON serialization."""
|
|
240
|
-
for
|
|
241
|
-
if
|
|
325
|
+
for section in ConfigSection:
|
|
326
|
+
if section.value in data and isinstance(data[section.value], dict):
|
|
327
|
+
# Convert API key
|
|
242
328
|
if (
|
|
243
|
-
API_KEY_FIELD in data[
|
|
244
|
-
and data[
|
|
329
|
+
API_KEY_FIELD in data[section.value]
|
|
330
|
+
and data[section.value][API_KEY_FIELD] is not None
|
|
245
331
|
):
|
|
246
|
-
if hasattr(data[
|
|
247
|
-
data[
|
|
332
|
+
if hasattr(data[section.value][API_KEY_FIELD], "get_secret_value"):
|
|
333
|
+
data[section.value][API_KEY_FIELD] = data[section.value][
|
|
248
334
|
API_KEY_FIELD
|
|
249
335
|
].get_secret_value()
|
|
336
|
+
# Convert supabase JWT (shotgun section only)
|
|
337
|
+
if (
|
|
338
|
+
section == ConfigSection.SHOTGUN
|
|
339
|
+
and SUPABASE_JWT_FIELD in data[section.value]
|
|
340
|
+
and data[section.value][SUPABASE_JWT_FIELD] is not None
|
|
341
|
+
):
|
|
342
|
+
if hasattr(
|
|
343
|
+
data[section.value][SUPABASE_JWT_FIELD], "get_secret_value"
|
|
344
|
+
):
|
|
345
|
+
data[section.value][SUPABASE_JWT_FIELD] = data[section.value][
|
|
346
|
+
SUPABASE_JWT_FIELD
|
|
347
|
+
].get_secret_value()
|
|
250
348
|
|
|
251
349
|
def _ensure_provider_enum(self, provider: ProviderType | str) -> ProviderType:
|
|
252
350
|
"""Normalize provider values to ProviderType enum."""
|
|
@@ -279,16 +377,81 @@ class ConfigManager:
|
|
|
279
377
|
|
|
280
378
|
return bool(value.strip())
|
|
281
379
|
|
|
282
|
-
def
|
|
283
|
-
"""
|
|
380
|
+
def _is_shotgun_provider(self, provider: ProviderType | str) -> bool:
|
|
381
|
+
"""Check if provider string represents Shotgun Account.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
provider: Provider type or string
|
|
284
385
|
|
|
285
386
|
Returns:
|
|
286
|
-
|
|
387
|
+
True if provider is shotgun account
|
|
388
|
+
"""
|
|
389
|
+
return (
|
|
390
|
+
isinstance(provider, str)
|
|
391
|
+
and provider.lower() == ConfigSection.SHOTGUN.value
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def _get_provider_config_and_type(
|
|
395
|
+
self, config: ShotgunConfig, provider: ProviderType | str
|
|
396
|
+
) -> tuple[ProviderConfig, bool]:
|
|
397
|
+
"""Get provider config, handling shotgun as special case.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
config: Shotgun configuration
|
|
401
|
+
provider: Provider type or string
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Tuple of (provider_config, is_shotgun)
|
|
405
|
+
"""
|
|
406
|
+
if self._is_shotgun_provider(provider):
|
|
407
|
+
return (config.shotgun, True)
|
|
408
|
+
|
|
409
|
+
provider_enum = self._ensure_provider_enum(provider)
|
|
410
|
+
return (self._get_provider_config(config, provider_enum), False)
|
|
411
|
+
|
|
412
|
+
def get_shotgun_instance_id(self) -> str:
|
|
413
|
+
"""Get the shotgun instance ID from configuration.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
The unique shotgun instance ID string
|
|
417
|
+
"""
|
|
418
|
+
config = self.load()
|
|
419
|
+
return config.shotgun_instance_id
|
|
420
|
+
|
|
421
|
+
def update_shotgun_account(
|
|
422
|
+
self, api_key: str | None = None, supabase_jwt: str | None = None
|
|
423
|
+
) -> None:
|
|
424
|
+
"""Update Shotgun Account configuration.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
api_key: LiteLLM proxy API key (optional)
|
|
428
|
+
supabase_jwt: Supabase authentication JWT (optional)
|
|
287
429
|
"""
|
|
288
430
|
config = self.load()
|
|
289
|
-
|
|
431
|
+
|
|
432
|
+
if api_key is not None:
|
|
433
|
+
config.shotgun.api_key = SecretStr(api_key) if api_key else None
|
|
434
|
+
|
|
435
|
+
if supabase_jwt is not None:
|
|
436
|
+
config.shotgun.supabase_jwt = (
|
|
437
|
+
SecretStr(supabase_jwt) if supabase_jwt else None
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
self.save(config)
|
|
441
|
+
logger.info("Updated Shotgun Account configuration")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# Global singleton instance
|
|
445
|
+
_config_manager_instance: ConfigManager | None = None
|
|
290
446
|
|
|
291
447
|
|
|
292
448
|
def get_config_manager() -> ConfigManager:
|
|
293
|
-
"""Get the global ConfigManager instance.
|
|
294
|
-
|
|
449
|
+
"""Get the global singleton ConfigManager instance.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
The singleton ConfigManager instance
|
|
453
|
+
"""
|
|
454
|
+
global _config_manager_instance
|
|
455
|
+
if _config_manager_instance is None:
|
|
456
|
+
_config_manager_instance = ConfigManager()
|
|
457
|
+
return _config_manager_instance
|