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/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,31 @@ 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
|
+
supabase_jwt: SecretStr | None = Field(
|
|
153
|
+
default=None, description="Supabase authentication JWT"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
134
157
|
class ShotgunConfig(BaseModel):
|
|
135
158
|
"""Main configuration for Shotgun CLI."""
|
|
136
159
|
|
|
137
160
|
openai: OpenAIConfig = Field(default_factory=OpenAIConfig)
|
|
138
161
|
anthropic: AnthropicConfig = Field(default_factory=AnthropicConfig)
|
|
139
162
|
google: GoogleConfig = Field(default_factory=GoogleConfig)
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
shotgun: ShotgunAccountConfig = Field(default_factory=ShotgunAccountConfig)
|
|
164
|
+
selected_model: ModelName | None = Field(
|
|
165
|
+
default=None,
|
|
166
|
+
description="User-selected model",
|
|
167
|
+
)
|
|
168
|
+
shotgun_instance_id: str = Field(
|
|
169
|
+
description="Unique shotgun instance identifier (also used for anonymous telemetry)",
|
|
142
170
|
)
|
|
143
|
-
|
|
144
|
-
|
|
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,
|
|
171
|
+
config_version: int = Field(default=3, description="Configuration schema version")
|
|
172
|
+
shown_welcome_screen: bool = Field(
|
|
173
|
+
default=False,
|
|
174
|
+
description="Whether the welcome screen has been shown to the user",
|
|
185
175
|
)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Provider management for LLM configuration."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
from pydantic import SecretStr
|
|
6
4
|
from pydantic_ai.models import Model
|
|
7
5
|
from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings
|
|
@@ -12,27 +10,36 @@ from pydantic_ai.providers.google import GoogleProvider
|
|
|
12
10
|
from pydantic_ai.providers.openai import OpenAIProvider
|
|
13
11
|
from pydantic_ai.settings import ModelSettings
|
|
14
12
|
|
|
13
|
+
from shotgun.llm_proxy import create_litellm_provider
|
|
15
14
|
from shotgun.logging_config import get_logger
|
|
16
15
|
|
|
17
|
-
from .constants import (
|
|
18
|
-
ANTHROPIC_API_KEY_ENV,
|
|
19
|
-
GEMINI_API_KEY_ENV,
|
|
20
|
-
OPENAI_API_KEY_ENV,
|
|
21
|
-
)
|
|
22
16
|
from .manager import get_config_manager
|
|
23
|
-
from .models import
|
|
17
|
+
from .models import (
|
|
18
|
+
MODEL_SPECS,
|
|
19
|
+
KeyProvider,
|
|
20
|
+
ModelConfig,
|
|
21
|
+
ModelName,
|
|
22
|
+
ProviderType,
|
|
23
|
+
ShotgunConfig,
|
|
24
|
+
)
|
|
24
25
|
|
|
25
26
|
logger = get_logger(__name__)
|
|
26
27
|
|
|
27
28
|
# Global cache for Model instances (singleton pattern)
|
|
28
|
-
_model_cache: dict[tuple[ProviderType,
|
|
29
|
+
_model_cache: dict[tuple[ProviderType, KeyProvider, ModelName, str], Model] = {}
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def get_or_create_model(
|
|
32
|
+
def get_or_create_model(
|
|
33
|
+
provider: ProviderType,
|
|
34
|
+
key_provider: "KeyProvider",
|
|
35
|
+
model_name: ModelName,
|
|
36
|
+
api_key: str,
|
|
37
|
+
) -> Model:
|
|
32
38
|
"""Get or create a singleton Model instance.
|
|
33
39
|
|
|
34
40
|
Args:
|
|
35
|
-
provider:
|
|
41
|
+
provider: Actual LLM provider (openai, anthropic, google)
|
|
42
|
+
key_provider: Authentication method (byok or shotgun)
|
|
36
43
|
model_name: Name of the model
|
|
37
44
|
api_key: API key for the provider
|
|
38
45
|
|
|
@@ -42,66 +49,88 @@ def get_or_create_model(provider: ProviderType, model_name: str, api_key: str) -
|
|
|
42
49
|
Raises:
|
|
43
50
|
ValueError: If provider is not supported
|
|
44
51
|
"""
|
|
45
|
-
cache_key = (provider, model_name, api_key)
|
|
52
|
+
cache_key = (provider, key_provider, model_name, api_key)
|
|
46
53
|
|
|
47
54
|
if cache_key not in _model_cache:
|
|
48
|
-
logger.debug(
|
|
55
|
+
logger.debug(
|
|
56
|
+
"Creating new %s model instance via %s: %s",
|
|
57
|
+
provider.value,
|
|
58
|
+
key_provider.value,
|
|
59
|
+
model_name,
|
|
60
|
+
)
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
# Get max_tokens from MODEL_SPECS
|
|
63
|
+
if model_name in MODEL_SPECS:
|
|
64
|
+
max_tokens = MODEL_SPECS[model_name].max_output_tokens
|
|
65
|
+
else:
|
|
66
|
+
# Fallback defaults based on provider
|
|
67
|
+
max_tokens = {
|
|
68
|
+
ProviderType.OPENAI: 16_000,
|
|
69
|
+
ProviderType.ANTHROPIC: 32_000,
|
|
70
|
+
ProviderType.GOOGLE: 64_000,
|
|
71
|
+
}.get(provider, 16_000)
|
|
72
|
+
|
|
73
|
+
# Use LiteLLM proxy for Shotgun Account, native providers for BYOK
|
|
74
|
+
if key_provider == KeyProvider.SHOTGUN:
|
|
75
|
+
# Shotgun Account uses LiteLLM proxy for any model
|
|
52
76
|
if model_name in MODEL_SPECS:
|
|
53
|
-
|
|
77
|
+
litellm_model_name = MODEL_SPECS[model_name].litellm_proxy_model_name
|
|
54
78
|
else:
|
|
55
|
-
|
|
79
|
+
# Fallback for unmapped models
|
|
80
|
+
litellm_model_name = f"openai/{model_name.value}"
|
|
56
81
|
|
|
57
|
-
|
|
82
|
+
litellm_provider = create_litellm_provider(api_key)
|
|
58
83
|
_model_cache[cache_key] = OpenAIChatModel(
|
|
59
|
-
|
|
60
|
-
provider=
|
|
84
|
+
litellm_model_name,
|
|
85
|
+
provider=litellm_provider,
|
|
61
86
|
settings=ModelSettings(max_tokens=max_tokens),
|
|
62
87
|
)
|
|
63
|
-
elif
|
|
64
|
-
#
|
|
65
|
-
if
|
|
66
|
-
|
|
88
|
+
elif key_provider == KeyProvider.BYOK:
|
|
89
|
+
# Use native provider implementations with user's API keys
|
|
90
|
+
if provider == ProviderType.OPENAI:
|
|
91
|
+
openai_provider = OpenAIProvider(api_key=api_key)
|
|
92
|
+
_model_cache[cache_key] = OpenAIChatModel(
|
|
93
|
+
model_name,
|
|
94
|
+
provider=openai_provider,
|
|
95
|
+
settings=ModelSettings(max_tokens=max_tokens),
|
|
96
|
+
)
|
|
97
|
+
elif provider == ProviderType.ANTHROPIC:
|
|
98
|
+
anthropic_provider = AnthropicProvider(api_key=api_key)
|
|
99
|
+
_model_cache[cache_key] = AnthropicModel(
|
|
100
|
+
model_name,
|
|
101
|
+
provider=anthropic_provider,
|
|
102
|
+
settings=AnthropicModelSettings(
|
|
103
|
+
max_tokens=max_tokens,
|
|
104
|
+
timeout=600, # 10 minutes timeout for large responses
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
elif provider == ProviderType.GOOGLE:
|
|
108
|
+
google_provider = GoogleProvider(api_key=api_key)
|
|
109
|
+
_model_cache[cache_key] = GoogleModel(
|
|
110
|
+
model_name,
|
|
111
|
+
provider=google_provider,
|
|
112
|
+
settings=ModelSettings(max_tokens=max_tokens),
|
|
113
|
+
)
|
|
67
114
|
else:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
anthropic_provider = AnthropicProvider(api_key=api_key)
|
|
71
|
-
_model_cache[cache_key] = AnthropicModel(
|
|
72
|
-
model_name,
|
|
73
|
-
provider=anthropic_provider,
|
|
74
|
-
settings=AnthropicModelSettings(
|
|
75
|
-
max_tokens=max_tokens,
|
|
76
|
-
timeout=600, # 10 minutes timeout for large responses
|
|
77
|
-
),
|
|
78
|
-
)
|
|
79
|
-
elif provider == ProviderType.GOOGLE:
|
|
80
|
-
# Get max_tokens from MODEL_SPECS to use full capacity
|
|
81
|
-
if model_name in MODEL_SPECS:
|
|
82
|
-
max_tokens = MODEL_SPECS[model_name].max_output_tokens
|
|
83
|
-
else:
|
|
84
|
-
max_tokens = 64_000 # Default for Gemini models
|
|
85
|
-
|
|
86
|
-
google_provider = GoogleProvider(api_key=api_key)
|
|
87
|
-
_model_cache[cache_key] = GoogleModel(
|
|
88
|
-
model_name,
|
|
89
|
-
provider=google_provider,
|
|
90
|
-
settings=ModelSettings(max_tokens=max_tokens),
|
|
91
|
-
)
|
|
115
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
92
116
|
else:
|
|
93
|
-
raise ValueError(f"Unsupported provider: {
|
|
117
|
+
raise ValueError(f"Unsupported key provider: {key_provider}")
|
|
94
118
|
else:
|
|
95
119
|
logger.debug("Reusing cached %s model instance: %s", provider.value, model_name)
|
|
96
120
|
|
|
97
121
|
return _model_cache[cache_key]
|
|
98
122
|
|
|
99
123
|
|
|
100
|
-
def get_provider_model(
|
|
124
|
+
def get_provider_model(
|
|
125
|
+
provider_or_model: ProviderType | ModelName | None = None,
|
|
126
|
+
) -> ModelConfig:
|
|
101
127
|
"""Get a fully configured ModelConfig with API key and Model instance.
|
|
102
128
|
|
|
103
129
|
Args:
|
|
104
|
-
|
|
130
|
+
provider_or_model: Either a ProviderType, ModelName, or None.
|
|
131
|
+
- If ModelName: returns that specific model with appropriate API key
|
|
132
|
+
- If ProviderType: returns default model for that provider (backward compatible)
|
|
133
|
+
- If None: uses default provider with its default model
|
|
105
134
|
|
|
106
135
|
Returns:
|
|
107
136
|
ModelConfig with API key configured and lazy Model instance
|
|
@@ -110,77 +139,119 @@ def get_provider_model(provider: ProviderType | None = None) -> ModelConfig:
|
|
|
110
139
|
ValueError: If provider is not configured properly or model not found
|
|
111
140
|
"""
|
|
112
141
|
config_manager = get_config_manager()
|
|
113
|
-
config
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
# Use cached config for read-only access (performance)
|
|
143
|
+
config = config_manager.load(force_reload=False)
|
|
144
|
+
|
|
145
|
+
# Priority 1: Check if Shotgun key exists - if so, use it for ANY model
|
|
146
|
+
shotgun_api_key = _get_api_key(config.shotgun.api_key)
|
|
147
|
+
if shotgun_api_key:
|
|
148
|
+
# Use selected model or default to claude-sonnet-4-5
|
|
149
|
+
model_name = config.selected_model or ModelName.CLAUDE_SONNET_4_5
|
|
150
|
+
if model_name not in MODEL_SPECS:
|
|
151
|
+
raise ValueError(f"Model '{model_name.value}' not found")
|
|
152
|
+
spec = MODEL_SPECS[model_name]
|
|
153
|
+
|
|
154
|
+
# Use Shotgun Account with selected model (provider = actual LLM provider)
|
|
155
|
+
return ModelConfig(
|
|
156
|
+
name=spec.name,
|
|
157
|
+
provider=spec.provider, # Actual LLM provider (OPENAI/ANTHROPIC/GOOGLE)
|
|
158
|
+
key_provider=KeyProvider.SHOTGUN, # Authenticated via Shotgun Account
|
|
159
|
+
max_input_tokens=spec.max_input_tokens,
|
|
160
|
+
max_output_tokens=spec.max_output_tokens,
|
|
161
|
+
api_key=shotgun_api_key,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Priority 2: Fall back to individual provider keys
|
|
165
|
+
|
|
166
|
+
# Check if a specific model was requested
|
|
167
|
+
if isinstance(provider_or_model, ModelName):
|
|
168
|
+
# Look up the model spec
|
|
169
|
+
if provider_or_model not in MODEL_SPECS:
|
|
170
|
+
raise ValueError(f"Model '{provider_or_model.value}' not found")
|
|
171
|
+
spec = MODEL_SPECS[provider_or_model]
|
|
172
|
+
provider_enum = spec.provider
|
|
173
|
+
requested_model = provider_or_model
|
|
174
|
+
else:
|
|
175
|
+
# Convert string to ProviderType enum if needed (backward compatible)
|
|
176
|
+
if provider_or_model:
|
|
177
|
+
provider_enum = (
|
|
178
|
+
provider_or_model
|
|
179
|
+
if isinstance(provider_or_model, ProviderType)
|
|
180
|
+
else ProviderType(provider_or_model)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
# No provider specified - find first available provider with a key
|
|
184
|
+
provider_enum = None
|
|
185
|
+
for provider in ProviderType:
|
|
186
|
+
if _has_provider_key(config, provider):
|
|
187
|
+
provider_enum = provider
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if provider_enum is None:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"No provider keys configured. Set via environment variables or config."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
requested_model = None # Will use provider's default model
|
|
122
196
|
|
|
123
197
|
if provider_enum == ProviderType.OPENAI:
|
|
124
|
-
api_key = _get_api_key(config.openai.api_key
|
|
198
|
+
api_key = _get_api_key(config.openai.api_key)
|
|
125
199
|
if not api_key:
|
|
126
|
-
raise ValueError(
|
|
127
|
-
f"OpenAI API key not configured. Set via environment variable {OPENAI_API_KEY_ENV} or config."
|
|
128
|
-
)
|
|
200
|
+
raise ValueError("OpenAI API key not configured. Set via config.")
|
|
129
201
|
|
|
130
|
-
#
|
|
131
|
-
model_name =
|
|
202
|
+
# Use requested model or default to gpt-5
|
|
203
|
+
model_name = requested_model if requested_model else ModelName.GPT_5
|
|
132
204
|
if model_name not in MODEL_SPECS:
|
|
133
|
-
raise ValueError(f"Model '{model_name}' not found")
|
|
205
|
+
raise ValueError(f"Model '{model_name.value}' not found")
|
|
134
206
|
spec = MODEL_SPECS[model_name]
|
|
135
207
|
|
|
136
208
|
# Create fully configured ModelConfig
|
|
137
209
|
return ModelConfig(
|
|
138
210
|
name=spec.name,
|
|
139
211
|
provider=spec.provider,
|
|
212
|
+
key_provider=KeyProvider.BYOK,
|
|
140
213
|
max_input_tokens=spec.max_input_tokens,
|
|
141
214
|
max_output_tokens=spec.max_output_tokens,
|
|
142
215
|
api_key=api_key,
|
|
143
216
|
)
|
|
144
217
|
|
|
145
218
|
elif provider_enum == ProviderType.ANTHROPIC:
|
|
146
|
-
api_key = _get_api_key(config.anthropic.api_key
|
|
219
|
+
api_key = _get_api_key(config.anthropic.api_key)
|
|
147
220
|
if not api_key:
|
|
148
|
-
raise ValueError(
|
|
149
|
-
f"Anthropic API key not configured. Set via environment variable {ANTHROPIC_API_KEY_ENV} or config."
|
|
150
|
-
)
|
|
221
|
+
raise ValueError("Anthropic API key not configured. Set via config.")
|
|
151
222
|
|
|
152
|
-
#
|
|
153
|
-
model_name =
|
|
223
|
+
# Use requested model or default to claude-sonnet-4-5
|
|
224
|
+
model_name = requested_model if requested_model else ModelName.CLAUDE_SONNET_4_5
|
|
154
225
|
if model_name not in MODEL_SPECS:
|
|
155
|
-
raise ValueError(f"Model '{model_name}' not found")
|
|
226
|
+
raise ValueError(f"Model '{model_name.value}' not found")
|
|
156
227
|
spec = MODEL_SPECS[model_name]
|
|
157
228
|
|
|
158
229
|
# Create fully configured ModelConfig
|
|
159
230
|
return ModelConfig(
|
|
160
231
|
name=spec.name,
|
|
161
232
|
provider=spec.provider,
|
|
233
|
+
key_provider=KeyProvider.BYOK,
|
|
162
234
|
max_input_tokens=spec.max_input_tokens,
|
|
163
235
|
max_output_tokens=spec.max_output_tokens,
|
|
164
236
|
api_key=api_key,
|
|
165
237
|
)
|
|
166
238
|
|
|
167
239
|
elif provider_enum == ProviderType.GOOGLE:
|
|
168
|
-
api_key = _get_api_key(config.google.api_key
|
|
240
|
+
api_key = _get_api_key(config.google.api_key)
|
|
169
241
|
if not api_key:
|
|
170
|
-
raise ValueError(
|
|
171
|
-
f"Gemini API key not configured. Set via environment variable {GEMINI_API_KEY_ENV} or config."
|
|
172
|
-
)
|
|
242
|
+
raise ValueError("Gemini API key not configured. Set via config.")
|
|
173
243
|
|
|
174
|
-
#
|
|
175
|
-
model_name =
|
|
244
|
+
# Use requested model or default to gemini-2.5-pro
|
|
245
|
+
model_name = requested_model if requested_model else ModelName.GEMINI_2_5_PRO
|
|
176
246
|
if model_name not in MODEL_SPECS:
|
|
177
|
-
raise ValueError(f"Model '{model_name}' not found")
|
|
247
|
+
raise ValueError(f"Model '{model_name.value}' not found")
|
|
178
248
|
spec = MODEL_SPECS[model_name]
|
|
179
249
|
|
|
180
250
|
# Create fully configured ModelConfig
|
|
181
251
|
return ModelConfig(
|
|
182
252
|
name=spec.name,
|
|
183
253
|
provider=spec.provider,
|
|
254
|
+
key_provider=KeyProvider.BYOK,
|
|
184
255
|
max_input_tokens=spec.max_input_tokens,
|
|
185
256
|
max_output_tokens=spec.max_output_tokens,
|
|
186
257
|
api_key=api_key,
|
|
@@ -190,12 +261,30 @@ def get_provider_model(provider: ProviderType | None = None) -> ModelConfig:
|
|
|
190
261
|
raise ValueError(f"Unsupported provider: {provider_enum}")
|
|
191
262
|
|
|
192
263
|
|
|
193
|
-
def
|
|
194
|
-
"""
|
|
264
|
+
def _has_provider_key(config: "ShotgunConfig", provider: ProviderType) -> bool:
|
|
265
|
+
"""Check if a provider has a configured API key.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
config: Shotgun configuration
|
|
269
|
+
provider: Provider to check
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if provider has a configured API key
|
|
273
|
+
"""
|
|
274
|
+
if provider == ProviderType.OPENAI:
|
|
275
|
+
return bool(_get_api_key(config.openai.api_key))
|
|
276
|
+
elif provider == ProviderType.ANTHROPIC:
|
|
277
|
+
return bool(_get_api_key(config.anthropic.api_key))
|
|
278
|
+
elif provider == ProviderType.GOOGLE:
|
|
279
|
+
return bool(_get_api_key(config.google.api_key))
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _get_api_key(config_key: SecretStr | None) -> str | None:
|
|
284
|
+
"""Get API key from config.
|
|
195
285
|
|
|
196
286
|
Args:
|
|
197
287
|
config_key: API key from configuration
|
|
198
|
-
env_var: Environment variable name to check
|
|
199
288
|
|
|
200
289
|
Returns:
|
|
201
290
|
API key string or None
|
|
@@ -203,4 +292,4 @@ def _get_api_key(config_key: SecretStr | None, env_var: str) -> str | None:
|
|
|
203
292
|
if config_key is not None:
|
|
204
293
|
return config_key.get_secret_value()
|
|
205
294
|
|
|
206
|
-
return
|
|
295
|
+
return None
|
|
@@ -31,7 +31,7 @@ async def apply_persistent_compaction(
|
|
|
31
31
|
|
|
32
32
|
try:
|
|
33
33
|
# Count actual token usage using shared utility
|
|
34
|
-
estimated_tokens = estimate_tokens_from_messages(messages, deps.llm_model)
|
|
34
|
+
estimated_tokens = await estimate_tokens_from_messages(messages, deps.llm_model)
|
|
35
35
|
|
|
36
36
|
# Create minimal usage info for compaction check
|
|
37
37
|
usage = RequestUsage(
|