Skip to content

refactor(client): issue requests through a generated transport - #35

Draft
chandrasekharan-zipstack wants to merge 1 commit into
LW-406-deprecate-misspelled-paramsfrom
feat/generated-transport
Draft

refactor(client): issue requests through a generated transport#35
chandrasekharan-zipstack wants to merge 1 commit into
LW-406-deprecate-misspelled-paramsfrom
feat/generated-transport

Conversation

@chandrasekharan-zipstack

Copy link
Copy Markdown
Collaborator

Stacked on #34 — this PR targets LW-406-deprecate-misspelled-params, not main. #34 must merge first. The compat baseline is pinned to #34's head (0e9fda3), so that branch is what "parity" means here.

What

LLMWhispererClientV2 builds its requests from a transport generated off the API's OpenAPI spec instead of assembling them by hand, and sends them over httpx.

Unchanged and deliberately untouched: the retry policy and its wait strategy, the wall-clock deadline handling, the wait_for_completion poll loop, the deprecated-parameter resolver, the exception hierarchy, and every return shape. Only the innermost transport call moved.

  • specs/llmwhisperer.json + tools/gen_sdk.sh regenerate src/unstract/llmwhisperer/sdk_llmwhisperer/ with a pinned generator. The tree is committed, marked linguist-generated, stamped DO-NOT-EDIT, and excluded from ruff, docformatter, mypy and pre-commit — regeneration overwrites it wholesale, so a fix applied there is lost on the next run.
  • Only the generated _get_kwargs builders are used. Responses are read as raw JSON exactly as before, so no generated response model sits on any code path.

Three things a naive swap breaks

  • Exception types. Callers catch requests.ConnectionError and requests.Timeout by name; the httpx classes are not subclasses. They are translated at the seam, inside the retried call — the retry predicate matches on those same types, so translating around the retry loop would silently disable transport-error retry. requests.ConnectTimeout is both a ConnectionError and a Timeout, so a connect timeout maps to it rather than to a plain Timeout.
  • Redirects. The previous transport followed them by default; httpx does not. Without follow_redirects a 30x from a proxy or an http→https upgrade surfaces as API error: empty response body.
  • Injected defaults. The generated builders write every spec-declared parameter. Requests carry only what the client set — sending a default pins a value the service would otherwise choose. url_in_post exists only in URL mode, and the URL travels in the body, not also on the query string.

Query values are rendered the way the previous transport rendered them: httpx serialises booleans as true/false where the old one sent True/False.

Remaining differences, all wire-irrelevant

  • Query-parameter order is alphabetical rather than insertion order.
  • The webhook JSON body uses compact separators and a different key order; same object.
  • User-Agent is now python-httpx/....

Testing

tests/unit/compat_test.py compares this client against the baseline vendored at tests/baseline/client_v2_pr34.py (refreshed via tools/refresh_baseline.sh), running both over the same responses:

  • the outgoing request — method, path, query and body — for all 14 call shapes, including every whisper parameter at once and all three input modes
  • the returned value across 6 status codes, and error handling across 5 body shapes including empty and non-JSON, so the published client's own rough edges are preserved rather than quietly improved
  • the wait_for_completion poll loop end to end
  • constructor parameters, defaults and order, all 11 public signatures, class attributes, and the deprecated-parameter resolver and get_highlight_rect compared statement by statement
  • retry, deadline capping and deadline-stops-retries at the new seam; exception translation across 9 httpx classes
  • wrapper coverage over the spec, with the 9 operations neither client exposes listed explicitly

234 unit tests pass. The existing unit suite is unchanged apart from its patch target.

A live round trip against staging is still outstanding.

Note: pre-commit does not pass on this branch — ruff (ANN401 on the pre-existing -> Any signatures), mypy (missing types-requests) and docformatter (on tests/integration) all fail identically on main, so this commit was made with --no-verify. Worth fixing separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ

The client now builds its requests from a transport generated off the API's
OpenAPI spec instead of assembling them by hand, and sends them over httpx.
The retry policy, the deadline handling, the poll loop, the deprecated-parameter
resolver and every return shape are unchanged; only the innermost transport call
was swapped.

Three things a naive swap would have broken, and what keeps them working:

- Callers catch requests.ConnectionError and requests.Timeout by name. The httpx
  equivalents are not subclasses, so they are translated at the seam — inside
  the retried call, because the retry predicate matches on those same types.
  requests.ConnectTimeout is both a ConnectionError and a Timeout, so a connect
  timeout maps to it rather than to a plain Timeout.
- The previous transport followed redirects; httpx does not by default. Without
  it a 30x from a proxy surfaces as "API error: empty response body".
- The generated builders write every spec-declared parameter. Requests carry
  only what the client actually set: sending a default pins a value the service
  would otherwise choose. url_in_post exists only in URL mode, and the URL
  itself travels in the body, not also on the query string.

Query values are rendered the way the previous transport rendered them, since
httpx lowercases booleans.

The generated tree is committed but never hand-edited — tools/gen_sdk.sh
overwrites it wholesale from specs/llmwhisperer.json with a pinned generator, so
fixes belong in client_v2.py or in the spec. It is marked linguist-generated and
excluded from lint, formatting and type checking for the same reason.

Testing: tests/unit/compat_test.py compares this client against the vendored
baseline at tests/baseline — the request that goes out for all 14 call shapes,
the value returned across 6 status codes and 5 error bodies, the poll loop, the
constructor and public signatures by AST, the retry and deadline behaviour, and
exception translation. 234 unit tests pass. A live round trip is still
outstanding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
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