ngpt 2.3.3__tar.gz → 2.4.0__tar.gz

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 (32) hide show
  1. {ngpt-2.3.3 → ngpt-2.4.0}/PKG-INFO +12 -2
  2. {ngpt-2.3.3 → ngpt-2.4.0}/README.md +11 -1
  3. {ngpt-2.3.3 → ngpt-2.4.0}/docs/api/config.md +60 -2
  4. {ngpt-2.3.3 → ngpt-2.4.0}/docs/configuration.md +18 -4
  5. {ngpt-2.3.3 → ngpt-2.4.0}/docs/usage/cli_usage.md +26 -7
  6. {ngpt-2.3.3 → ngpt-2.4.0}/ngpt/cli.py +131 -35
  7. {ngpt-2.3.3 → ngpt-2.4.0}/ngpt/client.py +6 -6
  8. {ngpt-2.3.3 → ngpt-2.4.0}/ngpt/config.py +80 -7
  9. {ngpt-2.3.3 → ngpt-2.4.0}/pyproject.toml +1 -1
  10. {ngpt-2.3.3 → ngpt-2.4.0}/uv.lock +1 -1
  11. {ngpt-2.3.3 → ngpt-2.4.0}/.github/workflows/python-publish.yml +0 -0
  12. {ngpt-2.3.3 → ngpt-2.4.0}/.gitignore +0 -0
  13. {ngpt-2.3.3 → ngpt-2.4.0}/.python-version +0 -0
  14. {ngpt-2.3.3 → ngpt-2.4.0}/COMMIT_GUIDELINES.md +0 -0
  15. {ngpt-2.3.3 → ngpt-2.4.0}/CONTRIBUTING.md +0 -0
  16. {ngpt-2.3.3 → ngpt-2.4.0}/LICENSE +0 -0
  17. {ngpt-2.3.3 → ngpt-2.4.0}/docs/CONTRIBUTING.md +0 -0
  18. {ngpt-2.3.3 → ngpt-2.4.0}/docs/LICENSE.md +0 -0
  19. {ngpt-2.3.3 → ngpt-2.4.0}/docs/README.md +0 -0
  20. {ngpt-2.3.3 → ngpt-2.4.0}/docs/_config.yml +0 -0
  21. {ngpt-2.3.3 → ngpt-2.4.0}/docs/api/README.md +0 -0
  22. {ngpt-2.3.3 → ngpt-2.4.0}/docs/api/client.md +0 -0
  23. {ngpt-2.3.3 → ngpt-2.4.0}/docs/assets/css/style.scss +0 -0
  24. {ngpt-2.3.3 → ngpt-2.4.0}/docs/examples/README.md +0 -0
  25. {ngpt-2.3.3 → ngpt-2.4.0}/docs/examples/advanced.md +0 -0
  26. {ngpt-2.3.3 → ngpt-2.4.0}/docs/examples/basic.md +0 -0
  27. {ngpt-2.3.3 → ngpt-2.4.0}/docs/examples/integrations.md +0 -0
  28. {ngpt-2.3.3 → ngpt-2.4.0}/docs/installation.md +0 -0
  29. {ngpt-2.3.3 → ngpt-2.4.0}/docs/overview.md +0 -0
  30. {ngpt-2.3.3 → ngpt-2.4.0}/docs/usage/README.md +0 -0
  31. {ngpt-2.3.3 → ngpt-2.4.0}/docs/usage/library_usage.md +0 -0
  32. {ngpt-2.3.3 → ngpt-2.4.0}/ngpt/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ngpt
3
- Version: 2.3.3
3
+ Version: 2.4.0
4
4
  Summary: A lightweight Python CLI and library for interacting with OpenAI-compatible APIs, supporting both official and self-hosted LLM endpoints.
5
5
  Project-URL: Homepage, https://github.com/nazdridoy/ngpt
6
6
  Project-URL: Repository, https://github.com/nazdridoy/ngpt
@@ -264,11 +264,12 @@ You can configure the client using the following options:
264
264
  | `-n, --no-stream` | Return the whole response without streaming |
265
265
  | `--temperature` | Set temperature (controls randomness, default: 0.7) |
266
266
  | `--top_p` | Set top_p (controls diversity, default: 1.0) |
267
- | `--max_length` | Set maximum response length in tokens |
267
+ | `--max_tokens` | Set maximum response length in tokens |
268
268
  | `--preprompt` | Set custom system prompt to control AI behavior |
269
269
  | `--log` | Set filepath to log conversation to (for interactive modes) |
270
270
  | `--config` | Path to a custom configuration file or, when used without a value, enters interactive configuration mode |
271
271
  | `--config-index` | Index of the configuration to use (default: 0) |
272
+ | `--provider` | Provider name to identify the configuration to use (alternative to --config-index) |
272
273
  | `--remove` | Remove the configuration at the specified index (requires --config and --config-index) |
273
274
  | `--show-config` | Show configuration details and exit |
274
275
  | `--all` | Used with `--show-config` to display all configurations |
@@ -291,8 +292,17 @@ ngpt --config
291
292
  # Edit an existing configuration at index 1
292
293
  ngpt --config --config-index 1
293
294
 
295
+ # Edit an existing configuration by provider name
296
+ ngpt --config --provider Gemini
297
+
294
298
  # Remove a configuration at index 2
