Guides

Integrate the SDK into your CLI

Embedding the SDK as a library adds LevelFour subcommands to a CLI your team already ships: acme-cli costs summary, acme-cli recs list. You get typed objects and language-native error handling, and the LevelFour calls sit in the same code as the rest of your commands.

For an alternative that adds no dependency and shells out to the official l4 binary, see Integrate l4 into your CLI. Embed or shell out compares the two.

Pattern

  1. Install the SDK matching your host CLI's language
  2. Construct a client once, share via your CLI's context object
  3. Map SDK responses into your CLI's existing output formatter
  4. Map SDK typed errors to your CLI's exit codes
  5. Use the SDK's iterators for paginated endpoints

The status code to error class table, for all three languages, is in Error handling.

Wire it into your CLI

pip install levelfour click
cli.py
from __future__ import annotations
import sys
import click

from levelfour import LevelFour
from levelfour.exceptions import (
    LevelFourError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
)


def make_client(ctx):
    if "l4" not in ctx.obj:
        ctx.obj["l4"] = LevelFour()
    return ctx.obj["l4"]


@click.group()
@click.pass_context
def cli(ctx):
    ctx.ensure_object(dict)


@cli.group()
def costs():
    pass


@costs.command("summary")
@click.option("--provider", help="aws | gcp | azure | k8s")
@click.pass_context
def costs_summary(ctx, provider):
    try:
        l4 = make_client(ctx)
        if provider:
            data = l4.costs.get_provider_summary(provider)
        else:
            data = l4.costs.get_summary()
        click.echo(f"Monthly spend: ${data.monthly_spending:,.2f}")
        click.echo(f"Forecast: ${data.forecasted_monthly_costs:,.2f}")
        click.echo(f"Potential savings: ${data.potential_savings:,.2f}")
    except AuthenticationError:
        click.echo("Auth required. Set LEVELFOUR_API_KEY.", err=True)
        sys.exit(4)
    except RateLimitError as e:
        click.echo(f"Rate limited; retry after {e.retry_after}s", err=True)
        sys.exit(2)
    except NotFoundError as e:
        click.echo(f"Not found: {e.message}", err=True)
        sys.exit(1)
    except LevelFourError as e:
        click.echo(f"LevelFour error {e.status_code}: {e.message}", err=True)
        sys.exit(1)


@cli.group()
def recs():
    pass


@recs.command("list")
@click.option("--status", multiple=True)
@click.option("--service", multiple=True)
@click.pass_context
def recs_list(ctx, status, service):
    l4 = make_client(ctx)
    for rec in l4.recommendations.list(
        page_size=100,
        display_status=list(status) or None,
        service=list(service) or None,
    ):
        click.echo(f"{rec.recommendation_id}\t${rec.monthly_savings:.2f}/mo\t{rec.service}")


if __name__ == "__main__":
    cli()
The SDK's SyncPager fetches every page as you iterate, so page_size sizes each request and does not cap what comes back. You do not need a manual for page in pages loop. See Pagination.
npm install levelfour commander
cli.ts
import { Command } from "commander";
import {
    LevelFourClient,
    AuthenticationError,
    RateLimitError,
    NotFoundError,
    LevelFourError,
} from "levelfour";

const program = new Command();
let client: LevelFourClient | null = null;

function getClient() {
    if (!client) client = new LevelFourClient();
    return client;
}

function exitOnError(err: unknown): never {
    if (err instanceof AuthenticationError) {
        console.error("Auth required. Set LEVELFOUR_API_KEY.");
        process.exit(4);
    }
    if (err instanceof RateLimitError) {
        console.error(`Rate limited; retry after ${err.retryAfter}s`);
        process.exit(2);
    }
    if (err instanceof NotFoundError) {
        console.error(`Not found: ${err.message}`);
        process.exit(1);
    }
    if (err instanceof LevelFourError) {
        console.error(`LevelFour error ${err.statusCode}: ${err.message}`);
        process.exit(1);
    }
    throw err;
}

const costs = program.command("costs");
costs
    .command("summary")
    .option("--provider <id>", "aws | gcp | azure | k8s")
    .action(async (opts) => {
        try {
            const l4 = getClient();
            const data = opts.provider
                ? await l4.costs.getProviderSummary({ provider_id: opts.provider })
                : await l4.costs.getSummary();
            console.log(`Monthly spend: $${data.monthly_spending.toLocaleString()}`);
            console.log(`Forecast: $${data.forecasted_monthly_costs.toLocaleString()}`);
            console.log(`Potential savings: $${data.potential_savings.toLocaleString()}`);
        } catch (e) {
            exitOnError(e);
        }
    });

const recs = program.command("recs");
recs
    .command("list")
    .option("--status <s...>")
    .option("--service <s...>")
    .action(async (opts) => {
        try {
            const l4 = getClient();
            for await (const rec of await l4.recommendations.list({
                page_size: 100,
                display_status: opts.status,
                service: opts.service,
            })) {
                console.log(`${rec.recommendation_id}\t$${rec.monthly_savings.toFixed(2)}/mo\t${rec.service}`);
            }
        } catch (e) {
            exitOnError(e);
        }
    });

await program.parseAsync();
go get github.com/LevelFourAI/levelfour-go github.com/spf13/cobra

The Go sample builds costs-summary only. The Python and TypeScript tabs also show recs list and the --provider option.

main.go
package main

import (
    "context"
    "errors"
    "fmt"
    "os"

    "github.com/LevelFourAI/levelfour-go/levelfour"
    "github.com/spf13/cobra"
)

var l4 *levelfour.Client

func makeClient() (*levelfour.Client, error) {
    if l4 != nil {
        return l4, nil
    }
    c, err := levelfour.NewClient("")
    if err != nil {
        return nil, err
    }
    l4 = c
    return l4, nil
}

func exitOnError(err error) {
    var authErr *levelfour.UnauthorizedError
    var rateErr *levelfour.TooManyRequestsError
    var notFoundErr *levelfour.NotFoundError
    switch {
    case errors.As(err, &authErr):
        fmt.Fprintln(os.Stderr, "Auth required. Set LEVELFOUR_API_KEY.")
        os.Exit(4)
    case errors.As(err, &rateErr):
        fmt.Fprintln(os.Stderr, "Rate limited")
        os.Exit(2)
    case errors.As(err, &notFoundErr):
        fmt.Fprintln(os.Stderr, "Not found")
        os.Exit(1)
    default:
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func main() {
    root := &cobra.Command{Use: "acme"}

    costsSummary := &cobra.Command{
        Use: "costs-summary",
        RunE: func(cmd *cobra.Command, args []string) error {
            client, err := makeClient()
            if err != nil {
                exitOnError(err)
            }
            data, err := client.Costs.GetSummary(context.Background())
            if err != nil {
                exitOnError(err)
            }
            fmt.Printf("Monthly spend: $%.2f\n", data.MonthlySpending)
            return nil
        },
    }
    root.AddCommand(costsSummary)

    if err := root.Execute(); err != nil {
        os.Exit(1)
    }
}

levelfour.PageIterator and levelfour.CollectAll handle pagination. See Pagination.

Authentication

Your CLI does not need a credential layer of its own. If your team already injects an API key from ~/.acme/credentials or a secret manager, export it as LEVELFOUR_API_KEY from that source and every SDK picks it up. Authentication covers the key formats and the other ways to pass one.

Export the key from that source before your CLI constructs the client.

Embed or shell out

ConcernEmbed the SDKShell out to l4
Adds a dependencyYes (PyPI / npm / go get)No (a single static binary)
Typed objectsYes (Pydantic / TS types / Go structs)No (you parse JSON)
Control over filtering and paginationYes, the full SDK surfaceYes, via l4 flags and jq
Works in any languageNo (Python, TypeScript and Go only)Yes (anything that can exec)
Adds a binary install step in CINo (the package manager handles it)Yes (brew install or a release download)
Keeps PR diffs language-nativeYesMixes shell into your codebase
Staying currentLockfile-controlledl4 self-checks for updates

Embed the SDK when your CLI is in one of those three languages and you want first-class types. Shell out to l4 when you are scripting in bash or Make, when your CLI is in some other language, or when you do not want the dependency.