@plurnk/plurnk-providers 1.11.0 → 1.12.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 +5 -1
- package/SPEC.md +57 -31
- package/dist/AiSdkProvider.d.ts +5 -1
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +29 -7
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +13 -3
- package/dist/accounting.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +91 -34
- package/dist/catalogProvider.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sdkModels.d.ts +13 -1
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +103 -5
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +2 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -6
- package/src/AiSdkProvider.ts +42 -11
- package/src/accounting.test.ts +39 -0
- package/src/accounting.ts +13 -4
- package/src/catalogProvider.test.ts +80 -13
- package/src/catalogProvider.ts +127 -38
- package/src/index.ts +2 -1
- package/src/sdkModels.test.ts +42 -1
- package/src/sdkModels.ts +144 -7
- package/src/types.ts +4 -15
package/src/sdkModels.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import { configuredProviderInfo, createSdkModel, providerReadiness } from "./sdkModels.ts";
|
|
3
|
+
import { configuredProviderInfo, createEmbeddingModel, createSdkModel, providerReadiness } from "./sdkModels.ts";
|
|
4
4
|
|
|
5
5
|
test("{§provider-fact-authority} one env declaration holds one credential name", () => {
|
|
6
6
|
assert.deepEqual(configuredProviderInfo("acme-cloud", {
|
|
@@ -290,3 +290,44 @@ test("createSdkModel fails clearly for a declared but unsupported SDK package",
|
|
|
290
290
|
/Models.dev declares unsupported AI SDK package @acme\/ai-sdk/,
|
|
291
291
|
);
|
|
292
292
|
});
|
|
293
|
+
|
|
294
|
+
test("{§provider-embedding-resolution} compatible catalog and declared routes share the standard embedding adapter", () => {
|
|
295
|
+
const cloudflare = createEmbeddingModel("cloudflare", "@cf/qwen/qwen3-embedding-0.6b", {
|
|
296
|
+
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
297
|
+
CLOUDFLARE_API_KEY: "key",
|
|
298
|
+
});
|
|
299
|
+
assert.equal(cloudflare?.providerId, "cloudflare-workers-ai");
|
|
300
|
+
assert.equal(cloudflare?.embeddingModel.provider, "cloudflare.embedding");
|
|
301
|
+
assert.equal(cloudflare?.embeddingModel.modelId, "@cf/qwen/qwen3-embedding-0.6b");
|
|
302
|
+
|
|
303
|
+
const fireworks = createEmbeddingModel("fireworks", "fireworks/qwen3-embedding-8b", {
|
|
304
|
+
FIREWORKS_API_KEY: "key",
|
|
305
|
+
});
|
|
306
|
+
assert.equal(fireworks?.providerId, "fireworks-ai");
|
|
307
|
+
assert.equal(fireworks?.embeddingModel.provider, "fireworks.embedding");
|
|
308
|
+
|
|
309
|
+
const local = createEmbeddingModel("local-embed", "Qwen/Qwen3-Embedding-0.6B", {
|
|
310
|
+
PLURNK_PROVIDERS_PROVIDER_LOCAL_EMBED_NPM: "@ai-sdk/openai-compatible",
|
|
311
|
+
PLURNK_PROVIDERS_PROVIDER_LOCAL_EMBED_BASE_URL: "http://127.0.0.1:8080/v1",
|
|
312
|
+
});
|
|
313
|
+
assert.equal(local?.providerId, "local-embed");
|
|
314
|
+
assert.equal(local?.embeddingModel.provider, "local-embed.embedding");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("{§provider-embedding-resolution} OpenRouter uses its official embedding model", () => {
|
|
318
|
+
const resolved = createEmbeddingModel("openrouter", "openai/text-embedding-3-small", {
|
|
319
|
+
OPENROUTER_API_KEY: "key",
|
|
320
|
+
OPENROUTER_HTTP_REFERER: "https://github.com/plurnk/plurnk-service",
|
|
321
|
+
OPENROUTER_APP_TITLE: "Plurnk",
|
|
322
|
+
});
|
|
323
|
+
assert.equal(resolved?.providerId, "openrouter");
|
|
324
|
+
assert.equal(resolved?.embeddingModel.provider, "openrouter");
|
|
325
|
+
assert.equal(resolved?.embeddingModel.modelId, "openai/text-embedding-3-small");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("{§provider-embedding-resolution} a generation-only SDK fails before transport", () => {
|
|
329
|
+
assert.throws(
|
|
330
|
+
() => createEmbeddingModel("cerebras", "text-embedding", { CEREBRAS_API_KEY: "key" }),
|
|
331
|
+
/@ai-sdk\/cerebras does not expose an embedding model/,
|
|
332
|
+
);
|
|
333
|
+
});
|
package/src/sdkModels.ts
CHANGED
|
@@ -6,18 +6,29 @@ import { createGoogle } from "@ai-sdk/google";
|
|
|
6
6
|
import { createGroq } from "@ai-sdk/groq";
|
|
7
7
|
import { createMistral } from "@ai-sdk/mistral";
|
|
8
8
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
9
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
9
10
|
import { createTogetherAI } from "@ai-sdk/togetherai";
|
|
10
11
|
import { createXai } from "@ai-sdk/xai";
|
|
12
|
+
import type { EmbeddingModelV4, EmbeddingModelV4Result } from "@ai-sdk/provider";
|
|
11
13
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
12
14
|
import {
|
|
13
15
|
isProviderCredentialName,
|
|
16
|
+
lookup,
|
|
14
17
|
lookupProvider,
|
|
15
18
|
providerCatalogSnapshot,
|
|
16
19
|
type ProviderInfo,
|
|
17
20
|
} from "@plurnk/plurnk-models";
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
Validator,
|
|
23
|
+
type ModelReadiness,
|
|
24
|
+
type ModelReadinessCause,
|
|
25
|
+
type ProviderRequestAccounting,
|
|
26
|
+
type ProviderUsage,
|
|
27
|
+
type ReasoningPolicy,
|
|
28
|
+
} from "@plurnk/plurnk-contracts";
|
|
19
29
|
import type { LanguageModel } from "ai";
|
|
20
|
-
import { providerCostNormalizer } from "./accounting.ts";
|
|
30
|
+
import { providerCostNormalizer, validateProviderRequestAccounting } from "./accounting.ts";
|
|
31
|
+
import { estimateProviderCost, resolveProviderCost } from "./cost.ts";
|
|
21
32
|
import type { AiSdkProviderOptions, CacheAffinity } from "./AiSdkProvider.ts";
|
|
22
33
|
import type { ProviderCostNormalizer } from "./types.ts";
|
|
23
34
|
|
|
@@ -35,6 +46,17 @@ export type SdkModel = {
|
|
|
35
46
|
readonly catalog: ProviderInfo | null;
|
|
36
47
|
};
|
|
37
48
|
|
|
49
|
+
export type EmbeddingModelResolution = {
|
|
50
|
+
readonly embeddingModel: EmbeddingModelV4;
|
|
51
|
+
readonly providerId: string;
|
|
52
|
+
readonly modelId: string;
|
|
53
|
+
readonly normalizeAccounting: (evidence: {
|
|
54
|
+
readonly outcome: "response" | "error";
|
|
55
|
+
readonly status?: number;
|
|
56
|
+
readonly result?: EmbeddingModelV4Result;
|
|
57
|
+
}) => ProviderRequestAccounting;
|
|
58
|
+
};
|
|
59
|
+
|
|
38
60
|
const cacheControl = { type: "ephemeral" as const };
|
|
39
61
|
// The release generator admits only packages implemented by this package.
|
|
40
62
|
// Derive runtime readiness from that same pinned provider projection instead
|
|
@@ -297,20 +319,30 @@ const openRouterReasoningSettings = (
|
|
|
297
319
|
? {}
|
|
298
320
|
: { reasoning: { effort: reasoning === "off" ? "none" : reasoning } };
|
|
299
321
|
|
|
300
|
-
|
|
322
|
+
const resolveSdkProvider = (
|
|
301
323
|
provider: string,
|
|
302
|
-
model: string,
|
|
303
324
|
env: NodeJS.ProcessEnv,
|
|
304
325
|
baseUrlOverride?: string,
|
|
305
|
-
|
|
306
|
-
): SdkModel | null => {
|
|
326
|
+
): { catalog: ProviderInfo; url: string | undefined } | null => {
|
|
307
327
|
const catalog = lookupProvider(provider) ?? configuredProviderInfo(provider, env);
|
|
308
328
|
if (catalog === null) return null;
|
|
309
329
|
if (!supportedSdkPackages.has(catalog.npm)) {
|
|
310
330
|
throw new Error(`${provider} provider: Models.dev declares unsupported AI SDK package ${catalog.npm}`);
|
|
311
331
|
}
|
|
312
332
|
assertProviderReady(provider, env, baseUrlOverride);
|
|
313
|
-
|
|
333
|
+
return { catalog, url: baseUrl(provider, env, catalog, baseUrlOverride) };
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export const createSdkModel = (
|
|
337
|
+
provider: string,
|
|
338
|
+
model: string,
|
|
339
|
+
env: NodeJS.ProcessEnv,
|
|
340
|
+
baseUrlOverride?: string,
|
|
341
|
+
reasoning?: ReasoningPolicy,
|
|
342
|
+
): SdkModel | null => {
|
|
343
|
+
const resolved = resolveSdkProvider(provider, env, baseUrlOverride);
|
|
344
|
+
if (resolved === null) return null;
|
|
345
|
+
const { catalog, url } = resolved;
|
|
314
346
|
const normalizeCost = providerCostNormalizer(catalog.npm);
|
|
315
347
|
|
|
316
348
|
switch (catalog.npm) {
|
|
@@ -430,3 +462,108 @@ export const createSdkModel = (
|
|
|
430
462
|
throw new Error(`${provider} provider: Models.dev declares unsupported AI SDK package ${catalog.npm}`);
|
|
431
463
|
}
|
|
432
464
|
};
|
|
465
|
+
|
|
466
|
+
// {§provider-embedding-resolution} Provider construction owns transport and
|
|
467
|
+
// authentication only. Model-space facts stay in the embedding profile.
|
|
468
|
+
export const createEmbeddingModel = (
|
|
469
|
+
provider: string,
|
|
470
|
+
model: string,
|
|
471
|
+
env: NodeJS.ProcessEnv,
|
|
472
|
+
baseUrlOverride?: string,
|
|
473
|
+
): EmbeddingModelResolution | null => {
|
|
474
|
+
const resolved = resolveSdkProvider(provider, env, baseUrlOverride);
|
|
475
|
+
if (resolved === null) return null;
|
|
476
|
+
const { catalog, url } = resolved;
|
|
477
|
+
const directCost = providerCostNormalizer(catalog.npm);
|
|
478
|
+
const modelCost = lookup(provider, model)?.cost ?? lookup(catalog.id, model)?.cost;
|
|
479
|
+
const rates = modelCost === undefined
|
|
480
|
+
? null
|
|
481
|
+
: {
|
|
482
|
+
input: modelCost.inputPer1M,
|
|
483
|
+
output: modelCost.outputPer1M,
|
|
484
|
+
...(modelCost.reasoningPer1M === undefined ? {} : { reasoning: modelCost.reasoningPer1M }),
|
|
485
|
+
...(modelCost.cacheReadPer1M === undefined ? {} : { cacheRead: modelCost.cacheReadPer1M }),
|
|
486
|
+
...(modelCost.cacheWritePer1M === undefined ? {} : { cacheWrite: modelCost.cacheWritePer1M }),
|
|
487
|
+
};
|
|
488
|
+
const normalizeAccounting: EmbeddingModelResolution["normalizeAccounting"] = (evidence) => {
|
|
489
|
+
const tokens = evidence.result?.usage?.tokens;
|
|
490
|
+
const usage: ProviderUsage | undefined = Number.isSafeInteger(tokens) && tokens! >= 0
|
|
491
|
+
? { inputTokens: tokens!, outputTokens: 0, totalTokens: tokens! }
|
|
492
|
+
: undefined;
|
|
493
|
+
const body = evidence.result?.response?.body;
|
|
494
|
+
const rawUsage = typeof body === "object" && body !== null && !Array.isArray(body)
|
|
495
|
+
? (body as Record<string, unknown>).usage
|
|
496
|
+
: undefined;
|
|
497
|
+
const direct = directCost?.({
|
|
498
|
+
providerMetadata: evidence.result?.providerMetadata,
|
|
499
|
+
usage: rawUsage,
|
|
500
|
+
response: { headers: evidence.result?.response?.headers },
|
|
501
|
+
});
|
|
502
|
+
const cost = resolveProviderCost(
|
|
503
|
+
direct,
|
|
504
|
+
estimateProviderCost(
|
|
505
|
+
usage,
|
|
506
|
+
rates,
|
|
507
|
+
`Models.dev ${catalog.id}/${model} rates`,
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
return validateProviderRequestAccounting({
|
|
511
|
+
provider: catalog.id,
|
|
512
|
+
model,
|
|
513
|
+
outcome: evidence.outcome,
|
|
514
|
+
...(evidence.status === undefined ? {} : { status: evidence.status }),
|
|
515
|
+
...(usage === undefined ? {} : { usage }),
|
|
516
|
+
cost,
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const result = (embeddingModel: EmbeddingModelV4): EmbeddingModelResolution => ({
|
|
520
|
+
embeddingModel,
|
|
521
|
+
providerId: catalog.id,
|
|
522
|
+
modelId: model,
|
|
523
|
+
normalizeAccounting,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
switch (catalog.npm) {
|
|
527
|
+
case "@ai-sdk/openai":
|
|
528
|
+
return result(createOpenAI({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embedding(model));
|
|
529
|
+
case "@ai-sdk/google":
|
|
530
|
+
return result(createGoogle({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embedding(model));
|
|
531
|
+
case "@ai-sdk/mistral":
|
|
532
|
+
return result(createMistral({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embedding(model));
|
|
533
|
+
case "@ai-sdk/deepinfra":
|
|
534
|
+
return result(createDeepInfra({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embeddingModel(model));
|
|
535
|
+
case "@ai-sdk/togetherai":
|
|
536
|
+
return result(createTogetherAI({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embeddingModel(model));
|
|
537
|
+
case "@ai-sdk/amazon-bedrock":
|
|
538
|
+
return result(createAmazonBedrock({
|
|
539
|
+
region: env.AWS_REGION ?? env.AWS_DEFAULT_REGION,
|
|
540
|
+
accessKeyId: env.AWS_ACCESS_KEY_ID,
|
|
541
|
+
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
|
|
542
|
+
sessionToken: env.AWS_SESSION_TOKEN,
|
|
543
|
+
apiKey: env.AWS_BEARER_TOKEN_BEDROCK,
|
|
544
|
+
baseURL: url,
|
|
545
|
+
}).embedding(model));
|
|
546
|
+
case "@openrouter/ai-sdk-provider":
|
|
547
|
+
return result(createOpenRouter({
|
|
548
|
+
apiKey: requireApiKey(provider, env, catalog),
|
|
549
|
+
baseURL: url,
|
|
550
|
+
headers: openRouterHeaders(provider, env, catalog),
|
|
551
|
+
}).textEmbeddingModel(model));
|
|
552
|
+
case "@ai-sdk/openai-compatible": {
|
|
553
|
+
if (url === undefined) {
|
|
554
|
+
throw new Error(`${provider} provider: Models.dev supplies no API URL and no base URL was configured`);
|
|
555
|
+
}
|
|
556
|
+
const keyNames = configuredKeyNames(provider, env, catalog);
|
|
557
|
+
const key = keyNames.length === 0 ? undefined : requireApiKey(provider, env, catalog);
|
|
558
|
+
return result(createOpenAICompatible({
|
|
559
|
+
name: provider,
|
|
560
|
+
baseURL: url,
|
|
561
|
+
...(key === undefined ? {} : { apiKey: key }),
|
|
562
|
+
}).embeddingModel(model));
|
|
563
|
+
}
|
|
564
|
+
default:
|
|
565
|
+
throw new Error(
|
|
566
|
+
`${provider} provider: ${catalog.npm} does not expose an embedding model through Plurnk's AI SDK adapter`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import type {
|
|
13
13
|
ProviderCost,
|
|
14
14
|
ProviderRequestAccounting,
|
|
15
|
+
ProviderRequestObserver,
|
|
15
16
|
ReasoningPolicy,
|
|
16
17
|
} from "@plurnk/plurnk-contracts";
|
|
17
18
|
|
|
@@ -19,6 +20,9 @@ export type {
|
|
|
19
20
|
ProviderAccounting,
|
|
20
21
|
ProviderCost,
|
|
21
22
|
ProviderRequestAccounting,
|
|
23
|
+
ProviderRequestIdentity,
|
|
24
|
+
ProviderRequestObserver,
|
|
25
|
+
ProviderRequestSettlement,
|
|
22
26
|
ProviderUsage,
|
|
23
27
|
ReasoningPolicy,
|
|
24
28
|
} from "@plurnk/plurnk-contracts";
|
|
@@ -105,26 +109,11 @@ export type ProviderCostNormalizer = (
|
|
|
105
109
|
evidence: ProviderChargeEvidence,
|
|
106
110
|
) => ProviderCost | undefined;
|
|
107
111
|
|
|
108
|
-
export interface ProviderRequestIdentity {
|
|
109
|
-
readonly provider: string;
|
|
110
|
-
readonly model: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export type ProviderRequestSettlement = (
|
|
114
|
-
accounting: ProviderRequestAccounting,
|
|
115
|
-
) => Promise<void>;
|
|
116
|
-
|
|
117
112
|
// {§provider-reasoning-observer} Transient readable-reasoning observation. The completed ProviderResponse is
|
|
118
113
|
// still the sole authoritative value; this callback exposes exact, ordered,
|
|
119
114
|
// nonempty deltas early enough for an attached client to render them live.
|
|
120
115
|
export type ProviderReasoningObserver = (delta: string) => void;
|
|
121
116
|
|
|
122
|
-
// Core opens durable physical-request identity through this observer before
|
|
123
|
-
// provider I/O. The returned settlement closes that exact identity.
|
|
124
|
-
export type ProviderRequestObserver = (
|
|
125
|
-
identity: ProviderRequestIdentity,
|
|
126
|
-
) => Promise<ProviderRequestSettlement>;
|
|
127
|
-
|
|
128
117
|
// A successful exchange's closed finish set. ProviderAttemptFinishReason adds
|
|
129
118
|
// the failed disposition that may occur only on ProviderError attempt evidence.
|
|
130
119
|
export type FinishReason = "stop" | "length" | "tool_calls" | "content_filter" | null;
|