Skip to content

Python SDK 2.4.0 (finance_research, research background mode, source_control, output_schema) - #12

Closed
tyler5673 wants to merge 1 commit into
mainfrom
release/2.4.0
Closed

Python SDK 2.4.0 (finance_research, research background mode, source_control, output_schema)#12
tyler5673 wants to merge 1 commit into
mainfrom
release/2.4.0

Conversation

@tyler5673

Copy link
Copy Markdown
Contributor

Summary

  • Speakeasy regen of You.com Python SDK at version 2.4.0.
  • 166 files changed (+8654 / −1723), single commit.

Added

  • Finance Research API: you.finance_research(...) over a finance-optimized index (SEC filings, transcripts, analyst coverage, news). Only deep and exhaustive efforts.
  • Research background mode: you.research(..., background=True) returns a TaskResponse. Submit without blocking.
  • Research task poll / stream: you.get_research_task(task_id) returns a TaskDetail; you.stream_research_task(task_id) returns a typed SSE EventStream.
  • Research source_control: optional include_domains / exclude_domains / boost_domains / freshness / country.
  • Research output_schema: structured JSON output, with output.content typed Union[str, object].
  • Search boost_domains: ranks BUT does NOT restrict (max 500 domains).
  • Contents max_age: int, nullable, seconds, default null.
  • Hand-maintained helpers in src/youdotcom/research_helpers.py (NOT touched by Speakeasy regen):
    • research_background[_async] — typed TaskResponse, drop-in for narrow.
    • poll_research_task[_async] — wait until terminal state.
    • research_and_wait[_async] — submit+wait (poll or stream), returns final TaskDetail.
    • stream_research_events_raw[_async] — tolerant SSE iterator that accepts non-enum event names (research.searching, etc.) and returns RawStreamEvent.
  • YDCUserAgentOverrideHook now respects sdk_configuration.user_agent so integrations (langchain-youdotcom, youdotcom-temporal, n8n-nodes-youdotcom) can identify themselves with simply client.sdk_configuration.user_agent = "<integration>/<version>" instead of swapping hooks.

Changed

  • Research 200 is Union[ResearchResponse, TaskResponse].
  • Research.output.content is Union[str, object] for output_schema responses.
  • ResearchEffortResearchResearchEffort rename (collision with FinanceResearchResearchEffort).
  • livecrawl_formats strictly Optional[List[LiveCrawlFormats]].
  • Shared 401/403/422 errors consolidated to UnauthorizedResponseError / ForbiddenResponseError / UnprocessableEntityResponseError. Per-endpoint classes (ResearchUnauthorizedError, FinanceResearchUnprocessableEntityError, etc.) still raised.

Tests: 98 mock/perf tests pass against tests/mockserver on :18080. 15 live tests cleanly skip (no YOU_API_KEY_AUTH).

