@plurnk/plurnk-providers 1.3.12 → 1.4.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 (132) hide show
  1. package/.env.defaults +39 -22
  2. package/README.md +65 -4
  3. package/SPEC.md +222 -56
  4. package/dist/AiSdkProvider.d.ts +18 -5
  5. package/dist/AiSdkProvider.d.ts.map +1 -1
  6. package/dist/AiSdkProvider.js +240 -153
  7. package/dist/AiSdkProvider.js.map +1 -1
  8. package/dist/Mock.d.ts +14 -15
  9. package/dist/Mock.d.ts.map +1 -1
  10. package/dist/Mock.js +26 -10
  11. package/dist/Mock.js.map +1 -1
  12. package/dist/Pool.d.ts +8 -2
  13. package/dist/Pool.d.ts.map +1 -1
  14. package/dist/Pool.js +41 -8
  15. package/dist/Pool.js.map +1 -1
  16. package/dist/ProviderRegistry.d.ts +4 -1
  17. package/dist/ProviderRegistry.d.ts.map +1 -1
  18. package/dist/ProviderRegistry.js +7 -3
  19. package/dist/ProviderRegistry.js.map +1 -1
  20. package/dist/aiSdkTransport.d.ts +4 -2
  21. package/dist/aiSdkTransport.d.ts.map +1 -1
  22. package/dist/aiSdkTransport.js +18 -3
  23. package/dist/aiSdkTransport.js.map +1 -1
  24. package/dist/catalogProvider.d.ts +3 -1
  25. package/dist/catalogProvider.d.ts.map +1 -1
  26. package/dist/catalogProvider.js +20 -7
  27. package/dist/catalogProvider.js.map +1 -1
  28. package/dist/compatibleProvider.d.ts.map +1 -1
  29. package/dist/compatibleProvider.js +11 -4
  30. package/dist/compatibleProvider.js.map +1 -1
  31. package/dist/cost.d.ts +11 -0
  32. package/dist/cost.d.ts.map +1 -0
  33. package/dist/cost.js +64 -0
  34. package/dist/cost.js.map +1 -0
  35. package/dist/discover.d.ts +2 -0
  36. package/dist/discover.d.ts.map +1 -1
  37. package/dist/discover.js +15 -9
  38. package/dist/discover.js.map +1 -1
  39. package/dist/env.d.ts +4 -0
  40. package/dist/env.d.ts.map +1 -1
  41. package/dist/env.js +29 -9
  42. package/dist/env.js.map +1 -1
  43. package/dist/errors.d.ts +27 -0
  44. package/dist/errors.d.ts.map +1 -0
  45. package/dist/errors.js +150 -0
  46. package/dist/errors.js.map +1 -0
  47. package/dist/index.d.ts +11 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +7 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/notices.d.ts +10 -0
  52. package/dist/notices.d.ts.map +1 -0
  53. package/dist/notices.js +11 -0
  54. package/dist/notices.js.map +1 -0
  55. package/dist/ollama.d.ts.map +1 -1
  56. package/dist/ollama.js +3 -3
  57. package/dist/ollama.js.map +1 -1
  58. package/dist/openai.d.ts +1 -1
  59. package/dist/openai.d.ts.map +1 -1
  60. package/dist/promptTokens.d.ts +4 -0
  61. package/dist/promptTokens.d.ts.map +1 -0
  62. package/dist/promptTokens.js +32 -0
  63. package/dist/promptTokens.js.map +1 -0
  64. package/dist/sdkModels.d.ts.map +1 -1
  65. package/dist/sdkModels.js +4 -3
  66. package/dist/sdkModels.js.map +1 -1
  67. package/dist/types.d.ts +43 -16
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +1 -1
  70. package/dist/types.js.map +1 -1
  71. package/dist/usage.d.ts +3 -0
  72. package/dist/usage.d.ts.map +1 -1
  73. package/dist/usage.js +26 -14
  74. package/dist/usage.js.map +1 -1
  75. package/dist/warnings.js +0 -0
  76. package/dist/warnings.js.map +1 -1
  77. package/package.json +13 -9
  78. package/src/AiSdkProvider.test.ts +480 -159
  79. package/src/AiSdkProvider.ts +320 -196
  80. package/src/Mock.test.ts +29 -14
  81. package/src/Mock.ts +33 -15
  82. package/src/Pool.test.ts +43 -6
  83. package/src/Pool.ts +56 -10
  84. package/src/ProviderRegistry.test.ts +158 -9
  85. package/src/ProviderRegistry.ts +19 -6
  86. package/src/aiSdkTransport.ts +25 -6
  87. package/src/boundaries.test.ts +8 -3
  88. package/src/catalogProvider.test.ts +17 -0
  89. package/src/catalogProvider.ts +25 -10
  90. package/src/compatibleProvider.test.ts +96 -0
  91. package/src/compatibleProvider.ts +15 -6
  92. package/src/cost.test.ts +63 -0
  93. package/src/cost.ts +83 -0
  94. package/src/defaults.test.ts +1 -0
  95. package/src/discover.test.ts +48 -7
  96. package/src/discover.ts +31 -21
  97. package/src/env.test.ts +38 -23
  98. package/src/env.ts +45 -18
  99. package/src/errors.test.ts +148 -0
  100. package/src/errors.ts +207 -0
  101. package/src/index.ts +29 -7
  102. package/src/lexicon-guard.test.ts +6 -6
  103. package/src/notices.ts +22 -0
  104. package/src/ollama.test.ts +64 -0
  105. package/src/ollama.ts +6 -3
  106. package/src/openai.ts +3 -0
  107. package/src/promptTokens.ts +41 -0
  108. package/src/sdkModels.test.ts +7 -0
  109. package/src/sdkModels.ts +4 -8
  110. package/src/types.ts +106 -64
  111. package/src/usage.test.ts +15 -4
  112. package/src/usage.ts +32 -14
  113. package/src/warnings.test.ts +10 -10
  114. package/src/warnings.ts +0 -0
  115. package/dist/OpenAICompat.d.ts +0 -76
  116. package/dist/OpenAICompat.d.ts.map +0 -1
  117. package/dist/OpenAICompat.js +0 -555
  118. package/dist/OpenAICompat.js.map +0 -1
  119. package/dist/openaiStream.d.ts +0 -47
  120. package/dist/openaiStream.d.ts.map +0 -1
  121. package/dist/openaiStream.js +0 -280
  122. package/dist/openaiStream.js.map +0 -1
  123. package/dist/standardProviders.d.ts +0 -31
  124. package/dist/standardProviders.d.ts.map +0 -1
  125. package/dist/standardProviders.js +0 -518
  126. package/dist/standardProviders.js.map +0 -1
  127. package/dist/telemetry.d.ts +0 -24
  128. package/dist/telemetry.d.ts.map +0 -1
  129. package/dist/telemetry.js +0 -85
  130. package/dist/telemetry.js.map +0 -1
  131. package/src/telemetry.test.ts +0 -69
  132. package/src/telemetry.ts +0 -116
