npcpy 1.1.28__py3-none-any.whl → 1.2.32__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.
Files changed (44) hide show
  1. npcpy/data/audio.py +16 -38
  2. npcpy/data/image.py +29 -29
  3. npcpy/data/load.py +4 -3
  4. npcpy/data/text.py +28 -28
  5. npcpy/data/video.py +6 -6
  6. npcpy/data/web.py +49 -21
  7. npcpy/ft/__init__.py +0 -0
  8. npcpy/ft/diff.py +110 -0
  9. npcpy/ft/ge.py +115 -0
  10. npcpy/ft/memory_trainer.py +171 -0
  11. npcpy/ft/model_ensembler.py +357 -0
  12. npcpy/ft/rl.py +360 -0
  13. npcpy/ft/sft.py +248 -0
  14. npcpy/ft/usft.py +128 -0
  15. npcpy/gen/audio_gen.py +24 -0
  16. npcpy/gen/embeddings.py +13 -13
  17. npcpy/gen/image_gen.py +37 -15
  18. npcpy/gen/response.py +287 -111
  19. npcpy/gen/video_gen.py +10 -9
  20. npcpy/llm_funcs.py +447 -79
  21. npcpy/memory/command_history.py +201 -48
  22. npcpy/memory/kg_vis.py +74 -74
  23. npcpy/memory/knowledge_graph.py +482 -115
  24. npcpy/memory/memory_processor.py +81 -0
  25. npcpy/memory/search.py +70 -70
  26. npcpy/mix/debate.py +192 -3
  27. npcpy/npc_compiler.py +1541 -879
  28. npcpy/npc_sysenv.py +250 -78
  29. npcpy/serve.py +1036 -321
  30. npcpy/sql/ai_function_tools.py +257 -0
  31. npcpy/sql/database_ai_adapters.py +186 -0
  32. npcpy/sql/database_ai_functions.py +163 -0
  33. npcpy/sql/model_runner.py +19 -19
  34. npcpy/sql/npcsql.py +706 -507
  35. npcpy/sql/sql_model_compiler.py +156 -0
  36. npcpy/tools.py +20 -20
  37. npcpy/work/plan.py +8 -8
  38. npcpy/work/trigger.py +3 -3
  39. {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/METADATA +169 -9
  40. npcpy-1.2.32.dist-info/RECORD +54 -0
  41. npcpy-1.1.28.dist-info/RECORD +0 -40
  42. {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
  43. {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
  44. {npcpy-1.1.28.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
npcpy/npc_sysenv.py CHANGED
@@ -4,16 +4,18 @@ import logging
4
4
  import re
5
5
  import os
6
6
  import socket
7
- import concurrent.futures # For managing timeouts on blocking calls
7
+ import concurrent.futures
8
8
  import platform
9
9
  import sqlite3
10
10
  import sys
11
11
  from typing import Dict, List
12
12
  import textwrap
13
13
  import json
14
+
15
+
16
+ import requests
14
17
  ON_WINDOWS = platform.system() == "Windows"
15
18
 
16
- # Try/except for termios, tty, pty, select, signal
17
19
  try:
18
20
  if not ON_WINDOWS:
19
21
  import termios
@@ -28,14 +30,14 @@ except ImportError:
28
30
  select = None
29
31
  signal = None
30
32
 
31
- # Try/except for readline
33
+
32
34
  try:
33
35
  import readline
34
36
  except ImportError:
35
37
  readline = None
36
38
  logging.warning('no readline support, some features may not work as desired.')
37
39
 
38
- # Try/except for rich imports
40
+
39
41
  try:
40
42
  from rich.console import Console
41
43
  from rich.markdown import Markdown
@@ -48,7 +50,7 @@ except ImportError:
48
50
  import warnings
49
51
  import time
50
52
 
51
- # Global variables
53
+
52
54
  running = True
53
55
  is_recording = False
54
56
  recording_data = []
@@ -61,7 +63,7 @@ warnings.filterwarnings("ignore", module="torch.serialization")
61
63
  os.environ["PYTHONWARNINGS"] = "ignore"
62
64
  os.environ["SDL_AUDIODRIVER"] = "dummy"
63
65
 
64
- def check_internet_connection(timeout=0.5):
66
+ def check_internet_connection(timeout=5):
65
67
  """
66
68
  Checks for internet connectivity by trying to connect to a well-known host.
67
69
  """
@@ -85,38 +87,143 @@ def get_locally_available_models(project_directory, airplane_mode=False):
85
87
  key, value = line.split("=", 1)
86
88
  env_vars[key.strip()] = value.strip().strip("\"'")
87
89
 
88
- # --- Internet check here ---
89
90
  internet_available = check_internet_connection()
90
91
  if not internet_available:
91
- logging.info("No internet connection detected. External API calls will be skipped (effective airplane_mode).")
92
- # If no internet, force airplane_mode to True, regardless of its initial value.
92
+ logging.info(
93
+ "No internet connection detected. "
94
+ "External API calls will be skipped."
95
+ )
93
96
  airplane_mode = True
94
97
  else:
95
- logging.info("Internet connection detected. Proceeding based on 'airplane_mode' parameter.")
96
- # --- End internet check ---
97
-
98
+ logging.info(
99
+ "Internet connection detected. "
100
+ "Proceeding based on 'airplane_mode' parameter."
101
+ )
102
+
103
+ custom_providers = load_custom_providers()
104
+
105
+ for provider_name, config in custom_providers.items():
106
+ api_key_var = config.get('api_key_var')
107
+ if not api_key_var:
108
+ api_key_var = f"{provider_name.upper()}_API_KEY"
109
+
110
+ if api_key_var in env_vars or os.environ.get(api_key_var):
111
+ try:
112
+ import requests
113
+
114
+ def fetch_custom_models():
115
+ base_url = config.get('base_url', '')
116
+ headers = config.get('headers', {})
117
+
118
+ api_key = env_vars.get(api_key_var) or \
119
+ os.environ.get(api_key_var)
120
+ if api_key:
121
+ headers['Authorization'] = f'Bearer {api_key}'
122
+
123
+ models_endpoint = f"{base_url.rstrip('/')}/models"
124
+ response = requests.get(
125
+ models_endpoint,
126
+ headers=headers,
127
+ timeout=3.5
128
+ )
129
+
130
+ if response.status_code == 200:
131
+ data = response.json()
132
+
133
+ if isinstance(data, dict) and 'data' in data:
134
+ return [
135
+ m['id'] for m in data['data']
136
+ if 'id' in m
137
+ ]
138
+ elif isinstance(data, list):
139
+ return [
140
+ m['id'] for m in data
141
+ if isinstance(m, dict) and 'id' in m
142
+ ]
143
+ return []
144
+
145
+ models = fetch_custom_models()
146
+ for model in models:
147
+ available_models[model] = 'openai-like'
148
+
149
+ logging.info(
150
+ f"Loaded {len(models)} models "
151
+ f"from custom provider '{provider_name}'"
152
+ )
153
+
154
+ except Exception as e:
155
+ logging.warning(
156
+ f"Failed to load models from "
157
+ f"custom provider '{provider_name}': {e}"
158
+ )
159
+
160
+
161
+ airplane_mode = False
98
162
  if not airplane_mode:
99
163
  timeout_seconds = 3.5
100
- # Use a single ThreadPoolExecutor for all network-dependent API calls
164
+
165
+
101
166
  with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
167
+
168
+ if 'NPCSH_API_URL' in env_vars or os.environ.get('NPCSH_API_URL'):
169
+ try:
170
+ import requests
171
+
172
+ def fetch_custom_models():
173
+ base_url = env_vars.get('NPCSH_API_URL') or os.environ.get('NPCSH_API_URL')
174
+ models_endpoint = f"{base_url.rstrip('/')}/models"
175
+ response = requests.get(
176
+ models_endpoint,
177
+
178
+ timeout=3.5
179
+ )
180
+
181
+ if response.status_code == 200:
182
+ data = response.json()
183
+
184
+ if isinstance(data, dict) and 'data' in data:
185
+ return [
186
+ m['id'] for m in data['data']
187
+ if 'id' in m
188
+ ]
189
+ elif isinstance(data, list):
190
+ return [
191
+ m['id'] for m in data
192
+ if isinstance(m, dict) and 'id' in m
193
+ ]
194
+ return []
195
+
196
+ models = fetch_custom_models()
197
+ for model in models:
198
+ available_models[model] = 'openai-like'
199
+
102
200
 
201
+
202
+
203
+ except Exception as e:
204
+ logging.warning(
205
+ f"Failed to load models from "
206
+ f"custom provider 'openai-like': {e}"
207
+ )
208
+
209
+
103
210
  if "ANTHROPIC_API_KEY" in env_vars or os.environ.get("ANTHROPIC_API_KEY"):
104
211
  try:
105
212
  import anthropic
106
213
 
107
214
  def fetch_anthropic_models():
108
215
  client = anthropic.Anthropic(api_key=env_vars.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_API_KEY"))
109
- # Modern Anthropic client can also take a timeout in constructor:
110
- # client = anthropic.Anthropic(api_key=..., timeout=timeout_seconds)
216
+
217
+
111
218
  return client.models.list()
112
219
 
113
220
  future = executor.submit(fetch_anthropic_models)
114
- models = future.result(timeout=timeout_seconds) # Apply timeout
221
+ models = future.result(timeout=timeout_seconds)
115
222
 
116
223
  for model in models.data:
117
224
  available_models[model.id] = 'anthropic'
118
225
 
119
- except (ImportError, anthropic.APIError, concurrent.futures.TimeoutError, Exception) as e:
226
+ except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
120
227
  logging.info(f"Anthropic models not indexed or timed out: {e}")
121
228
 
122
229
  if "OPENAI_API_KEY" in env_vars or os.environ.get("OPENAI_API_KEY"):
@@ -128,7 +235,7 @@ def get_locally_available_models(project_directory, airplane_mode=False):
128
235
  return openai.models.list()
129
236
 
130
237
  future = executor.submit(fetch_openai_models)
131
- models = future.result(timeout=timeout_seconds) # Apply timeout
238
+ models = future.result(timeout=timeout_seconds)
132
239
 
133
240
  for model in models.data:
134
241
  if (
@@ -151,7 +258,7 @@ def get_locally_available_models(project_directory, airplane_mode=False):
151
258
  def fetch_gemini_models():
152
259
  client = genai.Client(api_key=env_vars.get("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY"))
153
260
  found_models = []
154
- # Define the model names you want to filter for
261
+
155
262
  target_models = [
156
263
  'gemini-2.5-pro',
157
264
  'gemini-2.5-flash',
@@ -165,15 +272,15 @@ def get_locally_available_models(project_directory, airplane_mode=False):
165
272
  for action in m.supported_actions:
166
273
  if action == "generateContent":
167
274
  if 'models/' in m.name:
168
- model_name_part = m.name.split('/')[1] # Extract the part after 'models/'
169
- # Check if any of the target models are contained within the model name
275
+ model_name_part = m.name.split('/')[1]
276
+
170
277
  if any(model in model_name_part for model in target_models):
171
278
  found_models.append(model_name_part)
172
279
  return set(found_models)
173
280
  future = executor.submit(fetch_gemini_models)
174
- models = future.result(timeout=timeout_seconds) # Apply timeout
281
+ models = future.result(timeout=timeout_seconds)
175
282
 
176
- for model in models: # 'models' is already a set of strings here
283
+ for model in models:
177
284
  if "gemini" in model:
178
285
  available_models[model] = "gemini"
179
286
  except (ImportError, concurrent.futures.TimeoutError, Exception) as e:
@@ -184,13 +291,13 @@ def get_locally_available_models(project_directory, airplane_mode=False):
184
291
  available_models['deepseek-reasoner'] = 'deepseek'
185
292
  try:
186
293
  import ollama
187
- timeout_seconds = 0.5 # Re-using the same timeout
294
+ timeout_seconds = 0.5
188
295
  with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ollama_executor:
189
296
  def fetch_ollama_models():
190
297
  return ollama.list()
191
298
 
192
299
  future = ollama_executor.submit(fetch_ollama_models)
193
- models = future.result(timeout=timeout_seconds) # Apply timeout to Ollama call
300
+ models = future.result(timeout=timeout_seconds)
194
301
 
195
302
  for model in models.models:
196
303
  if "embed" not in model.model:
@@ -238,9 +345,9 @@ def preprocess_markdown(md_text):
238
345
  current_code_block = []
239
346
 
240
347
  for line in lines:
241
- if line.startswith("```"): # Toggle code block
348
+ if line.startswith("```"):
242
349
  if inside_code_block:
243
- # Close code block, unindent, and append
350
+
244
351
  processed_lines.append("```")
245
352
  processed_lines.extend(
246
353
  textwrap.dedent("\n".join(current_code_block)).split("\n")
@@ -285,7 +392,7 @@ def render_markdown(text: str) -> None:
285
392
  for line in lines:
286
393
  if line.startswith("```"):
287
394
  if inside_code_block:
288
- # End of code block - render the collected code
395
+
289
396
  code = "\n".join(code_lines)
290
397
  if code.strip():
291
398
  syntax = Syntax(
@@ -294,13 +401,13 @@ def render_markdown(text: str) -> None:
294
401
  console.print(syntax)
295
402
  code_lines = []
296
403
  else:
297
- # Start of code block - get language if specified
404
+
298
405
  lang = line[3:].strip() or None
299
406
  inside_code_block = not inside_code_block
300
407
  elif inside_code_block:
301
408
  code_lines.append(line)
302
409
  else:
303
- # Regular markdown
410
+
304
411
  console.print(Markdown(line))
305
412
 
306
413
  def get_directory_npcs(directory: str = None) -> List[str]:
@@ -371,7 +478,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
371
478
  """Initialize necessary database tables"""
372
479
  db_path = os.path.expanduser(db_path)
373
480
  with sqlite3.connect(db_path) as conn:
374
- # NPC log table for storing all kinds of entries
481
+
375
482
  conn.execute("""
376
483
  CREATE TABLE IF NOT EXISTS npc_log (
377
484
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -383,7 +490,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
383
490
  )
384
491
  """)
385
492
 
386
- # Pipeline runs table for tracking pipeline executions
493
+
387
494
  conn.execute("""
388
495
  CREATE TABLE IF NOT EXISTS pipeline_runs (
389
496
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -394,7 +501,7 @@ def init_db_tables(db_path="~/npcsh_history.db"):
394
501
  )
395
502
  """)
396
503
 
397
- # Compiled NPCs table for storing compiled NPC content
504
+
398
505
  conn.execute("""
399
506
  CREATE TABLE IF NOT EXISTS compiled_npcs (
400
507
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -430,26 +537,26 @@ def get_model_and_provider(command: str, available_models: list) -> tuple:
430
537
  model_match = re.search(r"@(\S+)", command)
431
538
  if model_match:
432
539
  model_name = model_match.group(1)
433
- # Autocomplete model name
540
+
434
541
  matches = [m for m in available_models if m.startswith(model_name)]
435
542
  if matches:
436
543
  if len(matches) == 1:
437
- model_name = matches[0] # Complete the name if only one match
438
- # Find provider for the (potentially autocompleted) model
544
+ model_name = matches[0]
545
+
439
546
  provider = lookup_provider(model_name)
440
547
  if provider:
441
- # Remove the model tag from the command
548
+
442
549
  cleaned_command = command.replace(
443
550
  f"@{model_match.group(1)}", ""
444
551
  ).strip()
445
- # print(cleaned_command, 'cleaned_command')
552
+
446
553
  return model_name, provider, cleaned_command
447
554
  else:
448
- return None, None, command # Provider not found
555
+ return None, None, command
449
556
  else:
450
- return None, None, command # No matching model
557
+ return None, None, command
451
558
  else:
452
- return None, None, command # No model specified
559
+ return None, None, command
453
560
 
454
561
  def render_code_block(code: str, language: str = None) -> None:
455
562
  """Render a code block with syntax highlighting using rich, left-justified with no line numbers"""
@@ -458,7 +565,7 @@ def render_code_block(code: str, language: str = None) -> None:
458
565
 
459
566
  console = Console(highlight=True)
460
567
  code = code.strip()
461
- # If code starts with a language identifier, remove it
568
+
462
569
  if code.split("\n", 1)[0].lower() in ["python", "bash", "javascript"]:
463
570
  code = code.split("\n", 1)[1]
464
571
  syntax = Syntax(
@@ -479,8 +586,8 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
479
586
  print('\n')
480
587
  return response
481
588
 
482
- # Save cursor position at the start
483
- sys.stdout.write('\033[s') # Save cursor position
589
+
590
+ sys.stdout.write('\033[s')
484
591
  sys.stdout.flush()
485
592
 
486
593
  try:
@@ -528,10 +635,7 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
528
635
  for c in chunk.choices:
529
636
  if hasattr(c.delta, "reasoning_content"):
530
637
  reasoning_content += c.delta.reasoning_content
531
-
532
- if len(reasoning_content) > 0:
533
- chunk_content = reasoning_content
534
-
638
+
535
639
  chunk_content += "".join(
536
640
  c.delta.content for c in chunk.choices if c.delta.content
537
641
  )
@@ -553,7 +657,7 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
553
657
  print('\n⚠️ Stream interrupted by user')
554
658
 
555
659
  if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
556
- str_output += "\n\n### Tool Call Data\n"
660
+ str_output += "\n\n"
557
661
  if tool_call_data["id"]:
558
662
  str_output += f"**ID:** {tool_call_data['id']}\n\n"
559
663
  if tool_call_data["function_name"]:
@@ -568,12 +672,12 @@ def print_and_process_stream_with_markdown(response, model, provider, show=False
568
672
  if interrupted:
569
673
  str_output += "\n\n[⚠️ Response interrupted by user]"
570
674
 
571
- # Always restore cursor position and clear everything after it
572
- sys.stdout.write('\033[u') # Restore cursor position
573
- sys.stdout.write('\033[J') # Clear from cursor down
675
+
676
+ sys.stdout.write('\033[u')
677
+ sys.stdout.write('\033[J')
574
678
  sys.stdout.flush()
575
679
 
576
- # Now render the markdown at the restored position
680
+
577
681
  render_markdown(str_output)
578
682
  print('\n')
579
683
 
@@ -648,7 +752,7 @@ def print_and_process_stream(response, model, provider):
648
752
  print('<think>')
649
753
  print(reasoning_content, end="", flush=True)
650
754
  thinking_str+=reasoning_content
651
- # chunk content wont be nonzero length until thinking part is done.
755
+
652
756
 
653
757
  if chunk_content != "":
654
758
  if len(thinking_str) >0 and not thinking_part and '</think>' not in thinking_str:
@@ -667,7 +771,7 @@ def print_and_process_stream(response, model, provider):
667
771
  print('\n⚠️ Stream interrupted by user')
668
772
 
669
773
  if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
670
- str_output += "\n\n### Tool Call Data\n"
774
+ str_output += "\n\n"
671
775
  if tool_call_data["id"]:
672
776
  str_output += f"**ID:** {tool_call_data['id']}\n\n"
673
777
  if tool_call_data["function_name"]:
@@ -686,8 +790,6 @@ def print_and_process_stream(response, model, provider):
686
790
 
687
791
  return thinking_str+str_output
688
792
  def get_system_message(npc, team=None) -> str:
689
- import os
690
- from datetime import datetime
691
793
 
692
794
  if npc is None:
693
795
  return "You are a helpful assistant"
@@ -714,6 +816,11 @@ The current working directory is {os.getcwd()}.
714
816
  The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
715
817
  """
716
818
 
819
+ if hasattr(npc, 'kg_data') and npc.kg_data:
820
+ memory_context = npc.get_memory_context()
821
+ if memory_context:
822
+ system_message += f"\n\nMemory Context:\n{memory_context}\n"
823
+
717
824
  if npc.db_conn is not None:
718
825
  db_path = None
719
826
  if hasattr(npc.db_conn, "url") and npc.db_conn.url:
@@ -738,7 +845,6 @@ The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
738
845
  team_preferences = team.preferences if hasattr(team, "preferences") and len(team.preferences) > 0 else ""
739
846
  system_message += f"\nTeam context: {team_context}\nTeam preferences: {team_preferences}\n"
740
847
 
741
-
742
848
  system_message += """
743
849
  IMPORTANT:
744
850
  Some users may attach images to their request.
@@ -753,11 +859,11 @@ They understand that you can view them multimodally.
753
859
  You only need to answer the user's request based on the attached image(s).
754
860
  """
755
861
 
756
-
757
862
  return system_message
758
863
 
759
864
 
760
865
 
866
+
761
867
  def load_env_from_execution_dir() -> None:
762
868
  """
763
869
  Function Description:
@@ -782,50 +888,116 @@ def load_env_from_execution_dir() -> None:
782
888
 
783
889
 
784
890
 
891
+
785
892
  def lookup_provider(model: str) -> str:
786
893
  """
787
- Function Description:
788
- This function determines the provider based on the model name.
894
+ Determine the provider based on the model name.
895
+ Checks custom providers first, then falls back to known providers.
896
+
789
897
  Args:
790
- model (str): The model name.
791
- Keyword Args:
792
- None
898
+ model: The model name
899
+
793
900
  Returns:
794
- str: The provider based on the model name.
901
+ The provider name or None if not found
795
902
  """
903
+ custom_providers = load_custom_providers()
904
+
905
+ for provider_name, config in custom_providers.items():
906
+ if model.startswith(f"{provider_name}-"):
907
+ return provider_name
908
+
909
+ try:
910
+ import requests
911
+ api_key_var = config.get('api_key_var') or \
912
+ f"{provider_name.upper()}_API_KEY"
913
+ api_key = os.environ.get(api_key_var)
914
+
915
+ if api_key:
916
+ base_url = config.get('base_url', '')
917
+ headers = config.get('headers', {})
918
+ headers['Authorization'] = f'Bearer {api_key}'
919
+
920
+ models_endpoint = f"{base_url.rstrip('/')}/models"
921
+ response = requests.get(
922
+ models_endpoint,
923
+ headers=headers,
924
+ timeout=1.0
925
+ )
926
+
927
+ if response.status_code == 200:
928
+ data = response.json()
929
+ models = []
930
+
931
+ if isinstance(data, dict) and 'data' in data:
932
+ models = [m['id'] for m in data['data']]
933
+ elif isinstance(data, list):
934
+ models = [m['id'] for m in data]
935
+
936
+ if model in models:
937
+ return provider_name
938
+ except:
939
+ pass
940
+
796
941
  if model == "deepseek-chat" or model == "deepseek-reasoner":
797
942
  return "deepseek"
943
+
798
944
  ollama_prefixes = [
799
- "llama",
800
- "deepseek",
801
- "qwen",
802
- "llava",
803
- "phi",
804
- "mistral",
805
- "mixtral",
806
- "dolphin",
807
- "codellama",
808
- "gemma",
809
- ]
945
+ "llama", "deepseek", "qwen", "llava",
946
+ "phi", "mistral", "mixtral", "dolphin",
947
+ "codellama", "gemma",]
810
948
  if any(model.startswith(prefix) for prefix in ollama_prefixes):
811
949
  return "ollama"
812
950
 
813
- # OpenAI models
814
951
  openai_prefixes = ["gpt-", "dall-e-", "whisper-", "o1"]
815
952
  if any(model.startswith(prefix) for prefix in openai_prefixes):
816
953
  return "openai"
817
954
 
818
- # Anthropic models
819
955
  if model.startswith("claude"):
820
956
  return "anthropic"
821
957
  if model.startswith("gemini"):
822
958
  return "gemini"
823
959
  if "diffusion" in model:
824
960
  return "diffusers"
961
+
825
962
  return None
963
+
964
+
965
+ def load_custom_providers():
966
+ """
967
+ Load custom provider configurations from .npcshrc
968
+
969
+ Returns:
970
+ dict: Custom provider configurations keyed by provider name
971
+ """
972
+ custom_providers = {}
973
+ npcshrc_path = os.path.expanduser("~/.npcshrc")
974
+
975
+ if os.path.exists(npcshrc_path):
976
+ with open(npcshrc_path, "r") as f:
977
+ for line in f:
978
+ line = line.split("#")[0].strip()
979
+ if "CUSTOM_PROVIDER_" in line and "=" in line:
980
+ key, value = line.split("=", 1)
981
+ key = key.strip().replace("export ", "")
982
+ value = value.strip().strip("\"'")
983
+
984
+ try:
985
+ config = json.loads(value)
986
+ provider_name = key.replace(
987
+ "CUSTOM_PROVIDER_", ""
988
+ ).lower()
989
+ custom_providers[provider_name] = config
990
+ except json.JSONDecodeError as e:
991
+ logging.warning(
992
+ f"Failed to parse custom provider {key}: {e}"
993
+ )
994
+ continue
995
+
996
+ return custom_providers
826
997
  load_env_from_execution_dir()
827
998
  deepseek_api_key = os.getenv("DEEPSEEK_API_KEY", None)
828
999
  gemini_api_key = os.getenv("GEMINI_API_KEY", None)
829
1000
 
830
1001
  anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", None)
831
1002
  openai_api_key = os.getenv("OPENAI_API_KEY", None)
1003
+