@graphorin/provider 0.6.1 → 0.7.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 (74) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/adapters/llamacpp-server.d.ts +1 -1
  4. package/dist/adapters/llamacpp-server.js.map +1 -1
  5. package/dist/adapters/ollama.d.ts.map +1 -1
  6. package/dist/adapters/ollama.js +19 -6
  7. package/dist/adapters/ollama.js.map +1 -1
  8. package/dist/adapters/vercel.d.ts +1 -1
  9. package/dist/adapters/vercel.d.ts.map +1 -1
  10. package/dist/adapters/vercel.js +111 -33
  11. package/dist/adapters/vercel.js.map +1 -1
  12. package/dist/errors/errors.d.ts +1 -1
  13. package/dist/errors/errors.js +1 -1
  14. package/dist/errors/errors.js.map +1 -1
  15. package/dist/index.js +0 -6
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal/http.js +111 -7
  18. package/dist/internal/http.js.map +1 -1
  19. package/dist/internal/openai-shaped.js +21 -4
  20. package/dist/internal/openai-shaped.js.map +1 -1
  21. package/dist/middleware/with-fallback.js +1 -1
  22. package/dist/middleware/with-rate-limit.d.ts +21 -8
  23. package/dist/middleware/with-rate-limit.d.ts.map +1 -1
  24. package/dist/middleware/with-rate-limit.js +65 -12
  25. package/dist/middleware/with-rate-limit.js.map +1 -1
  26. package/dist/middleware/with-retry.js +1 -1
  27. package/dist/package.js +1 -1
  28. package/dist/package.js.map +1 -1
  29. package/package.json +18 -16
  30. package/src/adapters/index.ts +14 -0
  31. package/src/adapters/llamacpp-server.ts +102 -0
  32. package/src/adapters/ollama.ts +382 -0
  33. package/src/adapters/openai-compatible.ts +95 -0
  34. package/src/adapters/vercel-messages.ts +308 -0
  35. package/src/adapters/vercel.ts +706 -0
  36. package/src/counters/anthropic-wire.ts +199 -0
  37. package/src/counters/anthropic.ts +114 -0
  38. package/src/counters/bedrock.ts +46 -0
  39. package/src/counters/dispatcher.ts +127 -0
  40. package/src/counters/global.ts +39 -0
  41. package/src/counters/google.ts +46 -0
  42. package/src/counters/heuristic.ts +107 -0
  43. package/src/counters/index.ts +35 -0
  44. package/src/counters/js-tiktoken.ts +135 -0
  45. package/src/counters/serialize.ts +85 -0
  46. package/src/errors/errors.ts +316 -0
  47. package/src/errors/index.ts +20 -0
  48. package/src/index.ts +42 -0
  49. package/src/internal/abort.ts +30 -0
  50. package/src/internal/http.ts +388 -0
  51. package/src/internal/openai-shaped.ts +555 -0
  52. package/src/internal/sse.ts +112 -0
  53. package/src/internal/url-utils.ts +20 -0
  54. package/src/middleware/compose.ts +213 -0
  55. package/src/middleware/index.ts +37 -0
  56. package/src/middleware/production-hook.ts +47 -0
  57. package/src/middleware/with-cost-limit.ts +131 -0
  58. package/src/middleware/with-cost-tracking.ts +216 -0
  59. package/src/middleware/with-fallback.ts +157 -0
  60. package/src/middleware/with-rate-limit.ts +306 -0
  61. package/src/middleware/with-redaction.ts +671 -0
  62. package/src/middleware/with-retry.ts +274 -0
  63. package/src/middleware/with-tracing.ts +117 -0
  64. package/src/model-tier/classify.ts +125 -0
  65. package/src/model-tier/index.ts +11 -0
  66. package/src/provider.ts +121 -0
  67. package/src/reasoning/apply-policy.ts +89 -0
  68. package/src/reasoning/classify-contract.ts +120 -0
  69. package/src/reasoning/index.ts +21 -0
  70. package/src/reasoning/retention.ts +64 -0
  71. package/src/tool-examples.ts +54 -0
  72. package/src/trust/classify-local-provider.ts +254 -0
  73. package/src/trust/index.ts +14 -0
  74. package/dist/adapters/index.js +0 -6
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Token-counter dispatcher and per-vendor strategies.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export {
8
+ AnthropicAPICounter,
9
+ type AnthropicAPICounterOptions,
10
+ } from './anthropic.js';
11
+ export {
12
+ BedrockAPICounter,
13
+ type BedrockAPICounterOptions,
14
+ } from './bedrock.js';
15
+ export {
16
+ type CreateDefaultCounterOptions,
17
+ createDefaultCounter,
18
+ } from './dispatcher.js';
19
+ export {
20
+ __resetGlobalTokenCounter,
21
+ getGlobalTokenCounter,
22
+ setGlobalTokenCounter,
23
+ } from './global.js';
24
+ export { GoogleAPICounter, type GoogleAPICounterOptions } from './google.js';
25
+ export { HeuristicCounter, type HeuristicCounterOptions } from './heuristic.js';
26
+ export {
27
+ __resetTiktokenCache,
28
+ JsTiktokenCounter,
29
+ type JsTiktokenCounterOptions,
30
+ } from './js-tiktoken.js';
31
+ export {
32
+ type SerializedMessage,
33
+ serialiseMessageForCount,
34
+ serializedToString,
35
+ } from './serialize.js';
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `JsTiktokenCounter` - default counter for OpenAI / OpenAI-compatible
3
+ * models. The package depends on the `js-tiktoken` peer; when the peer
4
+ * is missing, instantiation throws at first call rather than silently
5
+ * downgrading to a heuristic.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { Message, TokenCounter } from '@graphorin/core';
11
+
12
+ import { serialiseMessageForCount, serializedToString } from './serialize.js';
13
+
14
+ /**
15
+ * Loose shape of a `js-tiktoken` encoding. Matches the public
16
+ * `Tiktoken` instance returned by `getEncoding(...)` in the upstream
17
+ * package (we only use `encode` / `name`).
18
+ *
19
+ * @internal
20
+ */
21
+ interface TiktokenEncoding {
22
+ readonly name?: string;
23
+ encode(text: string): { length: number };
24
+ }
25
+
26
+ interface TiktokenModule {
27
+ getEncoding(name: string): TiktokenEncoding;
28
+ encodingForModel?: (model: string) => TiktokenEncoding;
29
+ }
30
+
31
+ /**
32
+ * Options for {@link JsTiktokenCounter}.
33
+ *
34
+ * @stable
35
+ */
36
+ export interface JsTiktokenCounterOptions {
37
+ /** Encoding name (`'cl100k_base'`, `'o200k_base'`, …). Default `'cl100k_base'`. */
38
+ readonly encoding?: string;
39
+ /** Optional model id used by `encodingForModel`. Falls back to the explicit `encoding`. */
40
+ readonly modelId?: string;
41
+ /**
42
+ * Override the dynamically-loaded module. Used by tests to inject a
43
+ * stub without taking the real `js-tiktoken` peer dependency.
44
+ */
45
+ readonly moduleOverride?: TiktokenModule;
46
+ /** Identifier carried on the produced counter. */
47
+ readonly id?: string;
48
+ }
49
+
50
+ let cachedModule: TiktokenModule | null = null;
51
+
52
+ /**
53
+ * Counter that delegates to the `js-tiktoken` package. Caches the
54
+ * dynamically-loaded module per process; tests use `moduleOverride`
55
+ * to supply a fixture-shaped substitute.
56
+ *
57
+ * @stable
58
+ */
59
+ export class JsTiktokenCounter implements TokenCounter {
60
+ readonly id: string;
61
+ readonly version: string;
62
+ readonly #encoding: string;
63
+ readonly #modelId?: string;
64
+ readonly #moduleOverride?: TiktokenModule;
65
+ #encoder: TiktokenEncoding | null = null;
66
+
67
+ constructor(options: JsTiktokenCounterOptions = {}) {
68
+ this.#encoding = options.encoding ?? 'cl100k_base';
69
+ if (options.modelId !== undefined) this.#modelId = options.modelId;
70
+ if (options.moduleOverride !== undefined) this.#moduleOverride = options.moduleOverride;
71
+ this.id = options.id ?? `js-tiktoken@${this.#encoding}`;
72
+ this.version = `js-tiktoken-${this.#encoding}-v1`;
73
+ }
74
+
75
+ async count(messages: ReadonlyArray<Message>): Promise<number> {
76
+ const enc = await this.#getEncoder();
77
+ let total = 0;
78
+ for (const msg of messages) {
79
+ const serialised = serialiseMessageForCount(msg);
80
+ total += enc.encode(serializedToString(serialised)).length;
81
+ }
82
+ return total;
83
+ }
84
+
85
+ async countText(text: string): Promise<number> {
86
+ if (text.length === 0) return 0;
87
+ const enc = await this.#getEncoder();
88
+ return enc.encode(text).length;
89
+ }
90
+
91
+ async #getEncoder(): Promise<TiktokenEncoding> {
92
+ if (this.#encoder !== null) return this.#encoder;
93
+ const mod = this.#moduleOverride ?? (await loadTiktokenModule());
94
+ if (this.#modelId !== undefined && mod.encodingForModel !== undefined) {
95
+ try {
96
+ this.#encoder = mod.encodingForModel(this.#modelId);
97
+ return this.#encoder;
98
+ } catch {
99
+ // Fall through to the explicit encoding.
100
+ }
101
+ }
102
+ this.#encoder = mod.getEncoding(this.#encoding);
103
+ return this.#encoder;
104
+ }
105
+ }
106
+
107
+ async function loadTiktokenModule(): Promise<TiktokenModule> {
108
+ if (cachedModule !== null) return cachedModule;
109
+ let mod: TiktokenModule;
110
+ try {
111
+ mod = (await import('js-tiktoken')) as TiktokenModule;
112
+ } catch (cause) {
113
+ throw new Error(
114
+ "[graphorin/provider] JsTiktokenCounter requires the 'js-tiktoken' peer dependency. " +
115
+ 'Install it with `pnpm add js-tiktoken` or pass moduleOverride for tests.',
116
+ { cause },
117
+ );
118
+ }
119
+ if (typeof mod.getEncoding !== 'function') {
120
+ throw new Error(
121
+ '[graphorin/provider] JsTiktokenCounter: installed js-tiktoken does not expose getEncoding().',
122
+ );
123
+ }
124
+ cachedModule = mod;
125
+ return cachedModule;
126
+ }
127
+
128
+ /**
129
+ * Test-only hook that resets the cached `js-tiktoken` module loader.
130
+ *
131
+ * @internal
132
+ */
133
+ export function __resetTiktokenCache(): void {
134
+ cachedModule = null;
135
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Canonical text-only serialization of a Graphorin `Message`. Used by
3
+ * counters that lack a vendor-native API (`JsTiktokenCounter`,
4
+ * `HeuristicCounter`).
5
+ *
6
+ * @internal
7
+ */
8
+
9
+ import type { Message, MessageContent } from '@graphorin/core';
10
+
11
+ /**
12
+ * Plain-text projection of a {@link Message}. The shape is what the
13
+ * counter actually feeds the tokenizer.
14
+ *
15
+ * @internal
16
+ */
17
+ export interface SerializedMessage {
18
+ readonly role: string;
19
+ readonly text: string;
20
+ readonly toolCalls: ReadonlyArray<{ readonly name: string; readonly args: string }>;
21
+ }
22
+
23
+ /**
24
+ * Project a `Message` into a flat string suitable for tokenizers that
25
+ * do not understand multimodal content. Image / audio / file parts
26
+ * are replaced with the canonical placeholder `[image]` / `[audio]` /
27
+ * `[file]` so byte counts stay deterministic.
28
+ *
29
+ * @internal
30
+ */
31
+ export function serialiseMessageForCount(msg: Message): SerializedMessage {
32
+ const role = msg.role;
33
+ const text = serialiseContent(msg.content);
34
+ const toolCalls =
35
+ (msg as { toolCalls?: ReadonlyArray<{ toolName: string; args: unknown }> }).toolCalls?.map(
36
+ (tc) => ({
37
+ name: tc.toolName,
38
+ args: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args),
39
+ }),
40
+ ) ?? [];
41
+ return { role, text, toolCalls };
42
+ }
43
+
44
+ function serialiseContent(content: string | ReadonlyArray<MessageContent>): string {
45
+ if (typeof content === 'string') return content;
46
+ const buffer: string[] = [];
47
+ for (const part of content) {
48
+ switch (part.type) {
49
+ case 'text':
50
+ buffer.push(part.text);
51
+ break;
52
+ case 'image':
53
+ buffer.push('[image]');
54
+ break;
55
+ case 'audio':
56
+ buffer.push('[audio]');
57
+ break;
58
+ case 'file':
59
+ buffer.push(`[file:${part.mimeType}]`);
60
+ break;
61
+ case 'reasoning':
62
+ buffer.push(part.text);
63
+ break;
64
+ default:
65
+ // Unknown content variants are silently skipped to keep the
66
+ // counter forward-compatible.
67
+ break;
68
+ }
69
+ }
70
+ return buffer.join('\n');
71
+ }
72
+
73
+ /**
74
+ * Render a `SerializedMessage` as a single string with role prefix.
75
+ * Useful for naive estimators that count by character length.
76
+ *
77
+ * @internal
78
+ */
79
+ export function serializedToString(msg: SerializedMessage): string {
80
+ const parts = [`[${msg.role}]`, msg.text];
81
+ for (const tc of msg.toolCalls) {
82
+ parts.push(`[tool-call:${tc.name}] ${tc.args}`);
83
+ }
84
+ return parts.join('\n');
85
+ }
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Typed error hierarchy for `@graphorin/provider`. Every error class
3
+ * carries a stable `kind` discriminant (lowercase, kebab-case) plus an
4
+ * optional `hint` string pointing the user at the recommended fix.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { ProviderErrorKind } from '@graphorin/core';
10
+
11
+ /**
12
+ * Base class for every error thrown by `@graphorin/provider`. Consumers
13
+ * narrow on the discriminant `kind` rather than `instanceof` - both
14
+ * work, but discriminants are subclass-friendly.
15
+ *
16
+ * @stable
17
+ */
18
+ export class GraphorinProviderError extends Error {
19
+ override readonly name: string = 'GraphorinProviderError';
20
+ /** Stable discriminant - `'middleware-ordering'`, `'rate-limit-exceeded'`, … */
21
+ readonly kind: string;
22
+ /** Optional remediation hint shown alongside the message. */
23
+ readonly hint?: string;
24
+
25
+ constructor(kind: string, message: string, options?: { cause?: unknown; hint?: string }) {
26
+ super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
27
+ this.kind = kind;
28
+ if (options?.hint !== undefined) {
29
+ this.hint = options.hint;
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * The middleware composer detected an out-of-order middleware. The
36
+ * `offendingPair` field carries the `[outer, inner]` middleware names
37
+ * that violated the canonical order so consumers can surface a precise
38
+ * fix in IDE diagnostics.
39
+ *
40
+ * @stable
41
+ */
42
+ export class MiddlewareOrderingError extends GraphorinProviderError {
43
+ override readonly name = 'MiddlewareOrderingError';
44
+ readonly offendingPair: readonly [string, string];
45
+ readonly canonicalOrder: readonly string[];
46
+
47
+ constructor(args: {
48
+ offendingPair: readonly [string, string];
49
+ canonicalOrder: readonly string[];
50
+ message?: string;
51
+ }) {
52
+ super(
53
+ 'middleware-ordering',
54
+ args.message ??
55
+ `Middleware order violation: '${args.offendingPair[0]}' must not wrap '${args.offendingPair[1]}'. ` +
56
+ `Canonical order (outer → inner): ${args.canonicalOrder.join(' → ')}.`,
57
+ {
58
+ hint:
59
+ 'Reorder the middleware array passed to composeProviderMiddleware([...]) to match the canonical order; ' +
60
+ 'see @graphorin/provider/middleware for documentation.',
61
+ },
62
+ );
63
+ this.offendingPair = args.offendingPair;
64
+ this.canonicalOrder = args.canonicalOrder;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * `withRedaction` was missing at production startup. Thrown by the
70
+ * production hook applied to every composed provider when
71
+ * `process.env.NODE_ENV === 'production'`.
72
+ *
73
+ * @stable
74
+ */
75
+ export class MissingProductionMiddlewareError extends GraphorinProviderError {
76
+ override readonly name = 'MissingProductionMiddlewareError';
77
+ readonly missing: string;
78
+
79
+ constructor(missing: string) {
80
+ super(
81
+ 'missing-production-middleware',
82
+ `Production startup hook: required middleware '${missing}' is missing from the provider chain.`,
83
+ {
84
+ hint:
85
+ `Compose '${missing}' as the innermost layer (closest to the underlying provider) before deploying. ` +
86
+ 'See @graphorin/provider/middleware for the canonical composition order.',
87
+ },
88
+ );
89
+ this.missing = missing;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * `withCostLimit` exceeded its configured ceiling. Carries the
95
+ * triggering scope (`session`, `agent`, `run`, …) so the caller can
96
+ * decide how to surface the breach.
97
+ *
98
+ * @stable
99
+ */
100
+ export class CostBudgetExceededError extends GraphorinProviderError {
101
+ override readonly name = 'CostBudgetExceededError';
102
+ readonly scope: string;
103
+ readonly limit: number;
104
+ readonly observed: number;
105
+
106
+ constructor(args: { scope: string; limit: number; observed: number; currency?: string }) {
107
+ super(
108
+ 'cost-budget-exceeded',
109
+ `Cost budget exceeded for '${args.scope}': observed ${args.observed.toFixed(4)} ${args.currency ?? 'USD'} ` +
110
+ `> limit ${args.limit.toFixed(4)} ${args.currency ?? 'USD'}.`,
111
+ {
112
+ hint: 'Raise the limit in withCostLimit({...}), tighten the prompt, or switch to a cheaper model tier.',
113
+ },
114
+ );
115
+ this.scope = args.scope;
116
+ this.limit = args.limit;
117
+ this.observed = args.observed;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * `withRateLimit` exhausted its bucket and the caller did not opt
123
+ * into `'queue'` mode.
124
+ *
125
+ * @stable
126
+ */
127
+ export class RateLimitExceededError extends GraphorinProviderError {
128
+ override readonly name = 'RateLimitExceededError';
129
+ readonly retryAfterMs: number;
130
+
131
+ constructor(retryAfterMs: number) {
132
+ super('rate-limit-exceeded', `Rate limit exceeded; retry after ${retryAfterMs}ms.`, {
133
+ hint: 'Slow down the request rate, raise the bucket size in withRateLimit({...}), or switch to mode: "queue".',
134
+ });
135
+ this.retryAfterMs = retryAfterMs;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * `withRedaction` detected a hit AND the policy is configured for
141
+ * `failClosed: true`. The matched value is **never** part of the
142
+ * message - only the pattern name and field path.
143
+ *
144
+ * @stable
145
+ */
146
+ export class PromptRedactionError extends GraphorinProviderError {
147
+ override readonly name = 'PromptRedactionError';
148
+ readonly patternName: string;
149
+ readonly fieldPath: string;
150
+ readonly role?: string;
151
+
152
+ constructor(args: { patternName: string; fieldPath: string; role?: string }) {
153
+ super(
154
+ 'prompt-redaction-fail-closed',
155
+ `Outbound prompt-redaction policy is fail-closed and matched pattern '${args.patternName}' ` +
156
+ `at '${args.fieldPath}'${args.role ? ` (role: ${args.role})` : ''}.`,
157
+ {
158
+ hint: 'Sanitize the prompt before calling the provider, or disable failClosed in withRedaction({...}) for this environment.',
159
+ },
160
+ );
161
+ this.patternName = args.patternName;
162
+ this.fieldPath = args.fieldPath;
163
+ if (args.role !== undefined) this.role = args.role;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Raised when a `baseUrl`-driven local adapter is configured against
169
+ * a public host over plaintext HTTP. Adapters refuse to start unless
170
+ * `allowInsecureTransport: true` is supplied (and the override is
171
+ * audit-logged).
172
+ *
173
+ * @stable
174
+ */
175
+ export class LocalProviderInsecureTransportError extends GraphorinProviderError {
176
+ override readonly name = 'LocalProviderInsecureTransportError';
177
+ readonly baseUrl: string;
178
+
179
+ constructor(baseUrl: string) {
180
+ super(
181
+ 'local-provider-insecure-transport',
182
+ `Refusing to start: the provider baseUrl '${baseUrl}' is public AND plaintext (http://). ` +
183
+ `Switch to https:// or set allowInsecureTransport: true to acknowledge the risk.`,
184
+ {
185
+ hint:
186
+ 'Public local-LLM endpoints must be served over TLS. Acceptable alternatives: terminate TLS in front of the model server, ' +
187
+ 'tunnel through SSH / WireGuard / Tailscale, or pass allowInsecureTransport: true (audit-logged).',
188
+ },
189
+ );
190
+ this.baseUrl = baseUrl;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Legacy alias preserved for one minor; prefer
196
+ * {@link LocalProviderInsecureTransportError}. Removed in v0.2.
197
+ *
198
+ * @deprecated Use {@link LocalProviderInsecureTransportError} instead.
199
+ */
200
+ export const OllamaInsecureTransportError = LocalProviderInsecureTransportError;
201
+ export type OllamaInsecureTransportError = LocalProviderInsecureTransportError;
202
+
203
+ /**
204
+ * Body phrasings used by the major providers when the request blew the
205
+ * model's context window. Sniffed by {@link classifyHttpStatus} so a
206
+ * 400 that is really a context overflow surfaces as `'context-length'`
207
+ * (the kind the compaction / emergency tiers key on), not a generic
208
+ * `'invalid-request'`.
209
+ */
210
+ const CONTEXT_LENGTH_BODY_RE =
211
+ /context[_ -]?length|context window|prompt is too long|maximum context|too many tokens|exceeds? the (?:model'?s? )?max/i;
212
+
213
+ /**
214
+ * Map an HTTP status (plus optional error-body text) onto the
215
+ * canonical `ProviderErrorKind`. One
216
+ * shared table so `withRetry` / `withFallback` and consumers switching
217
+ * on the documented kinds see consistent values from every HTTP
218
+ * adapter:
219
+ *
220
+ * - `429` → `'rate-limit'`
221
+ * - `401` / `403` → `'unauthorized'`
222
+ * - `400` / `404` / `422` → `'invalid-request'` (or
223
+ * `'context-length'` when the body says so)
224
+ * - `503` / `529` → `'capacity'` (529 is Anthropic's overloaded code)
225
+ * - other `5xx` and `0` (network failure) → `'transient'`
226
+ *
227
+ * @stable
228
+ */
229
+ export function classifyHttpStatus(status: number, bodyText?: string): ProviderErrorKind {
230
+ if (status === 429) return 'rate-limit';
231
+ if (status === 401 || status === 403) return 'unauthorized';
232
+ if (status === 400 || status === 404 || status === 422) {
233
+ if (bodyText !== undefined && CONTEXT_LENGTH_BODY_RE.test(bodyText)) return 'context-length';
234
+ return 'invalid-request';
235
+ }
236
+ if (status === 503 || status === 529) return 'capacity';
237
+ if (status >= 500 || status === 0) return 'transient';
238
+ return 'unknown';
239
+ }
240
+
241
+ /**
242
+ * Wrapped HTTP error returned by an adapter. Carries the original
243
+ * status code so middleware (`withRetry`, `withFallback`) can decide
244
+ * whether the error is retryable.
245
+ *
246
+ * @stable
247
+ */
248
+ export class ProviderHttpError extends GraphorinProviderError {
249
+ override readonly name = 'ProviderHttpError';
250
+ readonly status: number;
251
+ readonly providerName: string;
252
+ /**
253
+ * The canonical `ProviderErrorKind` mapped from the HTTP status via
254
+ * {@link classifyHttpStatus} (the `kind` field keeps its stable
255
+ * `'provider-http'` discriminant). Middleware predicates consult
256
+ * this so a 429 fails over / retries as a rate limit.
257
+ */
258
+ readonly errorKind: ProviderErrorKind;
259
+ /**
260
+ * Backoff-relevant response headers captured from the failed
261
+ * response (`retry-after`, `x-ratelimit-*`), lowercased.
262
+ * `withRetry`'s Retry-After hint reader consumes them.
263
+ */
264
+ readonly headers?: Readonly<Record<string, string>>;
265
+
266
+ constructor(args: {
267
+ providerName: string;
268
+ status: number;
269
+ message: string;
270
+ cause?: unknown;
271
+ errorKind?: ProviderErrorKind;
272
+ headers?: Readonly<Record<string, string>>;
273
+ }) {
274
+ super('provider-http', `[${args.providerName}] HTTP ${args.status}: ${args.message}`, {
275
+ ...(args.cause !== undefined ? { cause: args.cause } : {}),
276
+ });
277
+ this.providerName = args.providerName;
278
+ this.status = args.status;
279
+ this.errorKind = args.errorKind ?? classifyHttpStatus(args.status, args.message);
280
+ if (args.headers !== undefined) this.headers = args.headers;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * The streaming response did not match the documented wire format
286
+ * (e.g. malformed SSE chunk, truncated JSON).
287
+ *
288
+ * @stable
289
+ */
290
+ export class ProviderStreamParseError extends GraphorinProviderError {
291
+ override readonly name = 'ProviderStreamParseError';
292
+ readonly providerName: string;
293
+
294
+ constructor(providerName: string, message: string, cause?: unknown) {
295
+ super('provider-stream-parse', `[${providerName}] ${message}`, {
296
+ ...(cause !== undefined ? { cause } : {}),
297
+ });
298
+ this.providerName = providerName;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * The classifier dispatcher was given a non-Provider value. Programming
304
+ * error; fail-fast at the boundary.
305
+ *
306
+ * @stable
307
+ */
308
+ export class InvalidProviderError extends GraphorinProviderError {
309
+ override readonly name = 'InvalidProviderError';
310
+
311
+ constructor(message: string) {
312
+ super('invalid-provider', message, {
313
+ hint: 'Pass the value returned by createProvider(...) (or any adapter factory) instead of a raw object.',
314
+ });
315
+ }
316
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Public error surface for `@graphorin/provider`.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export {
8
+ CostBudgetExceededError,
9
+ classifyHttpStatus,
10
+ GraphorinProviderError,
11
+ InvalidProviderError,
12
+ LocalProviderInsecureTransportError,
13
+ MiddlewareOrderingError,
14
+ MissingProductionMiddlewareError,
15
+ OllamaInsecureTransportError,
16
+ PromptRedactionError,
17
+ ProviderHttpError,
18
+ ProviderStreamParseError,
19
+ RateLimitExceededError,
20
+ } from './errors.js';
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @graphorin/provider - vendor-neutral LLM provider layer for the
3
+ * Graphorin framework.
4
+ *
5
+ * The package owns:
6
+ *
7
+ * - The `createProvider(...)` factory for wrapping adapter modules in
8
+ * a stable `Provider` shape.
9
+ * - Four adapters out of the box: the Vercel AI SDK adapter for the
10
+ * default cloud path, plus three local-LLM adapters (Ollama HTTP,
11
+ * llama-server HTTP, and a generic OpenAI-compatible adapter for
12
+ * LMStudio / LocalAI / vLLM / Together-style endpoints). All three
13
+ * `baseUrl`-driven adapters share the same {@link LocalProviderTrust}
14
+ * classifier.
15
+ * - The canonical-order middleware composer
16
+ * ({@link composeProviderMiddleware}) and the seven built-in
17
+ * middlewares (`withTracing`, `withRetry`, `withRateLimit`,
18
+ * `withCostLimit`, `withCostTracking`, `withFallback`,
19
+ * `withRedaction`).
20
+ * - The pluggable `TokenCounter` dispatcher (`createDefaultCounter`)
21
+ * plus the per-vendor strategies and a heuristic fallback.
22
+ * - The per-provider model-tier auto-classifier
23
+ * ({@link classifyModelTier}) that maps a `Provider`'s `modelId` to
24
+ * one of `'fast' | 'balanced' | 'smart'`.
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+
29
+ /** Canonical version constant, derived from `package.json` at build time. */
30
+ import pkg from '../package.json' with { type: 'json' };
31
+
32
+ export const VERSION: string = pkg.version;
33
+
34
+ export * from './adapters/index.js';
35
+ export * from './counters/index.js';
36
+ export * from './errors/index.js';
37
+ export * from './middleware/index.js';
38
+ export * from './model-tier/index.js';
39
+ export * from './provider.js';
40
+ export * from './reasoning/index.js';
41
+ export { foldToolExamples } from './tool-examples.js';
42
+ export * from './trust/index.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Abort detection shared by the retry / fallback predicates (PS-2).
3
+ *
4
+ * A network failure surfaces as `ProviderHttpError{ status: 0, cause }`,
5
+ * and a user/agent abort surfaces the same way - the distinguishing
6
+ * signal is an `AbortError` (DOMException) somewhere on the `cause`
7
+ * chain. Network errors should be retried and failed over; an abort must
8
+ * not be, even though the retry/fallback loops also short-circuit on
9
+ * `req.signal?.aborted` independently.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ /**
15
+ * True when `err` - or any error on its `cause` chain - is an abort
16
+ * (`name === 'AbortError'` or Node's `code === 'ABORT_ERR'`). Walks at
17
+ * most a few links so a cyclic / pathological cause chain cannot loop.
18
+ *
19
+ * @internal
20
+ */
21
+ export function isAbortError(err: unknown): boolean {
22
+ let cursor: unknown = err;
23
+ for (let depth = 0; depth < 8 && cursor !== null && typeof cursor === 'object'; depth++) {
24
+ const e = cursor as { name?: unknown; code?: unknown; cause?: unknown };
25
+ if (e.name === 'AbortError' || e.code === 'ABORT_ERR') return true;
26
+ if (e.cause === cursor) break;
27
+ cursor = e.cause;
28
+ }
29
+ return false;
30
+ }