295
299
  ngpt --config --remove --config-index 2
300
+
301
+ # Remove a configuration by provider name
302
+ ngpt --config --remove --provider Gemini
303
+
304
+ # Use a specific configuration by provider name
305
+ ngpt --provider OpenAI "Tell me about quantum computing"
296
306
  ```
297
307
 
298
308
  In interactive mode:
@@ -230,11 +230,12 @@ You can configure the client using the following options:
230
230
  | `-n, --no-stream` | Return the whole response without streaming |
231
231
  | `--temperature` | Set temperature (controls randomness, default: 0.7) |
232
232
  | `--top_p` | Set top_p (controls diversity, default: 1.0) |
233
- | `--max_length` | Set maximum response length in tokens |
233
+ | `--max_tokens` | Set maximum response length in tokens |
234
234
  | `--preprompt` | Set custom system prompt to control AI behavior |
235
235
  | `--log` | Set filepath to log conversation to (for interactive modes) |
236
236
  | `--config` | Path to a custom configuration file or, when used without a value, enters interactive configuration mode |
237
237
  | `--config-index` | Index of the configuration to use (default: 0) |
238
+ | `--provider` | Provider name to identify the configuration to use (alternative to --config-index) |
238
239
  | `--remove` | Remove the configuration at the specified index (requires --config and --config-index) |
239
240
  | `--show-config` | Show configuration details and exit |
240
241
  | `--all` | Used with `--show-config` to display all configurations |
@@ -257,8 +258,17 @@ ngpt --config
257
258
  # Edit an existing configuration at index 1
258
259
  ngpt --config --config-index 1
259
260
 
261
+ # Edit an existing configuration by provider name
262
+ ngpt --config --provider Gemini
263
+
260
264
  # Remove a configuration at index 2
261
265
  ngpt --config --remove --config-index 2
266
+
267
+ # Remove a configuration by provider name
268
+ ngpt --config --remove --provider Gemini
269
+
270
+ # Use a specific configuration by provider name
271
+ ngpt --provider OpenAI "Tell me about quantum computing"
262
272
  ```
263
273
 
264
274
  In interactive mode:
@@ -122,7 +122,7 @@ custom_configs = load_configs("/path/to/custom/config.json")
122
122
 
123
123
  ### load_config()
124
124
 
125
- Loads a specific configuration by index and applies environment variables.
125
+ Loads a specific configuration by index or provider name and applies environment variables.
126
126
 
127
127
  ```python
128
128
  from ngpt.config import load_config
@@ -130,7 +130,8 @@ from typing import Dict, Any
130
130
 
131
131
  config: Dict[str, Any] = load_config(
132
132
  custom_path: Optional[str] = None,
133
- config_index: int = 0
133
+ config_index: int = 0,
134
+ provider: Optional[str] = None
134
135
  )
135
136
  ```
136
137
 
