tunacode-cli 0.1.21__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 tunacode-cli might be problematic. Click here for more details.

Files changed (174) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/textual_repl.tcss +283 -0
  3. tunacode/configuration/__init__.py +1 -0
  4. tunacode/configuration/defaults.py +45 -0
  5. tunacode/configuration/models.py +147 -0
  6. tunacode/configuration/models_registry.json +1 -0
  7. tunacode/configuration/pricing.py +74 -0
  8. tunacode/configuration/settings.py +35 -0
  9. tunacode/constants.py +227 -0
  10. tunacode/core/__init__.py +6 -0
  11. tunacode/core/agents/__init__.py +39 -0
  12. tunacode/core/agents/agent_components/__init__.py +48 -0
  13. tunacode/core/agents/agent_components/agent_config.py +441 -0
  14. tunacode/core/agents/agent_components/agent_helpers.py +290 -0
  15. tunacode/core/agents/agent_components/message_handler.py +99 -0
  16. tunacode/core/agents/agent_components/node_processor.py +477 -0
  17. tunacode/core/agents/agent_components/response_state.py +129 -0
  18. tunacode/core/agents/agent_components/result_wrapper.py +51 -0
  19. tunacode/core/agents/agent_components/state_transition.py +112 -0
  20. tunacode/core/agents/agent_components/streaming.py +271 -0
  21. tunacode/core/agents/agent_components/task_completion.py +40 -0
  22. tunacode/core/agents/agent_components/tool_buffer.py +44 -0
  23. tunacode/core/agents/agent_components/tool_executor.py +101 -0
  24. tunacode/core/agents/agent_components/truncation_checker.py +37 -0
  25. tunacode/core/agents/delegation_tools.py +109 -0
  26. tunacode/core/agents/main.py +545 -0
  27. tunacode/core/agents/prompts.py +66 -0
  28. tunacode/core/agents/research_agent.py +231 -0
  29. tunacode/core/compaction.py +218 -0
  30. tunacode/core/prompting/__init__.py +27 -0
  31. tunacode/core/prompting/loader.py +66 -0
  32. tunacode/core/prompting/prompting_engine.py +98 -0
  33. tunacode/core/prompting/sections.py +50 -0
  34. tunacode/core/prompting/templates.py +69 -0
  35. tunacode/core/state.py +409 -0
  36. tunacode/exceptions.py +313 -0
  37. tunacode/indexing/__init__.py +5 -0
  38. tunacode/indexing/code_index.py +432 -0
  39. tunacode/indexing/constants.py +86 -0
  40. tunacode/lsp/__init__.py +112 -0
  41. tunacode/lsp/client.py +351 -0
  42. tunacode/lsp/diagnostics.py +19 -0
  43. tunacode/lsp/servers.py +101 -0
  44. tunacode/prompts/default_prompt.md +952 -0
  45. tunacode/prompts/research/sections/agent_role.xml +5 -0
  46. tunacode/prompts/research/sections/constraints.xml +14 -0
  47. tunacode/prompts/research/sections/output_format.xml +57 -0
  48. tunacode/prompts/research/sections/tool_use.xml +23 -0
  49. tunacode/prompts/sections/advanced_patterns.xml +255 -0
  50. tunacode/prompts/sections/agent_role.xml +8 -0
  51. tunacode/prompts/sections/completion.xml +10 -0
  52. tunacode/prompts/sections/critical_rules.xml +37 -0
  53. tunacode/prompts/sections/examples.xml +220 -0
  54. tunacode/prompts/sections/output_style.xml +94 -0
  55. tunacode/prompts/sections/parallel_exec.xml +105 -0
  56. tunacode/prompts/sections/search_pattern.xml +100 -0
  57. tunacode/prompts/sections/system_info.xml +6 -0
  58. tunacode/prompts/sections/tool_use.xml +84 -0
  59. tunacode/prompts/sections/user_instructions.xml +3 -0
  60. tunacode/py.typed +0 -0
  61. tunacode/templates/__init__.py +5 -0
  62. tunacode/templates/loader.py +15 -0
  63. tunacode/tools/__init__.py +10 -0
  64. tunacode/tools/authorization/__init__.py +29 -0
  65. tunacode/tools/authorization/context.py +32 -0
  66. tunacode/tools/authorization/factory.py +20 -0
  67. tunacode/tools/authorization/handler.py +58 -0
  68. tunacode/tools/authorization/notifier.py +35 -0
  69. tunacode/tools/authorization/policy.py +19 -0
  70. tunacode/tools/authorization/requests.py +119 -0
  71. tunacode/tools/authorization/rules.py +72 -0
  72. tunacode/tools/bash.py +222 -0
  73. tunacode/tools/decorators.py +213 -0
  74. tunacode/tools/glob.py +353 -0
  75. tunacode/tools/grep.py +468 -0
  76. tunacode/tools/grep_components/__init__.py +9 -0
  77. tunacode/tools/grep_components/file_filter.py +93 -0
  78. tunacode/tools/grep_components/pattern_matcher.py +158 -0
  79. tunacode/tools/grep_components/result_formatter.py +87 -0
  80. tunacode/tools/grep_components/search_result.py +34 -0
  81. tunacode/tools/list_dir.py +205 -0
  82. tunacode/tools/prompts/bash_prompt.xml +10 -0
  83. tunacode/tools/prompts/glob_prompt.xml +7 -0
  84. tunacode/tools/prompts/grep_prompt.xml +10 -0
  85. tunacode/tools/prompts/list_dir_prompt.xml +7 -0
  86. tunacode/tools/prompts/read_file_prompt.xml +9 -0
  87. tunacode/tools/prompts/todoclear_prompt.xml +12 -0
  88. tunacode/tools/prompts/todoread_prompt.xml +16 -0
  89. tunacode/tools/prompts/todowrite_prompt.xml +28 -0
  90. tunacode/tools/prompts/update_file_prompt.xml +9 -0
  91. tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
  92. tunacode/tools/prompts/write_file_prompt.xml +7 -0
  93. tunacode/tools/react.py +111 -0
  94. tunacode/tools/read_file.py +68 -0
  95. tunacode/tools/todo.py +222 -0
  96. tunacode/tools/update_file.py +62 -0
  97. tunacode/tools/utils/__init__.py +1 -0
  98. tunacode/tools/utils/ripgrep.py +311 -0
  99. tunacode/tools/utils/text_match.py +352 -0
  100. tunacode/tools/web_fetch.py +245 -0
  101. tunacode/tools/write_file.py +34 -0
  102. tunacode/tools/xml_helper.py +34 -0
  103. tunacode/types/__init__.py +166 -0
  104. tunacode/types/base.py +94 -0
  105. tunacode/types/callbacks.py +53 -0
  106. tunacode/types/dataclasses.py +121 -0
  107. tunacode/types/pydantic_ai.py +31 -0
  108. tunacode/types/state.py +122 -0
  109. tunacode/ui/__init__.py +6 -0
  110. tunacode/ui/app.py +542 -0
  111. tunacode/ui/commands/__init__.py +430 -0
  112. tunacode/ui/components/__init__.py +1 -0
  113. tunacode/ui/headless/__init__.py +5 -0
  114. tunacode/ui/headless/output.py +72 -0
  115. tunacode/ui/main.py +252 -0
  116. tunacode/ui/renderers/__init__.py +41 -0
  117. tunacode/ui/renderers/errors.py +197 -0
  118. tunacode/ui/renderers/panels.py +550 -0
  119. tunacode/ui/renderers/search.py +314 -0
  120. tunacode/ui/renderers/tools/__init__.py +21 -0
  121. tunacode/ui/renderers/tools/bash.py +247 -0
  122. tunacode/ui/renderers/tools/diagnostics.py +186 -0
  123. tunacode/ui/renderers/tools/glob.py +226 -0
  124. tunacode/ui/renderers/tools/grep.py +228 -0
  125. tunacode/ui/renderers/tools/list_dir.py +198 -0
  126. tunacode/ui/renderers/tools/read_file.py +226 -0
  127. tunacode/ui/renderers/tools/research.py +294 -0
  128. tunacode/ui/renderers/tools/update_file.py +237 -0
  129. tunacode/ui/renderers/tools/web_fetch.py +182 -0
  130. tunacode/ui/repl_support.py +226 -0
  131. tunacode/ui/screens/__init__.py +16 -0
  132. tunacode/ui/screens/model_picker.py +303 -0
  133. tunacode/ui/screens/session_picker.py +181 -0
  134. tunacode/ui/screens/setup.py +218 -0
  135. tunacode/ui/screens/theme_picker.py +90 -0
  136. tunacode/ui/screens/update_confirm.py +69 -0
  137. tunacode/ui/shell_runner.py +129 -0
  138. tunacode/ui/styles/layout.tcss +98 -0
  139. tunacode/ui/styles/modals.tcss +38 -0
  140. tunacode/ui/styles/panels.tcss +81 -0
  141. tunacode/ui/styles/theme-nextstep.tcss +303 -0
  142. tunacode/ui/styles/widgets.tcss +33 -0
  143. tunacode/ui/styles.py +18 -0
  144. tunacode/ui/widgets/__init__.py +23 -0
  145. tunacode/ui/widgets/command_autocomplete.py +62 -0
  146. tunacode/ui/widgets/editor.py +402 -0
  147. tunacode/ui/widgets/file_autocomplete.py +47 -0
  148. tunacode/ui/widgets/messages.py +46 -0
  149. tunacode/ui/widgets/resource_bar.py +182 -0
  150. tunacode/ui/widgets/status_bar.py +98 -0
  151. tunacode/utils/__init__.py +0 -0
  152. tunacode/utils/config/__init__.py +13 -0
  153. tunacode/utils/config/user_configuration.py +91 -0
  154. tunacode/utils/messaging/__init__.py +10 -0
  155. tunacode/utils/messaging/message_utils.py +34 -0
  156. tunacode/utils/messaging/token_counter.py +77 -0
  157. tunacode/utils/parsing/__init__.py +13 -0
  158. tunacode/utils/parsing/command_parser.py +55 -0
  159. tunacode/utils/parsing/json_utils.py +188 -0
  160. tunacode/utils/parsing/retry.py +146 -0
  161. tunacode/utils/parsing/tool_parser.py +267 -0
  162. tunacode/utils/security/__init__.py +15 -0
  163. tunacode/utils/security/command.py +106 -0
  164. tunacode/utils/system/__init__.py +25 -0
  165. tunacode/utils/system/gitignore.py +155 -0
  166. tunacode/utils/system/paths.py +190 -0
  167. tunacode/utils/ui/__init__.py +9 -0
  168. tunacode/utils/ui/file_filter.py +135 -0
  169. tunacode/utils/ui/helpers.py +24 -0
  170. tunacode_cli-0.1.21.dist-info/METADATA +170 -0
  171. tunacode_cli-0.1.21.dist-info/RECORD +174 -0
  172. tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
  173. tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
  174. tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
