@plurnk/plurnk-providers 1.14.2 → 1.16.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/.env.defaults +1 -1
- package/SPEC.md +16 -2
- package/dist/AiSdkProvider.d.ts +3 -0
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +50 -339
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/AiSdkRequestBody.d.ts +40 -0
- package/dist/AiSdkRequestBody.d.ts.map +1 -0
- package/dist/AiSdkRequestBody.js +361 -0
- package/dist/AiSdkRequestBody.js.map +1 -0
- package/dist/Mock.d.ts +5 -1
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +9 -2
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +2 -1
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +5 -0
- package/dist/Pool.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +1 -1
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +21 -2
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/capacity.d.ts +0 -1
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +1 -1
- package/dist/capacity.js.map +1 -1
- package/dist/catalogProvider.d.ts +2 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +6 -0
- package/dist/catalogProvider.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/promptTokens.d.ts.map +1 -1
- package/dist/promptTokens.js +2 -1
- package/dist/promptTokens.js.map +1 -1
- package/dist/reasoning-effort.d.ts +4 -0
- package/dist/reasoning-effort.d.ts.map +1 -0
- package/dist/reasoning-effort.js +14 -0
- package/dist/reasoning-effort.js.map +1 -0
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/AiSdkProvider.test.ts +22 -3
- package/src/AiSdkProvider.ts +51 -375
- package/src/AiSdkRequestBody.ts +419 -0
- package/src/Mock.ts +10 -2
- package/src/Pool.test.ts +1 -0
- package/src/Pool.ts +6 -1
- package/src/aiSdkTransport.ts +23 -4
- package/src/boundaries.test.ts +2 -0
- package/src/capacity.ts +1 -1
- package/src/catalogProvider.ts +9 -1
- package/src/index.ts +3 -0
- package/src/inputModalities.test.ts +53 -0
- package/src/promptTokens.ts +2 -1
- package/src/reasoning-effort.ts +15 -0
- package/src/types.ts +18 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import Mock from "./Mock.ts";
|
|
4
|
+
import Pool from "./Pool.ts";
|
|
5
|
+
import { inputModalitiesOf } from "./catalogProvider.ts";
|
|
6
|
+
import { estimatePromptTokens } from "./promptTokens.ts";
|
|
7
|
+
import { chatMessageText, type ChatMessage, type PromptTokenMeasurement } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
const tokensOf = (measurement: PromptTokenMeasurement): number => "tokens" in measurement ? measurement.tokens : -1;
|
|
10
|
+
|
|
11
|
+
const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
12
|
+
const PDF = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
|
|
13
|
+
const parts: ChatMessage = {
|
|
14
|
+
role: "user",
|
|
15
|
+
content: [
|
|
16
|
+
{ type: "text", text: "look at this" },
|
|
17
|
+
{ type: "image", image: PNG, mediaType: "image/png" },
|
|
18
|
+
{ type: "file", data: PDF, mediaType: "application/pdf" },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test("{§provider-input-modalities} message text is the text parts alone; image and file parts count nothing here", () => {
|
|
23
|
+
assert.equal(chatMessageText(parts), "look at this");
|
|
24
|
+
assert.equal(chatMessageText({ content: "plain" }), "plain");
|
|
25
|
+
assert.equal(tokensOf(estimatePromptTokens([parts])), Math.ceil("look at this".length / 2));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("{§provider-input-modalities} the catalog's modalities are kept to the wire vocabulary; unknown models declare none", () => {
|
|
29
|
+
assert.deepEqual([...inputModalitiesOf(["text", "image", "video", "pdf"])], ["image", "video", "pdf"]);
|
|
30
|
+
assert.deepEqual([...inputModalitiesOf(["text", "audio", "hologram"])], ["audio"]);
|
|
31
|
+
assert.deepEqual([...inputModalitiesOf(undefined)], []);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("{§provider-input-modalities} the Mock declares modalities by option and records what it received", async () => {
|
|
35
|
+
const blind = new Mock({ contextWindow: 200000, responses: [] });
|
|
36
|
+
assert.deepEqual([...blind.inputModalities], []);
|
|
37
|
+
const seeing = new Mock({ contextWindow: 200000, responses: [], inputModalities: ["image", "pdf"] });
|
|
38
|
+
assert.ok(seeing.inputModalities.has("image") && seeing.inputModalities.has("pdf") && !seeing.inputModalities.has("audio"));
|
|
39
|
+
assert.equal(tokensOf(await seeing.countPromptTokens([parts])), Math.ceil("look at this".length / 2));
|
|
40
|
+
await assert.rejects(seeing.generate({ messages: [parts], maxOutputTokens: 16, workerId: "1", primaryWorkerId: "1", callKind: "bare" } as never));
|
|
41
|
+
assert.equal(seeing.received.length, 1, "the request was recorded before the empty queue refused it");
|
|
42
|
+
const received = seeing.received[0]?.[0]?.content;
|
|
43
|
+
assert.ok(Array.isArray(received) && received[1]?.type === "image" && received[2]?.type === "file" && received[2].mediaType === "application/pdf");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("{§provider-input-modalities} a pool accepts a modality only when every backend does", () => {
|
|
47
|
+
const both = new Mock({ contextWindow: 200000, responses: [], inputModalities: ["image", "pdf"] });
|
|
48
|
+
const imageOnly = new Mock({ contextWindow: 200000, responses: [], inputModalities: ["image"] });
|
|
49
|
+
const blind = new Mock({ contextWindow: 200000, responses: [] });
|
|
50
|
+
assert.deepEqual([...new Pool([both, both]).inputModalities], ["image", "pdf"]);
|
|
51
|
+
assert.deepEqual([...new Pool([both, imageOnly]).inputModalities], ["image"]);
|
|
52
|
+
assert.deepEqual([...new Pool([both, blind]).inputModalities], []);
|
|
53
|
+
});
|
package/src/promptTokens.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chatMessageText } from "./types.ts";
|
|
1
2
|
import type { ChatMessage, PromptTokenMeasurement } from "./types.ts";
|
|
2
3
|
|
|
3
4
|
const KINDS = new Set<PromptTokenMeasurement["kind"]>([
|
|
@@ -38,7 +39,7 @@ export const estimatePromptTokens = (
|
|
|
38
39
|
detail = "chars/2 over message content; provider request framing is unknown",
|
|
39
40
|
): PromptTokenMeasurement => ({
|
|
40
41
|
kind: "estimate",
|
|
41
|
-
tokens: Math.ceil(messages.reduce((sum,
|
|
42
|
+
tokens: Math.ceil(messages.reduce((sum, message) => sum + chatMessageText(message).length, 0) / 2),
|
|
42
43
|
source: "heuristic:chars2",
|
|
43
44
|
detail,
|
|
44
45
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A fixed reasoning effort from a reasoning policy, and its native SDK projection.
|
|
2
|
+
import type { ReasoningPolicy } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const fixedEffort = (mode: ReasoningPolicy): "low" | "medium" | "high" | "xhigh" | "max" => {
|
|
5
|
+
if (mode === "low" || mode === "medium" || mode === "high" || mode === "xhigh" || mode === "max") return mode;
|
|
6
|
+
throw new TypeError(`reasoning policy '${mode}' is not a fixed effort`);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// The native SDK effort surface tops at xhigh; admission never grants a native
|
|
10
|
+
// route "max", so reaching it here is a contract violation, not a fallback site.
|
|
11
|
+
export const nativeFixedEffort = (mode: ReasoningPolicy): "low" | "medium" | "high" | "xhigh" => {
|
|
12
|
+
const effort = fixedEffort(mode);
|
|
13
|
+
if (effort === "max") throw new TypeError(`reasoning policy 'max' has no native SDK effort surface`);
|
|
14
|
+
return effort;
|
|
15
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -47,10 +47,25 @@ export class UnsupportedReasoningPolicyError extends Error {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// {§provider-input-modalities} — the native non-text inputs a route's model accepts, in the
|
|
51
|
+
// catalog's vocabulary (Models.dev `modalities.input` minus `text`).
|
|
52
|
+
export type InputModality = "image" | "pdf" | "audio" | "video";
|
|
53
|
+
export const INPUT_MODALITIES: readonly InputModality[] = Object.freeze(["image", "pdf", "audio", "video"]);
|
|
54
|
+
// A user message may carry parts: text beside native images and files for a model that declares
|
|
55
|
+
// the matching modality. System and assistant messages stay text.
|
|
56
|
+
export type ChatContentPart =
|
|
57
|
+
| { readonly type: "text"; readonly text: string }
|
|
58
|
+
| { readonly type: "image"; readonly image: Uint8Array; readonly mediaType: string }
|
|
59
|
+
| { readonly type: "file"; readonly data: Uint8Array; readonly mediaType: string };
|
|
50
60
|
export interface ChatMessage {
|
|
51
61
|
role: "system" | "user" | "assistant";
|
|
52
|
-
content: string;
|
|
62
|
+
content: string | readonly ChatContentPart[];
|
|
53
63
|
}
|
|
64
|
+
// The text of a message, for counting and for transports that take only text.
|
|
65
|
+
export const chatMessageText = (message: Pick<ChatMessage, "content">): string =>
|
|
66
|
+
typeof message.content === "string"
|
|
67
|
+
? message.content
|
|
68
|
+
: message.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
54
69
|
|
|
55
70
|
// {§provider-call-kind} The caller-owned semantic contract for one logical model call. Providers do
|
|
56
71
|
// not infer this from messages or grammar presence: an emission expects PLURNK
|
|
@@ -299,6 +314,8 @@ export interface Provider {
|
|
|
299
314
|
readonly reasoningBudget: number | null;
|
|
300
315
|
// Exact durable policies this adapter can represent without coercion.
|
|
301
316
|
readonly supportedReasoningPolicies: readonly ReasoningPolicy[];
|
|
317
|
+
// {§provider-input-modalities} — the native non-text parts the route's model accepts.
|
|
318
|
+
readonly inputModalities: ReadonlySet<InputModality>;
|
|
302
319
|
readonly inputCapacity: number | null;
|
|
303
320
|
readonly model: string;
|
|
304
321
|
// Optional: the backend's self-reported served model id, from a
|