Skip to content
Merged
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
282 changes: 268 additions & 14 deletions BarWidget.qml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ A native Quickshell plugin for quotas, accounts, and your local AI proxy.
- **Ready for screenshots.** Emails are softly blurred by default. Click to reveal, click again to hide; closing the popup conceals them automatically. Inline logs redact email addresses.
- **Honest quota states.** Unknown is not zero. A failed refresh preserves the last reading with a stale-data warning.
- **Independent service.** The proxy keeps running when the desktop shell reloads.
- **Routing controls.** Adjust supported strategies, session affinity, credential weights, and retry limits. See [routing and API providers](docs/routing.md).
- **Private diagnostics.** Inspect available account counters and upstream-key aggregates, or explicitly capture the consuming activity queue. See [diagnostics and their limits](docs/diagnostics.md).
- **Named client keys.** Create, copy, and revoke separate downstream keys while preserving the primary key. See [client keys](docs/client-keys.md).
- **Optional quota alerts.** Enable low-quota, observed reset, and explicit authentication alerts. See [desktop alerts](docs/quota-alerts.md).

<details>
<summary><strong>See account management</strong></summary>
Expand Down Expand Up @@ -143,6 +147,8 @@ Integration tests use a separate proxy on an ephemeral loopback port and a mock

See [Codex and T3 compatibility checks](docs/client-compatibility.md) for Responses streaming, tool calls, WebSockets, interruption and optional real Codex CLI/app-server lanes. These fixtures verify local protocol behavior; actual T3 UI and authenticated provider behavior require separate acceptance checks.

Use the [isolated native preview](docs/native-preview.md) to exercise the panel with fake receipts and installed Omarchy components without changing your live plugin or service.

