# NGPT API Client Architecture: A Modular Abstraction for Multi-Provider LLMs

> Explore the NGPT API client architecture a modular abstraction separating LLM provider interactions into distinct layers for unified access to OpenAI, Anthropic, Gemini and more.

- Repository: [nazDridoy/ngpt](https://github.com/nazdridoy/ngpt)
- Tags: architecture
- Published: 2026-03-07

---

**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`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/cli/handlers/client_handler.py). This module merges command-line arguments, the CLI-wide configuration file managed by [`ngpt/core/config.py`](https://github.com/nazdridoy/ngpt/blob/main/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`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/core/config.py) reads the user's [`ngpt.conf`](https://github.com/nazdridoy/ngpt/blob/main/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`](https://github.com/nazdridoy/ngpt/blob/main/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:

1. **CLI Argument Parsing**: Arguments are parsed in [`ngpt/cli/main.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/cli/main.py), capturing flags like `--provider` or `--config-index`.
2. **Configuration Selection**: `process_config_selection()` in [`ngpt/cli/handlers/client_handler.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/cli/handlers/client_handler.py) resolves the effective provider and index, applying precedence rules and mutual exclusivity checks between the two flags.
3. **Configuration Loading**: `load_config()` in [`ngpt/core/config.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/core/config.py) retrieves the provider-specific settings as a dictionary.
4. **Client Initialization**: The configuration dictionary is passed to `NGPTClient` in [`ngpt/api/client.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/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`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/api/client.py) | Central client implementation | `NGPTClient` class, `chat()`, `embed()`, `list_models()`, `_request()` |
| [`ngpt/cli/handlers/client_handler.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/cli/handlers/client_handler.py) | Provider selection logic | `process_config_selection()`, mutual exclusivity enforcement |
| [`ngpt/core/config.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/core/config.py) | Configuration management | `load_config()`, configuration schema validation |
| [`ngpt/cli/main.py`](https://github.com/nazdridoy/ngpt/blob/main/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:

```python
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:

```python
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:

```python
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`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/cli/handlers/client_handler.py), enforcing mutual exclusivity between `--provider` and `--config-index` flags.
- **Configuration management** is handled by `load_config()` in [`ngpt/core/config.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/core/config.py), returning provider-agnostic dictionaries.
- The **`NGPTClient`** class in [`ngpt/api/client.py`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/api/client.py) adapts 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`](https://github.com/nazdridoy/ngpt/blob/main/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`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/core/config.py) via the `load_config()` function. This function reads the user's [`ngpt.conf`](https://github.com/nazdridoy/ngpt/blob/main/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`](https://github.com/nazdridoy/ngpt/blob/main/ngpt/api/client.py) is designed for standalone library usage. You can import it directly alongside `load_config()` from [`ngpt/core/config.py`](https://github.com/nazdridoy/ngpt/blob/main/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.