@graphorin/provider 0.5.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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/llamacpp-server.d.ts +69 -0
- package/dist/adapters/llamacpp-server.d.ts.map +1 -0
- package/dist/adapters/llamacpp-server.js +51 -0
- package/dist/adapters/llamacpp-server.js.map +1 -0
- package/dist/adapters/ollama.d.ts +43 -0
- package/dist/adapters/ollama.d.ts.map +1 -0
- package/dist/adapters/ollama.js +246 -0
- package/dist/adapters/ollama.js.map +1 -0
- package/dist/adapters/openai-compatible.d.ts +48 -0
- package/dist/adapters/openai-compatible.d.ts.map +1 -0
- package/dist/adapters/openai-compatible.js +28 -0
- package/dist/adapters/openai-compatible.js.map +1 -0
- package/dist/adapters/vercel.d.ts +135 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +306 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/counters/anthropic.d.ts +36 -0
- package/dist/counters/anthropic.d.ts.map +1 -0
- package/dist/counters/anthropic.js +63 -0
- package/dist/counters/anthropic.js.map +1 -0
- package/dist/counters/bedrock.d.ts +27 -0
- package/dist/counters/bedrock.d.ts.map +1 -0
- package/dist/counters/bedrock.js +29 -0
- package/dist/counters/bedrock.js.map +1 -0
- package/dist/counters/dispatcher.d.ts +34 -0
- package/dist/counters/dispatcher.d.ts.map +1 -0
- package/dist/counters/dispatcher.js +78 -0
- package/dist/counters/dispatcher.js.map +1 -0
- package/dist/counters/global.d.ts +26 -0
- package/dist/counters/global.d.ts.map +1 -0
- package/dist/counters/global.js +31 -0
- package/dist/counters/global.js.map +1 -0
- package/dist/counters/google.d.ts +27 -0
- package/dist/counters/google.d.ts.map +1 -0
- package/dist/counters/google.js +29 -0
- package/dist/counters/google.js.map +1 -0
- package/dist/counters/heuristic.d.ts +37 -0
- package/dist/counters/heuristic.d.ts.map +1 -0
- package/dist/counters/heuristic.js +64 -0
- package/dist/counters/heuristic.js.map +1 -0
- package/dist/counters/index.d.ts +9 -0
- package/dist/counters/index.js +10 -0
- package/dist/counters/js-tiktoken.d.ts +63 -0
- package/dist/counters/js-tiktoken.d.ts.map +1 -0
- package/dist/counters/js-tiktoken.js +73 -0
- package/dist/counters/js-tiktoken.js.map +1 -0
- package/dist/counters/serialize.d.ts +37 -0
- package/dist/counters/serialize.d.ts.map +1 -0
- package/dist/counters/serialize.js +57 -0
- package/dist/counters/serialize.js.map +1 -0
- package/dist/errors/errors.d.ts +167 -0
- package/dist/errors/errors.d.ts.map +1 -0
- package/dist/errors/errors.js +182 -0
- package/dist/errors/errors.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/abort.js +34 -0
- package/dist/internal/abort.js.map +1 -0
- package/dist/internal/http.js +163 -0
- package/dist/internal/http.js.map +1 -0
- package/dist/internal/openai-shaped.js +308 -0
- package/dist/internal/openai-shaped.js.map +1 -0
- package/dist/internal/sse.js +104 -0
- package/dist/internal/sse.js.map +1 -0
- package/dist/internal/url-utils.js +24 -0
- package/dist/internal/url-utils.js.map +1 -0
- package/dist/middleware/compose.d.ts +85 -0
- package/dist/middleware/compose.d.ts.map +1 -0
- package/dist/middleware/compose.js +174 -0
- package/dist/middleware/compose.js.map +1 -0
- package/dist/middleware/index.d.ts +10 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/production-hook.d.ts +27 -0
- package/dist/middleware/production-hook.d.ts.map +1 -0
- package/dist/middleware/production-hook.js +21 -0
- package/dist/middleware/production-hook.js.map +1 -0
- package/dist/middleware/with-cost-limit.d.ts +46 -0
- package/dist/middleware/with-cost-limit.d.ts.map +1 -0
- package/dist/middleware/with-cost-limit.js +66 -0
- package/dist/middleware/with-cost-limit.js.map +1 -0
- package/dist/middleware/with-cost-tracking.d.ts +91 -0
- package/dist/middleware/with-cost-tracking.d.ts.map +1 -0
- package/dist/middleware/with-cost-tracking.js +95 -0
- package/dist/middleware/with-cost-tracking.js.map +1 -0
- package/dist/middleware/with-fallback.d.ts +25 -0
- package/dist/middleware/with-fallback.d.ts.map +1 -0
- package/dist/middleware/with-fallback.js +84 -0
- package/dist/middleware/with-fallback.js.map +1 -0
- package/dist/middleware/with-rate-limit.d.ts +36 -0
- package/dist/middleware/with-rate-limit.d.ts.map +1 -0
- package/dist/middleware/with-rate-limit.js +141 -0
- package/dist/middleware/with-rate-limit.js.map +1 -0
- package/dist/middleware/with-redaction.d.ts +60 -0
- package/dist/middleware/with-redaction.d.ts.map +1 -0
- package/dist/middleware/with-redaction.js +305 -0
- package/dist/middleware/with-redaction.js.map +1 -0
- package/dist/middleware/with-retry.d.ts +26 -0
- package/dist/middleware/with-retry.d.ts.map +1 -0
- package/dist/middleware/with-retry.js +157 -0
- package/dist/middleware/with-retry.js.map +1 -0
- package/dist/middleware/with-tracing.d.ts +21 -0
- package/dist/middleware/with-tracing.d.ts.map +1 -0
- package/dist/middleware/with-tracing.js +55 -0
- package/dist/middleware/with-tracing.js.map +1 -0
- package/dist/model-tier/classify.d.ts +34 -0
- package/dist/model-tier/classify.d.ts.map +1 -0
- package/dist/model-tier/classify.js +117 -0
- package/dist/model-tier/classify.js.map +1 -0
- package/dist/model-tier/index.d.ts +2 -0
- package/dist/model-tier/index.js +3 -0
- package/dist/provider.d.ts +51 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +64 -0
- package/dist/provider.js.map +1 -0
- package/dist/reasoning/apply-policy.d.ts +22 -0
- package/dist/reasoning/apply-policy.d.ts.map +1 -0
- package/dist/reasoning/apply-policy.js +47 -0
- package/dist/reasoning/apply-policy.js.map +1 -0
- package/dist/reasoning/classify-contract.d.ts +41 -0
- package/dist/reasoning/classify-contract.d.ts.map +1 -0
- package/dist/reasoning/classify-contract.js +59 -0
- package/dist/reasoning/classify-contract.js.map +1 -0
- package/dist/reasoning/index.d.ts +4 -0
- package/dist/reasoning/index.js +5 -0
- package/dist/reasoning/retention.d.ts +39 -0
- package/dist/reasoning/retention.d.ts.map +1 -0
- package/dist/reasoning/retention.js +33 -0
- package/dist/reasoning/retention.js.map +1 -0
- package/dist/tool-examples.d.ts +14 -0
- package/dist/tool-examples.d.ts.map +1 -0
- package/dist/tool-examples.js +35 -0
- package/dist/tool-examples.js.map +1 -0
- package/dist/trust/classify-local-provider.d.ts +47 -0
- package/dist/trust/classify-local-provider.d.ts.map +1 -0
- package/dist/trust/classify-local-provider.js +164 -0
- package/dist/trust/classify-local-provider.js.map +1 -0
- package/dist/trust/index.d.ts +3 -0
- package/dist/trust/index.js +3 -0
- package/package.json +128 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as _graphorin_core2 from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/middleware/with-cost-tracking.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `withCostTracking` — reports `tokensUsed` + `cost` per `provider × model`
|
|
7
|
+
* from the `finish` event of every stream / one-shot generation via the
|
|
8
|
+
* `onUsage` hook. For a ready-made process-local accumulator, wire
|
|
9
|
+
* {@link createCostAccumulator}'s `onUsage` and read its `.totals()`; consumers
|
|
10
|
+
* that prefer the framework's `CostTracker` from `@graphorin/observability`
|
|
11
|
+
* pass their own delegate.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Aggregated totals for one `provider × model`, returned by
|
|
17
|
+
* {@link CostAccumulator.totalFor} / {@link CostAccumulator.totals}.
|
|
18
|
+
*
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
interface CostTrackingTotals {
|
|
22
|
+
readonly callCount: number;
|
|
23
|
+
readonly promptTokens: number;
|
|
24
|
+
readonly completionTokens: number;
|
|
25
|
+
readonly totalTokens: number;
|
|
26
|
+
readonly costUsd: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A process-local cost accumulator (PS-8). Wire {@link CostAccumulator.onUsage}
|
|
30
|
+
* into {@link withCostTracking} and read the running totals — keyed by
|
|
31
|
+
* `provider × model` — back via {@link CostAccumulator.totals} /
|
|
32
|
+
* {@link CostAccumulator.totalFor}.
|
|
33
|
+
*
|
|
34
|
+
* @stable
|
|
35
|
+
*/
|
|
36
|
+
interface CostAccumulator {
|
|
37
|
+
/** Pass this to {@link withCostTracking}'s `onUsage`. */
|
|
38
|
+
readonly onUsage: NonNullable<WithCostTrackingOptions['onUsage']>;
|
|
39
|
+
/** Snapshot of every tracked `provider::model` → totals. */
|
|
40
|
+
totals(): ReadonlyMap<string, CostTrackingTotals>;
|
|
41
|
+
/** Running totals for one `provider × model` (zeros when unseen). */
|
|
42
|
+
totalFor(providerName: string, modelId: string): CostTrackingTotals;
|
|
43
|
+
/** Clear all accumulated totals. */
|
|
44
|
+
reset(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a {@link CostAccumulator} — the process-local accumulator described on
|
|
48
|
+
* {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.
|
|
49
|
+
*
|
|
50
|
+
* @stable
|
|
51
|
+
*/
|
|
52
|
+
declare function createCostAccumulator(): CostAccumulator;
|
|
53
|
+
/**
|
|
54
|
+
* Options for {@link withCostTracking}.
|
|
55
|
+
*
|
|
56
|
+
* @stable
|
|
57
|
+
*/
|
|
58
|
+
interface WithCostTrackingOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Hook fired on every `finish` event with the parsed usage. The
|
|
61
|
+
* hook receives the underlying provider's name + modelId so the
|
|
62
|
+
* caller can route into a per-model accumulator.
|
|
63
|
+
*/
|
|
64
|
+
readonly onUsage?: (info: {
|
|
65
|
+
readonly providerName: string;
|
|
66
|
+
readonly modelId: string;
|
|
67
|
+
readonly promptTokens: number;
|
|
68
|
+
readonly completionTokens: number;
|
|
69
|
+
readonly totalTokens: number;
|
|
70
|
+
readonly costUsd: number;
|
|
71
|
+
readonly metadata: Readonly<Record<string, unknown>> | undefined;
|
|
72
|
+
}) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Optional pricing lookup. When set, the middleware computes
|
|
75
|
+
* `costUsd` from the returned price and surfaces it on the hook.
|
|
76
|
+
*/
|
|
77
|
+
readonly priceLookup?: (info: {
|
|
78
|
+
readonly providerName: string;
|
|
79
|
+
readonly modelId: string;
|
|
80
|
+
}) => {
|
|
81
|
+
readonly inputPerMtok?: number;
|
|
82
|
+
readonly outputPerMtok?: number;
|
|
83
|
+
} | null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* @stable
|
|
87
|
+
*/
|
|
88
|
+
declare const withCostTracking: (opts: WithCostTrackingOptions) => _graphorin_core2.ProviderMiddleware;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { CostAccumulator, CostTrackingTotals, WithCostTrackingOptions, createCostAccumulator, withCostTracking };
|
|
91
|
+
//# sourceMappingURL=with-cost-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-cost-tracking.d.ts","names":[],"sources":["../../src/middleware/with-cost-tracking.ts"],"sourcesContent":[],"mappings":";;;;;;;AAqBA;AAgBA;;;;;;;AAyBA;AA0BA;AA4BA;;;UA/FiB,kBAAA;;;;;;;;;;;;;;;UAgBA,eAAA;;oBAEG,YAAY;;YAEpB,oBAAoB;;mDAEmB;;;;;;;;;;iBAmBnC,qBAAA,CAAA,GAAyB;;;;;;UA0BxB,uBAAA;;;;;;;;;;;;;uBAaM,SAAS;;;;;;;;;;;;;;;;;cAenB,yBAAgB,4BAAA,gBAAA,CAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineProviderMiddleware } from "./compose.js";
|
|
2
|
+
|
|
3
|
+
//#region src/middleware/with-cost-tracking.ts
|
|
4
|
+
const ZERO_TOTALS = Object.freeze({
|
|
5
|
+
callCount: 0,
|
|
6
|
+
promptTokens: 0,
|
|
7
|
+
completionTokens: 0,
|
|
8
|
+
totalTokens: 0,
|
|
9
|
+
costUsd: 0
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Create a {@link CostAccumulator} — the process-local accumulator described on
|
|
13
|
+
* {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.
|
|
14
|
+
*
|
|
15
|
+
* @stable
|
|
16
|
+
*/
|
|
17
|
+
function createCostAccumulator() {
|
|
18
|
+
const map = /* @__PURE__ */ new Map();
|
|
19
|
+
const keyOf = (providerName, modelId) => `${providerName}::${modelId}`;
|
|
20
|
+
return {
|
|
21
|
+
onUsage: (info) => {
|
|
22
|
+
const key = keyOf(info.providerName, info.modelId);
|
|
23
|
+
const prev = map.get(key) ?? ZERO_TOTALS;
|
|
24
|
+
map.set(key, {
|
|
25
|
+
callCount: prev.callCount + 1,
|
|
26
|
+
promptTokens: prev.promptTokens + info.promptTokens,
|
|
27
|
+
completionTokens: prev.completionTokens + info.completionTokens,
|
|
28
|
+
totalTokens: prev.totalTokens + info.totalTokens,
|
|
29
|
+
costUsd: prev.costUsd + info.costUsd
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
totals: () => new Map(map),
|
|
33
|
+
totalFor: (providerName, modelId) => map.get(keyOf(providerName, modelId)) ?? ZERO_TOTALS,
|
|
34
|
+
reset: () => map.clear()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @stable
|
|
39
|
+
*/
|
|
40
|
+
const withCostTracking = defineProviderMiddleware({
|
|
41
|
+
kind: "withCostTracking",
|
|
42
|
+
factory: (opts) => {
|
|
43
|
+
return (next) => ({
|
|
44
|
+
name: next.name,
|
|
45
|
+
modelId: next.modelId,
|
|
46
|
+
capabilities: next.capabilities,
|
|
47
|
+
...next.acceptsSensitivity !== void 0 ? { acceptsSensitivity: next.acceptsSensitivity } : {},
|
|
48
|
+
stream(req) {
|
|
49
|
+
return trackingStream(next, req, opts);
|
|
50
|
+
},
|
|
51
|
+
async generate(req) {
|
|
52
|
+
const result = await next.generate(req);
|
|
53
|
+
emitUsage(result, next, opts, asRecord(req.metadata));
|
|
54
|
+
return result;
|
|
55
|
+
},
|
|
56
|
+
...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
async function* trackingStream(next, req, opts) {
|
|
61
|
+
const metadata = asRecord(req.metadata);
|
|
62
|
+
for await (const event of next.stream(req)) {
|
|
63
|
+
yield event;
|
|
64
|
+
if (event.type === "finish") emitUsage({
|
|
65
|
+
usage: event.usage,
|
|
66
|
+
finishReason: event.finishReason
|
|
67
|
+
}, next, opts, metadata);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function asRecord(value) {
|
|
71
|
+
if (value === void 0 || value === null) return void 0;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function emitUsage(result, next, opts, metadata) {
|
|
75
|
+
if (opts.onUsage === void 0) return;
|
|
76
|
+
const usage = result.usage;
|
|
77
|
+
const price = opts.priceLookup?.({
|
|
78
|
+
providerName: next.name,
|
|
79
|
+
modelId: next.modelId
|
|
80
|
+
}) ?? null;
|
|
81
|
+
const costUsd = price !== null ? usage.promptTokens * (price.inputPerMtok ?? 0) / 1e6 + usage.completionTokens * (price.outputPerMtok ?? 0) / 1e6 : usage.cost?.amount ?? 0;
|
|
82
|
+
opts.onUsage({
|
|
83
|
+
providerName: next.name,
|
|
84
|
+
modelId: next.modelId,
|
|
85
|
+
promptTokens: usage.promptTokens,
|
|
86
|
+
completionTokens: usage.completionTokens,
|
|
87
|
+
totalTokens: usage.totalTokens,
|
|
88
|
+
costUsd,
|
|
89
|
+
metadata
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { createCostAccumulator, withCostTracking };
|
|
95
|
+
//# sourceMappingURL=with-cost-tracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-cost-tracking.js","names":["ZERO_TOTALS: CostTrackingTotals"],"sources":["../../src/middleware/with-cost-tracking.ts"],"sourcesContent":["/**\n * `withCostTracking` — reports `tokensUsed` + `cost` per `provider × model`\n * from the `finish` event of every stream / one-shot generation via the\n * `onUsage` hook. For a ready-made process-local accumulator, wire\n * {@link createCostAccumulator}'s `onUsage` and read its `.totals()`; consumers\n * that prefer the framework's `CostTracker` from `@graphorin/observability`\n * pass their own delegate.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderResponse } from '@graphorin/core';\n\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Aggregated totals for one `provider × model`, returned by\n * {@link CostAccumulator.totalFor} / {@link CostAccumulator.totals}.\n *\n * @stable\n */\nexport interface CostTrackingTotals {\n readonly callCount: number;\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n readonly costUsd: number;\n}\n\n/**\n * A process-local cost accumulator (PS-8). Wire {@link CostAccumulator.onUsage}\n * into {@link withCostTracking} and read the running totals — keyed by\n * `provider × model` — back via {@link CostAccumulator.totals} /\n * {@link CostAccumulator.totalFor}.\n *\n * @stable\n */\nexport interface CostAccumulator {\n /** Pass this to {@link withCostTracking}'s `onUsage`. */\n readonly onUsage: NonNullable<WithCostTrackingOptions['onUsage']>;\n /** Snapshot of every tracked `provider::model` → totals. */\n totals(): ReadonlyMap<string, CostTrackingTotals>;\n /** Running totals for one `provider × model` (zeros when unseen). */\n totalFor(providerName: string, modelId: string): CostTrackingTotals;\n /** Clear all accumulated totals. */\n reset(): void;\n}\n\nconst ZERO_TOTALS: CostTrackingTotals = Object.freeze({\n callCount: 0,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n costUsd: 0,\n});\n\n/**\n * Create a {@link CostAccumulator} — the process-local accumulator described on\n * {@link withCostTracking}. Keys totals by `'<providerName>::<modelId>'`.\n *\n * @stable\n */\nexport function createCostAccumulator(): CostAccumulator {\n const map = new Map<string, CostTrackingTotals>();\n const keyOf = (providerName: string, modelId: string): string => `${providerName}::${modelId}`;\n return {\n onUsage: (info) => {\n const key = keyOf(info.providerName, info.modelId);\n const prev = map.get(key) ?? ZERO_TOTALS;\n map.set(key, {\n callCount: prev.callCount + 1,\n promptTokens: prev.promptTokens + info.promptTokens,\n completionTokens: prev.completionTokens + info.completionTokens,\n totalTokens: prev.totalTokens + info.totalTokens,\n costUsd: prev.costUsd + info.costUsd,\n });\n },\n totals: () => new Map(map),\n totalFor: (providerName, modelId) => map.get(keyOf(providerName, modelId)) ?? ZERO_TOTALS,\n reset: () => map.clear(),\n };\n}\n\n/**\n * Options for {@link withCostTracking}.\n *\n * @stable\n */\nexport interface WithCostTrackingOptions {\n /**\n * Hook fired on every `finish` event with the parsed usage. The\n * hook receives the underlying provider's name + modelId so the\n * caller can route into a per-model accumulator.\n */\n readonly onUsage?: (info: {\n readonly providerName: string;\n readonly modelId: string;\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n readonly costUsd: number;\n readonly metadata: Readonly<Record<string, unknown>> | undefined;\n }) => void;\n /**\n * Optional pricing lookup. When set, the middleware computes\n * `costUsd` from the returned price and surfaces it on the hook.\n */\n readonly priceLookup?: (info: {\n readonly providerName: string;\n readonly modelId: string;\n }) => { readonly inputPerMtok?: number; readonly outputPerMtok?: number } | null;\n}\n\n/**\n * @stable\n */\nexport const withCostTracking = defineProviderMiddleware<WithCostTrackingOptions>({\n kind: 'withCostTracking',\n factory: (opts: WithCostTrackingOptions) => {\n return (next: Provider): Provider => ({\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n return trackingStream(next, req, opts);\n },\n async generate(req) {\n const result = await next.generate(req);\n emitUsage(result, next, opts, asRecord(req.metadata));\n return result;\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* trackingStream(\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n opts: WithCostTrackingOptions,\n): AsyncIterable<ProviderEvent> {\n const metadata = asRecord(req.metadata);\n for await (const event of next.stream(req)) {\n yield event;\n if (event.type === 'finish') {\n emitUsage({ usage: event.usage, finishReason: event.finishReason }, next, opts, metadata);\n }\n }\n}\n\nfunction asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {\n if (value === undefined || value === null) return undefined;\n return value as Readonly<Record<string, unknown>>;\n}\n\nfunction emitUsage(\n result:\n | ProviderResponse\n | { usage: ProviderResponse['usage']; finishReason: ProviderResponse['finishReason'] },\n next: Provider,\n opts: WithCostTrackingOptions,\n metadata: Readonly<Record<string, unknown>> | undefined,\n): void {\n if (opts.onUsage === undefined) return;\n const usage = result.usage;\n const price = opts.priceLookup?.({ providerName: next.name, modelId: next.modelId }) ?? null;\n const costUsd =\n price !== null\n ? (usage.promptTokens * (price.inputPerMtok ?? 0)) / 1_000_000 +\n (usage.completionTokens * (price.outputPerMtok ?? 0)) / 1_000_000\n : (usage.cost?.amount ?? 0);\n opts.onUsage({\n providerName: next.name,\n modelId: next.modelId,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n costUsd,\n metadata,\n });\n}\n"],"mappings":";;;AAgDA,MAAMA,cAAkC,OAAO,OAAO;CACpD,WAAW;CACX,cAAc;CACd,kBAAkB;CAClB,aAAa;CACb,SAAS;CACV,CAAC;;;;;;;AAQF,SAAgB,wBAAyC;CACvD,MAAM,sBAAM,IAAI,KAAiC;CACjD,MAAM,SAAS,cAAsB,YAA4B,GAAG,aAAa,IAAI;AACrF,QAAO;EACL,UAAU,SAAS;GACjB,MAAM,MAAM,MAAM,KAAK,cAAc,KAAK,QAAQ;GAClD,MAAM,OAAO,IAAI,IAAI,IAAI,IAAI;AAC7B,OAAI,IAAI,KAAK;IACX,WAAW,KAAK,YAAY;IAC5B,cAAc,KAAK,eAAe,KAAK;IACvC,kBAAkB,KAAK,mBAAmB,KAAK;IAC/C,aAAa,KAAK,cAAc,KAAK;IACrC,SAAS,KAAK,UAAU,KAAK;IAC9B,CAAC;;EAEJ,cAAc,IAAI,IAAI,IAAI;EAC1B,WAAW,cAAc,YAAY,IAAI,IAAI,MAAM,cAAc,QAAQ,CAAC,IAAI;EAC9E,aAAa,IAAI,OAAO;EACzB;;;;;AAoCH,MAAa,mBAAmB,yBAAkD;CAChF,MAAM;CACN,UAAU,SAAkC;AAC1C,UAAQ,UAA8B;GACpC,MAAM,KAAK;GACX,SAAS,KAAK;GACd,cAAc,KAAK;GACnB,GAAI,KAAK,uBAAuB,SAC5B,EAAE,oBAAoB,KAAK,oBAAoB,GAC/C,EAAE;GACN,OAAO,KAAK;AACV,WAAO,eAAe,MAAM,KAAK,KAAK;;GAExC,MAAM,SAAS,KAAK;IAClB,MAAM,SAAS,MAAM,KAAK,SAAS,IAAI;AACvC,cAAU,QAAQ,MAAM,MAAM,SAAS,IAAI,SAAS,CAAC;AACrD,WAAO;;GAET,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,gBAAgB,eACd,MACA,KACA,MAC8B;CAC9B,MAAM,WAAW,SAAS,IAAI,SAAS;AACvC,YAAW,MAAM,SAAS,KAAK,OAAO,IAAI,EAAE;AAC1C,QAAM;AACN,MAAI,MAAM,SAAS,SACjB,WAAU;GAAE,OAAO,MAAM;GAAO,cAAc,MAAM;GAAc,EAAE,MAAM,MAAM,SAAS;;;AAK/F,SAAS,SAAS,OAA+D;AAC/E,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAO;;AAGT,SAAS,UACP,QAGA,MACA,MACA,UACM;AACN,KAAI,KAAK,YAAY,OAAW;CAChC,MAAM,QAAQ,OAAO;CACrB,MAAM,QAAQ,KAAK,cAAc;EAAE,cAAc,KAAK;EAAM,SAAS,KAAK;EAAS,CAAC,IAAI;CACxF,MAAM,UACJ,UAAU,OACL,MAAM,gBAAgB,MAAM,gBAAgB,KAAM,MAClD,MAAM,oBAAoB,MAAM,iBAAiB,KAAM,MACvD,MAAM,MAAM,UAAU;AAC7B,MAAK,QAAQ;EACX,cAAc,KAAK;EACnB,SAAS,KAAK;EACd,cAAc,MAAM;EACpB,kBAAkB,MAAM;EACxB,aAAa,MAAM;EACnB;EACA;EACD,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as _graphorin_core3 from "@graphorin/core";
|
|
2
|
+
import { Provider } from "@graphorin/core";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/with-fallback.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link withFallback}.
|
|
8
|
+
*
|
|
9
|
+
* @stable
|
|
10
|
+
*/
|
|
11
|
+
interface WithFallbackOptions {
|
|
12
|
+
/** Alternate providers tried in order. */
|
|
13
|
+
readonly fallbacks: ReadonlyArray<Provider>;
|
|
14
|
+
/** Predicate deciding whether an error should trigger a fallback. */
|
|
15
|
+
readonly shouldFallback?: (err: unknown) => boolean;
|
|
16
|
+
/** Optional log sink; defaults to `console.warn`. */
|
|
17
|
+
readonly logger?: (message: string, meta?: object) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
declare const withFallback: (opts: WithFallbackOptions) => _graphorin_core3.ProviderMiddleware;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { WithFallbackOptions, withFallback };
|
|
25
|
+
//# sourceMappingURL=with-fallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-fallback.d.ts","names":[],"sources":["../../src/middleware/with-fallback.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UAqBiB,mBAAA;;sBAEK,cAAc;;;;;;;;;cAUvB,qBAAY,wBAAA,gBAAA,CAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineProviderMiddleware } from "./compose.js";
|
|
2
|
+
import { isAbortError } from "../internal/abort.js";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/with-fallback.ts
|
|
5
|
+
/**
|
|
6
|
+
* @stable
|
|
7
|
+
*/
|
|
8
|
+
const withFallback = defineProviderMiddleware({
|
|
9
|
+
kind: "withFallback",
|
|
10
|
+
factory: (opts) => {
|
|
11
|
+
const shouldFallback = opts.shouldFallback ?? defaultShouldFallback;
|
|
12
|
+
const logger = opts.logger ?? defaultLogger;
|
|
13
|
+
if (!Array.isArray(opts.fallbacks) || opts.fallbacks.length === 0) throw new TypeError("withFallback: fallbacks array must contain at least one Provider.");
|
|
14
|
+
return (next) => {
|
|
15
|
+
const candidates = [next, ...opts.fallbacks];
|
|
16
|
+
return {
|
|
17
|
+
name: next.name,
|
|
18
|
+
modelId: next.modelId,
|
|
19
|
+
capabilities: next.capabilities,
|
|
20
|
+
...next.acceptsSensitivity !== void 0 ? { acceptsSensitivity: next.acceptsSensitivity } : {},
|
|
21
|
+
stream(req) {
|
|
22
|
+
return fallbackStream(candidates, req, shouldFallback, logger);
|
|
23
|
+
},
|
|
24
|
+
async generate(req) {
|
|
25
|
+
let lastErr;
|
|
26
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
27
|
+
const cand = candidates[i];
|
|
28
|
+
if (cand === void 0) continue;
|
|
29
|
+
try {
|
|
30
|
+
return await cand.generate(req);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (req.signal?.aborted) throw err;
|
|
33
|
+
lastErr = err;
|
|
34
|
+
if (i === candidates.length - 1 || !shouldFallback(err)) throw err;
|
|
35
|
+
logger(`[graphorin/provider] withFallback: '${cand.name}' failed, trying next.`, { error: err.message });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw lastErr ?? /* @__PURE__ */ new Error("withFallback: no fallback succeeded.");
|
|
39
|
+
},
|
|
40
|
+
...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
async function* fallbackStream(candidates, req, shouldFallback, logger) {
|
|
46
|
+
let lastErr;
|
|
47
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
48
|
+
const cand = candidates[i];
|
|
49
|
+
if (cand === void 0) continue;
|
|
50
|
+
let yieldedAny = false;
|
|
51
|
+
try {
|
|
52
|
+
for await (const event of cand.stream(req)) {
|
|
53
|
+
yieldedAny = true;
|
|
54
|
+
yield event;
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (req.signal?.aborted) throw err;
|
|
59
|
+
lastErr = err;
|
|
60
|
+
if (yieldedAny) throw err;
|
|
61
|
+
if (i === candidates.length - 1 || !shouldFallback(err)) throw err;
|
|
62
|
+
logger(`[graphorin/provider] withFallback: '${cand.name}' failed, trying next.`, { error: err.message });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw lastErr ?? /* @__PURE__ */ new Error("withFallback: no fallback succeeded.");
|
|
66
|
+
}
|
|
67
|
+
function defaultShouldFallback(err) {
|
|
68
|
+
if (err === null || typeof err !== "object") return false;
|
|
69
|
+
if (isAbortError(err)) return false;
|
|
70
|
+
const e = err;
|
|
71
|
+
if (e.kind === "unauthorized" || e.kind === "invalid-request") return false;
|
|
72
|
+
if (e.kind === "transient" || e.kind === "rate-limit" || e.kind === "capacity") return true;
|
|
73
|
+
if (typeof e.status === "number" && e.status >= 500) return true;
|
|
74
|
+
if (typeof e.status === "number" && e.status === 0) return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function defaultLogger(message, meta) {
|
|
78
|
+
if (meta !== void 0) console.warn(message, meta);
|
|
79
|
+
else console.warn(message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { withFallback };
|
|
84
|
+
//# sourceMappingURL=with-fallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-fallback.js","names":["candidates: ReadonlyArray<Provider>","lastErr: unknown"],"sources":["../../src/middleware/with-fallback.ts"],"sourcesContent":["/**\n * `withFallback` — request-level fallback chain. When the wrapped\n * provider raises a fallback-eligible error, the middleware retries\n * the same request against each alternate provider in `fallbacks`.\n * The model concept is preserved: callers wire `[primary, alt1, alt2]`\n * pointing at providers serving the same logical model. Cross-model\n * fallbacks are the agent-level concern (Phase 12).\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderResponse } from '@graphorin/core';\n\nimport { isAbortError } from '../internal/abort.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withFallback}.\n *\n * @stable\n */\nexport interface WithFallbackOptions {\n /** Alternate providers tried in order. */\n readonly fallbacks: ReadonlyArray<Provider>;\n /** Predicate deciding whether an error should trigger a fallback. */\n readonly shouldFallback?: (err: unknown) => boolean;\n /** Optional log sink; defaults to `console.warn`. */\n readonly logger?: (message: string, meta?: object) => void;\n}\n\n/**\n * @stable\n */\nexport const withFallback = defineProviderMiddleware<WithFallbackOptions>({\n kind: 'withFallback',\n factory: (opts: WithFallbackOptions) => {\n const shouldFallback = opts.shouldFallback ?? defaultShouldFallback;\n const logger = opts.logger ?? defaultLogger;\n if (!Array.isArray(opts.fallbacks) || opts.fallbacks.length === 0) {\n throw new TypeError('withFallback: fallbacks array must contain at least one Provider.');\n }\n return (next: Provider): Provider => {\n const candidates: ReadonlyArray<Provider> = [next, ...opts.fallbacks];\n return {\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n return fallbackStream(candidates, req, shouldFallback, logger);\n },\n async generate(req) {\n let lastErr: unknown;\n for (let i = 0; i < candidates.length; i++) {\n const cand = candidates[i];\n if (cand === undefined) continue;\n try {\n return await cand.generate(req);\n } catch (err) {\n if (req.signal?.aborted) throw err;\n lastErr = err;\n if (i === candidates.length - 1 || !shouldFallback(err)) throw err;\n logger(`[graphorin/provider] withFallback: '${cand.name}' failed, trying next.`, {\n error: (err as Error).message,\n });\n }\n }\n throw lastErr ?? new Error('withFallback: no fallback succeeded.');\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\nasync function* fallbackStream(\n candidates: ReadonlyArray<Provider>,\n req: import('@graphorin/core').ProviderRequest,\n shouldFallback: (err: unknown) => boolean,\n logger: (message: string, meta?: object) => void,\n): AsyncIterable<ProviderEvent> {\n let lastErr: unknown;\n for (let i = 0; i < candidates.length; i++) {\n const cand = candidates[i];\n if (cand === undefined) continue;\n let yieldedAny = false;\n try {\n for await (const event of cand.stream(req)) {\n yieldedAny = true;\n yield event;\n }\n return;\n } catch (err) {\n if (req.signal?.aborted) throw err;\n lastErr = err;\n if (yieldedAny) {\n // Once we have streamed events, falling back would split the\n // logical response across providers — propagate instead.\n throw err;\n }\n if (i === candidates.length - 1 || !shouldFallback(err)) throw err;\n logger(`[graphorin/provider] withFallback: '${cand.name}' failed, trying next.`, {\n error: (err as Error).message,\n });\n }\n }\n throw lastErr ?? new Error('withFallback: no fallback succeeded.');\n}\n\nfunction defaultShouldFallback(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false;\n // An aborted request must not trigger a fallback, even as a `status: 0`\n // network error (PS-2). The loop also short-circuits on `req.signal?.aborted`.\n if (isAbortError(err)) return false;\n const e = err as { kind?: string; status?: number };\n if (e.kind === 'unauthorized' || e.kind === 'invalid-request') return false;\n if (e.kind === 'transient' || e.kind === 'rate-limit' || e.kind === 'capacity') return true;\n if (typeof e.status === 'number' && e.status >= 500) return true;\n // PS-2: the headline fallback scenario — the primary provider is down\n // (a local server refusing connections) surfaces as `status: 0`. Treat a\n // network failure as fallback-eligible so the chain advances to the next\n // provider (abort already excluded above).\n if (typeof e.status === 'number' && e.status === 0) return true;\n return false;\n}\n\nfunction defaultLogger(message: string, meta?: object): void {\n if (meta !== undefined) console.warn(message, meta);\n else console.warn(message);\n}\n\n// Cast to silence the \"declared but unused\" lint when only the types\n// are needed by consumers.\nexport type _FallbackResponseGuard = ProviderResponse;\n"],"mappings":";;;;;;;AAiCA,MAAa,eAAe,yBAA8C;CACxE,MAAM;CACN,UAAU,SAA8B;EACtC,MAAM,iBAAiB,KAAK,kBAAkB;EAC9C,MAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,MAAM,QAAQ,KAAK,UAAU,IAAI,KAAK,UAAU,WAAW,EAC9D,OAAM,IAAI,UAAU,oEAAoE;AAE1F,UAAQ,SAA6B;GACnC,MAAMA,aAAsC,CAAC,MAAM,GAAG,KAAK,UAAU;AACrE,UAAO;IACL,MAAM,KAAK;IACX,SAAS,KAAK;IACd,cAAc,KAAK;IACnB,GAAI,KAAK,uBAAuB,SAC5B,EAAE,oBAAoB,KAAK,oBAAoB,GAC/C,EAAE;IACN,OAAO,KAAK;AACV,YAAO,eAAe,YAAY,KAAK,gBAAgB,OAAO;;IAEhE,MAAM,SAAS,KAAK;KAClB,IAAIC;AACJ,UAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;MAC1C,MAAM,OAAO,WAAW;AACxB,UAAI,SAAS,OAAW;AACxB,UAAI;AACF,cAAO,MAAM,KAAK,SAAS,IAAI;eACxB,KAAK;AACZ,WAAI,IAAI,QAAQ,QAAS,OAAM;AAC/B,iBAAU;AACV,WAAI,MAAM,WAAW,SAAS,KAAK,CAAC,eAAe,IAAI,CAAE,OAAM;AAC/D,cAAO,uCAAuC,KAAK,KAAK,yBAAyB,EAC/E,OAAQ,IAAc,SACvB,CAAC;;;AAGN,WAAM,2BAAW,IAAI,MAAM,uCAAuC;;IAEpE,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;IACzE;;;CAGN,CAAC;AAEF,gBAAgB,eACd,YACA,KACA,gBACA,QAC8B;CAC9B,IAAIA;AACJ,MAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;EAC1C,MAAM,OAAO,WAAW;AACxB,MAAI,SAAS,OAAW;EACxB,IAAI,aAAa;AACjB,MAAI;AACF,cAAW,MAAM,SAAS,KAAK,OAAO,IAAI,EAAE;AAC1C,iBAAa;AACb,UAAM;;AAER;WACO,KAAK;AACZ,OAAI,IAAI,QAAQ,QAAS,OAAM;AAC/B,aAAU;AACV,OAAI,WAGF,OAAM;AAER,OAAI,MAAM,WAAW,SAAS,KAAK,CAAC,eAAe,IAAI,CAAE,OAAM;AAC/D,UAAO,uCAAuC,KAAK,KAAK,yBAAyB,EAC/E,OAAQ,IAAc,SACvB,CAAC;;;AAGN,OAAM,2BAAW,IAAI,MAAM,uCAAuC;;AAGpE,SAAS,sBAAsB,KAAuB;AACpD,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU,QAAO;AAGpD,KAAI,aAAa,IAAI,CAAE,QAAO;CAC9B,MAAM,IAAI;AACV,KAAI,EAAE,SAAS,kBAAkB,EAAE,SAAS,kBAAmB,QAAO;AACtE,KAAI,EAAE,SAAS,eAAe,EAAE,SAAS,gBAAgB,EAAE,SAAS,WAAY,QAAO;AACvF,KAAI,OAAO,EAAE,WAAW,YAAY,EAAE,UAAU,IAAK,QAAO;AAK5D,KAAI,OAAO,EAAE,WAAW,YAAY,EAAE,WAAW,EAAG,QAAO;AAC3D,QAAO;;AAGT,SAAS,cAAc,SAAiB,MAAqB;AAC3D,KAAI,SAAS,OAAW,SAAQ,KAAK,SAAS,KAAK;KAC9C,SAAQ,KAAK,QAAQ"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as _graphorin_core4 from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/middleware/with-rate-limit.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `withRateLimit` — token-bucket rate limiter keyed per `provider × model`.
|
|
7
|
+
* Configurable mode: `'throw'` (default) raises
|
|
8
|
+
* {@link RateLimitExceededError} on overflow; `'queue'` waits for the
|
|
9
|
+
* next available slot.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Options for {@link withRateLimit}.
|
|
15
|
+
*
|
|
16
|
+
* @stable
|
|
17
|
+
*/
|
|
18
|
+
interface WithRateLimitOptions {
|
|
19
|
+
/** Allowed requests per minute. */
|
|
20
|
+
readonly requestsPerMinute: number;
|
|
21
|
+
/** Burst size — defaults to `requestsPerMinute / 4` (rounded up to >= 1). */
|
|
22
|
+
readonly burst?: number;
|
|
23
|
+
/** What to do on overflow. Default `'throw'`. */
|
|
24
|
+
readonly mode?: 'throw' | 'queue';
|
|
25
|
+
/** Test hook overriding `Date.now`. */
|
|
26
|
+
readonly nowImpl?: () => number;
|
|
27
|
+
/** Test hook overriding `setTimeout`-based wait. */
|
|
28
|
+
readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
declare const withRateLimit: (opts: WithRateLimitOptions) => _graphorin_core4.ProviderMiddleware;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { WithRateLimitOptions, withRateLimit };
|
|
36
|
+
//# sourceMappingURL=with-rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-rate-limit.d.ts","names":[],"sources":["../../src/middleware/with-rate-limit.ts"],"sourcesContent":[],"mappings":";;;;;;;AAmBA;AAgCA;;;;;;;;;UAhCiB,oBAAA;;;;;;;;;;6CAU4B,gBAAgB;;;;;cAsBhD,sBAAa,yBAAA,gBAAA,CAAA"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { RateLimitExceededError } from "../errors/errors.js";
|
|
2
|
+
import { defineProviderMiddleware } from "./compose.js";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/with-rate-limit.ts
|
|
5
|
+
/**
|
|
6
|
+
* @stable
|
|
7
|
+
*/
|
|
8
|
+
const withRateLimit = defineProviderMiddleware({
|
|
9
|
+
kind: "withRateLimit",
|
|
10
|
+
factory: (opts) => {
|
|
11
|
+
if (!(opts.requestsPerMinute > 0)) throw new RangeError("withRateLimit: requestsPerMinute must be > 0.");
|
|
12
|
+
const burst = Math.max(opts.burst ?? Math.ceil(opts.requestsPerMinute / 4), 1);
|
|
13
|
+
const refillPerMs = opts.requestsPerMinute / 6e4;
|
|
14
|
+
const mode = opts.mode ?? "throw";
|
|
15
|
+
const now = opts.nowImpl ?? (() => Date.now());
|
|
16
|
+
const sleep = opts.sleepImpl ?? defaultSleep;
|
|
17
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
18
|
+
return (next) => {
|
|
19
|
+
const key = `${next.name}::${next.modelId}`;
|
|
20
|
+
const bucketFor = () => {
|
|
21
|
+
let bucket = buckets.get(key);
|
|
22
|
+
if (bucket === void 0) {
|
|
23
|
+
bucket = {
|
|
24
|
+
tokens: burst,
|
|
25
|
+
lastRefill: now(),
|
|
26
|
+
queue: [],
|
|
27
|
+
draining: false
|
|
28
|
+
};
|
|
29
|
+
buckets.set(key, bucket);
|
|
30
|
+
}
|
|
31
|
+
return bucket;
|
|
32
|
+
};
|
|
33
|
+
const refill = (bucket) => {
|
|
34
|
+
const ts = now();
|
|
35
|
+
bucket.tokens = Math.min(burst, bucket.tokens + (ts - bucket.lastRefill) * refillPerMs);
|
|
36
|
+
bucket.lastRefill = ts;
|
|
37
|
+
};
|
|
38
|
+
const drain = (bucket) => {
|
|
39
|
+
if (bucket.draining) return;
|
|
40
|
+
bucket.draining = true;
|
|
41
|
+
(async () => {
|
|
42
|
+
while (bucket.queue.length > 0) {
|
|
43
|
+
const head = bucket.queue[0];
|
|
44
|
+
if (head === void 0) break;
|
|
45
|
+
if (head.cancelled) {
|
|
46
|
+
bucket.queue.shift();
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
refill(bucket);
|
|
50
|
+
if (bucket.tokens >= 1) {
|
|
51
|
+
bucket.tokens -= 1;
|
|
52
|
+
bucket.queue.shift();
|
|
53
|
+
head.resolve();
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const waitMs = Math.ceil((1 - bucket.tokens) / refillPerMs);
|
|
57
|
+
try {
|
|
58
|
+
await sleep(waitMs);
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
bucket.draining = false;
|
|
62
|
+
})();
|
|
63
|
+
};
|
|
64
|
+
const acquire = async (signal) => {
|
|
65
|
+
if (signal?.aborted === true) throw signal.reason ?? /* @__PURE__ */ new Error("aborted");
|
|
66
|
+
const bucket = bucketFor();
|
|
67
|
+
refill(bucket);
|
|
68
|
+
if (bucket.tokens >= 1 && bucket.queue.length === 0) {
|
|
69
|
+
bucket.tokens -= 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (mode === "throw") throw new RateLimitExceededError(Math.ceil((1 - bucket.tokens) / refillPerMs));
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
const waiter = {
|
|
75
|
+
resolve,
|
|
76
|
+
reject,
|
|
77
|
+
...signal ? { signal } : {},
|
|
78
|
+
cancelled: false
|
|
79
|
+
};
|
|
80
|
+
if (signal !== void 0) {
|
|
81
|
+
const onAbort = () => {
|
|
82
|
+
waiter.cancelled = true;
|
|
83
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
84
|
+
};
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
const settle = resolve;
|
|
87
|
+
waiter.resolve = () => {
|
|
88
|
+
signal.removeEventListener("abort", onAbort);
|
|
89
|
+
settle();
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
bucket.queue.push(waiter);
|
|
93
|
+
drain(bucket);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
name: next.name,
|
|
98
|
+
modelId: next.modelId,
|
|
99
|
+
capabilities: next.capabilities,
|
|
100
|
+
...next.acceptsSensitivity !== void 0 ? { acceptsSensitivity: next.acceptsSensitivity } : {},
|
|
101
|
+
stream(req) {
|
|
102
|
+
return rateLimitedStream(next, req, acquire);
|
|
103
|
+
},
|
|
104
|
+
async generate(req) {
|
|
105
|
+
await acquire(req.signal);
|
|
106
|
+
return next.generate(req);
|
|
107
|
+
},
|
|
108
|
+
...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
async function* rateLimitedStream(next, req, acquire) {
|
|
114
|
+
await acquire(req.signal);
|
|
115
|
+
for await (const event of next.stream(req)) yield event;
|
|
116
|
+
}
|
|
117
|
+
function defaultSleep(ms, signal) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
if (signal?.aborted) {
|
|
120
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
cleanup();
|
|
125
|
+
resolve();
|
|
126
|
+
}, ms);
|
|
127
|
+
const onAbort = () => {
|
|
128
|
+
cleanup();
|
|
129
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
130
|
+
};
|
|
131
|
+
function cleanup() {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
signal?.removeEventListener("abort", onAbort);
|
|
134
|
+
}
|
|
135
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
export { withRateLimit };
|
|
141
|
+
//# sourceMappingURL=with-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-rate-limit.js","names":["waiter: Waiter"],"sources":["../../src/middleware/with-rate-limit.ts"],"sourcesContent":["/**\n * `withRateLimit` — token-bucket rate limiter keyed per `provider × model`.\n * Configurable mode: `'throw'` (default) raises\n * {@link RateLimitExceededError} on overflow; `'queue'` waits for the\n * next available slot.\n *\n * @packageDocumentation\n */\n\nimport type { Provider } from '@graphorin/core';\n\nimport { RateLimitExceededError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withRateLimit}.\n *\n * @stable\n */\nexport interface WithRateLimitOptions {\n /** Allowed requests per minute. */\n readonly requestsPerMinute: number;\n /** Burst size — defaults to `requestsPerMinute / 4` (rounded up to >= 1). */\n readonly burst?: number;\n /** What to do on overflow. Default `'throw'`. */\n readonly mode?: 'throw' | 'queue';\n /** Test hook overriding `Date.now`. */\n readonly nowImpl?: () => number;\n /** Test hook overriding `setTimeout`-based wait. */\n readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}\n\ninterface Waiter {\n resolve: () => void;\n reject: (err: unknown) => void;\n signal?: AbortSignal;\n cancelled: boolean;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefill: number;\n /** FIFO waiters in `'queue'` mode (PS-18). */\n queue: Waiter[];\n /** Whether the single drain loop for this bucket is running. */\n draining: boolean;\n}\n\n/**\n * @stable\n */\nexport const withRateLimit = defineProviderMiddleware<WithRateLimitOptions>({\n kind: 'withRateLimit',\n factory: (opts: WithRateLimitOptions) => {\n if (!(opts.requestsPerMinute > 0)) {\n throw new RangeError('withRateLimit: requestsPerMinute must be > 0.');\n }\n const burst = Math.max(opts.burst ?? Math.ceil(opts.requestsPerMinute / 4), 1);\n const refillPerMs = opts.requestsPerMinute / 60_000;\n const mode = opts.mode ?? 'throw';\n const now = opts.nowImpl ?? (() => Date.now());\n const sleep = opts.sleepImpl ?? defaultSleep;\n const buckets = new Map<string, BucketState>();\n return (next: Provider): Provider => {\n const key = `${next.name}::${next.modelId}`;\n const bucketFor = (): BucketState => {\n let bucket = buckets.get(key);\n if (bucket === undefined) {\n bucket = { tokens: burst, lastRefill: now(), queue: [], draining: false };\n buckets.set(key, bucket);\n }\n return bucket;\n };\n const refill = (bucket: BucketState): void => {\n const ts = now();\n bucket.tokens = Math.min(burst, bucket.tokens + (ts - bucket.lastRefill) * refillPerMs);\n bucket.lastRefill = ts;\n };\n // PS-18: a single drain loop per bucket grants queued waiters ONE token at\n // a time, sleeping until the next token actually refills. Previously every\n // concurrent waiter computed the same wait, slept once, and then all\n // passed — letting N requests through on ~1 token (a burst). FIFO order is\n // preserved and grants track the real refill schedule.\n const drain = (bucket: BucketState): void => {\n if (bucket.draining) return;\n bucket.draining = true;\n void (async () => {\n while (bucket.queue.length > 0) {\n const head = bucket.queue[0];\n if (head === undefined) break;\n if (head.cancelled) {\n bucket.queue.shift();\n continue;\n }\n refill(bucket);\n if (bucket.tokens >= 1) {\n bucket.tokens -= 1;\n bucket.queue.shift();\n head.resolve();\n continue;\n }\n const waitMs = Math.ceil((1 - bucket.tokens) / refillPerMs);\n try {\n await sleep(waitMs);\n } catch {\n // The shared drain sleep carries no per-waiter signal; individual\n // aborts are handled on the waiter via its listener.\n }\n }\n bucket.draining = false;\n })();\n };\n const acquire = async (signal?: AbortSignal): Promise<void> => {\n if (signal?.aborted === true) throw signal.reason ?? new Error('aborted');\n const bucket = bucketFor();\n refill(bucket);\n // Fast path: a token is free AND nobody is queued ahead (preserve FIFO).\n if (bucket.tokens >= 1 && bucket.queue.length === 0) {\n bucket.tokens -= 1;\n return;\n }\n if (mode === 'throw') {\n throw new RateLimitExceededError(Math.ceil((1 - bucket.tokens) / refillPerMs));\n }\n await new Promise<void>((resolve, reject) => {\n const waiter: Waiter = {\n resolve,\n reject,\n ...(signal ? { signal } : {}),\n cancelled: false,\n };\n if (signal !== undefined) {\n const onAbort = (): void => {\n waiter.cancelled = true;\n reject(signal.reason ?? new Error('aborted'));\n };\n signal.addEventListener('abort', onAbort, { once: true });\n const settle = resolve;\n waiter.resolve = (): void => {\n signal.removeEventListener('abort', onAbort);\n settle();\n };\n }\n bucket.queue.push(waiter);\n drain(bucket);\n });\n };\n return {\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n return rateLimitedStream(next, req, acquire);\n },\n async generate(req) {\n await acquire(req.signal);\n return next.generate(req);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\nasync function* rateLimitedStream(\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n acquire: (signal?: AbortSignal) => Promise<void>,\n): AsyncIterable<import('@graphorin/core').ProviderEvent> {\n await acquire(req.signal);\n for await (const event of next.stream(req)) {\n yield event;\n }\n}\n\nfunction defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error('aborted'));\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n const onAbort = () => {\n cleanup();\n reject(signal?.reason ?? new Error('aborted'));\n };\n function cleanup(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as _graphorin_core5 from "@graphorin/core";
|
|
2
|
+
import { LocalProviderTrust } from "@graphorin/core";
|
|
3
|
+
import { RedactionPattern } from "@graphorin/observability/redaction/patterns";
|
|
4
|
+
|
|
5
|
+
//#region src/middleware/with-redaction.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Range of fields scanned by the middleware.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
type PromptRedactionScanScope = 'all' | 'untrusted' | 'secret-value-only';
|
|
13
|
+
/**
|
|
14
|
+
* Sanitized record handed to `onViolation`. Carries metadata only;
|
|
15
|
+
* never the matched value.
|
|
16
|
+
*
|
|
17
|
+
* @stable
|
|
18
|
+
*/
|
|
19
|
+
interface PromptRedactionViolation {
|
|
20
|
+
readonly patternName: string;
|
|
21
|
+
readonly fieldPath: string;
|
|
22
|
+
readonly role?: string;
|
|
23
|
+
readonly matchLength: number;
|
|
24
|
+
readonly trustClass?: LocalProviderTrust;
|
|
25
|
+
readonly action: 'redact' | 'throw' | 'block-and-prompt-user';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Full prompt-redaction policy.
|
|
29
|
+
*
|
|
30
|
+
* @stable
|
|
31
|
+
*/
|
|
32
|
+
interface PromptRedactionPolicy {
|
|
33
|
+
/** Pattern catalogue. Defaults to the 14 built-in patterns. */
|
|
34
|
+
readonly patterns?: ReadonlyArray<RedactionPattern>;
|
|
35
|
+
/** Action on detection. Defaults to `'redact'`. */
|
|
36
|
+
readonly action?: 'redact' | 'throw' | 'block-and-prompt-user';
|
|
37
|
+
/** Throw on the first hit instead of redacting in-place. */
|
|
38
|
+
readonly failClosed?: boolean;
|
|
39
|
+
/** Range of fields scanned. Defaults to `'all'`. */
|
|
40
|
+
readonly scanScope?: PromptRedactionScanScope;
|
|
41
|
+
/** Detect `SecretValue` instances anywhere in the request. */
|
|
42
|
+
readonly detectSecretValue?: boolean;
|
|
43
|
+
/** Strip Anthropic-shape `cache_control` markers on hit. */
|
|
44
|
+
readonly stripCacheControlOnHit?: boolean;
|
|
45
|
+
/** Per-trust-class override block. */
|
|
46
|
+
readonly byTrustClass?: Partial<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>;
|
|
47
|
+
/** Sanitised violation hook (audit emission lives downstream). */
|
|
48
|
+
readonly onViolation?: (violation: PromptRedactionViolation) => void;
|
|
49
|
+
/** Optional logger override. Defaults to `console.warn`. */
|
|
50
|
+
readonly logger?: (message: string, meta?: object) => void;
|
|
51
|
+
/** Test hook — synthetic trust class. */
|
|
52
|
+
readonly trustClassOverride?: LocalProviderTrust;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @stable
|
|
56
|
+
*/
|
|
57
|
+
declare const withRedaction: (opts: PromptRedactionPolicy) => _graphorin_core5.ProviderMiddleware;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { PromptRedactionPolicy, PromptRedactionScanScope, PromptRedactionViolation, withRedaction };
|
|
60
|
+
//# sourceMappingURL=with-redaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-redaction.d.ts","names":[],"sources":["../../src/middleware/with-redaction.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;KAmDY,wBAAA;;;;;;;UAQK,wBAAA;;;;;wBAKO;;;;;;;;UAqBP,qBAAA;;sBAEK,cAAc;;;;;;uBAMb;;;;;;0BAMG,QAAQ,OAAO,oBAAoB,QAAQ;;qCAEhC;;;;gCAIL;;;;;cAcnB,sBAAa,0BAAA,gBAAA,CAAA"}
|