@@ -140,6 +141,7 @@ config: Dict[str, Any] = load_config(
140
141
  |-----------|------|---------|-------------|
141
142
  | `custom_path` | `Optional[str]` | `None` | Optional custom path to the configuration file |
142
143
  | `config_index` | `int` | `0` | Index of the configuration to load (0-based) |
144
+ | `provider` | `Optional[str]` | `None` | Provider name to identify the configuration to use (alternative to config_index) |
143
145
 
144
146
  #### Returns
145
147
 
@@ -160,6 +162,11 @@ config_1 = load_config(config_index=1)
160
162
  print(f"Using provider: {config_1.get('provider', 'Unknown')}")
161
163
  print(f"Using model: {config_1.get('model', 'Unknown')}")
162
164
 
165
+ # Load a specific configuration by provider name
166
+ gemini_config = load_config(provider="Gemini")
167
+ print(f"Using provider: {gemini_config.get('provider', 'Unknown')}")
168
+ print(f"Using model: {gemini_config.get('model', 'Unknown')}")
169
+
163
170
  # Load from a custom path
164
171
  custom_config = load_config(custom_path="/path/to/custom/config.json")
165
172
  ```
@@ -277,6 +284,57 @@ else:
277
284
  print("Failed to remove configuration")
278
285
  ```
279
286
 
287
+ ### is_provider_unique()
288
+
289
+ Checks if a provider name is unique among configurations.
290
+
291
+ ```python
292
+ from ngpt.config import is_provider_unique
293
+ from typing import List, Dict, Any, Optional
294
+
295
+ is_unique: bool = is_provider_unique(
296
+ configs: List[Dict[str, Any]],
297
+ provider: str,
298
+ exclude_index: Optional[int] = None
299
+ )
300
+ ```
301
+
302
+ #### Parameters
303
+
304
+ | Parameter | Type | Default | Description |
305
+ |-----------|------|---------|-------------|
306
+ | `configs` | `List[Dict[str, Any]]` | Required | List of configuration dictionaries |
307
+ | `provider` | `str` | Required | Provider name to check for uniqueness |
308
+ | `exclude_index` | `Optional[int]` | `None` | Optional index to exclude from the check (useful when updating an existing config) |
309
+
310
+ #### Returns
311
+
312
+ `True` if the provider name is unique among all configurations, `False` otherwise.
313
+
314
+ #### Examples
315
+
316
+ ```python
317
+ from ngpt.config import load_configs, is_provider_unique
318
+
319
+ # Load all configurations
320
+ configs = load_configs()
321
+
322
+ # Check if a provider name is unique
323
+ provider_name = "New Provider"
324
+ if is_provider_unique(configs, provider_name):
325
+ print(f"'{provider_name}' is a unique provider name")
326
+ else:
327
+ print(f"'{provider_name}' is already used by another configuration")
328
+
329
+ # Check if provider name is unique when updating an existing config
330
+ existing_idx = 1
331
+ update_provider = "Updated Provider"
332
+ if is_provider_unique(configs, update_provider, exclude_index=existing_idx):
333
+ print(f"'{update_provider}' is unique and can be used to update config at index {existing_idx}")
334
+ else:
335
+ print(f"'{update_provider}' is already used by another configuration")
336
+ ```
337
+
280
338
  ## Complete Examples
281
339
 
282
340
  ### Managing Multiple Configurations
@@ -87,8 +87,14 @@ ngpt --config
87
87
  # Edit an existing configuration at index 1
88
88
  ngpt --config --config-index 1
89
89
 
90
+ # Edit an existing configuration by provider name
91
+ ngpt --config --provider Gemini
92
+
90
93
  # Remove a configuration at index 2
91
94
  ngpt --config --remove --config-index 2
95
+
96
+ # Remove a configuration by provider name
97
+ ngpt --config --remove --provider Gemini
92
98
  ```
93
99
 
94
100
  The interactive configuration will prompt you for values and guide you through the process.
@@ -104,8 +110,11 @@ ngpt --api-key "your-key" --base-url "https://api.example.com/v1/" --model "cust
104
110
  # Select a specific configuration by index
105
111
  ngpt --config-index 2 "Your prompt here"
106
112
 
113
+ # Select a specific configuration by provider name
114
+ ngpt --provider Gemini "Your prompt here"
115
+
107
116
  # Control response generation parameters
108
- ngpt --temperature 0.8 --top_p 0.95 --max_length 300 "Write a creative story"
117
+ ngpt --temperature 0.8 --top_p 0.95 --max_tokens 300 "Write a creative story"
109
118
 
110
119
  # Set a custom system prompt (preprompt)
111
120
  ngpt --preprompt "You are a Linux command line expert. Focus on efficient solutions." "How do I find the largest files in a directory?"
@@ -158,6 +167,11 @@ ngpt --config-index 1 "Tell me about quantum computing"
158
167
 
159
168
  # Use local Ollama (config at index 2)
160
169
  ngpt --config-index 2 "Tell me about quantum computing"
170
+
171
+ # Or use provider names instead of indices (more intuitive)
172
+ ngpt --provider OpenAI "Tell me about quantum computing"
173
+ ngpt --provider Groq "Tell me about quantum computing"
174
+ ngpt --provider Ollama-Local "Tell me about quantum computing"
161
175
  ```
162
176
 
163
177
  ### Programmatically Loading Configurations
@@ -167,12 +181,12 @@ In your Python code, you can load and use different configurations:
167
181
  ```python
168
182
  from ngpt import NGPTClient, load_config
169
183
 
170
- # Load OpenAI configuration
184
+ # Load OpenAI configuration by index
171
185
  openai_config = load_config(config_index=0)
172
186
  openai_client = NGPTClient(**openai_config)
173
187
 
174
- # Load Groq configuration
175
- groq_config = load_config(config_index=1)
188
+ # Load Groq configuration by provider name
189
+ groq_config = load_config(provider="Groq")
176
190
  groq_client = NGPTClient(**groq_config)
177
191
 
178
192
  # Use the clients
@@ -47,7 +47,9 @@ Here are the most commonly used options:
47
47
  | `--web-search` | Enable web search capability (if supported by your API) |
48
48
  | `--temperature` | Set temperature (controls randomness, default: 0.7) |
49
49
  | `--top_p` | Set top_p (controls diversity, default: 1.0) |
50
- | `--max_length` | Set maximum response length in tokens |
50
+ | `--max_tokens` | Set maximum response length in tokens |
51
+ | `--config-index` | Index of the configuration to use (default: 0) |
52
+ | `--provider` | Provider name to identify the configuration to use (alternative to --config-index) |
51
53
 
52
54
  ## Feature Details
53
55
 
@@ -172,6 +174,12 @@ Select a specific configuration by index:
172
174
  ngpt --config-index 1 "Your prompt here"
173
175
  ```
174
176
 
177
+ Select a specific configuration by provider name:
178
+
179
+ ```bash
180
+ ngpt --provider Gemini "Your prompt here"
181
+ ```
182
+
175
183
  Specify API credentials directly:
176
184
 
177
185
  ```bash
@@ -186,18 +194,30 @@ Add a new configuration interactively:
186
194
  ngpt --config
187
195
  ```
188
196
 
189
- Edit an existing configuration:
197
+ Edit an existing configuration by index:
190
198
 
191
199
  ```bash
192
200
  ngpt --config --config-index 1
193
201
  ```
194
202
 
195
- Remove a configuration:
203
+ Edit an existing configuration by provider name:
204
+
205
+ ```bash
206
+ ngpt --config --provider Gemini
207
+ ```
208
+
209
+ Remove a configuration by index:
196
210
 
197
211
  ```bash
198
212
  ngpt --config --remove --config-index 2
199
213
  ```
200
214
 
215
+ Remove a configuration by provider name:
216
+
217
+ ```bash
218
+ ngpt --config --remove --provider Gemini
219
+ ```
220
+
201
221
  ### Model Management
202
222
 
203
223
  List all available models for the current configuration:
@@ -270,10 +290,10 @@ Set the maximum response length in tokens:
270
290
 
271
291
  ```bash
272
292
  # Get a concise response
273
- ngpt --max_length 100 "Explain quantum computing"
293
+ ngpt --max_tokens 100 "Explain quantum computing"
274
294
 
275
295
  # Allow for a longer, more detailed response
276
- ngpt --max_length 500 "Write a comprehensive guide to machine learning"
296
+ ngpt --max_tokens 500 "Write a comprehensive guide to machine learning"
277
297
  ```
278
298
 
279
299
  ## Examples by Task
@@ -338,7 +358,6 @@ ngpt --show-config
338
358
  # Try specifying the base URL directly
339
359
  ngpt --base-url "https://api.example.com/v1/" "Test connection"
340
360
  ```
341
-
342
361
  ### Authorization Problems
343
362
 
344
363
  If you're experiencing authentication issues:
@@ -384,4 +403,4 @@ If the `ngpt` command is not found after installation:
384
403
  5. **Improve efficiency**:
385
404
  - Use `-n` (no streaming) for faster responses in scripts
386
405
  - Use interactive mode when having a conversation
387
- - Exit interactive sessions when not in use to save API costs
406
+ - Exit interactive sessions when not in use to save API costs
@@ -302,13 +302,20 @@ def show_config_help():
302
302
  print(f" 5. {COLORS['cyan']}Use --config-index to specify which configuration to use or edit:{COLORS['reset']}")
303
303
  print(f" {COLORS['yellow']}ngpt --config-index 1 \"Your prompt\"{COLORS['reset']}")
304
304
 
305
- print(f" 6. {COLORS['cyan']}Use --config without arguments to add a new configuration:{COLORS['reset']}")
305
+ print(f" 6. {COLORS['cyan']}Use --provider to specify which configuration to use by provider name:{COLORS['reset']}")
306
+ print(f" {COLORS['yellow']}ngpt --provider Gemini \"Your prompt\"{COLORS['reset']}")
307
+
308
+ print(f" 7. {COLORS['cyan']}Use --config without arguments to add a new configuration:{COLORS['reset']}")
306
309
  print(f" {COLORS['yellow']}ngpt --config{COLORS['reset']}")
307
- print(f" Or specify an index to edit an existing configuration:")
310
+ print(f" Or specify an index or provider to edit an existing configuration:")
308
311
  print(f" {COLORS['yellow']}ngpt --config --config-index 1{COLORS['reset']}")
309
- print(f" 7. {COLORS['cyan']}Remove a configuration at a specific index:{COLORS['reset']}")
312
+ print(f" {COLORS['yellow']}ngpt --config --provider Gemini{COLORS['reset']}")
313
+
314
+ print(f" 8. {COLORS['cyan']}Remove a configuration by index or provider:{COLORS['reset']}")
310
315
  print(f" {COLORS['yellow']}ngpt --config --remove --config-index 1{COLORS['reset']}")
311
- print(f" 8. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
316
+ print(f" {COLORS['yellow']}ngpt --config --remove --provider Gemini{COLORS['reset']}")
317
+
318
+ print(f" 9. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
312
319
  print(f" {COLORS['yellow']}ngpt --list-models{COLORS['reset']}")
313
320
 
314
321
  def check_config(config):
@@ -325,7 +332,7 @@ def check_config(config):
325
332
 
326
333
  return True
327
334
 
328
- def interactive_chat_session(client, web_search=False, no_stream=False, temperature=0.7, top_p=1.0, max_length=None, log_file=None, preprompt=None):
335
+ def interactive_chat_session(client, web_search=False, no_stream=False, temperature=0.7, top_p=1.0, max_tokens=None, log_file=None, preprompt=None):
329
336
  """Run an interactive chat session with conversation history."""
330
337
  # Get terminal width for better formatting
331
338
  try:
@@ -467,6 +474,7 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
467
474
 
468
475
  # Skip empty messages but don't raise an error
469
476
  if not user_input.strip():
477
+ print(f"{COLORS['yellow']}Empty message skipped. Type 'exit' to quit.{COLORS['reset']}")
470
478
  continue
471
479
 
472
480
  # Add user message to conversation
@@ -492,7 +500,7 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
492
500
  web_search=web_search,
493
501
  temperature=temperature,
494
502
  top_p=top_p,
495
- max_tokens=max_length
503
+ max_tokens=max_tokens
496
504
  )
497
505
 
498
506
  # Add AI response to conversation history
@@ -554,6 +562,7 @@ def main():
554
562
  config_group = parser.add_argument_group('Configuration Options')
555
563
  config_group.add_argument('--config', nargs='?', const=True, help='Path to a custom config file or, if no value provided, enter interactive configuration mode to create a new config')
556
564
  config_group.add_argument('--config-index', type=int, default=0, help='Index of the configuration to use or edit (default: 0)')
565
+ config_group.add_argument('--provider', help='Provider name to identify the configuration to use')
557
566
  config_group.add_argument('--remove', action='store_true', help='Remove the configuration at the specified index (requires --config and --config-index)')
558
567
  config_group.add_argument('--show-config', action='store_true', help='Show the current configuration(s) and exit')
559
568
  config_group.add_argument('--all', action='store_true', help='Show details for all configurations (requires --show-config)')
@@ -572,12 +581,12 @@ def main():
572
581
  help='Set temperature (controls randomness, default: 0.7)')
573
582
  global_group.add_argument('--top_p', type=float, default=1.0,
574
583
  help='Set top_p (controls diversity, default: 1.0)')
575
- global_group.add_argument('--max_length', type=int,
584
+ global_group.add_argument('--max_tokens', type=int,
576
585
  help='Set max response length in tokens')
577
586
  global_group.add_argument('--log', metavar='FILE',
578
587
  help='Set filepath to log conversation to (For interactive modes)')
579
588
  global_group.add_argument('--preprompt',
580
- help='Set preprompt')
589
+ help='Set custom system prompt to control AI behavior')
581
590
 
582
591
  # Mode flags (mutually exclusive)
583
592
  mode_group = parser.add_argument_group('Modes (mutually exclusive)')
@@ -599,6 +608,10 @@ def main():
599
608
  # Validate --all usage
600
609
  if args.all and not args.show_config:
601
610
  parser.error("--all can only be used with --show-config")
611
+
612
+ # Check for mutual exclusivity between --config-index and --provider
613
+ if args.config_index != 0 and args.provider:
614
+ parser.error("--config-index and --provider cannot be used together")
602
615
 
603
616
  # Handle interactive configuration mode
604
617
  if args.config is True: # --config was used without a value
@@ -607,20 +620,46 @@ def main():
607
620
  # Handle configuration removal if --remove flag is present
608
621
  if args.remove:
609
622
  # Validate that config_index is explicitly provided
610
- if '--config-index' not in sys.argv:
611
- parser.error("--remove requires explicitly specifying --config-index")
623
+ if '--config-index' not in sys.argv and not args.provider:
624
+ parser.error("--remove requires explicitly specifying --config-index or --provider")
612
625
 
613
626
  # Show config details before asking for confirmation
614
627
  configs = load_configs(str(config_path))
615
628
 
629
+ # Determine the config index to remove
630
+ config_index = args.config_index
631
+ if args.provider:
632
+ # Find config index by provider name
633
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == args.provider.lower()]
634
+ if not matching_configs:
635
+ print(f"Error: No configuration found for provider '{args.provider}'")
636
+ return
637
+ elif len(matching_configs) > 1:
638
+ print(f"Multiple configurations found for provider '{args.provider}':")
639
+ for i, idx in enumerate(matching_configs):
640
+ print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
641
+
642
+ try:
643
+ choice = input("Choose a configuration to remove (or press Enter to cancel): ")
644
+ if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
645
+ config_index = matching_configs[int(choice)]
646
+ else:
647
+ print("Configuration removal cancelled.")
648
+ return
649
+ except (ValueError, IndexError, KeyboardInterrupt):
650
+ print("\nConfiguration removal cancelled.")
651
+ return
652
+ else:
653
+ config_index = matching_configs[0]
654
+
616
655
  # Check if index is valid
617
- if args.config_index < 0 or args.config_index >= len(configs):
618
- print(f"Error: Configuration index {args.config_index} is out of range. Valid range: 0-{len(configs)-1}")
656
+ if config_index < 0 or config_index >= len(configs):
657
+ print(f"Error: Configuration index {config_index} is out of range. Valid range: 0-{len(configs)-1}")
619
658
  return
620
659
 
621
660
  # Show the configuration that will be removed
622
- config = configs[args.config_index]
623
- print(f"Configuration to remove (index {args.config_index}):")
661
+ config = configs[config_index]
662
+ print(f"Configuration to remove (index {config_index}):")
624
663
  print(f" Provider: {config.get('provider', 'N/A')}")
625
664
  print(f" Model: {config.get('model', 'N/A')}")
626
665
  print(f" Base URL: {config.get('base_url', 'N/A')}")
@@ -631,7 +670,7 @@ def main():
631
670
  print("\nAre you sure you want to remove this configuration? [y/N] ", end='')
632
671
  response = input().lower()
633
672
  if response in ('y', 'yes'):
634
- remove_config_entry(config_path, args.config_index)
673
+ remove_config_entry(config_path, config_index)
635
674
  else:
636
675
  print("Configuration removal cancelled.")
637
676
  except KeyboardInterrupt:
@@ -643,20 +682,51 @@ def main():
643
682
  # If --config-index was not explicitly specified, create a new entry by passing None
644
683
  # This will cause add_config_entry to create a new entry at the end of the list
645
684
  # Otherwise, edit the existing config at the specified index
646
- config_index = None if args.config_index == 0 and '--config-index' not in sys.argv else args.config_index
685
+ config_index = None
647
686
 
648
- # Load existing configs to determine the new index if creating a new config
649
- configs = load_configs(str(config_path))
650
- if config_index is None:
651
- print(f"Creating new configuration at index {len(configs)}")
652
- else:
687
+ # Determine if we're editing an existing config or creating a new one
688
+ if args.provider:
689
+ # Find config by provider name
690
+ configs = load_configs(str(config_path))
691
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == args.provider.lower()]
692
+
693
+ if not matching_configs:
694
+ print(f"No configuration found for provider '{args.provider}'. Creating a new configuration.")
695
+ elif len(matching_configs) > 1:
696
+ print(f"Multiple configurations found for provider '{args.provider}':")
697
+ for i, idx in enumerate(matching_configs):
698
+ print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
699
+
700
+ try:
701
+ choice = input("Choose a configuration to edit (or press Enter for the first one): ")
702
+ if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
703
+ config_index = matching_configs[int(choice)]
704
+ else:
705
+ config_index = matching_configs[0]
706
+ except (ValueError, IndexError, KeyboardInterrupt):
707
+ config_index = matching_configs[0]
708
+ else:
709
+ config_index = matching_configs[0]
710
+
653
711
  print(f"Editing existing configuration at index {config_index}")
712
+ elif args.config_index != 0 or '--config-index' in sys.argv:
713
+ # Check if the index is valid
714
+ configs = load_configs(str(config_path))
715
+ if args.config_index >= 0 and args.config_index < len(configs):
716
+ config_index = args.config_index
717
+ print(f"Editing existing configuration at index {config_index}")
718
+ else:
719
+ print(f"Configuration index {args.config_index} is out of range. Creating a new configuration.")
720
+ else:
721
+ # Creating a new config
722
+ configs = load_configs(str(config_path))
723
+ print(f"Creating new configuration at index {len(configs)}")
654
724
 
655
725
  add_config_entry(config_path, config_index)
656
726
  return
657
727
 
658
- # Load configuration using the specified index (needed for active config display)
659
- active_config = load_config(args.config, args.config_index)
728
+ # Load configuration using the specified index or provider (needed for active config display)
729
+ active_config = load_config(args.config, args.config_index, args.provider)
660
730
 
661
731
  # Command-line arguments override config settings for active config display
662
732
  # This part is kept to ensure the active config display reflects potential overrides,
@@ -675,31 +745,57 @@ def main():
675
745
 
676
746
  print(f"Configuration file: {config_path}")
677
747
  print(f"Total configurations: {len(configs)}")
678
- print(f"Active configuration index: {args.config_index}")
748
+
749
+ # Determine active configuration and display identifier
750
+ active_identifier = f"index {args.config_index}"
751
+ if args.provider:
752
+ active_identifier = f"provider '{args.provider}'"
753
+ print(f"Active configuration: {active_identifier}")
679
754
 
680
755
  if args.all:
681
756
  # Show details for all configurations
682
757
  print("\nAll configuration details:")
683
758
  for i, cfg in enumerate(configs):
684
- active_str = '(Active)' if i == args.config_index else ''
685
- print(f"\n--- Configuration Index {i} {active_str} ---")
759
+ provider = cfg.get('provider', 'N/A')
760
+ active_str = '(Active)' if (
761
+ (args.provider and provider.lower() == args.provider.lower()) or
762
+ (not args.provider and i == args.config_index)
763
+ ) else ''
764
+ print(f"\n--- Configuration Index {i} / Provider: {COLORS['green']}{provider}{COLORS['reset']} {active_str} ---")
686
765
  print(f" API Key: {'[Set]' if cfg.get('api_key') else '[Not Set]'}")
687
766
  print(f" Base URL: {cfg.get('base_url', 'N/A')}")
688
- print(f" Provider: {cfg.get('provider', 'N/A')}")
689
767
  print(f" Model: {cfg.get('model', 'N/A')}")
690
768
  else:
691
769
  # Show active config details and summary list
692
770
  print("\nActive configuration details:")
771
+ print(f" Provider: {COLORS['green']}{active_config.get('provider', 'N/A')}{COLORS['reset']}")
693
772
  print(f" API Key: {'[Set]' if active_config.get('api_key') else '[Not Set]'}")
694
773
  print(f" Base URL: {active_config.get('base_url', 'N/A')}")
695
- print(f" Provider: {active_config.get('provider', 'N/A')}")
696
774
  print(f" Model: {active_config.get('model', 'N/A')}")
697
775
 
698
776
  if len(configs) > 1:
699
777
  print("\nAvailable configurations:")
778
+ # Check for duplicate provider names for warning
779
+ provider_counts = {}
780
+ for cfg in configs:
781
+ provider = cfg.get('provider', 'N/A').lower()
782
+ provider_counts[provider] = provider_counts.get(provider, 0) + 1
783
+
700
784
  for i, cfg in enumerate(configs):
701
- active_marker = "*" if i == args.config_index else " "
702
- print(f"[{i}]{active_marker} {cfg.get('provider', 'N/A')} - {cfg.get('model', 'N/A')} ({'[API Key Set]' if cfg.get('api_key') else '[API Key Not Set]'})")
785
+ provider = cfg.get('provider', 'N/A')
786
+ provider_display = provider
787
+ # Add warning for duplicate providers
788
+ if provider_counts.get(provider.lower(), 0) > 1:
789
+ provider_display = f"{provider} {COLORS['yellow']}(duplicate){COLORS['reset']}"
790
+
791
+ active_marker = "*" if (
792
+ (args.provider and provider.lower() == args.provider.lower()) or
793
+ (not args.provider and i == args.config_index)
794
+ ) else " "
795
+ print(f"[{i}]{active_marker} {COLORS['green']}{provider_display}{COLORS['reset']} - {cfg.get('model', 'N/A')} ({'[API Key Set]' if cfg.get('api_key') else '[API Key Not Set]'})")
796
+
797
+ # Show instruction for using --provider
798
+ print(f"\nTip: Use {COLORS['yellow']}--provider NAME{COLORS['reset']} to select a configuration by provider name.")
703
799
 
704
800
  return
705
801
 
@@ -738,7 +834,7 @@ def main():
738
834
  # Interactive chat mode
739
835
  interactive_chat_session(client, web_search=args.web_search, no_stream=args.no_stream,
740
836
  temperature=args.temperature, top_p=args.top_p,
741
- max_length=args.max_length, log_file=args.log, preprompt=args.preprompt)
837
+ max_tokens=args.max_tokens, log_file=args.log, preprompt=args.preprompt)
742
838
  elif args.shell:
743
839
  if args.prompt is None:
744
840
  try:
@@ -752,7 +848,7 @@ def main():
752
848
 
753
849
  command = client.generate_shell_command(prompt, web_search=args.web_search,
754
850
  temperature=args.temperature, top_p=args.top_p,
755
- max_length=args.max_length)
851
+ max_tokens=args.max_tokens)
756
852
  if not command:
757
853
  return # Error already printed by client
758
854
 
@@ -790,7 +886,7 @@ def main():
790
886
 
791
887
  generated_code = client.generate_code(prompt, args.language, web_search=args.web_search,
792
888
  temperature=args.temperature, top_p=args.top_p,
793
- max_length=args.max_length)
889
+ max_tokens=args.max_tokens)
794
890
  if generated_code:
795
891
  print(f"\nGenerated code:\n{generated_code}")
796
892
 
@@ -913,7 +1009,7 @@ def main():
913
1009
 
914
1010
  response = client.chat(prompt, stream=not args.no_stream, web_search=args.web_search,
915
1011
  temperature=args.temperature, top_p=args.top_p,
916
- max_tokens=args.max_length, messages=messages)
1012
+ max_tokens=args.max_tokens, messages=messages)
917
1013
  if args.no_stream and response:
918
1014
  print(response)
919
1015
 
@@ -939,7 +1035,7 @@ def main():
939
1035
 
940
1036
  response = client.chat(prompt, stream=not args.no_stream, web_search=args.web_search,
941
1037
  temperature=args.temperature, top_p=args.top_p,
942
- max_tokens=args.max_length, messages=messages)
1038
+ max_tokens=args.max_tokens, messages=messages)
943
1039
  if args.no_stream and response:
944
1040
  print(response)
945
1041
 
@@ -167,7 +167,7 @@ class NGPTClient:
167
167
  web_search: bool = False,
168
168
  temperature: float = 0.4,
169
169
  top_p: float = 0.95,
170
- max_length: Optional[int] = None
170
+ max_tokens: Optional[int] = None
171
171
  ) -> str:
172
172
  """
173
173
  Generate a shell command based on the prompt.
@@ -177,7 +177,7 @@ class NGPTClient:
177
177
  web_search: Whether to enable web search capability
178
178
  temperature: Controls randomness in the response
179
179
  top_p: Controls diversity via nucleus sampling
180
- max_length: Maximum number of tokens to generate
180
+ max_tokens: Maximum number of tokens to generate
181
181
 
182
182
  Returns:
183
183
  The generated shell command
@@ -228,7 +228,7 @@ Command:"""
228
228
  web_search=web_search,
229
229
  temperature=temperature,
230
230
  top_p=top_p,
231
- max_tokens=max_length
231
+ max_tokens=max_tokens
232
232
  )
233
233
  except Exception as e:
234
234
  print(f"Error generating shell command: {e}")
@@ -241,7 +241,7 @@ Command:"""
241
241
  web_search: bool = False,
242
242
  temperature: float = 0.4,
243
243
  top_p: float = 0.95,
244
- max_length: Optional[int] = None
244
+ max_tokens: Optional[int] = None
245
245
  ) -> str:
246
246
  """
247
247
  Generate code based on the prompt.
@@ -252,7 +252,7 @@ Command:"""
252
252
  web_search: Whether to enable web search capability
253
253
  temperature: Controls randomness in the response
254
254
  top_p: Controls diversity via nucleus sampling
255
- max_length: Maximum number of tokens to generate
255
+ max_tokens: Maximum number of tokens to generate
256
256
 
257
257
  Returns:
258
258
  The generated code
@@ -285,7 +285,7 @@ Code:"""
285
285
  web_search=web_search,
286
286
  temperature=temperature,
287
287
  top_p=top_p,
288
- max_tokens=max_length
288
+ max_tokens=max_tokens
289
289
  )
290
290
  except Exception as e:
291
291
  print(f"Error generating code: {e}")
@@ -75,9 +75,29 @@ def add_config_entry(config_path: Path, config_index: Optional[int] = None) -> N
75
75
  if user_input:
76
76
  entry["base_url"] = user_input
77
77
 
78
- user_input = input(f"Provider [{entry['provider']}]: ")
79
- if user_input:
80
- entry["provider"] = user_input
78
+ # For provider, check for uniqueness when creating new config
79
+ provider_unique = False
80
+ original_provider = entry['provider']
81
+ while not provider_unique:
82
+ user_input = input(f"Provider [{entry['provider']}]: ")
83
+ if user_input:
84
+ provider = user_input
85
+ else:
86
+ provider = entry['provider']
87
+
88
+ # When creating new config or changing provider, check uniqueness
89
+ if is_existing_config and provider.lower() == original_provider.lower():
90
+ # No change in provider name, so keep it
91
+ provider_unique = True
92
+ elif is_provider_unique(configs, provider, config_index if is_existing_config else None):
93
+ provider_unique = True
94
+ else:
95
+ print(f"Error: Provider '{provider}' already exists. Please choose a unique provider name.")
96
+ # If it's the existing provider, allow keeping it (for existing configs)
97
+ if is_existing_config and provider.lower() == original_provider.lower():
98
+ provider_unique = True
99
+
100
+ entry["provider"] = provider
81
101
 
82
102
  user_input = input(f"Model [{entry['model']}]: ")
83
103
  if user_input:
@@ -127,13 +147,46 @@ def load_configs(custom_path: Optional[str] = None) -> List[Dict[str, Any]]:
127
147
 
128
148
  return configs
129
149
 
130
- def load_config(custom_path: Optional[str] = None, config_index: int = 0) -> Dict[str, Any]:
150
+ def load_config(custom_path: Optional[str] = None, config_index: int = 0, provider: Optional[str] = None) -> Dict[str, Any]:
131
151
  """
132
- Load a specific configuration by index and apply environment variables.
152
+ Load a specific configuration by index or provider name and apply environment variables.
133
153
  Environment variables take precedence over the config file.
154
+
155
+ Args:
156
+ custom_path: Optional path to a custom config file
157
+ config_index: Index of the configuration to use (default: 0)
158
+ provider: Provider name to identify the configuration
159
+
160
+ Returns:
161
+ The selected configuration with environment variables applied
134
162
  """
135
163
  configs = load_configs(custom_path)
136
164
 
165
+ # If provider is specified, try to find a matching config
166
+ if provider:
167
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == provider.lower()]
168
+
169
+ if not matching_configs:
170
+ print(f"Warning: No configuration found for provider '{provider}'. Using default configuration.")
171
+ config_index = 0
172
+ elif len(matching_configs) > 1:
173
+ print(f"Warning: Multiple configurations found for provider '{provider}'.")
174
+ for i, idx in enumerate(matching_configs):
175
+ print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
176
+
177
+ try:
178
+ choice = input("Choose a configuration (or press Enter for the first one): ")
179
+ if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
180
+ config_index = matching_configs[int(choice)]
181
+ else:
182
+ config_index = matching_configs[0]
183
+ print(f"Using first matching configuration (index {config_index}).")
184
+ except (ValueError, IndexError, KeyboardInterrupt):
185
+ config_index = matching_configs[0]
186
+ print(f"Using first matching configuration (index {config_index}).")
187
+ else:
188
+ config_index = matching_configs[0]
189
+
137
190
  # If config_index is out of range, use the first config
138
191
  if config_index < 0 or config_index >= len(configs):
139
192
  if len(configs) > 0:
@@ -157,7 +210,7 @@ def load_config(custom_path: Optional[str] = None, config_index: int = 0) -> Dic
157
210
  if env_var in os.environ and os.environ[env_var]:
158
211
  config[config_key] = os.environ[env_var]
159
212
 
160
- return config
213
+ return config
161
214
 
162
215
  def remove_config_entry(config_path: Path, config_index: int) -> bool:
163
216
  """
@@ -182,4 +235,24 @@ def remove_config_entry(config_path: Path, config_index: int) -> bool:
182
235
  return True
183
236
  except Exception as e:
184
237
  print(f"Error saving configuration: {e}")
185
- return False
238
+ return False
239
+
240
+ def is_provider_unique(configs: List[Dict[str, Any]], provider: str, exclude_index: Optional[int] = None) -> bool:
241
+ """
242
+ Check if a provider name is unique among configurations.
243
+
244
+ Args:
245
+ configs: List of configuration dictionaries
246
+ provider: Provider name to check
247
+ exclude_index: Optional index to exclude from the check (for updating existing config)
248
+
249
+ Returns:
250
+ True if the provider name is unique, False otherwise
251
+ """
252
+ provider = provider.lower()
253
+ for i, cfg in enumerate(configs):
254
+ if i == exclude_index:
255
+ continue
256
+ if cfg.get('provider', '').lower() == provider:
257
+ return False
258
+ return True
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ngpt"
3
- version = "2.3.3"
3
+ version = "2.4.0"
4
4
  description = "A lightweight Python CLI and library for interacting with OpenAI-compatible APIs, supporting both official and self-hosted LLM endpoints."
5
5
  authors = [
6
6
  {name = "nazDridoy", email = "nazdridoy399@gmail.com"},
@@ -113,7 +113,7 @@ wheels = [
113
113
 
114
114
  [[package]]
115
115
  name = "ngpt"
116
- version = "2.3.3"
116
+ version = "2.4.0"
117
117
  source = { editable = "." }
118
118
  dependencies = [
119
119
  { name = "prompt-toolkit" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes