Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 397 additions & 31 deletions bin/omarchy-agent-account-add

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions bin/omarchy-agent-usage-deepseek
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ Local stats come from pi/omp session files whose assistant messages ran on the
deepseek provider, from opencode sessions that ran on a deepseek provider, and
from DeepSeek Harness (dsh) session logs on its DeepSeek routes. DeepSeek
billing is prepaid, so the record carries the live credit balance from
GET https://api.deepseek.com/user/balance when a key is available, plus the
optional funded/spent detail derived from ~/.config/omarchy/agents/deepseek.json
(the same shape the fireworks collector uses). The agents panel only ever reads
GET https://api.deepseek.com/user/balance when a key is available
(DEEPSEEK_API_KEY, then the key pasted into the agents panel, `apiKey` in
~/.config/omarchy/agents/deepseek.json, then the agents that keep one), plus
the optional funded/spent detail derived from that same file (the shape the
fireworks collector uses). The agents panel only ever reads
the JSON this prints.
"""

Expand Down Expand Up @@ -212,13 +214,14 @@ def read_dotenv_key(path: Path) -> str:


def api_key() -> str:
# The environment first, then the agents that keep a DeepSeek key, read
# only: pi and omp, opencode, and dsh's managed store with its `.env`
# fallback.
# The environment first, then the key pasted into the agents panel, then
# the agents that keep a DeepSeek key, read only: pi and omp, opencode, and
# dsh's managed store with its `.env` fallback.
env_key = str(os.environ.get("DEEPSEEK_API_KEY", "")).strip()
if env_key:
return env_key
for read in (
lambda: str(read_config().get("apiKey") or "").strip(),
lambda: read_json_key(pi_agent_dir() / "auth.json", "deepseek"),
lambda: read_json_key(Path.home() / ".omp" / "agent" / "auth.json", "deepseek"),
lambda: read_json_key(data_home() / "opencode" / "auth.json", "deepseek"),
Expand Down Expand Up @@ -267,7 +270,7 @@ def fetch_balance(api_base_url: str, key: str) -> dict[str, Any]:
payload = open_json(request, timeout=15)
except HttpError as error:
if error.status in (401, 403):
raise DeepSeekError("DeepSeek rejected the saved API key. Set a current one in DEEPSEEK_API_KEY or the agent that holds it.", refused=True) from error
raise DeepSeekError("DeepSeek rejected the saved API key. Add a current one in the agents panel (~/.config/omarchy/agents/deepseek.json), DEEPSEEK_API_KEY, or the agent that holds it.", refused=True) from error
if error.status:
raise DeepSeekError(f"DeepSeek's balance endpoint returned HTTP {error.status}.") from error
if error.transport:
Expand Down
10 changes: 7 additions & 3 deletions bin/omarchy-agent-usage-fireworks
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Token stats come from the Fireworks billing API grouped by day and model for
the last 30 days. Fireworks does not expose its prepaid ledger, so the record
carries an estimated balance instead of rate limits: the credits configured in
~/.config/omarchy/agents/fireworks.json minus rated account costs since the
funding date. The agents panel only ever reads the JSON this prints.
funding date. The API key is FIREWORKS_API_KEY, then the one pasted into the
agents panel (`apiKey` in that same fireworks.json), then firectl's, pi's or
opencode's. The agents panel only ever reads the JSON this prints.

Each API key keeps its account's last good usage rows and balance under the
agent-usage cache, so a failed check shows them stale rather than blank.
Expand Down Expand Up @@ -250,10 +252,12 @@ def read_config() -> dict[str, Any]:

def credentials(auth_path: Path, config: dict[str, Any]) -> tuple[str, str]:
file_key, file_account = read_auth_file(auth_path)
# Agent logins are the fallback ladder below an explicit key or a firectl
# login: pi first as Omarchy's default agent, opencode as the last resort.
# Agent logins are the fallback ladder below an explicit key, the key
# pasted into the agents panel, or a firectl login: pi first as Omarchy's
# default agent, opencode as the last resort.
api_key = (
str(os.environ.get("FIREWORKS_API_KEY", "")).strip()
or str(config.get("apiKey") or "").strip()
or file_key
or read_pi_key(pi_auth_path())
or read_opencode_key(opencode_auth_path())
Expand Down
14 changes: 11 additions & 3 deletions bin/omarchy-agent-usage-minimax
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"""Collect MiniMax usage into a display-ready JSON record.

The 5-hour session and weekly windows come from MiniMax's Token Plan API with
the key MiniMax's own `mmx` CLI, opencode, or pi saved. Each credential's last
MINIMAX_API_KEY, the key pasted into the agents panel (`apiKey` and an
optional `"region": "global"|"cn"` in ~/.config/omarchy/agents/minimax.json),
or the key MiniMax's own `mmx` CLI, opencode, or pi saved. Each credential's last
good limits are cached, so a failed check keeps showing them, marked stale,
rather than dropping the section.

Expand Down Expand Up @@ -34,6 +36,7 @@ from collectors import ( # noqa: E402
Stats,
base_record as shared_base_record,
cached_scan,
config_home,
data_home,
emit,
open_json,
Expand All @@ -54,8 +57,8 @@ AGENT_ID = "minimax"
AGENT_NAME = "MiniMax"
DEFAULT_BASE_URL = "https://api.minimax.io"
CN_BASE_URL = "https://api.minimaxi.com"
AUTH_HELP = "Set MINIMAX_API_KEY, or sign in to MiniMax in opencode, to show Token Plan limits."
AUTH_HELP_BAD_KEY = "MiniMax rejected the saved credentials. Set MINIMAX_API_KEY, or sign in to MiniMax again with `mmx` or opencode."
AUTH_HELP = "Add a MiniMax API key in the agents panel (~/.config/omarchy/agents/minimax.json), set MINIMAX_API_KEY, or sign in to MiniMax in opencode, to show Token Plan limits."
AUTH_HELP_BAD_KEY = "MiniMax rejected the saved credentials. Add a current key in the agents panel (~/.config/omarchy/agents/minimax.json), set MINIMAX_API_KEY, or sign in to MiniMax again with `mmx` or opencode."
# The plan the quota endpoint describes: only a Token Plan has these windows.
TIER_LABEL = "Token Plan"
# The pi/omp and opencode provider names MiniMax bills, by region. Exact
Expand Down Expand Up @@ -168,6 +171,11 @@ def credentials() -> tuple[str, str]:
if env_key:
return env_key, override or (CN_BASE_URL if os.environ.get("MINIMAX_REGION") == "cn" else DEFAULT_BASE_URL)

# The key pasted into the agents panel, on the region saved beside it.
saved = read_json(config_home() / "omarchy" / "agents" / "minimax.json")
if saved_api_key := str(saved.get("apiKey") or "").strip():
return saved_api_key, override or (CN_BASE_URL if saved.get("region") == "cn" else DEFAULT_BASE_URL)

config = mmx_config()
if mmx_key := config_key(config):
return mmx_key, override or config_base_url(config)
Expand Down
14 changes: 10 additions & 4 deletions bin/omarchy-agent-usage-opencode
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ Local stats come from pi/omp sessions that ran through an opencode provider
and from opencode's own message database, both of its stores. Only the Zen
and Go providers count: opencode runs on Anthropic or OpenAI belong to the
claude and codex records. The limits are the Go plan's rolling, weekly, and
monthly meters from OpenCode's usage endpoint. The agents panel only ever
reads the JSON this prints.
monthly meters from OpenCode's usage endpoint, asked with OPENCODE_API_KEY or
OPENCODE_GO_API_KEY, the key pasted into the agents panel (`apiKey` in
~/.config/omarchy/agents/opencode.json), then the keys opencode, pi and omp
keep. The agents panel only ever reads the JSON this prints.
"""

import argparse
Expand All @@ -34,13 +36,15 @@ from collectors import ( # noqa: E402
base_record,
cache_root, # noqa: F401 - the tests reach the cache through it
cached_scan,
config_home,
data_home,
emit,
limits_cache_path as shared_limits_cache_path,
number,
open_json,
opencode_db,
pi_session_roots,
read_json,
scan_max_age,
scan_opencode,
scan_pi_sessions,
Expand Down Expand Up @@ -198,14 +202,16 @@ def read_store_credentials(db):


def credential_candidates():
# An explicit key wins, then opencode's own credential stores, then pi's:
# An explicit key wins, then the key pasted into the agents panel, then
# opencode's own credential stores, then pi's:
# pi drives the same Zen subscription and records it under the same
# provider ids, so a machine that codes through pi never signs the opencode
# CLI in. The Console session is the implicit one, so it comes last. Which
# one the endpoint accepts isn't knowable from here, so a refused one only
# hands over to the next.
store_keys, console = read_store_credentials(opencode_db())
tokens = [str(os.environ.get(name, "")).strip() for name in ("OPENCODE_API_KEY", "OPENCODE_GO_API_KEY")]
tokens.append(str(read_json(config_home() / "omarchy" / "agents" / "opencode.json").get("apiKey") or "").strip())
tokens += store_keys
tokens.append(read_auth_key(data_home() / "opencode" / "auth.json"))
tokens.append(read_auth_key(pi_agent_dir() / "auth.json"))
Expand Down Expand Up @@ -287,7 +293,7 @@ def probe_limits(credential):
# A refused credential may just be the wrong one for this account, so the
# next way in still gets its turn.
if error.status == 401:
return {"ok": False, "refused": True, "helpText": "OpenCode rejected the saved key. Reconnect OpenCode Go with /connect in the TUI, or set OPENCODE_API_KEY."}
return {"ok": False, "refused": True, "helpText": "OpenCode rejected the saved key. Reconnect OpenCode Go with /connect in the TUI, add a current key in the agents panel (~/.config/omarchy/agents/opencode.json), or set OPENCODE_API_KEY."}
if error.status == 403:
# A key without a Go plan behind it: Zen pay-as-you-go has no meters,
# which is no trouble at all.
Expand Down
15 changes: 8 additions & 7 deletions bin/omarchy-agent-usage-openrouter
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ opencode SQLite DB
(providerID == "openrouter", role == "assistant") and from pi/omp session
files whose assistant messages ran on the openrouter provider, read-only.
Each key keeps its own last good meter and balance, shown stale when a
check fails. The key is OPENROUTER_API_KEY, opencode's or pi/omp's saved
OpenRouter login, or the config file's. Without any, the local stats still
show and the record waits for auth.
check fails. The key is OPENROUTER_API_KEY, the one pasted into the agents
panel (`apiKey` in ~/.config/omarchy/agents/openrouter.json), or opencode's
or pi/omp's saved OpenRouter login. Without any, the local stats still show
and the record waits for auth.

Tier 2 (management key): when OPENROUTER_MANAGEMENT_KEY (or the management
key in the config file) is set, the last-30-days token history comes from
Expand Down Expand Up @@ -80,7 +81,7 @@ from collectors import ( # noqa: E402
AGENT_ID = "openrouter"
AGENT_NAME = "OpenRouter"
PROVIDER = "openrouter"
AUTH_HELP = "Set OPENROUTER_API_KEY, or sign in to OpenRouter in opencode, pi or omp. For the 30-day history, add a management key as OPENROUTER_MANAGEMENT_KEY (openrouter.ai/settings/management-keys)."
AUTH_HELP = "Set OPENROUTER_API_KEY, add a key in the agents panel (~/.config/omarchy/agents/openrouter.json), or sign in to OpenRouter in opencode, pi or omp. For the 30-day history, add a management key as OPENROUTER_MANAGEMENT_KEY (openrouter.ai/settings/management-keys)."
MANAGEMENT_AUTH_HELP = "Replace OPENROUTER_MANAGEMENT_KEY (or managementKey in ~/.config/omarchy/agents/openrouter.json) with a working one from openrouter.ai/settings/management-keys."
API_BASE = "https://openrouter.ai/api/v1"

Expand Down Expand Up @@ -121,14 +122,14 @@ def read_pi_key(path: Path) -> str:


def api_key(config: dict[str, Any]) -> str:
# Explicit key or an opencode, pi or omp login wins; the config file is
# the last resort for headless/refresh-timer runs without any.
# An explicit key wins, then the key pasted into the agents panel, then
# an opencode, pi or omp login.
return (
str(os.environ.get("OPENROUTER_API_KEY", "")).strip()
or str(config.get("apiKey") or "").strip()
or read_opencode_key(data_home() / "opencode" / "auth.json")
or read_pi_key(pi_agent_dir() / "auth.json")
or read_pi_key(Path.home() / ".omp" / "agent" / "auth.json")
or str(config.get("apiKey") or "").strip()
)


Expand Down
13 changes: 10 additions & 3 deletions bin/omarchy-agent-usage-synthetic
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ synthetic provider, and from Claude Code, MiniMax Code and Codex sessions sent
to Synthetic's API. The account's allowances (the rolling five-hour request
limit, the weekly credits, search and free tool calls) come from Synthetic's
documented GET https://api.synthetic.new/v2/quotas, which doesn't count
against them. The agents panel only ever reads the JSON this prints.
against them, asked with SYNTHETIC_API_KEY, the key pasted into the agents
panel (`apiKey` in ~/.config/omarchy/agents/synthetic.json), or the one pi,
omp or opencode saved. The agents panel only ever reads the JSON this prints.
"""

import argparse
Expand All @@ -30,6 +32,7 @@ from collectors import ( # noqa: E402
Stats,
base_record,
cached_scan,
config_home,
data_home,
emit,
number,
Expand All @@ -51,8 +54,8 @@ AGENT_ID = "synthetic"
AGENT_NAME = "Synthetic"
PROVIDER = "synthetic"
API_BASE_URL = "https://api.synthetic.new"
AUTH_HELP = "Set SYNTHETIC_API_KEY, or sign in to Synthetic in pi, omp or opencode, to read its quotas."
REFUSED_HELP = "Synthetic rejected the API key. Set a current one in SYNTHETIC_API_KEY or sign in to Synthetic again."
AUTH_HELP = "Add a Synthetic API key in the agents panel (~/.config/omarchy/agents/synthetic.json), set SYNTHETIC_API_KEY, or sign in to Synthetic in pi, omp or opencode, to read its quotas."
REFUSED_HELP = "Synthetic rejected the API key. Add a current one in the agents panel (~/.config/omarchy/agents/synthetic.json) or SYNTHETIC_API_KEY, or sign in to Synthetic again."


def finite(value):
Expand Down Expand Up @@ -141,6 +144,10 @@ def api_key():
explicit = str(os.environ.get("SYNTHETIC_API_KEY", "")).strip()
if explicit:
return explicit
# The key pasted into the agents panel, ahead of any other agent's store.
saved = str(read_json(config_home() / "omarchy" / "agents" / "synthetic.json").get("apiKey") or "").strip()
if saved:
return saved
# The default pi and omp sign-ins, then opencode's, then each named profile's:
# a profile's sessions are counted, so a sign-in made only there must read
# its quotas too, without outranking the defaults.
Expand Down
7 changes: 7 additions & 0 deletions default/i18n/en/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"agents.homeFailed": "Couldn't set up a home for the new account.",
"agents.installCliFailed": "Installing the %1 CLI didn't finish.",
"agents.installingCli": "Installing the %1 CLI…",
"agents.keyChecking": "Checking the key with %1…",
"agents.keyEmpty": "No API key was pasted.",
"agents.keyRejected": "%1 rejected the key.",
"agents.keySaveFailed": "Couldn't save the %1 API key.",
"agents.keySaved": "Saved the %1 API key.",
"agents.keyUnreadable": "%1 isn't valid JSON, so the key wasn't saved there.",
"agents.loginUnfinished": "The login didn't finish, so no account was added.",
"agents.newSessionsUse": "New %1 sessions now use %2",
"agents.noAccountNamed": "No %1 account named %2.",
Expand All @@ -21,6 +27,7 @@
"agents.signInBrowser": "Sign in to %1 in the browser window that opens.",
"agents.signInFailed": "The %1 sign-in didn't finish.",
"agents.signInPrivate": "Sign in as the account you're adding in the private window that opens.",
"agents.signInTerminal": "Finish signing in to %1 in the terminal window that opens.",
"agents.signedIn": "Signed in to %1.",
"agents.signedInAgain": "Signed in to %1 again.",
"agents.stayingOn": "Staying on %1.",
Expand Down
7 changes: 7 additions & 0 deletions default/i18n/en/shell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"agents.addAccount": "Add an account",
"agents.addProviderAccount": "Add %1 account",
"agents.addSubscription": "Add a subscription",
"agents.apiKeys": "API keys",
"agents.app": "App",
"agents.autoswitch": "Autoswitch",
"agents.backToLimits": "Back to the limits",
Expand All @@ -25,6 +26,7 @@
"one": "%1 free reset · next expires in %2",
"other": "%1 free resets · next expires in %2"
},
"agents.getKey": "Get a key",
"agents.helpFireworksSignIn": "Set %1, run %2, or sign in to Fireworks in pi or opencode.",
"agents.helpNotInPath": "%1 not found in PATH",
"agents.helpRunToAuthenticate": "Run %1 to authenticate.",
Expand All @@ -33,6 +35,7 @@
"agents.helpSignInExpired": "%1's saved sign-in expired. Start %1 to refresh it.",
"agents.helpStartOrRunToSignIn": "Start %1, or run %2, to sign in.",
"agents.helpStartToSeeLimits": "Start %1 to see its limits; signing in again is not needed.",
"agents.keySaved": "Key saved",
"agents.lastUpdatedAgo": "Last updated %1 ago",
"agents.lastUpdatedJustNow": "Last updated less than a minute ago",
"agents.lastUpdatedUnknown": "Last updated time unavailable",
Expand All @@ -49,7 +52,9 @@
"agents.nameAccount": "Name this account. It signs in through a private window, so your browser's current account isn't picked up.",
"agents.outOfCredits": "Out of credits",
"agents.pasteCode": "If the page shows a code instead of finishing, paste it here.",
"agents.pasteKey": "Paste your %1 API key. It stays on this machine, readable only by you, and is checked with %1 once.",
"agents.placeholderCode": "Code",
"agents.placeholderKey": "API key",
"agents.placeholderWork": "Work",
"agents.plugin": "Plugin",
"agents.promptsToday": {
Expand All @@ -59,6 +64,7 @@
"agents.providerLimitsUnavailable": "%1 limits unavailable",
"agents.providerUnavailable": "%1 unavailable",
"agents.reopenSignIn": "Open the sign-in page again",
"agents.savingKey": "Checking the key…",
"agents.scopedMonthly": "%1: %2% of its monthly allowance",
"agents.scopedOther": "%1: %2% of its %3 allowance",
"agents.scopedSession": "%1: %2% of its session allowance",
Expand All @@ -73,6 +79,7 @@
"agents.signInIntro": "Sign in to an AI coding subscription, and this panel keeps track of how much of it you have left.",
"agents.signInRequired": "Sign-in required",
"agents.signInUnfinished": "The sign-in didn't finish.",
"agents.signedInShort": "Signed in",
"agents.startDefaultAgent": "Start the default agent",
"agents.startOnNew": "Start your default agent on a new %1",
"agents.starting": "Starting…",
Expand Down
1 change: 1 addition & 0 deletions docs/agent-usage-collectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ New collectors build on the shared module, [`shell/plugins/agents/collectors.py`
- `--force` (a person pressing refresh) reuses nothing.
- The record is printed on stdout. Diagnostics go to stderr and never carry a credential.
- A collector never writes to an agent's own files. Credentials are only read, and a token is refreshed only where the agent's own CLI does the same under its lock, never by rewriting another harness's store.
- A key-only provider (an API key, no sign-in) reads the key pasted into the panel's add-account screen, which `omarchy-agent-account-add --key <id>` saves as `apiKey` in `~/.config/omarchy/agents/<id>.json` (Kimchi's goes to Kimchi's own `~/.config/kimchi/config.json` instead). It comes right after the provider's own environment variables and before any other agent's or CLI's store, so a pasted key is the one asked with unless one is exported (Z.ai's `zai.json` predates this and outranks its environment variables too). The file can hold other fields a person wrote by hand (a funded amount, a region, a management key), so the writer merges into it.

## The shared module

Expand Down
Loading
Loading