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.

Files changed (55) hide show
  1. shotgun/agents/common.py +4 -5
  2. shotgun/agents/config/constants.py +23 -6
  3. shotgun/agents/config/manager.py +239 -76
  4. shotgun/agents/config/models.py +74 -84
  5. shotgun/agents/config/provider.py +174 -85
  6. shotgun/agents/history/compaction.py +1 -1
  7. shotgun/agents/history/history_processors.py +18 -9
  8. shotgun/agents/history/token_counting/__init__.py +31 -0
  9. shotgun/agents/history/token_counting/anthropic.py +89 -0
  10. shotgun/agents/history/token_counting/base.py +67 -0
  11. shotgun/agents/history/token_counting/openai.py +80 -0
  12. shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
  13. shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
  14. shotgun/agents/history/token_counting/utils.py +147 -0
  15. shotgun/agents/history/token_estimation.py +12 -12
  16. shotgun/agents/llm.py +62 -0
  17. shotgun/agents/models.py +2 -2
  18. shotgun/agents/tools/web_search/__init__.py +42 -15
  19. shotgun/agents/tools/web_search/anthropic.py +54 -50
  20. shotgun/agents/tools/web_search/gemini.py +31 -20
  21. shotgun/agents/tools/web_search/openai.py +4 -4
  22. shotgun/build_constants.py +2 -2
  23. shotgun/cli/config.py +34 -63
  24. shotgun/cli/feedback.py +4 -2
  25. shotgun/cli/models.py +2 -2
  26. shotgun/codebase/core/ingestor.py +47 -8
  27. shotgun/codebase/core/manager.py +7 -3
  28. shotgun/codebase/models.py +4 -4
  29. shotgun/llm_proxy/__init__.py +16 -0
  30. shotgun/llm_proxy/clients.py +39 -0
  31. shotgun/llm_proxy/constants.py +8 -0
  32. shotgun/main.py +6 -0
  33. shotgun/posthog_telemetry.py +15 -11
  34. shotgun/sentry_telemetry.py +3 -3
  35. shotgun/shotgun_web/__init__.py +19 -0
  36. shotgun/shotgun_web/client.py +138 -0
  37. shotgun/shotgun_web/constants.py +17 -0
  38. shotgun/shotgun_web/models.py +47 -0
  39. shotgun/telemetry.py +7 -4
  40. shotgun/tui/app.py +26 -8
  41. shotgun/tui/screens/chat.py +2 -8
  42. shotgun/tui/screens/chat_screen/command_providers.py +118 -11
  43. shotgun/tui/screens/chat_screen/history.py +3 -1
  44. shotgun/tui/screens/feedback.py +2 -2
  45. shotgun/tui/screens/model_picker.py +327 -0
  46. shotgun/tui/screens/provider_config.py +118 -28
  47. shotgun/tui/screens/shotgun_auth.py +295 -0
  48. shotgun/tui/screens/welcome.py +176 -0
  49. shotgun/utils/env_utils.py +12 -0
  50. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/METADATA +2 -2
  51. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/RECORD +54 -37
  52. shotgun/agents/history/token_counting.py +0 -429
  53. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/WHEEL +0 -0
  54. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/entry_points.txt +0 -0
  55. {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, get_config_manager, get_provider_model
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 hardcoded default
118
+ # Get configured model or fall back to first available provider
119
119
  try:
120
120
  model_config = get_provider_model(provider)
121
- config_manager = get_config_manager()
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
- DEFAULT_PROVIDER_FIELD = "default_provider"
6
- USER_ID_FIELD = "user_id"
7
+ SUPABASE_JWT_FIELD = "supabase_jwt"
8
+ SHOTGUN_INSTANCE_ID_FIELD = "shotgun_instance_id"
7
9
  CONFIG_VERSION_FIELD = "config_version"
8
10
 
9
- # Provider names (for consistency with data dict keys)
10
- OPENAI_PROVIDER = "openai"
11
- ANTHROPIC_PROVIDER = "anthropic"
12
- GOOGLE_PROVIDER = "google"
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
@@ -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
- GEMINI_API_KEY_ENV,
19
- GOOGLE_PROVIDER,
20
- OPENAI_API_KEY_ENV,
21
- OPENAI_PROVIDER,
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 with user_id: %s",
65
+ "Configuration file not found, creating new config at: %s",
56
66
  self.config_path,
57
67
  )
58
- # Create new config with generated user_id
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
- # Check if the default provider has a key, if not find one that does
73
- if not self.has_provider_key(self._config.default_provider):
74
- original_default = self._config.default_provider
75
- # Find first provider with a configured key
76
- for provider in ProviderType:
77
- if self.has_provider_key(provider):
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
- "Default provider %s has no API key, updating to %s",
80
- original_default.value,
81
- provider.value,
109
+ "Selected model %s not found in MODEL_SPECS, resetting",
110
+ self._config.selected_model.value,
82
111
  )
83
- self._config.default_provider = provider
84
- self.save(self._config)
85
- break
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 user_id")
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 user_id
161
+ # Create a new config with generated shotgun_instance_id
108
162
  config = ShotgunConfig(
109
- user_id=str(uuid.uuid4()),
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
- provider_enum = self._ensure_provider_enum(provider)
140
- provider_config = self._get_provider_config(config, provider_enum)
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 this provider as the default
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
- config.default_provider = provider_enum
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
- provider_enum = self._ensure_provider_enum(provider)
168
- provider_config = self._get_provider_config(config, provider_enum)
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 both the configuration file and environment variables.
257
+ This checks only the configuration file.
176
258
  """
177
- config = self.load()
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
- # Check config first
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
- config = self.load()
198
- return any(
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 user ID for new config
289
+ # Generate unique shotgun instance ID for new config
214
290
  config = ShotgunConfig(
215
- user_id=str(uuid.uuid4()),
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 user_id: %s",
295
+ "Configuration initialized at %s with shotgun_instance_id: %s",
221
296
  self.config_path,
222
- config.user_id,
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 provider in [OPENAI_PROVIDER, ANTHROPIC_PROVIDER, GOOGLE_PROVIDER]:
229
- if provider in data and isinstance(data[provider], dict):
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[provider]
232
- and data[provider][API_KEY_FIELD] is not None
307
+ API_KEY_FIELD in data[section.value]
308
+ and data[section.value][API_KEY_FIELD] is not None
233
309
  ):
234
- data[provider][API_KEY_FIELD] = SecretStr(
235
- data[provider][API_KEY_FIELD]
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 provider in [OPENAI_PROVIDER, ANTHROPIC_PROVIDER, GOOGLE_PROVIDER]:
241
- if provider in data and isinstance(data[provider], dict):
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[provider]
244
- and data[provider][API_KEY_FIELD] is not None
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[provider][API_KEY_FIELD], "get_secret_value"):
247
- data[provider][API_KEY_FIELD] = data[provider][
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 get_user_id(self) -> str:
283
- """Get the user ID from configuration.
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
- The unique user ID string
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
- return config.user_id
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
- return ConfigManager()
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