Skip to content

fix: apply review-recommended fixes for 2.3.0 - #8

Merged
tyler5673 merged 2 commits into
release/2.3.0from
fix/claude-review-2.3.0
Mar 3, 2026
Merged

fix: apply review-recommended fixes for 2.3.0#8
tyler5673 merged 2 commits into
release/2.3.0from
fix/claude-review-2.3.0

Conversation

@tyler5673

@tyler5673 tyler5673 commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Applies non-generated fixes for the 2.3.0 release:

  • README.md: add missing imports to the streaming SSE example (ExpressAgentRunsRequest, WebSearchTool, eventstreaming, all Response* event types were used but never imported). fix run_time_ms unit label ("seconds" -> "ms")
  • MIGRATION.md: add 2.3.0 breaking-changes section covering Python >=3.10 requirement, Search.count new default of 10, and crawl_timeout int type
  • tests/test_research.py: replace misleading if-guard on required sources field with explicit assert; add len > 0 check to async test

Dropped from original PR

Changes to speakeasy-generated files (retries.py, security.py, sdkconfiguration.py) were reverted because:

  1. All three files are marked "Code generated by Speakeasy. DO NOT EDIT." and tracked in .speakeasy/gen.lock — patches get silently overwritten on the next speakeasy generate run
  2. The retries.py conditional restructure (if flag: raise -> if not flag: PermanentError) was logically equivalent — not an actual bug fix
  3. The retries.py error-swallowing removal and sdkconfiguration.py pydantic.Field -> dataclasses.field fix are real but edge-case issues that should be reported upstream to Speakeasy so they're fixed in the generator itself
  4. The security.py f-string prefixes only affect ValueError messages in error paths that don't fire during normal operation

Test plan

  • Verify README streaming example runs without NameError on missing imports
  • Confirm MIGRATION.md 2.3.0 section accurately reflects breaking changes on release/2.3.0
  • Run pytest tests/test_research.py against live API to validate stronger assertions

- README.md: add missing imports to streaming SSE example
  (ExpressAgentRunsRequest, WebSearchTool, eventstreaming, all
  ResponseXxx event types were used but not imported)
- README.md: fix run_time_ms unit label ("seconds" → "ms")
- retries.py: fix inverted retry_connection_errors flag logic (sync+async)
  When True the flag now retries; when False it raises PermanentError
- retries.py: raise instead of returning error response on max_elapsed_time
  exceeded so callers are not silently handed a non-2xx httpx.Response
- security.py: add missing f-prefix to three non-interpolating ValueError
  messages so the actual sub_type/scheme_type values appear in errors
- sdkconfiguration.py: replace pydantic.Field with dataclasses.field so
  retry_config default_factory is evaluated correctly at dataclass init
- MIGRATION.md: add 2.3.0 breaking-changes section covering Python ≥3.10
  requirement, Search.count new default of 10, and crawl_timeout int type
- tests/test_research.py: replace misleading if-guard on required
  sources field with explicit assert; add len > 0 check to async test

Co-authored-by: Tyler Eastman <tyler5673@users.noreply.github.com>
if isinstance(exception, TemporaryError):
return exception.response

raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavioral change that may surprise callers: max_elapsed_time exhaustion now always raises.

Before this change, when all retry time was exhausted and the last error was a TemporaryError (e.g. a 429 or 503 response), the function would silently return that non-2xx httpx.Response. Any caller that checked response.status_code after a retry-exhausted request would see the error without hitting an exception.

After this change, the function unconditionally raises — callers that relied on the old return-on-timeout pattern will now receive an unhandled exception.

The new behavior is more correct (returning a failed response silently is bad), but this is a de-facto behavioral breaking change. Consider:

  1. Documenting this in the CHANGELOG under 2.3.0 "Changed".
  2. Making sure the SDK's own exception-handling layer converts the re-raised TemporaryError into a user-facing typed error (e.g. YouDefaultError) rather than leaking the internal TemporaryError class to callers.

if isinstance(exception, TemporaryError):
return exception.response

raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async path has the same max_elapsed_time change (see retry_with_backoff_async). Same CHANGELOG note applies.

Comment thread src/youdotcom/utils/retries.py Outdated
except (httpx.NetworkError, httpx.TimeoutException) as exception:
if retries.config.retry_connection_errors:
raise
if not retries.config.retry_connection_errors:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix — logic was completely inverted.

