feat: unify the SDK with the Seam SDKs for other languages - #460
Open
razor-x wants to merge 2 commits into
Open
Conversation
The Python, Ruby, and JavaScript SDKs share a runtime core, a common README skeleton, and a test suite aligned to the same baseline. This SDK had the codegen and release tooling but not the runtime surface. This brings it in line. Added: - Personal access token authentication, with the seam-workspace header, and token format validation that rejects client session tokens, JWTs, and publishable keys with a specific message. - from_api_key, from_personal_access_token, and from_client factories. - SeamMultiWorkspace for the endpoints that are not scoped to a workspace. - SeamWebhook, verifying incoming webhooks with svix. - SEAM_ENDPOINT support, plus the deprecated SEAM_API_URL and its warnings. - Retries, two by default with exponential backoff, via caseyamcl/guzzle_retry_middleware. A request that never reached the server is always retried; a status code is only retried for idempotent methods, since retrying a POST the server may already have processed could duplicate a write. The other Seam SDKs make the same trade. - HTTP layer configuration: guzzle_options, retries, and an injectable client. - A client level wait_for_action_attempt default, accepting a bool or a timeout and polling_interval. - A test suite covering auth, env, headers, errors, malformed responses, retries, pagination, serialization, action attempts, and webhooks, run against @seamapi/fake-seam-connect. - Psalm, wired into composer lint. Fixed: - Responses in the 3xx range were treated as successful. - The Seam error check accepted any body with a truthy error key. It now checks the content type and that error.type and error.message are strings, matching the other SDKs. - throw_http_errors let Guzzle throw before the SDK could map the error, making the whole error mapping unreachable. The option is gone. - Malformed JSON silently decoded to null and then failed on property access. - Non-Seam error responses raised an exception built from a fabricated request rather than the real one. - getRequestId returned an empty string rather than null when the header was absent, and the fallback error type was unknown rather than unknown_error. - HttpInvalidInputError never actually overrode the error code, and the action attempt errors wrote to an undeclared property. - Paginator::firstPage indexed its cache unconditionally, and the null cursor guard was unreachable. BREAKING CHANGE: The client is Seam\Seam; Seam\SeamClient remains as a deprecated alias. The constructor takes named options, so endpoint is no longer the second positional argument, and throw_http_errors is removed. Exceptions moved to the Seam\Exceptions namespace. poll_until_ready is removed in favor of wait_for_action_attempt, whose defaults change from 20s/0.4s to 10s/1s. $seam->client is a Seam\Http\SeamHttpClient rather than a Guzzle client. The $api_key property and the global LTS_VERSION constant are removed. Responses in the 3xx range are no longer treated as successful. Requests are now retried. Pagination metadata is a Seam\Pagination object. PHP 8.1 or later is required, and svix/svix is a new dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fwgphpc9YhtxBbxuxppy8m
The core public API is small enough to read at a glance, so nesting part of it under Seam\Exceptions bought organization it does not need. Keeping the classes where 3.x had them also means existing catch blocks keep working. Sub-namespacing errors is the more common PHP convention, but the Python and JavaScript SDKs both export theirs at the package root, so this is closer to them as well. The new SeamException marker interface, InvalidOptionsError, and InvalidTokenError are all that changes for a caller upgrading from 3.x. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fwgphpc9YhtxBbxuxppy8m
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.
The Python, Ruby, and JavaScript SDKs share a runtime core, a common README skeleton, and a test suite aligned to the same baseline. This SDK already had the codegen and release tooling but not the runtime surface. This brings it in line.
The parity gaps were found by reading
client.ts,client.py,request.rb, andSeamClient.phpside by side rather than by comparing READMEs, which is how the retry layer and several behavioral divergences surfaced at all — none of them are documented anywhere.Added
seam-workspaceheader, plus token format validation that rejects client session tokens, JWTs, and publishable keys with a specific message instead of letting the server return an opaque 401.from_api_key,from_personal_access_token, andfrom_clientfactories.SeamMultiWorkspacefor the endpoints that are not scoped to a workspace, exposing onlyworkspaces->list()andworkspaces->create().SeamWebhook, verifying incoming webhooks with svix and returning a typedEvent.SEAM_ENDPOINTsupport, plus the deprecatedSEAM_API_URLand both of its warnings.caseyamcl/guzzle_retry_middleware.guzzle_options,retries, and an injectable client.wait_for_action_attemptdefault, accepting a bool or atimeoutandpolling_interval.@seamapi/fake-seam-connect.composer lint.A note on retry semantics
A request that never reached the server (a connection failure) is always retried. A request that did reach the server is only retried on a retryable status when the HTTP method is idempotent.
Every Seam endpoint is a
POST, so in practice an SDK call retries on transport failures and never because of a response status. Retrying a POST the server may already have processed could duplicate a write — double-creating an access code, say. Every sibling SDK makes the same trade, and Ruby asserts it explicitly (spec/seam_client/retry_spec.rb: "does not retry POST requests by default").tests/RetryTest.phpasserts it here too.Fixed
Bugs found in the existing client while doing this:
>= 400rather than outside 200–299).errorkey. It now checks the content type and thaterror.typeanderror.messageare strings, matching the five-condition predicate the other SDKs use.throw_http_errorslet Guzzle throw before the SDK could map the error, making the entire error-mapping block unreachable when set. The option is gone; errors always map.nulland then failed on property access. Thetry/catcharoundjson_decodewas dead code, since it returnsnullrather than throwing.getRequestId()returned""rather thannullwhen the header was absent, and the fallback error type wasunknownrather thanunknown_error.HttpInvalidInputErrornever actually overrode the error code — it wrote to a dynamic property because the parent's wasprivate— and the action attempt errors wrote to an undeclared$nameproperty (a PHP 8.2 deprecation that CI would have hit on 8.5).Paginator::firstPage()indexed its cache unconditionally, and the null cursor guard was unreachable because the parameter was typed non-nullable.Two more came out of writing the tests: the
SEAM_API_KEYenvironment variable was overriding an explicitly passed personal access token, and Guzzle resolves its handler stack in reverse, so a history middleware pushed by a caller sits outside the SDK's retry middleware and cannot observe retries at all — which would have made the retry assertions silently vacuous.Deliberate divergences
Both are documented in the README:
guzzle_options, which it was not before.Breaking changes
Covered in the new "Upgrading from 3.x" README section.
svix/svixis a new dependency.Seam\Seam;Seam\SeamClientremains as a deprecated alias.endpointis no longer the second positional argument, andthrow_http_errorsis removed.Seam\Exceptionsnamespace.poll_until_ready()is removed in favor ofwait_for_action_attempt, whose defaults change from 20s/0.4s to 10s/1s.$seam->clientis aSeam\Http\SeamHttpClient; the Guzzle client is available via$seam->client->get_client().$api_keyproperty and the globalLTS_VERSIONconstant are removed.Seam\Paginationobject rather than astdClass.Notes on the diff
src/Seam.phpis the generated client;src/SeamClient.phpis now a three-line handwritten alias. The split is required by PSR-4, and thelinguist-generatedmark moved accordingly.nikic/php-parserv4 while Psalm 6 needs v5. The config was migrated to the 10.5 schema.8.0, 8.5; it is now8.1through8.5, so the versions in between are actually exercised.src/Resourcesandsrc/Routes, matching Ruby's SimpleCov filters — measuring coverage on generated code only creates pressure to test the generator.Verification
Run locally against PHP 8.4:
vendor/bin/phpunit— 89 tests, 150 assertions, all passing against a realfake-seam-connectinstancevendor/bin/psalm— no errorscomposer validate --strict,npm run lint,npx tsc --noEmit— all cleannpm run generatetwice — second run produces an empty diff, so the generator stays idempotent asgenerate.ymlrequiresPlus a smoke script exercising construction via env var, api key,
from_api_key,from_personal_access_token,from_client, and the deprecated alias;unlock_doorwith waiting on, off, and configured; the paginator'sfirstPage/nextPage/flatten; multi-workspaceworkspaces->list(); and a webhook verify round trip.CI is the real check for 8.1 through 8.3 and 8.5, which this environment could not exercise.
Generated by Claude Code