tunacode/__init__.py ADDED
File without changes
@@ -0,0 +1,283 @@
1
+ /* TunaCode Textual REPL Stylesheet
2
+ * NeXTSTEP Zone-Based Layout (Modern):
3
+ * ◇ tokens: 1.2k ◇ model ◇ $0.00 ◇ tunacode
4
+ * ┌────────────────────────────────────────────────────┐
5
+ * │ │
6
+ * │ Main viewport (thin subtle border) │
7
+ * │ │
8
+ * ├──────────┬─────────────────────────────────────────┤
9
+ * │ Context │ Editor (Enter to submit) │
10
+ * └──────────┴─────────────────────────────────────────┘
11
+ */
12
+
13
+ /* ============================================
14
+ * RESOURCE BAR - Compact, no border
15
+ * NeXTSTEP: "Glanceable, rarely changes"
16
+ * ============================================ */
17
+ ResourceBar {
18
+ width: 100%;
19
+ height: 1;
20
+ background: $background;
21
+ padding: 0 1;
22
+ }
23
+
24
+ /* ============================================
25
+ * PRIMARY VIEWPORT - Thin border, dark bg
26
+ * NeXTSTEP: "Maximum real estate, user focus"
27
+ * ============================================ */
28
+ RichLog {
29
+ height: 1fr;
30
+ background: $background;
31
+ border: solid $border;
32
+ padding: 1;
33
+ scrollbar-gutter: stable;
34
+ }
35
+
36
+ /* Streaming - accent border during generation */
37
+ RichLog.streaming {
38
+ border: solid $accent;
39
+ }
40
+
41
+ /* Pause mode - NeXTSTEP: "Modes must be visually apparent" */
42
+ RichLog.paused {
43
+ border: solid $warning;
44
+ }
45
+
46
+ /* ============================================
47
+ * BOTTOM ZONE - Container for tool status + command area
48
+ * Docked as single unit to prevent RichLog (1fr) from crushing children
49
+ * ============================================ */
50
+ /* ============================================
51
+ * UNUSED - Removed complex docking layout
52
+ * ============================================ */
53
+ /*
54
+ #bottom-zone {
55
+ dock: bottom;
56
+ height: auto;
57
+ width: 100%;
58
+ }
59
+
60
+ #input-row {
61
+ height: 6;
62
+ width: 100%;
63
+ dock: top;
64
+ }
65
+
66
+ #input-prompt {
67
+ width: 3;
68
+ height: 100%;
69
+ color: $primary;
70
+ background: $background;
71
+ border-top: solid $border;
72
+ border-left: solid $border;
73
+ border-bottom: solid $border;
74
+ padding: 0 0 0 1;
75
+ }
76
+ */
77
+
78
+ /* ============================================
79
+ * EDITOR - Input bar (compact)
80
+ * ============================================ */
81
+ Editor {
82
+ width: 1fr;
83
+ height: 6;
84
+ background: $background;
85
+ border-top: solid $border;
86
+ border-right: solid $border;
87
+ border-bottom: solid $border;
88
+ border-left: solid $border;
89
+ padding: 0 1;
90
+ }
91
+
92
+ /* ============================================
93
+ * TEST BAR - Simple test bar to debug text visibility
94
+ * ============================================ */
95
+ TestBar {
96
+ height: 3;
97
+ width: 100%;
98
+ background: $surface;
99
+ border: solid $border;
100
+ }
101
+
102
+ TestBar Static {
103
+ color: $text;
104
+ text-style: bold;
105
+ }
106
+
107
+ #test-left {
108
+ width: 1fr;
109
+ height: 3;
110
+ padding: 1 1;
111
+ text-align: left;
112
+ }
113
+
114
+ #test-mid {
115
+ width: 1fr;
116
+ height: 3;
117
+ text-align: center;
118
+ border-left: solid $primary;
119
+ border-right: solid $primary;
120
+ padding: 1 0;
121
+ }
122
+
123
+ #test-right {
124
+ width: 1fr;
125
+ height: 3;
126
+ text-align: right;
127
+ padding: 1 1;
128
+ }
129
+
130
+ /* ============================================
131
+ * STATUS BAR - 3-column bottom status (no borders)
132
+ * NeXTSTEP: "left=identity, center=metrics, right=state"
133
+ * ============================================ */
134
+ StatusBar {
135
+ height: 1;
136
+ width: 100%;
137
+ background: $background;
138
+ }
139
+
140
+ StatusBar Static {
141
+ color: $text;
142
+ }
143
+
144
+ #status-left {
145
+ width: 1fr;
146
+ text-align: left;
147
+ padding: 0 1;
148
+ }
149
+
150
+ #status-mid {
151
+ width: auto;
152
+ text-align: center;
153
+ color: $text-muted;
154
+ padding: 0 2;
155
+ }
156
+
157
+ #status-right {
158
+ width: 1fr;
159
+ text-align: right;
160
+ padding: 0 1;
161
+ color: $text-muted;
162
+ }
163
+
164
+ /* ============================================
165
+ * MODAL OVERLAYS - Tool Confirmation
166
+ * NeXTSTEP: "Controls change appearance immediately"
167
+ * ============================================ */
168
+ ToolConfirmationModal {
169
+ align: center middle;
170
+ }
171
+
172
+ #modal-body {
173
+ width: 60;
174
+ height: auto;
175
+ border: solid $primary;
176
+ background: $surface;
177
+ padding: 1 2;
178
+ }
179
+
180
+ #modal-body Label {
181
+ margin-bottom: 1;
182
+ }
183
+
184
+ #modal-body Checkbox {
185
+ margin-top: 1;
186
+ }
187
+
188
+ #tool-title {
189
+ text-style: bold;
190
+ color: $primary;
191
+ margin-bottom: 1;
192
+ }
193
+
194
+ #actions {
195
+ margin-top: 1;
196
+ align: center middle;
197
+ }
198
+
199
+ #actions Button {
200
+ margin: 0 1;
201
+ }
202
+
203
+ /* ============================================
204
+ * RICH PANELS - Tool/Error/Search Display
205
+ * NeXTSTEP: "Objects that look the same should act the same"
206
+ * ============================================ */
207
+
208
+ /* Tool execution panels - cyan accent */
209
+ .tool-panel {
210
+ border: solid $primary;
211
+ background: $surface;
212
+ padding: 0 1;
213
+ margin: 0 0 1 0;
214
+ }
215
+
216
+ .tool-panel.running {
217
+ border: solid $accent;
218
+ }
219
+
220
+ .tool-panel.completed {
221
+ border: solid $success;
222
+ }
223
+
224
+ .tool-panel.failed {
225
+ border: solid $error;
226
+ }
227
+
228
+ /* Error panels - red border, recovery actions */
229
+ .error-panel {
230
+ border: solid $error;
231
+ background: $surface;
232
+ padding: 0 1;
233
+ margin: 0 0 1 0;
234
+ }
235
+
236
+ .error-panel.warning {
237
+ border: solid $warning;
238
+ }
239
+
240
+ .error-panel.info {
241
+ border: solid $secondary;
242
+ }
243
+
244
+ /* Search result panels - accent border */
245
+ .search-panel {
246
+ border: solid $accent;
247
+ background: $surface;
248
+ padding: 0 1;
249
+ margin: 0 0 1 0;
250
+ }
251
+
252
+ /* Pagination controls in search */
253
+ .pagination {
254
+ height: 1;
255
+ padding: 0 1;
256
+ color: $text-muted;
257
+ }
258
+
259
+ /* Result item highlighting */
260
+ .result-item {
261
+ padding: 0 1;
262
+ }
263
+
264
+ .result-item:hover {
265
+ background: $surface;
266
+ }
267
+
268
+ .result-item.selected {
269
+ background: $primary 20%;
270
+ border-left: solid $primary;
271
+ }
272
+
273
+ /* Recovery command styling */
274
+ .recovery-command {
275
+ color: $primary;
276
+ text-style: underline;
277
+ }
278
+
279
+ /* Context detail styling */
280
+ .context-detail {
281
+ color: $text-muted;
282
+ padding-left: 2;
283
+ }
@@ -0,0 +1 @@
1
+ # Config package
@@ -0,0 +1,45 @@
1
+ """
2
+ Module: tunacode.configuration.defaults
3
+
4
+ Default configuration values for the TunaCode CLI.
5
+ Provides sensible defaults for user configuration and environment variables.
6
+ """
7
+
8
+ from tunacode.constants import ENV_OPENAI_BASE_URL, GUIDE_FILE_NAME
9
+ from tunacode.types import UserConfig
10
+
11
+ DEFAULT_USER_CONFIG: UserConfig = {
12
+ "default_model": "openrouter:openai/gpt-4.1",
13
+ "env": {
14
+ "ANTHROPIC_API_KEY": "",
15
+ "GEMINI_API_KEY": "",
16
+ "OPENAI_API_KEY": "",
17
+ ENV_OPENAI_BASE_URL: "",
18
+ "OPENROUTER_API_KEY": "",
19
+ },
20
+ "settings": {
21
+ "max_retries": 3,
22
+ "max_iterations": 40,
23
+ "request_delay": 0.0,
24
+ "global_request_timeout": 90.0,
25
+ "tool_ignore": [],
26
+ "guide_file": GUIDE_FILE_NAME,
27
+ "fallback_response": True,
28
+ "fallback_verbosity": "normal", # Options: minimal, normal, detailed
29
+ "context_window_size": 200000,
30
+ "enable_streaming": True, # Always enable streaming
31
+ "theme": "dracula", # UI theme name
32
+ "ripgrep": {
33
+ "timeout": 10, # Search timeout in seconds
34
+ "max_buffer_size": 1048576, # 1MB max output buffer
35
+ "max_results": 100, # Maximum results per search
36
+ "enable_metrics": False, # Enable performance metrics collection
37
+ "debug": False, # Enable debug logging for ripgrep operations
38
+ },
39
+ "lsp": {
40
+ "enabled": True, # Requires pyright: uv pip install pyright
41
+ "timeout": 5.0, # Maximum seconds to wait for diagnostics
42
+ "max_diagnostics": 20, # Maximum number of diagnostics to show
43
+ },
44
+ },
45
+ }
@@ -0,0 +1,147 @@
1
+ """
2
+ Module: tunacode.configuration.models
3
+
4
+ Configuration for loading model data from models_registry.json.
5
+ """
6
+
7
+ from tunacode.constants import DEFAULT_CONTEXT_WINDOW
8
+
9
+ # --- Models.dev Registry Functions ---
10
+
11
+ MODELS_REGISTRY_FILE_NAME = "models_registry.json"
12
+
13
+ _models_registry_cache: dict | None = None
14
+
15
+
16
+ def parse_model_string(model_string: str) -> tuple[str, str]:
17
+ """Parse 'provider:model_id' into (provider_id, model_id).
18
+
19
+ Args:
20
+ model_string: Full model identifier (e.g., "openrouter:openai/gpt-4.1")
21
+
22
+ Returns:
23
+ Tuple of (provider_id, model_id)
24
+
25
+ Raises:
26
+ ValueError: If model_string doesn't contain a colon separator
27
+ """
28
+ if ":" not in model_string:
29
+ raise ValueError(f"Invalid model string format: {model_string}")
30
+ parts = model_string.split(":", 1)
31
+ return (parts[0], parts[1])
32
+
33
+
34
+ def load_models_registry() -> dict:
35
+ """Load bundled models.dev registry from JSON file.
36
+
37
+ Returns cached data on subsequent calls for performance.
38
+ """
39
+ global _models_registry_cache
40
+ if _models_registry_cache is not None:
41
+ return _models_registry_cache
42
+
43
+ import json
44
+ from pathlib import Path
45
+
46
+ registry_path = Path(__file__).parent / MODELS_REGISTRY_FILE_NAME
47
+ with open(registry_path) as f:
48
+ _models_registry_cache = json.load(f)
49
+ return _models_registry_cache
50
+
51
+
52
+ def get_cached_models_registry() -> dict | None:
53
+ """Return cached registry data if already loaded."""
54
+ return _models_registry_cache
55
+
56
+
57
+ def get_providers() -> list[tuple[str, str]]:
58
+ """Return (display_name, id) tuples for all providers.
59
+
60
+ Sorted alphabetically by display name.
61
+ """
62
+ registry = load_models_registry()
63
+ providers = [(p["name"], p["id"]) for p in registry.values()]
64
+ return sorted(providers, key=lambda x: x[0].lower())
65
+
66
+
67
+ def get_models_for_provider(provider_id: str) -> list[tuple[str, str]]:
68
+ """Return (display_name, id) tuples for a provider's models.
69
+
70
+ Args:
71
+ provider_id: The provider identifier (e.g., "openai", "anthropic")
72
+
73
+ Returns:
74
+ List of (model_name, model_id) tuples, sorted alphabetically.
75
+ """
76
+ registry = load_models_registry()
77
+ provider = registry.get(provider_id, {})
78
+ models = provider.get("models", {})
79
+ result = [(m["name"], mid) for mid, m in models.items()]
80
+ return sorted(result, key=lambda x: x[0].lower())
81
+
82
+
83
+ def get_provider_env_var(provider_id: str) -> str:
84
+ """Return the environment variable name for a provider's API key.
85
+
86
+ Args:
87
+ provider_id: The provider identifier
88
+
89
+ Returns:
90
+ Environment variable name (e.g., "OPENAI_API_KEY")
91
+ """
92
+ registry = get_cached_models_registry()
93
+ if registry is None:
94
+ return f"{provider_id.upper()}_API_KEY"
95
+
96
+ provider = registry.get(provider_id, {})
97
+ env_vars = provider.get("env", [])
98
+ if env_vars:
99
+ return env_vars[0]
100
+ return f"{provider_id.upper()}_API_KEY"
101
+
102
+
103
+ def get_provider_base_url(provider_id: str) -> str | None:
104
+ """Return the API base URL for a provider.
105
+
106
+ Args:
107
+ provider_id: The provider identifier
108
+
109
+ Returns:
110
+ Base URL string or None if not specified
111
+ """
112
+ registry = get_cached_models_registry()
113
+ if registry is None:
114
+ return None
115
+
116
+ provider = registry.get(provider_id, {})
117
+ return provider.get("api")
118
+
119
+
120
+ def get_model_context_window(model_string: str) -> int:
121
+ """Get context window limit for a model from cached models_registry data.
122
+
123
+ Args:
124
+ model_string: Full model identifier (e.g., "openrouter:openai/gpt-4.1")
125
+
126
+ Returns:
127
+ Context window size in tokens. Falls back to DEFAULT_CONTEXT_WINDOW
128
+ if registry is not loaded, model not found, or limit not specified.
129
+ """
130
+ registry = get_cached_models_registry()
131
+ if registry is None:
132
+ return DEFAULT_CONTEXT_WINDOW
133
+
134
+ try:
135
+ provider_id, model_id = parse_model_string(model_string)
136
+ except ValueError:
137
+ return DEFAULT_CONTEXT_WINDOW
138
+
139
+ provider = registry.get(provider_id, {})
140
+ model = provider.get("models", {}).get(model_id, {})
141
+ limit = model.get("limit", {})
142
+
143
+ context = limit.get("context")
144
+ if context is None:
145
+ return DEFAULT_CONTEXT_WINDOW
146
+
147
+ return context