meterline-anthropic-admin 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meterline_anthropic_admin-0.1.0/LICENSE +202 -0
- meterline_anthropic_admin-0.1.0/PKG-INFO +263 -0
- meterline_anthropic_admin-0.1.0/README.md +230 -0
- meterline_anthropic_admin-0.1.0/pyproject.toml +41 -0
- meterline_anthropic_admin-0.1.0/setup.cfg +4 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin/__init__.py +18 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin/cli.py +596 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin/client.py +191 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin/fetch.py +196 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin/normalise.py +420 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin/testing.py +288 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin.egg-info/PKG-INFO +263 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin.egg-info/SOURCES.txt +33 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin.egg-info/dependency_links.txt +1 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin.egg-info/entry_points.txt +2 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin.egg-info/requires.txt +10 -0
- meterline_anthropic_admin-0.1.0/src/meterline_anthropic_admin.egg-info/top_level.txt +1 -0
- meterline_anthropic_admin-0.1.0/tests/test_conformance_anthropic_admin.py +36 -0
- meterline_anthropic_admin-0.1.0/tests/test_done_check_A01_A03_client.py +129 -0
- meterline_anthropic_admin-0.1.0/tests/test_done_check_A04_A09_records.py +124 -0
- meterline_anthropic_admin-0.1.0/tests/test_done_check_A10_A11_key_and_final.py +109 -0
- meterline_anthropic_admin-0.1.0/tests/test_done_check_A12_A13_calibration.py +217 -0
- meterline_anthropic_admin-0.1.0/tests/test_done_check_A14_network_guard.py +52 -0
- meterline_anthropic_admin-0.1.0/tests/test_done_check_B6_python_entry_anthropic_admin.py +83 -0
- meterline_anthropic_admin-0.1.0/tests/test_empty_answer_admin.py +67 -0
- meterline_anthropic_admin-0.1.0/tests/test_exit_codes_anthropic_admin.py +80 -0
- meterline_anthropic_admin-0.1.0/tests/test_live_status_admin.py +20 -0
- meterline_anthropic_admin-0.1.0/tests/test_no_float_on_cost_path_admin.py +31 -0
- meterline_anthropic_admin-0.1.0/tests/test_prompt_content_never_written_admin.py +110 -0
- meterline_anthropic_admin-0.1.0/tests/test_raw_written_before_the_rename_admin.py +46 -0
- meterline_anthropic_admin-0.1.0/tests/test_replayed_page_admin.py +84 -0
- meterline_anthropic_admin-0.1.0/tests/test_runs_admin.py +169 -0
- meterline_anthropic_admin-0.1.0/tests/test_shapes_admin.py +75 -0
- meterline_anthropic_admin-0.1.0/tests/test_unknown_fields_admin.py +83 -0
- meterline_anthropic_admin-0.1.0/tests/test_write_phase_admin.py +55 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 Xavier Meyer
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meterline-anthropic-admin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reads the Anthropic Admin Usage and Cost API of a Console organisation and emits meterline records.
|
|
5
|
+
Author: Xavier Meyer
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/meyerx/meterline
|
|
8
|
+
Project-URL: Source, https://github.com/meyerx/meterline/tree/main/packages/meterline-anthropic-admin
|
|
9
|
+
Project-URL: Issues, https://github.com/meyerx/meterline/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/meyerx/meterline/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: ai,llm,usage,cost,tokens,finops,anthropic,claude,admin-api
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: meterline>=0.2
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: parquet
|
|
27
|
+
Requires-Dist: pyarrow>=17; extra == "parquet"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-xdist==3.8.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff==0.17.3; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# meterline-anthropic-admin
|
|
35
|
+
|
|
36
|
+
> Part of [meterline](https://github.com/meyerx/meterline/blob/main/README.md). meterline helps people who run or pay for AI tools in an organisation see and compare usage and cost across providers: it reads each provider's usage API and writes the data in one common record format.
|
|
37
|
+
|
|
38
|
+
Reads the Anthropic Admin Usage and Cost API of a Console organisation and emits [`meterline`](https://github.com/meyerx/meterline/blob/main/packages/meterline/README.md) records. Python 3.10 or later; runtime dependencies `meterline` and `httpx`.
|
|
39
|
+
|
|
40
|
+
**Status:** verified live ([evidence level](https://github.com/meyerx/meterline/blob/main/docs/glossary.md#evidence-levels)) on a small Console organisation (one model, two days of usage; cache tokens, priority tier and several workspaces not yet seen), as of 2026-10-30. It is also tested on a fake API in the shapes of the Anthropic API reference. The live run is described under [Known limits](#known-limits).
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install meterline-anthropic-admin
|
|
44
|
+
pip install "meterline-anthropic-admin[parquet]" # optional: Parquet output
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**From a clone** of the repository (for the tests and development):
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pip install -e packages/meterline -e "packages/meterline-anthropic-admin[dev]"
|
|
51
|
+
pip install -e "packages/meterline-anthropic-admin[parquet]" # optional: Parquet output
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then, with an Admin key in the environment (see "Before you run it"):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
export ANTHROPIC_ADMIN_KEY=... # never commit this
|
|
58
|
+
meterline-anthropic-admin backfill --provider-account-id my-org --since 2026-01-01
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Read this first: which Anthropic connector
|
|
62
|
+
|
|
63
|
+
meterline has two Anthropic connectors, and they read different APIs with different keys.
|
|
64
|
+
|
|
65
|
+
- `meterline-anthropic` reads the Enterprise Analytics API (key `ANTHROPIC_ANALYTICS_KEY`): per-person activity of a Claude Enterprise organisation.
|
|
66
|
+
- `meterline-anthropic-admin` (this package) reads the Admin Usage and Cost API (key `ANTHROPIC_ADMIN_KEY`): API usage and billed cost of a Console organisation, per API key and model.
|
|
67
|
+
|
|
68
|
+
This API sees API-key usage only. Subscription chat and Claude Code subscription usage are not in it.
|
|
69
|
+
|
|
70
|
+
## Provider status
|
|
71
|
+
|
|
72
|
+
As of 2026-10-30, from Anthropic's documentation read on 2026-10-29.
|
|
73
|
+
|
|
74
|
+
- **Both endpoints are legacy.** `GET /v1/organizations/usage_report/messages` and `GET /v1/organizations/cost_report` became legacy on 2026-10-29 (the Usage and Cost API, in Anthropic's release notes). They are still supported, and no sunset date is known. The live run of 2026-10-30 (see Known limits) used them.
|
|
75
|
+
- **The successors.** `GET /v1/organizations/analytics/usage` and `GET /v1/organizations/analytics/cost` (beta; they need the header `anthropic-beta: analytics-usage-cost-2026-08-24`). This connector does not call them: it stays on the legacy endpoints in the current releases. The new endpoints answer in another shape (rows with a `group` object and a `usage` or `cost` object, `time_bucket_width` instead of `bucket_width`), so the move needs new shape files and a new normaliser, not only a new header. Follow-up: the move to the new endpoints is tracked separately.
|
|
76
|
+
- **The version header.** The connector sends `anthropic-version: 2023-06-01`, as Anthropic's guide does, and the live run used it. Anthropic's reference examples show `2026-10-21`. **Unverified:** whether the two versions answer differently on these endpoints.
|
|
77
|
+
|
|
78
|
+
## Before you run it
|
|
79
|
+
|
|
80
|
+
Check these four things first:
|
|
81
|
+
|
|
82
|
+
- **An organisation.** The Admin API is unavailable for individual accounts. The Console account must be set up as an organisation.
|
|
83
|
+
- **An Admin key.** A key of the form `sk-ant-admin...`, created in the Console by an admin of the organisation. It can do much more than read reports: read [What the Admin key can do](#what-the-admin-key-can-do) before you create one. It is read from the environment variable `ANTHROPIC_ADMIN_KEY` in `run` and nowhere else. It is never read from a keychain. It is sent only as the `x-api-key` header and is never written to disk, a log or a manifest.
|
|
84
|
+
- **An organisation label.** `--provider-account-id` is required for `backfill` and `daily`: any non-empty label you choose. It becomes `provider_account_id`. The Anthropic organisation id is not needed and is not fetched.
|
|
85
|
+
- **A private data directory.** Records name API keys and show your usage. The default `exports/meterline-anthropic-admin` is gitignored. Never commit real records.
|
|
86
|
+
|
|
87
|
+
A wrapper that loads the key without echoing it keeps it out of the shell history. On macOS: `ANTHROPIC_ADMIN_KEY="$(security find-generic-password -s <service> -w)" meterline-anthropic-admin daily --provider-account-id <label>`. Run it with no `set -x` and no `env` dump.
|
|
88
|
+
|
|
89
|
+
## What the Admin key can do
|
|
90
|
+
|
|
91
|
+
An Admin key is not a read-only key, and it cannot be narrowed. The Console gives an Admin key no scopes: it has full access to every endpoint that accepts an Admin key. Besides the two reports this connector reads, the Admin API lets such a key manage members and roles, invites, workspaces and workspace members, and API keys (as the Anthropic documentation said on 2026-10-29). Treat the key like an administrator's password.
|
|
92
|
+
|
|
93
|
+
- **Who creates it.** Only a member of the Console organisation with the admin role, on the Admin keys page of the Console (`platform.claude.com/settings/admin-keys`). The key is shown once. Individual accounts have no Admin API.
|
|
94
|
+
- **Which keys work.** An Admin key, of the form `sk-ant-admin...`. The Anthropic documentation says a workspace API key does not work. The Anthropic documentation names other credentials that can call these endpoints (an OAuth token with the `org:admin` scope, or a personal or service-account key that is not tied to a workspace). meterline is built for the Admin key and has not been tried with them.
|
|
95
|
+
- **Make a dedicated key.** Create one key for meterline, name it so, give it an expiry date when the Console asks, and do not reuse it anywhere else: not in another script and not in another tool. A key that does one job can be revoked without breaking anything else, and its expiry limits the harm of a leak that nobody notices.
|
|
96
|
+
- **Keep it out of reach.** The environment of the process that runs meterline, or a secret store that puts it there. Not git, not a job file, not the shell history, not the data directory. meterline never writes the key, so rotating it changes nothing in your data.
|
|
97
|
+
- **Rotate it.** Before the expiry date, and whenever someone who had it leaves: create a new Admin key, put it in the environment, run `meterline-anthropic-admin daily` once and check that it exits 0, then revoke the old key in the Console. The documentation says an expired, revoked or malformed key gets a 401; the run then exits 3.
|
|
98
|
+
- **Revoke it at once** if it may have leaked (a log, a chat, a commit, a shared screen): revoke it in the Console first, then make a new one as above. Then look at the organisation's members, invites, workspaces and API keys for changes you did not make, because the key could change them.
|
|
99
|
+
|
|
100
|
+
**What meterline calls.** Only these two requests, both `GET`, both to `https://api.anthropic.com`, both with the key in the `x-api-key` header. It sends nothing else, writes nothing to your organisation and never calls the endpoints that manage members, invites, workspaces or keys:
|
|
101
|
+
|
|
102
|
+
- `GET /v1/organizations/usage_report/messages`
|
|
103
|
+
- `GET /v1/organizations/cost_report`
|
|
104
|
+
|
|
105
|
+
## Commands
|
|
106
|
+
|
|
107
|
+
All commands take `--data-dir` (default `exports/meterline-anthropic-admin`) and `--parquet`.
|
|
108
|
+
|
|
109
|
+
| Command | What it does |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `backfill --since YYYY-MM-DD` | Walks 31-day windows back from yesterday to the start date. Resumes after a failure by re-running the same command. |
|
|
112
|
+
| `daily` | Reads every closed UTC day since the last day loaded, the three days before today, and every day whose latest record is not yet final. With no `state.json` it stops (exit 2) and asks for `--since YYYY-MM-DD`; `--since` is ignored once a state file exists. Safe to run twice a day: unchanged data publishes nothing. |
|
|
113
|
+
| `rebuild` | Re-creates every record from the raw store and the published run manifests. No API calls, no key. |
|
|
114
|
+
|
|
115
|
+
Options of `backfill` and `daily`: `--provider-account-id` (required), `--workspace-id` (optional filter), `--rate-per-minute` (default 20; the API asks for about one poll a minute, and bursts for paging are fine), `--max-retry-after`.
|
|
116
|
+
|
|
117
|
+
The state file `state.json` is checked on every `backfill` and `daily`. If it is cut short, empty, not the shape the program writes or points outside the data directory, the run stops with exit 2 before any request and names the file. Restore it from a backup, or move it aside and run `backfill --since`: published records are kept, and unchanged rows are not written twice. `rebuild` does not read it.
|
|
118
|
+
|
|
119
|
+
Exit codes, which a run also reports as `failure_kind` (`RunResult.failure_kind` from Python; a manifest written by a run that failed a check carries it too):
|
|
120
|
+
|
|
121
|
+
| Code | `failure_kind` | Meaning | What to do |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| `0` | `none` | The run finished. | Nothing. |
|
|
124
|
+
| `1` | `transient` | A 5xx, a 429 after the retries, a timeout or a connection error. | Run the same command again later; progress is kept. |
|
|
125
|
+
| `2` | `input` | A failed check, a response that lacks a field the connector needs or holds one of the wrong type, bad arguments or a configuration error such as a missing key. | Fix the input; running the same command again fails the same way. |
|
|
126
|
+
| `3` | `permanent` | The provider refused the request: 400, 401, 403, 404 or 422. The message starts "permanent:" and `RunResult.http_status` holds the status. | Fix the key, its scope or the identifier the message names; do not retry unchanged. |
|
|
127
|
+
|
|
128
|
+
To run this connector every day, with a retry rule and a gold build after it, see [the daily job example](https://github.com/meyerx/meterline/blob/main/examples/03-daily-job/README.md).
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
A run has three parts:
|
|
133
|
+
|
|
134
|
+
1. **Fetcher** (`client.py`, `fetch.py`). Per window of at most 31 days, two reports. `GET /v1/organizations/usage_report/messages` grouped by `api_key_id`, `workspace_id`, `model` and `service_tier` (arrays sent as `group_by[]`), and `GET /v1/organizations/cost_report` grouped by `description` and `workspace_id`. Both use one-day buckets and follow `next_page` while `has_more` is true. Every response page is stored as received under `raw/<endpoint>/<window>/<pulled_at>.json.gz`.
|
|
135
|
+
2. **Normaliser** (`normalise.py`). Reads raw files only. It checks every field against the documented shape. A field it does not know goes under `unknown_fields` in the run manifest, with one warning on stderr, and the run goes on. A field it needs and does not find, or one of the wrong type, stops the run. Then it maps rows to records and checks the batch before anything is written.
|
|
136
|
+
3. **Publisher** (`cli.py`). Writes only the records that differ from the latest published record of the same key. A later batch wins (the silver revision rule).
|
|
137
|
+
|
|
138
|
+
The `--workspace-id` filter is sent with the usage query. The cost report has no workspace filter, so the normaliser applies it to the cost rows by their `workspace_id`.
|
|
139
|
+
|
|
140
|
+
## Files under the data directory
|
|
141
|
+
|
|
142
|
+
A run reads and writes these four things under `--data-dir`:
|
|
143
|
+
|
|
144
|
+
| Path | Contents |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `raw/<endpoint>/<window>/<pulled_at>.json.gz` | One file per report and window pull, with every page's status and body text, the organisation label and the workspace filter. Endpoints: `usage_report_messages`, `cost_report`. |
|
|
147
|
+
| `runs/<batch_id>.json` | The run manifest: `command`, `batch_id`, `status`, `windows`, `pages`, `row_counts`, `totals` (figures per day and model), `raw_files`, `published_counts`, `failures` and `unknown_fields` (the fields of the provider that the connector does not know, as endpoint, key path and count, never a value). |
|
|
148
|
+
| `records/<kind>/<batch_id>.jsonl` | Published records (v2); kinds are `usage` and `org_totals`. A batch with no changed records writes no file. |
|
|
149
|
+
| `state.json` | `days_loaded`, and a `pending` block that lets an interrupted run resume. |
|
|
150
|
+
|
|
151
|
+
## Record mapping
|
|
152
|
+
|
|
153
|
+
Two kinds of record come from the two reports:
|
|
154
|
+
|
|
155
|
+
| Record | From | Notes |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `UsageRecord`, `api_key` | usage rows, per day, key and model | `actor_id` is the `api_key_id`. `product` `api`, `grain` `day`, `scope` `""`, `provider` `anthropic`, `cost` null (the API does not report cost per key). |
|
|
158
|
+
| `UsageRecord`, `unassigned` | usage rows whose `api_key_id` is null (Console playground use) | `actor_id` is `(unassigned)`; the format requires that value. |
|
|
159
|
+
| `OrgTotal`, `product` `api` | the same usage rows summed per day and model, with the token cost rows of the cost report | `cost` is the sum of `amount` divided by 100, `currency` `USD`, `cost_basis` `billed`. A model with usage and no cost row has a null cost. |
|
|
160
|
+
| `OrgTotal`, `api-web-search` or `api-code-execution` | the cost rows of that `cost_type` | `model` `""`, tokens null. |
|
|
161
|
+
|
|
162
|
+
Fields: `tokens_input_uncached` = `uncached_input_tokens`; `tokens_cache_read` = `cache_read_input_tokens`; `tokens_cache_write_5m` and `tokens_cache_write_1h` = the two `cache_creation` buckets; `tokens_cache_write` = their sum; `tokens_input_total` = uncached + read + write; `tokens_output` = `output_tokens`. `requests`, `tokens_reasoning` and `cost_list` are null. `actor_email`, `actor_login`, `actor_deleted` and `model_provider` are null; `cost_only` is false.
|
|
163
|
+
|
|
164
|
+
`extras` of a usage record: `web_search_requests` when the row reports `server_tool_use`, and `priority_tokens` (all token buckets of the rows with `service_tier` `priority`) when it is above zero. Priority Tier is in the usage report but has no line in the cost report, so its tokens are named here and not priced.
|
|
165
|
+
|
|
166
|
+
The cost report sends `amount` as a decimal string in cents with sub-cent digits. The connector keeps every digit: `"123.78912"` becomes `Decimal("1.2378912")` dollars. A day's `is_final` is true when its bucket ended at least 48 hours before `pulled_at`.
|
|
167
|
+
|
|
168
|
+
## Response shapes
|
|
169
|
+
|
|
170
|
+
The shapes are in [`docs/api-shapes/anthropic-admin/`](https://github.com/meyerx/meterline/blob/main/docs/api-shapes/anthropic-admin/README.md), transcribed from the Anthropic API reference on 2026-10-17. The API version header is `anthropic-version: 2023-06-01`. For a new API version, read the changelog, update the shape files, and re-run the shape check before trusting a run.
|
|
171
|
+
|
|
172
|
+
What the documentation leaves open is learned from the first live response: which fields are present when a query does not group by them, which can be null, and how the cost is rounded. A field it does not know goes under `unknown_fields` in the run manifest and in one warning on stderr, and the run goes on. A needed field that is missing or of the wrong type stops the run with exit 2, so nothing wrong is published.
|
|
173
|
+
|
|
174
|
+
**When a live response differs:**
|
|
175
|
+
|
|
176
|
+
1. Run `backfill` for one day, then read the failed manifest under `runs/`. It names the endpoint and the field, never a value.
|
|
177
|
+
2. Copy the structure of the raw file's page into `docs/api-shapes/anthropic-admin/`, and replace every value with an invented one: ids, models, amounts. The shape files are synthetic and never carry real data. Never commit a real response.
|
|
178
|
+
3. Change `testing.py` and `normalise.py` to match, run `python -m pytest -q packages/meterline-anthropic-admin`, and note the difference in this package's change list under `docs/dev/`.
|
|
179
|
+
|
|
180
|
+
## Known limits
|
|
181
|
+
|
|
182
|
+
The limits of this connector, grouped by kind.
|
|
183
|
+
|
|
184
|
+
**Live run.** On 2026-10-30 a backfill and a daily run against a small Console organisation both exited 0, published, and reported no field the connector did not know. The billed cost per day in the records equalled the Console's Cost page for the same days, at the Console's two-decimal display. That organisation had one model and two days of usage, so cache tokens, the priority tier and several workspaces were not exercised. This paragraph is the record that level 3 of the [evidence levels](https://github.com/meyerx/meterline/blob/main/docs/glossary.md#evidence-levels) asks for.
|
|
185
|
+
|
|
186
|
+
**What the API shows.** The records carry these limits of the source:
|
|
187
|
+
|
|
188
|
+
- **API-key usage only.** Subscription chat and Claude Code subscription usage are not in this API. Claude Code analytics is a separate endpoint and out of scope.
|
|
189
|
+
- **Dimensions summed away.** Context window, inference geography and speed are summed in the query and are not split in the records. The service tier is split only to name Priority Tier tokens.
|
|
190
|
+
- **Cost per model and day, not per key.** The cost report has no key dimension, so a usage record has no cost. The sum of the model's cost is on the `OrgTotal`.
|
|
191
|
+
- **Gold's `ref_vs_source_gap` is blank for this connector.** Gold's `reconciliation_month` computes `ref_vs_source_gap` as the reference cost minus `actor_cost_source`, the cost the usage records carry. Admin usage records carry none, so `actor_cost_source` is empty and so are `ref_vs_source_gap` and `ref_vs_source_share`. The billed cost of the organisation stays in `org_cost_source`. Gold does not compare its reference cost with that billed cost.
|
|
192
|
+
- **Freshness.** Data is usually available within minutes and sometimes later. Cost is daily, so compare a day against the Console only after it has closed.
|
|
193
|
+
- **Priority Tier has no cost line.** Its tokens appear in `priority_tokens` and are not priced.
|
|
194
|
+
|
|
195
|
+
**How the connector reads it.** These follow from the connector's rules:
|
|
196
|
+
|
|
197
|
+
- **A key in two workspaces fails the batch.** The format has one record per day, key and model. If a named key shows up in two workspaces on one day, the run stops with exit 2 and names the key, instead of adding or dropping usage.
|
|
198
|
+
- **The console (`unassigned`) record sums workspaces.** Rows with no key are summed across workspaces, because there is no key to confuse.
|
|
199
|
+
- **Raw files are kept and never replaced.** A pull in the same UTC second as a finished run is refused (exit 2). Wait a second and run again.
|
|
200
|
+
- **A resumed run reuses the pages it already has.** After a failure, the same command picks up the pages the failed run stored (the `pending` block of `state.json`) and publishes them under the new run's `pulled_at`.
|
|
201
|
+
- **Finality.** A day is final from 48 hours after its bucket ended. `daily` reads again the three days before today and every day published as not final, so a gap between runs does not leave a day open.
|
|
202
|
+
- **Waits are capped.** A `Retry-After` header is honoured up to 300 seconds (`--max-retry-after`). One query follows at most 10,000 pages.
|
|
203
|
+
|
|
204
|
+
**Personal data.** Key ids and usage appear in raw files and records. `api_key_id` is the record's `actor_id` and can identify a person. `workspace_id`, `account_id`, `service_account_id` and the cost `description` stay in raw files and never reach a record. Logs and manifests carry counts, statuses and totals, with two exceptions. The unknown-field report names a field the provider sent, never its value, and a key seen in two workspaces on one day is named by its id. The first is kept in the `unknown_fields` and the second in the `failures` of `runs/<batch_id>.json`. [`docs/PRIVACY.md`](https://github.com/meyerx/meterline/blob/main/docs/PRIVACY.md#error-messages-that-name-an-identifier) lists them.
|
|
205
|
+
|
|
206
|
+
## From Python
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from meterline_anthropic_admin import run
|
|
210
|
+
|
|
211
|
+
result = run("backfill", since="2026-01-01", provider_account_id="my-org", data_dir="exports/admin",
|
|
212
|
+
credentials={"ANTHROPIC_ADMIN_KEY": my_key}) # my_key: from your own secret store
|
|
213
|
+
print(result.exit_code, result.batch_id, result.records)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`run(command, *, data_dir, credentials=None, transport=None, **options)` does what the command line does, and `command` is `backfill`, `daily` or `rebuild`. The options are those of the command under their Python names (`since`, `provider_account_id`, `workspace_id`, `rate_per_minute`, `max_retry_after`, `parquet`); dates are `datetime.date` or `YYYY-MM-DD` text, and an unknown name raises `TypeError`. `credentials` is a mapping from `ANTHROPIC_ADMIN_KEY` to the key, or a function that returns one; when it is given, the environment is not read for the key. With `None`, the key comes from the environment as on the command line. The key goes only to the HTTP client.
|
|
217
|
+
|
|
218
|
+
It returns a `RunResult` (from `meterline`): `exit_code`, `ok`, `batch_id`, `manifest_path`, `records` (count per kind), `windows`, `skipped`, `failure_kind` (`none`, `transient`, `input` or `permanent`), `http_status` and `unknown_fields` (the same list as the manifest's). A failed run does not raise: it prints the same message as the command line and returns the exit code the command line would exit with (see the exit-code table under Commands).
|
|
219
|
+
|
|
220
|
+
## Testing your integration without a key
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from datetime import datetime, timezone
|
|
224
|
+
from meterline_anthropic_admin import run
|
|
225
|
+
from meterline_anthropic_admin.testing import FakeAPI
|
|
226
|
+
|
|
227
|
+
fake = FakeAPI() # serves a small synthetic dataset, 2026-09-01 to 2026-09-14
|
|
228
|
+
result = run("backfill", since="2026-09-01", provider_account_id="my-org", data_dir="demo-data",
|
|
229
|
+
credentials={"ANTHROPIC_ADMIN_KEY": fake.api_key}, transport=fake.transport,
|
|
230
|
+
now=lambda: datetime(2026, 9, 15, 6, 0, tzinfo=timezone.utc))
|
|
231
|
+
assert result.ok
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`FakeAPI` answers the usage and cost endpoints in the documented shapes on an `httpx.MockTransport`; pass your own rows (`FakeAPI(usage_rows, cost_rows)`) or script faults (`fake.fail(...)`). Everything it serves is synthetic. It proves that your code drives the connector and reads its records. It does not prove live authentication, the live response shapes or rate limits.
|
|
235
|
+
|
|
236
|
+
## Public interface
|
|
237
|
+
|
|
238
|
+
What later releases keep working, under the rules of the compatibility policy in [`docs/records.md`](https://github.com/meyerx/meterline/blob/main/docs/records.md#compatibility):
|
|
239
|
+
|
|
240
|
+
- the `meterline-anthropic-admin` command line: its commands, options and exit codes, as listed under Commands;
|
|
241
|
+
- the records it publishes under `records/`, in the v2 format of [`docs/records.md`](https://github.com/meyerx/meterline/blob/main/docs/records.md);
|
|
242
|
+
- from Python, the names in `meterline_anthropic_admin.__all__`: `run` and `RunResult` (see From Python), the error classes `ApiError` and `PaginationError`, and `__version__`;
|
|
243
|
+
- the `meterline_anthropic_admin.testing` module: `FakeAPI` and `sample_dataset`, as described under Testing your integration without a key.
|
|
244
|
+
|
|
245
|
+
Everything else is internal and may change in any release: every other module of the `meterline_anthropic_admin` package, and the layout of the raw files, the run manifests and `state.json`.
|
|
246
|
+
|
|
247
|
+
## Tests
|
|
248
|
+
|
|
249
|
+
```sh
|
|
250
|
+
python -m pytest -q packages/meterline-anthropic-admin # everything
|
|
251
|
+
python -m pytest -q packages/meterline-anthropic-admin -k done_check # the acceptance checks A01 to A14
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Tests run against a fake API built on `httpx.MockTransport` that serves synthetic data in the documented shapes and can script faults. The clock and sleep are injected, so nothing really waits. The network guard in `packages/conftest.py` fails any test that opens a connection to a non-loopback address.
|
|
255
|
+
|
|
256
|
+
## Calibration: a known spend, then a check
|
|
257
|
+
|
|
258
|
+
`tools/calibrate_anthropic_admin.py` spends about 1.20 USD of API credit on calls whose usage it records exactly, and then checks that the connector finds the same numbers. It spends real money, so the owner runs it; the tests never do.
|
|
259
|
+
|
|
260
|
+
1. Create a workspace with its own spend limit (for example 5 USD), and a standard API key in it. The default workspace cannot carry a limit; the limit is the hard stop.
|
|
261
|
+
2. Before 20:00 UTC, run `ANTHROPIC_API_KEY=... python tools/calibrate_anthropic_admin.py`. It makes the planned calls on three models, stops before a call that could take the total past 2.00 USD, and writes `calibration-<date>.jsonl` with each response's own `usage`.
|
|
262
|
+
3. An hour after the last call, run the connector for that one day: `meterline-anthropic-admin backfill --provider-account-id <label> --workspace-id <workspace> --since <date>`.
|
|
263
|
+
4. Run `python tools/calibrate_anthropic_admin.py --compare --calibration <file> --data-dir <dir> --day <date>`. Tokens must match exactly per model and bucket; cost must agree within 0.01 USD per model. The output is figures only.
|