NGPT API Client Architecture: A Modular Abstraction for Multi-Provider LLMs
The NGPT library implements a provider-agnostic client architecture that separates provider selection, configuration management, and HTTP request dispatch into distinct layers, enabling seamless interaction with OpenAI, Anthropic, Gemini, and other LLM providers through a unified interface.
The nazdridoy/ngpt repository provides a Python CLI and library for interacting with various large language model providers. At its core, the NGPT API client architecture centralizes all provider-specific logic within a single client class while delegating configuration and selection concerns to dedicated handler modules.
Three-Layer Architecture Design
The architecture separates concerns into three distinct layers to ensure provider-agnostic operation.
Provider Selection and Configuration Resolution
The active provider and its configuration index are resolved by the client handler in ngpt/cli/handlers/client_handler.py. This module merges command-line arguments, the CLI-wide configuration file managed by ngpt/core/config.py, and system defaults. It enforces the constraint that --provider and --config-index flags are mutually exclusive, preventing ambiguous configuration states.
Configuration Loading
The load_config() function in ngpt/core/config.py reads the user's ngpt.conf file (or a custom path) and returns a dictionary containing the provider name, API key, endpoint URL, and model defaults. This dictionary is passed directly to the client constructor, ensuring the client remains decoupled from file system operations.
Request Dispatch and Adaptation
The NGPTClient class in ngpt/api/client.py handles HTTP request construction and dispatch. It builds payloads for high-level operations like chat, embedding, and model listing, then sends them through a generic _request() helper. Because payload formats vary between providers—such as OpenAI's messages array versus Gemini's content structure—the client inspects the loaded configuration and adapts the request dynamically.
Provider Resolution Flow
The path from CLI invocation to active client follows a strict pipeline:
- CLI Argument Parsing: Arguments are parsed in
ngpt/cli/main.py, capturing flags like--provideror--config-index. - Configuration Selection:
process_config_selection()inngpt/cli/handlers/client_handler.pyresolves the effective provider and index, applying precedence rules and mutual exclusivity checks between the two flags. - Configuration Loading:
load_config()inngpt/core/config.pyretrieves the provider-specific settings as a dictionary. - Client Initialization: The configuration dictionary is passed to
NGPTClientinngpt/api/client.py, completing the abstraction layer.
This flow ensures that UI components, shell modes, and rewrite utilities can instantiate a client without knowledge of the underlying provider implementation.
Core Implementation Files
| File | Role | Key Components |
|---|---|---|
ngpt/api/client.py |
Central client implementation | NGPTClient class, chat(), embed(), list_models(), _request() |
ngpt/cli/handlers/client_handler.py |
Provider selection logic | process_config_selection(), mutual exclusivity enforcement |
ngpt/core/config.py |
Configuration management | load_config(), configuration schema validation |
ngpt/cli/main.py |
Entry point and wiring | CLI argument parsing, handler orchestration |
Practical Usage Examples
Basic Script Usage
The following example demonstrates direct library usage without CLI overhead:
from ngpt.api.client import NGPTClient
from ngpt.core.config import load_config
# Load configuration for the OpenAI provider
cfg = load_config(provider="OpenAI")
# Initialize the client with resolved settings
client = NGPTClient(
provider=cfg["provider"],
config_index=0,
api_key=cfg["api_key"],
base_url=cfg.get("base_url"),
model=cfg.get("model")
)
# Execute a chat completion
response = client.chat(
messages=[{"role": "user", "content": "Explain quantum tunneling in one sentence."}]
)
print(response["choices"][0]["message"]["content"])
This pattern works identically for Anthropic, Gemini, or other providers by changing the provider argument to load_config().
CLI-Level Abstraction
To replicate the internal flow of the ngpt command:
from ngpt.cli.handlers.client_handler import process_config_selection
from ngpt.core.config import load_config
from ngpt.api.client import NGPTClient
# Simulate parsed CLI arguments
class Args:
provider = None # Would be set by --provider flag
config_index = 0 # Would be set by --config-index flag
config = False # Custom config file path
args = Args()
cli_cfg = {} # CLI-wide configuration from ngpt-cli.conf
# Resolve effective provider and index
effective_provider, effective_index = process_config_selection(args, cli_cfg)
# Load the active configuration
active_cfg = load_config(args.config, effective_index, effective_provider)
# Initialize client
client = NGPTClient(
provider=active_cfg["provider"],
config_index=effective_index,
api_key=active_cfg["api_key"],
base_url=active_cfg.get("base_url"),
model=active_cfg.get("model")
)
# Perform request
reply = client.chat(messages=[{"role": "user", "content": "What is NGPT?"}])
print(reply)
This demonstrates how the CLI maintains clean separation between argument parsing, configuration resolution, and API execution.
Listing Available Models
The client automatically routes to the correct endpoint for model discovery:
models = client.list_models()
print("Supported models:", models)
This method hits provider-specific endpoints such as /v1/models for OpenAI or the Gemini model-list API, returning a standardized Python list regardless of the underlying response format.
Summary
- The NGPT API client architecture employs a three-layer design separating provider selection, configuration loading, and HTTP request dispatch.
- Provider resolution occurs in
ngpt/cli/handlers/client_handler.py, enforcing mutual exclusivity between--providerand--config-indexflags. - Configuration management is handled by
load_config()inngpt/core/config.py, returning provider-agnostic dictionaries. - The
NGPTClientclass inngpt/api/client.pyadapts request payloads dynamically based on the active provider, supporting OpenAI, Anthropic, Gemini, and others through a unified interface. - Adding new providers requires only configuration schema updates and minimal payload adaptation logic, leaving UI and shell components unchanged.
Frequently Asked Questions
How does NGPT handle different API payload formats between providers?
The NGPTClient class inspects the provider name stored in its configuration and adapts the request payload accordingly. For example, it constructs OpenAI-compatible messages arrays for ChatGPT endpoints while formatting Gemini-style content objects for Google AI Studio. This adaptation happens within methods like chat() before calling the generic _request() helper, ensuring provider-specific formatting is isolated from the rest of the application.
What is the relationship between --provider and --config-index CLI flags?
These flags are mutually exclusive according to the resolution logic in ngpt/cli/handlers/client_handler.py. The --provider flag selects a provider by name (e.g., "OpenAI" or "Anthropic"), while --config-index selects a specific configuration entry by its numeric index in the config file. Using both simultaneously creates an ambiguous selection state, so the handler enforces that only one may be specified per invocation.
Where is the provider configuration actually loaded from?
Configuration loading occurs in ngpt/core/config.py via the load_config() function. This function reads the user's ngpt.conf file (or a custom path specified via --config), validates the structure, and returns a dictionary containing the provider name, API key, base URL, and model defaults. The client remains decoupled from file I/O by receiving this dictionary through its constructor.
Can I use the NGPT client directly without the CLI?
Yes, the NGPTClient class in ngpt/api/client.py is designed for standalone library usage. You can import it directly alongside load_config() from ngpt/core/config.py to instantiate a client programmatically. This allows integration into custom Python scripts, web applications, or automation tools without invoking the command-line interface, while still benefiting from the same provider abstraction and configuration management.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →