shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
shotgun/agents/config/models.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Pydantic models for configuration."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
from enum import StrEnum
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field, PrivateAttr, SecretStr
|
|
@@ -24,12 +25,17 @@ class KeyProvider(StrEnum):
|
|
|
24
25
|
class ModelName(StrEnum):
|
|
25
26
|
"""Available AI model names."""
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
GPT_5_1 = "gpt-5.1"
|
|
29
|
+
GPT_5_1_CODEX = "gpt-5.1-codex"
|
|
30
|
+
GPT_5_1_CODEX_MINI = "gpt-5.1-codex-mini"
|
|
31
|
+
CLAUDE_OPUS_4_5 = "claude-opus-4-5"
|
|
32
|
+
CLAUDE_SONNET_4 = "claude-sonnet-4"
|
|
30
33
|
CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
|
|
34
|
+
CLAUDE_HAIKU_4_5 = "claude-haiku-4-5"
|
|
31
35
|
GEMINI_2_5_PRO = "gemini-2.5-pro"
|
|
32
36
|
GEMINI_2_5_FLASH = "gemini-2.5-flash"
|
|
37
|
+
GEMINI_2_5_FLASH_LITE = "gemini-2.5-flash-lite"
|
|
38
|
+
GEMINI_3_PRO_PREVIEW = "gemini-3-pro-preview"
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
class ModelSpec(BaseModel):
|
|
@@ -42,6 +48,7 @@ class ModelSpec(BaseModel):
|
|
|
42
48
|
litellm_proxy_model_name: (
|
|
43
49
|
str # LiteLLM format (e.g., "openai/gpt-5", "gemini/gemini-2-pro")
|
|
44
50
|
)
|
|
51
|
+
short_name: str # Display name for UI (e.g., "Sonnet 4.5", "GPT-5")
|
|
45
52
|
|
|
46
53
|
|
|
47
54
|
class ModelConfig(BaseModel):
|
|
@@ -53,6 +60,10 @@ class ModelConfig(BaseModel):
|
|
|
53
60
|
max_input_tokens: int
|
|
54
61
|
max_output_tokens: int
|
|
55
62
|
api_key: str
|
|
63
|
+
supports_streaming: bool = Field(
|
|
64
|
+
default=True,
|
|
65
|
+
description="Whether this model configuration supports streaming. False only for BYOK GPT-5 models without streaming enabled.",
|
|
66
|
+
)
|
|
56
67
|
_model_instance: Model | None = PrivateAttr(default=None)
|
|
57
68
|
|
|
58
69
|
class Config:
|
|
@@ -79,29 +90,41 @@ class ModelConfig(BaseModel):
|
|
|
79
90
|
}
|
|
80
91
|
return f"{provider_prefix[self.provider]}:{self.name}"
|
|
81
92
|
|
|
93
|
+
@property
|
|
94
|
+
def is_shotgun_account(self) -> bool:
|
|
95
|
+
"""Check if this model is using Shotgun Account authentication.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if using Shotgun Account, False if BYOK
|
|
99
|
+
"""
|
|
100
|
+
return self.key_provider == KeyProvider.SHOTGUN
|
|
101
|
+
|
|
82
102
|
|
|
83
103
|
# Model specifications registry (static metadata)
|
|
84
104
|
MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
85
|
-
ModelName.
|
|
86
|
-
name=ModelName.
|
|
105
|
+
ModelName.GPT_5_1: ModelSpec(
|
|
106
|
+
name=ModelName.GPT_5_1,
|
|
87
107
|
provider=ProviderType.OPENAI,
|
|
88
|
-
max_input_tokens=
|
|
108
|
+
max_input_tokens=272_000,
|
|
89
109
|
max_output_tokens=128_000,
|
|
90
|
-
litellm_proxy_model_name="openai/gpt-5",
|
|
110
|
+
litellm_proxy_model_name="openai/gpt-5.1",
|
|
111
|
+
short_name="GPT-5.1",
|
|
91
112
|
),
|
|
92
|
-
ModelName.
|
|
93
|
-
name=ModelName.
|
|
113
|
+
ModelName.GPT_5_1_CODEX: ModelSpec(
|
|
114
|
+
name=ModelName.GPT_5_1_CODEX,
|
|
94
115
|
provider=ProviderType.OPENAI,
|
|
95
|
-
max_input_tokens=
|
|
116
|
+
max_input_tokens=272_000,
|
|
96
117
|
max_output_tokens=128_000,
|
|
97
|
-
litellm_proxy_model_name="openai/gpt-5-
|
|
118
|
+
litellm_proxy_model_name="openai/gpt-5.1-codex",
|
|
119
|
+
short_name="GPT-5.1 Codex",
|
|
98
120
|
),
|
|
99
|
-
ModelName.
|
|
100
|
-
name=ModelName.
|
|
101
|
-
provider=ProviderType.
|
|
102
|
-
max_input_tokens=
|
|
103
|
-
max_output_tokens=
|
|
104
|
-
litellm_proxy_model_name="
|
|
121
|
+
ModelName.GPT_5_1_CODEX_MINI: ModelSpec(
|
|
122
|
+
name=ModelName.GPT_5_1_CODEX_MINI,
|
|
123
|
+
provider=ProviderType.OPENAI,
|
|
124
|
+
max_input_tokens=272_000,
|
|
125
|
+
max_output_tokens=128_000,
|
|
126
|
+
litellm_proxy_model_name="openai/gpt-5.1-codex-mini",
|
|
127
|
+
short_name="GPT-5.1 Codex Mini",
|
|
105
128
|
),
|
|
106
129
|
ModelName.CLAUDE_SONNET_4_5: ModelSpec(
|
|
107
130
|
name=ModelName.CLAUDE_SONNET_4_5,
|
|
@@ -109,6 +132,15 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
109
132
|
max_input_tokens=200_000,
|
|
110
133
|
max_output_tokens=16_000,
|
|
111
134
|
litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
|
|
135
|
+
short_name="Sonnet 4.5",
|
|
136
|
+
),
|
|
137
|
+
ModelName.CLAUDE_HAIKU_4_5: ModelSpec(
|
|
138
|
+
name=ModelName.CLAUDE_HAIKU_4_5,
|
|
139
|
+
provider=ProviderType.ANTHROPIC,
|
|
140
|
+
max_input_tokens=200_000,
|
|
141
|
+
max_output_tokens=64_000,
|
|
142
|
+
litellm_proxy_model_name="anthropic/claude-haiku-4-5",
|
|
143
|
+
short_name="Haiku 4.5",
|
|
112
144
|
),
|
|
113
145
|
ModelName.GEMINI_2_5_PRO: ModelSpec(
|
|
114
146
|
name=ModelName.GEMINI_2_5_PRO,
|
|
@@ -116,6 +148,7 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
116
148
|
max_input_tokens=1_000_000,
|
|
117
149
|
max_output_tokens=64_000,
|
|
118
150
|
litellm_proxy_model_name="gemini/gemini-2.5-pro",
|
|
151
|
+
short_name="Gemini 2.5 Pro",
|
|
119
152
|
),
|
|
120
153
|
ModelName.GEMINI_2_5_FLASH: ModelSpec(
|
|
121
154
|
name=ModelName.GEMINI_2_5_FLASH,
|
|
@@ -123,6 +156,39 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
123
156
|
max_input_tokens=1_000_000,
|
|
124
157
|
max_output_tokens=64_000,
|
|
125
158
|
litellm_proxy_model_name="gemini/gemini-2.5-flash",
|
|
159
|
+
short_name="Gemini 2.5 Flash",
|
|
160
|
+
),
|
|
161
|
+
ModelName.CLAUDE_OPUS_4_5: ModelSpec(
|
|
162
|
+
name=ModelName.CLAUDE_OPUS_4_5,
|
|
163
|
+
provider=ProviderType.ANTHROPIC,
|
|
164
|
+
max_input_tokens=200_000,
|
|
165
|
+
max_output_tokens=64_000,
|
|
166
|
+
litellm_proxy_model_name="anthropic/claude-opus-4-5",
|
|
167
|
+
short_name="Opus 4.5",
|
|
168
|
+
),
|
|
169
|
+
ModelName.CLAUDE_SONNET_4: ModelSpec(
|
|
170
|
+
name=ModelName.CLAUDE_SONNET_4,
|
|
171
|
+
provider=ProviderType.ANTHROPIC,
|
|
172
|
+
max_input_tokens=200_000,
|
|
173
|
+
max_output_tokens=64_000,
|
|
174
|
+
litellm_proxy_model_name="anthropic/claude-sonnet-4",
|
|
175
|
+
short_name="Sonnet 4",
|
|
176
|
+
),
|
|
177
|
+
ModelName.GEMINI_2_5_FLASH_LITE: ModelSpec(
|
|
178
|
+
name=ModelName.GEMINI_2_5_FLASH_LITE,
|
|
179
|
+
provider=ProviderType.GOOGLE,
|
|
180
|
+
max_input_tokens=1_048_576,
|
|
181
|
+
max_output_tokens=65_536,
|
|
182
|
+
litellm_proxy_model_name="gemini/gemini-2.5-flash-lite",
|
|
183
|
+
short_name="Gemini 2.5 Flash Lite",
|
|
184
|
+
),
|
|
185
|
+
ModelName.GEMINI_3_PRO_PREVIEW: ModelSpec(
|
|
186
|
+
name=ModelName.GEMINI_3_PRO_PREVIEW,
|
|
187
|
+
provider=ProviderType.GOOGLE,
|
|
188
|
+
max_input_tokens=1_048_576,
|
|
189
|
+
max_output_tokens=65_536,
|
|
190
|
+
litellm_proxy_model_name="gemini/gemini-3-pro-preview",
|
|
191
|
+
short_name="Gemini 3 Pro",
|
|
126
192
|
),
|
|
127
193
|
}
|
|
128
194
|
|
|
@@ -131,6 +197,10 @@ class OpenAIConfig(BaseModel):
|
|
|
131
197
|
"""Configuration for OpenAI provider."""
|
|
132
198
|
|
|
133
199
|
api_key: SecretStr | None = None
|
|
200
|
+
supports_streaming: bool | None = Field(
|
|
201
|
+
default=None,
|
|
202
|
+
description="Whether streaming is supported for this API key. None = not tested yet",
|
|
203
|
+
)
|
|
134
204
|
|
|
135
205
|
|
|
136
206
|
class AnthropicConfig(BaseModel):
|
|
@@ -152,6 +222,24 @@ class ShotgunAccountConfig(BaseModel):
|
|
|
152
222
|
supabase_jwt: SecretStr | None = Field(
|
|
153
223
|
default=None, description="Supabase authentication JWT"
|
|
154
224
|
)
|
|
225
|
+
workspace_id: str | None = Field(
|
|
226
|
+
default=None, description="Default workspace ID for shared specs"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class MarketingMessageRecord(BaseModel):
|
|
231
|
+
"""Record of when a marketing message was shown to the user."""
|
|
232
|
+
|
|
233
|
+
shown_at: datetime = Field(description="Timestamp when the message was shown")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class MarketingConfig(BaseModel):
|
|
237
|
+
"""Configuration for marketing messages shown to users."""
|
|
238
|
+
|
|
239
|
+
messages: dict[str, MarketingMessageRecord] = Field(
|
|
240
|
+
default_factory=dict,
|
|
241
|
+
description="Tracking which marketing messages have been shown. Key is message ID (e.g., 'github_star_v1')",
|
|
242
|
+
)
|
|
155
243
|
|
|
156
244
|
|
|
157
245
|
class ShotgunConfig(BaseModel):
|
|
@@ -168,8 +256,24 @@ class ShotgunConfig(BaseModel):
|
|
|
168
256
|
shotgun_instance_id: str = Field(
|
|
169
257
|
description="Unique shotgun instance identifier (also used for anonymous telemetry)",
|
|
170
258
|
)
|
|
171
|
-
config_version: int = Field(default=
|
|
259
|
+
config_version: int = Field(default=5, description="Configuration schema version")
|
|
172
260
|
shown_welcome_screen: bool = Field(
|
|
173
261
|
default=False,
|
|
174
262
|
description="Whether the welcome screen has been shown to the user",
|
|
175
263
|
)
|
|
264
|
+
shown_onboarding_popup: datetime | None = Field(
|
|
265
|
+
default=None,
|
|
266
|
+
description="Timestamp when the onboarding popup was shown to the user (ISO8601 format)",
|
|
267
|
+
)
|
|
268
|
+
marketing: MarketingConfig = Field(
|
|
269
|
+
default_factory=MarketingConfig,
|
|
270
|
+
description="Marketing messages configuration and tracking",
|
|
271
|
+
)
|
|
272
|
+
migration_failed: bool = Field(
|
|
273
|
+
default=False,
|
|
274
|
+
description="Whether the last config migration failed (cleared after user configures a provider)",
|
|
275
|
+
)
|
|
276
|
+
migration_backup_path: str | None = Field(
|
|
277
|
+
default=None,
|
|
278
|
+
description="Path to the backup file created when migration failed",
|
|
279
|
+
)
|
|
@@ -25,6 +25,7 @@ from .models import (
|
|
|
25
25
|
ProviderType,
|
|
26
26
|
ShotgunConfig,
|
|
27
27
|
)
|
|
28
|
+
from .streaming_test import check_streaming_capability
|
|
28
29
|
|
|
29
30
|
logger = get_logger(__name__)
|
|
30
31
|
|
|
@@ -32,6 +33,34 @@ logger = get_logger(__name__)
|
|
|
32
33
|
_model_cache: dict[tuple[ProviderType, KeyProvider, ModelName, str], Model] = {}
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
def get_default_model_for_provider(config: ShotgunConfig) -> ModelName:
|
|
37
|
+
"""Get the default model based on which provider/account is configured.
|
|
38
|
+
|
|
39
|
+
Checks API keys in priority order and returns appropriate default model.
|
|
40
|
+
Treats Shotgun Account as a provider context.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Shotgun configuration containing API keys
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Default ModelName for the configured provider/account
|
|
47
|
+
"""
|
|
48
|
+
# Priority 1: Shotgun Account
|
|
49
|
+
if _get_api_key(config.shotgun.api_key):
|
|
50
|
+
return ModelName.GPT_5_1
|
|
51
|
+
|
|
52
|
+
# Priority 2: Individual provider keys
|
|
53
|
+
if _get_api_key(config.anthropic.api_key):
|
|
54
|
+
return ModelName.CLAUDE_HAIKU_4_5
|
|
55
|
+
if _get_api_key(config.openai.api_key):
|
|
56
|
+
return ModelName.GPT_5_1
|
|
57
|
+
if _get_api_key(config.google.api_key):
|
|
58
|
+
return ModelName.GEMINI_2_5_PRO
|
|
59
|
+
|
|
60
|
+
# Fallback: system-wide default
|
|
61
|
+
return ModelName.CLAUDE_HAIKU_4_5
|
|
62
|
+
|
|
63
|
+
|
|
35
64
|
def get_or_create_model(
|
|
36
65
|
provider: ProviderType,
|
|
37
66
|
key_provider: "KeyProvider",
|
|
@@ -142,7 +171,7 @@ def get_or_create_model(
|
|
|
142
171
|
return _model_cache[cache_key]
|
|
143
172
|
|
|
144
173
|
|
|
145
|
-
def get_provider_model(
|
|
174
|
+
async def get_provider_model(
|
|
146
175
|
provider_or_model: ProviderType | ModelName | None = None,
|
|
147
176
|
) -> ModelConfig:
|
|
148
177
|
"""Get a fully configured ModelConfig with API key and Model instance.
|
|
@@ -161,7 +190,7 @@ def get_provider_model(
|
|
|
161
190
|
"""
|
|
162
191
|
config_manager = get_config_manager()
|
|
163
192
|
# Use cached config for read-only access (performance)
|
|
164
|
-
config = config_manager.load(force_reload=False)
|
|
193
|
+
config = await config_manager.load(force_reload=False)
|
|
165
194
|
|
|
166
195
|
# Priority 1: Check if Shotgun key exists - if so, use it for ANY model
|
|
167
196
|
shotgun_api_key = _get_api_key(config.shotgun.api_key)
|
|
@@ -172,13 +201,16 @@ def get_provider_model(
|
|
|
172
201
|
model_name = provider_or_model
|
|
173
202
|
else:
|
|
174
203
|
# No specific model requested - use selected or default
|
|
175
|
-
model_name = config.selected_model or
|
|
204
|
+
model_name = config.selected_model or get_default_model_for_provider(config)
|
|
176
205
|
|
|
206
|
+
# Gracefully fall back if the selected model doesn't exist (backwards compatibility)
|
|
177
207
|
if model_name not in MODEL_SPECS:
|
|
178
|
-
|
|
208
|
+
model_name = get_default_model_for_provider(config)
|
|
209
|
+
|
|
179
210
|
spec = MODEL_SPECS[model_name]
|
|
180
211
|
|
|
181
212
|
# Use Shotgun Account with determined model (provider = actual LLM provider)
|
|
213
|
+
# Shotgun accounts always support streaming (via LiteLLM proxy)
|
|
182
214
|
return ModelConfig(
|
|
183
215
|
name=spec.name,
|
|
184
216
|
provider=spec.provider, # Actual LLM provider (OPENAI/ANTHROPIC/GOOGLE)
|
|
@@ -186,6 +218,7 @@ def get_provider_model(
|
|
|
186
218
|
max_input_tokens=spec.max_input_tokens,
|
|
187
219
|
max_output_tokens=spec.max_output_tokens,
|
|
188
220
|
api_key=shotgun_api_key,
|
|
221
|
+
supports_streaming=True, # Shotgun accounts always support streaming
|
|
189
222
|
)
|
|
190
223
|
|
|
191
224
|
# Priority 2: Fall back to individual provider keys
|
|
@@ -194,10 +227,12 @@ def get_provider_model(
|
|
|
194
227
|
if isinstance(provider_or_model, ModelName):
|
|
195
228
|
# Look up the model spec
|
|
196
229
|
if provider_or_model not in MODEL_SPECS:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
230
|
+
requested_model = None # Fall back to provider default
|
|
231
|
+
provider_enum = None # Will be determined below
|
|
232
|
+
else:
|
|
233
|
+
spec = MODEL_SPECS[provider_or_model]
|
|
234
|
+
provider_enum = spec.provider
|
|
235
|
+
requested_model = provider_or_model
|
|
201
236
|
else:
|
|
202
237
|
# Convert string to ProviderType enum if needed (backward compatible)
|
|
203
238
|
if provider_or_model:
|
|
@@ -226,12 +261,40 @@ def get_provider_model(
|
|
|
226
261
|
if not api_key:
|
|
227
262
|
raise ValueError("OpenAI API key not configured. Set via config.")
|
|
228
263
|
|
|
229
|
-
# Use requested model or default to gpt-5
|
|
230
|
-
model_name = requested_model if requested_model else ModelName.
|
|
264
|
+
# Use requested model or default to gpt-5.1
|
|
265
|
+
model_name = requested_model if requested_model else ModelName.GPT_5_1
|
|
266
|
+
# Gracefully fall back if model doesn't exist
|
|
231
267
|
if model_name not in MODEL_SPECS:
|
|
232
|
-
|
|
268
|
+
model_name = ModelName.GPT_5_1
|
|
233
269
|
spec = MODEL_SPECS[model_name]
|
|
234
270
|
|
|
271
|
+
# Check and test streaming capability for GPT-5 family models
|
|
272
|
+
supports_streaming = True # Default to True for all models
|
|
273
|
+
if model_name in (
|
|
274
|
+
ModelName.GPT_5_1,
|
|
275
|
+
ModelName.GPT_5_1_CODEX,
|
|
276
|
+
ModelName.GPT_5_1_CODEX_MINI,
|
|
277
|
+
):
|
|
278
|
+
# Check if streaming capability has been tested
|
|
279
|
+
streaming_capability = config.openai.supports_streaming
|
|
280
|
+
|
|
281
|
+
if streaming_capability is None:
|
|
282
|
+
# Not tested yet - run streaming test (test once for all GPT-5 models)
|
|
283
|
+
logger.info("Testing streaming capability for OpenAI GPT-5 family...")
|
|
284
|
+
streaming_capability = await check_streaming_capability(
|
|
285
|
+
api_key, model_name.value
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Save result to config (applies to all OpenAI models)
|
|
289
|
+
config.openai.supports_streaming = streaming_capability
|
|
290
|
+
await config_manager.save(config)
|
|
291
|
+
logger.info(
|
|
292
|
+
f"Streaming test result: "
|
|
293
|
+
f"{'enabled' if streaming_capability else 'disabled'}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
supports_streaming = streaming_capability
|
|
297
|
+
|
|
235
298
|
# Create fully configured ModelConfig
|
|
236
299
|
return ModelConfig(
|
|
237
300
|
name=spec.name,
|
|
@@ -240,6 +303,7 @@ def get_provider_model(
|
|
|
240
303
|
max_input_tokens=spec.max_input_tokens,
|
|
241
304
|
max_output_tokens=spec.max_output_tokens,
|
|
242
305
|
api_key=api_key,
|
|
306
|
+
supports_streaming=supports_streaming,
|
|
243
307
|
)
|
|
244
308
|
|
|
245
309
|
elif provider_enum == ProviderType.ANTHROPIC:
|
|
@@ -247,10 +311,11 @@ def get_provider_model(
|
|
|
247
311
|
if not api_key:
|
|
248
312
|
raise ValueError("Anthropic API key not configured. Set via config.")
|
|
249
313
|
|
|
250
|
-
# Use requested model or default to claude-
|
|
251
|
-
model_name = requested_model if requested_model else ModelName.
|
|
314
|
+
# Use requested model or default to claude-haiku-4-5
|
|
315
|
+
model_name = requested_model if requested_model else ModelName.CLAUDE_HAIKU_4_5
|
|
316
|
+
# Gracefully fall back if model doesn't exist
|
|
252
317
|
if model_name not in MODEL_SPECS:
|
|
253
|
-
|
|
318
|
+
model_name = ModelName.CLAUDE_HAIKU_4_5
|
|
254
319
|
spec = MODEL_SPECS[model_name]
|
|
255
320
|
|
|
256
321
|
# Create fully configured ModelConfig
|
|
@@ -270,8 +335,9 @@ def get_provider_model(
|
|
|
270
335
|
|
|
271
336
|
# Use requested model or default to gemini-2.5-pro
|
|
272
337
|
model_name = requested_model if requested_model else ModelName.GEMINI_2_5_PRO
|
|
338
|
+
# Gracefully fall back if model doesn't exist
|
|
273
339
|
if model_name not in MODEL_SPECS:
|
|
274
|
-
|
|
340
|
+
model_name = ModelName.GEMINI_2_5_PRO
|
|
275
341
|
spec = MODEL_SPECS[model_name]
|
|
276
342
|
|
|
277
343
|
# Create fully configured ModelConfig
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Utility for testing streaming capability of OpenAI models."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
# Maximum number of attempts to test streaming capability
|
|
10
|
+
MAX_STREAMING_TEST_ATTEMPTS = 3
|
|
11
|
+
|
|
12
|
+
# Timeout for each streaming test attempt (in seconds)
|
|
13
|
+
STREAMING_TEST_TIMEOUT = 10.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def check_streaming_capability(
|
|
17
|
+
api_key: str, model_name: str, max_attempts: int = MAX_STREAMING_TEST_ATTEMPTS
|
|
18
|
+
) -> bool:
|
|
19
|
+
"""Check if the given OpenAI model supports streaming with this API key.
|
|
20
|
+
|
|
21
|
+
Retries multiple times to handle transient network issues. Only returns False
|
|
22
|
+
if streaming definitively fails after all retry attempts.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: The OpenAI API key to test
|
|
26
|
+
model_name: The model name (e.g., "gpt-5", "gpt-5-mini")
|
|
27
|
+
max_attempts: Maximum number of attempts (default: 3)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if streaming is supported, False if it definitively fails
|
|
31
|
+
"""
|
|
32
|
+
url = "https://api.openai.com/v1/chat/completions"
|
|
33
|
+
headers = {
|
|
34
|
+
"Authorization": f"Bearer {api_key}",
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
}
|
|
37
|
+
# GPT-5 family uses max_completion_tokens instead of max_tokens
|
|
38
|
+
payload = {
|
|
39
|
+
"model": model_name,
|
|
40
|
+
"messages": [{"role": "user", "content": "test"}],
|
|
41
|
+
"stream": True,
|
|
42
|
+
"max_completion_tokens": 10,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
last_error = None
|
|
46
|
+
|
|
47
|
+
for attempt in range(1, max_attempts + 1):
|
|
48
|
+
logger.debug(
|
|
49
|
+
f"Streaming test attempt {attempt}/{max_attempts} for {model_name}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
async with httpx.AsyncClient(timeout=STREAMING_TEST_TIMEOUT) as client:
|
|
54
|
+
async with client.stream(
|
|
55
|
+
"POST", url, json=payload, headers=headers
|
|
56
|
+
) as response:
|
|
57
|
+
# Check if we get a successful response
|
|
58
|
+
if response.status_code != 200:
|
|
59
|
+
last_error = f"HTTP {response.status_code}"
|
|
60
|
+
logger.warning(
|
|
61
|
+
f"Streaming test attempt {attempt} failed for {model_name}: {last_error}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# For definitive errors (403 Forbidden, 404 Not Found), don't retry
|
|
65
|
+
if response.status_code in (403, 404):
|
|
66
|
+
logger.info(
|
|
67
|
+
f"Streaming definitively unsupported for {model_name} (HTTP {response.status_code})"
|
|
68
|
+
)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# For other errors, retry
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Try to read at least one chunk from the stream
|
|
75
|
+
try:
|
|
76
|
+
async for _ in response.aiter_bytes():
|
|
77
|
+
# Successfully received streaming data
|
|
78
|
+
logger.info(
|
|
79
|
+
f"Streaming test passed for {model_name} (attempt {attempt})"
|
|
80
|
+
)
|
|
81
|
+
return True
|
|
82
|
+
except Exception as e:
|
|
83
|
+
last_error = str(e)
|
|
84
|
+
logger.warning(
|
|
85
|
+
f"Streaming test attempt {attempt} failed for {model_name} while reading stream: {e}"
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
except httpx.TimeoutException:
|
|
90
|
+
last_error = "timeout"
|
|
91
|
+
logger.warning(
|
|
92
|
+
f"Streaming test attempt {attempt} timed out for {model_name}"
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
except httpx.HTTPStatusError as e:
|
|
96
|
+
last_error = str(e)
|
|
97
|
+
logger.warning(
|
|
98
|
+
f"Streaming test attempt {attempt} failed for {model_name}: {e}"
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
except Exception as e:
|
|
102
|
+
last_error = str(e)
|
|
103
|
+
logger.warning(
|
|
104
|
+
f"Streaming test attempt {attempt} failed for {model_name} with unexpected error: {e}"
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# If we got here without reading any chunks, streaming didn't work
|
|
109
|
+
last_error = "no data received"
|
|
110
|
+
logger.warning(
|
|
111
|
+
f"Streaming test attempt {attempt} failed for {model_name}: no data received"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# All attempts exhausted
|
|
115
|
+
logger.error(
|
|
116
|
+
f"Streaming test failed for {model_name} after {max_attempts} attempts. "
|
|
117
|
+
f"Last error: {last_error}. Assuming streaming is NOT supported."
|
|
118
|
+
)
|
|
119
|
+
return False
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Context analysis module for conversation composition statistics.
|
|
2
|
+
|
|
3
|
+
This module provides tools for analyzing conversation context usage, breaking down
|
|
4
|
+
token consumption by message type and tool category.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .analyzer import ContextAnalyzer
|
|
8
|
+
from .constants import ToolCategory, get_tool_category
|
|
9
|
+
from .formatter import ContextFormatter
|
|
10
|
+
from .models import (
|
|
11
|
+
ContextAnalysis,
|
|
12
|
+
ContextAnalysisOutput,
|
|
13
|
+
ContextCompositionTelemetry,
|
|
14
|
+
MessageTypeStats,
|
|
15
|
+
TokenAllocation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ContextAnalyzer",
|
|
20
|
+
"ContextAnalysis",
|
|
21
|
+
"ContextAnalysisOutput",
|
|
22
|
+
"ContextCompositionTelemetry",
|
|
23
|
+
"ContextFormatter",
|
|
24
|
+
"MessageTypeStats",
|
|
25
|
+
"TokenAllocation",
|
|
26
|
+
"ToolCategory",
|
|
27
|
+
"get_tool_category",
|
|
28
|
+
]
|