Before:

if retry_connection_errors:   # True → raise (prevented retry!)
    raise
raise PermanentError(...)     # False → also prevented retry

Both branches prevented retrying.

After this fix, when retry_connection_errors=True the raw NetworkError/TimeoutException propagates up to the outer retry_with_backoff loop, where it will be caught, the back-off sleep applied, and the request retried. When retry_connection_errors=False it wraps in PermanentError to short-circuit the loop immediately. Correct.

One minor nit: it would help to add a one-line comment explaining why we re-raise the unwrapped exception here vs. wrapping in PermanentError, since the asymmetry is non-obvious:

Suggested change
if not retries.config.retry_connection_errors:
except (httpx.NetworkError, httpx.TimeoutException) as exception:
if not retries.config.retry_connection_errors:
# Wrap so the outer loop treats this as non-retryable
raise PermanentError(exception) from exception
# Let the raw error propagate so the outer loop retries it
raise

Comment thread tests/test_research.py
if res.output.sources:
for source in res.output.sources:
assert source.url is not None
assert res.output.sources is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good strengthening of the assertion. The previous if res.output.sources: guard was effectively a no-op test — if the mock server returned sources=None or [] the test would silently pass without verifying anything. The explicit assert now catches that.

However, these tests depend on the mock server handling POST /v1/research, which it does nottests/mockserver/internal/handler/generated_handlers.go only registers handlers for /v1/search, /v1/agents/runs, and /v1/contents. A request to /v1/research hits the fallback root handler (404 Not Found), so the SDK raises a YouDefaultError and none of these assertions are ever reached. The tests should either:

  1. Be skipped with pytest.mark.skip until the mock server is extended, or
  2. Be converted to live-only tests guarded by pytestmark = pytest.mark.skipif(not os.getenv("YOU_API_KEY_AUTH"), ...)

The mock server binary also needs to be rebuilt for Linux (see the separate comment on the binary).

Comment thread tests/test_research.py
assert isinstance(res, ResearchResponse)
assert res.output is not None
assert res.output.content is not None
assert len(res.output.content) > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent with the sources fix above — good to add this. The same mock server gap applies here: test_basic_research_async will also fail against the mock server since /v1/research returns 404.

Comment thread src/youdotcom/sdkconfiguration.py Outdated
gen_version: str = __gen_version__
user_agent: str = __user_agent__
retry_config: OptionalNullable[RetryConfig] = Field(default_factory=lambda: UNSET)
retry_config: OptionalNullable[RetryConfig] = field(default_factory=lambda: UNSET)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix. SDKConfiguration is a @dataclass, and Pydantic's Field(default_factory=...) only works inside Pydantic BaseModel subclasses. Using it in a plain dataclass would set the default value to the FieldInfo object itself rather than calling the factory, meaning retry_config would always be a FieldInfo rather than UNSET. Switching to dataclasses.field(default_factory=...) is correct.

