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.
- package/README.md +95 -0
- package/dist/cjs/context.js +47 -0
- package/dist/cjs/errors.js +57 -0
- package/dist/cjs/gateway.js +127 -0
- package/dist/cjs/ids.js +60 -0
- package/dist/cjs/index.js +49 -0
- package/dist/cjs/interceptors/audit/index.js +12 -0
- package/dist/cjs/interceptors/audit/interceptor.js +77 -0
- package/dist/cjs/interceptors/audit/record.js +107 -0
- package/dist/cjs/interceptors/audit/sink.js +3 -0
- package/dist/cjs/interceptors/audit/sinks/index.js +5 -0
- package/dist/cjs/interceptors/audit/sinks/stdout.js +33 -0
- package/dist/cjs/interceptors/base.js +7 -0
- package/dist/cjs/interceptors/cache/backend.js +9 -0
- package/dist/cjs/interceptors/cache/backends/index.js +5 -0
- package/dist/cjs/interceptors/cache/backends/memory.js +53 -0
- package/dist/cjs/interceptors/cache/index.js +9 -0
- package/dist/cjs/interceptors/chain.js +57 -0
- package/dist/cjs/interceptors/index.js +18 -0
- package/dist/cjs/interceptors/pii/context.js +25 -0
- package/dist/cjs/interceptors/pii/guard.js +161 -0
- package/dist/cjs/interceptors/pii/index.js +28 -0
- package/dist/cjs/interceptors/pii/match.js +21 -0
- package/dist/cjs/interceptors/pii/scanner.js +31 -0
- package/dist/cjs/interceptors/pii/scanners/bsn.js +41 -0
- package/dist/cjs/interceptors/pii/scanners/credit-card.js +51 -0
- package/dist/cjs/interceptors/pii/scanners/email.js +26 -0
- package/dist/cjs/interceptors/pii/scanners/iban.js +58 -0
- package/dist/cjs/interceptors/pii/scanners/index.js +45 -0
- package/dist/cjs/interceptors/pii/scanners/ip-address.js +36 -0
- package/dist/cjs/interceptors/pii/scanners/phone.js +37 -0
- package/dist/cjs/interceptors/pii/scanners/secret.js +46 -0
- package/dist/cjs/interceptors/pii/scanners/ssn.js +28 -0
- package/dist/cjs/interceptors/reliability/fallback.js +53 -0
- package/dist/cjs/interceptors/reliability/index.js +11 -0
- package/dist/cjs/interceptors/reliability/retry.js +69 -0
- package/dist/cjs/interceptors/reliability/timeout.js +41 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pricing.js +70 -0
- package/dist/cjs/providers/anthropic.js +80 -0
- package/dist/cjs/providers/base.js +30 -0
- package/dist/cjs/providers/http.js +42 -0
- package/dist/cjs/providers/index.js +34 -0
- package/dist/cjs/providers/mock.js +54 -0
- package/dist/cjs/providers/openai.js +63 -0
- package/dist/cjs/request.js +60 -0
- package/dist/cjs/response.js +55 -0
- package/dist/cjs/testing/harness.js +70 -0
- package/dist/cjs/testing/index.js +8 -0
- package/dist/cjs/types.js +61 -0
- package/dist/esm/context.d.ts +33 -0
- package/dist/esm/context.js +43 -0
- package/dist/esm/errors.d.ts +36 -0
- package/dist/esm/errors.js +44 -0
- package/dist/esm/gateway.d.ts +54 -0
- package/dist/esm/gateway.js +123 -0
- package/dist/esm/ids.d.ts +11 -0
- package/dist/esm/ids.js +56 -0
- package/dist/esm/index.d.ts +25 -0
- package/dist/esm/index.js +20 -0
- package/dist/esm/interceptors/audit/index.d.ts +7 -0
- package/dist/esm/interceptors/audit/index.js +3 -0
- package/dist/esm/interceptors/audit/interceptor.d.ts +11 -0
- package/dist/esm/interceptors/audit/interceptor.js +72 -0
- package/dist/esm/interceptors/audit/record.d.ts +66 -0
- package/dist/esm/interceptors/audit/record.js +103 -0
- package/dist/esm/interceptors/audit/sink.d.ts +8 -0
- package/dist/esm/interceptors/audit/sink.js +2 -0
- package/dist/esm/interceptors/audit/sinks/index.d.ts +2 -0
- package/dist/esm/interceptors/audit/sinks/index.js +1 -0
- package/dist/esm/interceptors/audit/sinks/stdout.d.ts +8 -0
- package/dist/esm/interceptors/audit/sinks/stdout.js +30 -0
- package/dist/esm/interceptors/base.d.ts +37 -0
- package/dist/esm/interceptors/base.js +4 -0
- package/dist/esm/interceptors/cache/backend.d.ts +14 -0
- package/dist/esm/interceptors/cache/backend.js +8 -0
- package/dist/esm/interceptors/cache/backends/index.d.ts +2 -0
- package/dist/esm/interceptors/cache/backends/index.js +1 -0
- package/dist/esm/interceptors/cache/backends/memory.d.ts +7 -0
- package/dist/esm/interceptors/cache/backends/memory.js +50 -0
- package/dist/esm/interceptors/cache/index.d.ts +7 -0
- package/dist/esm/interceptors/cache/index.js +5 -0
- package/dist/esm/interceptors/chain.d.ts +17 -0
- package/dist/esm/interceptors/chain.js +53 -0
- package/dist/esm/interceptors/index.d.ts +8 -0
- package/dist/esm/interceptors/index.js +7 -0
- package/dist/esm/interceptors/pii/context.d.ts +15 -0
- package/dist/esm/interceptors/pii/context.js +21 -0
- package/dist/esm/interceptors/pii/guard.d.ts +30 -0
- package/dist/esm/interceptors/pii/guard.js +157 -0
- package/dist/esm/interceptors/pii/index.d.ts +10 -0
- package/dist/esm/interceptors/pii/index.js +7 -0
- package/dist/esm/interceptors/pii/match.d.ts +26 -0
- package/dist/esm/interceptors/pii/match.js +17 -0
- package/dist/esm/interceptors/pii/scanner.d.ts +32 -0
- package/dist/esm/interceptors/pii/scanner.js +26 -0
- package/dist/esm/interceptors/pii/scanners/bsn.d.ts +5 -0
- package/dist/esm/interceptors/pii/scanners/bsn.js +37 -0
- package/dist/esm/interceptors/pii/scanners/credit-card.d.ts +4 -0
- package/dist/esm/interceptors/pii/scanners/credit-card.js +47 -0
- package/dist/esm/interceptors/pii/scanners/email.d.ts +3 -0
- package/dist/esm/interceptors/pii/scanners/email.js +23 -0
- package/dist/esm/interceptors/pii/scanners/iban.d.ts +5 -0
- package/dist/esm/interceptors/pii/scanners/iban.js +54 -0
- package/dist/esm/interceptors/pii/scanners/index.d.ts +13 -0
- package/dist/esm/interceptors/pii/scanners/index.js +30 -0
- package/dist/esm/interceptors/pii/scanners/ip-address.d.ts +3 -0
- package/dist/esm/interceptors/pii/scanners/ip-address.js +33 -0
- package/dist/esm/interceptors/pii/scanners/phone.d.ts +6 -0
- package/dist/esm/interceptors/pii/scanners/phone.js +34 -0
- package/dist/esm/interceptors/pii/scanners/secret.d.ts +9 -0
- package/dist/esm/interceptors/pii/scanners/secret.js +43 -0
- package/dist/esm/interceptors/pii/scanners/ssn.d.ts +3 -0
- package/dist/esm/interceptors/pii/scanners/ssn.js +25 -0
- package/dist/esm/interceptors/reliability/fallback.d.ts +9 -0
- package/dist/esm/interceptors/reliability/fallback.js +50 -0
- package/dist/esm/interceptors/reliability/index.d.ts +7 -0
- package/dist/esm/interceptors/reliability/index.js +4 -0
- package/dist/esm/interceptors/reliability/retry.d.ts +13 -0
- package/dist/esm/interceptors/reliability/retry.js +66 -0
- package/dist/esm/interceptors/reliability/timeout.d.ts +9 -0
- package/dist/esm/interceptors/reliability/timeout.js +37 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/pricing.d.ts +19 -0
- package/dist/esm/pricing.js +65 -0
- package/dist/esm/providers/anthropic.d.ts +30 -0
- package/dist/esm/providers/anthropic.js +77 -0
- package/dist/esm/providers/base.d.ts +23 -0
- package/dist/esm/providers/base.js +28 -0
- package/dist/esm/providers/http.d.ts +8 -0
- package/dist/esm/providers/http.js +39 -0
- package/dist/esm/providers/index.d.ts +15 -0
- package/dist/esm/providers/index.js +25 -0
- package/dist/esm/providers/mock.d.ts +31 -0
- package/dist/esm/providers/mock.js +51 -0
- package/dist/esm/providers/openai.d.ts +26 -0
- package/dist/esm/providers/openai.js +60 -0
- package/dist/esm/request.d.ts +36 -0
- package/dist/esm/request.js +56 -0
- package/dist/esm/response.d.ts +38 -0
- package/dist/esm/response.js +51 -0
- package/dist/esm/testing/harness.d.ts +37 -0
- package/dist/esm/testing/harness.js +66 -0
- package/dist/esm/testing/index.d.ts +5 -0
- package/dist/esm/testing/index.js +3 -0
- package/dist/esm/types.d.ts +58 -0
- package/dist/esm/types.js +56 -0
- package/package.json +115 -0
- package/src/context.ts +57 -0
- package/src/errors.ts +47 -0
- package/src/gateway.ts +174 -0
- package/src/ids.ts +69 -0
- package/src/index.ts +52 -0
- package/src/interceptors/audit/index.ts +7 -0
- package/src/interceptors/audit/interceptor.ts +93 -0
- package/src/interceptors/audit/record.ts +138 -0
- package/src/interceptors/audit/sink.ts +10 -0
- package/src/interceptors/audit/sinks/index.ts +2 -0
- package/src/interceptors/audit/sinks/stdout.ts +42 -0
- package/src/interceptors/base.ts +58 -0
- package/src/interceptors/cache/backend.ts +15 -0
- package/src/interceptors/cache/backends/index.ts +2 -0
- package/src/interceptors/cache/backends/memory.ts +68 -0
- package/src/interceptors/cache/index.ts +8 -0
- package/src/interceptors/chain.ts +65 -0
- package/src/interceptors/index.ts +9 -0
- package/src/interceptors/pii/context.ts +24 -0
- package/src/interceptors/pii/guard.ts +201 -0
- package/src/interceptors/pii/index.ts +21 -0
- package/src/interceptors/pii/match.ts +43 -0
- package/src/interceptors/pii/scanner.ts +54 -0
- package/src/interceptors/pii/scanners/bsn.ts +44 -0
- package/src/interceptors/pii/scanners/credit-card.ts +52 -0
- package/src/interceptors/pii/scanners/email.ts +31 -0
- package/src/interceptors/pii/scanners/iban.ts +60 -0
- package/src/interceptors/pii/scanners/index.ts +35 -0
- package/src/interceptors/pii/scanners/ip-address.ts +41 -0
- package/src/interceptors/pii/scanners/phone.ts +46 -0
- package/src/interceptors/pii/scanners/secret.ts +51 -0
- package/src/interceptors/pii/scanners/ssn.ts +33 -0
- package/src/interceptors/reliability/fallback.ts +66 -0
- package/src/interceptors/reliability/index.ts +8 -0
- package/src/interceptors/reliability/retry.ts +97 -0
- package/src/interceptors/reliability/timeout.ts +53 -0
- package/src/pricing.ts +72 -0
- package/src/providers/anthropic.ts +113 -0
- package/src/providers/base.ts +52 -0
- package/src/providers/http.ts +50 -0
- package/src/providers/index.ts +39 -0
- package/src/providers/mock.ts +73 -0
- package/src/providers/openai.ts +94 -0
- package/src/request.ts +76 -0
- package/src/response.ts +73 -0
- package/src/testing/harness.ts +98 -0
- package/src/testing/index.ts +6 -0
- package/src/types.ts +83 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gavio — the open standard AI gateway for production systems.
|
|
3
|
+
*
|
|
4
|
+
* Public API surface (v0.1.0):
|
|
5
|
+
*
|
|
6
|
+
* import { Gateway, GavioRequest, GavioResponse, Provider } from 'gavio'
|
|
7
|
+
*
|
|
8
|
+
* See https://gavio.io for documentation. MIT licensed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const VERSION = '0.1.0'
|
|
12
|
+
|
|
13
|
+
export { Gateway } from './gateway.js'
|
|
14
|
+
export type { GatewayOptions, CompleteOptions } from './gateway.js'
|
|
15
|
+
|
|
16
|
+
export { GavioRequest } from './request.js'
|
|
17
|
+
export type { GavioRequestInit } from './request.js'
|
|
18
|
+
export { GavioResponse } from './response.js'
|
|
19
|
+
export type { GavioResponseInit } from './response.js'
|
|
20
|
+
export { InterceptorContext } from './context.js'
|
|
21
|
+
export type { InterceptorContextInit } from './context.js'
|
|
22
|
+
|
|
23
|
+
export { uuid7, newTraceId } from './ids.js'
|
|
24
|
+
export { PricingProvider, estimateTokens } from './pricing.js'
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
Provider,
|
|
28
|
+
CacheType,
|
|
29
|
+
PiiMode,
|
|
30
|
+
Sensitivity,
|
|
31
|
+
GuardrailOutcome,
|
|
32
|
+
TokenUsage,
|
|
33
|
+
coerceProvider,
|
|
34
|
+
} from './types.js'
|
|
35
|
+
export type { Message } from './types.js'
|
|
36
|
+
|
|
37
|
+
export type { Interceptor, Executor, ExecutorPolicy } from './interceptors/base.js'
|
|
38
|
+
export { InterceptorChain } from './interceptors/chain.js'
|
|
39
|
+
|
|
40
|
+
// errors
|
|
41
|
+
export {
|
|
42
|
+
GavioError,
|
|
43
|
+
ConfigurationError,
|
|
44
|
+
ProviderError,
|
|
45
|
+
ProviderUnavailableError,
|
|
46
|
+
RateLimitError,
|
|
47
|
+
ServerError,
|
|
48
|
+
TimeoutError,
|
|
49
|
+
PiiBlockedError,
|
|
50
|
+
BudgetExceededError,
|
|
51
|
+
GuardrailViolationError,
|
|
52
|
+
} from './errors.js'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { auditInterceptor, isAuditInterceptor, AUDIT_NAME } from './interceptor.js'
|
|
2
|
+
export type { AuditInterceptorOptions } from './interceptor.js'
|
|
3
|
+
export { AuditRecord, SCHEMA_VERSION } from './record.js'
|
|
4
|
+
export type { AuditRecordInit } from './record.js'
|
|
5
|
+
export type { AuditSink } from './sink.js'
|
|
6
|
+
export { stdoutSink } from './sinks/stdout.js'
|
|
7
|
+
export type { StdoutSinkOptions } from './sinks/stdout.js'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** auditInterceptor (F-OBS-01) — captures a full record of every call. */
|
|
2
|
+
|
|
3
|
+
import type { InterceptorContext } from '../../context.js'
|
|
4
|
+
import type { GavioRequest } from '../../request.js'
|
|
5
|
+
import type { GavioResponse } from '../../response.js'
|
|
6
|
+
import type { Interceptor } from '../base.js'
|
|
7
|
+
import { AuditRecord } from './record.js'
|
|
8
|
+
import type { AuditSink } from './sink.js'
|
|
9
|
+
import { stdoutSink } from './sinks/stdout.js'
|
|
10
|
+
|
|
11
|
+
const PROMPT_HASH_KEY = 'audit_prompt_hash'
|
|
12
|
+
|
|
13
|
+
export const AUDIT_NAME = 'audit'
|
|
14
|
+
|
|
15
|
+
export interface AuditInterceptorOptions {
|
|
16
|
+
sink?: AuditSink | 'stdout'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build an AuditRecord per request and write it to a sink.
|
|
21
|
+
*
|
|
22
|
+
* Register this as the outermost interceptor so its `after` runs last and sees
|
|
23
|
+
* the final, fully-processed response. It hashes the (already PII-redacted)
|
|
24
|
+
* prompt in `before` and the response in `after` — content is never stored,
|
|
25
|
+
* only digests and metadata.
|
|
26
|
+
*/
|
|
27
|
+
class AuditInterceptor implements Interceptor {
|
|
28
|
+
readonly name = AUDIT_NAME
|
|
29
|
+
readonly dryRunSafe = true // auditing is observation-only, so it always runs
|
|
30
|
+
|
|
31
|
+
private readonly sink: AuditSink
|
|
32
|
+
|
|
33
|
+
constructor(options: AuditInterceptorOptions = {}) {
|
|
34
|
+
this.sink = resolveSink(options.sink)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async before(request: GavioRequest, ctx: InterceptorContext): Promise<GavioRequest> {
|
|
38
|
+
ctx.state[PROMPT_HASH_KEY] = AuditRecord.hashText(request.promptText())
|
|
39
|
+
return request
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async after(
|
|
43
|
+
response: GavioResponse,
|
|
44
|
+
ctx: InterceptorContext,
|
|
45
|
+
): Promise<GavioResponse> {
|
|
46
|
+
const record = new AuditRecord({
|
|
47
|
+
traceId: response.traceId,
|
|
48
|
+
parentTraceId: ctx.parentTraceId,
|
|
49
|
+
agentId: ctx.agentId,
|
|
50
|
+
sessionId: ctx.sessionId,
|
|
51
|
+
timestampUtc: AuditRecord.nowUtc(),
|
|
52
|
+
provider: response.provider,
|
|
53
|
+
model: response.model,
|
|
54
|
+
modelVersion: response.modelVersion,
|
|
55
|
+
promptHash: (ctx.state[PROMPT_HASH_KEY] as string | undefined) ?? '',
|
|
56
|
+
responseHash: AuditRecord.hashText(response.content),
|
|
57
|
+
tokenUsage: response.usage,
|
|
58
|
+
costUsd: response.costUsd,
|
|
59
|
+
latencyMs: response.latencyMs,
|
|
60
|
+
piiEntityTypes: [...ctx.piiEntityTypes],
|
|
61
|
+
piiEntityCounts: { ...ctx.piiEntityCounts },
|
|
62
|
+
interceptorsFired: [...ctx.interceptorsFired],
|
|
63
|
+
cacheHit: response.cacheHit,
|
|
64
|
+
cacheType: response.cacheType,
|
|
65
|
+
guardrailOutcome: ctx.guardrailOutcome,
|
|
66
|
+
riskScore: ctx.riskScore,
|
|
67
|
+
})
|
|
68
|
+
response.audit = record
|
|
69
|
+
try {
|
|
70
|
+
await this.sink.write(record)
|
|
71
|
+
} catch {
|
|
72
|
+
// Auditing must never break the call.
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.error(`[gavio:audit] sink write failed for trace ${record.traceId}`)
|
|
75
|
+
}
|
|
76
|
+
return response
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveSink(sink: AuditSink | 'stdout' | undefined): AuditSink {
|
|
81
|
+
if (sink === undefined || sink === 'stdout') return stdoutSink()
|
|
82
|
+
return sink
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Factory: build an audit interceptor. */
|
|
86
|
+
export function auditInterceptor(options: AuditInterceptorOptions = {}): Interceptor {
|
|
87
|
+
return new AuditInterceptor(options)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True if an interceptor is the audit interceptor (used by dev-mode auto-wiring). */
|
|
91
|
+
export function isAuditInterceptor(i: Interceptor): boolean {
|
|
92
|
+
return i.name === AUDIT_NAME
|
|
93
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** AuditRecord — the immutable, per-request audit entry. */
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { TokenUsage } from '../../types.js'
|
|
5
|
+
|
|
6
|
+
export const SCHEMA_VERSION = '1.0'
|
|
7
|
+
|
|
8
|
+
function sha256(text: string): string {
|
|
9
|
+
return createHash('sha256').update(text, 'utf-8').digest('hex')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AuditRecordInit {
|
|
13
|
+
traceId: string
|
|
14
|
+
provider: string
|
|
15
|
+
model: string
|
|
16
|
+
timestampUtc: string
|
|
17
|
+
parentTraceId?: string | null
|
|
18
|
+
agentId?: string | null
|
|
19
|
+
sessionId?: string | null
|
|
20
|
+
modelVersion?: string
|
|
21
|
+
promptHash?: string
|
|
22
|
+
responseHash?: string
|
|
23
|
+
tokenUsage?: TokenUsage
|
|
24
|
+
costUsd?: number
|
|
25
|
+
latencyMs?: number
|
|
26
|
+
piiEntityTypes?: string[]
|
|
27
|
+
piiEntityCounts?: Record<string, number>
|
|
28
|
+
interceptorsFired?: string[]
|
|
29
|
+
cacheHit?: boolean
|
|
30
|
+
cacheType?: string | null
|
|
31
|
+
guardrailOutcome?: string | null
|
|
32
|
+
riskScore?: number | null
|
|
33
|
+
previousHash?: string
|
|
34
|
+
schemaVersion?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One append-only audit entry. Carries metadata only — never raw content.
|
|
39
|
+
*
|
|
40
|
+
* `promptHash` / `responseHash` are SHA-256 digests so the entry is verifiable
|
|
41
|
+
* without storing sensitive text. `previousHash` is reserved for the v0.2.0
|
|
42
|
+
* hash-chain (F-OBS-02); empty in v0.1.0.
|
|
43
|
+
*/
|
|
44
|
+
export class AuditRecord {
|
|
45
|
+
traceId: string
|
|
46
|
+
provider: string
|
|
47
|
+
model: string
|
|
48
|
+
timestampUtc: string
|
|
49
|
+
parentTraceId: string | null
|
|
50
|
+
agentId: string | null
|
|
51
|
+
sessionId: string | null
|
|
52
|
+
modelVersion: string
|
|
53
|
+
promptHash: string
|
|
54
|
+
responseHash: string
|
|
55
|
+
tokenUsage: TokenUsage
|
|
56
|
+
costUsd: number
|
|
57
|
+
latencyMs: number
|
|
58
|
+
piiEntityTypes: string[]
|
|
59
|
+
piiEntityCounts: Record<string, number>
|
|
60
|
+
interceptorsFired: string[]
|
|
61
|
+
cacheHit: boolean
|
|
62
|
+
cacheType: string | null
|
|
63
|
+
guardrailOutcome: string | null
|
|
64
|
+
riskScore: number | null
|
|
65
|
+
previousHash: string
|
|
66
|
+
schemaVersion: string
|
|
67
|
+
|
|
68
|
+
constructor(init: AuditRecordInit) {
|
|
69
|
+
this.traceId = init.traceId
|
|
70
|
+
this.provider = init.provider
|
|
71
|
+
this.model = init.model
|
|
72
|
+
this.timestampUtc = init.timestampUtc
|
|
73
|
+
this.parentTraceId = init.parentTraceId ?? null
|
|
74
|
+
this.agentId = init.agentId ?? null
|
|
75
|
+
this.sessionId = init.sessionId ?? null
|
|
76
|
+
this.modelVersion = init.modelVersion ?? ''
|
|
77
|
+
this.promptHash = init.promptHash ?? ''
|
|
78
|
+
this.responseHash = init.responseHash ?? ''
|
|
79
|
+
this.tokenUsage = init.tokenUsage ?? new TokenUsage()
|
|
80
|
+
this.costUsd = init.costUsd ?? 0.0
|
|
81
|
+
this.latencyMs = init.latencyMs ?? 0
|
|
82
|
+
this.piiEntityTypes = init.piiEntityTypes ?? []
|
|
83
|
+
this.piiEntityCounts = init.piiEntityCounts ?? {}
|
|
84
|
+
this.interceptorsFired = init.interceptorsFired ?? []
|
|
85
|
+
this.cacheHit = init.cacheHit ?? false
|
|
86
|
+
this.cacheType = init.cacheType ?? null
|
|
87
|
+
this.guardrailOutcome = init.guardrailOutcome ?? null
|
|
88
|
+
this.riskScore = init.riskScore ?? null
|
|
89
|
+
this.previousHash = init.previousHash ?? ''
|
|
90
|
+
this.schemaVersion = init.schemaVersion ?? SCHEMA_VERSION
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static nowUtc(): string {
|
|
94
|
+
return new Date().toISOString()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static hashText(text: string): string {
|
|
98
|
+
return sha256(text)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toJSON(): Record<string, unknown> {
|
|
102
|
+
return {
|
|
103
|
+
traceId: this.traceId,
|
|
104
|
+
parentTraceId: this.parentTraceId,
|
|
105
|
+
agentId: this.agentId,
|
|
106
|
+
sessionId: this.sessionId,
|
|
107
|
+
provider: this.provider,
|
|
108
|
+
model: this.model,
|
|
109
|
+
modelVersion: this.modelVersion,
|
|
110
|
+
timestampUtc: this.timestampUtc,
|
|
111
|
+
promptHash: this.promptHash,
|
|
112
|
+
responseHash: this.responseHash,
|
|
113
|
+
tokenUsage: this.tokenUsage.toJSON(),
|
|
114
|
+
costUsd: this.costUsd,
|
|
115
|
+
latencyMs: this.latencyMs,
|
|
116
|
+
piiEntityTypes: this.piiEntityTypes,
|
|
117
|
+
piiEntityCounts: this.piiEntityCounts,
|
|
118
|
+
interceptorsFired: this.interceptorsFired,
|
|
119
|
+
cacheHit: this.cacheHit,
|
|
120
|
+
cacheType: this.cacheType,
|
|
121
|
+
guardrailOutcome: this.guardrailOutcome,
|
|
122
|
+
riskScore: this.riskScore,
|
|
123
|
+
previousHash: this.previousHash,
|
|
124
|
+
schemaVersion: this.schemaVersion,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
|
|
129
|
+
toCanonicalJson(): string {
|
|
130
|
+
const data = this.toJSON()
|
|
131
|
+
return JSON.stringify(data, Object.keys(data).sort())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Hash of this record's content — used to build the v0.2.0 chain. */
|
|
135
|
+
contentHash(): string {
|
|
136
|
+
return sha256(this.toCanonicalJson())
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** AuditSink — the extensible destination for audit records. */
|
|
2
|
+
|
|
3
|
+
import type { AuditRecord } from './record.js'
|
|
4
|
+
|
|
5
|
+
/** Where audit records go. Implement `write` to add a backend. */
|
|
6
|
+
export interface AuditSink {
|
|
7
|
+
write(record: AuditRecord): Promise<void>
|
|
8
|
+
/** Flush/close any resources. Optional. */
|
|
9
|
+
close?(): Promise<void>
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** stdoutSink — human-readable audit output for development (F-OBS-05). */
|
|
2
|
+
|
|
3
|
+
import type { AuditRecord } from '../record.js'
|
|
4
|
+
import type { AuditSink } from '../sink.js'
|
|
5
|
+
|
|
6
|
+
export interface StdoutSinkOptions {
|
|
7
|
+
pretty?: boolean
|
|
8
|
+
write?: (line: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Print each audit record to stdout. Zero dependencies. */
|
|
12
|
+
export function stdoutSink(options: StdoutSinkOptions = {}): AuditSink {
|
|
13
|
+
const pretty = options.pretty ?? true
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
const emit = options.write ?? ((line: string) => console.log(line))
|
|
16
|
+
return {
|
|
17
|
+
async write(record: AuditRecord): Promise<void> {
|
|
18
|
+
const data = record.toJSON()
|
|
19
|
+
emit(pretty ? formatPretty(data) : JSON.stringify(data))
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatPretty(data: Record<string, unknown>): string {
|
|
25
|
+
const usage = data['tokenUsage'] as { totalTokens: number }
|
|
26
|
+
const piiTypes = data['piiEntityTypes'] as string[]
|
|
27
|
+
const pii = piiTypes.length > 0 ? piiTypes : ['none']
|
|
28
|
+
const interceptors = data['interceptorsFired'] as string[]
|
|
29
|
+
const traceId = String(data['traceId'])
|
|
30
|
+
const cost = Number(data['costUsd'])
|
|
31
|
+
return (
|
|
32
|
+
'[gavio:audit] ' +
|
|
33
|
+
`trace=${traceId.slice(0, 18)}… ` +
|
|
34
|
+
`${String(data['provider'])}/${String(data['model'])} ` +
|
|
35
|
+
`tokens=${usage.totalTokens} ` +
|
|
36
|
+
`cost=$${cost.toFixed(6)} ` +
|
|
37
|
+
`latency=${String(data['latencyMs'])}ms ` +
|
|
38
|
+
`cache=${data['cacheHit'] ? 'HIT' : 'miss'} ` +
|
|
39
|
+
`pii=${pii.join(',')} ` +
|
|
40
|
+
`interceptors=[${interceptors.join(',')}]`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The Interceptor interface — the unit of composition in Gavio. */
|
|
2
|
+
|
|
3
|
+
import type { InterceptorContext } from '../context.js'
|
|
4
|
+
import type { GavioRequest } from '../request.js'
|
|
5
|
+
import type { GavioResponse } from '../response.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A pre/post hook around the provider call.
|
|
9
|
+
*
|
|
10
|
+
* `before` runs in registration order on the request; `after` runs in reverse
|
|
11
|
+
* order on the response (onion model). Either may be omitted. Throwing from
|
|
12
|
+
* `before` aborts the call.
|
|
13
|
+
*/
|
|
14
|
+
export interface Interceptor {
|
|
15
|
+
/** Unique name used in audit logs and metrics. */
|
|
16
|
+
readonly name: string
|
|
17
|
+
|
|
18
|
+
/** Pre-interceptor: runs before the provider call. */
|
|
19
|
+
before?(
|
|
20
|
+
request: GavioRequest,
|
|
21
|
+
ctx: InterceptorContext,
|
|
22
|
+
): Promise<GavioRequest> | GavioRequest
|
|
23
|
+
|
|
24
|
+
/** Post-interceptor: runs after the provider call. */
|
|
25
|
+
after?(
|
|
26
|
+
response: GavioResponse,
|
|
27
|
+
ctx: InterceptorContext,
|
|
28
|
+
): Promise<GavioResponse> | GavioResponse
|
|
29
|
+
|
|
30
|
+
/** Called if the provider call or a downstream interceptor throws. */
|
|
31
|
+
onError?(error: Error, ctx: InterceptorContext): void | Promise<void>
|
|
32
|
+
|
|
33
|
+
/** If true (default), participates in dry-run mode (logs only). */
|
|
34
|
+
readonly dryRunSafe?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A function that takes the final request and returns a response (the provider call). */
|
|
38
|
+
export type Executor = (request: GavioRequest) => Promise<GavioResponse>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Base class for executor-wrapping reliability policies.
|
|
42
|
+
*
|
|
43
|
+
* Retry, timeout, and fallback can't be expressed as plain before/after hooks:
|
|
44
|
+
* they need to re-invoke (or race) the executor. They implement `around` so the
|
|
45
|
+
* Gateway composes them *around* the provider call, innermost-last.
|
|
46
|
+
*/
|
|
47
|
+
export interface ExecutorPolicy extends Interceptor {
|
|
48
|
+
readonly isExecutorPolicy: true
|
|
49
|
+
around(
|
|
50
|
+
request: GavioRequest,
|
|
51
|
+
ctx: InterceptorContext,
|
|
52
|
+
callNext: Executor,
|
|
53
|
+
): Promise<GavioResponse>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isExecutorPolicy(i: Interceptor): i is ExecutorPolicy {
|
|
57
|
+
return (i as Partial<ExecutorPolicy>).isExecutorPolicy === true
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheBackend — the key/value contract behind the cache interceptors.
|
|
3
|
+
*
|
|
4
|
+
* The full SemanticCache interceptor lands in v0.2.0 (F-CACHE-01/02). v0.1.0
|
|
5
|
+
* ships the backend interface and the in-memory backend so dev mode has a
|
|
6
|
+
* working, dependency-free cache substrate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A minimal async key/value store. */
|
|
10
|
+
export interface CacheBackend {
|
|
11
|
+
get(key: string): Promise<unknown | null>
|
|
12
|
+
set(key: string, value: unknown, ttlSeconds?: number | null): Promise<void>
|
|
13
|
+
delete(key: string): Promise<void>
|
|
14
|
+
clear(): Promise<void>
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** In-memory cache backend (F-CACHE-03) — default zero-dependency dev backend. */
|
|
2
|
+
|
|
3
|
+
import type { CacheBackend } from '../backend.js'
|
|
4
|
+
|
|
5
|
+
interface Entry {
|
|
6
|
+
value: unknown
|
|
7
|
+
expiresAt: number | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MemoryCacheBackendOptions {
|
|
11
|
+
maxSize?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** LRU-bounded, optionally TTL'd in-process cache. Not shared across processes. */
|
|
15
|
+
class MemoryBackend implements CacheBackend {
|
|
16
|
+
readonly maxSize: number
|
|
17
|
+
// Map preserves insertion order, which we use for LRU eviction.
|
|
18
|
+
private store = new Map<string, Entry>()
|
|
19
|
+
|
|
20
|
+
constructor(maxSize = 1000) {
|
|
21
|
+
this.maxSize = maxSize
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async get(key: string): Promise<unknown | null> {
|
|
25
|
+
const entry = this.store.get(key)
|
|
26
|
+
if (entry === undefined) return null
|
|
27
|
+
if (entry.expiresAt !== null && now() > entry.expiresAt) {
|
|
28
|
+
this.store.delete(key)
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
// Move to end (most-recently-used).
|
|
32
|
+
this.store.delete(key)
|
|
33
|
+
this.store.set(key, entry)
|
|
34
|
+
return entry.value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async set(key: string, value: unknown, ttlSeconds?: number | null): Promise<void> {
|
|
38
|
+
const expiresAt = ttlSeconds ? now() + ttlSeconds * 1000 : null
|
|
39
|
+
this.store.delete(key)
|
|
40
|
+
this.store.set(key, { value, expiresAt })
|
|
41
|
+
while (this.store.size > this.maxSize) {
|
|
42
|
+
const oldest = this.store.keys().next().value
|
|
43
|
+
if (oldest === undefined) break
|
|
44
|
+
this.store.delete(oldest)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async delete(key: string): Promise<void> {
|
|
49
|
+
this.store.delete(key)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async clear(): Promise<void> {
|
|
53
|
+
this.store.clear()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get size(): number {
|
|
57
|
+
return this.store.size
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function now(): number {
|
|
62
|
+
return Date.now()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Factory: build an in-memory cache backend. */
|
|
66
|
+
export function memoryCacheBackend(options: MemoryCacheBackendOptions = {}): CacheBackend {
|
|
67
|
+
return new MemoryBackend(options.maxSize ?? 1000)
|
|
68
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching substrate. The SemanticCache interceptor ships in v0.2.0; v0.1.0
|
|
3
|
+
* exposes the CacheBackend interface and the in-memory backend only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type { CacheBackend } from './backend.js'
|
|
7
|
+
export { memoryCacheBackend } from './backends/memory.js'
|
|
8
|
+
export type { MemoryCacheBackendOptions } from './backends/memory.js'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** InterceptorChain — runs the pre/post pipeline around the provider call. */
|
|
2
|
+
|
|
3
|
+
import type { InterceptorContext } from '../context.js'
|
|
4
|
+
import type { GavioRequest } from '../request.js'
|
|
5
|
+
import type { GavioResponse } from '../response.js'
|
|
6
|
+
import type { Executor, Interceptor } from './base.js'
|
|
7
|
+
|
|
8
|
+
const dryRunSafe = (i: Interceptor): boolean => i.dryRunSafe !== false
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ordered list of interceptors wrapping an executor.
|
|
12
|
+
*
|
|
13
|
+
* `before` hooks fire in order; the executor runs; `after` hooks fire in
|
|
14
|
+
* reverse order (onion model). If any stage throws, every interceptor's
|
|
15
|
+
* `onError` is invoked before the error propagates.
|
|
16
|
+
*/
|
|
17
|
+
export class InterceptorChain {
|
|
18
|
+
private readonly interceptors: Interceptor[]
|
|
19
|
+
|
|
20
|
+
constructor(interceptors: Interceptor[]) {
|
|
21
|
+
this.interceptors = [...interceptors]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async execute(
|
|
25
|
+
request: GavioRequest,
|
|
26
|
+
ctx: InterceptorContext,
|
|
27
|
+
executor: Executor,
|
|
28
|
+
): Promise<GavioResponse> {
|
|
29
|
+
try {
|
|
30
|
+
let req = request
|
|
31
|
+
for (const interceptor of this.interceptors) {
|
|
32
|
+
if (ctx.dryRun && !dryRunSafe(interceptor)) continue
|
|
33
|
+
if (interceptor.before) {
|
|
34
|
+
req = await interceptor.before(req, ctx)
|
|
35
|
+
}
|
|
36
|
+
ctx.markFired(interceptor.name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let response = await executor(req)
|
|
40
|
+
|
|
41
|
+
for (let i = this.interceptors.length - 1; i >= 0; i--) {
|
|
42
|
+
const interceptor = this.interceptors[i]!
|
|
43
|
+
if (ctx.dryRun && !dryRunSafe(interceptor)) continue
|
|
44
|
+
if (interceptor.after) {
|
|
45
|
+
response = await interceptor.after(response, ctx)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
response.interceptorsFired = [...ctx.interceptorsFired]
|
|
50
|
+
return response
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
53
|
+
for (const interceptor of this.interceptors) {
|
|
54
|
+
if (interceptor.onError) {
|
|
55
|
+
try {
|
|
56
|
+
await interceptor.onError(err, ctx)
|
|
57
|
+
} catch {
|
|
58
|
+
// on_error must never break the propagation of the original error.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Interceptor barrel. */
|
|
2
|
+
|
|
3
|
+
export type { Interceptor, Executor, ExecutorPolicy } from './base.js'
|
|
4
|
+
export { isExecutorPolicy } from './base.js'
|
|
5
|
+
export { InterceptorChain } from './chain.js'
|
|
6
|
+
export { piiGuard } from './pii/index.js'
|
|
7
|
+
export { auditInterceptor } from './audit/index.js'
|
|
8
|
+
export { retryInterceptor, timeoutPolicy, fallbackChain } from './reliability/index.js'
|
|
9
|
+
export { memoryCacheBackend } from './cache/index.js'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** ScanContext — per-request state shared across PII scanners. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context threaded through every scanner for one request.
|
|
5
|
+
*
|
|
6
|
+
* Tracks a monotonic per-entity-type index so repeated entities get stable,
|
|
7
|
+
* distinct placeholders (`[EMAIL_1]`, `[EMAIL_2]`).
|
|
8
|
+
*/
|
|
9
|
+
export class ScanContext {
|
|
10
|
+
readonly language: string
|
|
11
|
+
readonly locale: string
|
|
12
|
+
private counters: Record<string, number> = {}
|
|
13
|
+
|
|
14
|
+
constructor(language = 'en', locale = 'NL') {
|
|
15
|
+
this.language = language
|
|
16
|
+
this.locale = locale
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Return the next 1-based index for an entity type. */
|
|
20
|
+
nextIndex(entityType: string): number {
|
|
21
|
+
this.counters[entityType] = (this.counters[entityType] ?? 0) + 1
|
|
22
|
+
return this.counters[entityType]
|
|
23
|
+
}
|
|
24
|
+
}
|