feat(#3674): Perplexity connector + web-grounded search tooling (epic)#164
Merged
Conversation
Thin OpenAiClient subclass for api.perplexity.ai (mirrors KimiClient/
DeepSeekClient): providerName "perplexity", DEFAULT_BASE_URL, and a
model { perplexity("sonar") } DSL slot. Unlike Kimi/DeepSeek, Perplexity
accepts OpenAI's response_format.json_schema, so constrained decoding is
left ON (inherited).
Wires ModelProvider.PERPLEXITY through ModelConfig (perplexityBaseUrl),
ModelBuilder (slot + build() error pointing at .secrets/perplexity-key),
ModelClientFactory (dispatch + semconv name), and PermissionManifest
(baseUrl + manifestName). Slice 1 of epic #3674.
Tests: PerplexityClientTest (parse/identity/error/headers/constrained-
decoding ON), PerplexityModelDslTest (DSL + factory resolution),
gated live PerplexityClientIntegrationTest, plus a Perplexity branch in
SessionCancellationTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `perplexity_search` ToolDef (untrustedOutput=true) so an agent on its
OWN model can fetch grounded, cited facts from Perplexity's Sonar API.
Result renders the answer + a numbered source list (sources land in the
model context AND the JSONL audit row); the agentic loop wraps it in the
{trusted:false} envelope (#642) since web output is the canonical
injection vector.
- PerplexitySearch.kt: PerplexitySearchArgs (@generable), PerplexitySource,
PerplexitySearchResult (render), PerplexitySearchOptions, a
PerplexitySearchBackend seam + HttpPerplexitySearchBackend default, and
pure buildPerplexitySearchBody / parsePerplexitySearchResponse
(search_results[] preferred, citations[] fallback, error envelope ->
PerplexitySearchException). perplexitySearchTool(apiKey) factory returns
ERROR strings on blank query / backend failure (loop convention).
- Register via `tools { +perplexitySearchTool(key) }`. Slice 2 of #3674.
Tests: PerplexitySearchTest (12 — body/parse/fallback/render/tool/envelope),
gated live PerplexitySearchLiveTest. Live run confirmed the request reaches
api.perplexity.ai and the error path parses correctly (local key currently
invalid -> live tests stay `live-llm`, documented; flip to live-cloud-api
once a valid key lands). Also trims an over-length factory error line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends PerplexitySearchOptions with the documented Perplexity controls, all omitted when unset (a bare options object reproduces the slice-2 body): - search_mode (SearchMode: WEB/ACADEMIC/SEC) - search_recency_filter (SearchRecency: HOUR..YEAR) - search_domain_filter (allow plain + deny with a leading '-') - web_search_options.search_context_size (SearchContextSize: LOW/MED/HIGH) - reasoning_effort (reuses ReasoningEffort LOW/MED/HIGH; API 'minimal' n/a) - response_format json_schema via structuredOutput(KClass) — maps a @generable type to a strict schema (reuses generation.jsonSchema()). Adds a perplexitySearchOptions { } builder DSL (allowDomains/denyDomains/ structuredOutput). buildPerplexitySearchBody now assembles optional fields. Slice 3 of #3674. Tests: PerplexitySearchControlsTest (9) pins each control's exact wire param + the builder round-trip + non-@generable rejection. Controls are verified by serialization unit tests; live confirmation stays blocked on the invalid local key (same as slices 1-2). detekt + compile green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-file split (#3199)
Docs (slice 4 of #3674):
- README: provider count 6 -> 7 in both spots; add the `model { perplexity("sonar") }`
slot and a `perplexitySearch` web-grounded-search feature bullet.
- docs/providers.md: intro enum list (seven values; Perplexity extends the OpenAI
wire shape, constrained-decoding ON) + a "Web-grounded search tool" section with
register/controls examples.
- CHANGELOG [Unreleased]: the epic entry (connector #3675, tool #3676, controls #3677).
- internals-agent adjuncts: model/PerplexityClient.md + model/PerplexitySearch.md
(auto-discovered by the InternalsAgent scan).
Refactor (#3199): split the original multi-type PerplexitySearch.kt into one type
per file (Args/Source/Result/Options/OptionsBuilder/Backend/HttpBackend/Exception +
SearchMode/Recency/ContextSize), keeping only the pure wire helpers + the
perplexitySearchTool factory in PerplexitySearch.kt. The checkOneTypePerFile
allowlist is empty (burndown complete) — not regressed.
Full `./gradlew build` green (all modules, detekt, guards, adjunct frontmatter).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… seam Live verification with a valid key surfaced the bug: OpenAiClient hardcoded `/v1/chat/completions`. Perplexity serves `/chat/completions` (no `/v1`) — hitting `/v1` there returns HTTP 404 with an empty body, which sendBounded passed through, so the connector returned blank text. (The perplexitySearch tool was unaffected — its backend already used the correct path.) - OpenAiClient: extract a `protected open val chatCompletionsPath` (default `/v1/chat/completions`), used by both sendChat + sendChatStream. Unchanged for OpenAI / DeepSeek / Kimi / OpenRouter. - PerplexityClient: override to `/chat/completions`. - Regression test pins both paths. Verified end-to-end against api.perplexity.ai: connector chat + streaming return grounded text + usage; perplexitySearch returns answer + real citations. Live tests re-tagged `live-llm` -> `live-cloud-api` (DeepSeek parity: run in default :test when a key is present, skip otherwise) and the stale "invalid key" notes in tests/docs/CHANGELOG updated. Full build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Perplexity connector + web-grounded search tooling (epic #3674)
Adds Perplexity as the seventh model provider and — the valuable part — a
perplexitySearchtool so an agent on its own model (Claude/OpenAI/Ollama/…) can fetch live, cited facts. Grounded in the Perplexity cookbook + API reference. Additive only; no public-API change to existing surfaces.Slices
PerplexityClient(03ad2be): thinOpenAiClientsubclass forapi.perplexity.ai(mirrors DeepSeek/Kimi/OpenRouter),model { perplexity("sonar") }. Unlike Kimi/DeepSeek, Perplexity acceptsresponse_formatjson_schema, so constrained decoding stays on. Wired throughModelProvider,ModelConfig,ModelBuilder, the factory, and the permission manifest.perplexitySearchtool (7dedc86):ToolDefwithuntrustedOutput = true, so results are wrapped in the{"trusted":false}envelope (#642). Renders the answer + a numbered source list parsed fromsearch_results[](fallbackcitations[]); sources reach the model context and the JSONL audit row. InjectablePerplexitySearchBackendseam.33562ac):perplexitySearchOptions { }→search_mode(web/academic/sec),search_recency_filter,search_domain_filter(allow +-deny),web_search_options.search_context_size,reasoning_effort, and nativeresponse_formatjson_schema viastructuredOutput(@Generable).7349e46): README +docs/providers.md+ CHANGELOG + two internals-agent adjuncts. Also split the tool module into one-type-per-file (#3199; allowlist stays empty).Tests
Unit: PerplexityClientTest (6) + PerplexityModelDslTest (5) + PerplexitySearchTest (12) + PerplexitySearchControlsTest (9) — parse / fallback / render / untrusted-envelope / every control's wire param / builder round-trip / factory resolution. Full
./gradlew buildgreen across all modules (detekt + all gradle guards + adjunct frontmatter).Live verification
Live tests are tagged
live-llm(excluded from the merge gate). A live run confirmed the request reachesapi.perplexity.aiand the error-envelope path parses correctly — the local.secrets/perplexity-keycurrently returns "Invalid API key", so live tags staylive-llm(documented); flip tolive-cloud-apionce a valid key lands, same precedent as Kimi.Related: WebTools epic #2677 (crawl & render) — Perplexity is the grounded-answer/search surface, distinct.
🤖 Generated with Claude Code