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.
Files changed (59) hide show
  1. cost_per_task-0.4.0/.github/workflows/ci.yml +23 -0
  2. cost_per_task-0.4.0/.github/workflows/publish-pypi.yml +183 -0
  3. cost_per_task-0.4.0/.gitignore +14 -0
  4. cost_per_task-0.4.0/CHANGELOG.md +71 -0
  5. cost_per_task-0.4.0/CLAUDE.md +50 -0
  6. cost_per_task-0.4.0/LICENSE +21 -0
  7. cost_per_task-0.4.0/PKG-INFO +415 -0
  8. cost_per_task-0.4.0/README.md +392 -0
  9. cost_per_task-0.4.0/docs/testing-guide.md +246 -0
  10. cost_per_task-0.4.0/examples/ask.py +85 -0
  11. cost_per_task-0.4.0/examples/level1.ps1 +52 -0
  12. cost_per_task-0.4.0/examples/level2/csvtotal/TASK.md +1 -0
  13. cost_per_task-0.4.0/examples/level2/csvtotal/spend.csv +7 -0
  14. cost_per_task-0.4.0/examples/level2/csvtotal/spend.py +4 -0
  15. cost_per_task-0.4.0/examples/level2/csvtotal/test_spend.py +16 -0
  16. cost_per_task-0.4.0/examples/level2/datefix/TASK.md +1 -0
  17. cost_per_task-0.4.0/examples/level2/datefix/invoices.py +12 -0
  18. cost_per_task-0.4.0/examples/level2/datefix/test_invoices.py +17 -0
  19. cost_per_task-0.4.0/examples/level2/fizzbuzz/TASK.md +1 -0
  20. cost_per_task-0.4.0/examples/level2/fizzbuzz/fizzbuzz.py +7 -0
  21. cost_per_task-0.4.0/examples/level2/fizzbuzz/test_fizzbuzz.py +16 -0
  22. cost_per_task-0.4.0/examples/level2.ps1 +64 -0
  23. cost_per_task-0.4.0/prices/anthropic.json +47 -0
  24. cost_per_task-0.4.0/prices/example.json +15 -0
  25. cost_per_task-0.4.0/prices/openai.json +39 -0
  26. cost_per_task-0.4.0/pyproject.toml +44 -0
  27. cost_per_task-0.4.0/src/cost_per_task/__init__.py +3 -0
  28. cost_per_task-0.4.0/src/cost_per_task/analysis.py +65 -0
  29. cost_per_task-0.4.0/src/cost_per_task/cli.py +406 -0
  30. cost_per_task-0.4.0/src/cost_per_task/importers/__init__.py +10 -0
  31. cost_per_task-0.4.0/src/cost_per_task/importers/common.py +183 -0
  32. cost_per_task-0.4.0/src/cost_per_task/importers/langfuse.py +106 -0
  33. cost_per_task-0.4.0/src/cost_per_task/importers/litellm.py +80 -0
  34. cost_per_task-0.4.0/src/cost_per_task/labels.py +86 -0
  35. cost_per_task-0.4.0/src/cost_per_task/mcp_server.py +153 -0
  36. cost_per_task-0.4.0/src/cost_per_task/metrics.py +314 -0
  37. cost_per_task-0.4.0/src/cost_per_task/prices_hub.py +181 -0
  38. cost_per_task-0.4.0/src/cost_per_task/pricing.py +167 -0
  39. cost_per_task-0.4.0/src/cost_per_task/providers/__init__.py +0 -0
  40. cost_per_task-0.4.0/src/cost_per_task/providers/anthropic.py +87 -0
  41. cost_per_task-0.4.0/src/cost_per_task/providers/base.py +41 -0
  42. cost_per_task-0.4.0/src/cost_per_task/providers/openai.py +145 -0
  43. cost_per_task-0.4.0/src/cost_per_task/proxy.py +388 -0
  44. cost_per_task-0.4.0/src/cost_per_task/report.py +287 -0
  45. cost_per_task-0.4.0/src/cost_per_task/schema.py +75 -0
  46. cost_per_task-0.4.0/src/cost_per_task/stats.py +97 -0
  47. cost_per_task-0.4.0/tests/conftest.py +233 -0
  48. cost_per_task-0.4.0/tests/fixtures/hub_sample.json +13 -0
  49. cost_per_task-0.4.0/tests/test_cli_attempts.py +25 -0
  50. cost_per_task-0.4.0/tests/test_importers.py +120 -0
  51. cost_per_task-0.4.0/tests/test_json_and_mcp.py +128 -0
  52. cost_per_task-0.4.0/tests/test_labels.py +51 -0
  53. cost_per_task-0.4.0/tests/test_metrics.py +176 -0
  54. cost_per_task-0.4.0/tests/test_openai_provider.py +81 -0
  55. cost_per_task-0.4.0/tests/test_prices_hub.py +100 -0
  56. cost_per_task-0.4.0/tests/test_pricing.py +138 -0
  57. cost_per_task-0.4.0/tests/test_proxy.py +313 -0
  58. cost_per_task-0.4.0/tests/test_schema.py +38 -0
  59. 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,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ cpt-log.jsonl
10
+ cpt-labels.jsonl
11
+ level1-log.jsonl
12
+ level1-labels.jsonl
13
+ level2-log.jsonl
14
+ level2-labels.jsonl
@@ -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.