@plurnk/plurnk-providers 1.16.4 → 1.17.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.
Files changed (69) hide show
  1. package/.env.defaults +87 -200
  2. package/README.md +2 -1
  3. package/SPEC.md +73 -66
  4. package/dist/AiSdkProvider.d.ts +0 -1
  5. package/dist/AiSdkProvider.d.ts.map +1 -1
  6. package/dist/AiSdkProvider.js +25 -55
  7. package/dist/AiSdkProvider.js.map +1 -1
  8. package/dist/AiSdkRequestBody.d.ts +0 -1
  9. package/dist/AiSdkRequestBody.d.ts.map +1 -1
  10. package/dist/AiSdkRequestBody.js +1 -17
  11. package/dist/AiSdkRequestBody.js.map +1 -1
  12. package/dist/LeadingReasoning.d.ts +16 -0
  13. package/dist/LeadingReasoning.d.ts.map +1 -0
  14. package/dist/LeadingReasoning.js +81 -0
  15. package/dist/LeadingReasoning.js.map +1 -0
  16. package/dist/aiSdkTransport.d.ts +7 -0
  17. package/dist/aiSdkTransport.d.ts.map +1 -1
  18. package/dist/aiSdkTransport.js +19 -5
  19. package/dist/aiSdkTransport.js.map +1 -1
  20. package/dist/catalogProvider.d.ts +3 -1
  21. package/dist/catalogProvider.d.ts.map +1 -1
  22. package/dist/catalogProvider.js +24 -18
  23. package/dist/catalogProvider.js.map +1 -1
  24. package/dist/compatibleProvider.d.ts.map +1 -1
  25. package/dist/compatibleProvider.js +0 -3
  26. package/dist/compatibleProvider.js.map +1 -1
  27. package/dist/env.d.ts.map +1 -1
  28. package/dist/env.js +0 -1
  29. package/dist/env.js.map +1 -1
  30. package/dist/errors.d.ts +6 -0
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +46 -15
  33. package/dist/errors.js.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/providerError.d.ts +1 -1
  39. package/dist/providerError.d.ts.map +1 -1
  40. package/dist/providerError.js +3 -0
  41. package/dist/providerError.js.map +1 -1
  42. package/dist/sdkModels.d.ts +1 -13
  43. package/dist/sdkModels.d.ts.map +1 -1
  44. package/dist/sdkModels.js +2 -94
  45. package/dist/sdkModels.js.map +1 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/docs/models.md +90 -0
  48. package/package.json +6 -6
  49. package/src/AiSdkProvider.test.ts +99 -38
  50. package/src/AiSdkProvider.ts +24 -78
  51. package/src/AiSdkRequestBody.ts +1 -16
  52. package/src/LeadingReasoning.test.ts +34 -0
  53. package/src/LeadingReasoning.ts +87 -0
  54. package/src/ProviderRegistry.test.ts +19 -1
  55. package/src/aiSdkTransport.test.ts +56 -0
  56. package/src/aiSdkTransport.ts +31 -5
  57. package/src/boundaries.test.ts +1 -0
  58. package/src/catalogProvider.test.ts +110 -20
  59. package/src/catalogProvider.ts +40 -18
  60. package/src/compatibleProvider.ts +0 -3
  61. package/src/env.test.ts +1 -0
  62. package/src/env.ts +1 -2
  63. package/src/errors.test.ts +58 -7
  64. package/src/errors.ts +55 -18
  65. package/src/index.ts +2 -2
  66. package/src/providerError.ts +4 -0
  67. package/src/sdkModels.test.ts +1 -42
  68. package/src/sdkModels.ts +1 -123
  69. package/src/types.ts +8 -10
@@ -7,6 +7,7 @@ export type ProviderErrorKind =
7
7
  | "network_failure"
8
8
  | "deadline_exceeded"
9
9
  | "model_refused"
10
+ | "request_rejected"
10
11
  | "invalid_response"
11
12
  | "unauthorized"
12
13
  | "quota_exceeded"
@@ -16,6 +17,7 @@ export type ProviderErrorKind =
16
17
 
