Lcr.Keys.Cli
0.1.0
dotnet tool install --global Lcr.Keys.Cli --version 0.1.0
dotnet new tool-manifest
dotnet tool install --local Lcr.Keys.Cli --version 0.1.0
#tool dotnet:?package=Lcr.Keys.Cli&version=0.1.0
nuke :add-package Lcr.Keys.Cli --version 0.1.0
lcr-keys
A command-line tool to manage LLM provider API keys stored in the
LLM Cost Router. Wraps the /v1/credentials HTTP API so customers can
add, list, inspect, and revoke their OpenAI / Anthropic / DeepSeek
keys without leaving the terminal.
- No plaintext key ever leaves the tool in stdout, logs, or files.
The router stores keys under envelope encryption; this CLI only
passes them through once during
addand never displays them again. - Single-binary install via
dotnet tool install --global. - Scriptable: every command supports
--jsonfor clean machine output, and exit codes follow Unix conventions.
Table of contents
- Requirements
- Install
- Quick start
- Configuration
- Commands
- Global flags
- JSON output shape
- Exit codes
- Security notes
- Troubleshooting
- Upgrade and uninstall
- License and issues
Requirements
- .NET 8 runtime (the SDK is not required;
dotnet --infoshould show at least one8.0.xruntime). - Network access to your LCR deployment (
LCR_API_URL). - A valid LCR bearer token (
LCR_API_KEY) issued to your customer account.
Install
dotnet tool install --global Lcr.Keys.Cli
The command name installed on your PATH is lcr-keys (not
Lcr.Keys.Cli). Confirm:
lcr-keys --version
lcr-keys --help
If lcr-keys is not found, add the dotnet tools directory to your
PATH:
| OS | Add to PATH |
|---|---|
| macOS / Linux | export PATH="$PATH:$HOME/.dotnet/tools" (add to ~/.zshrc / ~/.bashrc) |
| Windows (PowerShell) | $env:Path += ";$env:USERPROFILE\.dotnet\tools" |
Quick start
# 1. Tell the CLI where your router lives and who you are.
export LCR_API_URL=https://api.your-router.com
export LCR_API_KEY=lcr_live_xxxxxxxxxxxxxxxxxxxx
# 2. Store an OpenAI key (you'll be prompted; input is hidden).
lcr-keys add --provider openai --label prod-2026Q2
# 3. Verify it landed.
lcr-keys list
Expected output of list:
ID PROVIDER LABEL PREFIX LAST4 STATUS CREATED
------------------------------------------------------------------------------------------------------------------------
8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a openai prod-2026Q2 sk-proj a9f1 active 2026-05-20 18:00:00Z
Configuration
The CLI reads two values per invocation. Set them once via environment variables for the common case; flags override per-call.
| Variable | Purpose | Flag override |
|---|---|---|
LCR_API_URL |
Base URL of your LCR deployment, e.g. https://api.your-router.com |
--url |
LCR_API_KEY |
LCR bearer token (issued when your account was provisioned) | --token |
Resolution order (highest priority first):
--url/--tokenflag on the command lineLCR_API_URL/LCR_API_KEYenvironment variables- Error — the tool exits with a clear message naming the missing piece
The bearer token authenticates you to LCR. It is not the same thing as an OpenAI / Anthropic / DeepSeek provider key — those go in as the
--keyargument toadd.
Commands
All commands share the global flags. Use
lcr-keys <command> --help for the full per-command reference.
add — store a new key
Registers a provider API key with the router. The plaintext key never leaves your machine in cleartext after this call: the router encrypts it under your account's data key (envelope encryption, AES-256-GCM) before persisting.
lcr-keys add --provider <openai|anthropic|deepseek> \
--label <human-friendly-name> \
[--key <plaintext-key> | (prompted)] \
[--expires-at <YYYY-MM-DD>]
Hidden-input prompt
Omit --key and the tool will prompt without echoing:
$ lcr-keys add --provider anthropic --label staging
Provider API key (input hidden):
Explicit key (CI / scripting)
Suitable when the key already lives in a CI secret store and you have a way to inject it:
lcr-keys add --provider openai --label ci-runner \
--key "$OPENAI_API_KEY"
Beware of shell history: passing
--key sk-...literally records it in~/.bash_history. Prefer the prompt form or pull the key from an env var as shown above.
Output
Human-readable (default):
id: 8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a
provider: openai
label: prod-2026Q2
prefix...last4: sk-proj...a9f1
fingerprint: e5e280f3c1cc…
created: 2026-05-20 18:00:00Z
last used: (never)
last validated: (never)
JSON (--json):
{
"id": "8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a",
"provider": "openai",
"label": "prod-2026Q2",
"prefix": "sk-proj",
"last4": "a9f1",
"fingerprint_hex": "e5e280f3c1cc5539940bfde9e7fd9edb…",
"last_used_at": null,
"last_validated_at": null,
"last_validation_error": null,
"created_at": "2026-05-20T18:00:00Z",
"revoked_at": null
}
Conflict on duplicate
Re-submitting the same plaintext key (same SHA-256 fingerprint) returns HTTP 409 and the CLI exits with a non-zero status:
HTTP 409 Conflict: {"message":"credential already exists","id":"8f3c1e2a..."}
This is intentional — the router never accepts duplicates so revocation remains unambiguous.
list — show your stored keys
Returns every credential owned by your customer account (cross-tenant isolation is enforced server-side).
lcr-keys list # all active credentials
lcr-keys list --provider openai # filter by provider
lcr-keys list --include-revoked # include soft-deleted rows
lcr-keys ls # alias for list
Output
Default table form:
ID PROVIDER LABEL PREFIX LAST4 STATUS CREATED
------------------------------------------------------------------------------------------------------------------------
8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a openai prod-2026Q2 sk-proj a9f1 active 2026-05-20 18:00:00Z
1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a anthropic staging sk-ant 8b2c active 2026-05-20 17:42:11Z
JSON for scripting:
lcr-keys list --json | jq '.[] | select(.provider=="openai") | .id'
get — inspect one key
lcr-keys get 8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a
lcr-keys get 8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a --json
Returns the same per-credential view as add. If the id is not yours
(or does not exist), the server returns 404 to avoid leaking the
existence of other customers' credentials.
delete — revoke a key
Soft-revokes a credential. The row is preserved (audit trail), but excluded from routing lookups going forward, so the next LLM call will fail to find a key for that provider unless another active credential exists.
lcr-keys delete 8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a
# Soft-revoke credential 8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a? [y/N] y
# Revoked credential 8f3c1e2a9b6d4f0e8c7a2b5d1f9e3c4a.
lcr-keys delete 8f3c1e2a... --yes # skip the confirmation prompt
lcr-keys delete 8f3c1e2a... --yes --json
# {"deleted": "8f3c1e2a..."}
lcr-keys rm 8f3c1e2a... # alias for delete
To undo a soft-revoke, contact your LCR operator — there's no client-side restore in v1.
Global flags
All commands accept these in addition to their own options.
| Flag | Description |
|---|---|
--url <URL> |
Override LCR_API_URL for this invocation. |
--token <TOKEN> |
Override LCR_API_KEY for this invocation. |
--json |
Emit JSON instead of the human-readable table. |
-? / -h / --help |
Show help for the current command. |
--version |
Show the installed CLI version. |
Per-command flags:
| Command | Flag | Notes |
|---|---|---|
add |
--provider, -p |
One of openai, anthropic, deepseek. Required. |
add |
--label, -l |
Required. Used to recognize keys in list. |
add |
--key, -k |
Optional. Hidden-input prompt if omitted. |
add |
--expires-at |
ISO-8601 date reminder. Not server-enforced in v1. |
list |
--provider, -p |
Filter to one provider. |
list |
--include-revoked |
Show soft-deleted rows too. |
delete |
--yes, -y |
Skip the interactive [y/N] confirmation. |
JSON output shape
--json returns the raw router response. Useful fields:
{
"id": "<32-hex-char uuid>",
"provider": "openai | anthropic | deepseek",
"label": "string | null",
"prefix": "first 7 chars of the plaintext key, e.g. sk-proj",
"last4": "last 4 chars of the plaintext key",
"fingerprint_hex": "SHA-256(plaintext) as hex; deterministic; not reversible",
"last_used_at": "ISO-8601 timestamp | null",
"last_validated_at": "ISO-8601 timestamp | null",
"last_validation_error": "string | null",
"created_at": "ISO-8601 timestamp",
"revoked_at": "ISO-8601 timestamp | null (null = active)"
}
list returns a JSON array of the same shape.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Generic failure — bad arguments, HTTP error, network failure, server validation error (4xx / 5xx). The message on stderr names the cause. |
2 |
User aborted an interactive confirmation (e.g. answered n to delete). |
Suitable for shell scripts:
if lcr-keys get "$id" --json > /tmp/cred.json; then
echo "found"
else
echo "missing or unauthorized" >&2
fi
Security notes
- Plaintext keys never appear in tool output. Only
prefix(first 7 chars),last4(last 4), and a SHA-256fingerprint_hex(one-way) are ever returned by the router. - No on-disk cache. This CLI does not write any state to disk — every invocation is a one-shot HTTP request.
- Bearer token in env, not stored.
LCR_API_KEYlives in your shell's environment for the session. Use your OS keychain or a password manager to inject it into your shell on demand if you're concerned about shell history. - HTTPS enforced by the LCR deployment. Connecting over plain HTTP will fail unless your operator explicitly allows it.
- Cross-tenant safety. The server resolves
customer_idfrom the bearer token only — you cannot pivot to view or modify another customer's keys, and unknown ids return 404 (not 403) to avoid leaking existence.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
LCR_API_URL not configured… |
Neither env var nor --url set |
export LCR_API_URL=https://… or pass --url. |
LCR_API_KEY not configured… |
Neither env var nor --token set |
export LCR_API_KEY=lcr_live_… or pass --token. |
HTTP 401 Unauthorized |
Bearer token is wrong or expired | Verify with your operator; rotate via the LCR dashboard. |
HTTP 403 Forbidden: Provider 'X' is not enabled for this customer |
Account is not yet authorized for provider X |
Have your operator add X to customers.enabled_providers. |
HTTP 404 on get / delete |
Either id is wrong, or it belongs to another customer (return is identical for both) | Re-list and copy the id verbatim. |
HTTP 409 Conflict on add |
Same plaintext key already registered | Use list to find the existing entry; revoke the old one if rotating. |
lcr-keys: command not found |
~/.dotnet/tools not on PATH |
See Install. |
Hangs on add with no prompt |
The terminal is non-interactive (e.g. CI without a TTY) | Provide the key with --key "$ENV_VAR" instead. |
For verbose diagnostic output, set DOTNET_CLI_TELEMETRY_OPTOUT=1 and
re-run with --help to ensure the parser sees your arguments.
Upgrade and uninstall
# Latest stable
dotnet tool update --global Lcr.Keys.Cli
# Pinned version
dotnet tool update --global Lcr.Keys.Cli --version 0.1.1
# List all installed dotnet tools
dotnet tool list --global
# Remove
dotnet tool uninstall --global Lcr.Keys.Cli
License and issues
- License: MIT
- Source / issue tracker: https://github.com/bakhtiyar/llm_cost_saver
- Report bugs or request features by opening a GitHub issue.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 91 | 5/20/2026 |