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
- Install the SDK matching your host CLI's language
- Construct a client once, share via your CLI's context object
- Map SDK responses into your CLI's existing output formatter
- Map SDK typed errors to your CLI's exit codes
- 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 clickfrom __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()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 commanderimport { 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/cobraThe Go sample builds costs-summary only. The Python and TypeScript tabs also show recs list and the --provider option.
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, ¬FoundErr):
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.
Embed or shell out
| Concern | Embed the SDK | Shell out to l4 |
|---|---|---|
| Adds a dependency | Yes (PyPI / npm / go get) | No (a single static binary) |
| Typed objects | Yes (Pydantic / TS types / Go structs) | No (you parse JSON) |
| Control over filtering and pagination | Yes, the full SDK surface | Yes, via l4 flags and jq |
| Works in any language | No (Python, TypeScript and Go only) | Yes (anything that can exec) |
| Adds a binary install step in CI | No (the package manager handles it) | Yes (brew install or a release download) |
| Keeps PR diffs language-native | Yes | Mixes shell into your codebase |
| Staying current | Lockfile-controlled | l4 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.
CI/CD Integration
Gate a pull request on cloud cost with l4 estimate and l4 diff, using ready-made workflows for GitHub Actions and GitLab CI.
Integrate l4 into your CLI
Shell out to the l4 binary from scripts, Makefiles and CI runners. Machine-readable output through --json and --jq, and stable exit codes to branch on.