17
18
  const defaultStatus = (kind: ProviderErrorKind): number => {
18
19
  switch (kind) {
20
+ case "request_rejected": return 400;
19
21
  case "unauthorized": return 401;
20
22
  case "quota_exceeded": return 402;
21
23
  case "capacity_exceeded": return 413;
@@ -36,6 +38,7 @@ const retryable = (kind: ProviderErrorKind): boolean => {
36
38
  return true;
37
39
  case "deadline_exceeded":
38
40
  case "invalid_response":
41
+ case "request_rejected":
39
42
  case "grammar_invalid":
40
43
  case "capacity_exceeded":
41
44
  case "resource_interrupted":
@@ -60,6 +63,7 @@ const buildProblem = (
60
63
  deadline_exceeded: "deadline-exceeded",
61
64
  model_refused: "model-refused",
62
65
  invalid_response: "invalid-response",
66
+ request_rejected: "request-rejected",
63
67
  unauthorized: "unauthorized",
64
68
  quota_exceeded: "quota-exceeded",
65
69
  grammar_invalid: "grammar-invalid",
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
- import { configuredProviderInfo, createEmbeddingModel, createSdkModel, providerReadiness } from "./sdkModels.ts";
3
+ import { configuredProviderInfo, 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", {
@@ -301,44 +301,3 @@ test("createSdkModel fails clearly for a declared but unsupported SDK package",
301
301
  /Models.dev declares unsupported AI SDK package @acme\/ai-sdk/,
302
302
  );
303
303
  });
304
-
305
- test("{§provider-embedding-resolution} compatible catalog and declared routes share the standard embedding adapter", () => {
306
- const cloudflare = createEmbeddingModel("cloudflare-workers-ai", "@cf/qwen/qwen3-embedding-0.6b", {
307
- CLOUDFLARE_ACCOUNT_ID: "account",
308
- CLOUDFLARE_API_KEY: "key",
309
- });
310
- assert.equal(cloudflare?.providerId, "cloudflare-workers-ai");
311
- assert.equal(cloudflare?.embeddingModel.provider, "cloudflare-workers-ai.embedding");
312
- assert.equal(cloudflare?.embeddingModel.modelId, "@cf/qwen/qwen3-embedding-0.6b");
313
-
314
- const fireworks = createEmbeddingModel("fireworks-ai", "fireworks/qwen3-embedding-8b", {
315
- FIREWORKS_API_KEY: "key",
316
- });
317
- assert.equal(fireworks?.providerId, "fireworks-ai");
318
- assert.equal(fireworks?.embeddingModel.provider, "fireworks-ai.embedding");
319
-
320
- const local = createEmbeddingModel("local-embed", "Qwen/Qwen3-Embedding-0.6B", {
321
- PLURNK_PROVIDERS_PROVIDER_LOCAL_EMBED_NPM: "@ai-sdk/openai-compatible",
322
- PLURNK_PROVIDERS_PROVIDER_LOCAL_EMBED_BASE_URL: "http://127.0.0.1:8080/v1",
323
- });
324
- assert.equal(local?.providerId, "local-embed");
325
- assert.equal(local?.embeddingModel.provider, "local-embed.embedding");
326
- });
327
-
328
- test("{§provider-embedding-resolution} OpenRouter uses its official embedding model", () => {
329
- const resolved = createEmbeddingModel("openrouter", "openai/text-embedding-3-small", {
330
- OPENROUTER_API_KEY: "key",
331
- OPENROUTER_HTTP_REFERER: "https://github.com/plurnk/plurnk-service",
332
- OPENROUTER_APP_TITLE: "Plurnk",
333
- });
334
- assert.equal(resolved?.providerId, "openrouter");
335
- assert.equal(resolved?.embeddingModel.provider, "openrouter");
336
- assert.equal(resolved?.embeddingModel.modelId, "openai/text-embedding-3-small");
337
- });
338
-
339
- test("{§provider-embedding-resolution} a generation-only SDK fails before transport", () => {
340
- assert.throws(
341
- () => createEmbeddingModel("cerebras", "text-embedding", { CEREBRAS_API_KEY: "key" }),
342
- /@ai-sdk\/cerebras does not expose an embedding model/,
343
- );
344
- });
package/src/sdkModels.ts CHANGED
@@ -6,15 +6,12 @@ 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";
10
9
  import { createTogetherAI } from "@ai-sdk/togetherai";
11
10
  import { createXai } from "@ai-sdk/xai";
12
- import type { EmbeddingModelV4, EmbeddingModelV4Result } from "@ai-sdk/provider";
13
11
  import { createOpenRouter, type OpenRouterChatSettings } from "@openrouter/ai-sdk-provider";
14
12
  import {
15
13
  resolveModel,
16
14
  isProviderCredentialName,
17
- lookup,
18
15
  lookupProvider,
19
16
  providerCatalogSnapshot,
20
17
  type ProviderInfo,
@@ -23,13 +20,10 @@ import {
23
20
  Validator,
24
21
  type ModelReadiness,
25
22
  type ModelReadinessCause,
26
- type ProviderRequestAccounting,
27
- type ProviderUsage,
28
23
  type ReasoningPolicy,
29
24
  } from "@plurnk/plurnk-contracts";
30
25
  import type { LanguageModel } from "ai";
31
- import { providerCostNormalizer, validateProviderRequestAccounting } from "./accounting.ts";
32
- import { estimateProviderCost, resolveProviderCost } from "./cost.ts";
26
+ import { providerCostNormalizer } from "./accounting.ts";
33
27
  import type { AiSdkProviderOptions, CacheAffinity } from "./AiSdkProvider.ts";
34
28
  import type { ProviderCostNormalizer } from "./types.ts";
35
29
 
@@ -47,17 +41,6 @@ export type SdkModel = {
47
41
  readonly catalog: ProviderInfo | null;
48
42
  };
49
43
 
50
- export type EmbeddingModelResolution = {
51
- readonly embeddingModel: EmbeddingModelV4;
52
- readonly providerId: string;
53
- readonly modelId: string;
54
- readonly normalizeAccounting: (evidence: {
55
- readonly outcome: "response" | "error";
56
- readonly status?: number;
57
- readonly result?: EmbeddingModelV4Result;
58
- }) => ProviderRequestAccounting;
59
- };
60
-
61
44
  const cacheControl = { type: "ephemeral" as const };
62
45
  // The release generator admits only packages implemented by this package.
63
46
  // Derive runtime readiness from that same pinned provider projection instead
@@ -485,108 +468,3 @@ export const createSdkModel = (
485
468
  throw new Error(`${provider} provider: Models.dev declares unsupported AI SDK package ${catalog.npm}`);
486
469
  }
487
470
  };
488
-
489
- // {§provider-embedding-resolution} Provider construction owns transport and
490
- // authentication only. Model-space facts stay in the embedding profile.
491
- export const createEmbeddingModel = (
492
- provider: string,
493
- model: string,
494
- env: NodeJS.ProcessEnv,
495
- baseUrlOverride?: string,
496
- ): EmbeddingModelResolution | null => {
497
- const resolved = resolveSdkProvider(provider, env, baseUrlOverride);
498
- if (resolved === null) return null;
499
- const { catalog, url } = resolved;
500
- const directCost = providerCostNormalizer(catalog.npm);
501
- const modelCost = lookup(provider, model)?.cost ?? lookup(catalog.id, model)?.cost;
502
- const rates = modelCost === undefined
503
- ? null
504
- : {
505
- input: modelCost.inputPer1M,
506
- output: modelCost.outputPer1M,
507
- ...(modelCost.reasoningPer1M === undefined ? {} : { reasoning: modelCost.reasoningPer1M }),
508
- ...(modelCost.cacheReadPer1M === undefined ? {} : { cacheRead: modelCost.cacheReadPer1M }),
509
- ...(modelCost.cacheWritePer1M === undefined ? {} : { cacheWrite: modelCost.cacheWritePer1M }),
510
- };
511
- const normalizeAccounting: EmbeddingModelResolution["normalizeAccounting"] = (evidence) => {
512
- const tokens = evidence.result?.usage?.tokens;
513
- const usage: ProviderUsage | undefined = Number.isSafeInteger(tokens) && tokens! >= 0
514
- ? { inputTokens: tokens!, outputTokens: 0, totalTokens: tokens! }
515
- : undefined;
516
- const body = evidence.result?.response?.body;
517
- const rawUsage = typeof body === "object" && body !== null && !Array.isArray(body)
518
- ? (body as Record<string, unknown>).usage
519
- : undefined;
520
- const direct = directCost?.({
521
- providerMetadata: evidence.result?.providerMetadata,
522
- usage: rawUsage,
523
- response: { headers: evidence.result?.response?.headers },
524
- });
525
- const cost = resolveProviderCost(
526
- direct,
527
- estimateProviderCost(
528
- usage,
529
- rates,
530
- `Models.dev ${catalog.id}/${model} rates`,
531
- ),
532
- );
533
- return validateProviderRequestAccounting({
534
- provider: catalog.id,
535
- model,
536
- outcome: evidence.outcome,
537
- ...(evidence.status === undefined ? {} : { status: evidence.status }),
538
- ...(usage === undefined ? {} : { usage }),
539
- cost,
540
- });
541
- };
542
- const result = (embeddingModel: EmbeddingModelV4): EmbeddingModelResolution => ({
543
- embeddingModel,
544
- providerId: catalog.id,
545
- modelId: model,
546
- normalizeAccounting,
547
- });
548
-
549
- switch (catalog.npm) {
550
- case "@ai-sdk/openai":
551
- return result(createOpenAI({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embedding(model));
552
- case "@ai-sdk/google":
553
- return result(createGoogle({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embedding(model));
554
- case "@ai-sdk/mistral":
555
- return result(createMistral({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embedding(model));
556
- case "@ai-sdk/deepinfra":
557
- return result(createDeepInfra({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embeddingModel(model));
558
- case "@ai-sdk/togetherai":
559
- return result(createTogetherAI({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).embeddingModel(model));
560
- case "@ai-sdk/amazon-bedrock":
561
- return result(createAmazonBedrock({
562
- region: env.AWS_REGION ?? env.AWS_DEFAULT_REGION,
563
- accessKeyId: env.AWS_ACCESS_KEY_ID,
564
- secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
565
- sessionToken: env.AWS_SESSION_TOKEN,
566
- apiKey: env.AWS_BEARER_TOKEN_BEDROCK,
567
- baseURL: url,
568
- }).embedding(model));
569
- case "@openrouter/ai-sdk-provider":
570
- return result(createOpenRouter({
571
- apiKey: requireApiKey(provider, env, catalog),
572
- baseURL: url,
573
- headers: openRouterHeaders(provider, env, catalog),
574
- }).textEmbeddingModel(model));
575
- case "@ai-sdk/openai-compatible": {
576
- if (url === undefined) {
577
- throw new Error(`${provider} provider: Models.dev supplies no API URL and no base URL was configured`);
578
- }
579
- const keyNames = configuredKeyNames(provider, env, catalog);
580
- const key = keyNames.length === 0 ? undefined : requireApiKey(provider, env, catalog);
581
- return result(createOpenAICompatible({
582
- name: provider,
583
- baseURL: url,
584
- ...(key === undefined ? {} : { apiKey: key }),
585
- }).embeddingModel(model));
586
- }
587
- default:
588
- throw new Error(
589
- `${provider} provider: ${catalog.npm} does not expose an embedding model through Plurnk's AI SDK adapter`,
590
- );
591
- }
592
- };
package/src/types.ts CHANGED
@@ -187,10 +187,10 @@ export interface ProviderAssistant<TFinish extends ProviderAttemptFinishReason =
187
187
  readonly meanLogprob?: number;
188
188
  }
189
189
 
190
+ // {§provider-grammar-transport} — transport evidence for an operator's grammar: the exact
191
+ // pre-projection response, where the projected content starts within it, and whether the
192
+ // grammar reached the wire. Evidence only; nothing grades it.
190
193
  export interface GrammarEvidence {
191
- // Exact pre-projection response represented by the provider. A generated
192
- // rail's response root composes any template prefix before @plurnk/gbnf
193
- // grades it. Offsets are Unicode code points, matching validator verdicts.
194
194
  readonly input: string;
195
195
  readonly contentStart: number;
196
196
  readonly transported: boolean;
@@ -203,7 +203,7 @@ export interface ProviderResponse<TFinish extends ProviderAttemptFinishReason =
203
203
  // failover that preceded this response. {§provider-request-accounting}
204
204
  readonly accounting: readonly ProviderRequestAccounting[];
205
205
  readonly capacity: ProviderRequestCapacity;
206
- // {§gbnf-response-observation} — evidence only; the consumer owns the verdict.
206
+ // {§provider-grammar-transport} — transport evidence; present only when a grammar was configured.
207
207
  readonly grammarEvidence?: GrammarEvidence;
208
208
  // Per-turn provider→client metadata bag: the backend's non-standard top-level
209
209
  // response fields passed through verbatim. Monetary values carry their own
@@ -250,12 +250,10 @@ export interface Provider {
250
250
  // Optional package-authored folksonomy evaluated by the consumer immediately
251
251
  // before a provider emission attempt ({§plugin-attribution}).
252
252
  attributions?(context: PluginAttributionContext): PluginAttribution;
253
- // `grammar` is an optional GBNF string (canonically @plurnk/plurnk-contracts'
254
- // plurnk.gemma.gbnf or plurnk.qwen.gbnf, possibly root-substituted by the consumer). Backends that
255
- // support grammar-constrained sampling attach it verbatim; all others
256
- // ignore it. The provider never chooses or modifies the grammar — whether
257
- // to constrain and which root variant to send is consumer policy
258
- // ({§gbnf-response-observation}).
253
+ // `grammar` is an optional GBNF string, the operator's own file read by the consumer.
254
+ // Backends that support grammar-constrained sampling attach it verbatim; all others
255
+ // ignore it. The provider never chooses or modifies the grammar
256
+ // ({§provider-grammar-transport}).
259
257
  //
260
258
  // `maxOutputTokens` may tighten the provider's configured total output
261
259
  // budget for this call. It includes visible output and hidden reasoning;