@graphorin/provider 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +7 -7
- package/dist/adapters/llamacpp-server.d.ts +1 -1
- package/dist/adapters/llamacpp-server.js +1 -1
- package/dist/adapters/llamacpp-server.js.map +1 -1
- package/dist/adapters/ollama.d.ts.map +1 -1
- package/dist/adapters/ollama.js +3 -2
- package/dist/adapters/ollama.js.map +1 -1
- package/dist/adapters/openai-compatible.d.ts +14 -1
- package/dist/adapters/openai-compatible.d.ts.map +1 -1
- package/dist/adapters/openai-compatible.js +2 -0
- package/dist/adapters/openai-compatible.js.map +1 -1
- package/dist/adapters/vercel-messages.js +245 -0
- package/dist/adapters/vercel-messages.js.map +1 -0
- package/dist/adapters/vercel.d.ts +10 -8
- package/dist/adapters/vercel.d.ts.map +1 -1
- package/dist/adapters/vercel.js +54 -14
- package/dist/adapters/vercel.js.map +1 -1
- package/dist/counters/anthropic-wire.js +149 -0
- package/dist/counters/anthropic-wire.js.map +1 -0
- package/dist/counters/anthropic.d.ts +6 -1
- package/dist/counters/anthropic.d.ts.map +1 -1
- package/dist/counters/anthropic.js +25 -4
- package/dist/counters/anthropic.js.map +1 -1
- package/dist/counters/bedrock.js.map +1 -1
- package/dist/counters/dispatcher.d.ts.map +1 -1
- package/dist/counters/dispatcher.js +6 -4
- package/dist/counters/dispatcher.js.map +1 -1
- package/dist/counters/google.js.map +1 -1
- package/dist/counters/js-tiktoken.js.map +1 -1
- package/dist/errors/errors.d.ts +39 -11
- package/dist/errors/errors.d.ts.map +1 -1
- package/dist/errors/errors.js +54 -11
- package/dist/errors/errors.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/abort.js +2 -2
- package/dist/internal/abort.js.map +1 -1
- package/dist/internal/http.js +19 -4
- package/dist/internal/http.js.map +1 -1
- package/dist/internal/openai-shaped.js +8 -3
- package/dist/internal/openai-shaped.js.map +1 -1
- package/dist/middleware/compose.d.ts +6 -6
- package/dist/middleware/compose.js +7 -7
- package/dist/middleware/compose.js.map +1 -1
- package/dist/middleware/production-hook.js.map +1 -1
- package/dist/middleware/with-cost-limit.d.ts +1 -1
- package/dist/middleware/with-cost-limit.js.map +1 -1
- package/dist/middleware/with-cost-tracking.d.ts +17 -4
- package/dist/middleware/with-cost-tracking.d.ts.map +1 -1
- package/dist/middleware/with-cost-tracking.js +12 -2
- package/dist/middleware/with-cost-tracking.js.map +1 -1
- package/dist/middleware/with-fallback.js +4 -2
- package/dist/middleware/with-fallback.js.map +1 -1
- package/dist/middleware/with-rate-limit.d.ts +2 -2
- package/dist/middleware/with-rate-limit.js.map +1 -1
- package/dist/middleware/with-redaction.d.ts +1 -1
- package/dist/middleware/with-redaction.js.map +1 -1
- package/dist/middleware/with-retry.js +3 -1
- package/dist/middleware/with-retry.js.map +1 -1
- package/dist/middleware/with-tracing.js +27 -6
- package/dist/middleware/with-tracing.js.map +1 -1
- package/dist/model-tier/classify.d.ts +1 -1
- package/dist/model-tier/classify.js +13 -5
- package/dist/model-tier/classify.js.map +1 -1
- package/dist/provider.js.map +1 -1
- package/dist/reasoning/apply-policy.js.map +1 -1
- package/dist/reasoning/classify-contract.js +14 -5
- package/dist/reasoning/classify-contract.js.map +1 -1
- package/dist/reasoning/retention.js.map +1 -1
- package/dist/tool-examples.d.ts +1 -1
- package/dist/tool-examples.js +1 -1
- package/dist/tool-examples.js.map +1 -1
- package/dist/trust/classify-local-provider.d.ts +1 -1
- package/dist/trust/classify-local-provider.js.map +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-fallback.js","names":["candidates: ReadonlyArray<Provider>","lastErr: unknown"],"sources":["../../src/middleware/with-fallback.ts"],"sourcesContent":["/**\n * `withFallback`
|
|
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`
|
|
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
|
|
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`
|
|
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
|
|
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
|
-
|
|
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":";;;;;;;AAiCA,MAAa,YAAY,yBAA2C;CAClE,MAAM;CACN,UAAU,SAA2B;EACnC,MAAM,aAAa,KAAK,cAAc;EACtC,MAAM,UAAU,KAAK,WAAW;EAChC,MAAM,eAAe,KAAK,kBAAkB;EAC5C,MAAM,WAAW,KAAK,cAAc;EACpC,MAAM,SAAS,KAAK,UAAU;EAC9B,MAAM,cAAc,KAAK,mBAAmB;EAC5C,MAAM,QAAQ,KAAK,aAAa;AAChC,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,eACL,MACA,KACA,YACA,SACA,cACA,UACA,QACA,aACA,MACD;;GAEH,MAAM,SAAS,KAAK;IAClB,IAAI,UAAU;AACd,YACE,KAAI;AACF,YAAO,MAAM,KAAK,SAAS,IAAI;aACxB,KAAK;AACZ,SAAI,IAAI,QAAQ,QAAS,OAAM;AAC/B,SAAI,WAAW,WAAY,OAAM;AACjC,SAAI,CAAC,YAAY,IAAI,CAAE,OAAM;KAC7B,MAAM,OAAO,eAAe,IAAI;AAKhC,WAAM,MAHJ,SAAS,OACL,KAAK,IAAI,MAAM,SAAS,GACxB,aAAa,SAAS,cAAc,SAAS,UAAU,OAAO,EACjD,IAAI,OAAO;AAC9B;;;GAIN,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;GACzE;;CAEJ,CAAC;AAEF,gBAAgB,eACd,MACA,KACA,YACA,SACA,cACA,UACA,QACA,aACA,OAC8B;CAC9B,IAAI,UAAU;CAMd,IAAI,aAAa;AACjB,SACE,KAAI;AACF,aAAW,MAAM,SAAS,KAAK,OAAO,IAAI,EAAE;AAC1C,gBAAa;AACb,SAAM;;AAER,MAAI,WAAY;AAEhB,QAAM;GACJ,MAAM;GACN,cAAc;GACd,OAAO;IAAE,cAAc;IAAG,kBAAkB;IAAG,aAAa;IAAG;GAChE;AACD;UACO,KAAK;AACZ,MAAI,IAAI,QAAQ,QAAS,OAAM;AAE/B,MAAI,WAAY,OAAM;AACtB,MAAI,WAAW,WAAY,OAAM;AACjC,MAAI,CAAC,YAAY,IAAI,CAAE,OAAM;EAC7B,MAAM,OAAO,eAAe,IAAI;AAKhC,QAAM,MAHJ,SAAS,OACL,KAAK,IAAI,MAAM,SAAS,GACxB,aAAa,SAAS,cAAc,SAAS,UAAU,OAAO,EACjD,IAAI,OAAO;AAC9B;;;;;;;;;;;;;;;;;AAmBN,SAAS,eAAe,KAA6B;AACnD,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU,QAAO;CACpD,MAAM,IAAI;AAKV,KACE,OAAO,EAAE,iBAAiB,YAC1B,OAAO,SAAS,EAAE,aAAa,IAC/B,EAAE,gBAAgB,EAElB,QAAO,EAAE;AAEX,KACE,OAAO,EAAE,sBAAsB,YAC/B,OAAO,SAAS,EAAE,kBAAkB,IACpC,EAAE,qBAAqB,EAEvB,QAAO,KAAK,MAAM,EAAE,oBAAoB,IAAK;CAE/C,MAAM,cACJ,EAAE,UAAU,kBAAkB,EAAE,UAAU,kBAAkB,EAAE,UAAU;AAC1E,KAAI,OAAO,gBAAgB,YAAY,YAAY,SAAS,GAAG;EAC7D,MAAM,UAAU,OAAO,YAAY;AACnC,MAAI,OAAO,SAAS,QAAQ,IAAI,WAAW,EAAG,QAAO,KAAK,MAAM,UAAU,IAAK;EAC/E,MAAM,aAAa,KAAK,MAAM,YAAY;AAC1C,MAAI,OAAO,SAAS,WAAW,EAAE;GAC/B,MAAM,QAAQ,aAAa,KAAK,KAAK;AACrC,UAAO,QAAQ,IAAI,QAAQ;;;AAG/B,QAAO;;AAGT,SAAS,iBAAiB,KAAuB;AAC/C,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU,QAAO;AAKpD,KAAI,aAAa,IAAI,CAAE,QAAO;CAC9B,MAAM,IAAI;CAGV,MAAM,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU;AACnC,KACE,MAAM,SAAS,YAAY,IAC3B,MAAM,SAAS,aAAa,IAC5B,MAAM,SAAS,sBAAsB,IACrC,MAAM,SAAS,WAAW,CAE1B,QAAO;AAET,KACE,MAAM,SAAS,eAAe,IAC9B,MAAM,SAAS,kBAAkB,IACjC,MAAM,SAAS,iBAAiB,IAChC,MAAM,SAAS,iBAAiB,CAEhC,QAAO;AAET,KAAI,OAAO,EAAE,WAAW,YAAY,EAAE,WAAW,IAAK,QAAO;AAC7D,KAAI,OAAO,EAAE,WAAW,YAAY,EAAE,UAAU,OAAO,EAAE,SAAS,IAAK,QAAO;AAI9E,KAAI,OAAO,EAAE,WAAW,YAAY,EAAE,WAAW,EAAG,QAAO;AAC3D,QAAO;;AAGT,SAAS,aACP,SACA,SACA,SACA,UACA,QACQ;CACR,IAAIA;AACJ,SAAQ,SAAR;EACE,KAAK;AACH,WAAQ,UAAU,KAAK;AACvB;EACF,KAAK;AACH,WAAQ,WAAW,UAAU;AAC7B;EACF;AACE,WAAQ;AACR;;AAEJ,SAAQ,KAAK,IAAI,OAAO,SAAS;AACjC,KAAI,QAAQ;EACV,MAAM,SAAS,KAAM,KAAK,QAAQ,GAAG;AACrC,UAAQ,KAAK,MAAM,QAAQ,OAAO;;AAEpC,QAAO;;AAGT,SAAS,aAAa,IAAY,QAAqC;AACrE,QAAO,IAAI,SAAe,SAAS,WAAW;AAC5C,MAAI,QAAQ,SAAS;AACnB,UAAO,OAAO,0BAAU,IAAI,MAAM,UAAU,CAAC;AAC7C;;EAEF,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,YAAS;KACR,GAAG;EACN,MAAM,gBAAgB;AACpB,YAAS;AACT,UAAO,QAAQ,0BAAU,IAAI,MAAM,UAAU,CAAC;;EAEhD,SAAS,UAAgB;AACvB,gBAAa,MAAM;AACnB,WAAQ,oBAAoB,SAAS,QAAQ;;AAE/C,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;GAC1D"}
|
|
@@ -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
|
-
|
|
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))
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
111
|
-
if (
|
|
112
|
-
|
|
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
|
|
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"}
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","names":["capabilities: ProviderCapabilities"],"sources":["../src/provider.ts"],"sourcesContent":["/**\n * `createProvider(...)`
|
|
1
|
+
{"version":3,"file":"provider.js","names":["capabilities: ProviderCapabilities"],"sources":["../src/provider.ts"],"sourcesContent":["/**\n * `createProvider(...)` - wrap an adapter return value in the canonical\n * Graphorin `Provider` shape and apply optional sensitivity / capability\n * overrides.\n *\n * Adapters return a \"raw\" provider object; `createProvider(...)` gives\n * downstream code a single place to centralise:\n *\n * - per-instance `acceptsSensitivity` declarations,\n * - capability overrides (e.g. forcing `multimodal: false` for a tool\n * that does not need it),\n * - default `reasoningRetention` resolution from the adapter's declared\n * `reasoningContract`,\n * - and the agreed-upon `name` / `modelId` lock-in.\n *\n * @stable\n */\n\nimport type {\n Provider,\n ProviderCapabilities,\n ProviderRequest,\n ReasoningRetention,\n Sensitivity,\n} from '@graphorin/core';\n\nimport { resolveReasoningRetention } from './reasoning/retention.js';\nimport { foldToolExamples } from './tool-examples.js';\n\n/**\n * Options accepted by {@link createProvider}.\n *\n * @stable\n */\nexport interface CreateProviderOptions {\n /**\n * Sensitivity tiers this provider is allowed to receive. When\n * unset, the value is forwarded from the wrapped adapter (which is\n * itself populated from the trust class for `baseUrl`-driven\n * adapters).\n */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /**\n * Per-request override of the reasoning-retention default. The\n * adapter's `capabilities.reasoningContract` decides the auto-\n * detected default; this option pins a different value for every\n * request the wrapper sees.\n */\n readonly reasoningRetention?: ReasoningRetention;\n /**\n * Optional capability override. Useful for narrowing what a\n * downstream tool advertises (e.g. setting `multimodal: false`\n * when the consumer's prompt cache is text-only).\n */\n readonly capabilities?: Partial<ProviderCapabilities>;\n}\n\n/**\n * Wrap an adapter in the canonical `Provider` shape. Adapters returned\n * by the bundled factories already implement `Provider`; passing them\n * through `createProvider(...)` is the recommended entry point because\n * it keeps the construction site documented and gives downstream\n * middleware a single attachment surface.\n *\n * @example\n * ```ts\n * const provider = createProvider(vercelAdapter(model), {\n * acceptsSensitivity: ['public', 'internal'],\n * });\n * ```\n *\n * @stable\n */\nexport function createProvider(adapter: Provider, options: CreateProviderOptions = {}): Provider {\n const capabilities: ProviderCapabilities = {\n ...adapter.capabilities,\n ...options.capabilities,\n };\n const acceptsSensitivity = options.acceptsSensitivity ?? adapter.acceptsSensitivity ?? undefined;\n\n const wrapped: Provider = {\n name: adapter.name,\n modelId: adapter.modelId,\n capabilities,\n ...(acceptsSensitivity ? { acceptsSensitivity } : {}),\n stream(req) {\n return adapter.stream(applyDefaults(req, options, capabilities));\n },\n generate(req) {\n return adapter.generate(applyDefaults(req, options, capabilities));\n },\n ...(adapter.countTokens ? { countTokens: adapter.countTokens.bind(adapter) } : {}),\n };\n return wrapped;\n}\n\nfunction applyDefaults(\n req: ProviderRequest,\n options: CreateProviderOptions,\n capabilities: ProviderCapabilities,\n): ProviderRequest {\n let next = req;\n // A1: fold worked tool examples into the model-facing description so the model\n // actually sees them (the wire contract carries them; adapters never rendered\n // them). Deterministic ⇒ the tool spec stays prompt-cache-stable.\n if (next.tools !== undefined) {\n const foldedTools = foldToolExamples(next.tools);\n if (foldedTools !== next.tools) next = { ...next, tools: foldedTools };\n }\n const effectiveRetention = resolveReasoningRetention({\n ...(next.reasoningRetention !== undefined ? { requested: next.reasoningRetention } : {}),\n ...(options.reasoningRetention !== undefined ? { overridden: options.reasoningRetention } : {}),\n ...(capabilities.reasoningContract !== undefined\n ? { contract: capabilities.reasoningContract }\n : {}),\n });\n if (effectiveRetention !== next.reasoningRetention) {\n next = { ...next, reasoningRetention: effectiveRetention };\n }\n return next;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyEA,SAAgB,eAAe,SAAmB,UAAiC,EAAE,EAAY;CAC/F,MAAMA,eAAqC;EACzC,GAAG,QAAQ;EACX,GAAG,QAAQ;EACZ;CACD,MAAM,qBAAqB,QAAQ,sBAAsB,QAAQ,sBAAsB;AAevF,QAb0B;EACxB,MAAM,QAAQ;EACd,SAAS,QAAQ;EACjB;EACA,GAAI,qBAAqB,EAAE,oBAAoB,GAAG,EAAE;EACpD,OAAO,KAAK;AACV,UAAO,QAAQ,OAAO,cAAc,KAAK,SAAS,aAAa,CAAC;;EAElE,SAAS,KAAK;AACZ,UAAO,QAAQ,SAAS,cAAc,KAAK,SAAS,aAAa,CAAC;;EAEpE,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,KAAK,QAAQ,EAAE,GAAG,EAAE;EAClF;;AAIH,SAAS,cACP,KACA,SACA,cACiB;CACjB,IAAI,OAAO;AAIX,KAAI,KAAK,UAAU,QAAW;EAC5B,MAAM,cAAc,iBAAiB,KAAK,MAAM;AAChD,MAAI,gBAAgB,KAAK,MAAO,QAAO;GAAE,GAAG;GAAM,OAAO;GAAa;;CAExE,MAAM,qBAAqB,0BAA0B;EACnD,GAAI,KAAK,uBAAuB,SAAY,EAAE,WAAW,KAAK,oBAAoB,GAAG,EAAE;EACvF,GAAI,QAAQ,uBAAuB,SAAY,EAAE,YAAY,QAAQ,oBAAoB,GAAG,EAAE;EAC9F,GAAI,aAAa,sBAAsB,SACnC,EAAE,UAAU,aAAa,mBAAmB,GAC5C,EAAE;EACP,CAAC;AACF,KAAI,uBAAuB,KAAK,mBAC9B,QAAO;EAAE,GAAG;EAAM,oBAAoB;EAAoB;AAE5D,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply-policy.js","names":["filtered: MessageContent[]"],"sources":["../../src/reasoning/apply-policy.ts"],"sourcesContent":["/**\n * `applyReasoningPolicy`
|
|
1
|
+
{"version":3,"file":"apply-policy.js","names":["filtered: MessageContent[]"],"sources":["../../src/reasoning/apply-policy.ts"],"sourcesContent":["/**\n * `applyReasoningPolicy` - given a list of `Message`s and the resolved\n * {@link ReasoningRetention}, return a transformed list with\n * `reasoning` content parts treated according to the policy:\n *\n * - `'strip'` - drop every `reasoning` content part.\n * - `'pass-through-claude'` - keep parts that carry Anthropic-shaped\n * metadata (`provider === 'anthropic'` OR a `signature` field);\n * drop the rest.\n * - `'pass-through-all'` - keep every `reasoning` content part\n * unchanged.\n *\n * The transformation is shallow: only the `assistant` role's content\n * arrays are inspected (the only role that legitimately carries\n * reasoning blocks).\n *\n * @packageDocumentation\n */\n\nimport type {\n AssistantMessage,\n Message,\n MessageContent,\n ReasoningContent,\n ReasoningRetention,\n} from '@graphorin/core';\n\n/**\n * Inputs to {@link applyReasoningPolicy}.\n *\n * @stable\n */\nexport interface ApplyReasoningPolicyInput {\n readonly messages: ReadonlyArray<Message>;\n readonly retention: ReasoningRetention;\n}\n\n/**\n * Apply the resolved retention policy to the provided messages.\n *\n * @stable\n */\nexport function applyReasoningPolicy(input: ApplyReasoningPolicyInput): ReadonlyArray<Message> {\n if (input.retention === 'pass-through-all') return input.messages;\n return input.messages.map((msg) => {\n if (msg.role !== 'assistant') return msg;\n return rewriteAssistantMessage(msg, input.retention);\n });\n}\n\nfunction rewriteAssistantMessage(\n msg: AssistantMessage,\n retention: ReasoningRetention,\n): AssistantMessage {\n const content = msg.content;\n if (typeof content === 'string') return msg;\n const filtered: MessageContent[] = [];\n let mutated = false;\n for (const part of content) {\n if (part.type !== 'reasoning') {\n filtered.push(part);\n continue;\n }\n if (retention === 'strip') {\n mutated = true;\n continue;\n }\n if (retention === 'pass-through-claude') {\n if (isAnthropicShapedReasoning(part)) {\n filtered.push(part);\n } else {\n mutated = true;\n }\n }\n }\n if (!mutated) return msg;\n return {\n ...msg,\n content: filtered,\n };\n}\n\nfunction isAnthropicShapedReasoning(part: ReasoningContent): boolean {\n const meta = part.meta;\n if (meta === undefined) return false;\n if (meta.provider === 'anthropic') return true;\n if (typeof meta.signature === 'string' && meta.signature.length > 0) return true;\n return false;\n}\n"],"mappings":";;;;;;AA0CA,SAAgB,qBAAqB,OAA0D;AAC7F,KAAI,MAAM,cAAc,mBAAoB,QAAO,MAAM;AACzD,QAAO,MAAM,SAAS,KAAK,QAAQ;AACjC,MAAI,IAAI,SAAS,YAAa,QAAO;AACrC,SAAO,wBAAwB,KAAK,MAAM,UAAU;GACpD;;AAGJ,SAAS,wBACP,KACA,WACkB;CAClB,MAAM,UAAU,IAAI;AACpB,KAAI,OAAO,YAAY,SAAU,QAAO;CACxC,MAAMA,WAA6B,EAAE;CACrC,IAAI,UAAU;AACd,MAAK,MAAM,QAAQ,SAAS;AAC1B,MAAI,KAAK,SAAS,aAAa;AAC7B,YAAS,KAAK,KAAK;AACnB;;AAEF,MAAI,cAAc,SAAS;AACzB,aAAU;AACV;;AAEF,MAAI,cAAc,sBAChB,KAAI,2BAA2B,KAAK,CAClC,UAAS,KAAK,KAAK;MAEnB,WAAU;;AAIhB,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO;EACL,GAAG;EACH,SAAS;EACV;;AAGH,SAAS,2BAA2B,MAAiC;CACnE,MAAM,OAAO,KAAK;AAClB,KAAI,SAAS,OAAW,QAAO;AAC/B,KAAI,KAAK,aAAa,YAAa,QAAO;AAC1C,KAAI,OAAO,KAAK,cAAc,YAAY,KAAK,UAAU,SAAS,EAAG,QAAO;AAC5E,QAAO"}
|
|
@@ -41,17 +41,26 @@ const REASONING_CONTRACT_RULES = Object.freeze([
|
|
|
41
41
|
function inferReasoningContract(input) {
|
|
42
42
|
if (typeof input.modelId !== "string" || input.modelId.length === 0) return "optional";
|
|
43
43
|
const provider = input.provider?.toLowerCase();
|
|
44
|
-
if (provider
|
|
44
|
+
if (provider !== void 0 && (provider.startsWith("anthropic") || provider.includes("bedrock"))) return "round-trip-required";
|
|
45
45
|
const normalised = stripPrefix(input.modelId.toLowerCase());
|
|
46
46
|
for (const rule of REASONING_CONTRACT_RULES) if (rule.pattern.test(normalised)) return rule.contract;
|
|
47
47
|
return "optional";
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Bedrock cross-region inference-profile prefix (`us.anthropic.claude-…`,
|
|
51
|
+
* the standard way to invoke Claude on Bedrock since 2025). Stripped
|
|
52
|
+
* before pattern matching so the `^anthropic\.claude` rules still fire
|
|
53
|
+
* (core-provider-11).
|
|
54
|
+
*/
|
|
55
|
+
const BEDROCK_REGION_PREFIX = /^(?:us|eu|apac|jp|au|us-gov)\./;
|
|
49
56
|
function stripPrefix(model) {
|
|
50
57
|
const slash = model.indexOf("/");
|
|
51
|
-
if (slash !== -1) return model.slice(slash + 1);
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
58
|
+
if (slash !== -1) return model.slice(slash + 1).replace(BEDROCK_REGION_PREFIX, "");
|
|
59
|
+
const deRegioned = model.replace(BEDROCK_REGION_PREFIX, "");
|
|
60
|
+
if (deRegioned.startsWith("anthropic.")) return deRegioned;
|
|
61
|
+
const colon = deRegioned.indexOf(":");
|
|
62
|
+
if (colon !== -1 && !deRegioned.startsWith("http")) return deRegioned.slice(colon + 1);
|
|
63
|
+
return deRegioned;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
//#endregion
|