gavio 0.1.0 → 0.3.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/dist/cjs/config.js +106 -0
- package/dist/cjs/errors.js +29 -1
- package/dist/cjs/gateway.js +88 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/interceptors/audit/index.js +4 -1
- package/dist/cjs/interceptors/audit/interceptor.js +11 -0
- package/dist/cjs/interceptors/audit/record.js +17 -3
- package/dist/cjs/interceptors/audit/trace.js +43 -0
- package/dist/cjs/interceptors/cache/embedding.js +53 -0
- package/dist/cjs/interceptors/cache/index.js +9 -5
- package/dist/cjs/interceptors/cache/interceptor.js +80 -0
- package/dist/cjs/interceptors/cache/vector.js +35 -0
- package/dist/cjs/interceptors/governance/budget.js +45 -0
- package/dist/cjs/interceptors/governance/index.js +10 -0
- package/dist/cjs/interceptors/governance/model-policy.js +18 -0
- package/dist/cjs/interceptors/governance/rate-limit.js +46 -0
- package/dist/cjs/interceptors/guardrails/index.js +11 -0
- package/dist/cjs/interceptors/guardrails/interceptor.js +40 -0
- package/dist/cjs/interceptors/guardrails/validator.js +8 -0
- package/dist/cjs/interceptors/guardrails/validators/regex.js +32 -0
- package/dist/cjs/interceptors/guardrails/validators/schema.js +63 -0
- package/dist/cjs/interceptors/injection.js +62 -0
- package/dist/cjs/interceptors/metrics/index.js +9 -0
- package/dist/cjs/interceptors/metrics/interceptor.js +37 -0
- package/dist/cjs/interceptors/metrics/registry.js +0 -0
- package/dist/cjs/interceptors/quality/index.js +7 -0
- package/dist/cjs/interceptors/quality/risk.js +49 -0
- package/dist/cjs/interceptors/reliability/circuit-breaker.js +82 -0
- package/dist/cjs/interceptors/reliability/index.js +8 -1
- package/dist/cjs/interceptors/reliability/load-balancer.js +38 -0
- package/dist/cjs/interceptors/reliability/stream-buffer.js +28 -0
- package/dist/cjs/pricing.js +5 -1
- package/dist/cjs/providers/azure-openai.js +56 -0
- package/dist/cjs/providers/base.js +9 -0
- package/dist/cjs/providers/gemini.js +73 -0
- package/dist/cjs/providers/index.js +22 -6
- package/dist/cjs/providers/ollama.js +41 -0
- package/dist/cjs/request.js +3 -0
- package/dist/cjs/shim/openai.js +57 -0
- package/dist/cjs/types.js +53 -1
- package/dist/esm/config.d.ts +12 -0
- package/dist/esm/config.js +102 -0
- package/dist/esm/errors.d.ts +17 -0
- package/dist/esm/errors.js +24 -0
- package/dist/esm/gateway.d.ts +18 -1
- package/dist/esm/gateway.js +55 -0
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/interceptors/audit/index.d.ts +2 -0
- package/dist/esm/interceptors/audit/index.js +1 -0
- package/dist/esm/interceptors/audit/interceptor.d.ts +2 -0
- package/dist/esm/interceptors/audit/interceptor.js +11 -0
- package/dist/esm/interceptors/audit/record.d.ts +4 -2
- package/dist/esm/interceptors/audit/record.js +18 -4
- package/dist/esm/interceptors/audit/trace.d.ts +19 -0
- package/dist/esm/interceptors/audit/trace.js +39 -0
- package/dist/esm/interceptors/cache/embedding.d.ts +14 -0
- package/dist/esm/interceptors/cache/embedding.js +49 -0
- package/dist/esm/interceptors/cache/index.d.ts +7 -4
- package/dist/esm/interceptors/cache/index.js +4 -4
- package/dist/esm/interceptors/cache/interceptor.d.ts +19 -0
- package/dist/esm/interceptors/cache/interceptor.js +77 -0
- package/dist/esm/interceptors/cache/vector.d.ts +9 -0
- package/dist/esm/interceptors/cache/vector.js +32 -0
- package/dist/esm/interceptors/governance/budget.d.ts +11 -0
- package/dist/esm/interceptors/governance/budget.js +42 -0
- package/dist/esm/interceptors/governance/index.d.ts +7 -0
- package/dist/esm/interceptors/governance/index.js +4 -0
- package/dist/esm/interceptors/governance/model-policy.d.ts +8 -0
- package/dist/esm/interceptors/governance/model-policy.js +15 -0
- package/dist/esm/interceptors/governance/rate-limit.d.ts +9 -0
- package/dist/esm/interceptors/governance/rate-limit.js +43 -0
- package/dist/esm/interceptors/guardrails/index.d.ts +6 -0
- package/dist/esm/interceptors/guardrails/index.js +4 -0
- package/dist/esm/interceptors/guardrails/interceptor.d.ts +15 -0
- package/dist/esm/interceptors/guardrails/interceptor.js +37 -0
- package/dist/esm/interceptors/guardrails/validator.d.ts +11 -0
- package/dist/esm/interceptors/guardrails/validator.js +3 -0
- package/dist/esm/interceptors/guardrails/validators/regex.d.ts +6 -0
- package/dist/esm/interceptors/guardrails/validators/regex.js +28 -0
- package/dist/esm/interceptors/guardrails/validators/schema.d.ts +5 -0
- package/dist/esm/interceptors/guardrails/validators/schema.js +60 -0
- package/dist/esm/interceptors/injection.d.ts +17 -0
- package/dist/esm/interceptors/injection.js +59 -0
- package/dist/esm/interceptors/metrics/index.d.ts +5 -0
- package/dist/esm/interceptors/metrics/index.js +3 -0
- package/dist/esm/interceptors/metrics/interceptor.d.ts +22 -0
- package/dist/esm/interceptors/metrics/interceptor.js +33 -0
- package/dist/esm/interceptors/metrics/registry.d.ts +31 -0
- package/dist/esm/interceptors/metrics/registry.js +0 -0
- package/dist/esm/interceptors/quality/index.d.ts +3 -0
- package/dist/esm/interceptors/quality/index.js +2 -0
- package/dist/esm/interceptors/quality/risk.d.ts +32 -0
- package/dist/esm/interceptors/quality/risk.js +44 -0
- package/dist/esm/interceptors/reliability/circuit-breaker.d.ts +15 -0
- package/dist/esm/interceptors/reliability/circuit-breaker.js +78 -0
- package/dist/esm/interceptors/reliability/index.d.ts +5 -0
- package/dist/esm/interceptors/reliability/index.js +3 -0
- package/dist/esm/interceptors/reliability/load-balancer.d.ts +8 -0
- package/dist/esm/interceptors/reliability/load-balancer.js +35 -0
- package/dist/esm/interceptors/reliability/stream-buffer.d.ts +18 -0
- package/dist/esm/interceptors/reliability/stream-buffer.js +24 -0
- package/dist/esm/pricing.js +5 -1
- package/dist/esm/providers/azure-openai.d.ts +28 -0
- package/dist/esm/providers/azure-openai.js +53 -0
- package/dist/esm/providers/base.d.ts +7 -0
- package/dist/esm/providers/base.js +9 -1
- package/dist/esm/providers/gemini.d.ts +36 -0
- package/dist/esm/providers/gemini.js +69 -0
- package/dist/esm/providers/index.d.ts +7 -1
- package/dist/esm/providers/index.js +18 -5
- package/dist/esm/providers/ollama.d.ts +21 -0
- package/dist/esm/providers/ollama.js +38 -0
- package/dist/esm/request.d.ts +4 -1
- package/dist/esm/request.js +4 -1
- package/dist/esm/shim/openai.d.ts +56 -0
- package/dist/esm/shim/openai.js +53 -0
- package/dist/esm/types.d.ts +54 -0
- package/dist/esm/types.js +50 -0
- package/package.json +41 -2
- package/src/config.ts +125 -0
- package/src/errors.ts +28 -0
- package/src/gateway.ts +62 -1
- package/src/index.ts +4 -2
- package/src/interceptors/audit/index.ts +2 -0
- package/src/interceptors/audit/interceptor.ts +13 -0
- package/src/interceptors/audit/record.ts +18 -4
- package/src/interceptors/audit/trace.ts +47 -0
- package/src/interceptors/cache/embedding.ts +53 -0
- package/src/interceptors/cache/index.ts +7 -4
- package/src/interceptors/cache/interceptor.ts +111 -0
- package/src/interceptors/cache/vector.ts +45 -0
- package/src/interceptors/governance/budget.ts +59 -0
- package/src/interceptors/governance/index.ts +8 -0
- package/src/interceptors/governance/model-policy.ts +25 -0
- package/src/interceptors/governance/rate-limit.ts +63 -0
- package/src/interceptors/guardrails/index.ts +7 -0
- package/src/interceptors/guardrails/interceptor.ts +56 -0
- package/src/interceptors/guardrails/validator.ts +14 -0
- package/src/interceptors/guardrails/validators/regex.ts +29 -0
- package/src/interceptors/guardrails/validators/schema.ts +62 -0
- package/src/interceptors/injection.ts +72 -0
- package/src/interceptors/metrics/index.ts +6 -0
- package/src/interceptors/metrics/interceptor.ts +46 -0
- package/src/interceptors/metrics/registry.ts +0 -0
- package/src/interceptors/quality/index.ts +4 -0
- package/src/interceptors/quality/risk.ts +64 -0
- package/src/interceptors/reliability/circuit-breaker.ts +102 -0
- package/src/interceptors/reliability/index.ts +5 -0
- package/src/interceptors/reliability/load-balancer.ts +56 -0
- package/src/interceptors/reliability/stream-buffer.ts +27 -0
- package/src/pricing.ts +5 -1
- package/src/providers/azure-openai.ts +77 -0
- package/src/providers/base.ts +21 -1
- package/src/providers/gemini.ts +95 -0
- package/src/providers/index.ts +21 -5
- package/src/providers/ollama.ts +61 -0
- package/src/request.ts +6 -2
- package/src/shim/openai.ts +76 -0
- package/src/types.ts +77 -0
package/dist/esm/gateway.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ConfigurationError } from './errors.js';
|
|
|
4
4
|
import { auditInterceptor, isAuditInterceptor } from './interceptors/audit/index.js';
|
|
5
5
|
import { isExecutorPolicy } from './interceptors/base.js';
|
|
6
6
|
import { InterceptorChain } from './interceptors/chain.js';
|
|
7
|
+
import { StreamBuffer } from './interceptors/reliability/stream-buffer.js';
|
|
7
8
|
import { PricingProvider } from './pricing.js';
|
|
8
9
|
import { buildAdapter } from './providers/index.js';
|
|
9
10
|
import { mockProvider } from './providers/mock.js';
|
|
@@ -37,6 +38,15 @@ export class Gateway {
|
|
|
37
38
|
this.dryRunMode = options.dryRun ?? false;
|
|
38
39
|
this.pricing = options.pricing ?? new PricingProvider();
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a Gateway from a config object or a JSON file path (F-DX-05).
|
|
43
|
+
* Async so the config module loads lazily (avoids a circular import).
|
|
44
|
+
*/
|
|
45
|
+
static async fromConfig(config) {
|
|
46
|
+
const mod = await import('./config.js');
|
|
47
|
+
const data = typeof config === 'string' ? mod.loadConfig(config) : config;
|
|
48
|
+
return mod.buildFromConfig(data);
|
|
49
|
+
}
|
|
40
50
|
/** Register an interceptor or executor policy. First-registered = outermost. */
|
|
41
51
|
use(interceptor) {
|
|
42
52
|
this.interceptors.push(interceptor);
|
|
@@ -65,6 +75,7 @@ export class Gateway {
|
|
|
65
75
|
sessionId: opts.sessionId ?? null,
|
|
66
76
|
options: opts.options ?? {},
|
|
67
77
|
metadata: opts.metadata ?? {},
|
|
78
|
+
lineage: opts.lineage ?? null,
|
|
68
79
|
});
|
|
69
80
|
const ctx = new InterceptorContext({
|
|
70
81
|
traceId: request.traceId,
|
|
@@ -76,6 +87,50 @@ export class Gateway {
|
|
|
76
87
|
const { chain, executor } = this.buildPipeline(adapter, ctx);
|
|
77
88
|
return chain.execute(request, ctx, executor);
|
|
78
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Stream a completion, buffering the provider stream (F-REL-06).
|
|
92
|
+
*
|
|
93
|
+
* The provider stream is buffered in full so the post-interceptor pipeline
|
|
94
|
+
* (guardrails, PII restore, audit) runs on the complete response before any
|
|
95
|
+
* chunk reaches the caller. Pre/post interceptors run via the chain; executor
|
|
96
|
+
* policies (retry, circuit breaker, cache) are not applied to the streaming
|
|
97
|
+
* path.
|
|
98
|
+
*/
|
|
99
|
+
async *stream(opts) {
|
|
100
|
+
const adapter = this.resolveAdapter();
|
|
101
|
+
if (adapter.stream === undefined || adapter.buildStreamResponse === undefined) {
|
|
102
|
+
throw new ConfigurationError(`${adapter.providerName} does not support streaming`);
|
|
103
|
+
}
|
|
104
|
+
const model = opts.model ?? this.modelHint ?? this.resolveModel(adapter);
|
|
105
|
+
const request = new GavioRequest({
|
|
106
|
+
messages: opts.messages,
|
|
107
|
+
model,
|
|
108
|
+
provider: coerceProvider(adapter.providerName),
|
|
109
|
+
agentId: opts.agentId ?? null,
|
|
110
|
+
parentTraceId: opts.parentTraceId ?? null,
|
|
111
|
+
sessionId: opts.sessionId ?? null,
|
|
112
|
+
options: opts.options ?? {},
|
|
113
|
+
metadata: opts.metadata ?? {},
|
|
114
|
+
});
|
|
115
|
+
const ctx = new InterceptorContext({
|
|
116
|
+
traceId: request.traceId,
|
|
117
|
+
agentId: request.agentId,
|
|
118
|
+
parentTraceId: request.parentTraceId,
|
|
119
|
+
sessionId: request.sessionId,
|
|
120
|
+
dryRun: this.dryRunMode,
|
|
121
|
+
});
|
|
122
|
+
const startedAt = performance.now();
|
|
123
|
+
const buffer = new StreamBuffer();
|
|
124
|
+
const { chain } = this.buildPipeline(adapter, ctx);
|
|
125
|
+
const bufferingExecutor = async (req) => {
|
|
126
|
+
for await (const chunk of adapter.stream(req))
|
|
127
|
+
buffer.append(chunk);
|
|
128
|
+
return adapter.buildStreamResponse(req, buffer.text(), startedAt);
|
|
129
|
+
};
|
|
130
|
+
const response = await chain.execute(request, ctx, bufferingExecutor);
|
|
131
|
+
// Post-interceptors have run on the fully buffered response; emit it now.
|
|
132
|
+
yield response.content;
|
|
133
|
+
}
|
|
79
134
|
async healthCheck() {
|
|
80
135
|
return this.resolveAdapter().healthCheck();
|
|
81
136
|
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* See https://gavio.io for documentation. MIT licensed.
|
|
9
9
|
*/
|
|
10
|
-
export declare const VERSION = "0.
|
|
10
|
+
export declare const VERSION = "0.3.0";
|
|
11
11
|
export { Gateway } from './gateway.js';
|
|
12
12
|
export type { GatewayOptions, CompleteOptions } from './gateway.js';
|
|
13
13
|
export { GavioRequest } from './request.js';
|
|
@@ -18,8 +18,8 @@ export { InterceptorContext } from './context.js';
|
|
|
18
18
|
export type { InterceptorContextInit } from './context.js';
|
|
19
19
|
export { uuid7, newTraceId } from './ids.js';
|
|
20
20
|
export { PricingProvider, estimateTokens } from './pricing.js';
|
|
21
|
-
export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, coerceProvider, } from './types.js';
|
|
22
|
-
export type { Message } from './types.js';
|
|
21
|
+
export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, PromptLineage, RagChunk, coerceProvider, } from './types.js';
|
|
22
|
+
export type { Message, PromptLineageInit, RagChunkInit } from './types.js';
|
|
23
23
|
export type { Interceptor, Executor, ExecutorPolicy } from './interceptors/base.js';
|
|
24
24
|
export { InterceptorChain } from './interceptors/chain.js';
|
|
25
25
|
export { GavioError, ConfigurationError, ProviderError, ProviderUnavailableError, RateLimitError, ServerError, TimeoutError, PiiBlockedError, BudgetExceededError, GuardrailViolationError, } from './errors.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* See https://gavio.io for documentation. MIT licensed.
|
|
9
9
|
*/
|
|
10
|
-
export const VERSION = '0.
|
|
10
|
+
export const VERSION = '0.3.0';
|
|
11
11
|
export { Gateway } from './gateway.js';
|
|
12
12
|
export { GavioRequest } from './request.js';
|
|
13
13
|
export { GavioResponse } from './response.js';
|
|
14
14
|
export { InterceptorContext } from './context.js';
|
|
15
15
|
export { uuid7, newTraceId } from './ids.js';
|
|
16
16
|
export { PricingProvider, estimateTokens } from './pricing.js';
|
|
17
|
-
export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, coerceProvider, } from './types.js';
|
|
17
|
+
export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, PromptLineage, RagChunk, coerceProvider, } from './types.js';
|
|
18
18
|
export { InterceptorChain } from './interceptors/chain.js';
|
|
19
19
|
// errors
|
|
20
20
|
export { GavioError, ConfigurationError, ProviderError, ProviderUnavailableError, RateLimitError, ServerError, TimeoutError, PiiBlockedError, BudgetExceededError, GuardrailViolationError, } from './errors.js';
|
|
@@ -5,3 +5,5 @@ export type { AuditRecordInit } from './record.js';
|
|
|
5
5
|
export type { AuditSink } from './sink.js';
|
|
6
6
|
export { stdoutSink } from './sinks/stdout.js';
|
|
7
7
|
export type { StdoutSinkOptions } from './sinks/stdout.js';
|
|
8
|
+
export { verifyChain, buildCallGraph } from './trace.js';
|
|
9
|
+
export type { TraceNode } from './trace.js';
|
|
@@ -4,6 +4,8 @@ import type { AuditSink } from './sink.js';
|
|
|
4
4
|
export declare const AUDIT_NAME = "audit";
|
|
5
5
|
export interface AuditInterceptorOptions {
|
|
6
6
|
sink?: AuditSink | 'stdout';
|
|
7
|
+
/** F-OBS-02: link each record via previousHash into a tamper-evident chain. */
|
|
8
|
+
hashChain?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/** Factory: build an audit interceptor. */
|
|
9
11
|
export declare function auditInterceptor(options?: AuditInterceptorOptions): Interceptor;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { AuditRecord } from './record.js';
|
|
3
3
|
import { stdoutSink } from './sinks/stdout.js';
|
|
4
4
|
const PROMPT_HASH_KEY = 'audit_prompt_hash';
|
|
5
|
+
const LINEAGE_KEY = 'audit_lineage';
|
|
5
6
|
export const AUDIT_NAME = 'audit';
|
|
6
7
|
/**
|
|
7
8
|
* Build an AuditRecord per request and write it to a sink.
|
|
@@ -15,11 +16,16 @@ class AuditInterceptor {
|
|
|
15
16
|
name = AUDIT_NAME;
|
|
16
17
|
dryRunSafe = true; // auditing is observation-only, so it always runs
|
|
17
18
|
sink;
|
|
19
|
+
hashChain;
|
|
20
|
+
lastHash = '';
|
|
18
21
|
constructor(options = {}) {
|
|
19
22
|
this.sink = resolveSink(options.sink);
|
|
23
|
+
this.hashChain = options.hashChain ?? false;
|
|
20
24
|
}
|
|
21
25
|
async before(request, ctx) {
|
|
22
26
|
ctx.state[PROMPT_HASH_KEY] = AuditRecord.hashText(request.promptText());
|
|
27
|
+
if (request.lineage != null)
|
|
28
|
+
ctx.state[LINEAGE_KEY] = request.lineage;
|
|
23
29
|
return request;
|
|
24
30
|
}
|
|
25
31
|
async after(response, ctx) {
|
|
@@ -44,7 +50,12 @@ class AuditInterceptor {
|
|
|
44
50
|
cacheType: response.cacheType,
|
|
45
51
|
guardrailOutcome: ctx.guardrailOutcome,
|
|
46
52
|
riskScore: ctx.riskScore,
|
|
53
|
+
lineage: ctx.state[LINEAGE_KEY] ?? null,
|
|
47
54
|
});
|
|
55
|
+
if (this.hashChain) {
|
|
56
|
+
record.previousHash = this.lastHash;
|
|
57
|
+
this.lastHash = record.contentHash();
|
|
58
|
+
}
|
|
48
59
|
response.audit = record;
|
|
49
60
|
try {
|
|
50
61
|
await this.sink.write(record);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** AuditRecord — the immutable, per-request audit entry. */
|
|
2
|
-
import { TokenUsage } from '../../types.js';
|
|
2
|
+
import { PromptLineage, TokenUsage } from '../../types.js';
|
|
3
3
|
export declare const SCHEMA_VERSION = "1.0";
|
|
4
4
|
export interface AuditRecordInit {
|
|
5
5
|
traceId: string;
|
|
@@ -22,6 +22,7 @@ export interface AuditRecordInit {
|
|
|
22
22
|
cacheType?: string | null;
|
|
23
23
|
guardrailOutcome?: string | null;
|
|
24
24
|
riskScore?: number | null;
|
|
25
|
+
lineage?: PromptLineage | null;
|
|
25
26
|
previousHash?: string;
|
|
26
27
|
schemaVersion?: string;
|
|
27
28
|
}
|
|
@@ -53,13 +54,14 @@ export declare class AuditRecord {
|
|
|
53
54
|
cacheType: string | null;
|
|
54
55
|
guardrailOutcome: string | null;
|
|
55
56
|
riskScore: number | null;
|
|
57
|
+
lineage: PromptLineage | null;
|
|
56
58
|
previousHash: string;
|
|
57
59
|
schemaVersion: string;
|
|
58
60
|
constructor(init: AuditRecordInit);
|
|
59
61
|
static nowUtc(): string;
|
|
60
62
|
static hashText(text: string): string;
|
|
61
63
|
toJSON(): Record<string, unknown>;
|
|
62
|
-
/** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
|
|
64
|
+
/** Stable JSON with recursively sorted keys — used for the v0.2.0 hash chain. */
|
|
63
65
|
toCanonicalJson(): string;
|
|
64
66
|
/** Hash of this record's content — used to build the v0.2.0 chain. */
|
|
65
67
|
contentHash(): string;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/** AuditRecord — the immutable, per-request audit entry. */
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { TokenUsage } from '../../types.js';
|
|
3
|
+
import { PromptLineage, TokenUsage } from '../../types.js';
|
|
4
4
|
export const SCHEMA_VERSION = '1.0';
|
|
5
5
|
function sha256(text) {
|
|
6
6
|
return createHash('sha256').update(text, 'utf-8').digest('hex');
|
|
7
7
|
}
|
|
8
|
+
/** Deterministic JSON with keys sorted at every nesting level. */
|
|
9
|
+
function stableStringify(value) {
|
|
10
|
+
if (value === null || typeof value !== 'object')
|
|
11
|
+
return JSON.stringify(value) ?? 'null';
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
14
|
+
const obj = value;
|
|
15
|
+
const parts = Object.keys(obj)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
|
|
18
|
+
return `{${parts.join(',')}}`;
|
|
19
|
+
}
|
|
8
20
|
/**
|
|
9
21
|
* One append-only audit entry. Carries metadata only — never raw content.
|
|
10
22
|
*
|
|
@@ -33,6 +45,7 @@ export class AuditRecord {
|
|
|
33
45
|
cacheType;
|
|
34
46
|
guardrailOutcome;
|
|
35
47
|
riskScore;
|
|
48
|
+
lineage;
|
|
36
49
|
previousHash;
|
|
37
50
|
schemaVersion;
|
|
38
51
|
constructor(init) {
|
|
@@ -56,6 +69,7 @@ export class AuditRecord {
|
|
|
56
69
|
this.cacheType = init.cacheType ?? null;
|
|
57
70
|
this.guardrailOutcome = init.guardrailOutcome ?? null;
|
|
58
71
|
this.riskScore = init.riskScore ?? null;
|
|
72
|
+
this.lineage = init.lineage ?? null;
|
|
59
73
|
this.previousHash = init.previousHash ?? '';
|
|
60
74
|
this.schemaVersion = init.schemaVersion ?? SCHEMA_VERSION;
|
|
61
75
|
}
|
|
@@ -87,14 +101,14 @@ export class AuditRecord {
|
|
|
87
101
|
cacheType: this.cacheType,
|
|
88
102
|
guardrailOutcome: this.guardrailOutcome,
|
|
89
103
|
riskScore: this.riskScore,
|
|
104
|
+
lineage: this.lineage ? this.lineage.toJSON() : null,
|
|
90
105
|
previousHash: this.previousHash,
|
|
91
106
|
schemaVersion: this.schemaVersion,
|
|
92
107
|
};
|
|
93
108
|
}
|
|
94
|
-
/** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
|
|
109
|
+
/** Stable JSON with recursively sorted keys — used for the v0.2.0 hash chain. */
|
|
95
110
|
toCanonicalJson() {
|
|
96
|
-
|
|
97
|
-
return JSON.stringify(data, Object.keys(data).sort());
|
|
111
|
+
return stableStringify(this.toJSON());
|
|
98
112
|
}
|
|
99
113
|
/** Hash of this record's content — used to build the v0.2.0 chain. */
|
|
100
114
|
contentHash() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Audit-chain verification (F-OBS-02) and multi-agent DAG trace (F-OBS-03). */
|
|
2
|
+
import type { AuditRecord } from './record.js';
|
|
3
|
+
/**
|
|
4
|
+
* Return true if the records form an intact hash chain. Each record's
|
|
5
|
+
* previousHash must equal the content hash of the record before it; the first
|
|
6
|
+
* must be empty. Any edit, reorder, or deletion breaks the chain.
|
|
7
|
+
*/
|
|
8
|
+
export declare function verifyChain(records: AuditRecord[]): boolean;
|
|
9
|
+
export interface TraceNode {
|
|
10
|
+
traceId: string;
|
|
11
|
+
agentId: string | null;
|
|
12
|
+
parentTraceId: string | null;
|
|
13
|
+
children: TraceNode[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reconstruct the multi-agent DAG from audit records using parentTraceId +
|
|
17
|
+
* traceId. Returns the root nodes (those with no known parent).
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildCallGraph(records: AuditRecord[]): TraceNode[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Audit-chain verification (F-OBS-02) and multi-agent DAG trace (F-OBS-03). */
|
|
2
|
+
/**
|
|
3
|
+
* Return true if the records form an intact hash chain. Each record's
|
|
4
|
+
* previousHash must equal the content hash of the record before it; the first
|
|
5
|
+
* must be empty. Any edit, reorder, or deletion breaks the chain.
|
|
6
|
+
*/
|
|
7
|
+
export function verifyChain(records) {
|
|
8
|
+
let prevHash = '';
|
|
9
|
+
for (const rec of records) {
|
|
10
|
+
if (rec.previousHash !== prevHash)
|
|
11
|
+
return false;
|
|
12
|
+
prevHash = rec.contentHash();
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reconstruct the multi-agent DAG from audit records using parentTraceId +
|
|
18
|
+
* traceId. Returns the root nodes (those with no known parent).
|
|
19
|
+
*/
|
|
20
|
+
export function buildCallGraph(records) {
|
|
21
|
+
const nodes = new Map();
|
|
22
|
+
for (const rec of records) {
|
|
23
|
+
nodes.set(rec.traceId, {
|
|
24
|
+
traceId: rec.traceId,
|
|
25
|
+
agentId: rec.agentId,
|
|
26
|
+
parentTraceId: rec.parentTraceId,
|
|
27
|
+
children: [],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const roots = [];
|
|
31
|
+
for (const node of nodes.values()) {
|
|
32
|
+
const parent = node.parentTraceId ? nodes.get(node.parentTraceId) : undefined;
|
|
33
|
+
if (parent)
|
|
34
|
+
parent.children.push(node);
|
|
35
|
+
else
|
|
36
|
+
roots.push(node);
|
|
37
|
+
}
|
|
38
|
+
return roots;
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddings for the semantic cache (F-CACHE-02).
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency hashed bag-of-words embedder (L2-normalised) — good enough to
|
|
5
|
+
* dedup near-identical prompts. Plug in a real embedder implementing `Embedder`
|
|
6
|
+
* for production semantic matching.
|
|
7
|
+
*/
|
|
8
|
+
export interface Embedder {
|
|
9
|
+
embed(text: string): number[];
|
|
10
|
+
}
|
|
11
|
+
/** Deterministic hashed bag-of-words embedder. */
|
|
12
|
+
export declare function hashingEmbedder(dim?: number): Embedder;
|
|
13
|
+
/** Cosine similarity; safe for zero vectors. */
|
|
14
|
+
export declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddings for the semantic cache (F-CACHE-02).
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency hashed bag-of-words embedder (L2-normalised) — good enough to
|
|
5
|
+
* dedup near-identical prompts. Plug in a real embedder implementing `Embedder`
|
|
6
|
+
* for production semantic matching.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
const TOKEN = /[a-z0-9]+/g;
|
|
10
|
+
/** Deterministic hashed bag-of-words embedder. */
|
|
11
|
+
export function hashingEmbedder(dim = 256) {
|
|
12
|
+
return {
|
|
13
|
+
embed(text) {
|
|
14
|
+
const vec = new Array(dim).fill(0);
|
|
15
|
+
const tokens = text.toLowerCase().match(TOKEN) ?? [];
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
// Parity note: Python uses blake2b(digest_size=8); here we take the
|
|
18
|
+
// first 8 bytes of blake2b512. Both are deterministic; the JS cache is
|
|
19
|
+
// per-process so cross-language byte-parity is not required.
|
|
20
|
+
const digest = createHash('blake2b512').update(token).digest();
|
|
21
|
+
let n = 0n;
|
|
22
|
+
for (let i = 0; i < 8; i++)
|
|
23
|
+
n = (n << 8n) | BigInt(digest[i]);
|
|
24
|
+
const bucket = Number(n % BigInt(dim));
|
|
25
|
+
vec[bucket] += 1;
|
|
26
|
+
}
|
|
27
|
+
const norm = Math.sqrt(vec.reduce((s, x) => s + x * x, 0));
|
|
28
|
+
if (norm === 0)
|
|
29
|
+
return vec;
|
|
30
|
+
return vec.map((x) => x / norm);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Cosine similarity; safe for zero vectors. */
|
|
35
|
+
export function cosineSimilarity(a, b) {
|
|
36
|
+
if (a.length !== b.length)
|
|
37
|
+
throw new Error('vectors must have equal length');
|
|
38
|
+
let dot = 0;
|
|
39
|
+
let na = 0;
|
|
40
|
+
let nb = 0;
|
|
41
|
+
for (let i = 0; i < a.length; i++) {
|
|
42
|
+
dot += a[i] * b[i];
|
|
43
|
+
na += a[i] * a[i];
|
|
44
|
+
nb += b[i] * b[i];
|
|
45
|
+
}
|
|
46
|
+
if (na === 0 || nb === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
49
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
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
|
-
*/
|
|
1
|
+
/** Caching (F-CACHE-01 exact, F-CACHE-02 semantic, F-CACHE-03 in-memory). */
|
|
5
2
|
export type { CacheBackend } from './backend.js';
|
|
6
3
|
export { memoryCacheBackend } from './backends/memory.js';
|
|
7
4
|
export type { MemoryCacheBackendOptions } from './backends/memory.js';
|
|
5
|
+
export { semanticCache } from './interceptor.js';
|
|
6
|
+
export type { SemanticCacheOptions } from './interceptor.js';
|
|
7
|
+
export { hashingEmbedder, cosineSimilarity } from './embedding.js';
|
|
8
|
+
export type { Embedder } from './embedding.js';
|
|
9
|
+
export { inMemoryVectorBackend } from './vector.js';
|
|
10
|
+
export type { VectorBackend } from './vector.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
-
*/
|
|
1
|
+
/** Caching (F-CACHE-01 exact, F-CACHE-02 semantic, F-CACHE-03 in-memory). */
|
|
5
2
|
export { memoryCacheBackend } from './backends/memory.js';
|
|
3
|
+
export { semanticCache } from './interceptor.js';
|
|
4
|
+
export { hashingEmbedder, cosineSimilarity } from './embedding.js';
|
|
5
|
+
export { inMemoryVectorBackend } from './vector.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semanticCache (F-CACHE-01, F-CACHE-02) — two-level cache as an ExecutorPolicy.
|
|
3
|
+
*
|
|
4
|
+
* Exact SHA-256 cache, then optional semantic cosine cache; a hit returns the
|
|
5
|
+
* cached response and skips the provider. Register outermost.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExecutorPolicy } from '../base.js';
|
|
8
|
+
import type { CacheBackend } from './backend.js';
|
|
9
|
+
import type { Embedder } from './embedding.js';
|
|
10
|
+
import { type VectorBackend } from './vector.js';
|
|
11
|
+
export interface SemanticCacheOptions {
|
|
12
|
+
backend?: CacheBackend;
|
|
13
|
+
embedder?: Embedder;
|
|
14
|
+
vectorBackend?: VectorBackend;
|
|
15
|
+
exactTtlSeconds?: number;
|
|
16
|
+
semanticTtlSeconds?: number;
|
|
17
|
+
similarityThreshold?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function semanticCache(options?: SemanticCacheOptions): ExecutorPolicy;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semanticCache (F-CACHE-01, F-CACHE-02) — two-level cache as an ExecutorPolicy.
|
|
3
|
+
*
|
|
4
|
+
* Exact SHA-256 cache, then optional semantic cosine cache; a hit returns the
|
|
5
|
+
* cached response and skips the provider. Register outermost.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { GavioResponse } from '../../response.js';
|
|
9
|
+
import { CacheType, TokenUsage } from '../../types.js';
|
|
10
|
+
import { memoryCacheBackend } from './backends/memory.js';
|
|
11
|
+
import { inMemoryVectorBackend } from './vector.js';
|
|
12
|
+
export function semanticCache(options = {}) {
|
|
13
|
+
const backend = options.backend ?? memoryCacheBackend();
|
|
14
|
+
const embedder = options.embedder;
|
|
15
|
+
const semantic = embedder != null;
|
|
16
|
+
const vector = options.vectorBackend ?? (semantic ? inMemoryVectorBackend() : null);
|
|
17
|
+
const exactTtl = options.exactTtlSeconds ?? 3600;
|
|
18
|
+
const semanticTtl = options.semanticTtlSeconds ?? 86400;
|
|
19
|
+
const threshold = options.similarityThreshold ?? 0.95;
|
|
20
|
+
function exactKey(request) {
|
|
21
|
+
const opts = request.options ?? {};
|
|
22
|
+
const sorted = {};
|
|
23
|
+
for (const k of Object.keys(opts).sort())
|
|
24
|
+
sorted[k] = opts[k];
|
|
25
|
+
const payload = JSON.stringify({
|
|
26
|
+
provider: String(request.provider),
|
|
27
|
+
model: request.model,
|
|
28
|
+
messages: request.messages,
|
|
29
|
+
options: sorted,
|
|
30
|
+
});
|
|
31
|
+
return 'gavio:exact:' + createHash('sha256').update(payload).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
function hit(request, ctx, entry, type) {
|
|
34
|
+
ctx.cacheHit = true;
|
|
35
|
+
ctx.cacheType = type;
|
|
36
|
+
return new GavioResponse({
|
|
37
|
+
traceId: request.traceId,
|
|
38
|
+
content: entry.content,
|
|
39
|
+
model: request.model,
|
|
40
|
+
provider: String(request.provider),
|
|
41
|
+
modelVersion: entry.modelVersion,
|
|
42
|
+
usage: new TokenUsage(entry.promptTokens, entry.completionTokens),
|
|
43
|
+
costUsd: 0,
|
|
44
|
+
cacheHit: true,
|
|
45
|
+
cacheType: type,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: 'semantic_cache',
|
|
50
|
+
isExecutorPolicy: true,
|
|
51
|
+
async around(request, ctx, callNext) {
|
|
52
|
+
ctx.markFired('semantic_cache');
|
|
53
|
+
const key = exactKey(request);
|
|
54
|
+
const cached = (await backend.get(key));
|
|
55
|
+
if (cached)
|
|
56
|
+
return hit(request, ctx, cached, CacheType.EXACT);
|
|
57
|
+
let embedding = null;
|
|
58
|
+
if (semantic && vector && embedder) {
|
|
59
|
+
embedding = embedder.embed(request.promptText());
|
|
60
|
+
const semHit = (await vector.query(embedding, threshold));
|
|
61
|
+
if (semHit)
|
|
62
|
+
return hit(request, ctx, semHit, CacheType.SEMANTIC);
|
|
63
|
+
}
|
|
64
|
+
const response = await callNext(request);
|
|
65
|
+
const entry = {
|
|
66
|
+
content: response.content,
|
|
67
|
+
modelVersion: response.modelVersion,
|
|
68
|
+
promptTokens: response.usage.promptTokens,
|
|
69
|
+
completionTokens: response.usage.completionTokens,
|
|
70
|
+
};
|
|
71
|
+
await backend.set(key, entry, exactTtl);
|
|
72
|
+
if (embedding && vector)
|
|
73
|
+
await vector.add(embedding, entry, semanticTtl);
|
|
74
|
+
return response;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** VectorBackend — nearest-neighbour store for the semantic cache (F-CACHE-02). */
|
|
2
|
+
export interface VectorBackend {
|
|
3
|
+
add(vector: number[], value: unknown, ttlSeconds?: number | null): Promise<void>;
|
|
4
|
+
/** Return the value of the nearest entry with similarity >= threshold. */
|
|
5
|
+
query(vector: number[], threshold: number): Promise<unknown | null>;
|
|
6
|
+
clear(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Bounded, brute-force in-memory vector store (default dev backend). */
|
|
9
|
+
export declare function inMemoryVectorBackend(maxSize?: number): VectorBackend;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** VectorBackend — nearest-neighbour store for the semantic cache (F-CACHE-02). */
|
|
2
|
+
import { cosineSimilarity } from './embedding.js';
|
|
3
|
+
/** Bounded, brute-force in-memory vector store (default dev backend). */
|
|
4
|
+
export function inMemoryVectorBackend(maxSize = 1000) {
|
|
5
|
+
const items = [];
|
|
6
|
+
return {
|
|
7
|
+
async add(vector, value, ttlSeconds) {
|
|
8
|
+
const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
|
|
9
|
+
items.push({ vector, value, expiresAt });
|
|
10
|
+
if (items.length > maxSize)
|
|
11
|
+
items.shift();
|
|
12
|
+
},
|
|
13
|
+
async query(vector, threshold) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
let best = null;
|
|
16
|
+
let bestSim = threshold;
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
if (item.expiresAt !== null && now > item.expiresAt)
|
|
19
|
+
continue;
|
|
20
|
+
const sim = cosineSimilarity(vector, item.vector);
|
|
21
|
+
if (sim >= bestSim) {
|
|
22
|
+
bestSim = sim;
|
|
23
|
+
best = item.value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return best;
|
|
27
|
+
},
|
|
28
|
+
async clear() {
|
|
29
|
+
items.length = 0;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** costControl (F-GOV-02) — soft/hard budget caps per scope and window. */
|
|
2
|
+
import type { Interceptor } from '../base.js';
|
|
3
|
+
export type Scope = 'agent' | 'session' | 'global';
|
|
4
|
+
export type Window = 'day' | 'month' | 'total';
|
|
5
|
+
export interface CostControlOptions {
|
|
6
|
+
hardCapUsd: number;
|
|
7
|
+
softCapUsd?: number;
|
|
8
|
+
scope?: Scope;
|
|
9
|
+
window?: Window;
|
|
10
|
+
}
|
|
11
|
+
export declare function costControl(options: CostControlOptions): Interceptor;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** costControl (F-GOV-02) — soft/hard budget caps per scope and window. */
|
|
2
|
+
import { BudgetExceededError } from '../../errors.js';
|
|
3
|
+
function scopeKey(scope, ctx) {
|
|
4
|
+
if (scope === 'agent')
|
|
5
|
+
return `agent:${ctx.agentId ?? 'unknown'}`;
|
|
6
|
+
if (scope === 'session')
|
|
7
|
+
return `session:${ctx.sessionId ?? 'unknown'}`;
|
|
8
|
+
return 'global';
|
|
9
|
+
}
|
|
10
|
+
function windowBucket(window) {
|
|
11
|
+
const now = new Date().toISOString();
|
|
12
|
+
if (window === 'day')
|
|
13
|
+
return now.slice(0, 10);
|
|
14
|
+
if (window === 'month')
|
|
15
|
+
return now.slice(0, 7);
|
|
16
|
+
return 'total';
|
|
17
|
+
}
|
|
18
|
+
export function costControl(options) {
|
|
19
|
+
const { hardCapUsd, softCapUsd, scope = 'global', window = 'day' } = options;
|
|
20
|
+
const spend = new Map();
|
|
21
|
+
const key = (ctx) => `${scopeKey(scope, ctx)}|${windowBucket(window)}`;
|
|
22
|
+
return {
|
|
23
|
+
name: 'cost_control',
|
|
24
|
+
before(request, ctx) {
|
|
25
|
+
const spent = spend.get(key(ctx)) ?? 0;
|
|
26
|
+
if (spent >= hardCapUsd) {
|
|
27
|
+
throw new BudgetExceededError(`budget hard cap $${hardCapUsd.toFixed(2)} reached (spent $${spent.toFixed(4)})`);
|
|
28
|
+
}
|
|
29
|
+
return request;
|
|
30
|
+
},
|
|
31
|
+
after(response, ctx) {
|
|
32
|
+
const k = key(ctx);
|
|
33
|
+
const total = (spend.get(k) ?? 0) + response.costUsd;
|
|
34
|
+
spend.set(k, total);
|
|
35
|
+
if (softCapUsd !== undefined && total >= softCapUsd) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.warn(`[gavio:budget] soft cap: $${total.toFixed(4)} of $${softCapUsd} for ${k}`);
|
|
38
|
+
}
|
|
39
|
+
return response;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Cost & governance (F-GOV-02 budget, F-GOV-03 rate limit, F-GOV-04 RBAC). */
|
|
2
|
+
export { costControl } from './budget.js';
|
|
3
|
+
export type { CostControlOptions, Scope, Window } from './budget.js';
|
|
4
|
+
export { rateLimiter } from './rate-limit.js';
|
|
5
|
+
export type { RateLimiterOptions } from './rate-limit.js';
|
|
6
|
+
export { modelPolicy } from './model-policy.js';
|
|
7
|
+
export type { ModelPolicyOptions } from './model-policy.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** modelPolicy (F-GOV-04) — per-role model allowlists (RBAC). */
|
|
2
|
+
import type { Interceptor } from '../base.js';
|
|
3
|
+
export interface ModelPolicyOptions {
|
|
4
|
+
roles: Record<string, string[]>;
|
|
5
|
+
defaultRole?: string;
|
|
6
|
+
roleKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function modelPolicy(options: ModelPolicyOptions): Interceptor;
|