@graphorin/provider 0.5.0 → 0.6.1

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 (82) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +14 -9
  3. package/dist/adapters/llamacpp-server.d.ts +1 -1
  4. package/dist/adapters/llamacpp-server.js +1 -1
  5. package/dist/adapters/llamacpp-server.js.map +1 -1
  6. package/dist/adapters/ollama.d.ts.map +1 -1
  7. package/dist/adapters/ollama.js +3 -2
  8. package/dist/adapters/ollama.js.map +1 -1
  9. package/dist/adapters/openai-compatible.d.ts +14 -1
  10. package/dist/adapters/openai-compatible.d.ts.map +1 -1
  11. package/dist/adapters/openai-compatible.js +2 -0
  12. package/dist/adapters/openai-compatible.js.map +1 -1
  13. package/dist/adapters/vercel-messages.js +245 -0
  14. package/dist/adapters/vercel-messages.js.map +1 -0
  15. package/dist/adapters/vercel.d.ts +10 -8
  16. package/dist/adapters/vercel.d.ts.map +1 -1
  17. package/dist/adapters/vercel.js +54 -14
  18. package/dist/adapters/vercel.js.map +1 -1
  19. package/dist/counters/anthropic-wire.js +149 -0
  20. package/dist/counters/anthropic-wire.js.map +1 -0
  21. package/dist/counters/anthropic.d.ts +6 -1
  22. package/dist/counters/anthropic.d.ts.map +1 -1
  23. package/dist/counters/anthropic.js +25 -4
  24. package/dist/counters/anthropic.js.map +1 -1
  25. package/dist/counters/bedrock.js.map +1 -1
  26. package/dist/counters/dispatcher.d.ts.map +1 -1
  27. package/dist/counters/dispatcher.js +6 -4
  28. package/dist/counters/dispatcher.js.map +1 -1
  29. package/dist/counters/google.js.map +1 -1
  30. package/dist/counters/js-tiktoken.js.map +1 -1
  31. package/dist/errors/errors.d.ts +39 -11
  32. package/dist/errors/errors.d.ts.map +1 -1
  33. package/dist/errors/errors.js +54 -11
  34. package/dist/errors/errors.js.map +1 -1
  35. package/dist/errors/index.d.ts +2 -2
  36. package/dist/errors/index.js +2 -2
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +7 -6
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/abort.js +2 -2
  42. package/dist/internal/abort.js.map +1 -1
  43. package/dist/internal/http.js +19 -4
  44. package/dist/internal/http.js.map +1 -1
  45. package/dist/internal/openai-shaped.js +8 -3
  46. package/dist/internal/openai-shaped.js.map +1 -1
  47. package/dist/middleware/compose.d.ts +6 -6
  48. package/dist/middleware/compose.js +7 -7
  49. package/dist/middleware/compose.js.map +1 -1
  50. package/dist/middleware/production-hook.js.map +1 -1
  51. package/dist/middleware/with-cost-limit.d.ts +1 -1
  52. package/dist/middleware/with-cost-limit.js.map +1 -1
  53. package/dist/middleware/with-cost-tracking.d.ts +17 -4
  54. package/dist/middleware/with-cost-tracking.d.ts.map +1 -1
  55. package/dist/middleware/with-cost-tracking.js +12 -2
  56. package/dist/middleware/with-cost-tracking.js.map +1 -1
  57. package/dist/middleware/with-fallback.js +4 -2
  58. package/dist/middleware/with-fallback.js.map +1 -1
  59. package/dist/middleware/with-rate-limit.d.ts +2 -2
  60. package/dist/middleware/with-rate-limit.js.map +1 -1
  61. package/dist/middleware/with-redaction.d.ts +1 -1
  62. package/dist/middleware/with-redaction.js.map +1 -1
  63. package/dist/middleware/with-retry.js +3 -1
  64. package/dist/middleware/with-retry.js.map +1 -1
  65. package/dist/middleware/with-tracing.js +27 -6
  66. package/dist/middleware/with-tracing.js.map +1 -1
  67. package/dist/model-tier/classify.d.ts +1 -1
  68. package/dist/model-tier/classify.js +13 -5
  69. package/dist/model-tier/classify.js.map +1 -1
  70. package/dist/package.js +6 -0
  71. package/dist/package.js.map +1 -0
  72. package/dist/provider.js.map +1 -1
  73. package/dist/reasoning/apply-policy.js.map +1 -1
  74. package/dist/reasoning/classify-contract.js +14 -5
  75. package/dist/reasoning/classify-contract.js.map +1 -1
  76. package/dist/reasoning/retention.js.map +1 -1
  77. package/dist/tool-examples.d.ts +1 -1
  78. package/dist/tool-examples.js +1 -1
  79. package/dist/tool-examples.js.map +1 -1
  80. package/dist/trust/classify-local-provider.d.ts +1 -1
  81. package/dist/trust/classify-local-provider.js.map +1 -1
  82. package/package.json +6 -4
@@ -1 +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"}
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 /** Prompt tokens served from the provider cache (subset of promptTokens). */\n readonly cachedReadTokens: number;\n /** Prompt tokens written to the provider cache (subset of promptTokens). */\n readonly cacheWriteTokens: 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 cachedReadTokens: 0,\n cacheWriteTokens: 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 cachedReadTokens: prev.cachedReadTokens + (info.cachedReadTokens ?? 0),\n cacheWriteTokens: prev.cacheWriteTokens + (info.cacheWriteTokens ?? 0),\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 cachedReadTokens?: number;\n readonly cacheWriteTokens?: 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 * `cachedReadPerMtok` / `cacheWritePerMtok` price the prompt-cache legs\n * (core-provider-02); when omitted, cache tokens are billed at the full\n * input rate (never cheaper than reality, so absent price data degrades\n * to the pre-cache behaviour).\n */\n readonly priceLookup?: (info: { readonly providerName: string; readonly modelId: string }) => {\n readonly inputPerMtok?: number;\n readonly outputPerMtok?: number;\n readonly cachedReadPerMtok?: number;\n readonly cacheWritePerMtok?: number;\n } | 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 // Reasoning tokens are billed at the output rate (the PS-19 contract\n // in @graphorin/pricing) - providers that report them separately from\n // completionTokens must not get them for free.\n // Cache legs (core-provider-02): promptTokens INCLUDES cache reads and\n // writes, so subtract them from the base-rate leg and bill each at its\n // own rate; a missing cache rate falls back to the full input rate.\n const cachedRead = usage.cachedReadTokens ?? 0;\n const cacheWrite = usage.cacheWriteTokens ?? 0;\n const basePromptTokens = Math.max(0, usage.promptTokens - cachedRead - cacheWrite);\n const inputRate = price?.inputPerMtok ?? 0;\n const costUsd =\n price !== null\n ? (basePromptTokens * inputRate +\n cachedRead * (price.cachedReadPerMtok ?? inputRate) +\n cacheWrite * (price.cacheWritePerMtok ?? inputRate) +\n (usage.completionTokens + (usage.reasoningTokens ?? 0)) * (price.outputPerMtok ?? 0)) /\n 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 ...(usage.cachedReadTokens !== undefined ? { cachedReadTokens: usage.cachedReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n costUsd,\n metadata,\n });\n}\n"],"mappings":"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"}
@@ -68,8 +68,10 @@ function defaultShouldFallback(err) {
68
68
  if (err === null || typeof err !== "object") return false;
69
69
  if (isAbortError(err)) return false;
70
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;
71
+ const kinds = [e.kind, e.errorKind];
72
+ if (kinds.includes("unauthorized") || kinds.includes("invalid-request") || kinds.includes("context-length") || kinds.includes("content-filter")) return false;
73
+ if (kinds.includes("transient") || kinds.includes("rate-limit") || kinds.includes("rate-limit-exceeded") || kinds.includes("capacity")) return true;
74
+ if (typeof e.status === "number" && e.status === 429) return true;
73
75
  if (typeof e.status === "number" && e.status >= 500) return true;
74
76
  if (typeof e.status === "number" && e.status === 0) return true;
75
77
  return false;
@@ -1 +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"}
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; errorKind?: string; status?: number };\n // `ProviderHttpError.kind` is always 'provider-http' (stable discriminant);\n // the mapped canonical kind rides on `errorKind`. Consult both so a 429\n // fails over as a rate limit and a 400/context overflow never does.\n const kinds = [e.kind, e.errorKind];\n if (\n kinds.includes('unauthorized') ||\n kinds.includes('invalid-request') ||\n kinds.includes('context-length') ||\n kinds.includes('content-filter')\n ) {\n return false;\n }\n if (\n kinds.includes('transient') ||\n kinds.includes('rate-limit') ||\n kinds.includes('rate-limit-exceeded') ||\n kinds.includes('capacity')\n ) {\n return true;\n }\n // A 429 on the primary is exactly the case a same-model fallback chain\n // exists for - the alternate deployment has its own rate budget.\n if (typeof e.status === 'number' && e.status === 429) 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;CAIV,MAAM,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU;AACnC,KACE,MAAM,SAAS,eAAe,IAC9B,MAAM,SAAS,kBAAkB,IACjC,MAAM,SAAS,iBAAiB,IAChC,MAAM,SAAS,iBAAiB,CAEhC,QAAO;AAET,KACE,MAAM,SAAS,YAAY,IAC3B,MAAM,SAAS,aAAa,IAC5B,MAAM,SAAS,sBAAsB,IACrC,MAAM,SAAS,WAAW,CAE1B,QAAO;AAIT,KAAI,OAAO,EAAE,WAAW,YAAY,EAAE,WAAW,IAAK,QAAO;AAC7D,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"}
@@ -3,7 +3,7 @@ import * as _graphorin_core4 from "@graphorin/core";
3
3
  //#region src/middleware/with-rate-limit.d.ts
4
4
 
5
5
  /**
6
- * `withRateLimit` token-bucket rate limiter keyed per `provider × model`.
6
+ * `withRateLimit` - token-bucket rate limiter keyed per `provider × model`.
7
7
  * Configurable mode: `'throw'` (default) raises
8
8
  * {@link RateLimitExceededError} on overflow; `'queue'` waits for the
9
9
  * next available slot.
@@ -18,7 +18,7 @@ import * as _graphorin_core4 from "@graphorin/core";
18
18
  interface WithRateLimitOptions {
19
19
  /** Allowed requests per minute. */
20
20
  readonly requestsPerMinute: number;
21
- /** Burst size defaults to `requestsPerMinute / 4` (rounded up to >= 1). */
21
+ /** Burst size - defaults to `requestsPerMinute / 4` (rounded up to >= 1). */
22
22
  readonly burst?: number;
23
23
  /** What to do on overflow. Default `'throw'`. */
24
24
  readonly mode?: 'throw' | 'queue';
@@ -1 +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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"}
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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"}
@@ -48,7 +48,7 @@ interface PromptRedactionPolicy {
48
48
  readonly onViolation?: (violation: PromptRedactionViolation) => void;
49
49
  /** Optional logger override. Defaults to `console.warn`. */
50
50
  readonly logger?: (message: string, meta?: object) => void;
51
- /** Test hook synthetic trust class. */
51
+ /** Test hook - synthetic trust class. */
52
52
  readonly trustClassOverride?: LocalProviderTrust;
53
53
  }
