gavio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +95 -0
  2. package/dist/cjs/context.js +47 -0
  3. package/dist/cjs/errors.js +57 -0
  4. package/dist/cjs/gateway.js +127 -0
  5. package/dist/cjs/ids.js +60 -0
  6. package/dist/cjs/index.js +49 -0
  7. package/dist/cjs/interceptors/audit/index.js +12 -0
  8. package/dist/cjs/interceptors/audit/interceptor.js +77 -0
  9. package/dist/cjs/interceptors/audit/record.js +107 -0
  10. package/dist/cjs/interceptors/audit/sink.js +3 -0
  11. package/dist/cjs/interceptors/audit/sinks/index.js +5 -0
  12. package/dist/cjs/interceptors/audit/sinks/stdout.js +33 -0
  13. package/dist/cjs/interceptors/base.js +7 -0
  14. package/dist/cjs/interceptors/cache/backend.js +9 -0
  15. package/dist/cjs/interceptors/cache/backends/index.js +5 -0
  16. package/dist/cjs/interceptors/cache/backends/memory.js +53 -0
  17. package/dist/cjs/interceptors/cache/index.js +9 -0
  18. package/dist/cjs/interceptors/chain.js +57 -0
  19. package/dist/cjs/interceptors/index.js +18 -0
  20. package/dist/cjs/interceptors/pii/context.js +25 -0
  21. package/dist/cjs/interceptors/pii/guard.js +161 -0
  22. package/dist/cjs/interceptors/pii/index.js +28 -0
  23. package/dist/cjs/interceptors/pii/match.js +21 -0
  24. package/dist/cjs/interceptors/pii/scanner.js +31 -0
  25. package/dist/cjs/interceptors/pii/scanners/bsn.js +41 -0
  26. package/dist/cjs/interceptors/pii/scanners/credit-card.js +51 -0
  27. package/dist/cjs/interceptors/pii/scanners/email.js +26 -0
  28. package/dist/cjs/interceptors/pii/scanners/iban.js +58 -0
  29. package/dist/cjs/interceptors/pii/scanners/index.js +45 -0
  30. package/dist/cjs/interceptors/pii/scanners/ip-address.js +36 -0
  31. package/dist/cjs/interceptors/pii/scanners/phone.js +37 -0
  32. package/dist/cjs/interceptors/pii/scanners/secret.js +46 -0
  33. package/dist/cjs/interceptors/pii/scanners/ssn.js +28 -0
  34. package/dist/cjs/interceptors/reliability/fallback.js +53 -0
  35. package/dist/cjs/interceptors/reliability/index.js +11 -0
  36. package/dist/cjs/interceptors/reliability/retry.js +69 -0
  37. package/dist/cjs/interceptors/reliability/timeout.js +41 -0
  38. package/dist/cjs/package.json +3 -0
  39. package/dist/cjs/pricing.js +70 -0
  40. package/dist/cjs/providers/anthropic.js +80 -0
  41. package/dist/cjs/providers/base.js +30 -0
  42. package/dist/cjs/providers/http.js +42 -0
  43. package/dist/cjs/providers/index.js +34 -0
  44. package/dist/cjs/providers/mock.js +54 -0
  45. package/dist/cjs/providers/openai.js +63 -0
  46. package/dist/cjs/request.js +60 -0
  47. package/dist/cjs/response.js +55 -0
  48. package/dist/cjs/testing/harness.js +70 -0
  49. package/dist/cjs/testing/index.js +8 -0
  50. package/dist/cjs/types.js +61 -0
  51. package/dist/esm/context.d.ts +33 -0
  52. package/dist/esm/context.js +43 -0
  53. package/dist/esm/errors.d.ts +36 -0
  54. package/dist/esm/errors.js +44 -0
  55. package/dist/esm/gateway.d.ts +54 -0
  56. package/dist/esm/gateway.js +123 -0
  57. package/dist/esm/ids.d.ts +11 -0
  58. package/dist/esm/ids.js +56 -0
  59. package/dist/esm/index.d.ts +25 -0
  60. package/dist/esm/index.js +20 -0
  61. package/dist/esm/interceptors/audit/index.d.ts +7 -0
  62. package/dist/esm/interceptors/audit/index.js +3 -0
  63. package/dist/esm/interceptors/audit/interceptor.d.ts +11 -0
  64. package/dist/esm/interceptors/audit/interceptor.js +72 -0
  65. package/dist/esm/interceptors/audit/record.d.ts +66 -0
  66. package/dist/esm/interceptors/audit/record.js +103 -0
  67. package/dist/esm/interceptors/audit/sink.d.ts +8 -0
  68. package/dist/esm/interceptors/audit/sink.js +2 -0
  69. package/dist/esm/interceptors/audit/sinks/index.d.ts +2 -0
  70. package/dist/esm/interceptors/audit/sinks/index.js +1 -0
  71. package/dist/esm/interceptors/audit/sinks/stdout.d.ts +8 -0
  72. package/dist/esm/interceptors/audit/sinks/stdout.js +30 -0
  73. package/dist/esm/interceptors/base.d.ts +37 -0
  74. package/dist/esm/interceptors/base.js +4 -0
  75. package/dist/esm/interceptors/cache/backend.d.ts +14 -0
  76. package/dist/esm/interceptors/cache/backend.js +8 -0
  77. package/dist/esm/interceptors/cache/backends/index.d.ts +2 -0
  78. package/dist/esm/interceptors/cache/backends/index.js +1 -0
  79. package/dist/esm/interceptors/cache/backends/memory.d.ts +7 -0
  80. package/dist/esm/interceptors/cache/backends/memory.js +50 -0
  81. package/dist/esm/interceptors/cache/index.d.ts +7 -0
  82. package/dist/esm/interceptors/cache/index.js +5 -0
  83. package/dist/esm/interceptors/chain.d.ts +17 -0
  84. package/dist/esm/interceptors/chain.js +53 -0
  85. package/dist/esm/interceptors/index.d.ts +8 -0
  86. package/dist/esm/interceptors/index.js +7 -0
  87. package/dist/esm/interceptors/pii/context.d.ts +15 -0
  88. package/dist/esm/interceptors/pii/context.js +21 -0
  89. package/dist/esm/interceptors/pii/guard.d.ts +30 -0
  90. package/dist/esm/interceptors/pii/guard.js +157 -0
  91. package/dist/esm/interceptors/pii/index.d.ts +10 -0
  92. package/dist/esm/interceptors/pii/index.js +7 -0
  93. package/dist/esm/interceptors/pii/match.d.ts +26 -0
  94. package/dist/esm/interceptors/pii/match.js +17 -0
  95. package/dist/esm/interceptors/pii/scanner.d.ts +32 -0
  96. package/dist/esm/interceptors/pii/scanner.js +26 -0
  97. package/dist/esm/interceptors/pii/scanners/bsn.d.ts +5 -0
  98. package/dist/esm/interceptors/pii/scanners/bsn.js +37 -0
  99. package/dist/esm/interceptors/pii/scanners/credit-card.d.ts +4 -0
  100. package/dist/esm/interceptors/pii/scanners/credit-card.js +47 -0
  101. package/dist/esm/interceptors/pii/scanners/email.d.ts +3 -0
  102. package/dist/esm/interceptors/pii/scanners/email.js +23 -0
  103. package/dist/esm/interceptors/pii/scanners/iban.d.ts +5 -0
  104. package/dist/esm/interceptors/pii/scanners/iban.js +54 -0
  105. package/dist/esm/interceptors/pii/scanners/index.d.ts +13 -0
  106. package/dist/esm/interceptors/pii/scanners/index.js +30 -0
  107. package/dist/esm/interceptors/pii/scanners/ip-address.d.ts +3 -0
  108. package/dist/esm/interceptors/pii/scanners/ip-address.js +33 -0
  109. package/dist/esm/interceptors/pii/scanners/phone.d.ts +6 -0
  110. package/dist/esm/interceptors/pii/scanners/phone.js +34 -0
  111. package/dist/esm/interceptors/pii/scanners/secret.d.ts +9 -0
  112. package/dist/esm/interceptors/pii/scanners/secret.js +43 -0
  113. package/dist/esm/interceptors/pii/scanners/ssn.d.ts +3 -0
  114. package/dist/esm/interceptors/pii/scanners/ssn.js +25 -0
  115. package/dist/esm/interceptors/reliability/fallback.d.ts +9 -0
  116. package/dist/esm/interceptors/reliability/fallback.js +50 -0
  117. package/dist/esm/interceptors/reliability/index.d.ts +7 -0
  118. package/dist/esm/interceptors/reliability/index.js +4 -0
  119. package/dist/esm/interceptors/reliability/retry.d.ts +13 -0
  120. package/dist/esm/interceptors/reliability/retry.js +66 -0
  121. package/dist/esm/interceptors/reliability/timeout.d.ts +9 -0
  122. package/dist/esm/interceptors/reliability/timeout.js +37 -0
  123. package/dist/esm/package.json +3 -0
  124. package/dist/esm/pricing.d.ts +19 -0
  125. package/dist/esm/pricing.js +65 -0
  126. package/dist/esm/providers/anthropic.d.ts +30 -0
  127. package/dist/esm/providers/anthropic.js +77 -0
  128. package/dist/esm/providers/base.d.ts +23 -0
  129. package/dist/esm/providers/base.js +28 -0
  130. package/dist/esm/providers/http.d.ts +8 -0
  131. package/dist/esm/providers/http.js +39 -0
  132. package/dist/esm/providers/index.d.ts +15 -0
  133. package/dist/esm/providers/index.js +25 -0
  134. package/dist/esm/providers/mock.d.ts +31 -0
  135. package/dist/esm/providers/mock.js +51 -0
  136. package/dist/esm/providers/openai.d.ts +26 -0
  137. package/dist/esm/providers/openai.js +60 -0
  138. package/dist/esm/request.d.ts +36 -0
  139. package/dist/esm/request.js +56 -0
  140. package/dist/esm/response.d.ts +38 -0
  141. package/dist/esm/response.js +51 -0
  142. package/dist/esm/testing/harness.d.ts +37 -0
  143. package/dist/esm/testing/harness.js +66 -0
  144. package/dist/esm/testing/index.d.ts +5 -0
  145. package/dist/esm/testing/index.js +3 -0
  146. package/dist/esm/types.d.ts +58 -0
  147. package/dist/esm/types.js +56 -0
  148. package/package.json +115 -0
  149. package/src/context.ts +57 -0
  150. package/src/errors.ts +47 -0
  151. package/src/gateway.ts +174 -0
  152. package/src/ids.ts +69 -0
  153. package/src/index.ts +52 -0
  154. package/src/interceptors/audit/index.ts +7 -0
  155. package/src/interceptors/audit/interceptor.ts +93 -0
  156. package/src/interceptors/audit/record.ts +138 -0
  157. package/src/interceptors/audit/sink.ts +10 -0
  158. package/src/interceptors/audit/sinks/index.ts +2 -0
  159. package/src/interceptors/audit/sinks/stdout.ts +42 -0
  160. package/src/interceptors/base.ts +58 -0
  161. package/src/interceptors/cache/backend.ts +15 -0
  162. package/src/interceptors/cache/backends/index.ts +2 -0
  163. package/src/interceptors/cache/backends/memory.ts +68 -0
  164. package/src/interceptors/cache/index.ts +8 -0
  165. package/src/interceptors/chain.ts +65 -0
  166. package/src/interceptors/index.ts +9 -0
  167. package/src/interceptors/pii/context.ts +24 -0
  168. package/src/interceptors/pii/guard.ts +201 -0
  169. package/src/interceptors/pii/index.ts +21 -0
  170. package/src/interceptors/pii/match.ts +43 -0
  171. package/src/interceptors/pii/scanner.ts +54 -0
  172. package/src/interceptors/pii/scanners/bsn.ts +44 -0
  173. package/src/interceptors/pii/scanners/credit-card.ts +52 -0
  174. package/src/interceptors/pii/scanners/email.ts +31 -0
  175. package/src/interceptors/pii/scanners/iban.ts +60 -0
  176. package/src/interceptors/pii/scanners/index.ts +35 -0
  177. package/src/interceptors/pii/scanners/ip-address.ts +41 -0
  178. package/src/interceptors/pii/scanners/phone.ts +46 -0
  179. package/src/interceptors/pii/scanners/secret.ts +51 -0
  180. package/src/interceptors/pii/scanners/ssn.ts +33 -0
  181. package/src/interceptors/reliability/fallback.ts +66 -0
  182. package/src/interceptors/reliability/index.ts +8 -0
  183. package/src/interceptors/reliability/retry.ts +97 -0
  184. package/src/interceptors/reliability/timeout.ts +53 -0
  185. package/src/pricing.ts +72 -0
  186. package/src/providers/anthropic.ts +113 -0
  187. package/src/providers/base.ts +52 -0
  188. package/src/providers/http.ts +50 -0
  189. package/src/providers/index.ts +39 -0
  190. package/src/providers/mock.ts +73 -0
  191. package/src/providers/openai.ts +94 -0
  192. package/src/request.ts +76 -0
  193. package/src/response.ts +73 -0
  194. package/src/testing/harness.ts +98 -0
  195. package/src/testing/index.ts +6 -0
  196. package/src/types.ts +83 -0
@@ -0,0 +1,33 @@
1
+ /** US Social Security Number scanner. */
2
+
3
+ import type { ScanContext } from '../context.js'
4
+ import { makeMatch } from '../match.js'
5
+ import type { PiiMatch } from '../match.js'
6
+ import type { PiiScanner } from '../scanner.js'
7
+
8
+ // AAA-GG-SSSS with hyphens or spaces. Requires a separator to avoid colliding
9
+ // with bare 9-digit numbers (handled by the BSN scanner / others).
10
+ const SSN = /\b(?!000|666|9\d\d)\d{3}[ -](?!00)\d{2}[ -](?!0000)\d{4}\b/g
11
+
12
+ export function ssnScanner(): PiiScanner {
13
+ return {
14
+ entityType: 'SSN',
15
+ tier: 1,
16
+ scan(text: string, ctx: ScanContext): PiiMatch[] {
17
+ const out: PiiMatch[] = []
18
+ for (const m of text.matchAll(SSN)) {
19
+ const idx = ctx.nextIndex('SSN')
20
+ out.push(
21
+ makeMatch({
22
+ entityType: 'SSN',
23
+ start: m.index,
24
+ end: m.index + m[0].length,
25
+ value: m[0],
26
+ replacement: `[SSN_${idx}]`,
27
+ }),
28
+ )
29
+ }
30
+ return out
31
+ },
32
+ }
33
+ }
@@ -0,0 +1,66 @@
1
+ /** fallbackChain (F-REL-02) — route to a secondary provider on failure. */
2
+
3
+ import type { InterceptorContext } from '../../context.js'
4
+ import { ProviderError } from '../../errors.js'
5
+ import type { ProviderAdapter } from '../../providers/base.js'
6
+ import type { GavioRequest } from '../../request.js'
7
+ import type { GavioResponse } from '../../response.js'
8
+ import { coerceProvider } from '../../types.js'
9
+ import type { Executor, ExecutorPolicy } from '../base.js'
10
+
11
+ export interface FallbackChainOptions {
12
+ /** Provider adapters to try, in order, when the primary call fails. */
13
+ fallbacks: ProviderAdapter[]
14
+ }
15
+
16
+ /**
17
+ * Try the primary executor; on a provider error, try fallback adapters.
18
+ *
19
+ * Each fallback is a provider adapter (anything with an async `complete`). The
20
+ * request's provider/model are rewritten per fallback so the audit record
21
+ * reflects which provider actually answered.
22
+ */
23
+ class FallbackChain implements ExecutorPolicy {
24
+ readonly name = 'fallback'
25
+ readonly isExecutorPolicy = true as const
26
+ readonly dryRunSafe = true
27
+
28
+ private readonly fallbacks: ProviderAdapter[]
29
+
30
+ constructor(options: FallbackChainOptions) {
31
+ if (!options.fallbacks || options.fallbacks.length === 0) {
32
+ throw new Error('fallbackChain requires at least one fallback adapter')
33
+ }
34
+ this.fallbacks = options.fallbacks
35
+ }
36
+
37
+ async around(
38
+ request: GavioRequest,
39
+ ctx: InterceptorContext,
40
+ callNext: Executor,
41
+ ): Promise<GavioResponse> {
42
+ ctx.markFired(this.name)
43
+ try {
44
+ return await callNext(request)
45
+ } catch (primaryError) {
46
+ if (!(primaryError instanceof ProviderError)) throw primaryError
47
+ let lastError: unknown = primaryError
48
+ for (const adapter of this.fallbacks) {
49
+ try {
50
+ const rerouted = request.copyWithMessages(request.messages)
51
+ rerouted.provider = coerceProvider(adapter.providerName)
52
+ return await adapter.complete(rerouted)
53
+ } catch (error) {
54
+ if (!(error instanceof ProviderError)) throw error
55
+ lastError = error
56
+ }
57
+ }
58
+ throw lastError
59
+ }
60
+ }
61
+ }
62
+
63
+ /** Factory: build a fallback policy. */
64
+ export function fallbackChain(options: FallbackChainOptions): ExecutorPolicy {
65
+ return new FallbackChain(options)
66
+ }
@@ -0,0 +1,8 @@
1
+ /** Reliability policies (F-REL-01, F-REL-02, F-REL-07). */
2
+
3
+ export { retryInterceptor } from './retry.js'
4
+ export type { RetryInterceptorOptions } from './retry.js'
5
+ export { timeoutPolicy, timeout } from './timeout.js'
6
+ export type { TimeoutPolicyOptions } from './timeout.js'
7
+ export { fallbackChain } from './fallback.js'
8
+ export type { FallbackChainOptions } from './fallback.js'
@@ -0,0 +1,97 @@
1
+ /** retryInterceptor (F-REL-01) — exponential backoff with jitter. */
2
+
3
+ import { randomBytes } from 'node:crypto'
4
+ import type { InterceptorContext } from '../../context.js'
5
+ import {
6
+ ProviderUnavailableError,
7
+ RateLimitError,
8
+ ServerError,
9
+ TimeoutError,
10
+ } from '../../errors.js'
11
+ import type { GavioRequest } from '../../request.js'
12
+ import type { GavioResponse } from '../../response.js'
13
+ import type { Executor, ExecutorPolicy } from '../base.js'
14
+
15
+ /** Default predicate: retry transient provider errors. */
16
+ function defaultRetryable(error: unknown): boolean {
17
+ return (
18
+ error instanceof RateLimitError ||
19
+ error instanceof TimeoutError ||
20
+ error instanceof ServerError ||
21
+ error instanceof ProviderUnavailableError
22
+ )
23
+ }
24
+
25
+ export interface RetryInterceptorOptions {
26
+ maxAttempts?: number
27
+ baseDelayMs?: number
28
+ maxDelayMs?: number
29
+ jitter?: boolean
30
+ retryOn?: (error: unknown) => boolean
31
+ /** Override the sleep implementation (used in tests). */
32
+ sleep?: (ms: number) => Promise<void>
33
+ }
34
+
35
+ const defaultSleep = (ms: number): Promise<void> =>
36
+ new Promise((resolve) => setTimeout(resolve, ms))
37
+
38
+ class RetryInterceptor implements ExecutorPolicy {
39
+ readonly name = 'retry'
40
+ readonly isExecutorPolicy = true as const
41
+ readonly dryRunSafe = true
42
+
43
+ private readonly maxAttempts: number
44
+ private readonly baseDelayMs: number
45
+ private readonly maxDelayMs: number
46
+ private readonly jitter: boolean
47
+ private readonly retryOn: (error: unknown) => boolean
48
+ private readonly sleep: (ms: number) => Promise<void>
49
+
50
+ constructor(options: RetryInterceptorOptions = {}) {
51
+ const maxAttempts = options.maxAttempts ?? 3
52
+ if (maxAttempts < 1) throw new Error('maxAttempts must be >= 1')
53
+ this.maxAttempts = maxAttempts
54
+ this.baseDelayMs = options.baseDelayMs ?? 500
55
+ this.maxDelayMs = options.maxDelayMs ?? 10_000
56
+ this.jitter = options.jitter ?? true
57
+ this.retryOn = options.retryOn ?? defaultRetryable
58
+ this.sleep = options.sleep ?? defaultSleep
59
+ }
60
+
61
+ async around(
62
+ request: GavioRequest,
63
+ ctx: InterceptorContext,
64
+ callNext: Executor,
65
+ ): Promise<GavioResponse> {
66
+ ctx.markFired(this.name)
67
+ let lastError: unknown
68
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
69
+ try {
70
+ return await callNext(request)
71
+ } catch (error) {
72
+ if (!this.retryOn(error)) throw error
73
+ lastError = error
74
+ if (attempt >= this.maxAttempts) break
75
+ await this.sleep(this.delayMs(attempt))
76
+ }
77
+ }
78
+ throw lastError
79
+ }
80
+
81
+ private delayMs(attempt: number): number {
82
+ // Exponential: base * 2^(attempt-1), capped, with optional full jitter.
83
+ const raw = this.baseDelayMs * 2 ** (attempt - 1)
84
+ let capped = Math.min(raw, this.maxDelayMs)
85
+ if (this.jitter) {
86
+ // full jitter in [0, capped] using crypto bytes (no global RNG state)
87
+ const frac = randomBytes(2).readUInt16BE(0) / 0xffff
88
+ capped *= frac
89
+ }
90
+ return capped
91
+ }
92
+ }
93
+
94
+ /** Factory: build a retry policy. */
95
+ export function retryInterceptor(options: RetryInterceptorOptions = {}): ExecutorPolicy {
96
+ return new RetryInterceptor(options)
97
+ }
@@ -0,0 +1,53 @@
1
+ /** timeoutPolicy (F-REL-07) — per-request timeout enforcement. */
2
+
3
+ import type { InterceptorContext } from '../../context.js'
4
+ import { TimeoutError } from '../../errors.js'
5
+ import type { GavioRequest } from '../../request.js'
6
+ import type { GavioResponse } from '../../response.js'
7
+ import type { Executor, ExecutorPolicy } from '../base.js'
8
+
9
+ export interface TimeoutPolicyOptions {
10
+ timeoutSeconds?: number
11
+ }
12
+
13
+ class TimeoutPolicy implements ExecutorPolicy {
14
+ readonly name = 'timeout'
15
+ readonly isExecutorPolicy = true as const
16
+ readonly dryRunSafe = true
17
+
18
+ private readonly timeoutSeconds: number
19
+
20
+ constructor(options: TimeoutPolicyOptions = {}) {
21
+ const timeoutSeconds = options.timeoutSeconds ?? 30.0
22
+ if (timeoutSeconds <= 0) throw new Error('timeoutSeconds must be > 0')
23
+ this.timeoutSeconds = timeoutSeconds
24
+ }
25
+
26
+ async around(
27
+ request: GavioRequest,
28
+ ctx: InterceptorContext,
29
+ callNext: Executor,
30
+ ): Promise<GavioResponse> {
31
+ ctx.markFired(this.name)
32
+ const ms = this.timeoutSeconds * 1000
33
+ let timer: ReturnType<typeof setTimeout> | undefined
34
+ const timeout = new Promise<never>((_, reject) => {
35
+ timer = setTimeout(() => {
36
+ reject(new TimeoutError(`Request exceeded ${this.timeoutSeconds}s timeout`))
37
+ }, ms)
38
+ })
39
+ try {
40
+ return await Promise.race([callNext(request), timeout])
41
+ } finally {
42
+ if (timer !== undefined) clearTimeout(timer)
43
+ }
44
+ }
45
+ }
46
+
47
+ /** Factory: build a timeout policy. */
48
+ export function timeoutPolicy(options: TimeoutPolicyOptions = {}): ExecutorPolicy {
49
+ return new TimeoutPolicy(options)
50
+ }
51
+
52
+ /** Alias matching the SDK plan naming (`timeout`). */
53
+ export const timeout = timeoutPolicy
package/src/pricing.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Token cost tracking (F-GOV-01).
3
+ *
4
+ * Prices are USD per 1,000 tokens, sourced from public provider pricing and
5
+ * overridable. Unknown models price at zero (warned once) rather than guessing.
6
+ * Prices are intentionally data, not code — update the table, not the estimator.
7
+ */
8
+
9
+ import type { TokenUsage } from './types.js'
10
+
11
+ /** model -> [inputPer1kUsd, outputPer1kUsd] */
12
+ const DEFAULT_PRICES: Record<string, [number, number]> = {
13
+ // OpenAI
14
+ 'gpt-4o': [0.0025, 0.01],
15
+ 'gpt-4o-mini': [0.00015, 0.0006],
16
+ o1: [0.015, 0.06],
17
+ 'o1-mini': [0.0011, 0.0044],
18
+ // Anthropic
19
+ 'claude-sonnet-4-6': [0.003, 0.015],
20
+ 'claude-sonnet-4-20250514': [0.003, 0.015],
21
+ 'claude-haiku-4-5': [0.0008, 0.004],
22
+ 'claude-opus-4-1': [0.015, 0.075],
23
+ // Local / mock are free.
24
+ mock: [0.0, 0.0],
25
+ }
26
+
27
+ /** Estimates request cost from token usage and a model price table. */
28
+ export class PricingProvider {
29
+ private prices: Record<string, [number, number]>
30
+ private warned = new Set<string>()
31
+
32
+ constructor(prices?: Record<string, [number, number]>) {
33
+ this.prices = { ...DEFAULT_PRICES, ...(prices ?? {}) }
34
+ }
35
+
36
+ setPrice(model: string, inputPer1k: number, outputPer1k: number): void {
37
+ this.prices[model] = [inputPer1k, outputPer1k]
38
+ }
39
+
40
+ rates(model: string): [number, number] {
41
+ const rate = this.prices[model]
42
+ if (rate !== undefined) return rate
43
+ // try a prefix match (e.g. "gpt-4o-2024-..." -> "gpt-4o")
44
+ for (const [known, value] of Object.entries(this.prices)) {
45
+ if (model.startsWith(known)) return value
46
+ }
47
+ if (!this.warned.has(model)) {
48
+ // eslint-disable-next-line no-console
49
+ console.warn(`[gavio:pricing] no pricing for model '${model}'; treating as free`)
50
+ this.warned.add(model)
51
+ }
52
+ return [0.0, 0.0]
53
+ }
54
+
55
+ estimate(model: string, usage: TokenUsage): number {
56
+ const [inRate, outRate] = this.rates(model)
57
+ let cost = (usage.promptTokens / 1000.0) * inRate
58
+ cost += (usage.completionTokens / 1000.0) * outRate
59
+ return roundTo(cost, 8)
60
+ }
61
+ }
62
+
63
+ function roundTo(value: number, decimals: number): number {
64
+ const factor = 10 ** decimals
65
+ return Math.round(value * factor) / factor
66
+ }
67
+
68
+ /** Rough token estimate (~4 chars/token) for providers without a tokenizer. */
69
+ export function estimateTokens(text: string): number {
70
+ if (!text) return 0
71
+ return Math.max(1, Math.floor(text.length / 4))
72
+ }
@@ -0,0 +1,113 @@
1
+ /** anthropicAdapter — Messages API (Claude Sonnet, Haiku, Opus). */
2
+
3
+ import { ConfigurationError } from '../errors.js'
4
+ import type { PricingProvider } from '../pricing.js'
5
+ import type { GavioRequest } from '../request.js'
6
+ import type { GavioResponse } from '../response.js'
7
+ import { TokenUsage } from '../types.js'
8
+ import type { Message } from '../types.js'
9
+ import { BaseProviderAdapter } from './base.js'
10
+ import { postJson } from './http.js'
11
+
12
+ const DEFAULT_BASE_URL = 'https://api.anthropic.com/v1'
13
+ const API_VERSION = '2023-06-01'
14
+
15
+ export interface AnthropicAdapterOptions {
16
+ apiKey?: string
17
+ baseUrl?: string
18
+ timeoutMs?: number
19
+ pricing?: PricingProvider
20
+ }
21
+
22
+ /**
23
+ * Talks to the Anthropic Messages endpoint. Anthropic splits the system prompt
24
+ * from the message list, so any `role === "system"` messages are extracted into
25
+ * the `system` field.
26
+ */
27
+ class AnthropicAdapter extends BaseProviderAdapter {
28
+ private readonly apiKey: string | undefined
29
+ private readonly baseUrl: string
30
+ private readonly timeoutSeconds: number
31
+
32
+ constructor(options: AnthropicAdapterOptions = {}) {
33
+ super(options.pricing)
34
+ this.apiKey = options.apiKey ?? process.env['ANTHROPIC_API_KEY']
35
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '')
36
+ this.timeoutSeconds = (options.timeoutMs ?? 30_000) / 1000
37
+ }
38
+
39
+ get providerName(): string {
40
+ return 'anthropic'
41
+ }
42
+
43
+ private headers(): Record<string, string> {
44
+ if (!this.apiKey) {
45
+ throw new ConfigurationError(
46
+ 'ANTHROPIC_API_KEY not set (pass apiKey or set the env var)',
47
+ )
48
+ }
49
+ return {
50
+ 'x-api-key': this.apiKey,
51
+ 'anthropic-version': API_VERSION,
52
+ }
53
+ }
54
+
55
+ private static splitSystem(messages: Message[]): [string | null, Message[]] {
56
+ const systemParts = messages
57
+ .filter((m) => m.role === 'system')
58
+ .map((m) => m.content)
59
+ const chat = messages.filter((m) => m.role !== 'system')
60
+ const system = systemParts.length > 0 ? systemParts.join('\n') : null
61
+ return [system, chat]
62
+ }
63
+
64
+ async complete(request: GavioRequest): Promise<GavioResponse> {
65
+ const started = performance.now()
66
+ const [system, chat] = AnthropicAdapter.splitSystem(request.messages)
67
+ const payload: Record<string, unknown> = {
68
+ model: request.model,
69
+ messages: chat,
70
+ max_tokens: request.maxTokens,
71
+ temperature: request.temperature,
72
+ }
73
+ if (system) payload['system'] = system
74
+
75
+ const data = await postJson(
76
+ `${this.baseUrl}/messages`,
77
+ payload,
78
+ this.headers(),
79
+ this.timeoutSeconds,
80
+ )
81
+ const blocks = (data['content'] as Array<Record<string, unknown>>) ?? []
82
+ const content = blocks
83
+ .filter((b) => b['type'] === 'text')
84
+ .map((b) => (b['text'] as string) ?? '')
85
+ .join('')
86
+ const usageData = (data['usage'] as Record<string, number>) ?? {}
87
+ const usage = new TokenUsage(
88
+ usageData['input_tokens'] ?? 0,
89
+ usageData['output_tokens'] ?? 0,
90
+ )
91
+ return this.buildResponse(
92
+ request,
93
+ content,
94
+ usage,
95
+ (data['model'] as string) ?? request.model,
96
+ started,
97
+ )
98
+ }
99
+
100
+ async healthCheck(): Promise<boolean> {
101
+ try {
102
+ this.headers()
103
+ return true
104
+ } catch {
105
+ return false
106
+ }
107
+ }
108
+ }
109
+
110
+ /** Factory: build an Anthropic provider adapter. */
111
+ export function anthropicAdapter(options: AnthropicAdapterOptions = {}): AnthropicAdapter {
112
+ return new AnthropicAdapter(options)
113
+ }
@@ -0,0 +1,52 @@
1
+ /** ProviderAdapter interface and shared response-building helpers. */
2
+
3
+ import { PricingProvider } from '../pricing.js'
4
+ import { GavioRequest } from '../request.js'
5
+ import { GavioResponse } from '../response.js'
6
+ import { TokenUsage } from '../types.js'
7
+
8
+ /** Adapter to one LLM provider. */
9
+ export interface ProviderAdapter {
10
+ readonly providerName: string
11
+ complete(request: GavioRequest): Promise<GavioResponse>
12
+ stream?(request: GavioRequest): AsyncIterable<string>
13
+ healthCheck(): Promise<boolean>
14
+ readonly reportedModelVersion?: string | null
15
+ }
16
+
17
+ /** Base class with a shared pricing provider and response builder. */
18
+ export abstract class BaseProviderAdapter implements ProviderAdapter {
19
+ protected readonly pricing: PricingProvider
20
+
21
+ constructor(pricing?: PricingProvider) {
22
+ this.pricing = pricing ?? new PricingProvider()
23
+ }
24
+
25
+ abstract get providerName(): string
26
+ abstract complete(request: GavioRequest): Promise<GavioResponse>
27
+ abstract healthCheck(): Promise<boolean>
28
+
29
+ get reportedModelVersion(): string | null {
30
+ return null
31
+ }
32
+
33
+ protected buildResponse(
34
+ request: GavioRequest,
35
+ content: string,
36
+ usage: TokenUsage,
37
+ modelVersion: string,
38
+ startedAt: number,
39
+ ): GavioResponse {
40
+ const latencyMs = Math.floor(performance.now() - startedAt)
41
+ return new GavioResponse({
42
+ traceId: request.traceId,
43
+ content,
44
+ model: request.model,
45
+ provider: this.providerName,
46
+ modelVersion: modelVersion || request.model,
47
+ usage,
48
+ costUsd: this.pricing.estimate(request.model, usage),
49
+ latencyMs,
50
+ })
51
+ }
52
+ }
@@ -0,0 +1,50 @@
1
+ /** Tiny JSON-over-HTTP helper built on native fetch (keeps core dependency-free). */
2
+
3
+ import {
4
+ ProviderUnavailableError,
5
+ RateLimitError,
6
+ ServerError,
7
+ } from '../errors.js'
8
+
9
+ /**
10
+ * POST `payload` as JSON and return the parsed response.
11
+ *
12
+ * Maps HTTP status families onto Gavio's transient error types so the
13
+ * retry/fallback policies can react.
14
+ */
15
+ export async function postJson(
16
+ url: string,
17
+ payload: unknown,
18
+ headers: Record<string, string>,
19
+ timeoutSeconds = 30.0,
20
+ ): Promise<Record<string, unknown>> {
21
+ const controller = new AbortController()
22
+ const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1000)
23
+ let resp: Response
24
+ try {
25
+ resp = await fetch(url, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json', ...headers },
28
+ body: JSON.stringify(payload),
29
+ signal: controller.signal,
30
+ })
31
+ } catch (error) {
32
+ const reason = error instanceof Error ? error.message : String(error)
33
+ throw new ProviderUnavailableError(`network error: ${reason}`)
34
+ } finally {
35
+ clearTimeout(timer)
36
+ }
37
+
38
+ if (!resp.ok) {
39
+ const body = (await resp.text().catch(() => '')).slice(0, 200)
40
+ if (resp.status === 429) {
41
+ throw new RateLimitError(`429 from provider: ${body}`)
42
+ }
43
+ if (resp.status >= 500) {
44
+ throw new ServerError(`${resp.status} from provider: ${body}`)
45
+ }
46
+ throw new ProviderUnavailableError(`${resp.status} from provider: ${body}`)
47
+ }
48
+
49
+ return (await resp.json()) as Record<string, unknown>
50
+ }
@@ -0,0 +1,39 @@
1
+ /** Provider adapters and the provider registry. */
2
+
3
+ import { ConfigurationError } from '../errors.js'
4
+ import type { PricingProvider } from '../pricing.js'
5
+ import { Provider, coerceProvider } from '../types.js'
6
+ import { anthropicAdapter } from './anthropic.js'
7
+ import type { ProviderAdapter } from './base.js'
8
+ import { mockProvider } from './mock.js'
9
+ import { openaiAdapter } from './openai.js'
10
+
11
+ export type { ProviderAdapter } from './base.js'
12
+ export { BaseProviderAdapter } from './base.js'
13
+ export { mockProvider } from './mock.js'
14
+ export type { MockProviderOptions } from './mock.js'
15
+ export { openaiAdapter } from './openai.js'
16
+ export type { OpenAIAdapterOptions } from './openai.js'
17
+ export { anthropicAdapter } from './anthropic.js'
18
+ export type { AnthropicAdapterOptions } from './anthropic.js'
19
+ export { Provider } from '../types.js'
20
+
21
+ /** Instantiate the default adapter for a provider id. v0.1.0: OpenAI, Anthropic, Mock. */
22
+ export function buildAdapter(
23
+ provider: Provider | string,
24
+ pricing?: PricingProvider,
25
+ ): ProviderAdapter {
26
+ const p = coerceProvider(provider)
27
+ switch (p) {
28
+ case Provider.OPENAI:
29
+ return openaiAdapter(pricing ? { pricing } : {})
30
+ case Provider.ANTHROPIC:
31
+ return anthropicAdapter(pricing ? { pricing } : {})
32
+ case Provider.MOCK:
33
+ return mockProvider(pricing ? { pricing } : {})
34
+ default:
35
+ throw new ConfigurationError(
36
+ `Provider '${p}' is not available in v0.1.0 (available: openai, anthropic, mock)`,
37
+ )
38
+ }
39
+ }
@@ -0,0 +1,73 @@
1
+ /** mockProvider — deterministic, offline provider for dev mode and tests. */
2
+
3
+ import { PricingProvider, estimateTokens } from '../pricing.js'
4
+ import type { GavioRequest } from '../request.js'
5
+ import type { GavioResponse } from '../response.js'
6
+ import { TokenUsage } from '../types.js'
7
+ import { BaseProviderAdapter } from './base.js'
8
+
9
+ export interface MockProviderOptions {
10
+ response?: string | null
11
+ modelVersion?: string
12
+ pricing?: PricingProvider
13
+ }
14
+
15
+ /**
16
+ * Returns a canned response without any network call.
17
+ *
18
+ * If `response` is null/undefined, it echoes the last user message so the
19
+ * pipeline (including PII restore) is observable end to end.
20
+ */
21
+ class MockProvider extends BaseProviderAdapter {
22
+ private readonly response: string | null
23
+ private readonly modelVersion: string
24
+
25
+ constructor(options: MockProviderOptions = {}) {
26
+ super(options.pricing)
27
+ this.response = options.response ?? null
28
+ this.modelVersion = options.modelVersion ?? 'mock-1'
29
+ }
30
+
31
+ get providerName(): string {
32
+ return 'mock'
33
+ }
34
+
35
+ override get reportedModelVersion(): string | null {
36
+ return this.modelVersion
37
+ }
38
+
39
+ private contentFor(request: GavioRequest): string {
40
+ if (this.response !== null) return this.response
41
+ const lastUser = [...request.messages]
42
+ .reverse()
43
+ .find((m) => m.role === 'user')
44
+ return `[mock reply] ${lastUser?.content ?? ''}`
45
+ }
46
+
47
+ async complete(request: GavioRequest): Promise<GavioResponse> {
48
+ const started = performance.now()
49
+ const content = this.contentFor(request)
50
+ const usage = new TokenUsage(
51
+ estimateTokens(request.promptText()),
52
+ estimateTokens(content),
53
+ )
54
+ return this.buildResponse(request, content, usage, this.modelVersion, started)
55
+ }
56
+
57
+ async *stream(request: GavioRequest): AsyncIterable<string> {
58
+ for (const token of this.contentFor(request).split(' ')) {
59
+ yield token + ' '
60
+ }
61
+ }
62
+
63
+ async healthCheck(): Promise<boolean> {
64
+ return true
65
+ }
66
+ }
67
+
68
+ /** Factory: build a mock provider adapter. */
69
+ export function mockProvider(options: MockProviderOptions = {}): ProviderAdapterMock {
70
+ return new MockProvider(options)
71
+ }
72
+
73
+ export type ProviderAdapterMock = MockProvider