Checklist

  • Speakeasy generation ran successfully (speakeasy run against local spec overrides for PLT-1928 spec drift pass).
  • Version updated in pyproject.toml, gen.yaml, _version.py (all 2.4.0).
  • CHANGELOG.md updated (## [2.4.0] - 2026-07-09).
  • MIGRATION.md updated (## 2.3.0 → 2.4.0 (Latest)); ## 1.x → 2.3.0 retained.
  • README.md / USAGE.md / docs/ verified (auto-regen).
  • Mockserver wired for all 4 new endpoints (POST /v1/finance_research, POST /v1/research background case, GET /v1/research/{task_id}, GET /v1/research/{task_id}/stream).
  • Tests updated; full suite green.

Migration notes for downstream users

See MIGRATION.md. Key points:

# Rename
from youdotcom.models import ResearchResearchEffort  # was ResearchEffort

# list of formats, not single value
you.search.unified(query="...", livecrawl_formats=[LiveCrawlFormats.MARKDOWN])

# Background mode -> Union response
res = you.research(input="...", research_effort=ResearchResearchEffort.DEEP, background=True)
# res is TaskResponse

# Or use the helper
from youdotcom.research_helpers import research_background, research_and_wait
task = research_background(you, input="...", research_effort=ResearchResearchEffort.DEEP)
detail = research_and_wait(you, mode="poll", input="...", research_effort=ResearchResearchEffort.DEEP)

Outstanding at merge time

  • Frontend PR youdotcom #12477 (PLT-1928) is draft, unmerged at the time of authoring this PR. It introduces the canonical openapi_finance_research.yaml spec and the background-mode + SSE surface this SDK depends on. If #12477 is not merged before this PR is merged, releases publishing against https://you.com/specs/... public URLs will produce a diff against this commit (the public URLs will still serve the older 5 specs). The SDK source under src/ already contains all 2.4.0 work.
  • Once #12477 merges: a follow-up regen-from-public-URLs is expected to produce a near-empty diff against this commit. If regen produces a non-empty diff, investigate before merging; the hand-maintained files (src/youdotcom/research_helpers.py, src/youdotcom/_hooks/registration.py, tests/test_research_helpers.py) should NOT vanish in that diff.
  • Live API smoke coverage: tests/test_live.py runs against the real API and is skipped without YOU_API_KEY_AUTH. Recommended smoke run before merging: export YOU_API_KEY_AUTH="..." && .venv/bin/python -m pytest tests/test_live.py -v. Long efforts (DEEP, EXHAUSTIVE) may exceed default pytest timeout — pass --timeout=600.

@tyler5673
tyler5673 force-pushed the release/2.4.0 branch 6 times, most recently from bd33525 to a17ae2a Compare July 10, 2026 19:24
Comment thread MIGRATION.md
Comment on lines +92 to +111
```python
res = you.research(
input="Are Acme Logistics DE and Acme Logistics NJ the same entity?",
output_schema={
"type": "object",
"properties": {
"same_entity": {"type": "boolean"},
"confidence": {"type": "number"},
"evidence": {"type": "array", "items": {"type": "string"}},
},
"required": ["same_entity", "confidence", "evidence"],
},
)
assert res.output.content_type.value == "object"
# `content` is now a dict, not a str
verdict = res.output.content # type: dict
print(verdict["same_entity"])
```

Code that does `res.output.content.lower()` or similar string-only operations will still work for typical text responses (the value remains a `str`), but if you opt into `output_schema` you must branch on `content_type` before calling string methods.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This output_schema example (and the trailing "Code that does res.output.content.lower()..." paragraph) is placed under the "Environment variable renamed to YDC_API_KEY" heading, but it documents structured output — it belongs under "#### Research output.content is now Union[str, object]" (line 75), which currently has only prose and no example. As written, the env-var migration section confusingly ends with an unrelated structured-output snippet. Please move this block up under the output.content heading.

@tyler5673
tyler5673 force-pushed the release/2.4.0 branch 2 times, most recently from 64fa89c to 7443a06 Compare July 10, 2026 20:33
Comment on lines +411 to +415
results = you.search.unified(
query="latest advances in fusion energy research",
count=5,
boost_domains=["nature.com", "science.org", "arxiv.org"],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

search.unified (GET /v1/search) types boost_domains as Optional[str] — a single comma-separated value (see search.py:29 and SearchRequest.boost_domains: Optional[str] in models/searchop.py:129). Passing a list here builds SearchRequest(boost_domains=[...]), which raises a pydantic ValidationError at request time.

Either pass a comma-separated string, or use the POST variant you.search_post(..., boost_domains=[...]), whose SearchRequestBody.boost_domains is Optional[List[str]].

Suggested change
results = you.search.unified(
query="latest advances in fusion energy research",
count=5,
boost_domains=["nature.com", "science.org", "arxiv.org"],
)
results = you.search.unified(
query="latest advances in fusion energy research",
count=5,
boost_domains="nature.com,science.org,arxiv.org",
)

@tyler5673
tyler5673 force-pushed the release/2.4.0 branch 5 times, most recently from c355d90 to 1ce3dac Compare July 10, 2026 22:32
Comment on lines +244 to +263
# Stream mode: poll until terminal event OR deadline, then fetch the
# final detail. Track elapsed time against the user's ``timeout_s`` so
# a stuck stream can't block forever.
for evt in stream_research_events_raw(client, task.task_id):
name = evt.event
if name in {"response.done", "complete"}:
return poll_research_task(
client,
task.task_id,
interval_s=interval_s,
timeout_s=max(interval_s, deadline - time.monotonic()),
)
if name in {"error", "cancelled"}:
raise RuntimeError(
f"research task {task.task_id} ended in non-completed state: {name}"
)
if time.monotonic() >= deadline:
raise TimeoutError(
f"research task {task.task_id} did not complete within {timeout_s}s"
)

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 deadline check (line 260) only runs after an event is received. If the server holds the SSE connection open but stops emitting events — or emits only keepalive comment lines (:...), which _parse_event drops with data_required=False and never yields — the for evt in ... loop blocks on the network read and the timeout_s deadline is never evaluated. In that case only httpx's per-read timeout (timeout_ms, reset by each keepalive byte) bounds the wait, not timeout_s.

So the comment "a stuck stream can't block forever" overstates the guarantee: timeout_s is enforced only while terminal-less events keep flowing. Consider either documenting that timeout_s bounds inter-event progress (not wall-clock on an idle socket), or enforcing wall-clock deadline independently of event arrival (e.g. a watchdog / read timeout derived from remaining budget). Same pattern in research_and_wait_async (lines 296–312).

Comment on lines +296 to +312
async for evt in stream_research_events_raw_async(client, task.task_id):
name = evt.event
if name in {"response.done", "complete"}:
return await poll_research_task_async(
client,
task.task_id,
interval_s=interval_s,
timeout_s=max(interval_s, deadline - time.monotonic()),
)
if name in {"error", "cancelled"}:
raise RuntimeError(
f"research task {task.task_id} ended in non-completed state: {name}"
)
if time.monotonic() >= deadline:
raise TimeoutError(
f"research task {task.task_id} did not complete within {timeout_s}s"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor resource-cleanup nuance: when this async for returns early on a terminal event (line 299), the outer async generator stream_research_events_raw_async is left suspended. Its contextlib.aclosing(...) cleanup (which closes the underlying httpx SSE response) only fires when the generator is aclose()d — which here happens via GC-driven finalization, exactly the non-deterministic path the module docstring/comment (lines 479–481) warns against. The sync version is fine (refcount closes it promptly), but the async early-return can leak the connection until GC. Wrapping the iteration in contextlib.aclosing(stream_research_events_raw_async(...)) here would make closure deterministic.

Comment on lines +15 to +16
class Content(BaseModel):
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Root cause of the biggest usability gap in this release (well-documented in CHANGELOG/MIGRATION/tests, flagging for visibility): Content is a field-less model and BaseModel uses pydantic's default extra="ignore", so when content_type="object" the server's structured payload is silently dropped and output.content becomes an empty Content(). That means the headline output_schema feature returns nothing usable through the typed path — callers must re-issue a synchronous call to recover it (per MIGRATION). Same root cause makes TaskDetail.result empty for background research.

This is generated from the oneOf: [object, string] in the spec, so the durable fix belongs in overlays/python_overlay.yaml: give the object branch additionalProperties: true (or x-speakeasy-type/free-form map) so it generates as Dict[str, Any] and the structured content survives unmarshaling. Worth doing before promoting output_schema out of beta, since today the feature is effectively non-functional via the SDK.

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review: 2.4.0 — Finance Research, background/streaming research, output_schema/source_control, env-var rename

Focused on the manually-maintained surface (tests, CHANGELOG, MIGRATION, overlay, pyproject, and the hand-maintained research_helpers.py / hooks / security.py). Overall this is a high-quality, unusually well-documented PR — CHANGELOG and MIGRATION are candid about limitations, hand-edits are explicitly flagged as regen-fragile, and test coverage for the new helpers is thorough (sync + async, success/timeout/failed, poll + stream, tolerant SSE decode, UA hook). Nice work.

Worth addressing before promoting output_schema out of beta

  • output_schema structured output is dropped by the typed SDK (models/researchresponse.py:15). Content is a field-less model and BaseModel uses pydantic default extra="ignore", so content_type="object" responses come back as an empty Content(). The headline feature is effectively non-functional through the SDK — the documented workaround is to re-issue a synchronous call. Same root cause empties TaskDetail.result for background research (which is why research_and_wait cannot return typed results). To your credit this is documented in CHANGELOG, MIGRATION, research_helpers.py, and asserted in tests — but the durable fix belongs in overlays/python_overlay.yaml (make the object branch additionalProperties: true -> Dict[str, Any]) so it survives regen. Inline posted.

Minor / nits

  • research_and_wait stream-mode timeout (research_helpers.py:244): the timeout_s deadline is only checked when an event arrives, so an idle/keepalive-only stream is not bounded by timeout_s (only httpx read timeout). The "cannot block forever" comment overstates it. Inline posted.
  • Async SSE early-return cleanup (research_helpers.py:296): early return on a terminal event leaves the async generator aclosing() cleanup to GC — the non-deterministic path the module comment warns against. Wrapping the outer iteration in aclosing() makes closure deterministic. Inline posted.
  • __openapi_doc_version__ regressed 1.0.0 -> 0.0.1 (_version.py), sourced from .speakeasy/out.openapi.yaml info.version: 0.0.1. This flows into the User-Agent string. Likely inherited from the merged finance spec, but confirm it is intentional — it reads like a downgrade.

Verified good

  • Docs accuracy: README / USAGE / examples consistently updated to YDC_API_KEY; no stale YOU_API_KEY_AUTH remains in docs. MIGRATION examples map to real params — source_control, output_schema, research boost_domains (List), search GET boost_domains (comma-separated str), contents max_age, and finance_research correctly omits source_control/output_schema.
  • Breaking changes: livecrawl_formats -> list, Union[ResearchResponse, TaskResponse], consolidated error classes, and the env-var rename are documented in both CHANGELOG and MIGRATION and reflected in updated tests.
  • security.py env precedence (YDC_API_KEY -> YOU_API_KEY_AUTH fallback): correct, hand-edit flagged as regen-fragile, locked in by tests/test_security_env.py.
  • UA override hook: correctly passes a custom user_agent through and falls back to the default otherwise; covered by tests.
  • Security: no hardcoded secrets (test keys only), no injection surface, tolerant SSE decoder is bounded and safe.

Test-coverage gaps (non-blocking)

  • research_and_wait_async (poll and stream) and stream_research_events_raw_async have no direct tests — only their sync counterparts do.
  • _decode_raw_event non-dict-JSON branch (research_helpers.py:72) is untested.

Note: src/youdotcom/** is Speakeasy-generated; review concentrated on hand-maintained files per repo conventions. Could not execute the suite here (sandboxed pip/python + Go mockserver unavailable) — findings are from static review; the changed hand-maintained files parse cleanly.

Comment thread README.md
"nytimes.com",
"wired.com",
], crawl_timeout=10,
RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This generated example is not valid Python — RetryConfig(...) is a positional argument appearing after keyword arguments (crawl_timeout=10,), which is a SyntaxError. It should be passed as the retries= keyword. Since README is Speakeasy-generated, the real fix likely belongs in the overlay/spec (the per-operation example), but the rendered doc is currently broken.

Suggested change
RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False))
crawl_timeout=10,
retries=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False))

Comment on lines +244 to +263
# Stream mode: poll until terminal event OR deadline, then fetch the
# final detail. Track elapsed time against the user's ``timeout_s`` so
# a stuck stream can't block forever.
for evt in stream_research_events_raw(client, task.task_id):
name = evt.event
if name in {"response.done", "complete"}:
return poll_research_task(
client,
task.task_id,
interval_s=interval_s,
timeout_s=max(interval_s, deadline - time.monotonic()),
)
if name in {"error", "cancelled"}:
raise RuntimeError(
f"research task {task.task_id} ended in non-completed state: {name}"
)
if time.monotonic() >= deadline:
raise TimeoutError(
f"research task {task.task_id} did not complete within {timeout_s}s"
)

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 comment says the elapsed-time tracking means "a stuck stream can't block forever," but the deadline check only runs after an event is received. A stream that stays connected but emits nothing (or stalls mid-workflow) blocks in for evt in stream_research_events_raw(...) on the socket read and is bounded only by the httpx read timeout — not by timeout_s. Consider clarifying the comment, or enforcing timeout_s independently of event arrival (e.g. a wall-clock guard around the read). Same pattern in research_and_wait_async.

@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

PR Review: 2.4.0 — Finance Research, background/stream research, YDC_API_KEY

Reviewed with the understanding that src/youdotcom/ is largely Speakeasy-generated; focused on the hand-maintained surface (tests/, research_helpers.py, utils/security.py, CHANGELOG.md, MIGRATION.md, docs, pyproject.toml).

Overall. Solid, well-documented release. The env-var precedence change is clean and tested, the hand-maintained research_helpers module is coherent, and the docs (CHANGELOG/MIGRATION) are unusually thorough — including honest callouts of the extra="ignore" limitation that leaves TaskDetail.result / structured output.content untyped. Test coverage for the new functionality is strong: env precedence (primary/fallback/none/explicit), poll terminal + timeout + failed states (sync & async), tolerant SSE streaming with unknown event names, the User-Agent override hook, finance_research, and the output_schema/source_control beta params. Mockserver fixtures line up with every test name.

Findings (all posted inline):

  1. SemVer — breaking changes shipped as a minor bump (pyproject.toml). MIGRATION.md documents genuine breaking changes for 2.4.0 (livecrawl_formats now requires a list and raises ValidationError on a bare enum; UnprocessableEntityError / SearchForbiddenError / SearchUnauthorizedError removed). The CHANGELOG asserts adherence to SemVer, under which these warrant a MAJOR (3.0.0) bump.

  2. Version mismatch across generation config. .speakeasy/gen.lock has releaseVersion: 2.4.1, while pyproject.toml and .speakeasy/gen.yaml say 2.4.0. Reconcile before publishing.

  3. Broken Python in generated README (README.md:362, retries example). RetryConfig(...) is passed positionally after keyword args — a SyntaxError. Should be retries=RetryConfig(...). Fix likely belongs in the overlay/spec since README is generated. The Summary block header also got mangled to "You.com Finance Research API: Unified API for Express…" with a duplicated finance line — worth fixing at the spec info level.

  4. research_and_wait stream-mode timeout comment is overstated (research_helpers.py). The comment claims a stuck stream cannot block forever, but timeout_s is only checked after each received event; a connected-but-silent stream blocks on the socket read, bounded only by the httpx read timeout. Clarify or enforce the deadline independently. Same in the async variant.

Security. No hardcoded secrets, injection, or unsafe patterns introduced. The YDC_API_KEY -> YOU_API_KEY_AUTH fallback is reasonable and covered by tests/test_security_env.py. Test API keys are placeholder literals.

None of the above are hard blockers except the version reconciliation (item 2) and the SemVer decision (item 1), which are worth resolving before release.

@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
@youdotcom-oss youdotcom-oss deleted a comment from claude Bot Jul 10, 2026
Adds Finance Research API, Research background mode (GET poll / SSE stream),
Research source_control and output_schema, Search boost_domains, and Contents
max_age.

Breaking (with documented migration):
- New 'FinanceResearchEffort' enum (DEEP, EXHAUSTIVE) joins existing
  'ResearchEffort'. Both names preserved cleanly via named-component refs
  in the OpenAPI specs (no 'ResearchResearchEffort' doubling).
- livecrawl_formats now strictly typed as Optional[List[LiveCrawlFormats]].
- 'you.research()' response is now 'Union[ResearchResponse, TaskResponse]'
  (TaskResponse returned when background=True).
- 'output.content' is now 'Union[str, object]' for output_schema responses.
- Shared 401/403/422 error shapes consolidated to named '*ResponseError'
  classes (per-endpoint classes still raised).

'workflow.yaml' references 'https://you.com/specs/openapi_finance_research.yaml'
even though that URL is not yet live (frontend PR youdotcom #12477 is
unmerged). A regen from public URLs is therefore expected to produce a
diff against this commit until #12477 lands. The SDK source under 'src/'
contains all 2.4.0 work as of this commit. Two spec-side fixes on #12477
make this regen match cleanly when it lands:
  - 'ResearchEffort' and 'FinanceResearchEffort' promoted to named
    components (avoids Speakeasy disambiguator doubling the names).
  - 'ResearchTaskStreamEvent.event' enum widened to include the server's
    synthetic fallback names ('completed', 'failed') for stale RabbitMQ
    streams, in addition to the documented 'connected', 'response.done',
    'complete', 'error', 'cancelled'.

Post-generation work (hand-maintained, not regenerated):

- src/youdotcom/research_helpers.py adds research_background[_async],
  poll_research_task[_async], research_and_wait[_async], and tolerant
  stream_research_events_raw[_async] helpers for background-mode workflows.
- src/youdotcom/_hooks/registration.py's YDCUserAgentOverrideHook now
  respects custom 'sdk_configuration.user_agent' instead of
  unconditionally rewriting the header.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Comment on lines 21 to +34
x-speakeasy-name-override: create
# Allow extras on the anonymous object branch of `output.content` so
# `output_schema=` requests preserve the structured payload through SDK
# unmarshal (today: `class Content(BaseModel): pass` drops everything
# due to pydantic `extra="ignore"`). Reviewed as part of Step 4i-9.
- target: $["components"]["schemas"]["ResearchResponse"]["properties"]["output"]["properties"]["content"]["oneOf"][1]
update:
additionalProperties: true
# Same fix for `TaskDetail.result` so background-mode research surfaces
# the typed `ResearchResponse` payload (today: `class Result(BaseModel): pass`
# round-trips as an empty dict).
- target: $["components"]["schemas"]["TaskDetail"]["properties"]["result"]
update:
additionalProperties: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These two new overlay actions are not applied in this PR — they're a no-op as checked in.

I verified against the regenerated artifacts:

  • .speakeasy/out.openapi.yaml still has ResearchResponse.output.content.oneOf[1] as bare {type: object} and TaskDetail.result as {type: object, nullable: true} — no additionalProperties: true on either.
  • The generated models are unchanged: class Content(BaseModel): pass and class Result(BaseModel): pass (both inherit pydantic's default extra="ignore").

The original v1.0.0 overlay actions (x-speakeasy-name-override) are present in out.openapi.yaml, so the compiled spec is post-overlay — it was just regenerated against the overlay before these two actions were added (overlay bumped to 1.1.0 but no subsequent regen).

Net effect: the structured-output / TaskDetail.result payload-dropping bug these actions target is still live, exactly as the CHANGELOG and MIGRATION caveats describe. Before merge, please either (a) regenerate so the fix lands — and then update the CHANGELOG "Hand-maintained additions" + MIGRATION output_schema caveats that currently say it's broken, or (b) drop these actions so the overlay doesn't imply a fix that isn't shipped. Worth also confirming the JSONPath actually matches after regen (the oneOf[1] index in particular).

Comment thread README.md
Comment on lines +355 to +362
res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[
"spam-site.com",
"other-site.com",
], boost_domains=[
"nytimes.com",
"wired.com",
], crawl_timeout=10,
RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This generated snippet is invalid Python: RetryConfig(...) is a positional argument placed after keyword arguments (crawl_timeout=10, RetryConfig(...)), which is a SyntaxError. It should be retries=RetryConfig(...).

This is a Speakeasy code-sample generation artifact (the per-method example changed from research to search_post), not a hand edit — but it ships in the README, so worth being aware of. A regen with the overlay fix (see the overlay comment) may also refresh this; otherwise consider whether the example operation choice can be pinned.

interval_s=interval_s,
timeout_s=timeout_s,
)
# Stream mode: poll until terminal event OR deadline, then fetch the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: the comment says "so a stuck stream can't block forever," but the deadline is only checked after each event is received. If the SSE connection stays open but emits no events, the for evt in ... loop blocks on the socket read up to the httpx client timeout, not timeout_s. The timeout guarantee only holds while events keep arriving. Consider softening the comment, or wrapping the stream read with a real deadline if a hard timeout_s bound matters here.

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review summary — v2.4.0 (Finance Research + background research + SSE)

Overall this is a solid, well-documented release. The hand-maintained pieces (tests, CHANGELOG, MIGRATION, overlay, env-var precedence, UA hook) are thoughtful and the CHANGELOG/MIGRATION are unusually honest about current limitations. Test coverage for the new surface is strong. One issue should be resolved before merge; the rest are minor.

🔴 Should fix before merge

1. The overlay's output.content / TaskDetail.result fix is a no-op as checked in. (overlays/python_overlay.yaml, inline comment)
python_overlay.yaml was bumped to 1.1.0 with two additionalProperties: true actions to stop Content/Result from dropping structured payloads — but neither .speakeasy/out.openapi.yaml nor the generated models reflect them (class Content(BaseModel): pass, class Result(BaseModel): pass, default extra="ignore"). The original v1.0.0 overlay actions are present in the compiled spec, so it was regenerated against the overlay before these actions were added. The payload-dropping bug is therefore still live — consistent with the CHANGELOG/MIGRATION caveats, but the overlay implies a fix that isn't shipped. Either regenerate (and then update the now-stale "broken" caveats in CHANGELOG "Hand-maintained additions" and the MIGRATION output_schema section), or drop the overlay actions.

🟡 Minor / nits

  • README retries example is invalid Python (README.md:362, inline): you.search_post(query=..., crawl_timeout=10, RetryConfig(...)) — positional arg after kwargs → SyntaxError. Speakeasy code-sample artifact (example op switched from research to search_post), not a hand edit.
  • research_and_wait stream-mode timeout (research_helpers.py:244, inline): the timeout_s deadline is only checked after each event arrives; a stalled-but-open stream blocks on the socket read, not timeout_s. Comment slightly overstates the guarantee.
  • README summary title mangled: now reads "You.com Finance Research API: Unified API for Express, Advanced, and Custom Agents…" — the finance spec's title overrode the base spec title when it was added last. Cosmetic, auto-generated.
  • __openapi_doc_version__ regressed 1.0.00.0.1 (_version.py), picked up from the finance spec's version. Cosmetic.
  • Stale comment in Go mock (pathpostv1financeresearch.go:36): says FinanceResearchSource uses extra="forbid", but the generated model is a plain BaseModel (default extra="ignore") with url/title. Harmless.

✅ Verified good

  • Env-var precedence (security.py + tests/test_security_env.py): YDC_API_KEY primary, YOU_API_KEY_AUTH fallback, explicit-security override — all covered; backward compatible.
  • UA override hook (registration.py): correctly passes through a custom sdk_configuration.user_agent while keeping the default; default is __user_agent__ so is_custom is False out of the box. Both paths tested.
  • research_helpers tests: background→TaskResponse, poll (sync/async), timeout→TimeoutError, failed→RuntimeError, stream-mode submit→stream→poll ordering, tolerant SSE decoder surfacing unknown event names, _decode_raw_event units. Good breadth.
  • Breaking changes (livecrawl_formats list requirement, Union[ResearchResponse, TaskResponse], Union[str, object] content, consolidated error classes, env-var rename) are all documented in both CHANGELOG and MIGRATION with before/after examples; error-class renames reflected in test_search.py/test_research.py.
  • Security: no hardcoded real secrets — test keys only (test-api-key, invalid, etc.). X-API-Key header used; no injection surface introduced.
  • pyproject.toml: version bump + pydantic upper bound <2.13 are reasonable.

Note: I couldn't execute the test suite in this environment (sandboxed), so the test verification above is by inspection of the fixtures/handlers, which line up with the referenced x-speakeasy-test-name cases.

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