Skip to content

feat(parallel): traceAI-parallel for Parallel Search and Extract (TH-8326) - #217

Open
nik13 wants to merge 15 commits into
devfrom
feat/th-8326-parallel-ai
Open

nik13 wants to merge 15 commits into
devfrom
feat/th-8326-parallel-ai

Conversation

@nik13

@nik13 nik13 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

New Python package traceAI-parallel (python/frameworks/parallel) for the Parallel web API client (parallel-web). It wraps Parallel.search, Parallel.extract and their AsyncParallel twins, the methods the spec confirms.

  • Spans: one per call, parallel.search / parallel.extract, with fi.span.kind=RETRIEVER. The span is current during the HTTP call, so HTTP spans nest under it, and one span covers the SDK's retries.
  • Recorded by default:
    • mode, query/URL counts, result and failed-URL counts, search/extract and session ids, usage SKU counts when returned
    • warnings as parallel.warning events
    • the search queries in gen_ai.retrieval.query and input.value, with the key redacted first, then capped at about 1 KB (1024 UTF-8 bytes on a character boundary; with pii_redaction, FITracer's second PII pass can add a few bytes)
  • Never recorded: excerpts, titles, result URLs and page text. URLs and the objective are opt-in (capture_urls, capture_objective). TraceConfig hide_inputs / hide_outputs / pii_redaction and FI_HIDE_* / FI_PII_REDACTION are honoured. Spans come from FITracer, so using_session / using_user / using_metadata / using_tags / using_attributes context reaches them. With hide_inputs, input.value is __REDACTED__ (as in Tavily), and every verbatim copy of a search query, requested URL or the objective is replaced with __REDACTED__ in the error status, exception.message, exception.stacktrace and warning messages (a server error can quote the request). Only verbatim copies are matched.
  • API key: redacted from every place the SDK keeps it: api_key, auth/default/custom headers and per-call extra_headers, including in error status and exception events. exception.stacktrace is cut to 16 KB after the key (and PII, when enabled) is removed.
  • Cancellation: ERROR cancelled plus parallel.cancelled=true. Instrumentation failures never reach the caller. uninstrument() restores all four methods by identity.
  • Versions: parallel-web>=1.0.1,<2 (1.0.1 and 1.3.5 tested), fi-instrumentation-otel>=1.1.0, Python 3.10, 3.11, 3.12 and 3.13.

Deviations from the spec (with evidence)

  • Call shape: the docs draft's client.search(query=...) raises TypeError on the real SDK, which takes search_queries=[...]. The README uses the real signature.
  • Version range: >=1.0.1,<2 instead of a single pin. The 1.0.0-1.3.5 wheels have identical search/extract parameters and /v1/* paths; 0.6.0 still used /v1beta.
  • No parallel.api attribute: no 1.x method reaches /v1beta/search (tested).
  • Query key: gen_ai.retrieval.query, the repo's own constant (fi_types.py), rather than Exa's fi.retrieval.query.
  • Excerpt capture: PRD 7's optional 2 KB excerpt capture is not built. Excerpts are never recorded, as in Exa.

Review round 1 and fixes (pr-reviewer t_6f817f29 APPROVE; pr-verifier t_20f2e54e CHANGES_REQUESTED at 7e56fd3)

Finding Commit Fix
R2 (P2): Python versions claimed without runs 8002add Full 3.10-3.13 x parallel-web 1.0.1/1.3.5 matrix below; 3.12 classifier added; packaging test pins it.
R4: unbounded exception.stacktrace 967997c Cut to 16 KB of UTF-8 after key redaction; README lists every size limit.
R1: plain tracer skipped pii_redaction and using_* context b643847 FITracer(tracer, config=config). PII redaction also covers warning messages, the error status and the exception event (FITracer masks attributes only), and runs before the size caps. Side effect, documented and tested: an id with 10 consecutive digits is masked as a phone number.
R3: with_streaming_response timing 4ecdf34 README: the span ends when the headers arrive, not after the body is read; a test pins it.

Tests written first: the final test files against 7e56fd3 gave 12 failed, 78 passed (assertion failures). 12 deliberate breakages of the new code were each caught.

Verification round 2 and fix (pr-verifier t_71379c0d CHANGES_REQUESTED at 4ecdf34: R1-R4 fixed, new N1)

Finding Commit Fix
N1 (P2): with hide_inputs, the query reached the backend through error text (parallel-web puts the whole server body in APIStatusError), while the README said no query text is recorded 846410b As in traceai-tavily: under hide_inputs, every verbatim search query, extract URL and the objective becomes __REDACTED__ in the error status, exception message/stacktrace and warning messages. Order: key, then hidden inputs (one pass, longest first), then PII, then caps. Unreadable inputs under hide_inputs record __REDACTED__ for those texts. Sync and async share the path; with hide_inputs off nothing changes. Decision approved: Nikhil 2026-10-03 blanket.
F1 (P3): caps can be exceeded by a few bytes after FITracer's PII pass fadfee6 README says "about 1 KB" / "about 256 bytes" and why.
F2 (P3): stacktrace cap keeps the head - Left as is; exception.message keeps the message.

Tests written first: the final test files against 4ecdf34 gave 11 failed, 92 passed (10 assertion failures on leaked query/URL/objective text, 1 on the new helper); controls with hide_inputs off pass on both heads.

Tests

Head fadfee6, coordinator rerun from the repo root on a clean tree (tickets/TH-8326/exact-head-fadfee6.txt):

PYTHONPATH="python/frameworks/parallel:python:python/tests" uv run --no-project --python <py> --with pytest --with pytest-asyncio --with opentelemetry-api --with opentelemetry-sdk --with opentelemetry-instrumentation --with opentelemetry-exporter-otlp-proto-http --with wrapt --with requests --with jsonschema --with protobuf --with opentelemetry-proto --with 'parallel-web==<v>' pytest python/frameworks/parallel/tests -q -p no:cacheprovider --noconftest -o addopts= -rs
              parallel-web 1.0.1   parallel-web 1.3.5
Python 3.10   103 passed           103 passed
Python 3.11   103 passed           103 passed
Python 3.12   103 passed           103 passed
Python 3.13   103 passed           103 passed

The round-3 delta (4ecdf34..fadfee6) touches no file outside the package, and no secret-like lines were added. Across the whole branch, the only files outside the package are the package-table rows in README.md (+2) and python/README.md (+1), added in a770cf0. redact_pii_in_string and FITracer are present in the published fi-instrumentation-otel==1.1.0 wheel (checked by import); the suite itself ran on the in-repo fi_instrumentation.

  • The real parallel-web SDK runs against a loopback fake of the Parallel API.
  • The contract test exports through the real fi_instrumentation.register() into the shared harness Receiver. It asserts the /tracer/v1/traces path, both auth headers, project_type=observe, and that no key or content reaches the wire. A control run shows the opt-in capture does arrive.
  • The example runs through harness.run.
  • At 7e56fd3 the implementer also ran the built package against the published fi-instrumentation-otel==1.1.0 and the OTel 1.29.0 floor (79 passed each); those two runs were not repeated at fadfee6.

Limits

  • No live Parallel call and no real fi-collector. Trio-backend cancellation is not run.

Video demo

Narrated terminal demo, 6:01, recorded at head fadfee6, the head verified in round 3 (pr-verifier t_9da92a74). 1080p H.264/AAC, 10 chapters, burned-in captions. sha256 cb65aedddf78754e6db614b40ec1bf6cab2bb2a9f6ec06edc9fc16314063b731.

The video, captions, transcript, chapter list, preview and media check are attached privately to Linear TH-8326 (Future AGI workspace access needed).

Every run uses the real parallel-web 1.3.5 client against the package's loopback fake Parallel API, with placeholder keys and the shared harness receiver. There is no vendor call and no live fi-collector.

Time Chapter
00:00 The problem: no OpenTelemetry from Parallel; a naive wrapper would export the API key and page text; the server quotes requests back in errors and warnings
00:35 Head fadfee6: local and remote at the same SHA, 0 uncommitted files, 12 commits on 3eaadc8; outside the package only the README table rows
01:06 Search: one export to /tracer/v1/traces, auth header names only (values not shown), project_type=observe; a parallel.search RETRIEVER span under agent-turn with the pasted key as [redacted]; using_session/using_user attributes (R1); no key, objective, titles, excerpts or result URLs in the export
01:57 Extract: counts only by default (url 3, result 2, failed 1); async twin equal; capture_urls/capture_objective opt-ins; caps (20 URLs, 1,024-byte objective); page text never recorded
02:33 Hide inputs (N1): with FI_HIDE_INPUTS, a 400 and a warning quoting the query, URL and objective show __REDACTED__ in status, exception message, stacktrace and warning, while the caller's error keeps them. CONTROL (hide off) records them; BEFORE 4ecdf34 they leaked
03:05 PII (R1): through FITracer, pii_redaction turns an email into <EMAIL_ADDRESS> everywhere on the span; session and user on 3 of 3 spans; BEFORE 967997c (plain tracer) the email was in all 6 places
03:42 Errors (R4): a 401 echoing the key from the constructor, default_headers or extra_headers is [redacted] on the span; stacktrace capped at 16,384 bytes (BEFORE 8002add: 25,098); cancellation ends ERROR cancelled; instrumentation failures never reach the caller
04:25 The change: _hidden_inputs (every query, extract URL and the objective, key-redacted, longest first) and the scrub order
05:16 Tests: 103 passed (Python 3.11, parallel-web 1.3.5), 0 uncommitted files after, plus the logged matrix above
05:42 Review and limits: round 3 VERIFIED; P3 follow-ups (the 16 KB stacktrace cap keeps the head of the trace; only verbatim echoes of hidden inputs are removed)

Only a 6.8 s frozen test wait was cut; on-screen output matches the coordinator-verified driver output in all terminal chapters.

Coordinator checks:

  • sha256 of the uploaded files, with byte readback of each private upload
  • full decode, exit 0; 10 chapter markers
  • silence: three pauses of 2.2-2.7 s, all inside live terminal takes
  • one frame per chapter inspected; no keys or header values in frames, captions or transcript (placeholders and [redacted] only)

Stacked on #203 (shared harness, 3eaadc8), base dev. Not merged.

Linear: TH-8326. approved: Nikhil 2026-10-03 blanket

nik13 added 15 commits October 3, 2026 18:58
The Node OTLP exporter streams with Transfer-Encoding: chunked and sends
no Content-Length. The Receiver read Content-Length only, so every Node
export came back 400 with zero spans; two TH-8103 children had to put a
de-chunking relay in front of it. Receiver now accepts both framings.

It also records one entry per accepted export (path, lower-cased
headers, flattened resource attributes) via requests(), so a contract
test can assert X-Api-Key/X-Secret-Key and project_name through the
shared harness instead of a private recorder.

Verified: 6 harness tests pass, and the TanStack example's real Node
exporter delivered 4 chunked exports (4 spans, collector path, both
auth headers, project_name/project_type) with no relay.

Refs: TH-8339, TH-8103
Add the test suite for traceAI-parallel before the package exists. The
tests drive the real parallel-web client against a loopback fake of the
Parallel API (POST /v1/search, /v1/extract, /v1/tasks/runs) and cover:

- one RETRIEVER span per search/extract call with mode, query count,
  result count, search/extract/session ids, and no response content
- the Parallel key redacted wherever the SDK holds it (api_key, default
  and per-call x-api-key headers, PARALLEL_API_KEY), redacted before the
  1 KB UTF-8 cap on input.value / gen_ai.retrieval.query
- warnings as span events, usage only when returned, unknown counts
  omitted, HTTP and connection errors, async cancellation
- AsyncParallel parity, span current during the HTTP request, parent and
  root spans, uninstrument identity restore, instrumentation isolation
- scope: task_run and /v1beta are not traced, raw/streaming responses are
- TraceConfig hide flags, capture_urls / capture_objective opt-ins
- packaging ranges, the runtime version check, classifiers
- the shared harness Receiver contract and the example via harness.run

They fail to collect until traceai_parallel exists.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
…async

Add traceAI-parallel (import traceai_parallel, ParallelInstrumentor). It
wraps the four methods parallel-web 1.x defines on its own classes in
parallel._client: Parallel.search, Parallel.extract, AsyncParallel.search
and AsyncParallel.extract (POST /v1/search, /v1/extract). Task, Monitor,
FindAll and the rest of client.beta are not wrapped.

Each call is one span, parallel.search or parallel.extract, with
fi.span.kind=RETRIEVER, current while the SDK sends HTTP (use_span with
end_on_exit=False), a child of the active span or a root span.

- request: parallel.mode, parallel.query_count, parallel.url_count, the
  joined search_queries in gen_ai.retrieval.query and input.value, and a
  caller session_id; the Parallel key is redacted from every value before
  a 1 KB UTF-8 cap on a character boundary. The key is read from
  client.api_key, auth_headers, default/custom headers and extra_headers.
- response: parallel.result_count, parallel.failed_url_count (extract),
  parallel.search_id / parallel.extract_id, parallel.session_id, usage SKU
  names and counts only when returned, warnings as parallel.warning events.
  Unknown counts are omitted, never 0. No excerpts, titles, URLs or page
  text are read.
- errors: ERROR status and one exception event, both redacted; the
  vendor's exception is re-raised unchanged. Async cancellation sets
  ERROR "cancelled" and parallel.cancelled=true.
- capture_urls / capture_objective opt in to request URLs (at most 20)
  and the objective; TraceConfig hide_inputs / hide_outputs (and their
  FI_HIDE_* variables) drop request text and warning messages.
- every instrumentation step is isolated, so its failure never reaches
  the caller; suppress_tracing is honoured; uninstrument() restores each
  method by identity and disables bound copies taken while instrumented.

Dependencies are ranges: parallel-web >=1.0.1,<2 (the docs floor; every
1.x wheel has the same search/extract signatures and paths) and
fi-instrumentation-otel >=1.1.0. Classifiers list the Pythons the suite
ran on: 3.10, 3.11, 3.13.

Two test fixes from the first run: await AsyncAPIResponse.parse() and use
a query without the fake's "usage" trigger. The example test stays red
until the example lands in the next commit.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
One traced search and one extract with the real parallel-web client.
The client reads PARALLEL_API_KEY and PARALLEL_BASE_URL itself and
register() reads the FI_* variables, so the example takes no arguments.
test_parallel_example runs it through harness.run against the loopback
fake and harness.Receiver and checks both spans arrive at exit.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
Document what traceAI-parallel wraps (search and extract, sync and async;
not Task, Monitor, FindAll or beta), the correct keyword-only call shape,
every span attribute and event, what is never recorded, the
capture_urls / capture_objective opt-ins, where the API key is redacted
from, the TraceConfig hide flags, error and cancellation status, and the
known limits (with_raw_response copies, version range, /v1beta).

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
Add the Parallel row to the root README's Python "Tools and Libraries"
table and support matrix, and to python/README.md's "Tools &
Integrations" table, next to the sibling tool integrations.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
Warning and error messages are written by the server and recorded with
only the API key removed, so a message that quotes the request carries
that text even with hide_inputs. State it in the privacy section.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
The suite passes 79 tests with parallel-web 1.0.1 and 1.3.5 on Python
3.10, 3.12 and 3.13 (and on 3.11), so the README states the four
Pythons and pyproject carries the 3.12 classifier. The packaging test
now expects 3.10, 3.11, 3.12 and 3.13 and checks the python range
admits 3.12.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
exception.message and the status description were cut to 1 KB, but the
stacktrace was only redacted, so a server error that echoes a large
request produced an unbounded attribute. The stacktrace is now cut to
16 KB of UTF-8 on a character boundary, after the API key is replaced,
so a key that straddles the cut leaves no prefix.

Tests: a real 400 with a ~24 KB echoed body yields a 16 KB stacktrace
without the key; a key straddling 16 KB is redacted whole; the cut keeps
whole characters. The README states every size cap.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
The instrumentor used a plain OTel tracer, so TraceConfig(pii_redaction)
/ FI_PII_REDACTION never ran on the default-on query text, and the
using_session / using_user / using_metadata / using_tags /
using_attributes context attributes were never stamped on parallel.*
spans. The tracer is now FITracer(tracer, config=config), as in
traceai-tavily.

The package also applies the PII pass itself, after the API key is
replaced and before the size caps, to every text it records: a cut can
then not leave part of an email, and span events and the error status,
which FiSpan does not mask, are covered too. FiSpan passes add_event,
set_status and end through, so the package's own redacted exception
event, the OK status, cancellation and isolation are unchanged.

With hide_inputs, input.value is now FITracer's __REDACTED__ placeholder
(as in traceai-tavily) instead of being absent; gen_ai.retrieval.query,
parallel.urls and parallel.objective are still dropped and no query
text is recorded. The two hide tests assert the placeholder.

Tests: PII redaction from config and from the env var on both query
keys (key and email each replaced once), PII before the 1 KB cap, PII
in error text, using_session/using_user on sync and async search and
extract, using_attributes metadata and tags. README updated.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
with_streaming_response returns once the response headers arrive, and
the span ends there. The README now says the span covers the request up
to the headers, not the body read, so a failure while reading the body
is not recorded on the span. The streaming test pins that the span has
already ended before the body is read.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
With hide_inputs / FI_HIDE_INPUTS=true the span dropped the query, URL
and objective attributes, but parallel-web builds every APIStatusError
message from the whole server body, so a server that quoted the request
put the query back into the error status, exception.message,
exception.stacktrace and warning messages. The README row said no query
text was recorded while the paragraph below it said the opposite, and a
test pinned the leak.

As in traceai-tavily, under hide_inputs every verbatim occurrence of
each search query, each requested extract URL and the objective (with or
without the capture switches) now becomes __REDACTED__ in those texts.
It runs after the API key is redacted, matching each input as it reads
after that, and before the PII pass and the 1 KB / 16 KB caps. The
replacement is one pass, longest input first, so an input inside a
longer one or inside the placeholder cannot split a replacement. If the
inputs cannot be read, those texts are recorded as __REDACTED__; the
caller always gets the vendor's own exception. Sync and async share the
path. With hide_inputs off nothing changes.

Tests: the pinned test now asserts the query is absent with and without
pii_redaction; new tests cover objective and URLs on search and extract,
the key-then-input order, the one-pass replacement, warning messages,
the async twin, unreadable inputs, and controls showing error and
warning text unchanged without hide_inputs. The fake server quotes every
query, URL and the objective back. README rows and the paragraph below
now agree with the code and say that only verbatim copies are matched.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
FITracer runs its own PII pass on span attributes after the package's
cut, and a cut can leave a new match (for example the last ten digits of
a longer number) whose token makes the value a few bytes longer than the
cap. The README now says the joined queries, captured URLs and objective
are cut to about 1 KB, and mode, ids and usage SKU names to about 256
bytes, and explains why. Warning messages, the error status and the
exception event are not passed through FITracer again, so their caps
stay exact. Docs only.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8326
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