@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.
@@ -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 { Validator, type ModelReadiness, type ModelReadinessCause, type ReasoningPolicy } from "@plurnk/plurnk-contracts";
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
- export const createSdkModel = (
322
+ const resolveSdkProvider = (
301
323
  provider: string,
302
- model: string,
303
324
  env: NodeJS.ProcessEnv,
304
325
  baseUrlOverride?: string,
305
- reasoning?: ReasoningPolicy,
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
- const url = baseUrl(provider, env, catalog, baseUrlOverride);
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;