@effect-uai/jina 0.8.0 → 0.10.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,50 @@
1
+ import { JinaEngine } from "./models.mjs";
2
+ import { Context, Effect, Layer, Redacted } from "effect";
3
+ import { HttpClient } from "effect/unstable/http";
4
+ import * as AiError from "@effect-uai/core/AiError";
5
+ import { CommonReadRequest, ReadResponse, WebRead } from "@effect-uai/core/WebRead";
6
+
7
+ //#region src/JinaReader.d.ts
8
+ declare namespace JinaReader_d_exports {
9
+ export { Config, JinaReadRequest, JinaReader, JinaReaderService, layer, make };
10
+ }
11
+ /**
12
+ * Jina-typed read request. Widens {@link CommonReadRequest} with Jina
13
+ * Reader's own header knobs; the common fields map onto Reader headers (see
14
+ * the codec below).
15
+ */
16
+ type JinaReadRequest = CommonReadRequest & {
17
+ /** Fetching engine - see {@link JinaEngine}. */readonly engine?: JinaEngine; /** CSS selector to extract instead of the whole page (`X-Target-Selector`). */
18
+ readonly targetSelector?: string; /** Bypass Jina's page cache (`X-No-Cache`). */
19
+ readonly noCache?: boolean; /** Append a deduplicated link list, populating {@link ReadResponse.links}. */
20
+ readonly withLinksSummary?: boolean;
21
+ };
22
+ type JinaReaderService = {
23
+ readonly read: (request: JinaReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>;
24
+ };
25
+ declare const JinaReader_base: Context.ServiceClass<JinaReader, "@betalyra/effect-uai/providers/jina/JinaReader", JinaReaderService>;
26
+ /**
27
+ * Provider-typed service tag. Yield this for Jina-typed reads (its own
28
+ * Reader headers); yield the generic `WebRead` tag for provider-portable
29
+ * code. Both are registered by {@link layer}.
30
+ */
31
+ declare class JinaReader extends JinaReader_base {}
32
+ type Config = {
33
+ readonly apiKey: Redacted.Redacted;
34
+ readonly baseUrl?: string;
35
+ };
36
+ /**
37
+ * Build a `JinaReaderService` value. For Layer-based setup, prefer
38
+ * {@link layer}.
39
+ */
40
+ declare const make: (cfg: Config) => Effect.Effect<JinaReaderService, never, HttpClient.HttpClient>;
41
+ /**
42
+ * Layer registering both the provider-typed `JinaReader` tag and the generic
43
+ * `WebRead` tag over one implementation. A `CommonReadRequest` is
44
+ * structurally a `JinaReadRequest` with the extra knobs unset, so the generic
45
+ * registration forwards directly.
46
+ */
47
+ declare const layer: (cfg: Config) => Layer.Layer<JinaReader | WebRead, never, HttpClient.HttpClient>;
48
+ //#endregion
49
+ export { Config, JinaReadRequest, JinaReader, JinaReaderService, layer, make, JinaReader_d_exports as t };
50
+ //# sourceMappingURL=JinaReader.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JinaReader.d.mts","names":[],"sources":["../src/JinaReader.ts"],"mappings":";;;;;;;;;;;;;;;KAoBY,eAAA,GAAkB,iBAAA;2DAEnB,MAAA,GAAS,UAAA;WAET,cAAA;WAEA,OAAA,YANgB;EAAA,SAQhB,gBAAA;AAAA;AAAA,KAGC,iBAAA;EAAA,SACD,IAAA,GAAO,OAAA,EAAS,eAAA,KAAoB,MAAA,CAAO,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,OAAA;AAAA;AAAA,cAClF,eAAA;;;;;AAFD;cASa,UAAA,SAAmB,eAAA;AAAA,KAIpB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;cAkIE,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAS,MAAA,CAAO,MAAA,CAAO,iBAAA,SAA0B,UAAA,CAAW,UAAA;;;;;;;cAYzE,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,UAAA,GAAa,OAAA,SAAgB,UAAA,CAAW,UAAA"}
@@ -0,0 +1,130 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { Context, Duration, Effect, Layer, Schema } from "effect";
3
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
+ import * as AiError from "@effect-uai/core/AiError";
5
+ import { WebRead } from "@effect-uai/core/WebRead";
6
+ //#region src/JinaReader.ts
7
+ var JinaReader_exports = /* @__PURE__ */ __exportAll({
8
+ JinaReader: () => JinaReader,
9
+ layer: () => layer,
10
+ make: () => make
11
+ });
12
+ /**
13
+ * Provider-typed service tag. Yield this for Jina-typed reads (its own
14
+ * Reader headers); yield the generic `WebRead` tag for provider-portable
15
+ * code. Both are registered by {@link layer}.
16
+ */
17
+ var JinaReader = class extends Context.Service()("@betalyra/effect-uai/providers/jina/JinaReader") {};
18
+ const buildHeaders = (request) => {
19
+ return {
20
+ Accept: "application/json",
21
+ "X-Return-Format": request.format ?? "markdown",
22
+ ...request.timeout !== void 0 && { "X-Timeout": String(Math.max(1, Math.round(Duration.toMillis(request.timeout) / 1e3))) },
23
+ ...request.engine !== void 0 && { "X-Engine": request.engine },
24
+ ...request.targetSelector !== void 0 && { "X-Target-Selector": request.targetSelector },
25
+ ...request.noCache === true && { "X-No-Cache": "true" },
26
+ ...request.withLinksSummary === true && { "X-With-Links-Summary": "true" }
27
+ };
28
+ };
29
+ const WireData = Schema.Struct({
30
+ title: Schema.optional(Schema.NullOr(Schema.String)),
31
+ url: Schema.optional(Schema.NullOr(Schema.String)),
32
+ content: Schema.optional(Schema.NullOr(Schema.String)),
33
+ html: Schema.optional(Schema.NullOr(Schema.String)),
34
+ links: Schema.optional(Schema.Unknown)
35
+ });
36
+ const WireResponse = Schema.Struct({
37
+ code: Schema.optional(Schema.Number),
38
+ status: Schema.optional(Schema.Number),
39
+ data: WireData
40
+ });
41
+ const extractLinks = (links) => {
42
+ const urls = (Array.isArray(links) ? links : links !== null && typeof links === "object" ? Object.values(links) : []).filter((v) => typeof v === "string");
43
+ return urls.length > 0 ? urls : void 0;
44
+ };
45
+ const toResponse = (wire, request) => {
46
+ const { data } = wire;
47
+ const content = ((request.format ?? "markdown") === "html" ? data.html : data.content) ?? "";
48
+ const title = data.title ?? void 0;
49
+ const links = extractLinks(data.links);
50
+ return {
51
+ url: data.url ?? request.url,
52
+ content,
53
+ ...title !== void 0 && { title },
54
+ ...links !== void 0 && { links },
55
+ raw: wire
56
+ };
57
+ };
58
+ const transportFailure = (cause) => new AiError.Unavailable({
59
+ provider: "jina",
60
+ raw: cause
61
+ });
62
+ const httpStatusError = (status, body) => {
63
+ const provider = "jina";
64
+ const raw = body;
65
+ if (status === 429) return new AiError.RateLimited({
66
+ provider,
67
+ raw
68
+ });
69
+ if (status === 408 || status === 504) return new AiError.Timeout({
70
+ provider,
71
+ raw
72
+ });
73
+ if (status === 401) return new AiError.AuthFailed({
74
+ provider,
75
+ subtype: "auth",
76
+ raw
77
+ });
78
+ if (status === 403) return new AiError.AuthFailed({
79
+ provider,
80
+ subtype: "permission",
81
+ raw
82
+ });
83
+ if (status === 402) return new AiError.AuthFailed({
84
+ provider,
85
+ subtype: "billing",
86
+ raw
87
+ });
88
+ if (status >= 500) return new AiError.Unavailable({
89
+ provider,
90
+ status,
91
+ raw
92
+ });
93
+ return new AiError.InvalidRequest({
94
+ provider,
95
+ raw
96
+ });
97
+ };
98
+ const baseUrl = (cfg) => cfg.baseUrl ?? "https://r.jina.ai";
99
+ const getRead = (cfg, request) => Effect.gen(function* () {
100
+ const client = yield* HttpClient.HttpClient;
101
+ const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/${request.url}`).pipe(HttpClientRequest.bearerToken(cfg.apiKey), HttpClientRequest.setHeaders(buildHeaders(request)));
102
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
103
+ if (response.status >= 400) {
104
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
105
+ return yield* httpStatusError(response.status, text);
106
+ }
107
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure));
108
+ return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(Effect.mapError(transportFailure));
109
+ });
110
+ const readImpl = (cfg) => (request) => Effect.map(getRead(cfg, request), (wire) => toResponse(wire, request));
111
+ /**
112
+ * Build a `JinaReaderService` value. For Layer-based setup, prefer
113
+ * {@link layer}.
114
+ */
115
+ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({ read: (request) => readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
116
+ /**
117
+ * Layer registering both the provider-typed `JinaReader` tag and the generic
118
+ * `WebRead` tag over one implementation. A `CommonReadRequest` is
119
+ * structurally a `JinaReadRequest` with the extra knobs unset, so the generic
120
+ * registration forwards directly.
121
+ */
122
+ const layer = (cfg) => {
123
+ const typed = Layer.effect(JinaReader, make(cfg));
124
+ const generic = Layer.effect(WebRead, Effect.map(make(cfg), (s) => ({ read: (request) => s.read(request) })));
125
+ return Layer.merge(typed, generic);
126
+ };
127
+ //#endregion
128
+ export { JinaReader, layer, make, JinaReader_exports as t };
129
+
130
+ //# sourceMappingURL=JinaReader.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JinaReader.mjs","names":[],"sources":["../src/JinaReader.ts"],"sourcesContent":["import { Context, Duration, Effect, Layer, type Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonReadRequest,\n type ReadResponse,\n WebRead,\n type WebReadService,\n} from \"@effect-uai/core/WebRead\"\nimport type { JinaEngine } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Jina-typed read request. Widens {@link CommonReadRequest} with Jina\n * Reader's own header knobs; the common fields map onto Reader headers (see\n * the codec below).\n */\nexport type JinaReadRequest = CommonReadRequest & {\n /** Fetching engine - see {@link JinaEngine}. */\n readonly engine?: JinaEngine\n /** CSS selector to extract instead of the whole page (`X-Target-Selector`). */\n readonly targetSelector?: string\n /** Bypass Jina's page cache (`X-No-Cache`). */\n readonly noCache?: boolean\n /** Append a deduplicated link list, populating {@link ReadResponse.links}. */\n readonly withLinksSummary?: boolean\n}\n\nexport type JinaReaderService = {\n readonly read: (request: JinaReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Jina-typed reads (its own\n * Reader headers); yield the generic `WebRead` tag for provider-portable\n * code. Both are registered by {@link layer}.\n */\nexport class JinaReader extends Context.Service<JinaReader, JinaReaderService>()(\n \"@betalyra/effect-uai/providers/jina/JinaReader\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request headers\n// ---------------------------------------------------------------------------\n\n// Reader is header-driven: the URL is the path, everything else is a header.\n// `markdown` / `html` map one-to-one onto `X-Return-Format`.\nconst buildHeaders = (request: JinaReadRequest): Record<string, string> => {\n const format = request.format ?? \"markdown\"\n return {\n Accept: \"application/json\",\n \"X-Return-Format\": format,\n ...(request.timeout !== undefined && {\n // `X-Timeout` is whole seconds, capped at 180 by Jina.\n \"X-Timeout\": String(Math.max(1, Math.round(Duration.toMillis(request.timeout) / 1000))),\n }),\n ...(request.engine !== undefined && { \"X-Engine\": request.engine }),\n ...(request.targetSelector !== undefined && { \"X-Target-Selector\": request.targetSelector }),\n ...(request.noCache === true && { \"X-No-Cache\": \"true\" }),\n ...(request.withLinksSummary === true && { \"X-With-Links-Summary\": \"true\" }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireData = Schema.Struct({\n title: Schema.optional(Schema.NullOr(Schema.String)),\n url: Schema.optional(Schema.NullOr(Schema.String)),\n // `content` carries markdown/text; `html` carries html. Exactly one is set,\n // depending on the requested format.\n content: Schema.optional(Schema.NullOr(Schema.String)),\n html: Schema.optional(Schema.NullOr(Schema.String)),\n // Present only with `X-With-Links-Summary`; a { anchorText: url } record.\n links: Schema.optional(Schema.Unknown),\n})\n\nconst WireResponse = Schema.Struct({\n code: Schema.optional(Schema.Number),\n status: Schema.optional(Schema.Number),\n data: WireData,\n})\ntype WireResponse = typeof WireResponse.Type\n\n// Jina reports links as a { text: url } record; take the URLs. Tolerate an\n// array shape too, and omit when empty.\nconst extractLinks = (links: unknown): ReadonlyArray<string> | undefined => {\n const values = Array.isArray(links)\n ? links\n : links !== null && typeof links === \"object\"\n ? Object.values(links as Record<string, unknown>)\n : []\n const urls = values.filter((v): v is string => typeof v === \"string\")\n return urls.length > 0 ? urls : undefined\n}\n\nconst toResponse = (wire: WireResponse, request: JinaReadRequest): ReadResponse => {\n const { data } = wire\n const format = request.format ?? \"markdown\"\n const content = (format === \"html\" ? data.html : data.content) ?? \"\"\n const title = data.title ?? undefined\n const links = extractLinks(data.links)\n return {\n url: data.url ?? request.url,\n content,\n ...(title !== undefined && { title }),\n ...(links !== undefined && { links }),\n raw: wire,\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"jina\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"jina\"\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://r.jina.ai\"\n\nconst getRead = (\n cfg: Config,\n request: JinaReadRequest,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n // The target URL is appended as the path: `https://r.jina.ai/<url>`.\n const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/${request.url}`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n HttpClientRequest.setHeaders(buildHeaders(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 return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst readImpl =\n (cfg: Config) =>\n (request: JinaReadRequest): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.map(getRead(cfg, request), (wire) => toResponse(wire, request))\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `JinaReaderService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (cfg: Config): Effect.Effect<JinaReaderService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n read: (request) =>\n readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `JinaReader` tag and the generic\n * `WebRead` tag over one implementation. A `CommonReadRequest` is\n * structurally a `JinaReadRequest` with the extra knobs unset, so the generic\n * registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<JinaReader | WebRead, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(JinaReader, make(cfg))\n const generic = Layer.effect(\n WebRead,\n Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
- import { JinaEmbeddingModel } from "./models.mjs";
1
+ import { JinaEmbeddingModel, JinaEngine } from "./models.mjs";
2
2
  import { t as JinaEmbedding_d_exports } from "./JinaEmbedding.mjs";
3
- export { JinaEmbedding_d_exports as JinaEmbedding, JinaEmbeddingModel };
3
+ import { t as JinaReader_d_exports } from "./JinaReader.mjs";
4
+ export { JinaEmbedding_d_exports as JinaEmbedding, JinaEmbeddingModel, JinaEngine, JinaReader_d_exports as JinaReader };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
1
  import { t as JinaEmbedding_exports } from "./JinaEmbedding.mjs";
2
+ import { t as JinaReader_exports } from "./JinaReader.mjs";
2
3
  import "./models.mjs";
3
- export { JinaEmbedding_exports as JinaEmbedding };
4
+ export { JinaEmbedding_exports as JinaEmbedding, JinaReader_exports as JinaReader };
package/dist/models.d.mts CHANGED
@@ -19,6 +19,12 @@
19
19
  * Reference: https://jina.ai/embeddings/
20
20
  */
21
21
  type JinaEmbeddingModel = "jina-embeddings-v4" | "jina-embeddings-v5-text-small" | "jina-embeddings-v5-text-nano" | "jina-embeddings-v3" | "jina-clip-v2" | (string & {});
22
+ /**
23
+ * Reader fetching engine (`X-Engine`). `browser` renders JavaScript,
24
+ * `curl` is a fast static fetch, `auto` picks per page. Provider-specific,
25
+ * so it lives on `JinaReadRequest`.
26
+ */
27
+ type JinaEngine = "browser" | "curl" | "auto" | (string & {});
22
28
  //#endregion
23
- export { JinaEmbeddingModel };
29
+ export { JinaEmbeddingModel, JinaEngine };
24
30
  //# sourceMappingURL=models.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAmBA;;;;;;;;;;;;;;;;;;KAAY,kBAAA"}
1
+ {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAmBA;;;;;AAcA;;;;;;;;;;;;;KAdY,kBAAA;;;;;;KAcA,UAAA"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@effect-uai/jina",
3
- "version": "0.8.0",
4
- "description": "Jina AI provider for @effect-uai/core (embeddings, rerank).",
3
+ "version": "0.10.0",
4
+ "description": "Jina AI provider for @effect-uai/core (embeddings, rerank, web read).",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "effect",
8
8
  "embeddings",
9
9
  "jina",
10
- "rerank"
10
+ "rerank",
11
+ "web-read"
11
12
  ],
12
13
  "homepage": "https://github.com/betalyra/effect-uai/tree/main/packages/providers/jina",
13
14
  "bugs": {
@@ -37,6 +38,10 @@
37
38
  "./JinaEmbedding": {
38
39
  "types": "./dist/JinaEmbedding.d.mts",
39
40
  "import": "./dist/JinaEmbedding.mjs"
41
+ },
42
+ "./JinaReader": {
43
+ "types": "./dist/JinaReader.d.mts",
44
+ "import": "./dist/JinaReader.mjs"
40
45
  }
41
46
  },
42
47
  "publishConfig": {
@@ -45,7 +50,7 @@
45
50
  "devDependencies": {
46
51
  "effect": "4.0.0-beta.57",
47
52
  "typescript": "^6.0.3",
48
- "@effect-uai/core": "0.8.0"
53
+ "@effect-uai/core": "0.10.0"
49
54
  },
50
55
  "peerDependencies": {
51
56
  "@effect-uai/core": ">=0.2.0 <1",
@@ -0,0 +1,198 @@
1
+ import { Context, Duration, Effect, Layer, type Redacted, Schema } from "effect"
2
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
+ import * as AiError from "@effect-uai/core/AiError"
4
+ import {
5
+ type CommonReadRequest,
6
+ type ReadResponse,
7
+ WebRead,
8
+ type WebReadService,
9
+ } from "@effect-uai/core/WebRead"
10
+ import type { JinaEngine } from "./models.js"
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Public types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Jina-typed read request. Widens {@link CommonReadRequest} with Jina
18
+ * Reader's own header knobs; the common fields map onto Reader headers (see
19
+ * the codec below).
20
+ */
21
+ export type JinaReadRequest = CommonReadRequest & {
22
+ /** Fetching engine - see {@link JinaEngine}. */
23
+ readonly engine?: JinaEngine
24
+ /** CSS selector to extract instead of the whole page (`X-Target-Selector`). */
25
+ readonly targetSelector?: string
26
+ /** Bypass Jina's page cache (`X-No-Cache`). */
27
+ readonly noCache?: boolean
28
+ /** Append a deduplicated link list, populating {@link ReadResponse.links}. */
29
+ readonly withLinksSummary?: boolean
30
+ }
31
+
32
+ export type JinaReaderService = {
33
+ readonly read: (request: JinaReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>
34
+ }
35
+
36
+ /**
37
+ * Provider-typed service tag. Yield this for Jina-typed reads (its own
38
+ * Reader headers); yield the generic `WebRead` tag for provider-portable
39
+ * code. Both are registered by {@link layer}.
40
+ */
41
+ export class JinaReader extends Context.Service<JinaReader, JinaReaderService>()(
42
+ "@betalyra/effect-uai/providers/jina/JinaReader",
43
+ ) {}
44
+
45
+ export type Config = {
46
+ readonly apiKey: Redacted.Redacted
47
+ readonly baseUrl?: string
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Codec - request headers
52
+ // ---------------------------------------------------------------------------
53
+
54
+ // Reader is header-driven: the URL is the path, everything else is a header.
55
+ // `markdown` / `html` map one-to-one onto `X-Return-Format`.
56
+ const buildHeaders = (request: JinaReadRequest): Record<string, string> => {
57
+ const format = request.format ?? "markdown"
58
+ return {
59
+ Accept: "application/json",
60
+ "X-Return-Format": format,
61
+ ...(request.timeout !== undefined && {
62
+ // `X-Timeout` is whole seconds, capped at 180 by Jina.
63
+ "X-Timeout": String(Math.max(1, Math.round(Duration.toMillis(request.timeout) / 1000))),
64
+ }),
65
+ ...(request.engine !== undefined && { "X-Engine": request.engine }),
66
+ ...(request.targetSelector !== undefined && { "X-Target-Selector": request.targetSelector }),
67
+ ...(request.noCache === true && { "X-No-Cache": "true" }),
68
+ ...(request.withLinksSummary === true && { "X-With-Links-Summary": "true" }),
69
+ }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Codec - response
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const WireData = Schema.Struct({
77
+ title: Schema.optional(Schema.NullOr(Schema.String)),
78
+ url: Schema.optional(Schema.NullOr(Schema.String)),
79
+ // `content` carries markdown/text; `html` carries html. Exactly one is set,
80
+ // depending on the requested format.
81
+ content: Schema.optional(Schema.NullOr(Schema.String)),
82
+ html: Schema.optional(Schema.NullOr(Schema.String)),
83
+ // Present only with `X-With-Links-Summary`; a { anchorText: url } record.
84
+ links: Schema.optional(Schema.Unknown),
85
+ })
86
+
87
+ const WireResponse = Schema.Struct({
88
+ code: Schema.optional(Schema.Number),
89
+ status: Schema.optional(Schema.Number),
90
+ data: WireData,
91
+ })
92
+ type WireResponse = typeof WireResponse.Type
93
+
94
+ // Jina reports links as a { text: url } record; take the URLs. Tolerate an
95
+ // array shape too, and omit when empty.
96
+ const extractLinks = (links: unknown): ReadonlyArray<string> | undefined => {
97
+ const values = Array.isArray(links)
98
+ ? links
99
+ : links !== null && typeof links === "object"
100
+ ? Object.values(links as Record<string, unknown>)
101
+ : []
102
+ const urls = values.filter((v): v is string => typeof v === "string")
103
+ return urls.length > 0 ? urls : undefined
104
+ }
105
+
106
+ const toResponse = (wire: WireResponse, request: JinaReadRequest): ReadResponse => {
107
+ const { data } = wire
108
+ const format = request.format ?? "markdown"
109
+ const content = (format === "html" ? data.html : data.content) ?? ""
110
+ const title = data.title ?? undefined
111
+ const links = extractLinks(data.links)
112
+ return {
113
+ url: data.url ?? request.url,
114
+ content,
115
+ ...(title !== undefined && { title }),
116
+ ...(links !== undefined && { links }),
117
+ raw: wire,
118
+ }
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // HTTP
123
+ // ---------------------------------------------------------------------------
124
+
125
+ const transportFailure = (cause: unknown): AiError.AiError =>
126
+ new AiError.Unavailable({ provider: "jina", raw: cause })
127
+
128
+ const httpStatusError = (status: number, body: string): AiError.AiError => {
129
+ const provider = "jina"
130
+ const raw = body
131
+ if (status === 429) return new AiError.RateLimited({ provider, raw })
132
+ if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
133
+ if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
134
+ if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
135
+ if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
136
+ if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
137
+ return new AiError.InvalidRequest({ provider, raw })
138
+ }
139
+
140
+ const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://r.jina.ai"
141
+
142
+ const getRead = (
143
+ cfg: Config,
144
+ request: JinaReadRequest,
145
+ ): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>
146
+ Effect.gen(function* () {
147
+ const client = yield* HttpClient.HttpClient
148
+ // The target URL is appended as the path: `https://r.jina.ai/<url>`.
149
+ const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/${request.url}`).pipe(
150
+ HttpClientRequest.bearerToken(cfg.apiKey),
151
+ HttpClientRequest.setHeaders(buildHeaders(request)),
152
+ )
153
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
154
+ if (response.status >= 400) {
155
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
156
+ return yield* httpStatusError(response.status, text)
157
+ }
158
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure))
159
+ return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(
160
+ Effect.mapError(transportFailure),
161
+ )
162
+ })
163
+
164
+ const readImpl =
165
+ (cfg: Config) =>
166
+ (request: JinaReadRequest): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>
167
+ Effect.map(getRead(cfg, request), (wire) => toResponse(wire, request))
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Constructors
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Build a `JinaReaderService` value. For Layer-based setup, prefer
175
+ * {@link layer}.
176
+ */
177
+ export const make = (cfg: Config): Effect.Effect<JinaReaderService, never, HttpClient.HttpClient> =>
178
+ Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
179
+ read: (request) =>
180
+ readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
181
+ }))
182
+
183
+ /**
184
+ * Layer registering both the provider-typed `JinaReader` tag and the generic
185
+ * `WebRead` tag over one implementation. A `CommonReadRequest` is
186
+ * structurally a `JinaReadRequest` with the extra knobs unset, so the generic
187
+ * registration forwards directly.
188
+ */
189
+ export const layer = (
190
+ cfg: Config,
191
+ ): Layer.Layer<JinaReader | WebRead, never, HttpClient.HttpClient> => {
192
+ const typed = Layer.effect(JinaReader, make(cfg))
193
+ const generic = Layer.effect(
194
+ WebRead,
195
+ Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),
196
+ )
197
+ return Layer.merge(typed, generic)
198
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * as JinaEmbedding from "./JinaEmbedding.js"
2
+ export * as JinaReader from "./JinaReader.js"
2
3
  export * from "./models.js"
package/src/models.ts CHANGED
@@ -25,3 +25,15 @@ export type JinaEmbeddingModel =
25
25
  | "jina-clip-v2"
26
26
  // eslint-disable-next-line @typescript-eslint/ban-types
27
27
  | (string & {})
28
+
29
+ /**
30
+ * Reader fetching engine (`X-Engine`). `browser` renders JavaScript,
31
+ * `curl` is a fast static fetch, `auto` picks per page. Provider-specific,
32
+ * so it lives on `JinaReadRequest`.
33
+ */
34
+ export type JinaEngine =
35
+ | "browser"
36
+ | "curl"
37
+ | "auto"
38
+ // eslint-disable-next-line @typescript-eslint/ban-types
39
+ | (string & {})