release: rename to openrouter-agent-sdk, move to openrouter 1.x, add PyPI publishing - #23
Merged
Merged
Conversation
…PyPI publishing This package could not be published as-is. Two blockers, found by checking the index rather than assuming: 1. `openrouter-agent` is ALREADY TAKEN on PyPI — by an unrelated third-party Pydantic AI integration (VinnyVanGogh, v0.1.3, last released 2025-04-21). Not ours, not abandoned enough to assume, and not usable. 2. The dependency pin `openrouter>=0.10.2` was unbounded while PyPI's latest is 1.1.22, so a fresh `pip install` would resolve a major version the port had never been tested against. Package identity Renamed the distribution to `openrouter-agent-sdk`. The import stays `openrouter_agent` — a PyPI name differing from the import name is normal (scikit-learn/sklearn), and keeping the import aligned with upstream avoids churning every consumer's code and every doc example. Recorded as a fixed Package Identity table in .upstreamer/upstreamer.md so a sync does not "correct" the name back to match upstream's @openrouter/agent. Substrate: openrouter>=1.1,<2, and Python floor 3.10 Verified before bumping, not after: all 114 tests AND mypy pass against openrouter 1.1.22, and the shared fixtures still validate against 1.x's OpenResponsesResult (same 18 required fields). The bump forces dropping Python 3.9: every openrouter 1.x release requires >=3.10 — the SDK dropped 3.9 exactly at 1.0.0, with 0.10.8 the last 3.9-capable release. Python 3.9 reached EOL in October 2025, so `requires-python` is now ">=3.10" and the CI matrix is 3.10/3.11/3.13. Both new legs verified passing; 3.9 now correctly refuses to resolve. The upper bound `<2` is deliberate: a 2.x could move the Responses API surface this port binds to, and an unbounded floor is how the original problem happened. The contract's substrate-pin section forbids bumping this dependency on the port's own initiative, so it is updated in the same commit to authorize the new pin and record both consequences — otherwise the next sync reverts it or reports it as drift. Packaging metadata - Added [project.urls] (repository, issues, changelog, upstream) and trove classifiers. There was no repository link on the package metadata at all. - Added [tool.hatch.build.targets.sdist] include list. The sdist was shipping the entire porting apparatus — .upstreamer/ (contract, eval prompts, skills), .github/, opencode.json, scripts/upstream. None of it helps someone building from source, and shipping the contract invites confusion about what the package is. Now: src, tests, README, PORTING, LICENSE, pyproject, changelog. - Confirmed py.typed ships in the wheel (the README claims it does). Publish workflow .github/workflows/publish.yaml, using PyPI trusted publishing (OIDC) — no API token stored in this repo. Manual-only, defaults to dry-run, targets testpypi or pypi. Because publishing is irreversible — a version can never be reused, even after a yank — it re-runs verify.sh rather than trusting an earlier CI pass, checks metadata with `twine check --strict`, imports the built wheel in isolation, and refuses to upload a version already present on the target index. The version-collision guard is implemented in Python, not by word-splitting a shell string. The shell form is subtly non-portable (zsh does not split unquoted variables the way bash does) and I caught it failing to detect a real collision while testing it — a guard that silently stops matching is worse than no guard, since it would wave through exactly the re-upload it exists to prevent. Verified in all three states: collision refused, non-collision allowed, new project allowed. Verification verify.sh PASS (0 failures) · mypy src tests clean · 114 passed · coverage 83.89% over an 83% floor · 31/31 required symbols · twine check --strict PASSED on both artifacts · wheel imports in an isolated env · 3.10 and 3.13 both green. Not done here, deliberately: nothing is published. The first release needs two manual setup steps the workflow cannot perform — a PyPI trusted publisher (owner/repo/workflow/environment) and repo environments `pypi`/`testpypi` with their deployment branch policy set to main. That policy is the real ref restriction; PyPI's trusted publisher carries no branch claim, and workflow_dispatch runs the workflow file from whatever ref is selected, so the in-file guard stops accidents while the environment policy is what binds publishing to main. Both documented in PORTING.md. Co-Authored-By: Claude <noreply@anthropic.com>
The build job still looked up `openrouter-agent` metadata after the rename to `openrouter-agent-sdk`, so it failed with PackageNotFoundError. The import itself succeeded — only the version lookup was stale, since the import name (`openrouter_agent`) did not change. Caught by CI on the rename commit, which is the build job doing its job: it installs the built wheel with the source tree off sys.path, so a distribution-name mismatch surfaces there rather than at publish time. publish.yaml already used the correct name. Reproduced locally against the built wheel before and after the fix. Co-Authored-By: Claude <noreply@anthropic.com>
The changelog is the user-facing surface for a breaking change, and this release carries two: the distribution rename and the Python floor moving to 3.10. Someone on 3.9 needs to be told to stay on an earlier release rather than discovering it from a resolver error. Co-Authored-By: Claude <noreply@anthropic.com>
`openrouter-agent-sdk` is a brand-new project on PyPI. Publishing it as 0.8.0
would advertise a release history that does not exist, and would burn every
version number below it — permanently, since a PyPI version can never be reused.
This is not a one-line change, because the version was coupled to the port.
Splitting the two version numbers
The distribution version and the ported upstream version were the same field.
They now differ, so they are separate:
pyproject.toml `version` -> PyPI distribution version (0.0.1).
Humans change this at release time.
state.yaml `upstream_agent_version` -> the @openrouter/agent version actually
ported (0.8.0). A sync updates this
alongside upstream_commit.
`verify.sh`'s version-consistency check was comparing pyproject.toml against
upstream's package.json, so 0.0.1 would have failed it during a sync run. It now
checks `upstream_agent_version` instead, and reports the distribution version as
INFO.
The point was to retarget that check, not weaken it — version honesty is what
catches a port recording a version it did not achieve. Verified in three states:
matching passes; drift (state.yaml 0.8.0 vs upstream 0.9.0) fails with exit 1; a
missing `upstream_agent_version` fails loudly rather than silently skipping.
Everything that writes or reads the old coupling
- scripts/upstream told the porting agent to write only `upstream_commit`, which
would have dropped the new field on the next sync and failed the verifier. It
now instructs both fields, and explicitly not to touch pyproject.toml `version`.
- .upstreamer/eval.md told the grader to check the port's "declared version",
which is now the wrong field. Retargeted, and a sync that advances
upstream_commit without advancing upstream_agent_version is now a finding.
- The converter skill said "package version changes only where the substrate-pin
section directs" — now says not to touch it at all.
- The contract's Package Version section is a two-row table explaining which
number is which, who changes it, and why they had to split.
Changelog
Added a 0.0.1 entry. Also corrected a line from the previous commit that told
Python 3.9 users to "stay on an earlier release" — there is no earlier release, so
it now says so.
Verification
verify.sh PASS (0 failures) · artifacts build as openrouter_agent_sdk-0.0.1
(wheel + sdist) · twine check --strict PASSED on both · wheel imports 0.0.1 in an
isolated env · publish.yaml resolves the version dynamically, so its
already-published guard picks up 0.0.1 with no change · 114 passed, coverage
83.89%.
Co-Authored-By: Claude <noreply@anthropic.com>
Keeps the package version tracking the ported @openrouter/agent version, so a release of openrouter-agent-sdk states plainly which upstream version it is a port of. The first published release is therefore 0.8.0, not 0.0.1. This reverts the version split from 820616c. Restored to their pre-split state (verified byte-identical): verify.sh's version-consistency check compares pyproject.toml against upstream's package.json again; state.yaml drops upstream_agent_version; and the contract, eval, converter skill, and scripts/upstream go back to treating pyproject.toml `version` as the ported version. Re-confirmed the check still bites: match passes, drift (pyproject 0.8.0 vs upstream 0.9.0) fails with exit 1. Kept from the reverted commit, since both remain true: - The pyproject comment now explains the coupling, so the next reader knows the version is deliberate rather than stale. - The changelog's 0.8.0 entry says why the first release is 0.8.0 rather than 0.0.1 — the surprising direction needs stating. - Merged the duplicate 0.8.0 headings the version change had created (the release entry and the sync entry) into one, split into "Packaging" and "Packaged surface". Two identically-numbered sections read as a mistake. Verification: verify.sh PASS (0 failures) · artifacts build as openrouter_agent_sdk-0.8.0 · twine check --strict PASSED on wheel + sdist · wheel imports 0.8.0 in an isolated env · 114 passed, coverage 83.89%. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes this package publishable. Follows #22 (merged) — these commits were pushed to that branch after it was squash-merged, so they needed their own PR.
Two blockers, both found by checking the index rather than assuming.
1.
openrouter-agentis already taken on PyPIOwned by an unrelated third party — a Pydantic AI integration (VinnyVanGogh, v0.1.3, last released 2025-04-21). Not ours, and not safe to assume abandoned.
Distribution renamed to
openrouter-agent-sdk. The import is unchanged:A PyPI name differing from the import name is normal (
scikit-learn/sklearn), and keeping the import aligned with upstream avoids churning every consumer's code and every doc example. Recorded as a fixed Package Identity table in.upstreamer/upstreamer.mdso a port sync doesn't "correct" it back.2. The SDK pin was unbounded and untested at its upper range
openrouter>=0.10.2resolved to 0.10.3 locally, but PyPI's latest is 1.1.22 — so a freshpip installwould get a major version the port had never been tested against.Now
openrouter>=1.1,<2, verified before bumping: all 114 tests andmypypass against 1.1.22, and the shared fixtures still validate against 1.x'sOpenResponsesResult(same 18 required fields).This forces dropping Python 3.9
Every
openrouter1.x release requires>=3.10— the SDK dropped 3.9 exactly at 1.0.0:>=3.9.2>=3.10Python 3.9 reached EOL in October 2025, so
requires-python = ">=3.10"and the CI matrix is now 3.10/3.11/3.13. Both new legs verified passing; 3.9 correctly refuses to resolve. The<2bound is deliberate — an unbounded floor is how this problem happened, and a 2.x could move the Responses API surface this port binds to.The contract forbids bumping this dependency on the port's own initiative, so
.upstreamer/upstreamer.mdis updated in the same commit to authorize the pin and record both consequences. Otherwise the next sync reverts it or flags it as drift.Packaging metadata
[project.urls]+ trove classifiers — there was no repository link in the package metadata at all..upstreamer/(contract, eval prompts, skills),.github/,opencode.json,scripts/upstream. None of it helps someone building from source, and shipping the contract invites confusion about what the package is. Now just src, tests, README, PORTING, LICENSE, pyproject, changelog.py.typedactually ships in the wheel (the README claims it does).Publish workflow
.github/workflows/publish.yaml— PyPI trusted publishing (OIDC), so no API token is stored in this repo. Manual-only, defaults to dry-run, targetstestpypiorpypi.Publishing is irreversible (a version can never be reused, even after a yank), so it re-runs
verify.shrather than trusting an earlier CI pass, checks metadata withtwine check --strict, imports the built wheel in isolation, and refuses to upload a version already on the target index.Two bugs caught during this work
My own version-collision guard didn't work. Originally a shell
forloop over a command-substituted string, it failed to detect a real collision — zsh doesn't word-split unquoted variables the way bash does. It would have worked in CI (bash), but a guard that silently stops matching is worse than no guard, since it waves through exactly the re-upload it exists to prevent. Rewritten in Python and verified in all three states: collision refused, non-collision allowed, new project allowed.CI caught a stale distribution name (commit 2). The
buildjob still queriedopenrouter-agentmetadata after the rename, failing withPackageNotFoundError— the import succeeded, only the version lookup was stale. That's the build job earning its place: it installs the wheel with the source tree offsys.path, so the mismatch surfaced there instead of at publish time. Reproduced locally before and after the fix.Verification
All 7 CI jobs passed on this exact tree (run on the predecessor branch at
047d951, which is byte-identical to this branch's head):verify.shmypy src testspytest tests/unittwine check --strictNothing is published
Two manual setup steps are required before the first release, and the workflow cannot do them for you:
OpenRouterTeam, repopython-agent, workflowpublish.yaml, environmentpypi. Add it as a pending publisher since the project doesn't exist yet.pypiandtestpypienvironments, and set each one's deployment branch policy tomain.That branch policy is the real ref restriction. PyPI's trusted publisher pins owner/repo/workflow/environment but carries no branch claim, and
workflow_dispatchruns the workflow file from whatever ref is selected — so the in-fileif:guard stops accidents while the environment policy is what actually binds publishing tomain.Then:
testpypirehearsal →pypidry-run → real publish. Documented in PORTING.md.Two things needing your decision
check (py3.9)no longer exists — it should now becheck (py3.10),check (py3.11),check (py3.13),types,build,verify-port. (e2estays unrequired: it exits 0 without the secret, so requiring it would be a green rubber stamp on forks.)The version is still 0.8.0— resolved: staying at0.8.0(see below).🤖 Generated with Claude Code
Update: the first release is
0.8.0Resolved: the package version stays coupled to the ported
@openrouter/agentversion. So a release ofopenrouter-agent-sdkstates plainly which upstream version it is a port of, and the first published release is0.8.0— not0.0.1.I briefly split the two numbers (distribution version at
0.0.1, ported version recorded separately instate.yaml), then reverted it. The revert restored six files to byte-identical pre-split state — verified per file, not assumed:verify.shcomparespyproject.tomlagainst upstream'spackage.jsonagainstate.yamldrops the extraupstream_agent_versionfieldeval.md, the converter skill, andscripts/upstreamall treatpyproject.tomlversionas the ported version againRe-confirmed the check still has teeth after the revert: a match passes, and drift (
pyproject.toml0.8.0 vs upstream 0.9.0) fails with exit 1.Two things kept from the reverted commit
Both are still true regardless of which version scheme is used:
versioninpyproject.tomlexplaining the coupling, so the next reader knows0.8.0is deliberate rather than stale.0.8.0entry now says why the first release is0.8.0rather than0.0.1— that direction is surprising enough to warrant stating. I also merged the two identically-numbered## 0.8.0headings the version change had created into one entry, split into Packaging and Packaged surface; two sections with the same number read as a mistake.Verification
verify.shPASS (0 failures) · artifacts build asopenrouter_agent_sdk-0.8.0(wheel + sdist) ·twine check --strictPASSED on both · wheel imports0.8.0in an isolated env · 114 passed, coverage 83.89%.One open decision remains: branch protection needs
check (py3.10),check (py3.11),check (py3.13),types,build,verify-port(check (py3.9)no longer exists).