bone-agent 1.3.0

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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/bin/npm-wrapper.js +235 -0
  4. package/bin/rg +0 -0
  5. package/bin/rg.exe +0 -0
  6. package/config.yaml.example +133 -0
  7. package/package.json +53 -0
  8. package/requirements.txt +9 -0
  9. package/src/__init__.py +11 -0
  10. package/src/core/__init__.py +1 -0
  11. package/src/core/agentic.py +1054 -0
  12. package/src/core/chat_manager.py +1552 -0
  13. package/src/core/config_manager.py +247 -0
  14. package/src/core/cron.py +527 -0
  15. package/src/core/cron_allowlist.py +118 -0
  16. package/src/core/memory.py +232 -0
  17. package/src/core/retry.py +71 -0
  18. package/src/core/sub_agent.py +326 -0
  19. package/src/core/tool_approval.py +220 -0
  20. package/src/core/tool_feedback.py +778 -0
  21. package/src/exceptions.py +79 -0
  22. package/src/llm/__init__.py +1 -0
  23. package/src/llm/client.py +171 -0
  24. package/src/llm/config.py +466 -0
  25. package/src/llm/prompts.py +735 -0
  26. package/src/llm/providers.py +417 -0
  27. package/src/llm/streaming.py +163 -0
  28. package/src/llm/token_tracker.py +368 -0
  29. package/src/tools/__init__.py +212 -0
  30. package/src/tools/constants.py +59 -0
  31. package/src/tools/create_file.py +136 -0
  32. package/src/tools/directory.py +389 -0
  33. package/src/tools/edit.py +543 -0
  34. package/src/tools/file_reader.py +322 -0
  35. package/src/tools/helpers/__init__.py +105 -0
  36. package/src/tools/helpers/base.py +550 -0
  37. package/src/tools/helpers/converters.py +44 -0
  38. package/src/tools/helpers/file_helpers.py +189 -0
  39. package/src/tools/helpers/formatters.py +411 -0
  40. package/src/tools/helpers/loader.py +231 -0
  41. package/src/tools/helpers/parallel_executor.py +231 -0
  42. package/src/tools/helpers/path_resolver.py +226 -0
  43. package/src/tools/helpers/plugin_manifest.py +156 -0
  44. package/src/tools/obsidian.py +96 -0
  45. package/src/tools/review_sub_agent.py +189 -0
  46. package/src/tools/rg_search.py +393 -0
  47. package/src/tools/search_plugins.py +109 -0
  48. package/src/tools/select_option.py +593 -0
  49. package/src/tools/shell.py +302 -0
  50. package/src/tools/sub_agent.py +139 -0
  51. package/src/tools/task_list.py +269 -0
  52. package/src/tools/web_search.py +61 -0
  53. package/src/ui/__init__.py +1 -0
  54. package/src/ui/banner.py +87 -0
  55. package/src/ui/commands.py +2694 -0
  56. package/src/ui/displays.py +213 -0
  57. package/src/ui/loader.py +284 -0
  58. package/src/ui/main.py +646 -0
  59. package/src/ui/prompt_utils.py +113 -0
  60. package/src/ui/setting_selector.py +590 -0
  61. package/src/ui/setup_wizard.py +294 -0
  62. package/src/ui/sub_agent_panel.py +234 -0
  63. package/src/ui/tool_confirmation.py +215 -0
  64. package/src/utils/__init__.py +1 -0
  65. package/src/utils/citation_parser.py +199 -0
  66. package/src/utils/editor.py +158 -0
  67. package/src/utils/gitignore_filter.py +149 -0
  68. package/src/utils/logger.py +254 -0
  69. package/src/utils/paths.py +30 -0
  70. package/src/utils/result_parsers.py +108 -0
  71. package/src/utils/safe_commands.py +243 -0
  72. package/src/utils/settings.py +174 -0
  73. package/src/utils/validation.py +191 -0
  74. package/src/utils/web_search.py +173 -0