package/dist/telemetry.js DELETED
@@ -1,85 +0,0 @@
1
- // Provider telemetry — the TelemetryEvent envelope for transport failures.
2
- //
3
- // TelemetryEvent is plurnk's cross-ecosystem error envelope (canonical schema:
4
- // @plurnk/plurnk-grammar's schemas.plurnk.dev/v0/TelemetryEvent.json). It is
5
- // mirrored here STRUCTURALLY rather than imported, so the framework keeps zero
6
- // dependency on grammar (see SPEC §11). Consumers route provider events through
7
- // the same `source` + `kind` discriminator as parse/rail events.
8
- import { APICallError, RetryError } from "ai";
9
- // Build a provider source label (`provider:<vendor>`), schema-pattern-valid.
10
- export const providerSource = (vendor) => `provider:${vendor}`;
11
- // A transport failure carrying its TelemetryEvent classification. IS-an Error
12
- // (existing catchers keep working); telemetry-aware consumers call
13
- // toTelemetryEvent() and route on source+kind.
14
- export class ProviderError extends Error {
15
- source;
16
- kind;
17
- status;
18
- constructor(source, kind, message, options = {}) {
19
- super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
20
- this.name = "ProviderError";
21
- this.source = source;
22
- this.kind = kind;
23
- this.status = options.status ?? null;
24
- }
25
- toTelemetryEvent() {
26
- return { source: this.source, kind: this.kind, message: this.message, position: null };
27
- }
28
- }
29
- // An OpenAI-shaped error body carries error.type. null when the body is absent,
30
- // non-JSON (a proxy/CDN HTML page), or unshaped — none of which is a match.
31
- const wireErrorType = (body) => {
32
- try {
33
- const { error } = JSON.parse(body);
34
- return typeof error?.type === "string" ? error.type : null;
35
- }
36
- catch {
37
- return null;
38
- }
39
- };
40
- // Map a thrown transport error to a (kind, message). Conservative; the message
41
- // is factual, no guidance prose (consumer SPEC §15.1 policy).
42
- export const classifyProviderError = (err) => {
43
- if (RetryError.isInstance(err))
44
- return classifyProviderError(err.lastError);
45
- if (APICallError.isInstance(err)) {
46
- const status = err.statusCode ?? 0;
47
- const message = err.message;
48
- const body = err.responseBody ?? "";
49
- if (status === 401 || status === 403)
50
- return { kind: "unauthorized", message };
51
- if (status === 402)
52
- return { kind: "quota_exceeded", message };
53
- if (status === 429)
54
- return { kind: "rate_limit", message };
55
- if (status >= 500)
56
- return { kind: "network_failure", message };
57
- // A flagged 422 identifies a grammar-rejected exchange. It is not
58
- // transport replay policy; the engine decides whether to sample again.
59
- if (status === 422 && wireErrorType(body) === "grammar_invalid")
60
- return { kind: "grammar_invalid", message };
61
- return { kind: "invalid_response", message };
62
- }
63
- const wire = err;
64
- if (wire?.type === "grammar_invalid") {
65
- return {
66
- kind: "grammar_invalid",
67
- message: typeof wire.message === "string" ? wire.message : "grammar-invalid response",
68
- };
69
- }
70
- const e = err;
71
- const message = (e?.message ?? String(err)) || "request failed";
72
- // Timeouts / fetch-level failures are transport, not a usable response.
73
- return { kind: "network_failure", message };
74
- };
75
- // Wrap any thrown error as a ProviderError tagged with this provider's source.
76
- // Already-classified errors pass through unchanged.
77
- export const toProviderError = (err, source) => {
78
- if (err instanceof ProviderError)
79
- return err;
80
- const underlying = RetryError.isInstance(err) ? err.lastError : err;
81
- const { kind, message } = classifyProviderError(underlying);
82
- const status = APICallError.isInstance(underlying) ? underlying.statusCode ?? null : null;
83
- return new ProviderError(source, kind, message, { status, cause: err });
84
- };
85
- //# sourceMappingURL=telemetry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
@@ -1,69 +0,0 @@
1
- import test from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { APICallError } from "ai";
4
- import { ProviderError, classifyProviderError, toProviderError, providerSource } from "./telemetry.ts";
5
-
6
- const apiError = (statusCode: number, responseBody = "body") => new APICallError({
7
- message: `request failed (${statusCode})`,
8
- url: "https://example.test/v1/chat/completions",
9
- requestBodyValues: {},
10
- statusCode,
11
- responseBody,
12
- });
13
-
14
- // Schema pattern for TelemetryEvent.source (from grammar's TelemetryEvent.json).
15
- const SOURCE_PATTERN = /^[a-z]+(:[a-z][a-z0-9-]*)?$/;
16
-
17
- test("providerSource produces a schema-valid colon-namespaced source", () => {
18
- assert.equal(providerSource("openai"), "provider:openai");
19
- assert.match(providerSource("openrouter"), SOURCE_PATTERN);
20
- assert.match(providerSource("xai"), SOURCE_PATTERN); // contains no underscore — fine
21
- });
22
-
23
- test("classifyProviderError maps HTTP status to kind", () => {
24
- const k = (status: number) => classifyProviderError(apiError(status)).kind;
25
- assert.equal(k(401), "unauthorized");
26
- assert.equal(k(403), "unauthorized");
27
- assert.equal(k(402), "quota_exceeded");
28
- assert.equal(k(429), "rate_limit");
29
- assert.equal(k(500), "network_failure");
30
- assert.equal(k(503), "network_failure");
31
- assert.equal(k(400), "invalid_response");
32
- assert.equal(k(404), "invalid_response");
33
- });
34
-
35
- test("classifyProviderError: a 422 flagged grammar_invalid is distinct (#548); other 422s are invalid responses", () => {
36
- const rejected = apiError(422, JSON.stringify({ error: { type: "grammar_invalid", message: "non-conforming emission rejected: ..." } }));
37
- assert.equal(classifyProviderError(rejected).kind, "grammar_invalid");
38
- assert.equal(classifyProviderError(apiError(422, JSON.stringify({ error: { type: "invalid_request_error" } }))).kind, "invalid_response");
39
- assert.equal(classifyProviderError(apiError(422, "<html>Bad</html>")).kind, "invalid_response");
40
- });
41
-
42
- test("classifyProviderError treats non-HTTP errors as network_failure", () => {
43
- assert.equal(classifyProviderError(new TypeError("fetch failed")).kind, "network_failure");
44
- const timeout = Object.assign(new Error("timed out"), { name: "TimeoutError" });
45
- assert.equal(classifyProviderError(timeout).kind, "network_failure");
46
- });
47
-
48
- test("ProviderError.toTelemetryEvent emits the canonical envelope", () => {
49
- const e = new ProviderError("provider:openai", "rate_limit", "OpenAI 429 - slow down", { status: 429 });
50
- const ev = e.toTelemetryEvent();
51
- assert.deepEqual(ev, { source: "provider:openai", kind: "rate_limit", message: "OpenAI 429 - slow down", position: null });
52
- assert.match(ev.source, SOURCE_PATTERN);
53
- assert.ok(e instanceof Error); // still catchable as a plain Error
54
- assert.equal(e.status, 429);
55
- });
56
-
57
- test("toProviderError classifies and tags an HTTP error with the source + status", () => {
58
- const cause = apiError(401, "no key");
59
- const pe = toProviderError(cause, "provider:groq");
60
- assert.equal(pe.kind, "unauthorized");
61
- assert.equal(pe.source, "provider:groq");
62
- assert.equal(pe.status, 401);
63
- assert.equal(pe.cause, cause);
64
- });
65
-
66
- test("toProviderError passes an existing ProviderError through unchanged", () => {
67
- const original = new ProviderError("provider:xai", "rate_limit", "429");
68
- assert.equal(toProviderError(original, "provider:other"), original);
69
- });
package/src/telemetry.ts DELETED
@@ -1,116 +0,0 @@
1
- // Provider telemetry — the TelemetryEvent envelope for transport failures.
2
- //
3
- // TelemetryEvent is plurnk's cross-ecosystem error envelope (canonical schema:
4
- // @plurnk/plurnk-grammar's schemas.plurnk.dev/v0/TelemetryEvent.json). It is
5
- // mirrored here STRUCTURALLY rather than imported, so the framework keeps zero
6
- // dependency on grammar (see SPEC §11). Consumers route provider events through
7
- // the same `source` + `kind` discriminator as parse/rail events.
8
-
9
- import { APICallError, RetryError } from "ai";
10
-
11
- // Required by the schema: source (producer id) + kind (discriminator). message
12
- // and position are optional. A transport failure isn't localizable, so it carries
13
- // a null position; a `grammar_unenforced` event (#24) carries the divergence
14
- // code-point offset into the model's emission, so the consumer can render a
15
- // snippet around it — same recovery affordance the consumer already gives DSL
16
- // parse errors.
17
- export type TelemetryEvent = {
18
- source: string; // e.g. "provider:openai" — schema pattern ^[a-z]+(:[a-z][a-z0-9-]*)?$
19
- kind: string; // open vocabulary; providers mint from ProviderTelemetryKind
20
- message?: string | null;
21
- position?: number | null;
22
- };
23
-
24
- // The kinds plurnk-service routes provider failures on.
25
- export type ProviderTelemetryKind =
26
- | "rate_limit"
27
- | "network_failure"
28
- | "model_refused"
29
- | "invalid_response"
30
- | "unauthorized"
31
- | "quota_exceeded"
32
- // A 422 whose error.type is "grammar_invalid" (#548): the backend served one
33
- // generation and rejected it as non-conforming. This identifies the failure;
34
- // whether to start another model exchange belongs to the engine.
35
- | "grammar_invalid"
36
- // Output did not conform to the GBNF. ALWAYS an observation, never a throw:
37
- // a completed exchange returns its bytes with this event on
38
- // response.telemetry (message + divergence position), whether the grammar
39
- // was transported or withheld (filter mode). Discard/retry/escalate is
40
- // consumer policy (#24, SPEC §13).
41
- | "grammar_unenforced";
42
-
43
- // Build a provider source label (`provider:<vendor>`), schema-pattern-valid.
44
- export const providerSource = (vendor: string): string => `provider:${vendor}`;
45
-
46
- // A transport failure carrying its TelemetryEvent classification. IS-an Error
47
- // (existing catchers keep working); telemetry-aware consumers call
48
- // toTelemetryEvent() and route on source+kind.
49
- export class ProviderError extends Error {
50
- readonly source: string;
51
- readonly kind: ProviderTelemetryKind;
52
- readonly status: number | null;
53
-
54
- constructor(source: string, kind: ProviderTelemetryKind, message: string, options: { status?: number | null; cause?: unknown } = {}) {
55
- super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
56
- this.name = "ProviderError";
57
- this.source = source;
58
- this.kind = kind;
59
- this.status = options.status ?? null;
60
- }
61
-
62
- toTelemetryEvent(): TelemetryEvent {
63
- return { source: this.source, kind: this.kind, message: this.message, position: null };
64
- }
65
- }
66
-
67
- // An OpenAI-shaped error body carries error.type. null when the body is absent,
68
- // non-JSON (a proxy/CDN HTML page), or unshaped — none of which is a match.
69
- const wireErrorType = (body: string): string | null => {
70
- try {
71
- const { error } = JSON.parse(body) as { error?: { type?: unknown } };
72
- return typeof error?.type === "string" ? error.type : null;
73
- } catch {
74
- return null;
75
- }
76
- };
77
-
78
- // Map a thrown transport error to a (kind, message). Conservative; the message
79
- // is factual, no guidance prose (consumer SPEC §15.1 policy).
80
- export const classifyProviderError = (err: unknown): { kind: ProviderTelemetryKind; message: string } => {
81
- if (RetryError.isInstance(err)) return classifyProviderError(err.lastError);
82
- if (APICallError.isInstance(err)) {
83
- const status = err.statusCode ?? 0;
84
- const message = err.message;
85
- const body = err.responseBody ?? "";
86
- if (status === 401 || status === 403) return { kind: "unauthorized", message };
87
- if (status === 402) return { kind: "quota_exceeded", message };
88
- if (status === 429) return { kind: "rate_limit", message };
89
- if (status >= 500) return { kind: "network_failure", message };
90
- // A flagged 422 identifies a grammar-rejected exchange. It is not
91
- // transport replay policy; the engine decides whether to sample again.
92
- if (status === 422 && wireErrorType(body) === "grammar_invalid") return { kind: "grammar_invalid", message };
93
- return { kind: "invalid_response", message };
94
- }
95
- const wire = err as { message?: unknown; type?: unknown; status?: unknown };
96
- if (wire?.type === "grammar_invalid") {
97
- return {
98
- kind: "grammar_invalid",
99
- message: typeof wire.message === "string" ? wire.message : "grammar-invalid response",
100
- };
101
- }
102
- const e = err as { name?: string; message?: string };
103
- const message = (e?.message ?? String(err)) || "request failed";
104
- // Timeouts / fetch-level failures are transport, not a usable response.
105
- return { kind: "network_failure", message };
106
- };
107
-
108
- // Wrap any thrown error as a ProviderError tagged with this provider's source.
109
- // Already-classified errors pass through unchanged.
110
- export const toProviderError = (err: unknown, source: string): ProviderError => {
111
- if (err instanceof ProviderError) return err;
112
- const underlying = RetryError.isInstance(err) ? err.lastError : err;
113
- const { kind, message } = classifyProviderError(underlying);
114
- const status = APICallError.isInstance(underlying) ? underlying.statusCode ?? null : null;
115
- return new ProviderError(source, kind, message, { status, cause: err });
116
- };