Skip to content

release: rename to openrouter-agent-sdk, move to openrouter 1.x, add PyPI publishing - #23

Merged
LukasParke merged 5 commits into
mainfrom
LukasParke/pypi-publish
Jul 31, 2026
Merged

release: rename to openrouter-agent-sdk, move to openrouter 1.x, add PyPI publishing#23
LukasParke merged 5 commits into
mainfrom
LukasParke/pypi-publish

Conversation

@LukasParke

@LukasParke LukasParke commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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-agent is already taken on PyPI

Owned 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:

pip install openrouter-agent-sdk
from openrouter_agent import call_model, tool

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 port sync doesn't "correct" it back.

2. The SDK pin was unbounded and untested at its upper range

openrouter>=0.10.2 resolved to 0.10.3 locally, but PyPI's latest is 1.1.22 — so a fresh pip install would get a major version the port had never been tested against.

Now openrouter>=1.1,<2, verified before bumping: all 114 tests and mypy pass against 1.1.22, and the shared fixtures still validate against 1.x's OpenResponsesResult (same 18 required fields).

This forces dropping Python 3.9

Every openrouter 1.x release requires >=3.10 — the SDK dropped 3.9 exactly at 1.0.0:

SDK version requires_python
0.10.8 >=3.9.2
1.0.0 → 1.1.22 >=3.10

Python 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 <2 bound 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.md is 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.
  • sdist cleanup: it 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 just src, tests, README, PORTING, LICENSE, pyproject, changelog.
  • Confirmed py.typed actually 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, targets testpypi or pypi.

Publishing is irreversible (a version can never be reused, even after a yank), so 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 on the target index.

Two bugs caught during this work

My own version-collision guard didn't work. Originally a shell for loop 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 build job still queried openrouter-agent metadata after the rename, failing with PackageNotFoundError — 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 off sys.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):

Gate Result
verify.sh PASS, 0 failures
mypy src tests clean
pytest tests/unit 114 passed
Coverage 83.89% over an 83% floor
Required public API 31/31
twine check --strict PASSED on wheel + sdist
Isolated wheel import passes
Python 3.10 / 3.13 green

Nothing is published

Two manual setup steps are required before the first release, and the workflow cannot do them for you:

  1. On PyPI — add a GitHub trusted publisher: owner OpenRouterTeam, repo python-agent, workflow publish.yaml, environment pypi. Add it as a pending publisher since the project doesn't exist yet.
  2. In repo Settings — create the pypi and testpypi environments, and set each one's deployment branch policy to main.

That branch policy is the real ref restriction. PyPI's trusted publisher pins owner/repo/workflow/environment but carries no branch claim, and workflow_dispatch runs the workflow file from whatever ref is selected — so the in-file if: guard stops accidents while the environment policy is what actually binds publishing to main.

Then: testpypi rehearsal → pypi dry-run → real publish. Documented in PORTING.md.

Two things needing your decision

  1. Branch protection: the required checks changed with the matrix. check (py3.9) no longer exists — it should now be check (py3.10), check (py3.11), check (py3.13), types, build, verify-port. (e2e stays unrequired: it exits 0 without the secret, so requiring it would be a green rubber stamp on forks.)
  2. The version is still 0.8.0resolved: staying at 0.8.0 (see below).

🤖 Generated with Claude Code


Update: the first release is 0.8.0

Resolved: the package version stays coupled to the ported @openrouter/agent version. So a release of openrouter-agent-sdk states plainly which upstream version it is a port of, and the first published release is 0.8.0 — not 0.0.1.

I briefly split the two numbers (distribution version at 0.0.1, ported version recorded separately in state.yaml), then reverted it. The revert restored six files to byte-identical pre-split state — verified per file, not assumed:

  • verify.sh compares pyproject.toml against upstream's package.json again
  • state.yaml drops the extra upstream_agent_version field
  • the contract, eval.md, the converter skill, and scripts/upstream all treat pyproject.toml version as the ported version again

Re-confirmed the check still has teeth after the revert: a match passes, and drift (pyproject.toml 0.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:

  • A comment on version in pyproject.toml explaining the coupling, so the next reader knows 0.8.0 is deliberate rather than stale.
  • Changelog cleanup. The 0.8.0 entry now says why the first release is 0.8.0 rather than 0.0.1 — that direction is surprising enough to warrant stating. I also merged the two identically-numbered ## 0.8.0 headings 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.sh PASS (0 failures) · artifacts build as openrouter_agent_sdk-0.8.0 (wheel + sdist) · twine check --strict PASSED on both · wheel imports 0.8.0 in 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).

LukasParke and others added 5 commits July 30, 2026 19:47
…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>
@LukasParke
LukasParke merged commit 089de5b into main Jul 31, 2026
7 checks passed
@LukasParke
LukasParke deleted the LukasParke/pypi-publish branch July 31, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant