@effect-uai/perplexity 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ import { PerplexityReasoningEffort, PerplexityResearchModel } from "./models.mjs";
2
+ import { Context, Effect, Layer, Redacted } from "effect";
3
+ import { HttpClient } from "effect/unstable/http";
4
+ import { DeepResearch, DeepResearchServiceShape } from "@effect-uai/core/DeepResearch";
5
+ import * as Job from "@effect-uai/core/Job";
6
+ import { ResearchRequest } from "@effect-uai/core/Research";
7
+
8
+ //#region src/PerplexityDeepResearch.d.ts
9
+ declare namespace PerplexityDeepResearch_d_exports {
10
+ export { Config, PerplexityDeepResearch, PerplexityDeepResearchService, PerplexityResearchRequest, layer, make };
11
+ }
12
+ /**
13
+ * Perplexity-typed research request. Narrows `model` to the sonar research
14
+ * models and adds `reasoningEffort` (a Perplexity knob that also steers how
15
+ * many searches `sonar-deep-research` runs). Provider-portable code uses the
16
+ * generic `DeepResearch` tag instead.
17
+ */
18
+ type PerplexityResearchRequest = Omit<ResearchRequest, "model"> & {
19
+ readonly model?: PerplexityResearchModel;
20
+ readonly reasoningEffort?: PerplexityReasoningEffort;
21
+ };
22
+ type PerplexityDeepResearchService = DeepResearchServiceShape<PerplexityResearchRequest>;
23
+ declare const PerplexityDeepResearch_base: Context.ServiceClass<PerplexityDeepResearch, "@betalyra/effect-uai/providers/perplexity/PerplexityDeepResearch", PerplexityDeepResearchService>;
24
+ /**
25
+ * Provider-typed tag. Yield this for the Perplexity model / `reasoningEffort`
26
+ * knobs; yield the generic `DeepResearch` tag for provider-portable code. The
27
+ * async job is poll-only, so it has no live event stream: `researchStream` /
28
+ * `streamFrom` return the synthesized progress {@link fromJob} builds (a leading
29
+ * search event, then the terminal report). `cancel` fails `Unsupported` (no
30
+ * endpoint). A persisted ref survives a restart (the job is server-backed).
31
+ */
32
+ declare class PerplexityDeepResearch extends PerplexityDeepResearch_base {}
33
+ type Config = {
34
+ readonly apiKey: Redacted.Redacted;
35
+ readonly baseUrl?: string; /** Poll cadence / overall timeout for the derived poll loops. */
36
+ readonly job?: Job.JobConfig;
37
+ };
38
+ /** Build a `PerplexityDeepResearchService`. For Layer setup, prefer {@link layer}. */
39
+ declare const make: (cfg: Config) => Effect.Effect<PerplexityDeepResearchService, never, HttpClient.HttpClient>;
40
+ /**
41
+ * Layer registering the provider-typed `PerplexityDeepResearch` tag and the
42
+ * generic `DeepResearch` tag. No capability markers: the whole surface (poll,
43
+ * collect, synthesized stream) is universal.
44
+ */
45
+ declare const layer: (cfg: Config) => Layer.Layer<PerplexityDeepResearch | DeepResearch, never, HttpClient.HttpClient>;
46
+ //#endregion
47
+ export { Config, PerplexityDeepResearch, PerplexityDeepResearchService, PerplexityResearchRequest, layer, make, PerplexityDeepResearch_d_exports as t };
48
+ //# sourceMappingURL=PerplexityDeepResearch.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PerplexityDeepResearch.d.mts","names":[],"sources":["../src/PerplexityDeepResearch.ts"],"mappings":";;;;;;;;;;;;;;;;;KA2BY,yBAAA,GAA4B,IAAA,CAAK,eAAA;EAAA,SAClC,KAAA,GAAQ,uBAAA;EAAA,SACR,eAAA,GAAkB,yBAAA;AAAA;AAAA,KAGjB,6BAAA,GAAgC,wBAAA,CAAyB,yBAAA;AAAA,cAA0B,2BAAA;;;;;;;;;cAUlF,sBAAA,SAA+B,2BAAA;AAAA,KAKhC,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA,WApBkB;EAAA,SAsBlB,GAAA,GAAM,GAAA,CAAI,SAAA;AAAA;AAnBrB;AAAA,cAqOa,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,6BAAA,SAAsC,UAAA,CAAW,UAAA;;;;AAvO6B;;cA+OlF,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,sBAAA,GAAyB,YAAA,SAAqB,UAAA,CAAW,UAAA"}
@@ -0,0 +1,171 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { httpStatusError, transportFailure } from "./http.mjs";
3
+ import { Context, Effect, Layer, Match, Redacted, Schema } from "effect";
4
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
5
+ import * as AiError from "@effect-uai/core/AiError";
6
+ import { DeepResearch, fromJob } from "@effect-uai/core/DeepResearch";
7
+ import * as Job from "@effect-uai/core/Job";
8
+ //#region src/PerplexityDeepResearch.ts
9
+ var PerplexityDeepResearch_exports = /* @__PURE__ */ __exportAll({
10
+ PerplexityDeepResearch: () => PerplexityDeepResearch,
11
+ layer: () => layer,
12
+ make: () => make
13
+ });
14
+ /**
15
+ * Provider-typed tag. Yield this for the Perplexity model / `reasoningEffort`
16
+ * knobs; yield the generic `DeepResearch` tag for provider-portable code. The
17
+ * async job is poll-only, so it has no live event stream: `researchStream` /
18
+ * `streamFrom` return the synthesized progress {@link fromJob} builds (a leading
19
+ * search event, then the terminal report). `cancel` fails `Unsupported` (no
20
+ * endpoint). A persisted ref survives a restart (the job is server-backed).
21
+ */
22
+ var PerplexityDeepResearch = class extends Context.Service()("@betalyra/effect-uai/providers/perplexity/PerplexityDeepResearch") {};
23
+ const WireSearchResult = Schema.Struct({
24
+ title: Schema.String,
25
+ url: Schema.String,
26
+ snippet: Schema.optional(Schema.String)
27
+ });
28
+ const WireChoice = Schema.Struct({ message: Schema.optional(Schema.Struct({ content: Schema.optional(Schema.NullOr(Schema.String)) })) });
29
+ const WireUsage = Schema.Struct({
30
+ prompt_tokens: Schema.optional(Schema.Number),
31
+ completion_tokens: Schema.optional(Schema.Number),
32
+ total_tokens: Schema.optional(Schema.Number)
33
+ });
34
+ const WireCompletion = Schema.Struct({
35
+ choices: Schema.Array(WireChoice),
36
+ search_results: Schema.optional(Schema.Array(WireSearchResult)),
37
+ citations: Schema.optional(Schema.Array(Schema.String)),
38
+ usage: Schema.optional(WireUsage)
39
+ });
40
+ const WireJob = Schema.Struct({
41
+ id: Schema.String,
42
+ status: Schema.Literals([
43
+ "CREATED",
44
+ "IN_PROGRESS",
45
+ "COMPLETED",
46
+ "FAILED"
47
+ ]),
48
+ error_message: Schema.optional(Schema.NullOr(Schema.String)),
49
+ response: Schema.optional(Schema.NullOr(WireCompletion))
50
+ });
51
+ const decodeJob = Schema.decodeUnknownEffect(WireJob);
52
+ const blockText = Match.type().pipe(Match.discriminatorsExhaustive("type")({
53
+ input_text: (b) => b.text,
54
+ output_text: (b) => b.text,
55
+ input_image: () => "",
56
+ refusal: () => ""
57
+ }));
58
+ const historyToMessages = (history) => history.filter((i) => i.type === "message").map((m) => ({
59
+ role: m.role,
60
+ content: m.content.map(blockText).join("")
61
+ }));
62
+ const citationsOf = (wire) => wire.search_results !== void 0 && wire.search_results.length > 0 ? wire.search_results.map((r, i) => ({
63
+ type: "url_citation",
64
+ url: r.url,
65
+ title: r.title,
66
+ marker: i + 1,
67
+ ...r.snippet !== void 0 && r.snippet !== "" && { cited_text: r.snippet }
68
+ })) : (wire.citations ?? []).map((url, i) => ({
69
+ type: "url_citation",
70
+ url,
71
+ title: url,
72
+ marker: i + 1
73
+ }));
74
+ const completionToTurn = (wire) => {
75
+ const text = wire.choices[0]?.message?.content ?? "";
76
+ const annotations = citationsOf(wire);
77
+ return {
78
+ items: [{
79
+ type: "message",
80
+ role: "assistant",
81
+ content: [{
82
+ type: "output_text",
83
+ text,
84
+ ...annotations.length > 0 && { annotations }
85
+ }],
86
+ providerData: wire
87
+ }],
88
+ usage: {
89
+ ...wire.usage?.prompt_tokens !== void 0 && { input_tokens: wire.usage.prompt_tokens },
90
+ ...wire.usage?.completion_tokens !== void 0 && { output_tokens: wire.usage.completion_tokens },
91
+ ...wire.usage?.total_tokens !== void 0 && { total_tokens: wire.usage.total_tokens }
92
+ },
93
+ stop_reason: "stop"
94
+ };
95
+ };
96
+ const jobStateOf = (wire) => Match.value(wire.status).pipe(Match.when("CREATED", () => ({ _tag: "Pending" })), Match.when("IN_PROGRESS", () => ({ _tag: "Running" })), Match.when("COMPLETED", () => wire.response != null ? {
97
+ _tag: "Succeeded",
98
+ result: completionToTurn(wire.response)
99
+ } : {
100
+ _tag: "Failed",
101
+ reason: "job completed with no response body",
102
+ raw: wire
103
+ }), Match.when("FAILED", () => ({
104
+ _tag: "Failed",
105
+ ...wire.error_message != null && { reason: wire.error_message },
106
+ raw: wire
107
+ })), Match.exhaustive);
108
+ const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.perplexity.ai";
109
+ const authHeader = (cfg) => HttpClientRequest.setHeader("authorization", `Bearer ${Redacted.value(cfg.apiKey)}`);
110
+ const submitBody = (request) => ({ request: {
111
+ model: request.model ?? "sonar-deep-research",
112
+ messages: historyToMessages(request.history),
113
+ ...request.reasoningEffort !== void 0 && { reasoning_effort: request.reasoningEffort }
114
+ } });
115
+ const submitJob = (cfg, request) => Effect.gen(function* () {
116
+ const client = yield* HttpClient.HttpClient;
117
+ const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v1/async/sonar`).pipe(authHeader(cfg), HttpClientRequest.bodyJsonUnsafe(submitBody(request)));
118
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
119
+ if (response.status >= 400) {
120
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
121
+ return yield* httpStatusError(response.status, text);
122
+ }
123
+ const wire = yield* decodeJob(yield* response.json.pipe(Effect.mapError(transportFailure))).pipe(Effect.mapError(transportFailure));
124
+ return Job.jobRef("perplexity", wire.id);
125
+ });
126
+ const pollJob = (cfg, ref) => Effect.gen(function* () {
127
+ const client = yield* HttpClient.HttpClient;
128
+ const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/v1/async/sonar/${ref.id}`).pipe(authHeader(cfg));
129
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
130
+ if (response.status >= 400) {
131
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
132
+ return yield* httpStatusError(response.status, text);
133
+ }
134
+ return jobStateOf(yield* decodeJob(yield* response.json.pipe(Effect.mapError(transportFailure))).pipe(Effect.mapError(transportFailure)));
135
+ });
136
+ const unsupported = (capability) => new AiError.Unsupported({
137
+ provider: "perplexity",
138
+ capability,
139
+ reason: "Perplexity's async research job is poll-only and cannot be cancelled."
140
+ });
141
+ const jobOps = (cfg, client) => {
142
+ const withClient = (e) => Effect.provideService(e, HttpClient.HttpClient, client);
143
+ return {
144
+ submit: (request) => withClient(submitJob(cfg, request)),
145
+ poll: (ref) => withClient(pollJob(cfg, ref)),
146
+ cancel: () => Effect.fail(unsupported("cancel"))
147
+ };
148
+ };
149
+ /** Build a `PerplexityDeepResearchService`. For Layer setup, prefer {@link layer}. */
150
+ const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job));
151
+ /**
152
+ * Layer registering the provider-typed `PerplexityDeepResearch` tag and the
153
+ * generic `DeepResearch` tag. No capability markers: the whole surface (poll,
154
+ * collect, synthesized stream) is universal.
155
+ */
156
+ const layer = (cfg) => {
157
+ const typed = Layer.effect(PerplexityDeepResearch, make(cfg));
158
+ const generic = Layer.effect(DeepResearch, Effect.map(HttpClient.HttpClient, (client) => {
159
+ const ops = jobOps(cfg, client);
160
+ return fromJob({
161
+ submit: (request) => ops.submit(request),
162
+ poll: ops.poll,
163
+ cancel: ops.cancel
164
+ }, cfg.job);
165
+ }));
166
+ return Layer.merge(typed, generic);
167
+ };
168
+ //#endregion
169
+ export { PerplexityDeepResearch, layer, make, PerplexityDeepResearch_exports as t };
170
+
171
+ //# sourceMappingURL=PerplexityDeepResearch.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PerplexityDeepResearch.mjs","names":[],"sources":["../src/PerplexityDeepResearch.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n DeepResearch,\n type DeepResearchService,\n type DeepResearchServiceShape,\n fromJob,\n type ResearchJobOps,\n} from \"@effect-uai/core/DeepResearch\"\nimport type { Annotation, ContentBlock, HistoryItem, Message } from \"@effect-uai/core/Items\"\nimport * as Job from \"@effect-uai/core/Job\"\nimport type { ResearchJobRef, ResearchRequest, ResearchState } from \"@effect-uai/core/Research\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\nimport { httpStatusError, transportFailure } from \"./http.js\"\nimport type { PerplexityReasoningEffort, PerplexityResearchModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Perplexity-typed research request. Narrows `model` to the sonar research\n * models and adds `reasoningEffort` (a Perplexity knob that also steers how\n * many searches `sonar-deep-research` runs). Provider-portable code uses the\n * generic `DeepResearch` tag instead.\n */\nexport type PerplexityResearchRequest = Omit<ResearchRequest, \"model\"> & {\n readonly model?: PerplexityResearchModel\n readonly reasoningEffort?: PerplexityReasoningEffort\n}\n\nexport type PerplexityDeepResearchService = DeepResearchServiceShape<PerplexityResearchRequest>\n\n/**\n * Provider-typed tag. Yield this for the Perplexity model / `reasoningEffort`\n * knobs; yield the generic `DeepResearch` tag for provider-portable code. The\n * async job is poll-only, so it has no live event stream: `researchStream` /\n * `streamFrom` return the synthesized progress {@link fromJob} builds (a leading\n * search event, then the terminal report). `cancel` fails `Unsupported` (no\n * endpoint). A persisted ref survives a restart (the job is server-backed).\n */\nexport class PerplexityDeepResearch extends Context.Service<\n PerplexityDeepResearch,\n PerplexityDeepResearchService\n>()(\"@betalyra/effect-uai/providers/perplexity/PerplexityDeepResearch\") {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Poll cadence / overall timeout for the derived poll loops. */\n readonly job?: Job.JobConfig\n}\n\n// ---------------------------------------------------------------------------\n// Wire codec\n// ---------------------------------------------------------------------------\n\nconst WireSearchResult = Schema.Struct({\n title: Schema.String,\n url: Schema.String,\n snippet: Schema.optional(Schema.String),\n})\n\nconst WireChoice = Schema.Struct({\n message: Schema.optional(\n Schema.Struct({ content: Schema.optional(Schema.NullOr(Schema.String)) }),\n ),\n})\n\nconst WireUsage = Schema.Struct({\n prompt_tokens: Schema.optional(Schema.Number),\n completion_tokens: Schema.optional(Schema.Number),\n total_tokens: Schema.optional(Schema.Number),\n})\n\nconst WireCompletion = Schema.Struct({\n choices: Schema.Array(WireChoice),\n search_results: Schema.optional(Schema.Array(WireSearchResult)),\n citations: Schema.optional(Schema.Array(Schema.String)),\n usage: Schema.optional(WireUsage),\n})\ntype WireCompletion = typeof WireCompletion.Type\n\nconst WireJob = Schema.Struct({\n id: Schema.String,\n status: Schema.Literals([\"CREATED\", \"IN_PROGRESS\", \"COMPLETED\", \"FAILED\"]),\n error_message: Schema.optional(Schema.NullOr(Schema.String)),\n response: Schema.optional(Schema.NullOr(WireCompletion)),\n})\ntype WireJob = typeof WireJob.Type\n\nconst decodeJob = Schema.decodeUnknownEffect(WireJob)\n\n// Text of a content block; only the two text-bearing kinds contribute.\nconst blockText = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b) => b.text,\n output_text: (b) => b.text,\n input_image: () => \"\",\n refusal: () => \"\",\n }),\n)\n\nconst historyToMessages = (\n history: ReadonlyArray<HistoryItem>,\n): ReadonlyArray<{ readonly role: string; readonly content: string }> =>\n history\n .filter((i): i is Message => i.type === \"message\")\n .map((m) => ({ role: m.role, content: m.content.map(blockText).join(\"\") }))\n\n// Perplexity links answers to sources with positional `[n]` markers, so each\n// citation carries a 1-based `marker` (and `cited_text` from the snippet), the\n// provider-agnostic representation of that style. Prefer the richer\n// `search_results`; fall back to the legacy `citations` URL list.\nconst citationsOf = (wire: WireCompletion): ReadonlyArray<Annotation> =>\n wire.search_results !== undefined && wire.search_results.length > 0\n ? wire.search_results.map((r, i) => ({\n type: \"url_citation\" as const,\n url: r.url,\n title: r.title,\n marker: i + 1,\n ...(r.snippet !== undefined && r.snippet !== \"\" && { cited_text: r.snippet }),\n }))\n : (wire.citations ?? []).map((url, i) => ({\n type: \"url_citation\" as const,\n url,\n title: url,\n marker: i + 1,\n }))\n\nconst completionToTurn = (wire: WireCompletion): Turn => {\n const text = wire.choices[0]?.message?.content ?? \"\"\n const annotations = citationsOf(wire)\n return {\n items: [\n {\n type: \"message\",\n role: \"assistant\",\n content: [{ type: \"output_text\", text, ...(annotations.length > 0 && { annotations }) }],\n providerData: wire,\n },\n ],\n usage: {\n ...(wire.usage?.prompt_tokens !== undefined && { input_tokens: wire.usage.prompt_tokens }),\n ...(wire.usage?.completion_tokens !== undefined && {\n output_tokens: wire.usage.completion_tokens,\n }),\n ...(wire.usage?.total_tokens !== undefined && { total_tokens: wire.usage.total_tokens }),\n },\n stop_reason: \"stop\",\n }\n}\n\n// Map Perplexity's four job states onto the generic `JobState`, carrying the\n// assembled `Turn` on `Succeeded` and the provider message on `Failed`.\nconst jobStateOf = (wire: WireJob): ResearchState =>\n Match.value(wire.status).pipe(\n Match.when(\"CREATED\", (): ResearchState => ({ _tag: \"Pending\" })),\n Match.when(\"IN_PROGRESS\", (): ResearchState => ({ _tag: \"Running\" })),\n Match.when(\n \"COMPLETED\",\n (): ResearchState =>\n wire.response != null\n ? { _tag: \"Succeeded\", result: completionToTurn(wire.response) }\n : { _tag: \"Failed\", reason: \"job completed with no response body\", raw: wire },\n ),\n Match.when(\n \"FAILED\",\n (): ResearchState => ({\n _tag: \"Failed\",\n ...(wire.error_message != null && { reason: wire.error_message }),\n raw: wire,\n }),\n ),\n Match.exhaustive,\n )\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.perplexity.ai\"\n\nconst authHeader = (cfg: Config) =>\n HttpClientRequest.setHeader(\"authorization\", `Bearer ${Redacted.value(cfg.apiKey)}`)\n\nconst submitBody = (request: PerplexityResearchRequest): Record<string, unknown> => ({\n request: {\n model: request.model ?? \"sonar-deep-research\",\n messages: historyToMessages(request.history),\n ...(request.reasoningEffort !== undefined && { reasoning_effort: request.reasoningEffort }),\n },\n})\n\nconst submitJob = (\n cfg: Config,\n request: PerplexityResearchRequest,\n): Effect.Effect<ResearchJobRef, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v1/async/sonar`).pipe(\n authHeader(cfg),\n HttpClientRequest.bodyJsonUnsafe(submitBody(request)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeJob(json).pipe(Effect.mapError(transportFailure))\n return Job.jobRef<Turn>(\"perplexity\", wire.id)\n })\n\nconst pollJob = (\n cfg: Config,\n ref: ResearchJobRef,\n): Effect.Effect<ResearchState, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/v1/async/sonar/${ref.id}`).pipe(\n authHeader(cfg),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeJob(json).pipe(Effect.mapError(transportFailure))\n return jobStateOf(wire)\n })\n\n// Perplexity exposes no cancel endpoint and no event stream on the async job.\nconst unsupported = (capability: string): AiError.Unsupported =>\n new AiError.Unsupported({\n provider: \"perplexity\",\n capability,\n reason: \"Perplexity's async research job is poll-only and cannot be cancelled.\",\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n// The three job ops. Poll-only, so no `streamFrom`: `fromJob` synthesizes one.\nconst jobOps = (\n cfg: Config,\n client: HttpClient.HttpClient,\n): ResearchJobOps<PerplexityResearchRequest> => {\n const withClient = <A>(e: Effect.Effect<A, AiError.AiError, HttpClient.HttpClient>) =>\n Effect.provideService(e, HttpClient.HttpClient, client)\n return {\n submit: (request) => withClient(submitJob(cfg, request)),\n poll: (ref) => withClient(pollJob(cfg, ref)),\n cancel: () => Effect.fail(unsupported(\"cancel\")),\n }\n}\n\n/** Build a `PerplexityDeepResearchService`. For Layer setup, prefer {@link layer}. */\nexport const make = (\n cfg: Config,\n): Effect.Effect<PerplexityDeepResearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job))\n\n/**\n * Layer registering the provider-typed `PerplexityDeepResearch` tag and the\n * generic `DeepResearch` tag. No capability markers: the whole surface (poll,\n * collect, synthesized stream) is universal.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<PerplexityDeepResearch | DeepResearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(PerplexityDeepResearch, make(cfg))\n const generic = Layer.effect(\n DeepResearch,\n Effect.map(HttpClient.HttpClient, (client): DeepResearchService => {\n const ops = jobOps(cfg, client)\n return fromJob<ResearchRequest>(\n {\n submit: (request) => ops.submit(request as PerplexityResearchRequest),\n poll: ops.poll,\n cancel: ops.cancel,\n },\n cfg.job,\n )\n }),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -128,7 +128,7 @@ const searchImpl = (cfg) => (request) => postSearch(cfg, buildBody(request)).pip
128
128
  * Build a `PerplexitySearchService` value. For Layer-based setup, prefer
129
129
  * {@link layer}.
130
130
  */
131
- const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({ search: (request) => searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
131
+ const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => ({ search: (request) => searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
132
132
  /**
133
133
  * Layer registering both the provider-typed `PerplexitySearch` tag and the
134
134
  * generic `WebSearch` tag over one implementation. A `CommonSearchRequest`
@@ -1 +1 @@
1
- {"version":3,"file":"PerplexitySearch.mjs","names":[],"sources":["../src/PerplexitySearch.ts"],"sourcesContent":["import { Context, DateTime, Effect, Layer, Option, type Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonSearchRequest,\n type SearchResponse,\n type SearchResult,\n WebSearch,\n type WebSearchService,\n} from \"@effect-uai/core/WebSearch\"\nimport type { PerplexitySearchContextSize } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Perplexity-typed search request. Widens {@link CommonSearchRequest} with\n * the provider's own knobs; the common fields map onto Perplexity's wire\n * filters (see the codec below). There is no `model` - `/search` ranks the\n * web directly.\n */\nexport type PerplexitySearchRequest = CommonSearchRequest & {\n /** Depth / cost trade-off - see {@link PerplexitySearchContextSize}. */\n readonly searchContextSize?: PerplexitySearchContextSize\n /** Cap on tokens extracted per result page (1..1_000_000). */\n readonly maxTokensPerPage?: number\n}\n\nexport type PerplexitySearchService = {\n readonly search: (\n request: PerplexitySearchRequest,\n ) => Effect.Effect<SearchResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Perplexity-typed search\n * (`searchContextSize`, `maxTokensPerPage`); yield the generic `WebSearch`\n * tag for provider-portable code. Both are registered by {@link layer}.\n */\nexport class PerplexitySearch extends Context.Service<PerplexitySearch, PerplexitySearchService>()(\n \"@betalyra/effect-uai/providers/perplexity/PerplexitySearch\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly query: string\n readonly max_results?: number\n readonly country?: string\n readonly search_language_filter?: ReadonlyArray<string>\n readonly search_domain_filter?: ReadonlyArray<string>\n readonly search_recency_filter?: string\n readonly search_after_date_filter?: string\n readonly search_before_date_filter?: string\n readonly search_context_size?: string\n readonly max_tokens_per_page?: number\n}\n\n/** Perplexity wants dates as `MM/DD/YYYY`, in UTC. */\nconst formatMmDdYyyy = (dt: DateTime.DateTime): string => {\n const d = DateTime.toDateUtc(dt)\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\")\n const dd = String(d.getUTCDate()).padStart(2, \"0\")\n return `${mm}/${dd}/${d.getUTCFullYear()}`\n}\n\n/**\n * Perplexity has no separate include / exclude fields - it takes one\n * `search_domain_filter` array where an exclusion is a `-` prefix. Lossless\n * shape transform, so done silently (bucket 3).\n */\nconst domainFilter = (\n include: ReadonlyArray<string> | undefined,\n exclude: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> | undefined => {\n const all = [...(include ?? []), ...(exclude ?? []).map((d) => `-${d}`)]\n return all.length > 0 ? all : undefined\n}\n\nconst buildBody = (request: PerplexitySearchRequest): WireBody => {\n const domains = domainFilter(request.includeDomains, request.excludeDomains)\n return {\n query: request.query,\n ...(request.maxResults !== undefined && { max_results: request.maxResults }),\n ...(request.country !== undefined && { country: request.country }),\n ...(request.language !== undefined && { search_language_filter: [request.language] }),\n ...(domains !== undefined && { search_domain_filter: domains }),\n ...(request.recency !== undefined && { search_recency_filter: request.recency }),\n ...(request.startDate !== undefined && {\n search_after_date_filter: formatMmDdYyyy(request.startDate),\n }),\n ...(request.endDate !== undefined && {\n search_before_date_filter: formatMmDdYyyy(request.endDate),\n }),\n ...(request.searchContextSize !== undefined && {\n search_context_size: request.searchContextSize,\n }),\n ...(request.maxTokensPerPage !== undefined && {\n max_tokens_per_page: request.maxTokensPerPage,\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireResult = Schema.Struct({\n title: Schema.String,\n url: Schema.String,\n snippet: Schema.optional(Schema.String),\n date: Schema.optional(Schema.NullOr(Schema.String)),\n last_updated: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype WireResult = typeof WireResult.Type\n\nconst WireResponse = Schema.Struct({\n results: Schema.Array(WireResult),\n id: Schema.optional(Schema.String),\n})\ntype WireResponse = typeof WireResponse.Type\n\n/** Lenient parse of Perplexity's date strings; unparseable -> omitted. */\nconst parseDate = (s: string | null | undefined): DateTime.DateTime | undefined =>\n s == null ? undefined : Option.getOrUndefined(DateTime.make(s))\n\nconst toResult = (r: WireResult): SearchResult => {\n const publishedDate = parseDate(r.date)\n return {\n url: r.url,\n title: r.title,\n // Perplexity returns no relevance score, so `score` stays undefined.\n ...(r.snippet !== undefined && { snippet: r.snippet }),\n ...(publishedDate !== undefined && { publishedDate }),\n raw: r,\n }\n}\n\nconst toResponse = (wire: WireResponse): SearchResponse => ({\n results: wire.results.map(toResult),\n raw: wire,\n})\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"perplexity\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"perplexity\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.perplexity.ai\"\n\nconst postSearch = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/search`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst searchImpl =\n (cfg: Config) =>\n (\n request: PerplexitySearchRequest,\n ): Effect.Effect<SearchResponse, AiError.AiError, HttpClient.HttpClient> =>\n postSearch(cfg, buildBody(request)).pipe(Effect.map(toResponse))\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `PerplexitySearchService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<PerplexitySearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n search: (request) =>\n searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `PerplexitySearch` tag and the\n * generic `WebSearch` tag over one implementation. A `CommonSearchRequest`\n * is structurally a `PerplexitySearchRequest` with the provider extras\n * unset, so the generic registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<PerplexitySearch | WebSearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(PerplexitySearch, make(cfg))\n const generic = Layer.effect(\n WebSearch,\n Effect.map(make(cfg), (s): WebSearchService => ({ search: (request) => s.search(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerplexitySearch.mjs","names":[],"sources":["../src/PerplexitySearch.ts"],"sourcesContent":["import { Context, DateTime, Effect, Layer, Option, type Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonSearchRequest,\n type SearchResponse,\n type SearchResult,\n WebSearch,\n type WebSearchService,\n} from \"@effect-uai/core/WebSearch\"\nimport type { PerplexitySearchContextSize } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Perplexity-typed search request. Widens {@link CommonSearchRequest} with\n * the provider's own knobs; the common fields map onto Perplexity's wire\n * filters (see the codec below). There is no `model` - `/search` ranks the\n * web directly.\n */\nexport type PerplexitySearchRequest = CommonSearchRequest & {\n /** Depth / cost trade-off - see {@link PerplexitySearchContextSize}. */\n readonly searchContextSize?: PerplexitySearchContextSize\n /** Cap on tokens extracted per result page (1..1_000_000). */\n readonly maxTokensPerPage?: number\n}\n\nexport type PerplexitySearchService = {\n readonly search: (\n request: PerplexitySearchRequest,\n ) => Effect.Effect<SearchResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Perplexity-typed search\n * (`searchContextSize`, `maxTokensPerPage`); yield the generic `WebSearch`\n * tag for provider-portable code. Both are registered by {@link layer}.\n */\nexport class PerplexitySearch extends Context.Service<PerplexitySearch, PerplexitySearchService>()(\n \"@betalyra/effect-uai/providers/perplexity/PerplexitySearch\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly query: string\n readonly max_results?: number\n readonly country?: string\n readonly search_language_filter?: ReadonlyArray<string>\n readonly search_domain_filter?: ReadonlyArray<string>\n readonly search_recency_filter?: string\n readonly search_after_date_filter?: string\n readonly search_before_date_filter?: string\n readonly search_context_size?: string\n readonly max_tokens_per_page?: number\n}\n\n/** Perplexity wants dates as `MM/DD/YYYY`, in UTC. */\nconst formatMmDdYyyy = (dt: DateTime.DateTime): string => {\n const d = DateTime.toDateUtc(dt)\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\")\n const dd = String(d.getUTCDate()).padStart(2, \"0\")\n return `${mm}/${dd}/${d.getUTCFullYear()}`\n}\n\n/**\n * Perplexity has no separate include / exclude fields - it takes one\n * `search_domain_filter` array where an exclusion is a `-` prefix. Lossless\n * shape transform, so done silently (bucket 3).\n */\nconst domainFilter = (\n include: ReadonlyArray<string> | undefined,\n exclude: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> | undefined => {\n const all = [...(include ?? []), ...(exclude ?? []).map((d) => `-${d}`)]\n return all.length > 0 ? all : undefined\n}\n\nconst buildBody = (request: PerplexitySearchRequest): WireBody => {\n const domains = domainFilter(request.includeDomains, request.excludeDomains)\n return {\n query: request.query,\n ...(request.maxResults !== undefined && { max_results: request.maxResults }),\n ...(request.country !== undefined && { country: request.country }),\n ...(request.language !== undefined && { search_language_filter: [request.language] }),\n ...(domains !== undefined && { search_domain_filter: domains }),\n ...(request.recency !== undefined && { search_recency_filter: request.recency }),\n ...(request.startDate !== undefined && {\n search_after_date_filter: formatMmDdYyyy(request.startDate),\n }),\n ...(request.endDate !== undefined && {\n search_before_date_filter: formatMmDdYyyy(request.endDate),\n }),\n ...(request.searchContextSize !== undefined && {\n search_context_size: request.searchContextSize,\n }),\n ...(request.maxTokensPerPage !== undefined && {\n max_tokens_per_page: request.maxTokensPerPage,\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireResult = Schema.Struct({\n title: Schema.String,\n url: Schema.String,\n snippet: Schema.optional(Schema.String),\n date: Schema.optional(Schema.NullOr(Schema.String)),\n last_updated: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype WireResult = typeof WireResult.Type\n\nconst WireResponse = Schema.Struct({\n results: Schema.Array(WireResult),\n id: Schema.optional(Schema.String),\n})\ntype WireResponse = typeof WireResponse.Type\n\n/** Lenient parse of Perplexity's date strings; unparseable -> omitted. */\nconst parseDate = (s: string | null | undefined): DateTime.DateTime | undefined =>\n s == null ? undefined : Option.getOrUndefined(DateTime.make(s))\n\nconst toResult = (r: WireResult): SearchResult => {\n const publishedDate = parseDate(r.date)\n return {\n url: r.url,\n title: r.title,\n // Perplexity returns no relevance score, so `score` stays undefined.\n ...(r.snippet !== undefined && { snippet: r.snippet }),\n ...(publishedDate !== undefined && { publishedDate }),\n raw: r,\n }\n}\n\nconst toResponse = (wire: WireResponse): SearchResponse => ({\n results: wire.results.map(toResult),\n raw: wire,\n})\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"perplexity\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"perplexity\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.perplexity.ai\"\n\nconst postSearch = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/search`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst searchImpl =\n (cfg: Config) =>\n (\n request: PerplexitySearchRequest,\n ): Effect.Effect<SearchResponse, AiError.AiError, HttpClient.HttpClient> =>\n postSearch(cfg, buildBody(request)).pipe(Effect.map(toResponse))\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `PerplexitySearchService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<PerplexitySearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => ({\n search: (request) =>\n searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `PerplexitySearch` tag and the\n * generic `WebSearch` tag over one implementation. A `CommonSearchRequest`\n * is structurally a `PerplexitySearchRequest` with the provider extras\n * unset, so the generic registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<PerplexitySearch | WebSearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(PerplexitySearch, make(cfg))\n const generic = Layer.effect(\n WebSearch,\n Effect.map(make(cfg), (s): WebSearchService => ({ search: (request) => s.search(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import * as AiError from "@effect-uai/core/AiError";
2
+
3
+ //#region src/http.d.ts
4
+ /** Transport-level failure (connection dropped, DNS, etc.) before a response. */
5
+ declare const transportFailure: (cause: unknown) => AiError.AiError;
6
+ /** Map a >=400 HTTP status to the nearest `AiError`. */
7
+ declare const httpStatusError: (status: number, body: string) => AiError.AiError;
8
+ //#endregion
9
+ export { httpStatusError, transportFailure };
10
+ //# sourceMappingURL=http.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.mts","names":[],"sources":["../src/http.ts"],"mappings":";;;;cAKa,gBAAA,GAAoB,KAAA,cAAiB,OAAA,CAAQ,OAAA;AAA1D;AAAA,cAIa,eAAA,GAAmB,MAAA,UAAgB,IAAA,aAAe,OAAA,CAAQ,OAAA"}
package/dist/http.mjs ADDED
@@ -0,0 +1,48 @@
1
+ import * as AiError from "@effect-uai/core/AiError";
2
+ //#region src/http.ts
3
+ const provider = "perplexity";
4
+ /** Transport-level failure (connection dropped, DNS, etc.) before a response. */
5
+ const transportFailure = (cause) => new AiError.Unavailable({
6
+ provider,
7
+ raw: cause
8
+ });
9
+ /** Map a >=400 HTTP status to the nearest `AiError`. */
10
+ const httpStatusError = (status, body) => {
11
+ const raw = body;
12
+ if (status === 429) return new AiError.RateLimited({
13
+ provider,
14
+ raw
15
+ });
16
+ if (status === 408 || status === 504) return new AiError.Timeout({
17
+ provider,
18
+ raw
19
+ });
20
+ if (status === 401) return new AiError.AuthFailed({
21
+ provider,
22
+ subtype: "auth",
23
+ raw
24
+ });
25
+ if (status === 403) return new AiError.AuthFailed({
26
+ provider,
27
+ subtype: "permission",
28
+ raw
29
+ });
30
+ if (status === 402) return new AiError.AuthFailed({
31
+ provider,
32
+ subtype: "billing",
33
+ raw
34
+ });
35
+ if (status >= 500) return new AiError.Unavailable({
36
+ provider,
37
+ status,
38
+ raw
39
+ });
40
+ return new AiError.InvalidRequest({
41
+ provider,
42
+ raw
43
+ });
44
+ };
45
+ //#endregion
46
+ export { httpStatusError, transportFailure };
47
+
48
+ //# sourceMappingURL=http.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../src/http.ts"],"sourcesContent":["import * as AiError from \"@effect-uai/core/AiError\"\n\nconst provider = \"perplexity\"\n\n/** Transport-level failure (connection dropped, DNS, etc.) before a response. */\nexport const transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider, raw: cause })\n\n/** Map a >=400 HTTP status to the nearest `AiError`. */\nexport const httpStatusError = (status: number, body: string): AiError.AiError => {\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n"],"mappings":";;AAEA,MAAM,WAAW;;AAGjB,MAAa,oBAAoB,UAC/B,IAAI,QAAQ,YAAY;CAAE;CAAU,KAAK;CAAO,CAAC;;AAGnD,MAAa,mBAAmB,QAAgB,SAAkC;CAChF,MAAM,MAAM;AACZ,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,YAAY;EAAE;EAAU;EAAK,CAAC;AACrE,KAAI,WAAW,OAAO,WAAW,IAAK,QAAO,IAAI,QAAQ,QAAQ;EAAE;EAAU;EAAK,CAAC;AACnF,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,WAAW;EAAE;EAAU,SAAS;EAAQ;EAAK,CAAC;AACrF,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,WAAW;EAAE;EAAU,SAAS;EAAc;EAAK,CAAC;AAC3F,KAAI,WAAW,IAAK,QAAO,IAAI,QAAQ,WAAW;EAAE;EAAU,SAAS;EAAW;EAAK,CAAC;AACxF,KAAI,UAAU,IAAK,QAAO,IAAI,QAAQ,YAAY;EAAE;EAAU;EAAQ;EAAK,CAAC;AAC5E,QAAO,IAAI,QAAQ,eAAe;EAAE;EAAU;EAAK,CAAC"}
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
- import { PerplexitySearchContextSize } from "./models.mjs";
1
+ import { PerplexityReasoningEffort, PerplexityResearchModel, PerplexitySearchContextSize } from "./models.mjs";
2
+ import { t as PerplexityDeepResearch_d_exports } from "./PerplexityDeepResearch.mjs";
2
3
  import { t as PerplexitySearch_d_exports } from "./PerplexitySearch.mjs";
3
- export { PerplexitySearch_d_exports as PerplexitySearch, PerplexitySearchContextSize };
4
+ export { PerplexityDeepResearch_d_exports as PerplexityDeepResearch, PerplexityReasoningEffort, PerplexityResearchModel, PerplexitySearch_d_exports as PerplexitySearch, PerplexitySearchContextSize };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { t as PerplexityDeepResearch_exports } from "./PerplexityDeepResearch.mjs";
1
2
  import { t as PerplexitySearch_exports } from "./PerplexitySearch.mjs";
2
3
  import "./models.mjs";
3
- export { PerplexitySearch_exports as PerplexitySearch };
4
+ export { PerplexityDeepResearch_exports as PerplexityDeepResearch, PerplexitySearch_exports as PerplexitySearch };
package/dist/models.d.mts CHANGED
@@ -12,6 +12,18 @@
12
12
  * rather than the cross-provider `CommonSearchRequest`.
13
13
  */
14
14
  type PerplexitySearchContextSize = "low" | "medium" | "high";
15
+ /**
16
+ * Models the async research endpoint (`/v1/async/sonar`) accepts. Deep research
17
+ * is `sonar-deep-research`; the other sonar models are also submittable but are
18
+ * meant for the sync chat endpoint, so the default is `sonar-deep-research`.
19
+ */
20
+ type PerplexityResearchModel = "sonar-deep-research" | "sonar" | "sonar-pro" | "sonar-reasoning-pro";
21
+ /**
22
+ * Depth knob for reasoning-capable models. Perplexity-specific (not on the
23
+ * cross-provider `ResearchRequest`), it also steers how many searches
24
+ * `sonar-deep-research` runs.
25
+ */
26
+ type PerplexityReasoningEffort = "minimal" | "low" | "medium" | "high";
15
27
  //#endregion
16
- export { PerplexitySearchContextSize };
28
+ export { PerplexityReasoningEffort, PerplexityResearchModel, PerplexitySearchContextSize };
17
29
  //# sourceMappingURL=models.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAYA;;;;;;;;;;;KAAY,2BAAA"}
1
+ {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAYA;;;;;AAOA;;;;;AAWA;KAlBY,2BAAA;;;;;;KAOA,uBAAA;;;;;;KAWA,yBAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/perplexity",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Perplexity provider for @effect-uai/core (web search).",
5
5
  "keywords": [
6
6
  "ai",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/index.d.mts",
35
35
  "import": "./dist/index.mjs"
36
36
  },
37
+ "./PerplexityDeepResearch": {
38
+ "types": "./dist/PerplexityDeepResearch.d.mts",
39
+ "import": "./dist/PerplexityDeepResearch.mjs"
40
+ },
37
41
  "./PerplexitySearch": {
38
42
  "types": "./dist/PerplexitySearch.d.mts",
39
43
  "import": "./dist/PerplexitySearch.mjs"
@@ -43,13 +47,13 @@
43
47
  "access": "public"
44
48
  },
45
49
  "devDependencies": {
46
- "effect": "4.0.0-beta.57",
50
+ "effect": "4.0.0-beta.94",
47
51
  "typescript": "^6.0.3",
48
- "@effect-uai/core": "0.10.0"
52
+ "@effect-uai/core": "0.11.0"
49
53
  },
50
54
  "peerDependencies": {
51
55
  "@effect-uai/core": ">=0.2.0 <1",
52
- "effect": "4.0.0-beta.57"
56
+ "effect": ">=4.0.0-beta.94 <5.0.0"
53
57
  },
54
58
  "scripts": {
55
59
  "build": "tsdown",
@@ -0,0 +1,291 @@
1
+ import { Context, Effect, Layer, Match, Redacted, Schema } from "effect"
2
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
+ import * as AiError from "@effect-uai/core/AiError"
4
+ import {
5
+ DeepResearch,
6
+ type DeepResearchService,
7
+ type DeepResearchServiceShape,
8
+ fromJob,
9
+ type ResearchJobOps,
10
+ } from "@effect-uai/core/DeepResearch"
11
+ import type { Annotation, ContentBlock, HistoryItem, Message } from "@effect-uai/core/Items"
12
+ import * as Job from "@effect-uai/core/Job"
13
+ import type { ResearchJobRef, ResearchRequest, ResearchState } from "@effect-uai/core/Research"
14
+ import type { Turn } from "@effect-uai/core/Turn"
15
+ import { httpStatusError, transportFailure } from "./http.js"
16
+ import type { PerplexityReasoningEffort, PerplexityResearchModel } from "./models.js"
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Public types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Perplexity-typed research request. Narrows `model` to the sonar research
24
+ * models and adds `reasoningEffort` (a Perplexity knob that also steers how
25
+ * many searches `sonar-deep-research` runs). Provider-portable code uses the
26
+ * generic `DeepResearch` tag instead.
27
+ */
28
+ export type PerplexityResearchRequest = Omit<ResearchRequest, "model"> & {
29
+ readonly model?: PerplexityResearchModel
30
+ readonly reasoningEffort?: PerplexityReasoningEffort
31
+ }
32
+
33
+ export type PerplexityDeepResearchService = DeepResearchServiceShape<PerplexityResearchRequest>
34
+
35
+ /**
36
+ * Provider-typed tag. Yield this for the Perplexity model / `reasoningEffort`
37
+ * knobs; yield the generic `DeepResearch` tag for provider-portable code. The
38
+ * async job is poll-only, so it has no live event stream: `researchStream` /
39
+ * `streamFrom` return the synthesized progress {@link fromJob} builds (a leading
40
+ * search event, then the terminal report). `cancel` fails `Unsupported` (no
41
+ * endpoint). A persisted ref survives a restart (the job is server-backed).
42
+ */
43
+ export class PerplexityDeepResearch extends Context.Service<
44
+ PerplexityDeepResearch,
45
+ PerplexityDeepResearchService
46
+ >()("@betalyra/effect-uai/providers/perplexity/PerplexityDeepResearch") {}
47
+
48
+ export type Config = {
49
+ readonly apiKey: Redacted.Redacted
50
+ readonly baseUrl?: string
51
+ /** Poll cadence / overall timeout for the derived poll loops. */
52
+ readonly job?: Job.JobConfig
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Wire codec
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const WireSearchResult = Schema.Struct({
60
+ title: Schema.String,
61
+ url: Schema.String,
62
+ snippet: Schema.optional(Schema.String),
63
+ })
64
+
65
+ const WireChoice = Schema.Struct({
66
+ message: Schema.optional(
67
+ Schema.Struct({ content: Schema.optional(Schema.NullOr(Schema.String)) }),
68
+ ),
69
+ })
70
+
71
+ const WireUsage = Schema.Struct({
72
+ prompt_tokens: Schema.optional(Schema.Number),
73
+ completion_tokens: Schema.optional(Schema.Number),
74
+ total_tokens: Schema.optional(Schema.Number),
75
+ })
76
+
77
+ const WireCompletion = Schema.Struct({
78
+ choices: Schema.Array(WireChoice),
79
+ search_results: Schema.optional(Schema.Array(WireSearchResult)),
80
+ citations: Schema.optional(Schema.Array(Schema.String)),
81
+ usage: Schema.optional(WireUsage),
82
+ })
83
+ type WireCompletion = typeof WireCompletion.Type
84
+
85
+ const WireJob = Schema.Struct({
86
+ id: Schema.String,
87
+ status: Schema.Literals(["CREATED", "IN_PROGRESS", "COMPLETED", "FAILED"]),
88
+ error_message: Schema.optional(Schema.NullOr(Schema.String)),
89
+ response: Schema.optional(Schema.NullOr(WireCompletion)),
90
+ })
91
+ type WireJob = typeof WireJob.Type
92
+
93
+ const decodeJob = Schema.decodeUnknownEffect(WireJob)
94
+
95
+ // Text of a content block; only the two text-bearing kinds contribute.
96
+ const blockText = Match.type<ContentBlock>().pipe(
97
+ Match.discriminatorsExhaustive("type")({
98
+ input_text: (b) => b.text,
99
+ output_text: (b) => b.text,
100
+ input_image: () => "",
101
+ refusal: () => "",
102
+ }),
103
+ )
104
+
105
+ const historyToMessages = (
106
+ history: ReadonlyArray<HistoryItem>,
107
+ ): ReadonlyArray<{ readonly role: string; readonly content: string }> =>
108
+ history
109
+ .filter((i): i is Message => i.type === "message")
110
+ .map((m) => ({ role: m.role, content: m.content.map(blockText).join("") }))
111
+
112
+ // Perplexity links answers to sources with positional `[n]` markers, so each
113
+ // citation carries a 1-based `marker` (and `cited_text` from the snippet), the
114
+ // provider-agnostic representation of that style. Prefer the richer
115
+ // `search_results`; fall back to the legacy `citations` URL list.
116
+ const citationsOf = (wire: WireCompletion): ReadonlyArray<Annotation> =>
117
+ wire.search_results !== undefined && wire.search_results.length > 0
118
+ ? wire.search_results.map((r, i) => ({
119
+ type: "url_citation" as const,
120
+ url: r.url,
121
+ title: r.title,
122
+ marker: i + 1,
123
+ ...(r.snippet !== undefined && r.snippet !== "" && { cited_text: r.snippet }),
124
+ }))
125
+ : (wire.citations ?? []).map((url, i) => ({
126
+ type: "url_citation" as const,
127
+ url,
128
+ title: url,
129
+ marker: i + 1,
130
+ }))
131
+
132
+ const completionToTurn = (wire: WireCompletion): Turn => {
133
+ const text = wire.choices[0]?.message?.content ?? ""
134
+ const annotations = citationsOf(wire)
135
+ return {
136
+ items: [
137
+ {
138
+ type: "message",
139
+ role: "assistant",
140
+ content: [{ type: "output_text", text, ...(annotations.length > 0 && { annotations }) }],
141
+ providerData: wire,
142
+ },
143
+ ],
144
+ usage: {
145
+ ...(wire.usage?.prompt_tokens !== undefined && { input_tokens: wire.usage.prompt_tokens }),
146
+ ...(wire.usage?.completion_tokens !== undefined && {
147
+ output_tokens: wire.usage.completion_tokens,
148
+ }),
149
+ ...(wire.usage?.total_tokens !== undefined && { total_tokens: wire.usage.total_tokens }),
150
+ },
151
+ stop_reason: "stop",
152
+ }
153
+ }
154
+
155
+ // Map Perplexity's four job states onto the generic `JobState`, carrying the
156
+ // assembled `Turn` on `Succeeded` and the provider message on `Failed`.
157
+ const jobStateOf = (wire: WireJob): ResearchState =>
158
+ Match.value(wire.status).pipe(
159
+ Match.when("CREATED", (): ResearchState => ({ _tag: "Pending" })),
160
+ Match.when("IN_PROGRESS", (): ResearchState => ({ _tag: "Running" })),
161
+ Match.when(
162
+ "COMPLETED",
163
+ (): ResearchState =>
164
+ wire.response != null
165
+ ? { _tag: "Succeeded", result: completionToTurn(wire.response) }
166
+ : { _tag: "Failed", reason: "job completed with no response body", raw: wire },
167
+ ),
168
+ Match.when(
169
+ "FAILED",
170
+ (): ResearchState => ({
171
+ _tag: "Failed",
172
+ ...(wire.error_message != null && { reason: wire.error_message }),
173
+ raw: wire,
174
+ }),
175
+ ),
176
+ Match.exhaustive,
177
+ )
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // HTTP
181
+ // ---------------------------------------------------------------------------
182
+
183
+ const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.perplexity.ai"
184
+
185
+ const authHeader = (cfg: Config) =>
186
+ HttpClientRequest.setHeader("authorization", `Bearer ${Redacted.value(cfg.apiKey)}`)
187
+
188
+ const submitBody = (request: PerplexityResearchRequest): Record<string, unknown> => ({
189
+ request: {
190
+ model: request.model ?? "sonar-deep-research",
191
+ messages: historyToMessages(request.history),
192
+ ...(request.reasoningEffort !== undefined && { reasoning_effort: request.reasoningEffort }),
193
+ },
194
+ })
195
+
196
+ const submitJob = (
197
+ cfg: Config,
198
+ request: PerplexityResearchRequest,
199
+ ): Effect.Effect<ResearchJobRef, AiError.AiError, HttpClient.HttpClient> =>
200
+ Effect.gen(function* () {
201
+ const client = yield* HttpClient.HttpClient
202
+ const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v1/async/sonar`).pipe(
203
+ authHeader(cfg),
204
+ HttpClientRequest.bodyJsonUnsafe(submitBody(request)),
205
+ )
206
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
207
+ if (response.status >= 400) {
208
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
209
+ return yield* httpStatusError(response.status, text)
210
+ }
211
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure))
212
+ const wire = yield* decodeJob(json).pipe(Effect.mapError(transportFailure))
213
+ return Job.jobRef<Turn>("perplexity", wire.id)
214
+ })
215
+
216
+ const pollJob = (
217
+ cfg: Config,
218
+ ref: ResearchJobRef,
219
+ ): Effect.Effect<ResearchState, AiError.AiError, HttpClient.HttpClient> =>
220
+ Effect.gen(function* () {
221
+ const client = yield* HttpClient.HttpClient
222
+ const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/v1/async/sonar/${ref.id}`).pipe(
223
+ authHeader(cfg),
224
+ )
225
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
226
+ if (response.status >= 400) {
227
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
228
+ return yield* httpStatusError(response.status, text)
229
+ }
230
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure))
231
+ const wire = yield* decodeJob(json).pipe(Effect.mapError(transportFailure))
232
+ return jobStateOf(wire)
233
+ })
234
+
235
+ // Perplexity exposes no cancel endpoint and no event stream on the async job.
236
+ const unsupported = (capability: string): AiError.Unsupported =>
237
+ new AiError.Unsupported({
238
+ provider: "perplexity",
239
+ capability,
240
+ reason: "Perplexity's async research job is poll-only and cannot be cancelled.",
241
+ })
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Constructors
245
+ // ---------------------------------------------------------------------------
246
+
247
+ // The three job ops. Poll-only, so no `streamFrom`: `fromJob` synthesizes one.
248
+ const jobOps = (
249
+ cfg: Config,
250
+ client: HttpClient.HttpClient,
251
+ ): ResearchJobOps<PerplexityResearchRequest> => {
252
+ const withClient = <A>(e: Effect.Effect<A, AiError.AiError, HttpClient.HttpClient>) =>
253
+ Effect.provideService(e, HttpClient.HttpClient, client)
254
+ return {
255
+ submit: (request) => withClient(submitJob(cfg, request)),
256
+ poll: (ref) => withClient(pollJob(cfg, ref)),
257
+ cancel: () => Effect.fail(unsupported("cancel")),
258
+ }
259
+ }
260
+
261
+ /** Build a `PerplexityDeepResearchService`. For Layer setup, prefer {@link layer}. */
262
+ export const make = (
263
+ cfg: Config,
264
+ ): Effect.Effect<PerplexityDeepResearchService, never, HttpClient.HttpClient> =>
265
+ Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job))
266
+
267
+ /**
268
+ * Layer registering the provider-typed `PerplexityDeepResearch` tag and the
269
+ * generic `DeepResearch` tag. No capability markers: the whole surface (poll,
270
+ * collect, synthesized stream) is universal.
271
+ */
272
+ export const layer = (
273
+ cfg: Config,
274
+ ): Layer.Layer<PerplexityDeepResearch | DeepResearch, never, HttpClient.HttpClient> => {
275
+ const typed = Layer.effect(PerplexityDeepResearch, make(cfg))
276
+ const generic = Layer.effect(
277
+ DeepResearch,
278
+ Effect.map(HttpClient.HttpClient, (client): DeepResearchService => {
279
+ const ops = jobOps(cfg, client)
280
+ return fromJob<ResearchRequest>(
281
+ {
282
+ submit: (request) => ops.submit(request as PerplexityResearchRequest),
283
+ poll: ops.poll,
284
+ cancel: ops.cancel,
285
+ },
286
+ cfg.job,
287
+ )
288
+ }),
289
+ )
290
+ return Layer.merge(typed, generic)
291
+ }
@@ -209,7 +209,7 @@ const searchImpl =
209
209
  export const make = (
210
210
  cfg: Config,
211
211
  ): Effect.Effect<PerplexitySearchService, never, HttpClient.HttpClient> =>
212
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
212
+ Effect.map(HttpClient.HttpClient, (client) => ({
213
213
  search: (request) =>
214
214
  searchImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
215
215
  }))
package/src/http.ts ADDED
@@ -0,0 +1,19 @@
1
+ import * as AiError from "@effect-uai/core/AiError"
2
+
3
+ const provider = "perplexity"
4
+
5
+ /** Transport-level failure (connection dropped, DNS, etc.) before a response. */
6
+ export const transportFailure = (cause: unknown): AiError.AiError =>
7
+ new AiError.Unavailable({ provider, raw: cause })
8
+
9
+ /** Map a >=400 HTTP status to the nearest `AiError`. */
10
+ export const httpStatusError = (status: number, body: string): AiError.AiError => {
11
+ const raw = body
12
+ if (status === 429) return new AiError.RateLimited({ provider, raw })
13
+ if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
14
+ if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
15
+ if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
16
+ if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
17
+ if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
18
+ return new AiError.InvalidRequest({ provider, raw })
19
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * as PerplexityDeepResearch from "./PerplexityDeepResearch.js"
1
2
  export * as PerplexitySearch from "./PerplexitySearch.js"
2
3
  export * from "./models.js"
package/src/models.ts CHANGED
@@ -11,3 +11,21 @@
11
11
  * rather than the cross-provider `CommonSearchRequest`.
12
12
  */
13
13
  export type PerplexitySearchContextSize = "low" | "medium" | "high"
14
+
15
+ /**
16
+ * Models the async research endpoint (`/v1/async/sonar`) accepts. Deep research
17
+ * is `sonar-deep-research`; the other sonar models are also submittable but are
18
+ * meant for the sync chat endpoint, so the default is `sonar-deep-research`.
19
+ */
20
+ export type PerplexityResearchModel =
21
+ | "sonar-deep-research"
22
+ | "sonar"
23
+ | "sonar-pro"
24
+ | "sonar-reasoning-pro"
25
+
26
+ /**
27
+ * Depth knob for reasoning-capable models. Perplexity-specific (not on the
28
+ * cross-provider `ResearchRequest`), it also steers how many searches
29
+ * `sonar-deep-research` runs.
30
+ */
31
+ export type PerplexityReasoningEffort = "minimal" | "low" | "medium" | "high"