cost-per-task 0.4.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.
- cost_per_task-0.4.0/.github/workflows/ci.yml +23 -0
- cost_per_task-0.4.0/.github/workflows/publish-pypi.yml +183 -0
- cost_per_task-0.4.0/.gitignore +14 -0
- cost_per_task-0.4.0/CHANGELOG.md +71 -0
- cost_per_task-0.4.0/CLAUDE.md +50 -0
- cost_per_task-0.4.0/LICENSE +21 -0
- cost_per_task-0.4.0/PKG-INFO +415 -0
- cost_per_task-0.4.0/README.md +392 -0
- cost_per_task-0.4.0/docs/testing-guide.md +246 -0
- cost_per_task-0.4.0/examples/ask.py +85 -0
- cost_per_task-0.4.0/examples/level1.ps1 +52 -0
- cost_per_task-0.4.0/examples/level2/csvtotal/TASK.md +1 -0
- cost_per_task-0.4.0/examples/level2/csvtotal/spend.csv +7 -0
- cost_per_task-0.4.0/examples/level2/csvtotal/spend.py +4 -0
- cost_per_task-0.4.0/examples/level2/csvtotal/test_spend.py +16 -0
- cost_per_task-0.4.0/examples/level2/datefix/TASK.md +1 -0
- cost_per_task-0.4.0/examples/level2/datefix/invoices.py +12 -0
- cost_per_task-0.4.0/examples/level2/datefix/test_invoices.py +17 -0
- cost_per_task-0.4.0/examples/level2/fizzbuzz/TASK.md +1 -0
- cost_per_task-0.4.0/examples/level2/fizzbuzz/fizzbuzz.py +7 -0
- cost_per_task-0.4.0/examples/level2/fizzbuzz/test_fizzbuzz.py +16 -0
- cost_per_task-0.4.0/examples/level2.ps1 +64 -0
- cost_per_task-0.4.0/prices/anthropic.json +47 -0
- cost_per_task-0.4.0/prices/example.json +15 -0
- cost_per_task-0.4.0/prices/openai.json +39 -0
- cost_per_task-0.4.0/pyproject.toml +44 -0
- cost_per_task-0.4.0/src/cost_per_task/__init__.py +3 -0
- cost_per_task-0.4.0/src/cost_per_task/analysis.py +65 -0
- cost_per_task-0.4.0/src/cost_per_task/cli.py +406 -0
- cost_per_task-0.4.0/src/cost_per_task/importers/__init__.py +10 -0
- cost_per_task-0.4.0/src/cost_per_task/importers/common.py +183 -0
- cost_per_task-0.4.0/src/cost_per_task/importers/langfuse.py +106 -0
- cost_per_task-0.4.0/src/cost_per_task/importers/litellm.py +80 -0
- cost_per_task-0.4.0/src/cost_per_task/labels.py +86 -0
- cost_per_task-0.4.0/src/cost_per_task/mcp_server.py +153 -0
- cost_per_task-0.4.0/src/cost_per_task/metrics.py +314 -0
- cost_per_task-0.4.0/src/cost_per_task/prices_hub.py +181 -0
- cost_per_task-0.4.0/src/cost_per_task/pricing.py +167 -0
- cost_per_task-0.4.0/src/cost_per_task/providers/__init__.py +0 -0
- cost_per_task-0.4.0/src/cost_per_task/providers/anthropic.py +87 -0
- cost_per_task-0.4.0/src/cost_per_task/providers/base.py +41 -0
- cost_per_task-0.4.0/src/cost_per_task/providers/openai.py +145 -0
- cost_per_task-0.4.0/src/cost_per_task/proxy.py +388 -0
- cost_per_task-0.4.0/src/cost_per_task/report.py +287 -0
- cost_per_task-0.4.0/src/cost_per_task/schema.py +75 -0
- cost_per_task-0.4.0/src/cost_per_task/stats.py +97 -0
- cost_per_task-0.4.0/tests/conftest.py +233 -0
- cost_per_task-0.4.0/tests/fixtures/hub_sample.json +13 -0
- cost_per_task-0.4.0/tests/test_cli_attempts.py +25 -0
- cost_per_task-0.4.0/tests/test_importers.py +120 -0
- cost_per_task-0.4.0/tests/test_json_and_mcp.py +128 -0
- cost_per_task-0.4.0/tests/test_labels.py +51 -0
- cost_per_task-0.4.0/tests/test_metrics.py +176 -0
- cost_per_task-0.4.0/tests/test_openai_provider.py +81 -0
- cost_per_task-0.4.0/tests/test_prices_hub.py +100 -0
- cost_per_task-0.4.0/tests/test_pricing.py +138 -0
- cost_per_task-0.4.0/tests/test_proxy.py +313 -0
- cost_per_task-0.4.0/tests/test_schema.py +38 -0
- cost_per_task-0.4.0/tests/test_stats.py +69 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, windows-latest]
|
|
14
|
+
python: ["3.11", "3.12", "3.13"]
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python }}
|
|
21
|
+
- run: python -m pip install --upgrade pip
|
|
22
|
+
- run: pip install -e . pytest
|
|
23
|
+
- run: pytest -q
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
name: Publish cost-per-task to PyPI
|
|
2
|
+
|
|
3
|
+
# Release flow, mirroring OptimNow/cloud-finops-skills:
|
|
4
|
+
# 1. bump the version in pyproject.toml and src/cost_per_task/__init__.py,
|
|
5
|
+
# update CHANGELOG.md, merge to main
|
|
6
|
+
# 2. git tag vX.Y.Z && git push origin vX.Y.Z
|
|
7
|
+
# 3. this workflow tests the tagged commit, builds sdist + wheel with the
|
|
8
|
+
# version pinned from the tag, publishes via PyPI trusted publishing,
|
|
9
|
+
# then creates the GitHub Release with generated notes.
|
|
10
|
+
#
|
|
11
|
+
# One-time setup (repository owner):
|
|
12
|
+
# - PyPI: project "cost-per-task", trusted publisher = GitHub, owner
|
|
13
|
+
# OptimNow, repository cost-per-task, workflow publish-pypi.yml,
|
|
14
|
+
# environment pypi. Register it as a "pending publisher" before the
|
|
15
|
+
# first release so the project name is claimed by this workflow.
|
|
16
|
+
# - GitHub: Settings -> Environments -> create "pypi" (optionally with a
|
|
17
|
+
# required reviewer, which turns every publish into an approval click).
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
tags:
|
|
22
|
+
- 'v*'
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
inputs:
|
|
25
|
+
tag:
|
|
26
|
+
required: true
|
|
27
|
+
type: string
|
|
28
|
+
description: 'Existing tag to (re)publish, e.g. v0.4.0'
|
|
29
|
+
|
|
30
|
+
# Read-only by default; id-token and contents:write are granted per job below,
|
|
31
|
+
# so the build job (which executes the build backend) never holds a token
|
|
32
|
+
# that can authenticate to PyPI.
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
resolve-tag:
|
|
38
|
+
name: Resolve and validate the tag
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
outputs:
|
|
41
|
+
ref: ${{ steps.resolve.outputs.ref }}
|
|
42
|
+
version: ${{ steps.resolve.outputs.version }}
|
|
43
|
+
steps:
|
|
44
|
+
- name: Resolve tag
|
|
45
|
+
id: resolve
|
|
46
|
+
env:
|
|
47
|
+
INPUT_TAG: ${{ inputs.tag }}
|
|
48
|
+
FALLBACK_REF: ${{ github.ref }}
|
|
49
|
+
GH_TOKEN: ${{ github.token }}
|
|
50
|
+
run: |
|
|
51
|
+
set -euo pipefail
|
|
52
|
+
if [[ -z "${INPUT_TAG}" ]]; then
|
|
53
|
+
tag="${FALLBACK_REF##*/}"
|
|
54
|
+
else
|
|
55
|
+
tag="${INPUT_TAG#V}"
|
|
56
|
+
tag="${tag#v}"
|
|
57
|
+
tag="v${tag}"
|
|
58
|
+
if [[ "$tag" != "${INPUT_TAG}" ]]; then
|
|
59
|
+
echo "::notice::normalised tag input '${INPUT_TAG}' to '${tag}'"
|
|
60
|
+
fi
|
|
61
|
+
# Works for private repositories too, unlike an anonymous ls-remote.
|
|
62
|
+
if ! gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" >/dev/null 2>&1; then
|
|
63
|
+
echo "::error::tag ${tag} does not exist in ${GITHUB_REPOSITORY}; refs are case sensitive"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
version="${tag#v}"
|
|
68
|
+
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-z0-9.]*)$ ]]; then
|
|
69
|
+
echo "::error::tag ${tag} is not a semantic version"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
echo "ref=${tag}" >> "$GITHUB_OUTPUT"
|
|
73
|
+
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
|
74
|
+
echo "Releasing cost-per-task ${version} from tag ${tag}"
|
|
75
|
+
|
|
76
|
+
test:
|
|
77
|
+
name: Test the tagged commit
|
|
78
|
+
needs: resolve-tag
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
82
|
+
with:
|
|
83
|
+
ref: ${{ needs.resolve-tag.outputs.ref }}
|
|
84
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
85
|
+
with:
|
|
86
|
+
python-version: '3.12'
|
|
87
|
+
- run: python -m pip install --upgrade pip
|
|
88
|
+
- run: pip install -e . pytest
|
|
89
|
+
- run: pytest -q
|
|
90
|
+
|
|
91
|
+
build:
|
|
92
|
+
name: Build sdist + wheel
|
|
93
|
+
needs: [resolve-tag, test]
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
97
|
+
with:
|
|
98
|
+
ref: ${{ needs.resolve-tag.outputs.ref }}
|
|
99
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
100
|
+
with:
|
|
101
|
+
python-version: '3.12'
|
|
102
|
+
|
|
103
|
+
- name: Pin the package version to the tag
|
|
104
|
+
env:
|
|
105
|
+
VERSION: ${{ needs.resolve-tag.outputs.version }}
|
|
106
|
+
run: |
|
|
107
|
+
python - <<'PY'
|
|
108
|
+
import os, pathlib, re
|
|
109
|
+
version = os.environ["VERSION"]
|
|
110
|
+
for path, pattern, replacement in (
|
|
111
|
+
("pyproject.toml", r'^version = ".*"', f'version = "{version}"'),
|
|
112
|
+
("src/cost_per_task/__init__.py", r'^__version__ = ".*"', f'__version__ = "{version}"'),
|
|
113
|
+
):
|
|
114
|
+
file = pathlib.Path(path)
|
|
115
|
+
text = file.read_text(encoding="utf-8")
|
|
116
|
+
new, count = re.subn(pattern, replacement, text, count=1, flags=re.M)
|
|
117
|
+
if count != 1:
|
|
118
|
+
raise SystemExit(f"{path}: version line not found")
|
|
119
|
+
if new != text:
|
|
120
|
+
print(f"::warning::{path} said a different version than the tag; pinned to {version}")
|
|
121
|
+
file.write_text(new, encoding="utf-8")
|
|
122
|
+
print(f"version pinned to {version}")
|
|
123
|
+
PY
|
|
124
|
+
|
|
125
|
+
- name: Build
|
|
126
|
+
run: |
|
|
127
|
+
python -m pip install --upgrade pip build
|
|
128
|
+
python -m build
|
|
129
|
+
ls -lh dist/
|
|
130
|
+
|
|
131
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
132
|
+
with:
|
|
133
|
+
name: pypi-dist
|
|
134
|
+
path: dist/
|
|
135
|
+
retention-days: 7
|
|
136
|
+
|
|
137
|
+
publish:
|
|
138
|
+
name: Publish to PyPI
|
|
139
|
+
needs: build
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
permissions:
|
|
142
|
+
contents: read
|
|
143
|
+
id-token: write # PyPI trusted publishing (OIDC)
|
|
144
|
+
environment:
|
|
145
|
+
name: pypi
|
|
146
|
+
url: https://pypi.org/p/cost-per-task
|
|
147
|
+
steps:
|
|
148
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
149
|
+
with:
|
|
150
|
+
name: pypi-dist
|
|
151
|
+
path: dist/
|
|
152
|
+
- name: Publish via trusted publishing
|
|
153
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
154
|
+
with:
|
|
155
|
+
packages-dir: dist/
|
|
156
|
+
|
|
157
|
+
github-release:
|
|
158
|
+
name: Create the GitHub Release
|
|
159
|
+
needs: [resolve-tag, publish]
|
|
160
|
+
runs-on: ubuntu-latest
|
|
161
|
+
permissions:
|
|
162
|
+
contents: write
|
|
163
|
+
steps:
|
|
164
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
165
|
+
with:
|
|
166
|
+
name: pypi-dist
|
|
167
|
+
path: dist/
|
|
168
|
+
- uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
169
|
+
with:
|
|
170
|
+
name: ${{ needs.resolve-tag.outputs.ref }}
|
|
171
|
+
tag_name: ${{ needs.resolve-tag.outputs.ref }}
|
|
172
|
+
files: dist/*
|
|
173
|
+
generate_release_notes: true
|
|
174
|
+
body: |
|
|
175
|
+
## cost-per-task ${{ needs.resolve-tag.outputs.ref }}
|
|
176
|
+
|
|
177
|
+
Published to PyPI: https://pypi.org/project/cost-per-task/${{ needs.resolve-tag.outputs.version }}/
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
pip install cost-per-task==${{ needs.resolve-tag.outputs.version }}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
See CHANGELOG.md for the curated notes; the list below is generated from merged pull requests.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to cost-per-task. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semantic
|
|
5
|
+
versioning, with the minor digit bumped for any user-visible feature.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
- Release pipeline: tag `vX.Y.Z` on `main` to test, build, publish to PyPI via
|
|
10
|
+
trusted publishing and create the GitHub Release.
|
|
11
|
+
|
|
12
|
+
## [0.4.0] - 2026-09-02
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- OpenRouter and other OpenAI-compatible gateways as the `openai` upstream, with a
|
|
16
|
+
path prefix (`--openai-upstream https://openrouter.ai/api`).
|
|
17
|
+
- OpenRouter usage accounting requested automatically; the cost the gateway
|
|
18
|
+
charged is stored as `reported_cost` and reconciled against the table price in
|
|
19
|
+
the report and the disclosure checklist.
|
|
20
|
+
- Gateway model ids (`anthropic/claude-haiku-4.5`) match pricing tables without the
|
|
21
|
+
vendor prefix and with dots as hyphens.
|
|
22
|
+
- `--prices` may be repeated to merge vendor tables; the merged `as_of` is the oldest.
|
|
23
|
+
- The log's `provider` field names the gateway host when a call did not go to the
|
|
24
|
+
vendor directly.
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-09-02
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `cpt import langfuse` and `cpt import litellm`: convert usage exports (CSV, JSON,
|
|
30
|
+
JSONL) into cpt records with configurable task and attempt fields.
|
|
31
|
+
- `cpt prices refresh`: diff a pricing table against the OptimNow AI Pricing Hub
|
|
32
|
+
catalogue; write only with `--write`; anomalous cache-read prices are flagged and
|
|
33
|
+
models the hub cannot price fully are skipped.
|
|
34
|
+
- `cpt report --json` and `cpt compare --json`.
|
|
35
|
+
- `cpt mcp`: MCP server with `cpt_report`, `cpt_compare` and `cpt_risk_denominator`,
|
|
36
|
+
via the optional `[mcp]` extra (the only optional dependency).
|
|
37
|
+
- `prices/openai.json` with verified, dated rates for GPT-5.5, 5.4, 5.4-mini, 5.4-nano.
|
|
38
|
+
|
|
39
|
+
## [0.2.0] - 2026-09-02
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- OpenAI capture for the Chat Completions and Responses APIs, plain and streaming,
|
|
43
|
+
on the same proxy port as Anthropic (routed by path, then auth-header style).
|
|
44
|
+
`stream_options.include_usage` is injected on streaming chat requests.
|
|
45
|
+
- `cpt label`: pass, fail and leak labels in a separate append-only
|
|
46
|
+
`cpt-labels.jsonl`, plus CSV import.
|
|
47
|
+
- Statistics: Wilson interval, task-cluster bootstrap, P90, capped-retry success,
|
|
48
|
+
pass^k.
|
|
49
|
+
- Report per model and task type with CPT_solved, cost per task attempted,
|
|
50
|
+
CPT_risk, and the disclosure checklist; `cpt compare` with break-even K*.
|
|
51
|
+
- `--task-type` on `cpt run`.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- `reasoning_per_mtok: null` now means "billed at the output rate" rather than
|
|
55
|
+
"not priced".
|
|
56
|
+
|
|
57
|
+
## [0.1.0] - 2026-09-01
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- Local capture proxy for the Anthropic Messages API, plain and streaming, logging
|
|
61
|
+
only usage metadata (never keys, headers, prompts or completions).
|
|
62
|
+
- JSONL step schema aligned with the OpenTelemetry GenAI semantic conventions.
|
|
63
|
+
- Dated pricing tables and per-attempt cost; `prices/anthropic.json` verified against
|
|
64
|
+
the Anthropic price list.
|
|
65
|
+
- `cpt run`, `cpt serve`, `cpt report`.
|
|
66
|
+
|
|
67
|
+
[Unreleased]: https://github.com/OptimNow/cost-per-task/compare/v0.4.0...HEAD
|
|
68
|
+
[0.4.0]: https://github.com/OptimNow/cost-per-task/releases/tag/v0.4.0
|
|
69
|
+
[0.3.0]: https://github.com/OptimNow/cost-per-task/releases/tag/v0.3.0
|
|
70
|
+
[0.2.0]: https://github.com/OptimNow/cost-per-task/releases/tag/v0.2.0
|
|
71
|
+
[0.1.0]: https://github.com/OptimNow/cost-per-task/releases/tag/v0.1.0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# cost-per-task
|
|
2
|
+
|
|
3
|
+
Vendor-neutral Python tool measuring the real cost per completed task of LLM agents. Implements the DoiT "Cost Per Task, Not Cost Per Token" measurement framework (13 August 2026): a local reverse proxy captures token usage per API call, calls are grouped into steps, attempts and tasks, priced from a dated table, and reported as cost per attempt, cost per solved task (CPT_solved = E[C_attempt] / p) and risk-adjusted cost (CPT_risk = CPT_solved + L x K).
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
- `src/cost_per_task/proxy.py`: local reverse proxy serving Anthropic and OpenAI on one port (routed by path, then auth-header style); the agent is pointed at it via ANTHROPIC_BASE_URL / OPENAI_BASE_URL. Upstreams may carry a path prefix (OpenRouter: `https://openrouter.ai/api`); any OpenAI-compatible gateway works as the `openai` upstream and the log's `provider` becomes the gateway host. Forwards requests unchanged except for injecting `stream_options.include_usage` (streaming OpenAI chat) and `usage.include` (OpenRouter); logs only usage metadata plus `reported_cost` when the gateway states what it charged.
|
|
8
|
+
- `src/cost_per_task/providers/`: per-provider usage extraction (JSON and SSE): `anthropic.py`, `openai.py` (Chat Completions and Responses; subtracts cached and reasoning tokens out of the totals). Design allows Bedrock, Vertex, xAI.
|
|
9
|
+
- `src/cost_per_task/schema.py`: StepRecord (OpenTelemetry GenAI aligned) + JSONL persistence.
|
|
10
|
+
- `src/cost_per_task/pricing.py`: dated pricing tables, per-step cost; `reasoning_per_mtok: null` means output rate; `load_many` merges vendor tables (oldest `as_of` wins); `rates_for` strips gateway vendor prefixes and dot/hyphen differences.
|
|
11
|
+
- `src/cost_per_task/labels.py`: pass/fail/leak labels in a separate append-only `cpt-labels.jsonl` (latest wins), CSV import.
|
|
12
|
+
- `src/cost_per_task/stats.py`: Wilson interval, percentile, task-cluster bootstrap, p_N, pass^k. Standard library only.
|
|
13
|
+
- `src/cost_per_task/metrics.py`: records to Attempt (primary model = largest cost share) to GroupSummary per model and task type; CPT_solved, CPT_risk, K*.
|
|
14
|
+
- `src/cost_per_task/report.py`: text report, two-model comparison, disclosure checklist, JSON output.
|
|
15
|
+
- `src/cost_per_task/analysis.py`: files-to-summaries loader shared by the CLI and the MCP server.
|
|
16
|
+
- `src/cost_per_task/importers/`: Langfuse and LiteLLM export importers (`common.py` reads CSV/JSON/JSONL and groups rows into attempts). Built from documented schemas, not validated against live exports yet.
|
|
17
|
+
- `src/cost_per_task/prices_hub.py`: `cpt prices refresh`; fetches `optimtoken.optimnow.io/api/llm-models` with urllib, maps ids per provider (Anthropic dots to hyphens), derives cache-write rates from documented rules, diffs by default, writes only with `--write`, flags anomalous cache-read ratios.
|
|
18
|
+
- `src/cost_per_task/mcp_server.py`: MCP tools (`cpt_report`, `cpt_compare`, `cpt_risk_denominator`); the SDK is the optional `[mcp]` extra (mcp 2.x `MCPServer`, 1.x `FastMCP` fallback) and is imported lazily.
|
|
19
|
+
- `src/cost_per_task/cli.py`: `cpt serve`, `run`, `label`, `report`, `compare`, `import`, `prices refresh`, `mcp`.
|
|
20
|
+
- `prices/anthropic.json`, `prices/openai.json`: verified, dated rates; `prices/example.json` is a zero-value template. `tests/fixtures/hub_sample.json` is a recorded hub sample. Claude Fable 5.1's cache-read rate is a documented special 0.025x (0.25 USD/MTok); the refresh command flags it as suspicious by design, and Anthropic's table confirms it.
|
|
21
|
+
- `docs/testing-guide.md` plus `examples/` (`ask.py` stdlib client, `level1.ps1`, `level2.ps1`, `level2/` tasks): the guided model comparison for non-developers.
|
|
22
|
+
|
|
23
|
+
## Hard rules
|
|
24
|
+
|
|
25
|
+
- **Never log secrets or content.** The proxy must never write API keys, headers, prompts or completions to the log. Only token counts, model, provider, ids, latency, tool names. Tests enforce this (`test_no_secrets_or_content_in_log`); keep them passing.
|
|
26
|
+
- **Token counts come from the provider API response, never a local tokenizer.**
|
|
27
|
+
- **No invented prices.** Pricing tables carry `as_of` dates and a source; an undated table is rejected at load. Do not add or update a price without a verifiable source.
|
|
28
|
+
- **Zero runtime dependencies in the core.** Standard library only; pytest is the sole dev dependency. The only optional extra is `[mcp]` for `cpt mcp`. Any new dependency needs explicit justification and agreement.
|
|
29
|
+
- **Hub prices never land unseen.** `cpt prices refresh` diffs by default; `--write` is a deliberate step after reading the diff and warnings.
|
|
30
|
+
|
|
31
|
+
## Conventions
|
|
32
|
+
|
|
33
|
+
- Python 3.11+, src/ layout, hatchling build, entry point `cpt`.
|
|
34
|
+
- Tests with pytest (`python -m pytest -q`); proxy tests run against the fake upstream in `tests/conftest.py`, never the real API.
|
|
35
|
+
- Documentation and user-facing text: British English, no em dashes, no emojis, facts before interpretation.
|
|
36
|
+
- Semver: user-visible feature changes bump the minor digit (0.1.x to 0.2.0), not patch.
|
|
37
|
+
- Streaming: Anthropic reports input usage in `message_start` and final output usage in `message_delta`; OpenAI Chat Completions only reports usage on a stream when `stream_options.include_usage` is set (the proxy injects it); the Responses API reports usage in `response.completed`. Anthropic does not report reasoning tokens separately (billed inside output_tokens), so `reasoning_tokens` is null for Anthropic rows; OpenAI reports reasoning as a subset of output and the adapter splits it out.
|
|
38
|
+
- The usage log is never rewritten; outcomes go to the labels file.
|
|
39
|
+
- Pricing tables hold one rate per token class; long-context tiers (Anthropic >200K, OpenAI >272K) and batch discounts are a documented limitation, not silently approximated.
|
|
40
|
+
|
|
41
|
+
## Workflow and releases
|
|
42
|
+
|
|
43
|
+
- Work on a branch and open a pull request; CI (`ci.yml`) must be green before merging to `main`. Do not push to `main` directly.
|
|
44
|
+
- Release: bump `version` in `pyproject.toml` and `__version__` in `src/cost_per_task/__init__.py` (minor digit for features), move the `[Unreleased]` entries in `CHANGELOG.md` under the new version, merge, then `git tag vX.Y.Z` on `main` and push the tag. `publish-pypi.yml` tests the tagged commit, builds, publishes through PyPI trusted publishing (GitHub environment `pypi`, no stored token) and creates the GitHub Release. Re-publish an existing tag with `gh workflow run publish-pypi.yml -f tag=vX.Y.Z`.
|
|
45
|
+
- One-time setup owned by the repository owner: the PyPI pending publisher for `cost-per-task` (owner OptimNow, repo cost-per-task, workflow `publish-pypi.yml`, environment `pypi`) and the GitHub environment `pypi`.
|
|
46
|
+
|
|
47
|
+
## Roadmap
|
|
48
|
+
|
|
49
|
+
Phases 1 to 3 shipped (0.3.0), OpenRouter and OpenAI-compatible gateways in 0.4.0: capture for both providers, labelling, statistics, CPT_solved / CPT_risk / K*, disclosure checklist, importers, Pricing Hub refresh, JSON output, MCP server, provider-reported cost reconciliation.
|
|
50
|
+
Next: validate importers on real exports, further providers (Bedrock, Vertex, xAI), long-context price tiers, PyPI release and public repo (apply the OptimNow public-repo hardening standard first).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OptimNow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|