Worth noting: this is in auto-generated source (# Code generated by Speakeasy). You should verify that the upstream Speakeasy template has been patched so it doesn't regenerate the broken version on the next SDK refresh.

Comment thread src/youdotcom/utils/security.py Outdated
query_params[header_name] = [value]
else:
raise ValueError("sub type {sub_type} not supported")
raise ValueError(f"sub type {sub_type} not supported")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix. All three ValueError messages were missing the f prefix, so they would show the literal string "sub type {sub_type} not supported" instead of embedding the actual value. These are important for debugging unexpected security scheme configurations.

Since this is auto-generated code, same note as sdkconfiguration.py: verify the Speakeasy template is fixed upstream.

Comment thread README.md
elif isinstance(event_data, ResponseDone):
print("\n🎉 Response completed!")
print(f" Runtime: {event_data.response.run_time_ms} seconds")
print(f" Runtime: {event_data.response.run_time_ms} ms")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. run_time_ms is in milliseconds, not seconds. The unit label "seconds" was misleading — "ms" is correct.

One small style note: the rest of the streaming example output uses emoji (🔍, ✍️, 🌐, 🎉) which is fine for illustration, but the label should match the field name to avoid confusion. run_time_ms → "ms" is the right fix.

Comment thread MIGRATION.md

---

## 1.x to 2.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2.3.0 section is useful and well-structured.

Two minor issues to fix:

  1. Broken link to examples/: Line 331 links to examples/ but only examples/api-example-calls.py exists (not a directory). Consider changing to [examples/api-example-calls.py](examples/api-example-calls.py) or removing the link.

  2. Missing retries behavioral change: The retries fix (max_elapsed_time exhaustion now raises instead of returning the failed response) is a behavior change that existing users relying on response-code checking after retries might hit. Worth a brief note in this section.

Suggested change
## 1.x to 2.0
---
## 1.x to 2.0

@claude

claude Bot commented Mar 3, 2026

Copy link
Copy Markdown

PR Review Summary

This PR applies a focused set of bug fixes and doc improvements for the 2.3.0 release. The individual changes are all correct and well-targeted.


Approved changes

File Change Verdict
retries.py Inverted retry_connection_errors logic fixed correct
retries.py Raise instead of silently returning failed response after max retries correct
sdkconfiguration.py pydantic.Field replaced with dataclasses.field in a plain dataclass correct
security.py Missing f-prefix on three ValueError messages correct
README.md Missing imports in streaming example added correct
README.md run_time_ms unit label "seconds" fixed to "ms" correct
test_research.py if sources: guard replaced with explicit assert correct
MIGRATION.md 2.3.0 breaking-changes section added good

Issues requiring follow-up

1. (Must fix) Mock server missing /v1/research handler

tests/mockserver/internal/handler/generated_handlers.go only registers three routes (/v1/search, /v1/agents/runs, /v1/contents). Requests to /v1/research hit the fallback 404 handler, so all tests in test_research.py fail against the mock server — the stronger assertions added in this PR are never reached in CI. The mock server Go source needs a pathPostV1Research handler and binary rebuild.

2. (Must fix) Mock server binary is macOS/ARM64 — won't run on Linux CI

tests/mockserver/mockserver is a Mach-O 64-bit arm64 executable; it cannot execute on a Linux runner. The Dockerfile builds from source (correct), but the committed binary itself should be removed and .gitignore'd, or a cross-compiled linux/amd64 binary provided.

3. (Should fix) retries.py max_elapsed_time raise is an undocumented behavioral change

Removing the silent return exception.response after exhausting retry time is the right fix, but it changes observable behavior: callers that previously checked response.status_code after a long operation will now receive an unhandled exception instead. This should be noted in the CHANGELOG under 2.3.0 "Changed".

4. (Should fix) Auto-generated files with manual patches

sdkconfiguration.py and security.py carry # Code generated by Speakeasy. DO NOT EDIT. headers. The fixes are correct, but they will be reverted on the next Speakeasy regeneration cycle unless the upstream Speakeasy templates are also patched.

5. (Nice to have) CHANGELOG.md typo on line 176

The "After (2.0)" import example shows ContentsFormat, (missing trailing s). The actual exported class is ContentsFormats.

6. (Nice to have) MIGRATION.md broken link

Line 331 links to examples/ as a directory, but only examples/api-example-calls.py exists (not a directory).


Summary

The core bug fixes (retries logic, dataclass field, f-string errors) are all correct and needed. The main blockers before merging to release are the mock server gaps — the research tests added to verify 2.3.0 behavior will silently not run until /v1/research is handled by the mock server and the binary is rebuilt for Linux.

Reverts changes to retries.py, security.py, and sdkconfiguration.py.
These files are marked "DO NOT EDIT" and tracked in .speakeasy/gen.lock,
so any patches here get silently overwritten on the next speakeasy
generate run. The fixes should be reported upstream to Speakeasy instead.

Specifics:
- retries.py conditional restructure was a no-op (logically equivalent)
- retries.py error-swallowing removal is a real but edge-case change
- security.py f-string prefixes only affect error-path messages
- sdkconfiguration.py pydantic.Field vs dataclasses.field has not
  caused reported issues in practice

Keeps the non-generated fixes: README.md imports/label, MIGRATION.md
2.3.0 section, and test_research.py assertion improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tyler5673
tyler5673 merged commit b63f8b9 into release/2.3.0 Mar 3, 2026
2 checks passed
@tyler5673
tyler5673 deleted the fix/claude-review-2.3.0 branch March 3, 2026 23:46
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