54
54
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"with-redaction.js","names":["TRUST_CLASS_DEFAULTS: Readonly<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>","merged: RequiredPolicy","ctx: ScrubContext","next: AssistantMessage","out: Record<string, unknown>"],"sources":["../../src/middleware/with-redaction.ts"],"sourcesContent":["/**\n * `withRedaction` — outbound prompt-redaction middleware. Innermost\n * layer in the canonical order: it runs after every other middleware\n * has shaped the request and immediately before the underlying\n * provider call. The middleware:\n *\n * 1. Walks every text-bearing field of `ProviderRequest` (system /\n * user / assistant / tool messages, tool-call args, vendor-prefixed\n * `cache_control` spans).\n * 2. Detects `SecretValue`-shaped instances (cross-realm safe via\n * `Symbol.for('graphorin.SecretValue')`).\n * 3. Runs the configured pattern catalogue (defaults to the 14 built-in\n * patterns shared with the OTLP `RedactionValidator`).\n * 4. Applies the configured action (`'redact'` in-place by default;\n * `'throw'` for `failClosed: true` deployments).\n * 5. Strips Anthropic-shape `cache_control` markers from any span\n * that hit a pattern (default `stripCacheControlOnHit: true`).\n * 6. Emits one counter increment per detection plus a sanitised audit\n * record (audit emission is delegated to the consumer via\n * `onViolation`).\n * 7. Optionally scans streamed `text-delta` chunks for the same\n * patterns — observability-only in v0.1, no stream mutation.\n *\n * @packageDocumentation\n */\n\nimport type {\n AssistantMessage,\n LocalProviderTrust,\n Message,\n Provider,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n ToolMessage,\n UserMessage,\n} from '@graphorin/core';\nimport { SECRET_VALUE_BRAND } from '@graphorin/core';\nimport {\n BUILT_IN_PATTERNS,\n type RedactionPattern,\n} from '@graphorin/observability/redaction/patterns';\nimport { PromptRedactionError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Range of fields scanned by the middleware.\n *\n * @stable\n */\nexport type PromptRedactionScanScope = 'all' | 'untrusted' | 'secret-value-only';\n\n/**\n * Sanitized record handed to `onViolation`. Carries metadata only;\n * never the matched value.\n *\n * @stable\n */\nexport interface PromptRedactionViolation {\n readonly patternName: string;\n readonly fieldPath: string;\n readonly role?: string;\n readonly matchLength: number;\n readonly trustClass?: LocalProviderTrust;\n readonly action: 'redact' | 'throw' | 'block-and-prompt-user';\n}\n\n/**\n * Mutable per-request scrubbing context. Tracks whether any pattern\n * matched so the post-scrub `cache_control` strip can decide whether\n * to fire.\n *\n * @internal\n */\ninterface ScrubContext {\n hits: number;\n cacheControlHits: number;\n}\n\n/**\n * Full prompt-redaction policy.\n *\n * @stable\n */\nexport interface PromptRedactionPolicy {\n /** Pattern catalogue. Defaults to the 14 built-in patterns. */\n readonly patterns?: ReadonlyArray<RedactionPattern>;\n /** Action on detection. Defaults to `'redact'`. */\n readonly action?: 'redact' | 'throw' | 'block-and-prompt-user';\n /** Throw on the first hit instead of redacting in-place. */\n readonly failClosed?: boolean;\n /** Range of fields scanned. Defaults to `'all'`. */\n readonly scanScope?: PromptRedactionScanScope;\n /** Detect `SecretValue` instances anywhere in the request. */\n readonly detectSecretValue?: boolean;\n /** Strip Anthropic-shape `cache_control` markers on hit. */\n readonly stripCacheControlOnHit?: boolean;\n /** Per-trust-class override block. */\n readonly byTrustClass?: Partial<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>;\n /** Sanitised violation hook (audit emission lives downstream). */\n readonly onViolation?: (violation: PromptRedactionViolation) => void;\n /** Optional logger override. Defaults to `console.warn`. */\n readonly logger?: (message: string, meta?: object) => void;\n /** Test hook — synthetic trust class. */\n readonly trustClassOverride?: LocalProviderTrust;\n}\n\nconst TRUST_CLASS_DEFAULTS: Readonly<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>> =\n Object.freeze({\n loopback: { scanScope: 'secret-value-only' },\n private: { scanScope: 'all', failClosed: false },\n 'public-tls': { scanScope: 'all', failClosed: false },\n 'public-cleartext': {},\n });\n\n/**\n * @stable\n */\nexport const withRedaction = defineProviderMiddleware<PromptRedactionPolicy>({\n kind: 'withRedaction',\n factory: (rawPolicy: PromptRedactionPolicy) => {\n const baseLogger = rawPolicy.logger ?? defaultLogger;\n return (next: Provider): Provider => {\n const trustClass = rawPolicy.trustClassOverride ?? inferTrustClass(next.acceptsSensitivity);\n const policy = effectivePolicy(rawPolicy, trustClass);\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 const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);\n return scanStreamingResponse(next.stream(scrubbed), policy, baseLogger);\n },\n async generate(req) {\n const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);\n return next.generate(scrubbed);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\nfunction effectivePolicy(\n raw: PromptRedactionPolicy,\n trustClass: LocalProviderTrust | undefined,\n): RequiredPolicy {\n const tierDefaults = trustClass !== undefined ? TRUST_CLASS_DEFAULTS[trustClass] : {};\n const explicitOverride = trustClass !== undefined ? (raw.byTrustClass?.[trustClass] ?? {}) : {};\n const merged: RequiredPolicy = {\n patterns: ensureGlobalPatterns(\n raw.patterns ?? explicitOverride.patterns ?? tierDefaults.patterns ?? BUILT_IN_PATTERNS,\n ),\n action: raw.action ?? explicitOverride.action ?? tierDefaults.action ?? 'redact',\n failClosed: pick(raw.failClosed, explicitOverride.failClosed, tierDefaults.failClosed, false),\n scanScope: raw.scanScope ?? explicitOverride.scanScope ?? tierDefaults.scanScope ?? 'all',\n detectSecretValue: pick(\n raw.detectSecretValue,\n explicitOverride.detectSecretValue,\n tierDefaults.detectSecretValue,\n true,\n ),\n stripCacheControlOnHit: pick(\n raw.stripCacheControlOnHit,\n explicitOverride.stripCacheControlOnHit,\n tierDefaults.stripCacheControlOnHit,\n true,\n ),\n onViolation: raw.onViolation,\n };\n if (raw.failClosed === true || explicitOverride.failClosed === true) {\n merged.action = 'throw';\n }\n return merged;\n}\n\n/**\n * PS-22: callers can supply a pattern without the global flag. `.replace` /\n * `.match` then stop after the FIRST occurrence, so a second secret on the same\n * line is neither redacted nor counted. Re-compile every non-global pattern\n * with the `g` flag so all occurrences are covered. The 14 built-ins already\n * carry `/g`; this only rescues user-supplied patterns (identity otherwise).\n */\nfunction ensureGlobalPatterns(\n patterns: ReadonlyArray<RedactionPattern>,\n): ReadonlyArray<RedactionPattern> {\n let changed = false;\n const out = patterns.map((p) => {\n if (p.regex.global) return p;\n changed = true;\n return { ...p, regex: new RegExp(p.regex.source, `${p.regex.flags}g`) };\n });\n return changed ? out : patterns;\n}\n\nfunction pick<T>(...values: ReadonlyArray<T | undefined>): T {\n for (const v of values) {\n if (v !== undefined) return v as T;\n }\n // The last argument acts as the default and is always defined per\n // the call sites above.\n return values[values.length - 1] as T;\n}\n\ninterface RequiredPolicy {\n patterns: ReadonlyArray<RedactionPattern>;\n action: 'redact' | 'throw' | 'block-and-prompt-user';\n failClosed: boolean;\n scanScope: PromptRedactionScanScope;\n detectSecretValue: boolean;\n stripCacheControlOnHit: boolean;\n onViolation: ((violation: PromptRedactionViolation) => void) | undefined;\n}\n\nfunction scrubRequest(\n req: ProviderRequest,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n logger: (message: string, meta?: object) => void,\n): ProviderRequest {\n const ctx: ScrubContext = { hits: 0, cacheControlHits: 0 };\n const messages = req.messages.map((msg, i) =>\n scrubMessage(msg, policy, trustClass, `messages[${i}]`, ctx, logger),\n );\n const systemMessage =\n req.systemMessage !== undefined\n ? scrubText(req.systemMessage, 'system', 'systemMessage', policy, trustClass, ctx, logger)\n : undefined;\n let providerOptions = scrubProviderOptions(req.providerOptions, policy, trustClass, ctx, logger);\n if (\n policy.stripCacheControlOnHit &&\n ctx.hits > 0 &&\n providerOptions !== undefined &&\n hasAnthropicCacheControl(providerOptions)\n ) {\n providerOptions = stripAnthropicCacheControl(providerOptions);\n reportViolation(\n policy,\n {\n patternName: 'anthropic-cache-control-stripped',\n fieldPath: 'providerOptions.anthropic.cache_control',\n matchLength: 0,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: 'redact',\n },\n logger,\n );\n ctx.cacheControlHits++;\n }\n return {\n ...req,\n messages,\n ...(systemMessage !== undefined ? { systemMessage } : {}),\n ...(providerOptions !== undefined ? { providerOptions } : {}),\n };\n}\n\nfunction hasAnthropicCacheControl(providerOptions: Record<string, unknown>): boolean {\n const anthropic = providerOptions.anthropic;\n if (anthropic === null || typeof anthropic !== 'object') return false;\n return 'cache_control' in (anthropic as Record<string, unknown>);\n}\n\nfunction stripAnthropicCacheControl(\n providerOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const anthropic = providerOptions.anthropic;\n if (anthropic === null || typeof anthropic !== 'object') return providerOptions;\n const { cache_control: _drop, ...rest } = anthropic as Record<string, unknown>;\n void _drop;\n return { ...providerOptions, anthropic: rest };\n}\n\nfunction scrubMessage(\n msg: Message,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): Message {\n if (policy.scanScope === 'secret-value-only') {\n detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);\n return msg;\n }\n if (policy.scanScope === 'untrusted' && msg.role === 'system') {\n detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);\n return msg;\n }\n switch (msg.role) {\n case 'system':\n return {\n role: 'system',\n content: scrubText(\n msg.content,\n 'system',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n case 'user':\n return scrubUserMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n case 'assistant':\n return scrubAssistantMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n case 'tool':\n return scrubToolMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n default:\n return msg;\n }\n}\n\nfunction scrubUserMessage(\n msg: UserMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): UserMessage {\n const next: UserMessage = {\n role: 'user',\n content: scrubContent(\n msg.content,\n 'user',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as UserMessage['content'],\n ...(msg.userId !== undefined ? { userId: msg.userId } : {}),\n };\n return next;\n}\n\nfunction scrubAssistantMessage(\n msg: AssistantMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): AssistantMessage {\n const next: AssistantMessage = {\n role: 'assistant',\n content: scrubContent(\n msg.content,\n 'assistant',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as AssistantMessage['content'],\n ...(msg.agentId !== undefined ? { agentId: msg.agentId } : {}),\n };\n if (msg.toolCalls !== undefined) {\n (next as { toolCalls?: AssistantMessage['toolCalls'] }).toolCalls = msg.toolCalls.map(\n (tc, idx) => ({\n ...tc,\n args: scrubAny(\n tc.args,\n 'assistant',\n `${fieldPrefix}.toolCalls[${idx}].args`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n }),\n );\n }\n return next;\n}\n\nfunction scrubToolMessage(\n msg: ToolMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): ToolMessage {\n return {\n role: 'tool',\n toolCallId: msg.toolCallId,\n content: scrubContent(\n msg.content,\n 'tool',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as ToolMessage['content'],\n };\n}\n\nfunction scrubContent(\n content: string | ReadonlyArray<unknown>,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): string | ReadonlyArray<unknown> {\n if (typeof content === 'string') {\n return scrubText(content, role, fieldPath, policy, trustClass, ctx, logger);\n }\n return content.map((part, idx) => {\n if (typeof part === 'string') {\n return scrubText(part, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger);\n }\n if (part === null || typeof part !== 'object') return part;\n const partWithType = part as { type?: string; text?: string };\n if (partWithType.type === 'text' && typeof partWithType.text === 'string') {\n return {\n ...part,\n text: scrubText(\n partWithType.text,\n role,\n `${fieldPath}[${idx}].text`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n }\n if (partWithType.type === 'reasoning' && typeof partWithType.text === 'string') {\n return {\n ...part,\n text: scrubText(\n partWithType.text,\n role,\n `${fieldPath}[${idx}].text`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n }\n return part;\n });\n}\n\nfunction scrubText(\n text: string,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): string {\n let scrubbed = text;\n for (const pattern of policy.patterns) {\n pattern.regex.lastIndex = 0;\n if (pattern.regex.test(scrubbed)) {\n const matches = scrubbed.match(pattern.regex) ?? [];\n const matchLength = matches.reduce((sum, m) => sum + m.length, 0);\n ctx.hits++;\n reportViolation(\n policy,\n {\n patternName: pattern.name,\n fieldPath,\n role,\n matchLength,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: policy.action,\n },\n logger,\n );\n if (policy.action === 'throw' || policy.failClosed) {\n throw new PromptRedactionError({\n patternName: pattern.name,\n fieldPath,\n ...(role !== undefined ? { role } : {}),\n });\n }\n pattern.regex.lastIndex = 0;\n scrubbed = scrubbed.replace(pattern.regex, pattern.mask ?? `[REDACTED ${pattern.name}]`);\n }\n }\n return scrubbed;\n}\n\nfunction scrubAny(\n value: unknown,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): unknown {\n if (typeof value === 'string') {\n return scrubText(value, role, fieldPath, policy, trustClass, ctx, logger);\n }\n if (value === null || value === undefined) return value;\n if (Array.isArray(value)) {\n return value.map((item, idx) =>\n scrubAny(item, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger),\n );\n }\n if (typeof value === 'object') {\n if (policy.detectSecretValue && isSecretValue(value)) {\n ctx.hits++;\n reportViolation(\n policy,\n {\n patternName: 'graphorin-secret-value',\n fieldPath,\n role,\n matchLength: 0,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: policy.action,\n },\n logger,\n );\n return '[REDACTED graphorin-secret-value]';\n }\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAny(v, role, `${fieldPath}.${k}`, policy, trustClass, ctx, logger);\n }\n return out;\n }\n return value;\n}\n\nfunction scrubProviderOptions(\n providerOptions: Readonly<Record<string, unknown>> | undefined,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): Record<string, unknown> | undefined {\n if (providerOptions === undefined) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(providerOptions)) {\n out[k] = scrubAny(\n v,\n 'provider-options',\n `providerOptions.${k}`,\n policy,\n trustClass,\n ctx,\n logger,\n );\n }\n return out;\n}\n\nfunction detectSecretValuesIn(\n msg: Message,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): void {\n if (!policy.detectSecretValue) return;\n scrubAny(msg, msg.role, fieldPrefix, policy, trustClass, ctx, logger);\n}\n\nfunction reportViolation(\n policy: RequiredPolicy,\n violation: PromptRedactionViolation,\n logger: (message: string, meta?: object) => void,\n): void {\n policy.onViolation?.(violation);\n logger(\n `[graphorin/provider] withRedaction hit '${violation.patternName}' at '${violation.fieldPath}' (action: ${violation.action})`,\n {\n patternName: violation.patternName,\n fieldPath: violation.fieldPath,\n role: violation.role,\n matchLength: violation.matchLength,\n trustClass: violation.trustClass,\n },\n );\n}\n\nfunction isSecretValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n return (value as Record<string | symbol, unknown>)[SECRET_VALUE_BRAND] === true;\n}\n\nfunction inferTrustClass(\n acceptsSensitivity: ReadonlyArray<Sensitivity> | undefined,\n): LocalProviderTrust | undefined {\n if (acceptsSensitivity === undefined) return undefined;\n const set = new Set<Sensitivity>(acceptsSensitivity);\n if (set.has('secret')) return 'loopback';\n if (set.has('internal')) return 'private';\n if (set.has('public')) return 'public-tls';\n return undefined;\n}\n\nasync function* scanStreamingResponse(\n source: AsyncIterable<ProviderEvent>,\n policy: RequiredPolicy,\n logger: (message: string, meta?: object) => void,\n): AsyncIterable<ProviderEvent> {\n // PS-22: secrets can straddle text-delta boundaries. Keep a bounded tail of\n // recently-streamed text and scan `tail + delta`, reporting only matches that\n // reach into the freshly-arrived delta so a prior window's matches are not\n // double-counted. The tail is capped (no unbounded accumulation); a single\n // match longer than the cap can still slip a boundary (best-effort).\n const TAIL_WINDOW = 512;\n let tail = '';\n for await (const event of source) {\n if (event.type === 'text-delta' && policy.scanScope === 'all') {\n // Observability-only — match patterns and emit synthetic violation rows;\n // do NOT mutate the stream content (mid-stream mutation would break\n // structured-output / tool-call parsing).\n const haystack = tail + event.delta;\n for (const pattern of policy.patterns) {\n pattern.regex.lastIndex = 0;\n let newMatchLength = 0;\n let match = pattern.regex.exec(haystack);\n while (match !== null) {\n const endIndex = match.index + match[0].length;\n // A match wholly inside `tail` was reported on the prior delta.\n if (match[0].length > 0 && endIndex > tail.length) {\n newMatchLength += match[0].length;\n }\n if (!pattern.regex.global) break;\n if (match[0].length === 0) pattern.regex.lastIndex += 1;\n match = pattern.regex.exec(haystack);\n }\n if (newMatchLength > 0) {\n reportViolation(\n policy,\n {\n patternName: pattern.name,\n fieldPath: 'response.text-delta',\n role: 'assistant',\n matchLength: newMatchLength,\n action: 'redact',\n },\n logger,\n );\n }\n }\n tail =\n haystack.length > TAIL_WINDOW ? haystack.slice(haystack.length - TAIL_WINDOW) : haystack;\n }\n yield event;\n }\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// Re-export to silence \"declared but unused\" lints for downstream\n// consumers that import only the types.\nexport type _RedactionResponseGuard = ProviderResponse;\n"],"mappings":"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"}
1
+ {"version":3,"file":"with-redaction.js","names":["TRUST_CLASS_DEFAULTS: Readonly<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>","merged: RequiredPolicy","ctx: ScrubContext","next: AssistantMessage","out: Record<string, unknown>"],"sources":["../../src/middleware/with-redaction.ts"],"sourcesContent":["/**\n * `withRedaction` - outbound prompt-redaction middleware. Innermost\n * layer in the canonical order: it runs after every other middleware\n * has shaped the request and immediately before the underlying\n * provider call. The middleware:\n *\n * 1. Walks every text-bearing field of `ProviderRequest` (system /\n * user / assistant / tool messages, tool-call args, vendor-prefixed\n * `cache_control` spans).\n * 2. Detects `SecretValue`-shaped instances (cross-realm safe via\n * `Symbol.for('graphorin.SecretValue')`).\n * 3. Runs the configured pattern catalogue (defaults to the 14 built-in\n * patterns shared with the OTLP `RedactionValidator`).\n * 4. Applies the configured action (`'redact'` in-place by default;\n * `'throw'` for `failClosed: true` deployments).\n * 5. Strips Anthropic-shape `cache_control` markers from any span\n * that hit a pattern (default `stripCacheControlOnHit: true`).\n * 6. Emits one counter increment per detection plus a sanitised audit\n * record (audit emission is delegated to the consumer via\n * `onViolation`).\n * 7. Optionally scans streamed `text-delta` chunks for the same\n * patterns - observability-only in v0.1, no stream mutation.\n *\n * @packageDocumentation\n */\n\nimport type {\n AssistantMessage,\n LocalProviderTrust,\n Message,\n Provider,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n ToolMessage,\n UserMessage,\n} from '@graphorin/core';\nimport { SECRET_VALUE_BRAND } from '@graphorin/core';\nimport {\n BUILT_IN_PATTERNS,\n type RedactionPattern,\n} from '@graphorin/observability/redaction/patterns';\nimport { PromptRedactionError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Range of fields scanned by the middleware.\n *\n * @stable\n */\nexport type PromptRedactionScanScope = 'all' | 'untrusted' | 'secret-value-only';\n\n/**\n * Sanitized record handed to `onViolation`. Carries metadata only;\n * never the matched value.\n *\n * @stable\n */\nexport interface PromptRedactionViolation {\n readonly patternName: string;\n readonly fieldPath: string;\n readonly role?: string;\n readonly matchLength: number;\n readonly trustClass?: LocalProviderTrust;\n readonly action: 'redact' | 'throw' | 'block-and-prompt-user';\n}\n\n/**\n * Mutable per-request scrubbing context. Tracks whether any pattern\n * matched so the post-scrub `cache_control` strip can decide whether\n * to fire.\n *\n * @internal\n */\ninterface ScrubContext {\n hits: number;\n cacheControlHits: number;\n}\n\n/**\n * Full prompt-redaction policy.\n *\n * @stable\n */\nexport interface PromptRedactionPolicy {\n /** Pattern catalogue. Defaults to the 14 built-in patterns. */\n readonly patterns?: ReadonlyArray<RedactionPattern>;\n /** Action on detection. Defaults to `'redact'`. */\n readonly action?: 'redact' | 'throw' | 'block-and-prompt-user';\n /** Throw on the first hit instead of redacting in-place. */\n readonly failClosed?: boolean;\n /** Range of fields scanned. Defaults to `'all'`. */\n readonly scanScope?: PromptRedactionScanScope;\n /** Detect `SecretValue` instances anywhere in the request. */\n readonly detectSecretValue?: boolean;\n /** Strip Anthropic-shape `cache_control` markers on hit. */\n readonly stripCacheControlOnHit?: boolean;\n /** Per-trust-class override block. */\n readonly byTrustClass?: Partial<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>;\n /** Sanitised violation hook (audit emission lives downstream). */\n readonly onViolation?: (violation: PromptRedactionViolation) => void;\n /** Optional logger override. Defaults to `console.warn`. */\n readonly logger?: (message: string, meta?: object) => void;\n /** Test hook - synthetic trust class. */\n readonly trustClassOverride?: LocalProviderTrust;\n}\n\nconst TRUST_CLASS_DEFAULTS: Readonly<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>> =\n Object.freeze({\n loopback: { scanScope: 'secret-value-only' },\n private: { scanScope: 'all', failClosed: false },\n 'public-tls': { scanScope: 'all', failClosed: false },\n 'public-cleartext': {},\n });\n\n/**\n * @stable\n */\nexport const withRedaction = defineProviderMiddleware<PromptRedactionPolicy>({\n kind: 'withRedaction',\n factory: (rawPolicy: PromptRedactionPolicy) => {\n const baseLogger = rawPolicy.logger ?? defaultLogger;\n return (next: Provider): Provider => {\n const trustClass = rawPolicy.trustClassOverride ?? inferTrustClass(next.acceptsSensitivity);\n const policy = effectivePolicy(rawPolicy, trustClass);\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 const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);\n return scanStreamingResponse(next.stream(scrubbed), policy, baseLogger);\n },\n async generate(req) {\n const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);\n return next.generate(scrubbed);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\nfunction effectivePolicy(\n raw: PromptRedactionPolicy,\n trustClass: LocalProviderTrust | undefined,\n): RequiredPolicy {\n const tierDefaults = trustClass !== undefined ? TRUST_CLASS_DEFAULTS[trustClass] : {};\n const explicitOverride = trustClass !== undefined ? (raw.byTrustClass?.[trustClass] ?? {}) : {};\n const merged: RequiredPolicy = {\n patterns: ensureGlobalPatterns(\n raw.patterns ?? explicitOverride.patterns ?? tierDefaults.patterns ?? BUILT_IN_PATTERNS,\n ),\n action: raw.action ?? explicitOverride.action ?? tierDefaults.action ?? 'redact',\n failClosed: pick(raw.failClosed, explicitOverride.failClosed, tierDefaults.failClosed, false),\n scanScope: raw.scanScope ?? explicitOverride.scanScope ?? tierDefaults.scanScope ?? 'all',\n detectSecretValue: pick(\n raw.detectSecretValue,\n explicitOverride.detectSecretValue,\n tierDefaults.detectSecretValue,\n true,\n ),\n stripCacheControlOnHit: pick(\n raw.stripCacheControlOnHit,\n explicitOverride.stripCacheControlOnHit,\n tierDefaults.stripCacheControlOnHit,\n true,\n ),\n onViolation: raw.onViolation,\n };\n if (raw.failClosed === true || explicitOverride.failClosed === true) {\n merged.action = 'throw';\n }\n return merged;\n}\n\n/**\n * PS-22: callers can supply a pattern without the global flag. `.replace` /\n * `.match` then stop after the FIRST occurrence, so a second secret on the same\n * line is neither redacted nor counted. Re-compile every non-global pattern\n * with the `g` flag so all occurrences are covered. The 14 built-ins already\n * carry `/g`; this only rescues user-supplied patterns (identity otherwise).\n */\nfunction ensureGlobalPatterns(\n patterns: ReadonlyArray<RedactionPattern>,\n): ReadonlyArray<RedactionPattern> {\n let changed = false;\n const out = patterns.map((p) => {\n if (p.regex.global) return p;\n changed = true;\n return { ...p, regex: new RegExp(p.regex.source, `${p.regex.flags}g`) };\n });\n return changed ? out : patterns;\n}\n\nfunction pick<T>(...values: ReadonlyArray<T | undefined>): T {\n for (const v of values) {\n if (v !== undefined) return v as T;\n }\n // The last argument acts as the default and is always defined per\n // the call sites above.\n return values[values.length - 1] as T;\n}\n\ninterface RequiredPolicy {\n patterns: ReadonlyArray<RedactionPattern>;\n action: 'redact' | 'throw' | 'block-and-prompt-user';\n failClosed: boolean;\n scanScope: PromptRedactionScanScope;\n detectSecretValue: boolean;\n stripCacheControlOnHit: boolean;\n onViolation: ((violation: PromptRedactionViolation) => void) | undefined;\n}\n\nfunction scrubRequest(\n req: ProviderRequest,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n logger: (message: string, meta?: object) => void,\n): ProviderRequest {\n const ctx: ScrubContext = { hits: 0, cacheControlHits: 0 };\n const messages = req.messages.map((msg, i) =>\n scrubMessage(msg, policy, trustClass, `messages[${i}]`, ctx, logger),\n );\n const systemMessage =\n req.systemMessage !== undefined\n ? scrubText(req.systemMessage, 'system', 'systemMessage', policy, trustClass, ctx, logger)\n : undefined;\n let providerOptions = scrubProviderOptions(req.providerOptions, policy, trustClass, ctx, logger);\n if (\n policy.stripCacheControlOnHit &&\n ctx.hits > 0 &&\n providerOptions !== undefined &&\n hasAnthropicCacheControl(providerOptions)\n ) {\n providerOptions = stripAnthropicCacheControl(providerOptions);\n reportViolation(\n policy,\n {\n patternName: 'anthropic-cache-control-stripped',\n fieldPath: 'providerOptions.anthropic.cache_control',\n matchLength: 0,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: 'redact',\n },\n logger,\n );\n ctx.cacheControlHits++;\n }\n return {\n ...req,\n messages,\n ...(systemMessage !== undefined ? { systemMessage } : {}),\n ...(providerOptions !== undefined ? { providerOptions } : {}),\n };\n}\n\nfunction hasAnthropicCacheControl(providerOptions: Record<string, unknown>): boolean {\n const anthropic = providerOptions.anthropic;\n if (anthropic === null || typeof anthropic !== 'object') return false;\n return 'cache_control' in (anthropic as Record<string, unknown>);\n}\n\nfunction stripAnthropicCacheControl(\n providerOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const anthropic = providerOptions.anthropic;\n if (anthropic === null || typeof anthropic !== 'object') return providerOptions;\n const { cache_control: _drop, ...rest } = anthropic as Record<string, unknown>;\n void _drop;\n return { ...providerOptions, anthropic: rest };\n}\n\nfunction scrubMessage(\n msg: Message,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): Message {\n if (policy.scanScope === 'secret-value-only') {\n detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);\n return msg;\n }\n if (policy.scanScope === 'untrusted' && msg.role === 'system') {\n detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);\n return msg;\n }\n switch (msg.role) {\n case 'system':\n return {\n role: 'system',\n content: scrubText(\n msg.content,\n 'system',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n case 'user':\n return scrubUserMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n case 'assistant':\n return scrubAssistantMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n case 'tool':\n return scrubToolMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n default:\n return msg;\n }\n}\n\nfunction scrubUserMessage(\n msg: UserMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): UserMessage {\n const next: UserMessage = {\n role: 'user',\n content: scrubContent(\n msg.content,\n 'user',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as UserMessage['content'],\n ...(msg.userId !== undefined ? { userId: msg.userId } : {}),\n };\n return next;\n}\n\nfunction scrubAssistantMessage(\n msg: AssistantMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): AssistantMessage {\n const next: AssistantMessage = {\n role: 'assistant',\n content: scrubContent(\n msg.content,\n 'assistant',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as AssistantMessage['content'],\n ...(msg.agentId !== undefined ? { agentId: msg.agentId } : {}),\n };\n if (msg.toolCalls !== undefined) {\n (next as { toolCalls?: AssistantMessage['toolCalls'] }).toolCalls = msg.toolCalls.map(\n (tc, idx) => ({\n ...tc,\n args: scrubAny(\n tc.args,\n 'assistant',\n `${fieldPrefix}.toolCalls[${idx}].args`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n }),\n );\n }\n return next;\n}\n\nfunction scrubToolMessage(\n msg: ToolMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): ToolMessage {\n return {\n role: 'tool',\n toolCallId: msg.toolCallId,\n content: scrubContent(\n msg.content,\n 'tool',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as ToolMessage['content'],\n };\n}\n\nfunction scrubContent(\n content: string | ReadonlyArray<unknown>,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): string | ReadonlyArray<unknown> {\n if (typeof content === 'string') {\n return scrubText(content, role, fieldPath, policy, trustClass, ctx, logger);\n }\n return content.map((part, idx) => {\n if (typeof part === 'string') {\n return scrubText(part, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger);\n }\n if (part === null || typeof part !== 'object') return part;\n const partWithType = part as { type?: string; text?: string };\n if (partWithType.type === 'text' && typeof partWithType.text === 'string') {\n return {\n ...part,\n text: scrubText(\n partWithType.text,\n role,\n `${fieldPath}[${idx}].text`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n }\n if (partWithType.type === 'reasoning' && typeof partWithType.text === 'string') {\n return {\n ...part,\n text: scrubText(\n partWithType.text,\n role,\n `${fieldPath}[${idx}].text`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n }\n return part;\n });\n}\n\nfunction scrubText(\n text: string,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): string {\n let scrubbed = text;\n for (const pattern of policy.patterns) {\n pattern.regex.lastIndex = 0;\n if (pattern.regex.test(scrubbed)) {\n const matches = scrubbed.match(pattern.regex) ?? [];\n const matchLength = matches.reduce((sum, m) => sum + m.length, 0);\n ctx.hits++;\n reportViolation(\n policy,\n {\n patternName: pattern.name,\n fieldPath,\n role,\n matchLength,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: policy.action,\n },\n logger,\n );\n if (policy.action === 'throw' || policy.failClosed) {\n throw new PromptRedactionError({\n patternName: pattern.name,\n fieldPath,\n ...(role !== undefined ? { role } : {}),\n });\n }\n pattern.regex.lastIndex = 0;\n scrubbed = scrubbed.replace(pattern.regex, pattern.mask ?? `[REDACTED ${pattern.name}]`);\n }\n }\n return scrubbed;\n}\n\nfunction scrubAny(\n value: unknown,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): unknown {\n if (typeof value === 'string') {\n return scrubText(value, role, fieldPath, policy, trustClass, ctx, logger);\n }\n if (value === null || value === undefined) return value;\n if (Array.isArray(value)) {\n return value.map((item, idx) =>\n scrubAny(item, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger),\n );\n }\n if (typeof value === 'object') {\n if (policy.detectSecretValue && isSecretValue(value)) {\n ctx.hits++;\n reportViolation(\n policy,\n {\n patternName: 'graphorin-secret-value',\n fieldPath,\n role,\n matchLength: 0,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: policy.action,\n },\n logger,\n );\n return '[REDACTED graphorin-secret-value]';\n }\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAny(v, role, `${fieldPath}.${k}`, policy, trustClass, ctx, logger);\n }\n return out;\n }\n return value;\n}\n\nfunction scrubProviderOptions(\n providerOptions: Readonly<Record<string, unknown>> | undefined,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): Record<string, unknown> | undefined {\n if (providerOptions === undefined) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(providerOptions)) {\n out[k] = scrubAny(\n v,\n 'provider-options',\n `providerOptions.${k}`,\n policy,\n trustClass,\n ctx,\n logger,\n );\n }\n return out;\n}\n\nfunction detectSecretValuesIn(\n msg: Message,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): void {\n if (!policy.detectSecretValue) return;\n scrubAny(msg, msg.role, fieldPrefix, policy, trustClass, ctx, logger);\n}\n\nfunction reportViolation(\n policy: RequiredPolicy,\n violation: PromptRedactionViolation,\n logger: (message: string, meta?: object) => void,\n): void {\n policy.onViolation?.(violation);\n logger(\n `[graphorin/provider] withRedaction hit '${violation.patternName}' at '${violation.fieldPath}' (action: ${violation.action})`,\n {\n patternName: violation.patternName,\n fieldPath: violation.fieldPath,\n role: violation.role,\n matchLength: violation.matchLength,\n trustClass: violation.trustClass,\n },\n );\n}\n\nfunction isSecretValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n return (value as Record<string | symbol, unknown>)[SECRET_VALUE_BRAND] === true;\n}\n\nfunction inferTrustClass(\n acceptsSensitivity: ReadonlyArray<Sensitivity> | undefined,\n): LocalProviderTrust | undefined {\n if (acceptsSensitivity === undefined) return undefined;\n const set = new Set<Sensitivity>(acceptsSensitivity);\n if (set.has('secret')) return 'loopback';\n if (set.has('internal')) return 'private';\n if (set.has('public')) return 'public-tls';\n return undefined;\n}\n\nasync function* scanStreamingResponse(\n source: AsyncIterable<ProviderEvent>,\n policy: RequiredPolicy,\n logger: (message: string, meta?: object) => void,\n): AsyncIterable<ProviderEvent> {\n // PS-22: secrets can straddle text-delta boundaries. Keep a bounded tail of\n // recently-streamed text and scan `tail + delta`, reporting only matches that\n // reach into the freshly-arrived delta so a prior window's matches are not\n // double-counted. The tail is capped (no unbounded accumulation); a single\n // match longer than the cap can still slip a boundary (best-effort).\n const TAIL_WINDOW = 512;\n let tail = '';\n for await (const event of source) {\n if (event.type === 'text-delta' && policy.scanScope === 'all') {\n // Observability-only - match patterns and emit synthetic violation rows;\n // do NOT mutate the stream content (mid-stream mutation would break\n // structured-output / tool-call parsing).\n const haystack = tail + event.delta;\n for (const pattern of policy.patterns) {\n pattern.regex.lastIndex = 0;\n let newMatchLength = 0;\n let match = pattern.regex.exec(haystack);\n while (match !== null) {\n const endIndex = match.index + match[0].length;\n // A match wholly inside `tail` was reported on the prior delta.\n if (match[0].length > 0 && endIndex > tail.length) {\n newMatchLength += match[0].length;\n }\n if (!pattern.regex.global) break;\n if (match[0].length === 0) pattern.regex.lastIndex += 1;\n match = pattern.regex.exec(haystack);\n }\n if (newMatchLength > 0) {\n reportViolation(\n policy,\n {\n patternName: pattern.name,\n fieldPath: 'response.text-delta',\n role: 'assistant',\n matchLength: newMatchLength,\n action: 'redact',\n },\n logger,\n );\n }\n }\n tail =\n haystack.length > TAIL_WINDOW ? haystack.slice(haystack.length - TAIL_WINDOW) : haystack;\n }\n yield event;\n }\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// Re-export to silence \"declared but unused\" lints for downstream\n// consumers that import only the types.\nexport type _RedactionResponseGuard = ProviderResponse;\n"],"mappings":"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"}
@@ -104,7 +104,9 @@ function defaultRetryable(err) {
104
104
  if (err === null || typeof err !== "object") return false;
105
105
  if (isAbortError(err)) return false;
106
106
  const e = err;
107
- if (e.kind === "transient" || e.kind === "rate-limit" || e.kind === "rate-limit-exceeded" || e.kind === "capacity") return true;
107
+ const kinds = [e.kind, e.errorKind];
108
+ if (kinds.includes("transient") || kinds.includes("rate-limit") || kinds.includes("rate-limit-exceeded") || kinds.includes("capacity")) return true;
109
+ if (kinds.includes("unauthorized") || kinds.includes("invalid-request") || kinds.includes("context-length") || kinds.includes("content-filter")) return false;
108
110
  if (typeof e.status === "number" && e.status === 429) return true;
109
111
  if (typeof e.status === "number" && e.status >= 500 && e.status < 600) return true;
110
112
  if (typeof e.status === "number" && e.status === 0) return true;
@@ -1 +1 @@
1
- {"version":3,"file":"with-retry.js","names":["delay: number"],"sources":["../../src/middleware/with-retry.ts"],"sourcesContent":["/**\n * `withRetry` — retry idempotent failures (5xx, network, timeouts)\n * with exponential backoff + jitter. Lives outside `withRateLimit`\n * per the canonical order so retried requests respect the rate-limit\n * bucket of the next attempt.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderRequest, ProviderResponse } from '@graphorin/core';\n\nimport { isAbortError } from '../internal/abort.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withRetry}.\n *\n * @stable\n */\nexport interface WithRetryOptions {\n readonly maxRetries?: number;\n readonly backoff?: 'exponential' | 'linear' | 'constant';\n readonly initialDelayMs?: number;\n readonly maxDelayMs?: number;\n readonly jitter?: boolean;\n readonly retryableErrors?: (err: unknown) => boolean;\n /** Optional sleep override (test fixtures use a synchronous resolver). */\n readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}\n\n/**\n * @stable\n */\nexport const withRetry = defineProviderMiddleware<WithRetryOptions>({\n kind: 'withRetry',\n factory: (opts: WithRetryOptions) => {\n const maxRetries = opts.maxRetries ?? 3;\n const backoff = opts.backoff ?? 'exponential';\n const initialDelay = opts.initialDelayMs ?? 250;\n const maxDelay = opts.maxDelayMs ?? 30_000;\n const jitter = opts.jitter ?? true;\n const isRetryable = opts.retryableErrors ?? defaultRetryable;\n const sleep = opts.sleepImpl ?? defaultSleep;\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 retryingStream(\n next,\n req,\n maxRetries,\n backoff,\n initialDelay,\n maxDelay,\n jitter,\n isRetryable,\n sleep,\n );\n },\n async generate(req) {\n let attempt = 0;\n for (;;) {\n try {\n return await next.generate(req);\n } catch (err) {\n if (req.signal?.aborted) throw err;\n if (attempt >= maxRetries) throw err;\n if (!isRetryable(err)) throw err;\n const hint = readRetryAfter(err);\n const delay =\n hint !== null\n ? Math.min(hint, maxDelay)\n : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter);\n await sleep(delay, req.signal);\n attempt++;\n }\n }\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* retryingStream(\n next: Provider,\n req: ProviderRequest,\n maxRetries: number,\n backoff: 'exponential' | 'linear' | 'constant',\n initialDelay: number,\n maxDelay: number,\n jitter: boolean,\n isRetryable: (err: unknown) => boolean,\n sleep: (ms: number, signal?: AbortSignal) => Promise<void>,\n): AsyncIterable<ProviderEvent> {\n let attempt = 0;\n // Stream-level (not per-attempt): once *any* event has reached the\n // consumer, a retryable failure mid-stream must NOT restart the stream —\n // that would replay stream-start + already-delivered text/tool-call events\n // into the same iteration (duplicate output, potential double tool execution).\n // `withFallback` upholds the same invariant. Pre-yield failures still retry.\n let yieldedAny = false;\n for (;;) {\n try {\n for await (const event of next.stream(req)) {\n yieldedAny = true;\n yield event;\n }\n if (yieldedAny) return;\n // Empty stream — surface a finish so callers do not hang.\n yield {\n type: 'finish',\n finishReason: 'stop',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n };\n return;\n } catch (err) {\n if (req.signal?.aborted) throw err;\n // PS-1: never restart a stream that already emitted events.\n if (yieldedAny) throw err;\n if (attempt >= maxRetries) throw err;\n if (!isRetryable(err)) throw err;\n const hint = readRetryAfter(err);\n const delay =\n hint !== null\n ? Math.min(hint, maxDelay)\n : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter);\n await sleep(delay, req.signal);\n attempt++;\n }\n }\n}\n\n/**\n * Read a `Retry-After` hint from a thrown error. Recognises:\n *\n * - `RateLimitExceededError`-shaped errors carrying a `retryAfterMs`\n * field (already milliseconds).\n * - HTTP-shaped errors carrying a `headers['retry-after']` value\n * (numeric seconds; we convert to milliseconds).\n * - HTTP-shaped errors carrying a `retryAfterSeconds` numeric field.\n *\n * Returns the resolved delay in milliseconds or `null` when no hint\n * is available.\n *\n * @internal\n */\nfunction readRetryAfter(err: unknown): number | null {\n if (err === null || typeof err !== 'object') return null;\n const e = err as {\n retryAfterMs?: number;\n retryAfterSeconds?: number;\n headers?: Record<string, string | undefined>;\n };\n if (\n typeof e.retryAfterMs === 'number' &&\n Number.isFinite(e.retryAfterMs) &&\n e.retryAfterMs >= 0\n ) {\n return e.retryAfterMs;\n }\n if (\n typeof e.retryAfterSeconds === 'number' &&\n Number.isFinite(e.retryAfterSeconds) &&\n e.retryAfterSeconds >= 0\n ) {\n return Math.round(e.retryAfterSeconds * 1000);\n }\n const headerValue =\n e.headers?.['retry-after'] ?? e.headers?.['Retry-After'] ?? e.headers?.['RETRY-AFTER'];\n if (typeof headerValue === 'string' && headerValue.length > 0) {\n const seconds = Number(headerValue);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const httpDateMs = Date.parse(headerValue);\n if (Number.isFinite(httpDateMs)) {\n const delta = httpDateMs - Date.now();\n return delta > 0 ? delta : 0;\n }\n }\n return null;\n}\n\nfunction defaultRetryable(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false;\n // An aborted request is never retryable, even when it surfaces as a\n // `status: 0` network error (PS-2). The retry loop also short-circuits on\n // `req.signal?.aborted`, but the predicate must exclude abort independently\n // so an internally-aborted call is not retried.\n if (isAbortError(err)) return false;\n const e = err as { kind?: string; status?: number; name?: string };\n if (\n e.kind === 'transient' ||\n e.kind === 'rate-limit' ||\n e.kind === 'rate-limit-exceeded' ||\n e.kind === 'capacity'\n ) {\n return true;\n }\n if (typeof e.status === 'number' && e.status === 429) return true;\n if (typeof e.status === 'number' && e.status >= 500 && e.status < 600) return true;\n // PS-2: a `ProviderHttpError{ status: 0 }` is a fetch-level network failure\n // (ECONNREFUSED, DNS, connection reset) — exactly the transient class\n // `withRetry` documents. Retry it (abort already excluded above).\n if (typeof e.status === 'number' && e.status === 0) return true;\n return false;\n}\n\nfunction computeDelay(\n backoff: 'exponential' | 'linear' | 'constant',\n initial: number,\n attempt: number,\n maxDelay: number,\n jitter: boolean,\n): number {\n let delay: number;\n switch (backoff) {\n case 'exponential':\n delay = initial * 2 ** attempt;\n break;\n case 'linear':\n delay = initial * (attempt + 1);\n break;\n default:\n delay = initial;\n break;\n }\n delay = Math.min(delay, maxDelay);\n if (jitter) {\n const factor = 0.5 + Math.random() * 0.5; // 0.5..1.0\n delay = Math.floor(delay * factor);\n }\n return delay;\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\n// Cast for return-type compatibility with the discriminated union in\n// `Provider.generate(...)` (TypeScript has no concept of \"exhaustive\n// retry loop\" so the empty exit path is unreachable).\nexport type _RetryProviderResponseGuard = ProviderResponse;\n"],"mappings":"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"}
1
+ {"version":3,"file":"with-retry.js","names":["delay: number"],"sources":["../../src/middleware/with-retry.ts"],"sourcesContent":["/**\n * `withRetry` - retry idempotent failures (5xx, network, timeouts)\n * with exponential backoff + jitter. Lives outside `withRateLimit`\n * per the canonical order so retried requests respect the rate-limit\n * bucket of the next attempt.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderRequest, ProviderResponse } from '@graphorin/core';\n\nimport { isAbortError } from '../internal/abort.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withRetry}.\n *\n * @stable\n */\nexport interface WithRetryOptions {\n readonly maxRetries?: number;\n readonly backoff?: 'exponential' | 'linear' | 'constant';\n readonly initialDelayMs?: number;\n readonly maxDelayMs?: number;\n readonly jitter?: boolean;\n readonly retryableErrors?: (err: unknown) => boolean;\n /** Optional sleep override (test fixtures use a synchronous resolver). */\n readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}\n\n/**\n * @stable\n */\nexport const withRetry = defineProviderMiddleware<WithRetryOptions>({\n kind: 'withRetry',\n factory: (opts: WithRetryOptions) => {\n const maxRetries = opts.maxRetries ?? 3;\n const backoff = opts.backoff ?? 'exponential';\n const initialDelay = opts.initialDelayMs ?? 250;\n const maxDelay = opts.maxDelayMs ?? 30_000;\n const jitter = opts.jitter ?? true;\n const isRetryable = opts.retryableErrors ?? defaultRetryable;\n const sleep = opts.sleepImpl ?? defaultSleep;\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 retryingStream(\n next,\n req,\n maxRetries,\n backoff,\n initialDelay,\n maxDelay,\n jitter,\n isRetryable,\n sleep,\n );\n },\n async generate(req) {\n let attempt = 0;\n for (;;) {\n try {\n return await next.generate(req);\n } catch (err) {\n if (req.signal?.aborted) throw err;\n if (attempt >= maxRetries) throw err;\n if (!isRetryable(err)) throw err;\n const hint = readRetryAfter(err);\n const delay =\n hint !== null\n ? Math.min(hint, maxDelay)\n : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter);\n await sleep(delay, req.signal);\n attempt++;\n }\n }\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* retryingStream(\n next: Provider,\n req: ProviderRequest,\n maxRetries: number,\n backoff: 'exponential' | 'linear' | 'constant',\n initialDelay: number,\n maxDelay: number,\n jitter: boolean,\n isRetryable: (err: unknown) => boolean,\n sleep: (ms: number, signal?: AbortSignal) => Promise<void>,\n): AsyncIterable<ProviderEvent> {\n let attempt = 0;\n // Stream-level (not per-attempt): once *any* event has reached the\n // consumer, a retryable failure mid-stream must NOT restart the stream -\n // that would replay stream-start + already-delivered text/tool-call events\n // into the same iteration (duplicate output, potential double tool execution).\n // `withFallback` upholds the same invariant. Pre-yield failures still retry.\n let yieldedAny = false;\n for (;;) {\n try {\n for await (const event of next.stream(req)) {\n yieldedAny = true;\n yield event;\n }\n if (yieldedAny) return;\n // Empty stream - surface a finish so callers do not hang.\n yield {\n type: 'finish',\n finishReason: 'stop',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n };\n return;\n } catch (err) {\n if (req.signal?.aborted) throw err;\n // PS-1: never restart a stream that already emitted events.\n if (yieldedAny) throw err;\n if (attempt >= maxRetries) throw err;\n if (!isRetryable(err)) throw err;\n const hint = readRetryAfter(err);\n const delay =\n hint !== null\n ? Math.min(hint, maxDelay)\n : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter);\n await sleep(delay, req.signal);\n attempt++;\n }\n }\n}\n\n/**\n * Read a `Retry-After` hint from a thrown error. Recognises:\n *\n * - `RateLimitExceededError`-shaped errors carrying a `retryAfterMs`\n * field (already milliseconds).\n * - HTTP-shaped errors carrying a `headers['retry-after']` value\n * (numeric seconds; we convert to milliseconds).\n * - HTTP-shaped errors carrying a `retryAfterSeconds` numeric field.\n *\n * Returns the resolved delay in milliseconds or `null` when no hint\n * is available.\n *\n * @internal\n */\nfunction readRetryAfter(err: unknown): number | null {\n if (err === null || typeof err !== 'object') return null;\n const e = err as {\n retryAfterMs?: number;\n retryAfterSeconds?: number;\n headers?: Record<string, string | undefined>;\n };\n if (\n typeof e.retryAfterMs === 'number' &&\n Number.isFinite(e.retryAfterMs) &&\n e.retryAfterMs >= 0\n ) {\n return e.retryAfterMs;\n }\n if (\n typeof e.retryAfterSeconds === 'number' &&\n Number.isFinite(e.retryAfterSeconds) &&\n e.retryAfterSeconds >= 0\n ) {\n return Math.round(e.retryAfterSeconds * 1000);\n }\n const headerValue =\n e.headers?.['retry-after'] ?? e.headers?.['Retry-After'] ?? e.headers?.['RETRY-AFTER'];\n if (typeof headerValue === 'string' && headerValue.length > 0) {\n const seconds = Number(headerValue);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const httpDateMs = Date.parse(headerValue);\n if (Number.isFinite(httpDateMs)) {\n const delta = httpDateMs - Date.now();\n return delta > 0 ? delta : 0;\n }\n }\n return null;\n}\n\nfunction defaultRetryable(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false;\n // An aborted request is never retryable, even when it surfaces as a\n // `status: 0` network error (PS-2). The retry loop also short-circuits on\n // `req.signal?.aborted`, but the predicate must exclude abort independently\n // so an internally-aborted call is not retried.\n if (isAbortError(err)) return false;\n const e = err as { kind?: string; errorKind?: string; status?: number; name?: string };\n // `ProviderHttpError.kind` is always 'provider-http'; the canonical\n // mapped kind rides on `errorKind` - consult both.\n const kinds = [e.kind, e.errorKind];\n if (\n kinds.includes('transient') ||\n kinds.includes('rate-limit') ||\n kinds.includes('rate-limit-exceeded') ||\n kinds.includes('capacity')\n ) {\n return true;\n }\n if (\n kinds.includes('unauthorized') ||\n kinds.includes('invalid-request') ||\n kinds.includes('context-length') ||\n kinds.includes('content-filter')\n ) {\n return false;\n }\n if (typeof e.status === 'number' && e.status === 429) return true;\n if (typeof e.status === 'number' && e.status >= 500 && e.status < 600) return true;\n // PS-2: a `ProviderHttpError{ status: 0 }` is a fetch-level network failure\n // (ECONNREFUSED, DNS, connection reset) - exactly the transient class\n // `withRetry` documents. Retry it (abort already excluded above).\n if (typeof e.status === 'number' && e.status === 0) return true;\n return false;\n}\n\nfunction computeDelay(\n backoff: 'exponential' | 'linear' | 'constant',\n initial: number,\n attempt: number,\n maxDelay: number,\n jitter: boolean,\n): number {\n let delay: number;\n switch (backoff) {\n case 'exponential':\n delay = initial * 2 ** attempt;\n break;\n case 'linear':\n delay = initial * (attempt + 1);\n break;\n default:\n delay = initial;\n break;\n }\n delay = Math.min(delay, maxDelay);\n if (jitter) {\n const factor = 0.5 + Math.random() * 0.5; // 0.5..1.0\n delay = Math.floor(delay * factor);\n }\n return delay;\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\n// Cast for return-type compatibility with the discriminated union in\n// `Provider.generate(...)` (TypeScript has no concept of \"exhaustive\n// retry loop\" so the empty exit path is unreachable).\nexport type _RetryProviderResponseGuard = ProviderResponse;\n"],"mappings":"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"}
@@ -22,9 +22,20 @@ const withTracing = defineProviderMiddleware({
22
22
  type: "provider.generate",
23
23
  attrs: {
24
24
  "graphorin.provider.id": next.name,
25
- "graphorin.provider.model": next.modelId
26
- }
27
- }, async () => next.generate(req));
25
+ "graphorin.provider.model": next.modelId,
26
+ "gen_ai.operation.name": "chat",
27
+ "gen_ai.provider.name": next.name,
28
+ "gen_ai.request.model": next.modelId
29
+ },
30
+ ...req.parentSpan !== void 0 ? { parent: req.parentSpan } : {}
31
+ }, async (span) => {
32
+ const response = await next.generate(req);
33
+ span.setAttributes({
34
+ "gen_ai.usage.input_tokens": response.usage.promptTokens,
35
+ "gen_ai.usage.output_tokens": response.usage.completionTokens
36
+ });
37
+ return response;
38
+ });
28
39
  },
