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:

  1. CLI Argument Parsing: Arguments are parsed in ngpt/cli/main.py, capturing flags like --provider or --config-index.
  2. Configuration Selection: process_config_selection() in 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 retrieves the provider-specific settings as a dictionary.
  4. Client Initialization: The configuration dictionary is passed to NGPTClient in 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 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 --provider and --config-index flags.
  • Configuration management is handled by load_config() in ngpt/core/config.py, returning provider-agnostic dictionaries.
  • The NGPTClient class in 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. 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →