@effect-uai/jina 0.3.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.
- package/LICENSE +21 -0
- package/dist/JinaEmbedding.d.mts +86 -0
- package/dist/JinaEmbedding.d.mts.map +1 -0
- package/dist/JinaEmbedding.mjs +269 -0
- package/dist/JinaEmbedding.mjs.map +1 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/models.d.mts +24 -0
- package/dist/models.d.mts.map +1 -0
- package/dist/models.mjs +1 -0
- package/package.json +58 -0
- package/src/JinaEmbedding.ts +531 -0
- package/src/index.ts +2 -0
- package/src/models.ts +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Betalyra Sociedade Unipessoal Lda.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { JinaEmbeddingModel } 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 { CommonEmbedRequest, EmbedManyResponse, EmbedResponse, EmbeddingModel } from "@effect-uai/core/EmbeddingModel";
|
|
6
|
+
import { EmbedInput } from "@effect-uai/core/Embedding";
|
|
7
|
+
|
|
8
|
+
//#region src/JinaEmbedding.d.ts
|
|
9
|
+
declare namespace JinaEmbedding_d_exports {
|
|
10
|
+
export { Config, JinaEmbedManyRequest, JinaEmbedRequest, JinaEmbedding, JinaEmbeddingService, JinaEncoding, JinaTask, layer, make };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Jina's task vocabulary. v4 and v3/v5 share the dotted-pair form:
|
|
14
|
+
* - `retrieval.query` / `retrieval.passage` — asymmetric retrieval.
|
|
15
|
+
* - `text-matching` — symmetric similarity between two texts.
|
|
16
|
+
* - `code.query` / `code.passage` — code retrieval / matching (v4-only).
|
|
17
|
+
* - `classification` / `separation` — v3 / v5 only.
|
|
18
|
+
*
|
|
19
|
+
* The `(string & {})` tail accepts any string so newly-released task
|
|
20
|
+
* types work without an SDK update.
|
|
21
|
+
*/
|
|
22
|
+
type JinaTask = "retrieval.query" | "retrieval.passage" | "text-matching" | "code.query" | "code.passage" | "classification" | "separation" | (string & {});
|
|
23
|
+
/**
|
|
24
|
+
* Jina's supported `encoding` values. Provider-narrowed from the cross-
|
|
25
|
+
* provider `Encoding` union.
|
|
26
|
+
*
|
|
27
|
+
* Compatibility with models is checked at the response level rather than
|
|
28
|
+
* pre-flight: if you ask for `multivector` against a model that doesn't
|
|
29
|
+
* produce one, Jina's API rejects the request with a clear error; if
|
|
30
|
+
* you ask for `sparse` and the chosen model returns dense, our decoder
|
|
31
|
+
* fails with a typed mismatch. No hardcoded model-encoding table to
|
|
32
|
+
* maintain.
|
|
33
|
+
*
|
|
34
|
+
* Wire mapping:
|
|
35
|
+
* - `multivector` → request sets `return_multivector: true`
|
|
36
|
+
* (`jina-embeddings-v4` + ColBERT models).
|
|
37
|
+
* - `sparse` → no flag; the model itself (`elser-v2`) determines the
|
|
38
|
+
* output shape.
|
|
39
|
+
* - `binary` → request sets `embedding_type: "binary"`.
|
|
40
|
+
* - `float32` → no flag; default JSON `number[]`.
|
|
41
|
+
*/
|
|
42
|
+
type JinaEncoding = "float32" | "binary" | "sparse" | "multivector";
|
|
43
|
+
type JinaEmbedRequest = Omit<CommonEmbedRequest, "model" | "task" | "encoding"> & {
|
|
44
|
+
/** Narrows `CommonEmbedRequest.model` to the typed Jina union. */readonly model: JinaEmbeddingModel;
|
|
45
|
+
/**
|
|
46
|
+
* Widens the cross-provider `"query" | "document"` to Jina's task enum.
|
|
47
|
+
* Required by v3+ for retrieval-quality results. v4 unifies query and
|
|
48
|
+
* document under `retrieval`.
|
|
49
|
+
*/
|
|
50
|
+
readonly task?: JinaTask; /** Narrows `CommonEmbedRequest.encoding` to Jina's supported set. */
|
|
51
|
+
readonly encoding?: JinaEncoding;
|
|
52
|
+
};
|
|
53
|
+
type JinaEmbedManyRequest = Omit<JinaEmbedRequest, "input"> & {
|
|
54
|
+
readonly inputs: ReadonlyArray<EmbedInput>;
|
|
55
|
+
};
|
|
56
|
+
type JinaEmbeddingService = {
|
|
57
|
+
readonly embed: (request: JinaEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>;
|
|
58
|
+
readonly embedMany: (request: JinaEmbedManyRequest) => Effect.Effect<EmbedManyResponse, AiError.AiError>;
|
|
59
|
+
};
|
|
60
|
+
declare const JinaEmbedding_base: Context.ServiceClass<JinaEmbedding, "@betalyra/effect-uai/providers/jina/JinaEmbedding", JinaEmbeddingService>;
|
|
61
|
+
/**
|
|
62
|
+
* Provider-typed service tag. Yield this for Jina-typed embedding
|
|
63
|
+
* requests; yield the generic `EmbeddingModel` tag for provider-portable
|
|
64
|
+
* code. Both are registered by `layer`.
|
|
65
|
+
*/
|
|
66
|
+
declare class JinaEmbedding extends JinaEmbedding_base {}
|
|
67
|
+
type Config = {
|
|
68
|
+
readonly apiKey: Redacted.Redacted;
|
|
69
|
+
readonly baseUrl?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Build a `JinaEmbeddingService` value. For Layer-based setup, prefer
|
|
73
|
+
* `layer`.
|
|
74
|
+
*/
|
|
75
|
+
declare const make: (cfg: Config) => Effect.Effect<JinaEmbeddingService, never, HttpClient.HttpClient>;
|
|
76
|
+
/**
|
|
77
|
+
* Layer that registers both the provider-specific `JinaEmbedding` tag
|
|
78
|
+
* and the generic `EmbeddingModel` tag, sharing one underlying
|
|
79
|
+
* implementation. The generic registration maps cross-provider
|
|
80
|
+
* `query`/`document` to Jina's `retrieval.query` / `retrieval.passage`
|
|
81
|
+
* task strings; other request fields pass through.
|
|
82
|
+
*/
|
|
83
|
+
declare const layer: (cfg: Config) => Layer.Layer<JinaEmbedding | EmbeddingModel, never, HttpClient.HttpClient>;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { Config, JinaEmbedManyRequest, JinaEmbedRequest, JinaEmbedding, JinaEmbeddingService, JinaEncoding, JinaTask, layer, make, JinaEmbedding_d_exports as t };
|
|
86
|
+
//# sourceMappingURL=JinaEmbedding.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JinaEmbedding.d.mts","names":[],"sources":["../src/JinaEmbedding.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;KA6BY,QAAA;;;;AAAZ;;;;;AA8BA;;;;;AAEA;;;;;;KAFY,YAAA;AAAA,KAEA,gBAAA,GAAmB,IAAA,CAAK,kBAAA;EAUF,2EARvB,KAAA,EAAO,kBAAA;EAFa;;;;;EAAA,SAQpB,IAAA,GAAO,QAAA,EAEP;EAAA,SAAA,QAAA,GAAW,YAAA;AAAA;AAAA,KAGV,oBAAA,GAAuB,IAAA,CAAK,gBAAA;EAAA,SAC7B,MAAA,EAAQ,aAAA,CAAc,UAAA;AAAA;AAAA,KAGrB,oBAAA;EAAA,SACD,KAAA,GAAQ,OAAA,EAAS,gBAAA,KAAqB,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,OAAA,CAAQ,OAAA;EAAA,SAC3E,SAAA,GACP,OAAA,EAAS,oBAAA,KACN,MAAA,CAAO,MAAA,CAAO,iBAAA,EAAmB,OAAA,CAAQ,OAAA;AAAA;AAAA,cAC/C,kBAAA;;;;;;cAOY,aAAA,SAAsB,kBAAA;AAAA,KAIvB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;;cAoYE,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,oBAAA,SAA6B,UAAA,CAAW,UAAA;;;;;;;;cAe5C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,aAAA,GAAgB,cAAA,SAAuB,UAAA,CAAW,UAAA"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { Context, Effect, Encoding, Layer, Match, Result, Schema } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { EmbeddingModel } from "@effect-uai/core/EmbeddingModel";
|
|
6
|
+
//#region src/JinaEmbedding.ts
|
|
7
|
+
var JinaEmbedding_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
JinaEmbedding: () => JinaEmbedding,
|
|
9
|
+
layer: () => layer,
|
|
10
|
+
make: () => make
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Provider-typed service tag. Yield this for Jina-typed embedding
|
|
14
|
+
* requests; yield the generic `EmbeddingModel` tag for provider-portable
|
|
15
|
+
* code. Both are registered by `layer`.
|
|
16
|
+
*/
|
|
17
|
+
var JinaEmbedding = class extends Context.Service()("@betalyra/effect-uai/providers/jina/JinaEmbedding") {};
|
|
18
|
+
/**
|
|
19
|
+
* Jina v4 accepts `{ image: <url> }` or `{ image: <base64-string> }`.
|
|
20
|
+
* The server detects which form by looking at the value. We pass URLs
|
|
21
|
+
* through directly and base64-encode raw bytes.
|
|
22
|
+
*/
|
|
23
|
+
const imageSourceToItem = Match.type().pipe(Match.tag("url", (s) => ({ image: s.url })), Match.tag("base64", (s) => ({ image: s.base64 })), Match.tag("bytes", (s) => ({ image: Encoding.encodeBase64(s.bytes) })), Match.exhaustive);
|
|
24
|
+
const contentPartToItem = Match.type().pipe(Match.when({ text: Match.string }, ({ text }) => ({ text })), Match.when({ image: Match.any }, ({ image }) => imageSourceToItem(image)), Match.exhaustive);
|
|
25
|
+
const multiPartContentRejected = new AiError.InvalidRequest({
|
|
26
|
+
provider: "jina",
|
|
27
|
+
param: "input.content",
|
|
28
|
+
raw: "Jina treats each input[] entry as one item; multi-part `content[]` would lose the grouping. Split into separate `inputs[]` entries."
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Project an `EmbedInput` to one wire item. A single-part `content[]` is
|
|
32
|
+
* unwrapped; multi-part `content[]` is rejected because Jina's flat
|
|
33
|
+
* `input[]` would lose the grouping.
|
|
34
|
+
*/
|
|
35
|
+
const inputToItem = Match.type().pipe(Match.when(Match.string, (s) => Effect.succeed({ text: s })), Match.when({ text: Match.string }, ({ text }) => Effect.succeed({ text })), Match.when({ image: Match.any }, ({ image }) => Effect.succeed(imageSourceToItem(image))), Match.when({ content: Match.any }, ({ content }) => content.length === 1 ? Effect.succeed(contentPartToItem(content[0])) : Effect.fail(multiPartContentRejected)), Match.exhaustive);
|
|
36
|
+
/**
|
|
37
|
+
* Map our `JinaEncoding` to Jina's `embedding_type` wire field. Only
|
|
38
|
+
* `binary` actually changes the wire transport; the others either omit
|
|
39
|
+
* the field (`float32`) or ignore it (`sparse` / `multivector` use
|
|
40
|
+
* separate signalling — model choice and `return_multivector`).
|
|
41
|
+
*/
|
|
42
|
+
const encodingToQuant = Match.type().pipe(Match.when("binary", () => "binary"), Match.orElse(() => void 0));
|
|
43
|
+
const buildBody = (request) => {
|
|
44
|
+
const quant = encodingToQuant(request.encoding);
|
|
45
|
+
return {
|
|
46
|
+
model: request.model,
|
|
47
|
+
input: request.items,
|
|
48
|
+
...request.task !== void 0 && { task: request.task },
|
|
49
|
+
...request.dimensions !== void 0 && { dimensions: request.dimensions },
|
|
50
|
+
...quant !== void 0 && { embedding_type: quant },
|
|
51
|
+
...request.encoding === "multivector" && { return_multivector: true }
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Wire response payloads. Split into four discriminated schemas so the
|
|
56
|
+
* decoder can narrow precisely via `Schema.is`:
|
|
57
|
+
*
|
|
58
|
+
* - **Dense float** (`object: "embedding"`, `embedding: number[]`) —
|
|
59
|
+
* default JSON form.
|
|
60
|
+
* - **Dense base64** (`object: "embedding"`, `embedding: string`) — set
|
|
61
|
+
* when `embedding_type` was `base64` or `binary`.
|
|
62
|
+
* - **Sparse** (`object: "embedding"`, `embedding: Record<string,
|
|
63
|
+
* number>`) — token-keyed weights from an ELSER-style model.
|
|
64
|
+
* - **Multivector** (`object: "embeddings"`, plural) — token-level
|
|
65
|
+
* vectors when `return_multivector: true` or a ColBERT model is used.
|
|
66
|
+
*/
|
|
67
|
+
const WireDenseFloatPayload = Schema.Struct({
|
|
68
|
+
object: Schema.Literal("embedding"),
|
|
69
|
+
index: Schema.Number,
|
|
70
|
+
embedding: Schema.Array(Schema.Number)
|
|
71
|
+
});
|
|
72
|
+
const WireDenseBase64Payload = Schema.Struct({
|
|
73
|
+
object: Schema.Literal("embedding"),
|
|
74
|
+
index: Schema.Number,
|
|
75
|
+
embedding: Schema.String
|
|
76
|
+
});
|
|
77
|
+
const WireSparsePayload = Schema.Struct({
|
|
78
|
+
object: Schema.Literal("embedding"),
|
|
79
|
+
index: Schema.Number,
|
|
80
|
+
embedding: Schema.Record(Schema.String, Schema.Number)
|
|
81
|
+
});
|
|
82
|
+
const WireMultiPayload = Schema.Struct({
|
|
83
|
+
object: Schema.Literal("embeddings"),
|
|
84
|
+
index: Schema.Number,
|
|
85
|
+
embeddings: Schema.Array(Schema.Array(Schema.Number)),
|
|
86
|
+
tokenized_input: Schema.optional(Schema.Array(Schema.String))
|
|
87
|
+
});
|
|
88
|
+
const WirePayload = Schema.Union([
|
|
89
|
+
WireDenseFloatPayload,
|
|
90
|
+
WireSparsePayload,
|
|
91
|
+
WireDenseBase64Payload,
|
|
92
|
+
WireMultiPayload
|
|
93
|
+
]);
|
|
94
|
+
const isMultiPayload = Schema.is(WireMultiPayload);
|
|
95
|
+
const isSparsePayload = Schema.is(WireSparsePayload);
|
|
96
|
+
const isDenseBase64Payload = Schema.is(WireDenseBase64Payload);
|
|
97
|
+
const WireUsage = Schema.Struct({
|
|
98
|
+
total_tokens: Schema.Number,
|
|
99
|
+
prompt_tokens: Schema.optional(Schema.Number)
|
|
100
|
+
});
|
|
101
|
+
const WireResponse = Schema.Struct({
|
|
102
|
+
data: Schema.Array(WirePayload),
|
|
103
|
+
model: Schema.String,
|
|
104
|
+
usage: WireUsage
|
|
105
|
+
});
|
|
106
|
+
const transportFailure = (cause) => new AiError.Unavailable({
|
|
107
|
+
provider: "jina",
|
|
108
|
+
raw: cause
|
|
109
|
+
});
|
|
110
|
+
const encodingMismatch = (expected, got) => new AiError.InvalidRequest({
|
|
111
|
+
provider: "jina",
|
|
112
|
+
param: "encoding",
|
|
113
|
+
raw: `requested encoding="${expected}" but the response contains a ${got} embedding - confirm the chosen model supports the requested encoding`
|
|
114
|
+
});
|
|
115
|
+
/** Decode a base64 wire string to the right dense `Embedding` arm. */
|
|
116
|
+
const decodeBase64Embedding = (b64, encoding) => Result.match(Encoding.decodeBase64(b64), {
|
|
117
|
+
onFailure: (cause) => Effect.fail(transportFailure(cause)),
|
|
118
|
+
onSuccess: (bytes) => Match.value(encoding).pipe(Match.when("binary", () => Effect.succeed({
|
|
119
|
+
_tag: "binary",
|
|
120
|
+
vector: bytes
|
|
121
|
+
})), Match.orElse(() => Effect.succeed({
|
|
122
|
+
_tag: "float32",
|
|
123
|
+
vector: new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4)
|
|
124
|
+
})))
|
|
125
|
+
});
|
|
126
|
+
/**
|
|
127
|
+
* Project a wire payload to our `Embedding` discriminated union and
|
|
128
|
+
* verify the requested encoding matches what came back.
|
|
129
|
+
*/
|
|
130
|
+
const payloadToEmbedding = (payload, encoding) => {
|
|
131
|
+
if (isMultiPayload(payload)) return encoding === "multivector" ? Effect.succeed({
|
|
132
|
+
_tag: "multivector",
|
|
133
|
+
vectors: payload.embeddings.map((v) => Float32Array.from(v))
|
|
134
|
+
}) : Effect.fail(encodingMismatch(encoding ?? "float32", "multivector"));
|
|
135
|
+
if (isSparsePayload(payload)) return encoding === "sparse" ? Effect.succeed({
|
|
136
|
+
_tag: "sparse",
|
|
137
|
+
weights: payload.embedding
|
|
138
|
+
}) : Effect.fail(encodingMismatch(encoding ?? "float32", "sparse"));
|
|
139
|
+
if (encoding === "multivector" || encoding === "sparse") return Effect.fail(encodingMismatch(encoding, "dense"));
|
|
140
|
+
return isDenseBase64Payload(payload) ? decodeBase64Embedding(payload.embedding, encoding) : Effect.succeed({
|
|
141
|
+
_tag: "float32",
|
|
142
|
+
vector: Float32Array.from(payload.embedding)
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
const indexOf = (p) => p.index;
|
|
146
|
+
const usageOf = (u) => ({ inputTokens: u.total_tokens });
|
|
147
|
+
const httpStatusError = (status, body) => {
|
|
148
|
+
const provider = "jina";
|
|
149
|
+
const raw = body;
|
|
150
|
+
if (status === 429) return new AiError.RateLimited({
|
|
151
|
+
provider,
|
|
152
|
+
raw
|
|
153
|
+
});
|
|
154
|
+
if (status === 408 || status === 504) return new AiError.Timeout({
|
|
155
|
+
provider,
|
|
156
|
+
raw
|
|
157
|
+
});
|
|
158
|
+
if (status === 401) return new AiError.AuthFailed({
|
|
159
|
+
provider,
|
|
160
|
+
subtype: "auth",
|
|
161
|
+
raw
|
|
162
|
+
});
|
|
163
|
+
if (status === 403) return new AiError.AuthFailed({
|
|
164
|
+
provider,
|
|
165
|
+
subtype: "permission",
|
|
166
|
+
raw
|
|
167
|
+
});
|
|
168
|
+
if (status === 402) return new AiError.AuthFailed({
|
|
169
|
+
provider,
|
|
170
|
+
subtype: "billing",
|
|
171
|
+
raw
|
|
172
|
+
});
|
|
173
|
+
if (status === 413) return new AiError.ContextLengthExceeded({
|
|
174
|
+
provider,
|
|
175
|
+
raw
|
|
176
|
+
});
|
|
177
|
+
if (status >= 500) return new AiError.Unavailable({
|
|
178
|
+
provider,
|
|
179
|
+
status,
|
|
180
|
+
raw
|
|
181
|
+
});
|
|
182
|
+
return new AiError.InvalidRequest({
|
|
183
|
+
provider,
|
|
184
|
+
raw
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.jina.ai/v1";
|
|
188
|
+
const postEmbed = (cfg, body) => Effect.gen(function* () {
|
|
189
|
+
const client = yield* HttpClient.HttpClient;
|
|
190
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/embeddings`).pipe(HttpClientRequest.bearerToken(cfg.apiKey), HttpClientRequest.bodyJsonUnsafe(body));
|
|
191
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
192
|
+
if (response.status >= 400) {
|
|
193
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
194
|
+
return yield* Effect.fail(httpStatusError(response.status, text));
|
|
195
|
+
}
|
|
196
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure));
|
|
197
|
+
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(Effect.mapError(transportFailure));
|
|
198
|
+
});
|
|
199
|
+
const orderedEmbeddings = (data, encoding) => Effect.forEach([...data].sort((a, b) => indexOf(a) - indexOf(b)), (item) => payloadToEmbedding(item, encoding));
|
|
200
|
+
const embedImpl = (cfg) => (request) => inputToItem(request.input).pipe(Effect.flatMap((item) => postEmbed(cfg, buildBody({
|
|
201
|
+
model: request.model,
|
|
202
|
+
...request.task !== void 0 && { task: request.task },
|
|
203
|
+
...request.dimensions !== void 0 && { dimensions: request.dimensions },
|
|
204
|
+
...request.encoding !== void 0 && { encoding: request.encoding },
|
|
205
|
+
items: [item]
|
|
206
|
+
}))), Effect.flatMap((decoded) => {
|
|
207
|
+
const first = decoded.data[0];
|
|
208
|
+
if (first === void 0) return Effect.fail(transportFailure("Jina returned empty `data` array"));
|
|
209
|
+
return payloadToEmbedding(first, request.encoding).pipe(Effect.map((embedding) => ({
|
|
210
|
+
embedding,
|
|
211
|
+
usage: usageOf(decoded.usage)
|
|
212
|
+
})));
|
|
213
|
+
}));
|
|
214
|
+
const embedManyImpl = (cfg) => (request) => Effect.forEach(request.inputs, inputToItem).pipe(Effect.flatMap((items) => postEmbed(cfg, buildBody({
|
|
215
|
+
model: request.model,
|
|
216
|
+
...request.task !== void 0 && { task: request.task },
|
|
217
|
+
...request.dimensions !== void 0 && { dimensions: request.dimensions },
|
|
218
|
+
...request.encoding !== void 0 && { encoding: request.encoding },
|
|
219
|
+
items
|
|
220
|
+
}))), Effect.flatMap((decoded) => orderedEmbeddings(decoded.data, request.encoding).pipe(Effect.map((embeddings) => ({
|
|
221
|
+
embeddings,
|
|
222
|
+
usage: usageOf(decoded.usage)
|
|
223
|
+
})))));
|
|
224
|
+
/**
|
|
225
|
+
* Map cross-provider `query`/`document` to Jina's dotted-pair task
|
|
226
|
+
* vocabulary. `undefined` stays `undefined`.
|
|
227
|
+
*/
|
|
228
|
+
const mapGenericTask = Match.type().pipe(Match.when("query", () => "retrieval.query"), Match.when("document", () => "retrieval.passage"), Match.orElse(() => void 0));
|
|
229
|
+
/**
|
|
230
|
+
* Build a `JinaEmbeddingService` value. For Layer-based setup, prefer
|
|
231
|
+
* `layer`.
|
|
232
|
+
*/
|
|
233
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
|
|
234
|
+
embed: (request) => embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
235
|
+
embedMany: (request) => embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client))
|
|
236
|
+
}));
|
|
237
|
+
/**
|
|
238
|
+
* Layer that registers both the provider-specific `JinaEmbedding` tag
|
|
239
|
+
* and the generic `EmbeddingModel` tag, sharing one underlying
|
|
240
|
+
* implementation. The generic registration maps cross-provider
|
|
241
|
+
* `query`/`document` to Jina's `retrieval.query` / `retrieval.passage`
|
|
242
|
+
* task strings; other request fields pass through.
|
|
243
|
+
*/
|
|
244
|
+
const layer = (cfg) => {
|
|
245
|
+
const typed = Layer.effect(JinaEmbedding, make(cfg));
|
|
246
|
+
const generic = Layer.effect(EmbeddingModel, Effect.map(make(cfg), (s) => ({
|
|
247
|
+
embed: (req) => {
|
|
248
|
+
const task = mapGenericTask(req.task);
|
|
249
|
+
return s.embed({
|
|
250
|
+
...req,
|
|
251
|
+
model: req.model,
|
|
252
|
+
...task !== void 0 && { task }
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
embedMany: (req) => {
|
|
256
|
+
const task = mapGenericTask(req.task);
|
|
257
|
+
return s.embedMany({
|
|
258
|
+
...req,
|
|
259
|
+
model: req.model,
|
|
260
|
+
...task !== void 0 && { task }
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
})));
|
|
264
|
+
return Layer.merge(typed, generic);
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
export { JinaEmbedding, layer, make, JinaEmbedding_exports as t };
|
|
268
|
+
|
|
269
|
+
//# sourceMappingURL=JinaEmbedding.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JinaEmbedding.mjs","names":[],"sources":["../src/JinaEmbedding.ts"],"sourcesContent":["import { Context, Effect, Encoding, Layer, Match, Redacted, Result, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { Embedding, EmbedContentPart, EmbedInput, Usage } from \"@effect-uai/core/Embedding\"\nimport {\n type CommonEmbedManyRequest,\n type CommonEmbedRequest,\n EmbeddingModel,\n type EmbeddingModelService,\n type EmbedManyResponse,\n type EmbedResponse,\n} from \"@effect-uai/core/EmbeddingModel\"\nimport type { ImageSource } from \"@effect-uai/core/Image\"\nimport type { JinaEmbeddingModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Jina's task vocabulary. v4 and v3/v5 share the dotted-pair form:\n * - `retrieval.query` / `retrieval.passage` — asymmetric retrieval.\n * - `text-matching` — symmetric similarity between two texts.\n * - `code.query` / `code.passage` — code retrieval / matching (v4-only).\n * - `classification` / `separation` — v3 / v5 only.\n *\n * The `(string & {})` tail accepts any string so newly-released task\n * types work without an SDK update.\n */\nexport type JinaTask =\n | \"retrieval.query\"\n | \"retrieval.passage\"\n | \"text-matching\"\n | \"code.query\"\n | \"code.passage\"\n | \"classification\"\n | \"separation\"\n // eslint-disable-next-line @typescript-eslint/ban-types\n | (string & {})\n\n/**\n * Jina's supported `encoding` values. Provider-narrowed from the cross-\n * provider `Encoding` union.\n *\n * Compatibility with models is checked at the response level rather than\n * pre-flight: if you ask for `multivector` against a model that doesn't\n * produce one, Jina's API rejects the request with a clear error; if\n * you ask for `sparse` and the chosen model returns dense, our decoder\n * fails with a typed mismatch. No hardcoded model-encoding table to\n * maintain.\n *\n * Wire mapping:\n * - `multivector` → request sets `return_multivector: true`\n * (`jina-embeddings-v4` + ColBERT models).\n * - `sparse` → no flag; the model itself (`elser-v2`) determines the\n * output shape.\n * - `binary` → request sets `embedding_type: \"binary\"`.\n * - `float32` → no flag; default JSON `number[]`.\n */\nexport type JinaEncoding = \"float32\" | \"binary\" | \"sparse\" | \"multivector\"\n\nexport type JinaEmbedRequest = Omit<CommonEmbedRequest, \"model\" | \"task\" | \"encoding\"> & {\n /** Narrows `CommonEmbedRequest.model` to the typed Jina union. */\n readonly model: JinaEmbeddingModel\n /**\n * Widens the cross-provider `\"query\" | \"document\"` to Jina's task enum.\n * Required by v3+ for retrieval-quality results. v4 unifies query and\n * document under `retrieval`.\n */\n readonly task?: JinaTask\n /** Narrows `CommonEmbedRequest.encoding` to Jina's supported set. */\n readonly encoding?: JinaEncoding\n}\n\nexport type JinaEmbedManyRequest = Omit<JinaEmbedRequest, \"input\"> & {\n readonly inputs: ReadonlyArray<EmbedInput>\n}\n\nexport type JinaEmbeddingService = {\n readonly embed: (request: JinaEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>\n readonly embedMany: (\n request: JinaEmbedManyRequest,\n ) => Effect.Effect<EmbedManyResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Jina-typed embedding\n * requests; yield the generic `EmbeddingModel` tag for provider-portable\n * code. Both are registered by `layer`.\n */\nexport class JinaEmbedding extends Context.Service<JinaEmbedding, JinaEmbeddingService>()(\n \"@betalyra/effect-uai/providers/jina/JinaEmbedding\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireTextItem = {\n readonly text: string\n}\ntype WireImageItem = {\n readonly image: string\n}\ntype WireItem = WireTextItem | WireImageItem\n\n/**\n * Jina v4 accepts `{ image: <url> }` or `{ image: <base64-string> }`.\n * The server detects which form by looking at the value. We pass URLs\n * through directly and base64-encode raw bytes.\n */\nconst imageSourceToItem: (s: ImageSource) => WireItem = Match.type<ImageSource>().pipe(\n Match.tag(\"url\", (s): WireItem => ({ image: s.url })),\n Match.tag(\"base64\", (s): WireItem => ({ image: s.base64 })),\n Match.tag(\"bytes\", (s): WireItem => ({ image: Encoding.encodeBase64(s.bytes) })),\n Match.exhaustive,\n)\n\nconst contentPartToItem: (part: EmbedContentPart) => WireItem = Match.type<EmbedContentPart>().pipe(\n Match.when({ text: Match.string }, ({ text }): WireItem => ({ text })),\n Match.when({ image: Match.any }, ({ image }) => imageSourceToItem(image)),\n Match.exhaustive,\n)\n\nconst multiPartContentRejected: AiError.AiError = new AiError.InvalidRequest({\n provider: \"jina\",\n param: \"input.content\",\n raw: \"Jina treats each input[] entry as one item; multi-part `content[]` would lose the grouping. Split into separate `inputs[]` entries.\",\n})\n\n/**\n * Project an `EmbedInput` to one wire item. A single-part `content[]` is\n * unwrapped; multi-part `content[]` is rejected because Jina's flat\n * `input[]` would lose the grouping.\n */\nconst inputToItem: (input: EmbedInput) => Effect.Effect<WireItem, AiError.AiError> =\n Match.type<EmbedInput>().pipe(\n Match.when(\n Match.string,\n (s): Effect.Effect<WireItem, AiError.AiError> => Effect.succeed({ text: s }),\n ),\n Match.when(\n { text: Match.string },\n ({ text }): Effect.Effect<WireItem, AiError.AiError> => Effect.succeed({ text }),\n ),\n Match.when(\n { image: Match.any },\n ({ image }): Effect.Effect<WireItem, AiError.AiError> =>\n Effect.succeed(imageSourceToItem(image)),\n ),\n Match.when(\n { content: Match.any },\n ({ content }): Effect.Effect<WireItem, AiError.AiError> =>\n content.length === 1\n ? Effect.succeed(contentPartToItem(content[0]!))\n : Effect.fail(multiPartContentRejected),\n ),\n Match.exhaustive,\n )\n\ntype WireBody = {\n readonly model: string\n readonly input: ReadonlyArray<WireItem>\n readonly task?: string\n readonly dimensions?: number\n readonly embedding_type?: \"float\" | \"base64\" | \"binary\" | \"ubinary\"\n readonly return_multivector?: boolean\n}\n\ntype WireQuant = WireBody[\"embedding_type\"]\n\n/**\n * Map our `JinaEncoding` to Jina's `embedding_type` wire field. Only\n * `binary` actually changes the wire transport; the others either omit\n * the field (`float32`) or ignore it (`sparse` / `multivector` use\n * separate signalling — model choice and `return_multivector`).\n */\nconst encodingToQuant: (encoding: JinaEncoding | undefined) => WireQuant = Match.type<\n JinaEncoding | undefined\n>().pipe(\n Match.when(\"binary\", (): WireQuant => \"binary\"),\n Match.orElse((): WireQuant => undefined),\n)\n\nconst buildBody = (request: {\n readonly model: JinaEmbeddingModel\n readonly task?: JinaTask\n readonly dimensions?: number\n readonly encoding?: JinaEncoding\n readonly items: ReadonlyArray<WireItem>\n}): WireBody => {\n const quant = encodingToQuant(request.encoding)\n return {\n model: request.model,\n input: request.items,\n ...(request.task !== undefined && { task: request.task }),\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n ...(quant !== undefined && { embedding_type: quant }),\n ...(request.encoding === \"multivector\" && { return_multivector: true }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\n/**\n * Wire response payloads. Split into four discriminated schemas so the\n * decoder can narrow precisely via `Schema.is`:\n *\n * - **Dense float** (`object: \"embedding\"`, `embedding: number[]`) —\n * default JSON form.\n * - **Dense base64** (`object: \"embedding\"`, `embedding: string`) — set\n * when `embedding_type` was `base64` or `binary`.\n * - **Sparse** (`object: \"embedding\"`, `embedding: Record<string,\n * number>`) — token-keyed weights from an ELSER-style model.\n * - **Multivector** (`object: \"embeddings\"`, plural) — token-level\n * vectors when `return_multivector: true` or a ColBERT model is used.\n */\nconst WireDenseFloatPayload = Schema.Struct({\n object: Schema.Literal(\"embedding\"),\n index: Schema.Number,\n embedding: Schema.Array(Schema.Number),\n})\ntype WireDenseFloatPayload = typeof WireDenseFloatPayload.Type\n\nconst WireDenseBase64Payload = Schema.Struct({\n object: Schema.Literal(\"embedding\"),\n index: Schema.Number,\n embedding: Schema.String,\n})\ntype WireDenseBase64Payload = typeof WireDenseBase64Payload.Type\n\nconst WireSparsePayload = Schema.Struct({\n object: Schema.Literal(\"embedding\"),\n index: Schema.Number,\n embedding: Schema.Record(Schema.String, Schema.Number),\n})\ntype WireSparsePayload = typeof WireSparsePayload.Type\n\nconst WireMultiPayload = Schema.Struct({\n object: Schema.Literal(\"embeddings\"),\n index: Schema.Number,\n embeddings: Schema.Array(Schema.Array(Schema.Number)),\n tokenized_input: Schema.optional(Schema.Array(Schema.String)),\n})\ntype WireMultiPayload = typeof WireMultiPayload.Type\n\n// Order matters: more specific schemas first. Dense float (array) and\n// multivector (different `object`) are unambiguous; sparse vs base64\n// disambiguate by the `embedding` field type.\nconst WirePayload = Schema.Union([\n WireDenseFloatPayload,\n WireSparsePayload,\n WireDenseBase64Payload,\n WireMultiPayload,\n])\ntype WirePayload = typeof WirePayload.Type\n\nconst isMultiPayload = Schema.is(WireMultiPayload)\nconst isSparsePayload = Schema.is(WireSparsePayload)\nconst isDenseBase64Payload = Schema.is(WireDenseBase64Payload)\n\nconst WireUsage = Schema.Struct({\n total_tokens: Schema.Number,\n prompt_tokens: Schema.optional(Schema.Number),\n})\ntype WireUsage = typeof WireUsage.Type\n\nconst WireResponse = Schema.Struct({\n data: Schema.Array(WirePayload),\n model: Schema.String,\n usage: WireUsage,\n})\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"jina\", raw: cause })\n\nconst encodingMismatch = (expected: JinaEncoding, got: string): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"jina\",\n param: \"encoding\",\n raw: `requested encoding=\"${expected}\" but the response contains a ${got} embedding - confirm the chosen model supports the requested encoding`,\n })\n\n/** Decode a base64 wire string to the right dense `Embedding` arm. */\nconst decodeBase64Embedding = (\n b64: string,\n encoding: JinaEncoding | undefined,\n): Effect.Effect<Embedding, AiError.AiError> =>\n Result.match(Encoding.decodeBase64(b64), {\n onFailure: (cause) => Effect.fail(transportFailure(cause)),\n onSuccess: (bytes) =>\n Match.value(encoding).pipe(\n Match.when(\n \"binary\",\n (): Effect.Effect<Embedding, AiError.AiError> =>\n Effect.succeed({ _tag: \"binary\", vector: bytes }),\n ),\n Match.orElse(\n (): Effect.Effect<Embedding, AiError.AiError> =>\n Effect.succeed({\n _tag: \"float32\",\n vector: new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4),\n }),\n ),\n ),\n })\n\n/**\n * Project a wire payload to our `Embedding` discriminated union and\n * verify the requested encoding matches what came back.\n */\nconst payloadToEmbedding = (\n payload: WirePayload,\n encoding: JinaEncoding | undefined,\n): Effect.Effect<Embedding, AiError.AiError> => {\n if (isMultiPayload(payload)) {\n return encoding === \"multivector\"\n ? Effect.succeed<Embedding>({\n _tag: \"multivector\",\n vectors: payload.embeddings.map((v) => Float32Array.from(v)),\n })\n : Effect.fail(encodingMismatch(encoding ?? \"float32\", \"multivector\"))\n }\n if (isSparsePayload(payload)) {\n return encoding === \"sparse\"\n ? Effect.succeed<Embedding>({ _tag: \"sparse\", weights: payload.embedding })\n : Effect.fail(encodingMismatch(encoding ?? \"float32\", \"sparse\"))\n }\n // Dense (float array or base64 string).\n if (encoding === \"multivector\" || encoding === \"sparse\") {\n return Effect.fail(encodingMismatch(encoding, \"dense\"))\n }\n return isDenseBase64Payload(payload)\n ? decodeBase64Embedding(payload.embedding, encoding)\n : Effect.succeed<Embedding>({\n _tag: \"float32\",\n vector: Float32Array.from(payload.embedding),\n })\n}\n\nconst indexOf = (p: WirePayload): number => p.index\n\nconst usageOf = (u: WireUsage): Usage => ({ inputTokens: u.total_tokens })\n\n// ---------------------------------------------------------------------------\n// HTTP errors\n// ---------------------------------------------------------------------------\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 === 413) return new AiError.ContextLengthExceeded({ provider, raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.jina.ai/v1\"\n\nconst postEmbed = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<typeof WireResponse.Type, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/embeddings`).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* Effect.fail(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\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst orderedEmbeddings = (\n data: ReadonlyArray<WirePayload>,\n encoding: JinaEncoding | undefined,\n): Effect.Effect<ReadonlyArray<Embedding>, AiError.AiError> =>\n Effect.forEach(\n [...data].sort((a, b) => indexOf(a) - indexOf(b)),\n (item) => payloadToEmbedding(item, encoding),\n )\n\nconst embedImpl =\n (cfg: Config) =>\n (\n request: JinaEmbedRequest,\n ): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>\n inputToItem(request.input).pipe(\n Effect.flatMap((item) =>\n postEmbed(\n cfg,\n buildBody({\n model: request.model,\n ...(request.task !== undefined && { task: request.task }),\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n ...(request.encoding !== undefined && { encoding: request.encoding }),\n items: [item],\n }),\n ),\n ),\n Effect.flatMap((decoded) => {\n const first = decoded.data[0]\n if (first === undefined) {\n return Effect.fail(transportFailure(\"Jina returned empty `data` array\"))\n }\n return payloadToEmbedding(first, request.encoding).pipe(\n Effect.map((embedding): EmbedResponse => ({ embedding, usage: usageOf(decoded.usage) })),\n )\n }),\n )\n\nconst embedManyImpl =\n (cfg: Config) =>\n (\n request: JinaEmbedManyRequest,\n ): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.forEach(request.inputs, inputToItem).pipe(\n Effect.flatMap((items) =>\n postEmbed(\n cfg,\n buildBody({\n model: request.model,\n ...(request.task !== undefined && { task: request.task }),\n ...(request.dimensions !== undefined && { dimensions: request.dimensions }),\n ...(request.encoding !== undefined && { encoding: request.encoding }),\n items,\n }),\n ),\n ),\n Effect.flatMap((decoded) =>\n orderedEmbeddings(decoded.data, request.encoding).pipe(\n Effect.map(\n (embeddings): EmbedManyResponse => ({ embeddings, usage: usageOf(decoded.usage) }),\n ),\n ),\n ),\n )\n\n/**\n * Map cross-provider `query`/`document` to Jina's dotted-pair task\n * vocabulary. `undefined` stays `undefined`.\n */\nconst mapGenericTask: (t: \"query\" | \"document\" | undefined) => JinaTask | undefined = Match.type<\n \"query\" | \"document\" | undefined\n>().pipe(\n Match.when(\"query\", (): JinaTask => \"retrieval.query\"),\n Match.when(\"document\", (): JinaTask => \"retrieval.passage\"),\n Match.orElse((): JinaTask | undefined => undefined),\n)\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `JinaEmbeddingService` value. For Layer-based setup, prefer\n * `layer`.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<JinaEmbeddingService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n embed: (request) =>\n embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n embedMany: (request) =>\n embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer that registers both the provider-specific `JinaEmbedding` tag\n * and the generic `EmbeddingModel` tag, sharing one underlying\n * implementation. The generic registration maps cross-provider\n * `query`/`document` to Jina's `retrieval.query` / `retrieval.passage`\n * task strings; other request fields pass through.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<JinaEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(JinaEmbedding, make(cfg))\n const generic = Layer.effect(\n EmbeddingModel,\n Effect.map(\n make(cfg),\n (s): EmbeddingModelService => ({\n embed: (req: CommonEmbedRequest) => {\n const task = mapGenericTask(req.task)\n return s.embed({\n ...req,\n model: req.model as JinaEmbeddingModel,\n ...(task !== undefined && { task }),\n } as JinaEmbedRequest)\n },\n embedMany: (req: CommonEmbedManyRequest) => {\n const task = mapGenericTask(req.task)\n return s.embedMany({\n ...req,\n model: req.model as JinaEmbeddingModel,\n ...(task !== undefined && { task }),\n } as JinaEmbedManyRequest)\n },\n }),\n ),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/models.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Known Jina embedding model identifiers.
|
|
4
|
+
*
|
|
5
|
+
* - `jina-embeddings-v4` — flagship multimodal (text + image), 32k context.
|
|
6
|
+
* Tasks: `retrieval`, `text-matching`, `code` (LoRA-bound). Query and
|
|
7
|
+
* document modes share the `retrieval` task; the model handles the
|
|
8
|
+
* distinction internally.
|
|
9
|
+
* - `jina-embeddings-v5-text-small` / `-nano` — fifth-gen text-only,
|
|
10
|
+
* released Feb 2026. Multilingual, Matryoshka, GGUF-quantizable for
|
|
11
|
+
* edge deployment.
|
|
12
|
+
* - `jina-embeddings-v3` — legacy text-only. Tasks: `retrieval.query`,
|
|
13
|
+
* `retrieval.passage`, `text-matching`, `classification`, `separation`.
|
|
14
|
+
* - `jina-clip-v2` — CLIP-style multimodal embedding.
|
|
15
|
+
*
|
|
16
|
+
* The `(string & {})` tail accepts any string so newly-released models
|
|
17
|
+
* work without an SDK update.
|
|
18
|
+
*
|
|
19
|
+
* Reference: https://jina.ai/embeddings/
|
|
20
|
+
*/
|
|
21
|
+
type JinaEmbeddingModel = "jina-embeddings-v4" | "jina-embeddings-v5-text-small" | "jina-embeddings-v5-text-nano" | "jina-embeddings-v3" | "jina-clip-v2" | (string & {});
|
|
22
|
+
//#endregion
|
|
23
|
+
export { JinaEmbeddingModel };
|
|
24
|
+
//# sourceMappingURL=models.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAmBA;;;;;;;;;;;;;;;;;;KAAY,kBAAA"}
|
package/dist/models.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effect-uai/jina",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Jina AI provider for @effect-uai/core (embeddings, rerank).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"effect",
|
|
8
|
+
"embeddings",
|
|
9
|
+
"jina",
|
|
10
|
+
"rerank"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/betalyra/effect-uai/tree/main/packages/providers/jina",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/betalyra/effect-uai/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Betalyra",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/betalyra/effect-uai",
|
|
21
|
+
"directory": "packages/providers/jina"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./JinaEmbedding": {
|
|
38
|
+
"types": "./dist/JinaEmbedding.d.mts",
|
|
39
|
+
"import": "./dist/JinaEmbedding.mjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"effect": "4.0.0-beta.57",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"@effect-uai/core": "0.3.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@effect-uai/core": ">=0.2.0 <1",
|
|
52
|
+
"effect": "4.0.0-beta.57"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsdown",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { Context, Effect, Encoding, Layer, Match, Redacted, Result, Schema } from "effect"
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
|
+
import * as AiError from "@effect-uai/core/AiError"
|
|
4
|
+
import type { Embedding, EmbedContentPart, EmbedInput, Usage } from "@effect-uai/core/Embedding"
|
|
5
|
+
import {
|
|
6
|
+
type CommonEmbedManyRequest,
|
|
7
|
+
type CommonEmbedRequest,
|
|
8
|
+
EmbeddingModel,
|
|
9
|
+
type EmbeddingModelService,
|
|
10
|
+
type EmbedManyResponse,
|
|
11
|
+
type EmbedResponse,
|
|
12
|
+
} from "@effect-uai/core/EmbeddingModel"
|
|
13
|
+
import type { ImageSource } from "@effect-uai/core/Image"
|
|
14
|
+
import type { JinaEmbeddingModel } from "./models.js"
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Public types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Jina's task vocabulary. v4 and v3/v5 share the dotted-pair form:
|
|
22
|
+
* - `retrieval.query` / `retrieval.passage` — asymmetric retrieval.
|
|
23
|
+
* - `text-matching` — symmetric similarity between two texts.
|
|
24
|
+
* - `code.query` / `code.passage` — code retrieval / matching (v4-only).
|
|
25
|
+
* - `classification` / `separation` — v3 / v5 only.
|
|
26
|
+
*
|
|
27
|
+
* The `(string & {})` tail accepts any string so newly-released task
|
|
28
|
+
* types work without an SDK update.
|
|
29
|
+
*/
|
|
30
|
+
export type JinaTask =
|
|
31
|
+
| "retrieval.query"
|
|
32
|
+
| "retrieval.passage"
|
|
33
|
+
| "text-matching"
|
|
34
|
+
| "code.query"
|
|
35
|
+
| "code.passage"
|
|
36
|
+
| "classification"
|
|
37
|
+
| "separation"
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
39
|
+
| (string & {})
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Jina's supported `encoding` values. Provider-narrowed from the cross-
|
|
43
|
+
* provider `Encoding` union.
|
|
44
|
+
*
|
|
45
|
+
* Compatibility with models is checked at the response level rather than
|
|
46
|
+
* pre-flight: if you ask for `multivector` against a model that doesn't
|
|
47
|
+
* produce one, Jina's API rejects the request with a clear error; if
|
|
48
|
+
* you ask for `sparse` and the chosen model returns dense, our decoder
|
|
49
|
+
* fails with a typed mismatch. No hardcoded model-encoding table to
|
|
50
|
+
* maintain.
|
|
51
|
+
*
|
|
52
|
+
* Wire mapping:
|
|
53
|
+
* - `multivector` → request sets `return_multivector: true`
|
|
54
|
+
* (`jina-embeddings-v4` + ColBERT models).
|
|
55
|
+
* - `sparse` → no flag; the model itself (`elser-v2`) determines the
|
|
56
|
+
* output shape.
|
|
57
|
+
* - `binary` → request sets `embedding_type: "binary"`.
|
|
58
|
+
* - `float32` → no flag; default JSON `number[]`.
|
|
59
|
+
*/
|
|
60
|
+
export type JinaEncoding = "float32" | "binary" | "sparse" | "multivector"
|
|
61
|
+
|
|
62
|
+
export type JinaEmbedRequest = Omit<CommonEmbedRequest, "model" | "task" | "encoding"> & {
|
|
63
|
+
/** Narrows `CommonEmbedRequest.model` to the typed Jina union. */
|
|
64
|
+
readonly model: JinaEmbeddingModel
|
|
65
|
+
/**
|
|
66
|
+
* Widens the cross-provider `"query" | "document"` to Jina's task enum.
|
|
67
|
+
* Required by v3+ for retrieval-quality results. v4 unifies query and
|
|
68
|
+
* document under `retrieval`.
|
|
69
|
+
*/
|
|
70
|
+
readonly task?: JinaTask
|
|
71
|
+
/** Narrows `CommonEmbedRequest.encoding` to Jina's supported set. */
|
|
72
|
+
readonly encoding?: JinaEncoding
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type JinaEmbedManyRequest = Omit<JinaEmbedRequest, "input"> & {
|
|
76
|
+
readonly inputs: ReadonlyArray<EmbedInput>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type JinaEmbeddingService = {
|
|
80
|
+
readonly embed: (request: JinaEmbedRequest) => Effect.Effect<EmbedResponse, AiError.AiError>
|
|
81
|
+
readonly embedMany: (
|
|
82
|
+
request: JinaEmbedManyRequest,
|
|
83
|
+
) => Effect.Effect<EmbedManyResponse, AiError.AiError>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Provider-typed service tag. Yield this for Jina-typed embedding
|
|
88
|
+
* requests; yield the generic `EmbeddingModel` tag for provider-portable
|
|
89
|
+
* code. Both are registered by `layer`.
|
|
90
|
+
*/
|
|
91
|
+
export class JinaEmbedding extends Context.Service<JinaEmbedding, JinaEmbeddingService>()(
|
|
92
|
+
"@betalyra/effect-uai/providers/jina/JinaEmbedding",
|
|
93
|
+
) {}
|
|
94
|
+
|
|
95
|
+
export type Config = {
|
|
96
|
+
readonly apiKey: Redacted.Redacted
|
|
97
|
+
readonly baseUrl?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Codec - request body
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
type WireTextItem = {
|
|
105
|
+
readonly text: string
|
|
106
|
+
}
|
|
107
|
+
type WireImageItem = {
|
|
108
|
+
readonly image: string
|
|
109
|
+
}
|
|
110
|
+
type WireItem = WireTextItem | WireImageItem
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Jina v4 accepts `{ image: <url> }` or `{ image: <base64-string> }`.
|
|
114
|
+
* The server detects which form by looking at the value. We pass URLs
|
|
115
|
+
* through directly and base64-encode raw bytes.
|
|
116
|
+
*/
|
|
117
|
+
const imageSourceToItem: (s: ImageSource) => WireItem = Match.type<ImageSource>().pipe(
|
|
118
|
+
Match.tag("url", (s): WireItem => ({ image: s.url })),
|
|
119
|
+
Match.tag("base64", (s): WireItem => ({ image: s.base64 })),
|
|
120
|
+
Match.tag("bytes", (s): WireItem => ({ image: Encoding.encodeBase64(s.bytes) })),
|
|
121
|
+
Match.exhaustive,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const contentPartToItem: (part: EmbedContentPart) => WireItem = Match.type<EmbedContentPart>().pipe(
|
|
125
|
+
Match.when({ text: Match.string }, ({ text }): WireItem => ({ text })),
|
|
126
|
+
Match.when({ image: Match.any }, ({ image }) => imageSourceToItem(image)),
|
|
127
|
+
Match.exhaustive,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const multiPartContentRejected: AiError.AiError = new AiError.InvalidRequest({
|
|
131
|
+
provider: "jina",
|
|
132
|
+
param: "input.content",
|
|
133
|
+
raw: "Jina treats each input[] entry as one item; multi-part `content[]` would lose the grouping. Split into separate `inputs[]` entries.",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Project an `EmbedInput` to one wire item. A single-part `content[]` is
|
|
138
|
+
* unwrapped; multi-part `content[]` is rejected because Jina's flat
|
|
139
|
+
* `input[]` would lose the grouping.
|
|
140
|
+
*/
|
|
141
|
+
const inputToItem: (input: EmbedInput) => Effect.Effect<WireItem, AiError.AiError> =
|
|
142
|
+
Match.type<EmbedInput>().pipe(
|
|
143
|
+
Match.when(
|
|
144
|
+
Match.string,
|
|
145
|
+
(s): Effect.Effect<WireItem, AiError.AiError> => Effect.succeed({ text: s }),
|
|
146
|
+
),
|
|
147
|
+
Match.when(
|
|
148
|
+
{ text: Match.string },
|
|
149
|
+
({ text }): Effect.Effect<WireItem, AiError.AiError> => Effect.succeed({ text }),
|
|
150
|
+
),
|
|
151
|
+
Match.when(
|
|
152
|
+
{ image: Match.any },
|
|
153
|
+
({ image }): Effect.Effect<WireItem, AiError.AiError> =>
|
|
154
|
+
Effect.succeed(imageSourceToItem(image)),
|
|
155
|
+
),
|
|
156
|
+
Match.when(
|
|
157
|
+
{ content: Match.any },
|
|
158
|
+
({ content }): Effect.Effect<WireItem, AiError.AiError> =>
|
|
159
|
+
content.length === 1
|
|
160
|
+
? Effect.succeed(contentPartToItem(content[0]!))
|
|
161
|
+
: Effect.fail(multiPartContentRejected),
|
|
162
|
+
),
|
|
163
|
+
Match.exhaustive,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
type WireBody = {
|
|
167
|
+
readonly model: string
|
|
168
|
+
readonly input: ReadonlyArray<WireItem>
|
|
169
|
+
readonly task?: string
|
|
170
|
+
readonly dimensions?: number
|
|
171
|
+
readonly embedding_type?: "float" | "base64" | "binary" | "ubinary"
|
|
172
|
+
readonly return_multivector?: boolean
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type WireQuant = WireBody["embedding_type"]
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Map our `JinaEncoding` to Jina's `embedding_type` wire field. Only
|
|
179
|
+
* `binary` actually changes the wire transport; the others either omit
|
|
180
|
+
* the field (`float32`) or ignore it (`sparse` / `multivector` use
|
|
181
|
+
* separate signalling — model choice and `return_multivector`).
|
|
182
|
+
*/
|
|
183
|
+
const encodingToQuant: (encoding: JinaEncoding | undefined) => WireQuant = Match.type<
|
|
184
|
+
JinaEncoding | undefined
|
|
185
|
+
>().pipe(
|
|
186
|
+
Match.when("binary", (): WireQuant => "binary"),
|
|
187
|
+
Match.orElse((): WireQuant => undefined),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const buildBody = (request: {
|
|
191
|
+
readonly model: JinaEmbeddingModel
|
|
192
|
+
readonly task?: JinaTask
|
|
193
|
+
readonly dimensions?: number
|
|
194
|
+
readonly encoding?: JinaEncoding
|
|
195
|
+
readonly items: ReadonlyArray<WireItem>
|
|
196
|
+
}): WireBody => {
|
|
197
|
+
const quant = encodingToQuant(request.encoding)
|
|
198
|
+
return {
|
|
199
|
+
model: request.model,
|
|
200
|
+
input: request.items,
|
|
201
|
+
...(request.task !== undefined && { task: request.task }),
|
|
202
|
+
...(request.dimensions !== undefined && { dimensions: request.dimensions }),
|
|
203
|
+
...(quant !== undefined && { embedding_type: quant }),
|
|
204
|
+
...(request.encoding === "multivector" && { return_multivector: true }),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Codec - response
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Wire response payloads. Split into four discriminated schemas so the
|
|
214
|
+
* decoder can narrow precisely via `Schema.is`:
|
|
215
|
+
*
|
|
216
|
+
* - **Dense float** (`object: "embedding"`, `embedding: number[]`) —
|
|
217
|
+
* default JSON form.
|
|
218
|
+
* - **Dense base64** (`object: "embedding"`, `embedding: string`) — set
|
|
219
|
+
* when `embedding_type` was `base64` or `binary`.
|
|
220
|
+
* - **Sparse** (`object: "embedding"`, `embedding: Record<string,
|
|
221
|
+
* number>`) — token-keyed weights from an ELSER-style model.
|
|
222
|
+
* - **Multivector** (`object: "embeddings"`, plural) — token-level
|
|
223
|
+
* vectors when `return_multivector: true` or a ColBERT model is used.
|
|
224
|
+
*/
|
|
225
|
+
const WireDenseFloatPayload = Schema.Struct({
|
|
226
|
+
object: Schema.Literal("embedding"),
|
|
227
|
+
index: Schema.Number,
|
|
228
|
+
embedding: Schema.Array(Schema.Number),
|
|
229
|
+
})
|
|
230
|
+
type WireDenseFloatPayload = typeof WireDenseFloatPayload.Type
|
|
231
|
+
|
|
232
|
+
const WireDenseBase64Payload = Schema.Struct({
|
|
233
|
+
object: Schema.Literal("embedding"),
|
|
234
|
+
index: Schema.Number,
|
|
235
|
+
embedding: Schema.String,
|
|
236
|
+
})
|
|
237
|
+
type WireDenseBase64Payload = typeof WireDenseBase64Payload.Type
|
|
238
|
+
|
|
239
|
+
const WireSparsePayload = Schema.Struct({
|
|
240
|
+
object: Schema.Literal("embedding"),
|
|
241
|
+
index: Schema.Number,
|
|
242
|
+
embedding: Schema.Record(Schema.String, Schema.Number),
|
|
243
|
+
})
|
|
244
|
+
type WireSparsePayload = typeof WireSparsePayload.Type
|
|
245
|
+
|
|
246
|
+
const WireMultiPayload = Schema.Struct({
|
|
247
|
+
object: Schema.Literal("embeddings"),
|
|
248
|
+
index: Schema.Number,
|
|
249
|
+
embeddings: Schema.Array(Schema.Array(Schema.Number)),
|
|
250
|
+
tokenized_input: Schema.optional(Schema.Array(Schema.String)),
|
|
251
|
+
})
|
|
252
|
+
type WireMultiPayload = typeof WireMultiPayload.Type
|
|
253
|
+
|
|
254
|
+
// Order matters: more specific schemas first. Dense float (array) and
|
|
255
|
+
// multivector (different `object`) are unambiguous; sparse vs base64
|
|
256
|
+
// disambiguate by the `embedding` field type.
|
|
257
|
+
const WirePayload = Schema.Union([
|
|
258
|
+
WireDenseFloatPayload,
|
|
259
|
+
WireSparsePayload,
|
|
260
|
+
WireDenseBase64Payload,
|
|
261
|
+
WireMultiPayload,
|
|
262
|
+
])
|
|
263
|
+
type WirePayload = typeof WirePayload.Type
|
|
264
|
+
|
|
265
|
+
const isMultiPayload = Schema.is(WireMultiPayload)
|
|
266
|
+
const isSparsePayload = Schema.is(WireSparsePayload)
|
|
267
|
+
const isDenseBase64Payload = Schema.is(WireDenseBase64Payload)
|
|
268
|
+
|
|
269
|
+
const WireUsage = Schema.Struct({
|
|
270
|
+
total_tokens: Schema.Number,
|
|
271
|
+
prompt_tokens: Schema.optional(Schema.Number),
|
|
272
|
+
})
|
|
273
|
+
type WireUsage = typeof WireUsage.Type
|
|
274
|
+
|
|
275
|
+
const WireResponse = Schema.Struct({
|
|
276
|
+
data: Schema.Array(WirePayload),
|
|
277
|
+
model: Schema.String,
|
|
278
|
+
usage: WireUsage,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const transportFailure = (cause: unknown): AiError.AiError =>
|
|
282
|
+
new AiError.Unavailable({ provider: "jina", raw: cause })
|
|
283
|
+
|
|
284
|
+
const encodingMismatch = (expected: JinaEncoding, got: string): AiError.AiError =>
|
|
285
|
+
new AiError.InvalidRequest({
|
|
286
|
+
provider: "jina",
|
|
287
|
+
param: "encoding",
|
|
288
|
+
raw: `requested encoding="${expected}" but the response contains a ${got} embedding - confirm the chosen model supports the requested encoding`,
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
/** Decode a base64 wire string to the right dense `Embedding` arm. */
|
|
292
|
+
const decodeBase64Embedding = (
|
|
293
|
+
b64: string,
|
|
294
|
+
encoding: JinaEncoding | undefined,
|
|
295
|
+
): Effect.Effect<Embedding, AiError.AiError> =>
|
|
296
|
+
Result.match(Encoding.decodeBase64(b64), {
|
|
297
|
+
onFailure: (cause) => Effect.fail(transportFailure(cause)),
|
|
298
|
+
onSuccess: (bytes) =>
|
|
299
|
+
Match.value(encoding).pipe(
|
|
300
|
+
Match.when(
|
|
301
|
+
"binary",
|
|
302
|
+
(): Effect.Effect<Embedding, AiError.AiError> =>
|
|
303
|
+
Effect.succeed({ _tag: "binary", vector: bytes }),
|
|
304
|
+
),
|
|
305
|
+
Match.orElse(
|
|
306
|
+
(): Effect.Effect<Embedding, AiError.AiError> =>
|
|
307
|
+
Effect.succeed({
|
|
308
|
+
_tag: "float32",
|
|
309
|
+
vector: new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4),
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Project a wire payload to our `Embedding` discriminated union and
|
|
317
|
+
* verify the requested encoding matches what came back.
|
|
318
|
+
*/
|
|
319
|
+
const payloadToEmbedding = (
|
|
320
|
+
payload: WirePayload,
|
|
321
|
+
encoding: JinaEncoding | undefined,
|
|
322
|
+
): Effect.Effect<Embedding, AiError.AiError> => {
|
|
323
|
+
if (isMultiPayload(payload)) {
|
|
324
|
+
return encoding === "multivector"
|
|
325
|
+
? Effect.succeed<Embedding>({
|
|
326
|
+
_tag: "multivector",
|
|
327
|
+
vectors: payload.embeddings.map((v) => Float32Array.from(v)),
|
|
328
|
+
})
|
|
329
|
+
: Effect.fail(encodingMismatch(encoding ?? "float32", "multivector"))
|
|
330
|
+
}
|
|
331
|
+
if (isSparsePayload(payload)) {
|
|
332
|
+
return encoding === "sparse"
|
|
333
|
+
? Effect.succeed<Embedding>({ _tag: "sparse", weights: payload.embedding })
|
|
334
|
+
: Effect.fail(encodingMismatch(encoding ?? "float32", "sparse"))
|
|
335
|
+
}
|
|
336
|
+
// Dense (float array or base64 string).
|
|
337
|
+
if (encoding === "multivector" || encoding === "sparse") {
|
|
338
|
+
return Effect.fail(encodingMismatch(encoding, "dense"))
|
|
339
|
+
}
|
|
340
|
+
return isDenseBase64Payload(payload)
|
|
341
|
+
? decodeBase64Embedding(payload.embedding, encoding)
|
|
342
|
+
: Effect.succeed<Embedding>({
|
|
343
|
+
_tag: "float32",
|
|
344
|
+
vector: Float32Array.from(payload.embedding),
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const indexOf = (p: WirePayload): number => p.index
|
|
349
|
+
|
|
350
|
+
const usageOf = (u: WireUsage): Usage => ({ inputTokens: u.total_tokens })
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// HTTP errors
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
const httpStatusError = (status: number, body: string): AiError.AiError => {
|
|
357
|
+
const provider = "jina"
|
|
358
|
+
const raw = body
|
|
359
|
+
if (status === 429) return new AiError.RateLimited({ provider, raw })
|
|
360
|
+
if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
|
|
361
|
+
if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
|
|
362
|
+
if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
|
|
363
|
+
if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
|
|
364
|
+
if (status === 413) return new AiError.ContextLengthExceeded({ provider, raw })
|
|
365
|
+
if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
|
|
366
|
+
return new AiError.InvalidRequest({ provider, raw })
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// HTTP plumbing
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.jina.ai/v1"
|
|
374
|
+
|
|
375
|
+
const postEmbed = (
|
|
376
|
+
cfg: Config,
|
|
377
|
+
body: WireBody,
|
|
378
|
+
): Effect.Effect<typeof WireResponse.Type, AiError.AiError, HttpClient.HttpClient> =>
|
|
379
|
+
Effect.gen(function* () {
|
|
380
|
+
const client = yield* HttpClient.HttpClient
|
|
381
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/embeddings`).pipe(
|
|
382
|
+
HttpClientRequest.bearerToken(cfg.apiKey),
|
|
383
|
+
HttpClientRequest.bodyJsonUnsafe(body),
|
|
384
|
+
)
|
|
385
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
|
|
386
|
+
if (response.status >= 400) {
|
|
387
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
|
|
388
|
+
return yield* Effect.fail(httpStatusError(response.status, text))
|
|
389
|
+
}
|
|
390
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
391
|
+
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(
|
|
392
|
+
Effect.mapError(transportFailure),
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Service implementation
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
const orderedEmbeddings = (
|
|
401
|
+
data: ReadonlyArray<WirePayload>,
|
|
402
|
+
encoding: JinaEncoding | undefined,
|
|
403
|
+
): Effect.Effect<ReadonlyArray<Embedding>, AiError.AiError> =>
|
|
404
|
+
Effect.forEach(
|
|
405
|
+
[...data].sort((a, b) => indexOf(a) - indexOf(b)),
|
|
406
|
+
(item) => payloadToEmbedding(item, encoding),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
const embedImpl =
|
|
410
|
+
(cfg: Config) =>
|
|
411
|
+
(
|
|
412
|
+
request: JinaEmbedRequest,
|
|
413
|
+
): Effect.Effect<EmbedResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
414
|
+
inputToItem(request.input).pipe(
|
|
415
|
+
Effect.flatMap((item) =>
|
|
416
|
+
postEmbed(
|
|
417
|
+
cfg,
|
|
418
|
+
buildBody({
|
|
419
|
+
model: request.model,
|
|
420
|
+
...(request.task !== undefined && { task: request.task }),
|
|
421
|
+
...(request.dimensions !== undefined && { dimensions: request.dimensions }),
|
|
422
|
+
...(request.encoding !== undefined && { encoding: request.encoding }),
|
|
423
|
+
items: [item],
|
|
424
|
+
}),
|
|
425
|
+
),
|
|
426
|
+
),
|
|
427
|
+
Effect.flatMap((decoded) => {
|
|
428
|
+
const first = decoded.data[0]
|
|
429
|
+
if (first === undefined) {
|
|
430
|
+
return Effect.fail(transportFailure("Jina returned empty `data` array"))
|
|
431
|
+
}
|
|
432
|
+
return payloadToEmbedding(first, request.encoding).pipe(
|
|
433
|
+
Effect.map((embedding): EmbedResponse => ({ embedding, usage: usageOf(decoded.usage) })),
|
|
434
|
+
)
|
|
435
|
+
}),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const embedManyImpl =
|
|
439
|
+
(cfg: Config) =>
|
|
440
|
+
(
|
|
441
|
+
request: JinaEmbedManyRequest,
|
|
442
|
+
): Effect.Effect<EmbedManyResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
443
|
+
Effect.forEach(request.inputs, inputToItem).pipe(
|
|
444
|
+
Effect.flatMap((items) =>
|
|
445
|
+
postEmbed(
|
|
446
|
+
cfg,
|
|
447
|
+
buildBody({
|
|
448
|
+
model: request.model,
|
|
449
|
+
...(request.task !== undefined && { task: request.task }),
|
|
450
|
+
...(request.dimensions !== undefined && { dimensions: request.dimensions }),
|
|
451
|
+
...(request.encoding !== undefined && { encoding: request.encoding }),
|
|
452
|
+
items,
|
|
453
|
+
}),
|
|
454
|
+
),
|
|
455
|
+
),
|
|
456
|
+
Effect.flatMap((decoded) =>
|
|
457
|
+
orderedEmbeddings(decoded.data, request.encoding).pipe(
|
|
458
|
+
Effect.map(
|
|
459
|
+
(embeddings): EmbedManyResponse => ({ embeddings, usage: usageOf(decoded.usage) }),
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Map cross-provider `query`/`document` to Jina's dotted-pair task
|
|
467
|
+
* vocabulary. `undefined` stays `undefined`.
|
|
468
|
+
*/
|
|
469
|
+
const mapGenericTask: (t: "query" | "document" | undefined) => JinaTask | undefined = Match.type<
|
|
470
|
+
"query" | "document" | undefined
|
|
471
|
+
>().pipe(
|
|
472
|
+
Match.when("query", (): JinaTask => "retrieval.query"),
|
|
473
|
+
Match.when("document", (): JinaTask => "retrieval.passage"),
|
|
474
|
+
Match.orElse((): JinaTask | undefined => undefined),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Constructors
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Build a `JinaEmbeddingService` value. For Layer-based setup, prefer
|
|
483
|
+
* `layer`.
|
|
484
|
+
*/
|
|
485
|
+
export const make = (
|
|
486
|
+
cfg: Config,
|
|
487
|
+
): Effect.Effect<JinaEmbeddingService, never, HttpClient.HttpClient> =>
|
|
488
|
+
Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
|
|
489
|
+
embed: (request) =>
|
|
490
|
+
embedImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
491
|
+
embedMany: (request) =>
|
|
492
|
+
embedManyImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
493
|
+
}))
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Layer that registers both the provider-specific `JinaEmbedding` tag
|
|
497
|
+
* and the generic `EmbeddingModel` tag, sharing one underlying
|
|
498
|
+
* implementation. The generic registration maps cross-provider
|
|
499
|
+
* `query`/`document` to Jina's `retrieval.query` / `retrieval.passage`
|
|
500
|
+
* task strings; other request fields pass through.
|
|
501
|
+
*/
|
|
502
|
+
export const layer = (
|
|
503
|
+
cfg: Config,
|
|
504
|
+
): Layer.Layer<JinaEmbedding | EmbeddingModel, never, HttpClient.HttpClient> => {
|
|
505
|
+
const typed = Layer.effect(JinaEmbedding, make(cfg))
|
|
506
|
+
const generic = Layer.effect(
|
|
507
|
+
EmbeddingModel,
|
|
508
|
+
Effect.map(
|
|
509
|
+
make(cfg),
|
|
510
|
+
(s): EmbeddingModelService => ({
|
|
511
|
+
embed: (req: CommonEmbedRequest) => {
|
|
512
|
+
const task = mapGenericTask(req.task)
|
|
513
|
+
return s.embed({
|
|
514
|
+
...req,
|
|
515
|
+
model: req.model as JinaEmbeddingModel,
|
|
516
|
+
...(task !== undefined && { task }),
|
|
517
|
+
} as JinaEmbedRequest)
|
|
518
|
+
},
|
|
519
|
+
embedMany: (req: CommonEmbedManyRequest) => {
|
|
520
|
+
const task = mapGenericTask(req.task)
|
|
521
|
+
return s.embedMany({
|
|
522
|
+
...req,
|
|
523
|
+
model: req.model as JinaEmbeddingModel,
|
|
524
|
+
...(task !== undefined && { task }),
|
|
525
|
+
} as JinaEmbedManyRequest)
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
return Layer.merge(typed, generic)
|
|
531
|
+
}
|
package/src/index.ts
ADDED
package/src/models.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known Jina embedding model identifiers.
|
|
3
|
+
*
|
|
4
|
+
* - `jina-embeddings-v4` — flagship multimodal (text + image), 32k context.
|
|
5
|
+
* Tasks: `retrieval`, `text-matching`, `code` (LoRA-bound). Query and
|
|
6
|
+
* document modes share the `retrieval` task; the model handles the
|
|
7
|
+
* distinction internally.
|
|
8
|
+
* - `jina-embeddings-v5-text-small` / `-nano` — fifth-gen text-only,
|
|
9
|
+
* released Feb 2026. Multilingual, Matryoshka, GGUF-quantizable for
|
|
10
|
+
* edge deployment.
|
|
11
|
+
* - `jina-embeddings-v3` — legacy text-only. Tasks: `retrieval.query`,
|
|
12
|
+
* `retrieval.passage`, `text-matching`, `classification`, `separation`.
|
|
13
|
+
* - `jina-clip-v2` — CLIP-style multimodal embedding.
|
|
14
|
+
*
|
|
15
|
+
* The `(string & {})` tail accepts any string so newly-released models
|
|
16
|
+
* work without an SDK update.
|
|
17
|
+
*
|
|
18
|
+
* Reference: https://jina.ai/embeddings/
|
|
19
|
+
*/
|
|
20
|
+
export type JinaEmbeddingModel =
|
|
21
|
+
| "jina-embeddings-v4"
|
|
22
|
+
| "jina-embeddings-v5-text-small"
|
|
23
|
+
| "jina-embeddings-v5-text-nano"
|
|
24
|
+
| "jina-embeddings-v3"
|
|
25
|
+
| "jina-clip-v2"
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
27
|
+
| (string & {})
|