29
40
  ...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
30
41
  });
@@ -35,11 +46,21 @@ async function* tracedStream(tracer, next, req) {
35
46
  type: "provider.stream",
36
47
  attrs: {
37
48
  "graphorin.provider.id": next.name,
38
- "graphorin.provider.model": next.modelId
39
- }
49
+ "graphorin.provider.model": next.modelId,
50
+ "gen_ai.operation.name": "chat",
51
+ "gen_ai.provider.name": next.name,
52
+ "gen_ai.request.model": next.modelId
53
+ },
54
+ ...req.parentSpan !== void 0 ? { parent: req.parentSpan } : {}
40
55
  });
41
56
  try {
42
- for await (const event of next.stream(req)) yield event;
57
+ for await (const event of next.stream(req)) {
58
+ if (event.type === "finish") span.setAttributes({
59
+ "gen_ai.usage.input_tokens": event.usage.promptTokens,
60
+ "gen_ai.usage.output_tokens": event.usage.completionTokens
61
+ });
62
+ yield event;
63
+ }
43
64
  span.setStatus("ok");
44
65
  } catch (err) {
45
66
  span.recordException(err);
@@ -1 +1 @@
1
- {"version":3,"file":"with-tracing.js","names":[],"sources":["../../src/middleware/with-tracing.ts"],"sourcesContent":["/**\n * `withTracing` outermost middleware. Wraps every `provider.stream`\n * / `provider.generate` call in a Graphorin OTel-shaped span so the\n * span captures retry decisions, rate-limit waits, and the underlying\n * provider call as one logical unit.\n *\n * The middleware accepts any object that exposes the\n * {@link Tracer} contract from `@graphorin/core/contracts`. Tests pass\n * a `NOOP_TRACER`-shaped stub; production wires the real Graphorin\n * tracer from `@graphorin/observability`.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, Tracer } from '@graphorin/core';\n\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withTracing}.\n *\n * @stable\n */\nexport interface WithTracingOptions {\n /** Tracer instance. Defaults to a no-op tracer if unset. */\n readonly tracer?: Tracer;\n}\n\n/**\n * @stable\n */\nexport const withTracing = defineProviderMiddleware<WithTracingOptions>({\n kind: 'withTracing',\n factory: (opts: WithTracingOptions) => {\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 if (opts.tracer === undefined) return next.stream(req);\n return tracedStream(opts.tracer, next, req);\n },\n async generate(req) {\n if (opts.tracer === undefined) return next.generate(req);\n return opts.tracer.span(\n {\n type: 'provider.generate' as const,\n attrs: {\n 'graphorin.provider.id': next.name,\n 'graphorin.provider.model': next.modelId,\n },\n },\n async () => next.generate(req),\n );\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* tracedStream(\n tracer: Tracer,\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n): AsyncIterable<import('@graphorin/core').ProviderEvent> {\n const span = tracer.startSpan({\n type: 'provider.stream' as const,\n attrs: {\n 'graphorin.provider.id': next.name,\n 'graphorin.provider.model': next.modelId,\n },\n });\n try {\n for await (const event of next.stream(req)) {\n yield event;\n }\n span.setStatus('ok');\n } catch (err) {\n span.recordException(err);\n span.setStatus('error', (err as Error).message);\n throw err;\n } finally {\n // PS-7: a consumer `break`/early-return injects a generator `return` at the\n // `yield`, skipping both the success and catch paths. Ending the span in\n // `finally` guarantees it is closed exactly once on every exit normal\n // completion, error, or early abort (which leaves the status unset).\n span.end();\n }\n}\n"],"mappings":";;;;;;AA+BA,MAAa,cAAc,yBAA6C;CACtE,MAAM;CACN,UAAU,SAA6B;AACrC,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,QAAI,KAAK,WAAW,OAAW,QAAO,KAAK,OAAO,IAAI;AACtD,WAAO,aAAa,KAAK,QAAQ,MAAM,IAAI;;GAE7C,MAAM,SAAS,KAAK;AAClB,QAAI,KAAK,WAAW,OAAW,QAAO,KAAK,SAAS,IAAI;AACxD,WAAO,KAAK,OAAO,KACjB;KACE,MAAM;KACN,OAAO;MACL,yBAAyB,KAAK;MAC9B,4BAA4B,KAAK;MAClC;KACF,EACD,YAAY,KAAK,SAAS,IAAI,CAC/B;;GAEH,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,gBAAgB,aACd,QACA,MACA,KACwD;CACxD,MAAM,OAAO,OAAO,UAAU;EAC5B,MAAM;EACN,OAAO;GACL,yBAAyB,KAAK;GAC9B,4BAA4B,KAAK;GAClC;EACF,CAAC;AACF,KAAI;AACF,aAAW,MAAM,SAAS,KAAK,OAAO,IAAI,CACxC,OAAM;AAER,OAAK,UAAU,KAAK;UACb,KAAK;AACZ,OAAK,gBAAgB,IAAI;AACzB,OAAK,UAAU,SAAU,IAAc,QAAQ;AAC/C,QAAM;WACE;AAKR,OAAK,KAAK"}
1
+ {"version":3,"file":"with-tracing.js","names":[],"sources":["../../src/middleware/with-tracing.ts"],"sourcesContent":["/**\n * `withTracing` - outermost middleware. Wraps every `provider.stream`\n * / `provider.generate` call in a Graphorin OTel-shaped span so the\n * span captures retry decisions, rate-limit waits, and the underlying\n * provider call as one logical unit.\n *\n * The middleware accepts any object that exposes the\n * {@link Tracer} contract from `@graphorin/core/contracts`. Tests pass\n * a `NOOP_TRACER`-shaped stub; production wires the real Graphorin\n * tracer from `@graphorin/observability`.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, Tracer } from '@graphorin/core';\n\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withTracing}.\n *\n * @stable\n */\nexport interface WithTracingOptions {\n /** Tracer instance. Defaults to a no-op tracer if unset. */\n readonly tracer?: Tracer;\n}\n\n/**\n * @stable\n */\nexport const withTracing = defineProviderMiddleware<WithTracingOptions>({\n kind: 'withTracing',\n factory: (opts: WithTracingOptions) => {\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 if (opts.tracer === undefined) return next.stream(req);\n return tracedStream(opts.tracer, next, req);\n },\n async generate(req) {\n if (opts.tracer === undefined) return next.generate(req);\n return opts.tracer.span(\n {\n type: 'provider.generate' as const,\n attrs: {\n 'graphorin.provider.id': next.name,\n 'graphorin.provider.model': next.modelId,\n // C7: OTel GenAI semantic conventions.\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.provider.name': next.name,\n 'gen_ai.request.model': next.modelId,\n },\n // C7: parent under the agent step span when the loop supplies one.\n ...(req.parentSpan !== undefined ? { parent: req.parentSpan } : {}),\n },\n async (span) => {\n const response = await next.generate(req);\n span.setAttributes({\n 'gen_ai.usage.input_tokens': response.usage.promptTokens,\n 'gen_ai.usage.output_tokens': response.usage.completionTokens,\n });\n return response;\n },\n );\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* tracedStream(\n tracer: Tracer,\n next: Provider,\n req: import('@graphorin/core').ProviderRequest,\n): AsyncIterable<import('@graphorin/core').ProviderEvent> {\n const span = tracer.startSpan({\n type: 'provider.stream' as const,\n attrs: {\n 'graphorin.provider.id': next.name,\n 'graphorin.provider.model': next.modelId,\n // C7: OTel GenAI semantic conventions.\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.provider.name': next.name,\n 'gen_ai.request.model': next.modelId,\n },\n // C7: parent under the agent step span when the loop supplies one.\n ...(req.parentSpan !== undefined ? { parent: req.parentSpan } : {}),\n });\n try {\n for await (const event of next.stream(req)) {\n if (event.type === 'finish') {\n span.setAttributes({\n 'gen_ai.usage.input_tokens': event.usage.promptTokens,\n 'gen_ai.usage.output_tokens': event.usage.completionTokens,\n });\n }\n yield event;\n }\n span.setStatus('ok');\n } catch (err) {\n span.recordException(err);\n span.setStatus('error', (err as Error).message);\n throw err;\n } finally {\n // PS-7: a consumer `break`/early-return injects a generator `return` at the\n // `yield`, skipping both the success and catch paths. Ending the span in\n // `finally` guarantees it is closed exactly once on every exit - normal\n // completion, error, or early abort (which leaves the status unset).\n span.end();\n }\n}\n"],"mappings":";;;;;;AA+BA,MAAa,cAAc,yBAA6C;CACtE,MAAM;CACN,UAAU,SAA6B;AACrC,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,QAAI,KAAK,WAAW,OAAW,QAAO,KAAK,OAAO,IAAI;AACtD,WAAO,aAAa,KAAK,QAAQ,MAAM,IAAI;;GAE7C,MAAM,SAAS,KAAK;AAClB,QAAI,KAAK,WAAW,OAAW,QAAO,KAAK,SAAS,IAAI;AACxD,WAAO,KAAK,OAAO,KACjB;KACE,MAAM;KACN,OAAO;MACL,yBAAyB,KAAK;MAC9B,4BAA4B,KAAK;MAEjC,yBAAyB;MACzB,wBAAwB,KAAK;MAC7B,wBAAwB,KAAK;MAC9B;KAED,GAAI,IAAI,eAAe,SAAY,EAAE,QAAQ,IAAI,YAAY,GAAG,EAAE;KACnE,EACD,OAAO,SAAS;KACd,MAAM,WAAW,MAAM,KAAK,SAAS,IAAI;AACzC,UAAK,cAAc;MACjB,6BAA6B,SAAS,MAAM;MAC5C,8BAA8B,SAAS,MAAM;MAC9C,CAAC;AACF,YAAO;MAEV;;GAEH,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,gBAAgB,aACd,QACA,MACA,KACwD;CACxD,MAAM,OAAO,OAAO,UAAU;EAC5B,MAAM;EACN,OAAO;GACL,yBAAyB,KAAK;GAC9B,4BAA4B,KAAK;GAEjC,yBAAyB;GACzB,wBAAwB,KAAK;GAC7B,wBAAwB,KAAK;GAC9B;EAED,GAAI,IAAI,eAAe,SAAY,EAAE,QAAQ,IAAI,YAAY,GAAG,EAAE;EACnE,CAAC;AACF,KAAI;AACF,aAAW,MAAM,SAAS,KAAK,OAAO,IAAI,EAAE;AAC1C,OAAI,MAAM,SAAS,SACjB,MAAK,cAAc;IACjB,6BAA6B,MAAM,MAAM;IACzC,8BAA8B,MAAM,MAAM;IAC3C,CAAC;AAEJ,SAAM;;AAER,OAAK,UAAU,KAAK;UACb,KAAK;AACZ,OAAK,gBAAgB,IAAI;AACzB,OAAK,UAAU,SAAU,IAAc,QAAQ;AAC/C,QAAM;WACE;AAKR,OAAK,KAAK"}
@@ -14,7 +14,7 @@ interface ClassifierRule {
14
14
  readonly family: string;
15
15
  }
16
16
  /**
17
- * The static rule table. Order matters higher-specificity entries
17
+ * The static rule table. Order matters - higher-specificity entries
18
18
  * come first (e.g. `claude-haiku` before `claude-`). Tests assert
19
19
  * that the table covers the canonical 2026 model families.
20
20
  *
@@ -2,7 +2,7 @@ import { InvalidProviderError } from "../errors/errors.js";
2
2
 
3
3
  //#region src/model-tier/classify.ts
4
4
  /**
5
- * The static rule table. Order matters higher-specificity entries
5
+ * The static rule table. Order matters - higher-specificity entries
6
6
  * come first (e.g. `claude-haiku` before `claude-`). Tests assert
7
7
  * that the table covers the canonical 2026 model families.
8
8
  *
@@ -104,12 +104,20 @@ function classifyModelTier(provider) {
104
104
  const normalised = stripFamilyPrefix(modelId.toLowerCase());
105
105
  for (const rule of CLASSIFIER_RULES) if (rule.pattern.test(normalised)) return rule.tier;
106
106
  }
107
+ /**
108
+ * Bedrock cross-region inference-profile prefix (`us.anthropic.claude-…`).
109
+ * Stripped so region-qualified ids hit the `^anthropic\.claude` rules
110
+ * (core-provider-11).
111
+ */
112
+ const BEDROCK_REGION_PREFIX = /^(?:us|eu|apac|jp|au|us-gov)\./;
107
113
  function stripFamilyPrefix(model) {
108
114
  const slash = model.indexOf("/");
109
- if (slash !== -1) return model.slice(slash + 1);
110
- const colon = model.indexOf(":");
111
- if (colon !== -1 && !model.startsWith("http")) return model.slice(colon + 1);
112
- return model;
115
+ if (slash !== -1) return model.slice(slash + 1).replace(BEDROCK_REGION_PREFIX, "");
116
+ const deRegioned = model.replace(BEDROCK_REGION_PREFIX, "");
117
+ if (deRegioned.startsWith("anthropic.")) return deRegioned;
118
+ const colon = deRegioned.indexOf(":");
119
+ if (colon !== -1 && !deRegioned.startsWith("http")) return deRegioned.slice(colon + 1);
120
+ return deRegioned;
113
121
  }
114
122
 
115
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"classify.js","names":["CLASSIFIER_RULES: readonly ClassifierRule[]"],"sources":["../../src/model-tier/classify.ts"],"sourcesContent":["/**\n * Per-provider model-tier auto-classifier returns\n * `'fast' | 'balanced' | 'smart' | undefined` for any model id. The\n * classifier is consumed by the agent runtime (Phase 12) to validate\n * operator-supplied tier mappings and to surface tier-not-mapped\n * recommendations.\n *\n * The dispatcher is a pure function; it reads from a small static\n * rule table keyed on regex patterns matched against the lowercased\n * model id. The table can be inspected via {@link CLASSIFIER_RULES}\n * for debugging and downstream tooling (CLI, dashboard, lint rules).\n *\n * @packageDocumentation\n */\n\nimport type { ModelHint, ProviderLike } from '@graphorin/core';\n\nimport { InvalidProviderError } from '../errors/errors.js';\n\n/**\n * Single entry in the classifier rule table.\n *\n * @stable\n */\nexport interface ClassifierRule {\n readonly tier: ModelHint;\n readonly pattern: RegExp;\n /** Human-readable family label (`'anthropic-claude-haiku'`, …). */\n readonly family: string;\n}\n\n/**\n * The static rule table. Order matters higher-specificity entries\n * come first (e.g. `claude-haiku` before `claude-`). Tests assert\n * that the table covers the canonical 2026 model families.\n *\n * @stable\n */\nexport const CLASSIFIER_RULES: readonly ClassifierRule[] = Object.freeze([\n // Anthropic direct. PS-20: the version segment between `claude` and the\n // family word can be multi-part (`claude-3-5-haiku`, `claude-3-7-sonnet`) or\n // absent (`claude-haiku-4-5`), so allow zero-or-more `-<digits/dots>` groups.\n { tier: 'fast', family: 'anthropic-haiku', pattern: /^claude(?:-[\\d.]+)*-?haiku/ },\n { tier: 'balanced', family: 'anthropic-sonnet', pattern: /^claude(?:-[\\d.]+)*-?sonnet/ },\n { tier: 'smart', family: 'anthropic-opus', pattern: /^claude(?:-[\\d.]+)*-?opus/ },\n { tier: 'smart', family: 'anthropic-fable', pattern: /^claude.*fable/ },\n { tier: 'smart', family: 'anthropic-mythos', pattern: /^claude.*mythos/ },\n // Anthropic via Bedrock.\n {\n tier: 'fast',\n family: 'bedrock-claude-haiku',\n pattern: /^anthropic\\.claude(?:-[\\d.]+)*-?haiku/,\n },\n {\n tier: 'balanced',\n family: 'bedrock-claude-sonnet',\n pattern: /^anthropic\\.claude(?:-[\\d.]+)*-?sonnet/,\n },\n {\n tier: 'smart',\n family: 'bedrock-claude-opus',\n pattern: /^anthropic\\.claude(?:-[\\d.]+)*-?opus/,\n },\n // OpenAI.\n { tier: 'fast', family: 'openai-mini', pattern: /^gpt-(\\d|\\d+\\.\\d+)-?mini/ },\n { tier: 'fast', family: 'openai-nano', pattern: /^gpt-(\\d|\\d+\\.\\d+)-?nano/ },\n { tier: 'balanced', family: 'openai-gpt', pattern: /^gpt-(?!.*(?:mini|nano))/ },\n { tier: 'smart', family: 'openai-reasoning', pattern: /^o[1-9]\\b/ },\n { tier: 'smart', family: 'openai-reasoning-extended', pattern: /^o[1-9][a-z-]/ },\n // Google Gemini.\n { tier: 'fast', family: 'gemini-flash', pattern: /^gemini.*flash/ },\n { tier: 'balanced', family: 'gemini-pro', pattern: /^gemini.*pro\\b/ },\n { tier: 'smart', family: 'gemini-ultra', pattern: /^gemini.*ultra/ },\n]);\n\n/**\n * Classify a `Provider`'s `modelId` into one of `'fast' | 'balanced' |\n * 'smart'`. Returns `undefined` when the model id matches none of the\n * canonical 2026 mappings (Ollama / OpenAI-compatible / unknown).\n *\n * @stable\n */\nexport function classifyModelTier(provider: ProviderLike): ModelHint | undefined {\n if (provider === null || typeof provider !== 'object') {\n throw new InvalidProviderError('classifyModelTier: argument must be a Provider-shaped object.');\n }\n const modelId = provider.modelId;\n if (typeof modelId !== 'string' || modelId.length === 0) {\n throw new InvalidProviderError(\n 'classifyModelTier: provider.modelId must be a non-empty string.',\n );\n }\n const normalised = stripFamilyPrefix(modelId.toLowerCase());\n for (const rule of CLASSIFIER_RULES) {\n if (rule.pattern.test(normalised)) return rule.tier;\n }\n return undefined;\n}\n\nfunction stripFamilyPrefix(model: string): string {\n // Common prefixes used by adapters: `anthropic/...`, `openai/...`,\n // `google/...`, `provider:model`, etc. Strip them so the rule\n // patterns can stay anchored at `^`.\n const slash = model.indexOf('/');\n if (slash !== -1) return model.slice(slash + 1);\n const colon = model.indexOf(':');\n if (colon !== -1 && !model.startsWith('http')) {\n // Skip URL-like values (`http://localhost:8080`) `:` there is a\n // port separator, not a provider/model split.\n return model.slice(colon + 1);\n }\n return model;\n}\n"],"mappings":";;;;;;;;;;AAsCA,MAAaA,mBAA8C,OAAO,OAAO;CAIvE;EAAE,MAAM;EAAQ,QAAQ;EAAmB,SAAS;EAA8B;CAClF;EAAE,MAAM;EAAY,QAAQ;EAAoB,SAAS;EAA+B;CACxF;EAAE,MAAM;EAAS,QAAQ;EAAkB,SAAS;EAA6B;CACjF;EAAE,MAAM;EAAS,QAAQ;EAAmB,SAAS;EAAkB;CACvE;EAAE,MAAM;EAAS,QAAQ;EAAoB,SAAS;EAAmB;CAEzE;EACE,MAAM;EACN,QAAQ;EACR,SAAS;EACV;CACD;EACE,MAAM;EACN,QAAQ;EACR,SAAS;EACV;CACD;EACE,MAAM;EACN,QAAQ;EACR,SAAS;EACV;CAED;EAAE,MAAM;EAAQ,QAAQ;EAAe,SAAS;EAA4B;CAC5E;EAAE,MAAM;EAAQ,QAAQ;EAAe,SAAS;EAA4B;CAC5E;EAAE,MAAM;EAAY,QAAQ;EAAc,SAAS;EAA4B;CAC/E;EAAE,MAAM;EAAS,QAAQ;EAAoB,SAAS;EAAa;CACnE;EAAE,MAAM;EAAS,QAAQ;EAA6B,SAAS;EAAiB;CAEhF;EAAE,MAAM;EAAQ,QAAQ;EAAgB,SAAS;EAAkB;CACnE;EAAE,MAAM;EAAY,QAAQ;EAAc,SAAS;EAAkB;CACrE;EAAE,MAAM;EAAS,QAAQ;EAAgB,SAAS;EAAkB;CACrE,CAAC;;;;;;;;AASF,SAAgB,kBAAkB,UAA+C;AAC/E,KAAI,aAAa,QAAQ,OAAO,aAAa,SAC3C,OAAM,IAAI,qBAAqB,gEAAgE;CAEjG,MAAM,UAAU,SAAS;AACzB,KAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,EACpD,OAAM,IAAI,qBACR,kEACD;CAEH,MAAM,aAAa,kBAAkB,QAAQ,aAAa,CAAC;AAC3D,MAAK,MAAM,QAAQ,iBACjB,KAAI,KAAK,QAAQ,KAAK,WAAW,CAAE,QAAO,KAAK;;AAKnD,SAAS,kBAAkB,OAAuB;CAIhD,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,KAAI,UAAU,GAAI,QAAO,MAAM,MAAM,QAAQ,EAAE;CAC/C,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,KAAI,UAAU,MAAM,CAAC,MAAM,WAAW,OAAO,CAG3C,QAAO,MAAM,MAAM,QAAQ,EAAE;AAE/B,QAAO"}
1
+ {"version":3,"file":"classify.js","names":["CLASSIFIER_RULES: readonly ClassifierRule[]"],"sources":["../../src/model-tier/classify.ts"],"sourcesContent":["/**\n * Per-provider model-tier auto-classifier - returns\n * `'fast' | 'balanced' | 'smart' | undefined` for any model id. The\n * classifier is consumed by the agent runtime (Phase 12) to validate\n * operator-supplied tier mappings and to surface tier-not-mapped\n * recommendations.\n *\n * The dispatcher is a pure function; it reads from a small static\n * rule table keyed on regex patterns matched against the lowercased\n * model id. The table can be inspected via {@link CLASSIFIER_RULES}\n * for debugging and downstream tooling (CLI, dashboard, lint rules).\n *\n * @packageDocumentation\n */\n\nimport type { ModelHint, ProviderLike } from '@graphorin/core';\n\nimport { InvalidProviderError } from '../errors/errors.js';\n\n/**\n * Single entry in the classifier rule table.\n *\n * @stable\n */\nexport interface ClassifierRule {\n readonly tier: ModelHint;\n readonly pattern: RegExp;\n /** Human-readable family label (`'anthropic-claude-haiku'`, …). */\n readonly family: string;\n}\n\n/**\n * The static rule table. Order matters - higher-specificity entries\n * come first (e.g. `claude-haiku` before `claude-`). Tests assert\n * that the table covers the canonical 2026 model families.\n *\n * @stable\n */\nexport const CLASSIFIER_RULES: readonly ClassifierRule[] = Object.freeze([\n // Anthropic - direct. PS-20: the version segment between `claude` and the\n // family word can be multi-part (`claude-3-5-haiku`, `claude-3-7-sonnet`) or\n // absent (`claude-haiku-4-5`), so allow zero-or-more `-<digits/dots>` groups.\n { tier: 'fast', family: 'anthropic-haiku', pattern: /^claude(?:-[\\d.]+)*-?haiku/ },\n { tier: 'balanced', family: 'anthropic-sonnet', pattern: /^claude(?:-[\\d.]+)*-?sonnet/ },\n { tier: 'smart', family: 'anthropic-opus', pattern: /^claude(?:-[\\d.]+)*-?opus/ },\n { tier: 'smart', family: 'anthropic-fable', pattern: /^claude.*fable/ },\n { tier: 'smart', family: 'anthropic-mythos', pattern: /^claude.*mythos/ },\n // Anthropic - via Bedrock.\n {\n tier: 'fast',\n family: 'bedrock-claude-haiku',\n pattern: /^anthropic\\.claude(?:-[\\d.]+)*-?haiku/,\n },\n {\n tier: 'balanced',\n family: 'bedrock-claude-sonnet',\n pattern: /^anthropic\\.claude(?:-[\\d.]+)*-?sonnet/,\n },\n {\n tier: 'smart',\n family: 'bedrock-claude-opus',\n pattern: /^anthropic\\.claude(?:-[\\d.]+)*-?opus/,\n },\n // OpenAI.\n { tier: 'fast', family: 'openai-mini', pattern: /^gpt-(\\d|\\d+\\.\\d+)-?mini/ },\n { tier: 'fast', family: 'openai-nano', pattern: /^gpt-(\\d|\\d+\\.\\d+)-?nano/ },\n { tier: 'balanced', family: 'openai-gpt', pattern: /^gpt-(?!.*(?:mini|nano))/ },\n { tier: 'smart', family: 'openai-reasoning', pattern: /^o[1-9]\\b/ },\n { tier: 'smart', family: 'openai-reasoning-extended', pattern: /^o[1-9][a-z-]/ },\n // Google Gemini.\n { tier: 'fast', family: 'gemini-flash', pattern: /^gemini.*flash/ },\n { tier: 'balanced', family: 'gemini-pro', pattern: /^gemini.*pro\\b/ },\n { tier: 'smart', family: 'gemini-ultra', pattern: /^gemini.*ultra/ },\n]);\n\n/**\n * Classify a `Provider`'s `modelId` into one of `'fast' | 'balanced' |\n * 'smart'`. Returns `undefined` when the model id matches none of the\n * canonical 2026 mappings (Ollama / OpenAI-compatible / unknown).\n *\n * @stable\n */\nexport function classifyModelTier(provider: ProviderLike): ModelHint | undefined {\n if (provider === null || typeof provider !== 'object') {\n throw new InvalidProviderError('classifyModelTier: argument must be a Provider-shaped object.');\n }\n const modelId = provider.modelId;\n if (typeof modelId !== 'string' || modelId.length === 0) {\n throw new InvalidProviderError(\n 'classifyModelTier: provider.modelId must be a non-empty string.',\n );\n }\n const normalised = stripFamilyPrefix(modelId.toLowerCase());\n for (const rule of CLASSIFIER_RULES) {\n if (rule.pattern.test(normalised)) return rule.tier;\n }\n return undefined;\n}\n\n/**\n * Bedrock cross-region inference-profile prefix (`us.anthropic.claude-…`).\n * Stripped so region-qualified ids hit the `^anthropic\\.claude` rules\n * (core-provider-11).\n */\nconst BEDROCK_REGION_PREFIX = /^(?:us|eu|apac|jp|au|us-gov)\\./;\n\nfunction stripFamilyPrefix(model: string): string {\n // Common prefixes used by adapters: `anthropic/...`, `openai/...`,\n // `google/...`, `provider:model`, etc. Strip them so the rule\n // patterns can stay anchored at `^`.\n const slash = model.indexOf('/');\n if (slash !== -1) return model.slice(slash + 1).replace(BEDROCK_REGION_PREFIX, '');\n const deRegioned = model.replace(BEDROCK_REGION_PREFIX, '');\n // Bedrock ids end in ':<version>' (`anthropic.claude-...-v1:0`) - the\n // colon there is a version separator, not a provider/model split, and\n // the rule patterns are prefix-anchored so the suffix is harmless.\n if (deRegioned.startsWith('anthropic.')) return deRegioned;\n const colon = deRegioned.indexOf(':');\n if (colon !== -1 && !deRegioned.startsWith('http')) {\n // Skip URL-like values (`http://localhost:8080`) - `:` there is a\n // port separator, not a provider/model split.\n return deRegioned.slice(colon + 1);\n }\n return deRegioned;\n}\n"],"mappings":";;;;;;;;;;AAsCA,MAAaA,mBAA8C,OAAO,OAAO;CAIvE;EAAE,MAAM;EAAQ,QAAQ;EAAmB,SAAS;EAA8B;CAClF;EAAE,MAAM;EAAY,QAAQ;EAAoB,SAAS;EAA+B;CACxF;EAAE,MAAM;EAAS,QAAQ;EAAkB,SAAS;EAA6B;CACjF;EAAE,MAAM;EAAS,QAAQ;EAAmB,SAAS;EAAkB;CACvE;EAAE,MAAM;EAAS,QAAQ;EAAoB,SAAS;EAAmB;CAEzE;EACE,MAAM;EACN,QAAQ;EACR,SAAS;EACV;CACD;EACE,MAAM;EACN,QAAQ;EACR,SAAS;EACV;CACD;EACE,MAAM;EACN,QAAQ;EACR,SAAS;EACV;CAED;EAAE,MAAM;EAAQ,QAAQ;EAAe,SAAS;EAA4B;CAC5E;EAAE,MAAM;EAAQ,QAAQ;EAAe,SAAS;EAA4B;CAC5E;EAAE,MAAM;EAAY,QAAQ;EAAc,SAAS;EAA4B;CAC/E;EAAE,MAAM;EAAS,QAAQ;EAAoB,SAAS;EAAa;CACnE;EAAE,MAAM;EAAS,QAAQ;EAA6B,SAAS;EAAiB;CAEhF;EAAE,MAAM;EAAQ,QAAQ;EAAgB,SAAS;EAAkB;CACnE;EAAE,MAAM;EAAY,QAAQ;EAAc,SAAS;EAAkB;CACrE;EAAE,MAAM;EAAS,QAAQ;EAAgB,SAAS;EAAkB;CACrE,CAAC;;;;;;;;AASF,SAAgB,kBAAkB,UAA+C;AAC/E,KAAI,aAAa,QAAQ,OAAO,aAAa,SAC3C,OAAM,IAAI,qBAAqB,gEAAgE;CAEjG,MAAM,UAAU,SAAS;AACzB,KAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,EACpD,OAAM,IAAI,qBACR,kEACD;CAEH,MAAM,aAAa,kBAAkB,QAAQ,aAAa,CAAC;AAC3D,MAAK,MAAM,QAAQ,iBACjB,KAAI,KAAK,QAAQ,KAAK,WAAW,CAAE,QAAO,KAAK;;;;;;;AAUnD,MAAM,wBAAwB;AAE9B,SAAS,kBAAkB,OAAuB;CAIhD,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,KAAI,UAAU,GAAI,QAAO,MAAM,MAAM,QAAQ,EAAE,CAAC,QAAQ,uBAAuB,GAAG;CAClF,MAAM,aAAa,MAAM,QAAQ,uBAAuB,GAAG;AAI3D,KAAI,WAAW,WAAW,aAAa,CAAE,QAAO;CAChD,MAAM,QAAQ,WAAW,QAAQ,IAAI;AACrC,KAAI,UAAU,MAAM,CAAC,WAAW,WAAW,OAAO,CAGhD,QAAO,WAAW,MAAM,QAAQ,EAAE;AAEpC,QAAO"}
@@ -0,0 +1,6 @@
1
+ //#region package.json
2
+ var version = "0.6.1";
3
+
4
+ //#endregion
5
+ export { version };
6
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/provider\",\n \"version\": \"0.6.1\",\n \"description\": \"Vendor-neutral LLM provider layer for the Graphorin framework. Ships the canonical Provider interface, the Vercel AI SDK adapter (default cloud path), three local-LLM adapters (direct Ollama HTTP, llama-server HTTP, and a generic OpenAI-compatible adapter for LMStudio / LocalAI / vLLM / Together-style endpoints), the shared LocalProviderTrust classifier (loopback / private / public-tls / public-cleartext), the canonical-order middleware composer (withTracing / withRetry / withRateLimit / withCostLimit / withCostTracking / withFallback / withRedaction), the pluggable TokenCounter dispatcher (js-tiktoken default plus per-vendor native counters and a heuristic fallback), the per-provider model-tier auto-classifier (fast / balanced / smart), and the production startup hook that fails fast when withRedaction is missing.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/provider\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/provider\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"provider\",\n \"llm\",\n \"vercel-ai-sdk\",\n \"ollama\",\n \"llama-cpp\",\n \"openai-compatible\",\n \"middleware\",\n \"retry\",\n \"fallback\",\n \"rate-limit\",\n \"cost-limit\",\n \"redaction\",\n \"token-counter\",\n \"tiktoken\",\n \"local-first\"\n ],\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./adapters/vercel\": {\n \"types\": \"./dist/adapters/vercel.d.ts\",\n \"import\": \"./dist/adapters/vercel.js\"\n },\n \"./adapters/ollama\": {\n \"types\": \"./dist/adapters/ollama.d.ts\",\n \"import\": \"./dist/adapters/ollama.js\"\n },\n \"./adapters/llamacpp-server\": {\n \"types\": \"./dist/adapters/llamacpp-server.d.ts\",\n \"import\": \"./dist/adapters/llamacpp-server.js\"\n },\n \"./adapters/openai-compatible\": {\n \"types\": \"./dist/adapters/openai-compatible.d.ts\",\n \"import\": \"./dist/adapters/openai-compatible.js\"\n },\n \"./middleware\": {\n \"types\": \"./dist/middleware/index.d.ts\",\n \"import\": \"./dist/middleware/index.js\"\n },\n \"./counters\": {\n \"types\": \"./dist/counters/index.d.ts\",\n \"import\": \"./dist/counters/index.js\"\n },\n \"./trust\": {\n \"types\": \"./dist/trust/index.d.ts\",\n \"import\": \"./dist/trust/index.js\"\n },\n \"./model-tier\": {\n \"types\": \"./dist/model-tier/index.d.ts\",\n \"import\": \"./dist/model-tier/index.js\"\n },\n \"./reasoning\": {\n \"types\": \"./dist/reasoning/index.d.ts\",\n \"import\": \"./dist/reasoning/index.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors/index.d.ts\",\n \"import\": \"./dist/errors/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/observability\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"ai\": \"^7.0.0-beta.76\",\n \"js-tiktoken\": \"^1.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"ai\": {\n \"optional\": true\n },\n \"js-tiktoken\": {\n \"optional\": true\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/pricing\": \"workspace:*\",\n \"ai\": \"^7.0.15\",\n \"fast-check\": \"^3.23.0\",\n \"js-tiktoken\": \"^1.0.21\"\n }\n}\n"],"mappings":";cAEa"}