@@ -0,0 +1,466 @@
1
+ import os
2
+ import platform
3
+ from pathlib import Path
4
+ import yaml
5
+
6
+ # Provider selection - loaded from config (see after PROVIDER_REGISTRY definition)
7
+
8
+ def resolve_config_path() -> Path:
9
+ """Resolve config.yaml path.
10
+
11
+ Resolution order:
12
+ 1. BONE_CONFIG_PATH — explicit user config path (set by npm wrapper)
13
+ 2. ~/.bone/config.yaml — user home dotfile (dev and npm installs)
14
+ """
15
+ _user_cfg = os.environ.get('BONE_CONFIG_PATH')
16
+ if _user_cfg:
17
+ return Path(_user_cfg).resolve()
18
+ return Path.home() / '.bone' / 'config.yaml'
19
+
20
+ # Module-level config path (single source of truth)
21
+ CONFIG_PATH = resolve_config_path()
22
+
23
+ # Environment variable names for API keys (env vars take precedence over config file)
24
+ ENV_API_KEYS = {
25
+ 'ANTHROPIC_API_KEY': os.environ.get('ANTHROPIC_API_KEY'),
26
+ 'OPENAI_API_KEY': os.environ.get('OPENAI_API_KEY'),
27
+ 'GLM_PLAN_API_KEY': os.environ.get('GLM_PLAN_API_KEY'),
28
+ 'GLM_API_KEY': os.environ.get('GLM_API_KEY'),
29
+ 'GEMINI_API_KEY': os.environ.get('GEMINI_API_KEY'),
30
+ 'OPENROUTER_API_KEY': os.environ.get('OPENROUTER_API_KEY'),
31
+ 'KIMI_API_KEY': os.environ.get('KIMI_API_KEY'),
32
+ 'MINIMAX_PLAN_API_KEY': os.environ.get('MINIMAX_PLAN_API_KEY'),
33
+ 'MINIMAX_API_KEY': os.environ.get('MINIMAX_API_KEY'),
34
+ 'BONE_PROXY_API_KEY': os.environ.get('BONE_PROXY_API_KEY'),
35
+ }
36
+
37
+ # Detect platform for llama.cpp paths
38
+ _IS_WINDOWS = platform.system() == "Windows"
39
+ _IS_LINUX = platform.system() == "Linux"
40
+
41
+ # Set llama.cpp paths based on platform
42
+ if _IS_WINDOWS:
43
+ _LLAMA_SERVER_NAME = "llama-server.exe"
44
+ _LLAMA_BUILD_DIR = "build"
45
+ elif _IS_LINUX:
46
+ _LLAMA_SERVER_NAME = "llama-server"
47
+ _LLAMA_BUILD_DIR = "build-linux"
48
+ else:
49
+ # Fallback for macOS or other platforms
50
+ _LLAMA_SERVER_NAME = "llama-server"
51
+ _LLAMA_BUILD_DIR = "build"
52
+
53
+ def _load_config():
54
+ """Load config from YAML file, with environment variable overrides for API keys.
55
+
56
+ Environment variables take precedence over values in config.yaml.
57
+ """
58
+ config_path = resolve_config_path()
59
+ if not config_path.exists():
60
+ return {}
61
+ try:
62
+ config = yaml.safe_load(config_path.read_text(encoding="utf-8-sig")) or {}
63
+ except yaml.YAMLError:
64
+ config = {}
65
+
66
+ # Override API keys from environment variables (env vars take precedence)
67
+ for key, env_value in ENV_API_KEYS.items():
68
+ if env_value: # Only override if env var is set and non-empty
69
+ config[key] = env_value
70
+
71
+ return config
72
+
73
+
74
+ _CONFIG = _load_config()
75
+
76
+ # Cache for provider registry (built once at module load)
77
+ _provider_registry_cache = None
78
+ _cached_provider = None
79
+
80
+
81
+ def get_model_cost(model_name: str, config: dict | None = None) -> tuple[float, float]:
82
+ """Get model-specific (cost_in, cost_out) per 1M tokens from MODEL_PRICES.
83
+
84
+ Shared utility — used by both the provider registry builder and config_manager.
85
+ Returns (0.0, 0.0) for unknown models.
86
+
87
+ Args:
88
+ model_name: Model name to look up.
89
+ config: Optional config dict (defaults to _CONFIG). Pass a fresh config
90
+ after runtime edits to get up-to-date pricing.
91
+ """
92
+ model_prices = (config if config is not None else _CONFIG).get("MODEL_PRICES", {})
93
+ if model_name in model_prices:
94
+ mc = model_prices[model_name]
95
+ return float(mc.get("cost_in", 0.0)), float(mc.get("cost_out", 0.0))
96
+ return 0.0, 0.0
97
+
98
+
99
+ def _model_cost(model_config_key):
100
+ """Return {cost_in, cost_out} dict for use in provider registry via **spread.
101
+
102
+ Reads from module-level _CONFIG (not an injected config). Safe because
103
+ reload_config() invalidates the provider registry cache, forcing a rebuild.
104
+ """
105
+ ci, co = get_model_cost(_CONFIG.get(model_config_key, ""))
106
+ return {"cost_in": ci, "cost_out": co}
107
+
108
+
109
+ def _get_provider_registry():
110
+ """Build PROVIDER_REGISTRY from current config (cached)."""
111
+ global _provider_registry_cache
112
+ if _provider_registry_cache is not None:
113
+ return _provider_registry_cache
114
+
115
+ _provider_registry_cache = {
116
+ "local": {
117
+ "type": "local",
118
+ "api_key": None,
119
+ "model": _CONFIG.get("LOCAL_MODEL_PATH", ""),
120
+ "api_model": "model",
121
+ "api_base": "http://127.0.0.1:8080",
122
+ "endpoint": "/v1/chat/completions",
123
+ "error_prefix": "local server",
124
+ "config_keys": {
125
+ "LOCAL_MODEL_PATH": "",
126
+ "LOCAL_SERVER_PATH": str(
127
+ Path(__file__).resolve().parents[2] /
128
+ f"llama.cpp/{_LLAMA_BUILD_DIR}/bin/{_LLAMA_SERVER_NAME}"
129
+ ),
130
+ },
131
+ "extra": {
132
+ "host": "127.0.0.1",
133
+ "port": 8080,
134
+ },
135
+ "default_temperature": 0.1,
136
+ "default_top_p": 0.9,
137
+ "allow_top_p": True,
138
+ "allow_temperature": True,
139
+ "cost_in": 0.0,
140
+ "cost_out": 0.0
141
+ },
142
+ "openrouter": {
143
+ "type": "api",
144
+ "api_key": _CONFIG.get("OPENROUTER_API_KEY", ""),
145
+ "model": _CONFIG.get("OPENROUTER_MODEL", ""),
146
+ "api_base": _CONFIG.get("OPENROUTER_API_BASE", "https://openrouter.ai/api/v1"),
147
+ "endpoint": "/chat/completions",
148
+ "error_prefix": "OpenRouter",
149
+ "headers_extra": {
150
+ "HTTP-Referer": "http://localhost:8080",
151
+ "X-Title": "Chat App"
152
+ },
153
+ "config_keys": {
154
+ "OPENROUTER_API_KEY": "",
155
+ "OPENROUTER_MODEL": "",
156
+ "OPENROUTER_API_BASE": "https://openrouter.ai/api/v1",
157
+ },
158
+ "default_temperature": 0.1,
159
+ "default_top_p": 0.9,
160
+ "allow_top_p": True,
161
+ "allow_temperature": True,
162
+ **_model_cost("OPENROUTER_MODEL"),
163
+ },
164
+ "glm_plan": {
165
+ "type": "api",
166
+ "api_key": _CONFIG.get("GLM_PLAN_API_KEY", ""),
167
+ "model": _CONFIG.get("GLM_PLAN_MODEL", ""),
168
+ "api_base": _CONFIG.get("GLM_PLAN_API_BASE", "https://open.bigmodel.cn/api/paas/v4"),
169
+ "endpoint": "/chat/completions",
170
+ "error_prefix": "GLM",
171
+ "config_keys": {
172
+ "GLM_PLAN_API_KEY": "",
173
+ "GLM_PLAN_MODEL": "",
174
+ "GLM_PLAN_API_BASE": "https://open.bigmodel.cn/api/paas/v4",
175
+ },
176
+ "default_temperature": 0.1,
177
+ "default_top_p": 0.9,
178
+ "allow_top_p": True,
179
+ "allow_temperature": True,
180
+ **_model_cost("GLM_PLAN_MODEL"),
181
+ },
182
+ "glm": {
183
+ "type": "api",
184
+ "api_key": _CONFIG.get("GLM_API_KEY", ""),
185
+ "model": _CONFIG.get("GLM_MODEL", ""),
186
+ "api_base": _CONFIG.get("GLM_API_BASE", "https://open.bigmodel.cn/api/paas/v4"),
187
+ "endpoint": "/chat/completions",
188
+ "error_prefix": "GLM",
189
+ "config_keys": {
190
+ "GLM_API_KEY": "",
191
+ "GLM_MODEL": "",
192
+ "GLM_API_BASE": "https://open.bigmodel.cn/api/paas/v4",
193
+ },
194
+ "default_temperature": 0.1,
195
+ "default_top_p": 0.9,
196
+ "allow_top_p": True,
197
+ "allow_temperature": True,
198
+ **_model_cost("GLM_MODEL"),
199
+ },
200
+ "openai": {
201
+ "type": "api",
202
+ "api_key": _CONFIG.get("OPENAI_API_KEY", ""),
203
+ "model": _CONFIG.get("OPENAI_MODEL", ""),
204
+ "api_base": _CONFIG.get("OPENAI_API_BASE", "https://api.openai.com/v1"),
205
+ "endpoint": "/chat/completions",
206
+ "error_prefix": "OpenAI",
207
+ "config_keys": {
208
+ "OPENAI_API_KEY": "",
209
+ "OPENAI_MODEL": "",
210
+ "OPENAI_API_BASE": "https://api.openai.com/v1",
211
+ },
212
+ "default_temperature": 0.1,
213
+ "default_top_p": 0.9,
214
+ "allow_top_p": False,
215
+ "allow_temperature": False,
216
+ **_model_cost("OPENAI_MODEL"),
217
+ },
218
+ "gemini": {
219
+ "type": "api",
220
+ "api_key": _CONFIG.get("GEMINI_API_KEY", ""),
221
+ "model": _CONFIG.get("GEMINI_MODEL", ""),
222
+ "api_base": _CONFIG.get("GEMINI_API_BASE", "https://generativelanguage.googleapis.com/v1beta"),
223
+ "endpoint": "/chat/completions",
224
+ "error_prefix": "Gemini",
225
+ "config_keys": {
226
+ "GEMINI_API_KEY": "",
227
+ "GEMINI_MODEL": "",
228
+ "GEMINI_API_BASE": "https://generativelanguage.googleapis.com/v1beta",
229
+ },
230
+ "default_temperature": 0.1,
231
+ "default_top_p": 0.9,
232
+ "allow_top_p": True,
233
+ "allow_temperature": True,
234
+ **_model_cost("GEMINI_MODEL"),
235
+ },
236
+ "minimax_plan": {
237
+ "type": "api",
238
+ "api_key": _CONFIG.get("MINIMAX_PLAN_API_KEY", ""),
239
+ "model": _CONFIG.get("MINIMAX_PLAN_MODEL", ""),
240
+ "api_base": _CONFIG.get("MINIMAX_PLAN_API_BASE", "https://api.minimax.io/anthropic/v1"),
241
+ "endpoint": "/messages",
242
+ "error_prefix": "MiniMax",
243
+ "config_keys": {
244
+ "MINIMAX_PLAN_API_KEY": "",
245
+ "MINIMAX_PLAN_MODEL": "",
246
+ "MINIMAX_PLAN_API_BASE": "https://api.minimax.io/anthropic/v1",
247
+ },
248
+ "default_temperature": 0.1,
249
+ "default_top_p": 0.9,
250
+ "allow_top_p": False,
251
+ "allow_temperature": True,
252
+ "max_tokens": 4096,
253
+ **_model_cost("MINIMAX_PLAN_MODEL"),
254
+ },
255
+ "minimax": {
256
+ "type": "api",
257
+ "api_key": _CONFIG.get("MINIMAX_API_KEY", ""),
258
+ "model": _CONFIG.get("MINIMAX_MODEL", ""),
259
+ "api_base": _CONFIG.get("MINIMAX_API_BASE", "https://api.minimax.io/anthropic/v1"),
260
+ "endpoint": "/messages",
261
+ "error_prefix": "MiniMax",
262
+ "config_keys": {
263
+ "MINIMAX_API_KEY": "",
264
+ "MINIMAX_MODEL": "",
265
+ "MINIMAX_API_BASE": "https://api.minimax.io/anthropic/v1",
266
+ },
267
+ "default_temperature": 0.1,
268
+ "default_top_p": 0.9,
269
+ "allow_top_p": False,
270
+ "allow_temperature": True,
271
+ "max_tokens": 4096,
272
+ **_model_cost("MINIMAX_MODEL"),
273
+ },
274
+ "anthropic": {
275
+ "type": "api",
276
+ "api_key": _CONFIG.get("ANTHROPIC_API_KEY", ""),
277
+ "model": _CONFIG.get("ANTHROPIC_MODEL", ""),
278
+ "api_base": _CONFIG.get("ANTHROPIC_API_BASE", "https://api.anthropic.com/v1"),
279
+ "endpoint": "/messages",
280
+ "error_prefix": "Anthropic",
281
+ "headers_extra": {
282
+ "anthropic-version": "2023-06-01"
283
+ },
284
+ "config_keys": {
285
+ "ANTHROPIC_API_KEY": "",
286
+ "ANTHROPIC_MODEL": "",
287
+ "ANTHROPIC_API_BASE": "https://api.anthropic.com/v1",
288
+ },
289
+ "default_temperature": 0.1,
290
+ "default_top_p": 0.9,
291
+ "allow_top_p": False,
292
+ "allow_temperature": True,
293
+ "max_tokens": 4096,
294
+ **_model_cost("ANTHROPIC_MODEL"),
295
+ },
296
+ "kimi": {
297
+ "type": "api",
298
+ "api_key": _CONFIG.get("KIMI_API_KEY", ""),
299
+ "model": _CONFIG.get("KIMI_MODEL", ""),
300
+ "api_base": _CONFIG.get("KIMI_API_BASE", "https://api.moonshot.cn/v1"),
301
+ "endpoint": "/chat/completions",
302
+ "error_prefix": "Kimi",
303
+ "config_keys": {
304
+ "KIMI_API_KEY": "",
305
+ "KIMI_MODEL": "",
306
+ "KIMI_API_BASE": "https://api.moonshot.cn/v1",
307
+ },
308
+ "default_temperature": 0.1,
309
+ "default_top_p": 0.9,
310
+ "allow_top_p": True,
311
+ "allow_temperature": True,
312
+ **_model_cost("KIMI_MODEL"),
313
+ },
314
+ "bone": {
315
+ "type": "api",
316
+ "api_key": _CONFIG.get("BONE_PROXY_API_KEY", ""),
317
+ "model": _CONFIG.get("BONE_PROXY_MODEL", "openai/gpt-4o-mini"),
318
+ "api_base": _CONFIG.get("BONE_PROXY_API_BASE", "https://api.vmcode.dev"),
319
+ "endpoint": "/v1/chat/completions",
320
+ "error_prefix": "bone-agent Proxy",
321
+ "config_keys": {
322
+ "BONE_PROXY_API_KEY": "",
323
+ "BONE_PROXY_MODEL": "openai/gpt-4o-mini",
324
+ "BONE_PROXY_API_BASE": "https://api.vmcode.dev",
325
+ },
326
+ "default_temperature": 0.1,
327
+ "default_top_p": 0.9,
328
+ "allow_top_p": True,
329
+ "allow_temperature": True,
330
+ **_model_cost("BONE_PROXY_MODEL"),
331
+ },
332
+ }
333
+ return _provider_registry_cache
334
+
335
+
336
+ def _get_provider():
337
+ """Get the current provider from config (cached)."""
338
+ global _cached_provider
339
+ if _cached_provider is not None:
340
+ return _cached_provider
341
+
342
+ last_provider = _CONFIG.get("LAST_PROVIDER")
343
+ if last_provider and last_provider in _provider_registry_cache:
344
+ _cached_provider = last_provider
345
+ return _cached_provider
346
+ _cached_provider = "glm_plan"
347
+ return _cached_provider
348
+
349
+
350
+ def reload_config():
351
+ """Reload config from disk and invalidate caches.
352
+
353
+ Reloads both the config.yaml file and environment variables.
354
+
355
+ Note: This is a manual operation - call after config changes.
356
+ """
357
+ global _CONFIG, _provider_registry_cache, _cached_provider, PROVIDER_REGISTRY, LLM_PROVIDER, STATUS_BAR_SETTINGS
358
+ _CONFIG = _load_config()
359
+ _provider_registry_cache = None
360
+ _cached_provider = None
361
+ # Rebuild module-level variables
362
+ PROVIDER_REGISTRY = _get_provider_registry()
363
+ LLM_PROVIDER = _get_provider()
364
+ # Rebuild status bar settings
365
+ STATUS_BAR_SETTINGS = _build_status_bar_settings()
366
+
367
+
368
+ def _build_status_bar_settings():
369
+ """Build STATUS_BAR_SETTINGS dict from current _CONFIG."""
370
+ sbs = _CONFIG.get("STATUS_BAR_SETTINGS", {})
371
+ return {
372
+ "show_curr_tokens": sbs.get("show_curr_tokens", True),
373
+ "show_in_tokens": sbs.get("show_in_tokens", True),
374
+ "show_out_tokens": sbs.get("show_out_tokens", True),
375
+ "show_total_tokens": sbs.get("show_total_tokens", True),
376
+ "show_cost": sbs.get("show_cost", True),
377
+ }
378
+
379
+
380
+ def update_status_bar_settings(settings_dict):
381
+ """Update STATUS_BAR_SETTINGS at runtime and persist to config.
382
+
383
+ Args:
384
+ settings_dict: Dict of settings to update (e.g., {"show_cost": False})
385
+
386
+ Returns:
387
+ Updated STATUS_BAR_SETTINGS dict
388
+ """
389
+ global STATUS_BAR_SETTINGS
390
+ STATUS_BAR_SETTINGS.update(settings_dict)
391
+ return STATUS_BAR_SETTINGS
392
+
393
+
394
+ def get_providers():
395
+ """Get list of available providers.
396
+
397
+ Returns:
398
+ list: List of provider names from PROVIDER_REGISTRY.
399
+ """
400
+ return list(PROVIDER_REGISTRY.keys())
401
+
402
+
403
+ # ============================================================================
404
+ # PROVIDER REGISTRY - Centralized provider configuration
405
+ # ============================================================================
406
+
407
+ # Build provider registry and export as module-level constants (loaded once)
408
+ PROVIDER_REGISTRY = _get_provider_registry()
409
+ LLM_PROVIDER = _get_provider()
410
+
411
+
412
+ __all__ = [
413
+ "CONFIG_PATH",
414
+ "PROVIDER_REGISTRY",
415
+ "get_providers",
416
+ "get_model_cost",
417
+ "LLM_PROVIDER",
418
+ "TOOLS_ENABLED",
419
+ "TOOLS_REQUIRE_CONFIRMATION",
420
+ "WEB_SEARCH_REQUIRE_CONFIRMATION",
421
+ "APPROVE_MODES",
422
+ "APPROVE_MODE_LABELS",
423
+
424
+ "get_provider_config",
425
+ "generate_config_template",
426
+ "reload_config",
427
+ "STATUS_BAR_SETTINGS",
428
+ "update_status_bar_settings",
429
+ ]
430
+
431
+
432
+ def generate_config_template():
433
+ """Generate default template for config.json from provider registry."""
434
+ template = {}
435
+ for provider, config in PROVIDER_REGISTRY.items():
436
+ if "config_keys" in config:
437
+ template.update(config["config_keys"])
438
+ return template
439
+
440
+ # Tooling configuration
441
+ TOOLS_ENABLED = True
442
+ TOOLS_REQUIRE_CONFIRMATION = False
443
+ WEB_SEARCH_REQUIRE_CONFIRMATION = False
444
+
445
+ # Status bar configuration
446
+ STATUS_BAR_SETTINGS = _build_status_bar_settings()
447
+
448
+ # Tool approval modes
449
+ APPROVE_MODES = ("safe", "accept_edits", "danger")
450
+ CYCLEABLE_APPROVE_MODES = ("safe", "accept_edits")
451
+ APPROVE_MODE_LABELS = {
452
+ "safe": "Safe",
453
+ "accept_edits": "Accept Edits",
454
+ "danger": "Danger",
455
+ }
456
+
457
+ def get_provider_config(provider: str):
458
+ """Retrieve the configuration dictionary for a given provider.
459
+
460
+ Args:
461
+ provider (str): Provider name (e.g., 'local', 'openrouter', 'glm', 'openai').
462
+
463
+ Returns:
464
+ dict: Provider config from the PROVIDER_REGISTRY.
465
+ """
466
+ return PROVIDER_REGISTRY.get(provider, {})