@evalguard/vercel-ai 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +48 -4
- package/dist/guardrail-client.d.ts +41 -0
- package/dist/guardrail-client.d.ts.map +1 -0
- package/dist/guardrail-client.js +88 -0
- package/dist/guardrail-client.js.map +1 -0
- package/dist/index.d.ts +40 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +420 -88
- package/dist/index.js.map +1 -1
- package/dist/surface.d.ts +126 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +264 -0
- package/dist/surface.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -14
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type SurfacePolicy } from "@evalguard/wrapper-core";
|
|
2
|
+
/**
|
|
3
|
+
* Real API RESOURCES exempted by decision.
|
|
4
|
+
*
|
|
5
|
+
* Empty, deliberately. A model has no management endpoints — every method on
|
|
6
|
+
* every `@ai-sdk/provider` specification issues a provider request carrying
|
|
7
|
+
* caller-supplied content. Nothing has earned an exemption. An entry added here
|
|
8
|
+
* must carry its reason inline.
|
|
9
|
+
*/
|
|
10
|
+
export declare const EXEMPT_RESOURCES: ReadonlySet<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Capability / identity fields on a model instance that must be handed back
|
|
13
|
+
* unproxied.
|
|
14
|
+
*
|
|
15
|
+
* ⚠ THIS LIST IS A DENYLIST WEARING AN ALLOWLIST'S CLOTHES. Anything named here
|
|
16
|
+
* bypasses the guard entirely, so an entry that DOES reach the model is a silent
|
|
17
|
+
* leak. Every name below was read out of the installed `@ai-sdk/provider@3.0.10`
|
|
18
|
+
* specifications and each was driven through the real `ai` entry point with the
|
|
19
|
+
* firewall blocking: the firewall-check count stays 0 because NO REQUEST IS
|
|
20
|
+
* ISSUED, not because the guard was skipped.
|
|
21
|
+
*
|
|
22
|
+
* Why these specifically cannot be left to the generic gate:
|
|
23
|
+
*
|
|
24
|
+
* `supportedUrls` is `Record<string, RegExp[]> | PromiseLike<…>`. Default-deny
|
|
25
|
+
* proxies a non-function object, so `Object.entries(await model.supportedUrls)`
|
|
26
|
+
* would yield proxied `RegExp[]` arrays whose `.test` is a FUNCTION — gated,
|
|
27
|
+
* returning a Promise where the SDK expects a boolean. The capability map
|
|
28
|
+
* would break, and `supportedUrls` being silently dropped is already a
|
|
29
|
+
* MEDIUM this package fixed once (audit 2026-07-14).
|
|
30
|
+
*
|
|
31
|
+
* `maxImagesPerCall` / `maxVideosPerCall` are
|
|
32
|
+
* `number | undefined | GetMaxXPerCallFunction`. The FUNCTION form, gated,
|
|
33
|
+
* returns a Promise where `generateImage` reads a number.
|
|
34
|
+
*
|
|
35
|
+
* `maxEmbeddingsPerCall` / `supportsParallelCalls` are
|
|
36
|
+
* `PromiseLike<…> | value`. A proxied thenable is a different object identity
|
|
37
|
+
* than the SDK's own and buys nothing — neither carries caller text.
|
|
38
|
+
*
|
|
39
|
+
* `specificationVersion` / `provider` / `modelId` are strings and are passed
|
|
40
|
+
* through by the primitive rule without needing a name here; they are listed
|
|
41
|
+
* anyway so this file is a complete statement of the model spec's non-method
|
|
42
|
+
* surface rather than a partial one a reader has to cross-check.
|
|
43
|
+
*/
|
|
44
|
+
export declare const TRANSPORT_PASSTHROUGH: ReadonlySet<string>;
|
|
45
|
+
/** Union, for callers/tests asking "is this name exempt at all". */
|
|
46
|
+
export declare const KNOWN_SAFE_PASSTHROUGH: ReadonlySet<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Methods with a DEDICATED proxy in index.ts — the rich paths that also do
|
|
49
|
+
* stream teeing, token accounting, cost estimation and trace logging.
|
|
50
|
+
*
|
|
51
|
+
* These are NOT passthrough: `assertSurfacePolicy` fails at wrap time if the
|
|
52
|
+
* wrapper does not supply a handler for every name here, and it fails if a name
|
|
53
|
+
* appears here AND on a passthrough set.
|
|
54
|
+
*
|
|
55
|
+
* `doGenerate` is deliberately dedicated for EVERY model kind, not just language
|
|
56
|
+
* models. Image, speech, transcription and video specifications all spell their
|
|
57
|
+
* call method `doGenerate`, so routing it anywhere else would mean sniffing the
|
|
58
|
+
* model's shape to decide how hard to guard it — which is a denylist with extra
|
|
59
|
+
* steps. The rich override is shape-tolerant (`readTokens` and
|
|
60
|
+
* `extractOutputText` both degrade to 0 / "" on a non-language result), and its
|
|
61
|
+
* input rail now walks the whole call-options body, so a speech call is gated on
|
|
62
|
+
* `options.text` exactly like a language call is gated on `options.prompt`.
|
|
63
|
+
*/
|
|
64
|
+
export declare const DEDICATED_RESOURCES: ReadonlySet<string>;
|
|
65
|
+
/**
|
|
66
|
+
* True for an `@ai-sdk/provider` model specification object.
|
|
67
|
+
*
|
|
68
|
+
* Verified against `@ai-sdk/provider@3.0.10`: every one of the seven
|
|
69
|
+
* specifications declares `specificationVersion` AND `modelId` (language v2:52,
|
|
70
|
+
* v3:12/22; embedding v2:24/34, v3:18/28; image v2:27/37, v3:28/38; speech
|
|
71
|
+
* v2:17/27, v3:17/27; transcription v2:17/27, v3:17/27; reranking v3:15/25;
|
|
72
|
+
* video v3:49/59).
|
|
73
|
+
*
|
|
74
|
+
* This is what stops a passthrough NAME from un-guarding a MODEL: a wrapper or
|
|
75
|
+
* middleware that exposes an inner model under a capability name (e.g. a
|
|
76
|
+
* `provider` field holding a delegate rather than a string) still gets guarded,
|
|
77
|
+
* because the passthrough allowlist only wins when the VALUE is not a model.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isAiSdkModel(value: unknown): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Call-option fields KNOWN to carry model-visible caller text on
|
|
82
|
+
* `@ai-sdk/provider@3.0.10`.
|
|
83
|
+
*
|
|
84
|
+
* The FLOOR, not the gate — the shared extractor unions a whole-body walk on
|
|
85
|
+
* top, so a field nobody enumerated is scanned anyway. REMOVING a name here
|
|
86
|
+
* narrows detection; ADDING one is redundant. Every entry is a real field on a
|
|
87
|
+
* real specification's call-options type:
|
|
88
|
+
*
|
|
89
|
+
* prompt LanguageModelV{2,3}CallOptions, ImageModelV3CallOptions:3,
|
|
90
|
+
* VideoModelV3CallOptions:3
|
|
91
|
+
* values EmbeddingModelV3CallOptions:3
|
|
92
|
+
* text SpeechModelV3CallOptions:4
|
|
93
|
+
* instructions SpeechModelV3CallOptions:7
|
|
94
|
+
* audio TranscriptionModelV3CallOptions:4
|
|
95
|
+
* documents / query RerankingModelV3CallOptions:4/8
|
|
96
|
+
* messages / content / input the shapes `collapsePromptText` renders
|
|
97
|
+
*
|
|
98
|
+
* ⚠ MEASURED LIMIT — TRANSCRIPTION AUDIO IS NOT SCANNED. Listing `audio` above
|
|
99
|
+
* does not make it readable. `ai@6.0.208`'s `transcribe` normalises whatever the
|
|
100
|
+
* caller passed (base64 string, data URL, ArrayBuffer) into a `Uint8Array`
|
|
101
|
+
* BEFORE the model boundary, and wrapper-core's walker excludes typed arrays by
|
|
102
|
+
* SHAPE — decoding an upload as UTF-8 produces megabytes of mojibake that would
|
|
103
|
+
* trip the size bound and refuse every legitimate audio call. Measured, with a
|
|
104
|
+
* canary in the audio and the firewall recording what it was shown:
|
|
105
|
+
*
|
|
106
|
+
* transcribe(audio: Uint8Array) firewall saw ["audio/wav"]
|
|
107
|
+
* transcribe(audio: base64 string) firewall saw ["audio/wav"]
|
|
108
|
+
* generateSpeech(text) firewall saw ["<canary>"]
|
|
109
|
+
* embed(value) firewall saw ["<canary>"]
|
|
110
|
+
* rerank(documents, query) firewall saw ["<canary>", "<canary>"]
|
|
111
|
+
*
|
|
112
|
+
* So a transcription call IS gated — `mediaType` is model-visible text, the rail
|
|
113
|
+
* runs, and the firewall can refuse the call — but the gate is not looking at the
|
|
114
|
+
* audio. That is the same documented unscannable limit as an uploaded file body,
|
|
115
|
+
* stated here rather than left to be inferred from a green matrix.
|
|
116
|
+
*/
|
|
117
|
+
export declare const MODEL_INPUT_FIELDS: readonly string[];
|
|
118
|
+
export declare const VERCEL_AI_SURFACE_POLICY: SurfacePolicy;
|
|
119
|
+
/**
|
|
120
|
+
* The policy bound to one model instance, so traces carry the real model id.
|
|
121
|
+
*
|
|
122
|
+
* A function rather than a mutation: two wrapped models in the same process must
|
|
123
|
+
* not be able to overwrite each other's trace metadata.
|
|
124
|
+
*/
|
|
125
|
+
export declare function vercelAiSurfacePolicy(modelId: unknown): SurfacePolicy;
|
|
126
|
+
//# sourceMappingURL=surface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"AAqFA,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAuB,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAapD,CAAC;AAEH,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAGrD,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAGlD,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAW/C,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,aAatC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAOrE"}
|
package/dist/surface.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// ── Default-deny classification of the AI SDK model surface ─────────────────
|
|
2
|
+
//
|
|
3
|
+
// AUDIT 2026-08-08 (vercel-ai residual). `withEvalguard` returned a Proxy whose
|
|
4
|
+
// `get` trap branched on `doGenerate` / `doStream` and sent everything else to
|
|
5
|
+
// `Reflect.get`. That is the same DENYLIST shape that made `@evalguard/anthropic`
|
|
6
|
+
// and `@evalguard/gemini` bypassable, and it was left in place on the strength of
|
|
7
|
+
// an argument that was never executed:
|
|
8
|
+
//
|
|
9
|
+
// "`ai@6.0.208`'s type surface contains zero `doX` names and imports the model
|
|
10
|
+
// spec from `@ai-sdk/provider`, so it is a FIXED interface, not an open
|
|
11
|
+
// resource tree a provider can extend."
|
|
12
|
+
//
|
|
13
|
+
// Both halves are false. `@ai-sdk/provider@3.0.10` (the exact version `ai@6.0.208`
|
|
14
|
+
// depends on) declares FOUR distinct `doX` method names across SEVEN model
|
|
15
|
+
// specifications, and `withEvalguard` gated two of them:
|
|
16
|
+
//
|
|
17
|
+
// spec method file:line
|
|
18
|
+
// LanguageModelV2 doGenerate language-model/v2/language-model-v2.ts:52 GATED
|
|
19
|
+
// LanguageModelV2 doStream language-model/v2/language-model-v2.ts:114 GATED
|
|
20
|
+
// LanguageModelV3 doGenerate language-model/v3/language-model-v3.ts:46 GATED
|
|
21
|
+
// LanguageModelV3 doStream language-model/v3/language-model-v3.ts:58 GATED
|
|
22
|
+
// EmbeddingModelV2 doEmbed embedding-model/v2/embedding-model-v2.ts:57 ungated
|
|
23
|
+
// EmbeddingModelV3 doEmbed embedding-model/v3/embedding-model-v3.ts:51 ungated
|
|
24
|
+
// ImageModelV2 doGenerate image-model/v2/image-model-v2.ts:50 gated by NAME COLLISION
|
|
25
|
+
// ImageModelV3 doGenerate image-model/v3/image-model-v3.ts:51 gated by NAME COLLISION
|
|
26
|
+
// SpeechModelV2 doGenerate speech-model/v2/speech-model-v2.ts:32 ungated IN EFFECT
|
|
27
|
+
// SpeechModelV3 doGenerate speech-model/v3/speech-model-v3.ts:32 ungated IN EFFECT
|
|
28
|
+
// TranscriptionModelV2 doGenerate transcription-model/v2/transcription-model-v2.ts:32 ungated IN EFFECT
|
|
29
|
+
// TranscriptionModelV3 doGenerate transcription-model/v3/transcription-model-v3.ts:32 ungated IN EFFECT
|
|
30
|
+
// RerankingModelV3 doRerank reranking-model/v3/reranking-model-v3.ts:31 ungated
|
|
31
|
+
// VideoModelV3 doGenerate video-model/v3/video-model-v3.ts:75 gated by NAME COLLISION
|
|
32
|
+
//
|
|
33
|
+
// Measured by driving each model kind through the REAL `ai@6.0.208` entry point
|
|
34
|
+
// (`embed`, `embedMany`, `generateImage`, `experimental_generateSpeech`,
|
|
35
|
+
// `experimental_transcribe`, `rerank`, `experimental_generateVideo`) against the
|
|
36
|
+
// pre-conversion `src/index.ts`, firewall configured to BLOCK ALL input:
|
|
37
|
+
//
|
|
38
|
+
// model / entry point checks leaked outcome
|
|
39
|
+
// LanguageModelV3.doGenerate (generateText) 1 0 BLOCKED
|
|
40
|
+
// LanguageModelV3.doStream (streamText) 1 0 BLOCKED
|
|
41
|
+
// EmbeddingModelV3.doEmbed (embed) 0 1 RETURNED
|
|
42
|
+
// EmbeddingModelV3.doEmbed (embedMany) 0 1 RETURNED
|
|
43
|
+
// ImageModelV3.doGenerate (generateImage) 1 0 BLOCKED
|
|
44
|
+
// SpeechModelV3.doGenerate (generateSpeech) 0 1 RETURNED
|
|
45
|
+
// TranscriptionModelV3.doGenerate (transcribe) 0 1 RETURNED
|
|
46
|
+
// RerankingModelV3.doRerank (rerank) 0 1 RETURNED
|
|
47
|
+
// VideoModelV3.doGenerate (generateVideo) 1 0 BLOCKED
|
|
48
|
+
//
|
|
49
|
+
// 4 of 9 delivered the attack text; all 4 on ZERO firewall calls.
|
|
50
|
+
//
|
|
51
|
+
// TWO DISTINCT FAILURE MODES, and only one of them is the classic denylist:
|
|
52
|
+
//
|
|
53
|
+
// 1. UNGATED METHOD NAME — `doEmbed`, `doRerank`. `Reflect.get` handed the real
|
|
54
|
+
// method straight back. This is the anthropic/gemini bug verbatim, and it is
|
|
55
|
+
// what `createDefaultDenySurface` closes.
|
|
56
|
+
//
|
|
57
|
+
// 2. GATED METHOD, BLIND EXTRACTOR — speech and transcription. Their payload is
|
|
58
|
+
// `options.text` / `options.audio`; `collapsePrompt` read only
|
|
59
|
+
// `params.prompt`, produced "", and the caller's rule was
|
|
60
|
+
// `if (promptText) { …check… }` — so "the collapser surfaced nothing" meant
|
|
61
|
+
// SKIP THE RAIL and call the model anyway. checks=0 with the interception
|
|
62
|
+
// working perfectly. Default-deny alone does NOT close this; the extractor
|
|
63
|
+
// has to walk the WHOLE call-options body, which is what
|
|
64
|
+
// `makeBodyTextExtractor` does and why `collapsePrompt` now unions
|
|
65
|
+
// `collectRequestStrings` on top of `collapsePromptText`.
|
|
66
|
+
//
|
|
67
|
+
// Reachability, stated honestly: `tsc` REJECTS all five non-language model kinds
|
|
68
|
+
// at the `withEvalguard` boundary (measured — TS2345 on MockEmbeddingModelV3,
|
|
69
|
+
// MockImageModelV3, MockSpeechModelV3, MockTranscriptionModelV3,
|
|
70
|
+
// MockRerankingModelV3), because the `LanguageModelV2` constraint requires
|
|
71
|
+
// `doStream`. So a type-checked TypeScript consumer cannot reach the leaks. Plain
|
|
72
|
+
// JavaScript, `as any`, and any config/registry-driven factory typed `unknown`
|
|
73
|
+
// can, and always could. The type system is not the gate — that is the entire
|
|
74
|
+
// lesson of 2026-08-08 — and a denylist that is correct only for one of seven
|
|
75
|
+
// specifications becomes a bypass the day upstream adds the eighth.
|
|
76
|
+
// FOLLOW-UP OWNED BY wrapper-core, not by this file:
|
|
77
|
+
// `wrapper-core/src/__tests__/surface-conformance-registry.test.ts` still lists
|
|
78
|
+
// `vercel-ai-wrapper` in `NON_PROXY_WRAPPERS`, with the reason quoted above. That
|
|
79
|
+
// list only records "no conformance test is REQUIRED here", so the ratchet is
|
|
80
|
+
// green either way and this package now carries one regardless
|
|
81
|
+
// (`src/__tests__/surface-conformance.test.ts`). The entry should move into
|
|
82
|
+
// `PROXY_WRAPPERS` with `conformanceTests: ["surface-conformance.test.ts"]` so
|
|
83
|
+
// the registry can never let this wrapper silently lose it again.
|
|
84
|
+
import { makeBodyTextExtractor, } from "@evalguard/wrapper-core";
|
|
85
|
+
/**
|
|
86
|
+
* Real API RESOURCES exempted by decision.
|
|
87
|
+
*
|
|
88
|
+
* Empty, deliberately. A model has no management endpoints — every method on
|
|
89
|
+
* every `@ai-sdk/provider` specification issues a provider request carrying
|
|
90
|
+
* caller-supplied content. Nothing has earned an exemption. An entry added here
|
|
91
|
+
* must carry its reason inline.
|
|
92
|
+
*/
|
|
93
|
+
export const EXEMPT_RESOURCES = new Set([]);
|
|
94
|
+
/**
|
|
95
|
+
* Capability / identity fields on a model instance that must be handed back
|
|
96
|
+
* unproxied.
|
|
97
|
+
*
|
|
98
|
+
* ⚠ THIS LIST IS A DENYLIST WEARING AN ALLOWLIST'S CLOTHES. Anything named here
|
|
99
|
+
* bypasses the guard entirely, so an entry that DOES reach the model is a silent
|
|
100
|
+
* leak. Every name below was read out of the installed `@ai-sdk/provider@3.0.10`
|
|
101
|
+
* specifications and each was driven through the real `ai` entry point with the
|
|
102
|
+
* firewall blocking: the firewall-check count stays 0 because NO REQUEST IS
|
|
103
|
+
* ISSUED, not because the guard was skipped.
|
|
104
|
+
*
|
|
105
|
+
* Why these specifically cannot be left to the generic gate:
|
|
106
|
+
*
|
|
107
|
+
* `supportedUrls` is `Record<string, RegExp[]> | PromiseLike<…>`. Default-deny
|
|
108
|
+
* proxies a non-function object, so `Object.entries(await model.supportedUrls)`
|
|
109
|
+
* would yield proxied `RegExp[]` arrays whose `.test` is a FUNCTION — gated,
|
|
110
|
+
* returning a Promise where the SDK expects a boolean. The capability map
|
|
111
|
+
* would break, and `supportedUrls` being silently dropped is already a
|
|
112
|
+
* MEDIUM this package fixed once (audit 2026-07-14).
|
|
113
|
+
*
|
|
114
|
+
* `maxImagesPerCall` / `maxVideosPerCall` are
|
|
115
|
+
* `number | undefined | GetMaxXPerCallFunction`. The FUNCTION form, gated,
|
|
116
|
+
* returns a Promise where `generateImage` reads a number.
|
|
117
|
+
*
|
|
118
|
+
* `maxEmbeddingsPerCall` / `supportsParallelCalls` are
|
|
119
|
+
* `PromiseLike<…> | value`. A proxied thenable is a different object identity
|
|
120
|
+
* than the SDK's own and buys nothing — neither carries caller text.
|
|
121
|
+
*
|
|
122
|
+
* `specificationVersion` / `provider` / `modelId` are strings and are passed
|
|
123
|
+
* through by the primitive rule without needing a name here; they are listed
|
|
124
|
+
* anyway so this file is a complete statement of the model spec's non-method
|
|
125
|
+
* surface rather than a partial one a reader has to cross-check.
|
|
126
|
+
*/
|
|
127
|
+
export const TRANSPORT_PASSTHROUGH = new Set([
|
|
128
|
+
// Identity — every specification, all `string`.
|
|
129
|
+
"specificationVersion",
|
|
130
|
+
"provider",
|
|
131
|
+
"modelId",
|
|
132
|
+
// LanguageModelV2 / V3 capability map.
|
|
133
|
+
"supportedUrls",
|
|
134
|
+
// EmbeddingModelV2 / V3 batching capability.
|
|
135
|
+
"maxEmbeddingsPerCall",
|
|
136
|
+
"supportsParallelCalls",
|
|
137
|
+
// ImageModelV2 / V3 and VideoModelV3 batching capability (may be a function).
|
|
138
|
+
"maxImagesPerCall",
|
|
139
|
+
"maxVideosPerCall",
|
|
140
|
+
]);
|
|
141
|
+
/** Union, for callers/tests asking "is this name exempt at all". */
|
|
142
|
+
export const KNOWN_SAFE_PASSTHROUGH = new Set([
|
|
143
|
+
...TRANSPORT_PASSTHROUGH,
|
|
144
|
+
...EXEMPT_RESOURCES,
|
|
145
|
+
]);
|
|
146
|
+
/**
|
|
147
|
+
* Methods with a DEDICATED proxy in index.ts — the rich paths that also do
|
|
148
|
+
* stream teeing, token accounting, cost estimation and trace logging.
|
|
149
|
+
*
|
|
150
|
+
* These are NOT passthrough: `assertSurfacePolicy` fails at wrap time if the
|
|
151
|
+
* wrapper does not supply a handler for every name here, and it fails if a name
|
|
152
|
+
* appears here AND on a passthrough set.
|
|
153
|
+
*
|
|
154
|
+
* `doGenerate` is deliberately dedicated for EVERY model kind, not just language
|
|
155
|
+
* models. Image, speech, transcription and video specifications all spell their
|
|
156
|
+
* call method `doGenerate`, so routing it anywhere else would mean sniffing the
|
|
157
|
+
* model's shape to decide how hard to guard it — which is a denylist with extra
|
|
158
|
+
* steps. The rich override is shape-tolerant (`readTokens` and
|
|
159
|
+
* `extractOutputText` both degrade to 0 / "" on a non-language result), and its
|
|
160
|
+
* input rail now walks the whole call-options body, so a speech call is gated on
|
|
161
|
+
* `options.text` exactly like a language call is gated on `options.prompt`.
|
|
162
|
+
*/
|
|
163
|
+
export const DEDICATED_RESOURCES = new Set([
|
|
164
|
+
"doGenerate",
|
|
165
|
+
"doStream",
|
|
166
|
+
]);
|
|
167
|
+
/**
|
|
168
|
+
* True for an `@ai-sdk/provider` model specification object.
|
|
169
|
+
*
|
|
170
|
+
* Verified against `@ai-sdk/provider@3.0.10`: every one of the seven
|
|
171
|
+
* specifications declares `specificationVersion` AND `modelId` (language v2:52,
|
|
172
|
+
* v3:12/22; embedding v2:24/34, v3:18/28; image v2:27/37, v3:28/38; speech
|
|
173
|
+
* v2:17/27, v3:17/27; transcription v2:17/27, v3:17/27; reranking v3:15/25;
|
|
174
|
+
* video v3:49/59).
|
|
175
|
+
*
|
|
176
|
+
* This is what stops a passthrough NAME from un-guarding a MODEL: a wrapper or
|
|
177
|
+
* middleware that exposes an inner model under a capability name (e.g. a
|
|
178
|
+
* `provider` field holding a delegate rather than a string) still gets guarded,
|
|
179
|
+
* because the passthrough allowlist only wins when the VALUE is not a model.
|
|
180
|
+
*/
|
|
181
|
+
export function isAiSdkModel(value) {
|
|
182
|
+
if (!value || typeof value !== "object")
|
|
183
|
+
return false;
|
|
184
|
+
const v = value;
|
|
185
|
+
return "specificationVersion" in v && "modelId" in v;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Call-option fields KNOWN to carry model-visible caller text on
|
|
189
|
+
* `@ai-sdk/provider@3.0.10`.
|
|
190
|
+
*
|
|
191
|
+
* The FLOOR, not the gate — the shared extractor unions a whole-body walk on
|
|
192
|
+
* top, so a field nobody enumerated is scanned anyway. REMOVING a name here
|
|
193
|
+
* narrows detection; ADDING one is redundant. Every entry is a real field on a
|
|
194
|
+
* real specification's call-options type:
|
|
195
|
+
*
|
|
196
|
+
* prompt LanguageModelV{2,3}CallOptions, ImageModelV3CallOptions:3,
|
|
197
|
+
* VideoModelV3CallOptions:3
|
|
198
|
+
* values EmbeddingModelV3CallOptions:3
|
|
199
|
+
* text SpeechModelV3CallOptions:4
|
|
200
|
+
* instructions SpeechModelV3CallOptions:7
|
|
201
|
+
* audio TranscriptionModelV3CallOptions:4
|
|
202
|
+
* documents / query RerankingModelV3CallOptions:4/8
|
|
203
|
+
* messages / content / input the shapes `collapsePromptText` renders
|
|
204
|
+
*
|
|
205
|
+
* ⚠ MEASURED LIMIT — TRANSCRIPTION AUDIO IS NOT SCANNED. Listing `audio` above
|
|
206
|
+
* does not make it readable. `ai@6.0.208`'s `transcribe` normalises whatever the
|
|
207
|
+
* caller passed (base64 string, data URL, ArrayBuffer) into a `Uint8Array`
|
|
208
|
+
* BEFORE the model boundary, and wrapper-core's walker excludes typed arrays by
|
|
209
|
+
* SHAPE — decoding an upload as UTF-8 produces megabytes of mojibake that would
|
|
210
|
+
* trip the size bound and refuse every legitimate audio call. Measured, with a
|
|
211
|
+
* canary in the audio and the firewall recording what it was shown:
|
|
212
|
+
*
|
|
213
|
+
* transcribe(audio: Uint8Array) firewall saw ["audio/wav"]
|
|
214
|
+
* transcribe(audio: base64 string) firewall saw ["audio/wav"]
|
|
215
|
+
* generateSpeech(text) firewall saw ["<canary>"]
|
|
216
|
+
* embed(value) firewall saw ["<canary>"]
|
|
217
|
+
* rerank(documents, query) firewall saw ["<canary>", "<canary>"]
|
|
218
|
+
*
|
|
219
|
+
* So a transcription call IS gated — `mediaType` is model-visible text, the rail
|
|
220
|
+
* runs, and the firewall can refuse the call — but the gate is not looking at the
|
|
221
|
+
* audio. That is the same documented unscannable limit as an uploaded file body,
|
|
222
|
+
* stated here rather than left to be inferred from a green matrix.
|
|
223
|
+
*/
|
|
224
|
+
export const MODEL_INPUT_FIELDS = [
|
|
225
|
+
"prompt",
|
|
226
|
+
"messages",
|
|
227
|
+
"content",
|
|
228
|
+
"input",
|
|
229
|
+
"values",
|
|
230
|
+
"text",
|
|
231
|
+
"instructions",
|
|
232
|
+
"audio",
|
|
233
|
+
"documents",
|
|
234
|
+
"query",
|
|
235
|
+
];
|
|
236
|
+
export const VERCEL_AI_SURFACE_POLICY = {
|
|
237
|
+
provider: "vercel-ai",
|
|
238
|
+
exemptResources: EXEMPT_RESOURCES,
|
|
239
|
+
transportPassthrough: TRANSPORT_PASSTHROUGH,
|
|
240
|
+
dedicatedResources: DEDICATED_RESOURCES,
|
|
241
|
+
isProviderResource: isAiSdkModel,
|
|
242
|
+
extractText: makeBodyTextExtractor(MODEL_INPUT_FIELDS),
|
|
243
|
+
// The model id lives on the MODEL, not in the call options — no
|
|
244
|
+
// `@ai-sdk/provider` call-options type carries a `model` field, so
|
|
245
|
+
// `extractModelIdFromArgs` would report "unknown" for every call. The wrapper
|
|
246
|
+
// binds the real `model.modelId` via {@link vercelAiSurfacePolicy}; this
|
|
247
|
+
// default is only what a caller gets who asks for the policy without a model.
|
|
248
|
+
extractModelId: () => "unknown",
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* The policy bound to one model instance, so traces carry the real model id.
|
|
252
|
+
*
|
|
253
|
+
* A function rather than a mutation: two wrapped models in the same process must
|
|
254
|
+
* not be able to overwrite each other's trace metadata.
|
|
255
|
+
*/
|
|
256
|
+
export function vercelAiSurfacePolicy(modelId) {
|
|
257
|
+
// `modelId` is `string` on every specification, but this runs on whatever the
|
|
258
|
+
// caller handed `withEvalguard` — including, in the conformance suite, an
|
|
259
|
+
// object that is not a model at all. A non-string here must degrade to the
|
|
260
|
+
// trace-metadata default, never propagate `undefined` into a trace field.
|
|
261
|
+
const id = typeof modelId === "string" && modelId ? modelId : "unknown";
|
|
262
|
+
return { ...VERCEL_AI_SURFACE_POLICY, extractModelId: () => id };
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=surface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surface.js","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "1.0.
|
|
1
|
+
export declare const VERSION = "1.0.2";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Single source of the wrapper version WITHIN src (so the User-Agent header
|
|
2
2
|
// can't drift between call sites). Kept in lockstep with package.json#version;
|
|
3
3
|
// a unit test (version.test.ts) fails CI if the two ever diverge. (#120)
|
|
4
|
-
export const VERSION = "1.0.
|
|
4
|
+
export const VERSION = "1.0.2";
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evalguard/vercel-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Drop-in Vercel AI SDK middleware with EvalGuard guardrails, trace logging & cost tracking",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -13,18 +13,10 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
15
|
"README.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
16
17
|
"LICENSE",
|
|
17
18
|
"peer-matrix.json"
|
|
18
19
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc",
|
|
21
|
-
"dev": "tsc --watch",
|
|
22
|
-
"type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"lint": "eslint src --quiet",
|
|
25
|
-
"clean": "rm -rf dist .turbo",
|
|
26
|
-
"prepublishOnly": "pnpm run build"
|
|
27
|
-
},
|
|
28
20
|
"keywords": [
|
|
29
21
|
"vercel",
|
|
30
22
|
"ai-sdk",
|
|
@@ -49,22 +41,30 @@
|
|
|
49
41
|
"ai": ">=5.0.0"
|
|
50
42
|
},
|
|
51
43
|
"dependencies": {
|
|
52
|
-
"@evalguard/wrapper-core": "
|
|
44
|
+
"@evalguard/wrapper-core": "^1.3.0"
|
|
53
45
|
},
|
|
54
46
|
"devDependencies": {
|
|
55
|
-
"@evalguard/config": "workspace:*",
|
|
56
47
|
"@types/node": "^26.0.0",
|
|
57
48
|
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
|
58
49
|
"@typescript-eslint/parser": "^8.61.1",
|
|
59
50
|
"ai": "^6.0.208",
|
|
60
51
|
"eslint": "^10.5.0",
|
|
61
52
|
"typescript": "^6.0.3",
|
|
62
|
-
"vitest": "^4.1.9"
|
|
53
|
+
"vitest": "^4.1.9",
|
|
54
|
+
"@evalguard/config": "0.0.1"
|
|
63
55
|
},
|
|
64
56
|
"type": "module",
|
|
65
57
|
"types": "./dist/index.d.ts",
|
|
66
58
|
"publishConfig": {
|
|
67
59
|
"access": "public",
|
|
68
60
|
"registry": "https://registry.npmjs.org/"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsc",
|
|
64
|
+
"dev": "tsc --watch",
|
|
65
|
+
"type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"lint": "eslint src --quiet",
|
|
68
|
+
"clean": "rm -rf dist .turbo"
|
|
69
69
|
}
|
|
70
|
-
}
|
|
70
|
+
}
|