[Contributing](CONTRIBUTING.md) · [Architecture](docs/architecture.md) · [Report a bug](https://github.com/soojy/omaproxy/issues/new?template=bug_report.md)

## Credits
Expand Down
52 changes: 52 additions & 0 deletions docs/client-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Named client access keys

Use a separate downstream access key for each client, for example `codex-cli`, `t3-code`, `opencode`, `agy` or `kiro-cli`. Creating a named key enables it on the proxy. Copy it to the intended client's credential configuration. Creating a key does not change that client's configuration automatically.

Client names contain 1-60 ASCII letters, digits, dots, underscores or hyphens and start with a letter or digit. Names identify the keys the user created here. Existing backend keys remain unmanaged; the helper does not assign names to the primary key or other existing credentials.

## Storage and display

The private `client-keys.json` registry contains only a name, a keyed HMAC label and a creation state for each managed key. It never stores raw access keys. Writes use an atomic replacement with mode `0600`; newly created registry directories use mode `0700`. Invalid, oversized or symlinked registry files fail without replacement.

`key_label` is `client-` followed by the first 16 hex characters of HMAC-SHA256 over `client\0` plus the access-key value. The private management key is the HMAC salt. This matches diagnostics' client labels when diagnostics uses the same salt. The salt must be stable and nonempty. Changing the management key changes these labels; old registry entries cannot then be matched automatically.

Lists return only `{name, key_label, active}` rows. `active` describes whether the exact managed key appears in the current backend access-key list. An unavailable or invalid backend list raises a fixed error; it does not turn missing evidence into inactive rows. Both registry and backend lists are limited to 200 entries, and the registry is limited to 64 KiB. An oversized backend list blocks mutations rather than truncating the population.

Copy fetches the access-key list from the backend, matches the HMAC label, and sends the selected raw key only to `wl-copy` standard input. The token is absent from command arguments and returned JSON. Clipboard output and error streams are discarded. Errors use fixed messages without exception bodies. The clipboard contains the copied credential until the user replaces or clears it.

## Backend contract and recovery

The verified v7 and v8 sources retain the downstream `/v0/management/api-keys` contract:

| Operation | Request |
| --- | --- |
| Read | `GET /v0/management/api-keys`, response `{"api-keys": [...]}` |
| Create | `PATCH /v0/management/api-keys` with `{"old": generated_value, "new": generated_value}` |
| Revoke | `DELETE /v0/management/api-keys?value=encoded_value` |

For `PATCH`, matching `old` replaces that exact entry; an absent `old` appends `new`. Setting both to the same generated key makes a repeated append idempotent. The helper never submits a full replacement array or deletes by index.

Before creation, the helper writes a pending HMAC record. It marks the record active only after backend readback finds the key. A failed response or readback retains that record. Repeating creation with the same name reads the existing key and does not generate another credential. If the named key remains absent, creation fails until the user refreshes or revokes the inactive record. Revocation of an inactive record removes only its local name.

The backend's delete-by-value operation compares trimmed values. Revocation refuses a primary-key match or any distinct key with the same trimmed value. Callers must supply the real `settings.api_key` as `primary_key` to revoke; omission fails before contacting the backend. Readback must confirm absence before removing the name. A failed revocation retains the record for retry. Other backend keys remain untouched.

The caller must hold its management mutation lock across creation and revocation, including registry writes and readback. This protects local named operations; it does not lock changes made independently in another management client. The helper does not read private settings itself.

## Client attribution

A separate key supports attribution only when a request receipt actually includes the downstream client access-key value or an explicitly documented downstream client-key source field. Match that value with the same HMAC salt, then resolve its named record.

Upstream provider API-key counters, OAuth account counters, model names and routing choices do not identify a downstream client. A named key list proves access configuration, not that a particular request came from the named client. The current upstream aggregate diagnostics do not supply that request attribution.

## Helper API

```python
list_keys(api, path, salt, primary_key=None)
create_key(api, path, salt, name, primary_key=None)
revoke_key(api, path, salt, name, primary_key=None)
copy_key(api, path, salt, name, primary_key=None, runner=subprocess.run)
```

`path` is the complete registry filename. `api` accepts absolute management routes with `method="GET"`, `body=None` and optional `timeout=4`; it raises on request failures. Creation returns `client_keys` and the sanitized selected `client_key`. Revocation returns `client_keys`, `revoked` and `name`. Copy returns `copied`, `name` and `key_label`. No result contains the raw key.

Run `python -m unittest discover -s tests -p test_client_keys.py -v`. Set `OMAPROXY_TEST_BINARY` to a vetted backend executable to enable the isolated integration test. It starts a temporary loopback backend, creates and revokes one key, and verifies primary-key, upstream-key and YAML-comment preservation. It neither reads nor modifies live settings or the live service.
57 changes: 57 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Diagnostics and telemetry scope

`diagnostics.snapshot(api, accounts=None, salt=None, consume_queue=False)` returns a bounded, redacted snapshot. Its adapter accepts full management paths and a `timeout` keyword, performs a GET, and raises an HTTP error for unsuccessful responses. Each call uses a two-second timeout. Up to four calls probe usage and accounts; an explicit queue capture can add two calls. The bridge rejects JSON response bodies above 2 MiB before decoding them.

The return value contains `usage`, `accounts`, `queue`, `client_attribution`, and `limitations`. Usage and accounts contain `availability`, `source`, `error`, `records`, `retained`, `omitted`, and `invalid`. Supported records contain anonymous `label`, `provider`, `success`, `failed`, and `recent_requests`. No overall activity total is inferred from an incomplete population. Counts are cumulative backend attempt counters; they do not count unique client requests or prove billing charges. Retention is limited to 128 records and 20 recent buckets per record. Unknown values are not replaced with zero.

Pass the already fetched raw auth-file list through `accounts` to avoid fetching it again. Pass a private, stable installation salt (for example, the existing management credential, kept in Python) through `salt` to keep labels stable across bridge invocations. The salt never enters the result. Without it, labels remain stable only within the Python process. Resetting the salt changes every anonymous label. The helper writes no files and retains no request history.

## Verified routes and schemas

The implementation was checked against these exact upstream tags:

- [v7.2.154](https://github.com/router-for-me/CLIProxyAPI/tree/v7.2.154), commit `ba7e55836dee959e93ec6d41395865d9ec535086`.
- [v8.0.13](https://github.com/router-for-me/CLIProxyAPI/tree/v8.0.13), commit `d7914afdedca7af95ee974a42453dc49fc1388ce`.

| Data | v8 route | Legacy route | Meaning |
| --- | --- | --- | --- |
| Upstream API-key counters | `/v8/management/observability/usage/api-keys` | `/v0/management/api-key-usage` | Upstream API-key accounts grouped by provider and `base_url\|api_key`; excludes OAuth credentials |
| Account counters | `/v8/management/credentials` | `/v0/management/auth-files` | `files` array of runtime account records with `auth_index`, provider, success, failed and recent requests |
| Pending receipts, explicit capture only | `/v8/management/observability/usage/queue?count=50` | `/v0/management/usage-queue?count=50` | Removes up to 50 records from the shared usage queue |

The API-key usage endpoint does **not** count client access keys. A zero-length result does not prove that an OAuth account received no traffic. Account counters are useful for OAuth coverage but do not establish which account handled a particular client request. The legacy `/v0/management/usage` route is not used: it is absent in both inspected tags, and an installed backend returning 404 is not evidence that enabling usage statistics restores that route.

The usage response is a provider-keyed map whose second-level keys contain upstream secrets. The helper replaces every composite key with an anonymous label. Account responses contain private filenames and emails; those fields are dropped. Known provider IDs use a fixed allowlist. Custom provider names are anonymous. Recent bucket labels accept only `HH:MM-HH:MM`, use backend local time, and contain neither dates nor a timezone.

Primary source:

- [v8 API-key usage handler](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.13/internal/api/handlers/management/api_key_usage.go), also present in v7.2.154.
- [v8 credential response builder](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.13/internal/api/handlers/management/auth_files.go), also contains counters in v7.2.154.
- [v8 management routes](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.13/internal/api/server_management_v8.go) and [v7 legacy routes](https://github.com/router-for-me/CLIProxyAPI/blob/v7.2.154/internal/api/server_management.go).
- [v8 queue handler](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.13/internal/api/handlers/management/usage.go) calls `PopOldest`; this is also true in v7.2.154.
- [v8 queue receipt schema](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.13/internal/redisqueue/plugin.go).
- [Recent bucket schema](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.13/sdk/cliproxy/auth/types.go): 20 buckets of 10 minutes each.

## Explicit queue capture

Automatic snapshots never read the queue. Although its HTTP method is GET, the backend removes records as it returns them. A separate action may call `snapshot(..., consume_queue=True)` only after explaining that other collectors lose access to the consumed events. This is a capture of currently pending receipts, not a request log, a queue peek, a continuous monitor, or a complete history. An empty queue may mean no recent records, disabled statistics, disabled queue publishing, expired retention, or that another consumer already removed them.

The capture returns only the current snapshot, with at most 50 events. `sanitize_events(items, salt=...)` applies the same boundary to records supplied by another authorized consumer without making requests. Client labels come only from a receipt's `api_key`; account labels come only from `auth_index`. No attribution is inferred from account eligibility, source filenames, configured provider keys, or user agents. Request and execution labels preserve only grouping relationships. Model names become anonymous labels because arbitrary model strings can contain private text.

For a returned array, `retained` counts supported displayed receipts, `invalid` counts unsupported records among the newest 50 inspected records, and `omitted` counts older records beyond that inspection limit. These populations are separate: `retained + invalid + omitted` equals the number of records returned by the backend. A capture with some supported receipts remains `available` and reports its invalid count. A nonempty inspected population with no supported receipts reports `unknown`, while an empty array remains `available` with zero receipts.

`capture_requested` records the explicit capture action. `consumed` means the queue request succeeded, even if its response could not be parsed or all receipts were invalid; it does not mean receipts were retained or identified. Failed requests report `consumed: false`. `client_attribution` is `receipt_fields_only` only when at least one retained receipt supplies a valid `api_key` that becomes a `client_label`. Supported account or request fields alone leave it `unavailable`, and one identified receipt does not identify every other receipt.

Outcome, HTTP status, latency, time to first token (TTFT), and allowlisted token counts appear only when the receipt supplies correctly typed fields. A backend zero remains a backend zero; the helper does not prove whether it was measured or a backend default. Missing fields are omitted, not estimated. The inspected receipt schema does not provide a retry count, so the helper does not invent one. Failure bodies, headers, response bodies, prompts, messages, tool data, email addresses, raw access keys, auth filenames, network addresses and arbitrary error text never enter display output.

## Availability and verification

`available` means the response matched the inspected schema, even if the supported population is empty. `unsupported` means both route variants returned 404, 405, or 501. `unavailable` means another request failure, including denied access or a timeout; it does not trigger another route probe. `unknown` means the payload schema could not be recognized. Default queue snapshots report `read_only_unavailable` because reading that endpoint has a side effect. Exception messages and backend error bodies are replaced by fixed messages.

The tests use fake responses, HTTP failures and hostile secret-bearing fields. They check endpoint fallback, missing capabilities, OAuth/account separation, malformed counts, receipt attribution, output bounds, stable labels, exception redaction, and explicit queue capture. Capture regressions distinguish retained, invalid and omitted records; verify that an all-invalid response reports an unknown schema; and keep capture success separate from client attribution. Run:

```sh
python3 -m unittest discover -s tests -p test_diagnostics.py -v
```

These tests verify the local parsing and privacy boundary. They do not prove telemetry publishing or queue retention on an installed backend, and no live inference is needed to run them.
Loading
Loading