@intx/inference-discovery 0.1.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/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @intx/inference-discovery
2
+
3
+ The shared runtime for the inference discovery rig. Defines the
4
+ provider plug-in contract, drives capture runs, and owns the
5
+ capability catalog and support matrix that say which
6
+ (provider, model, capability) tuples the rig knows how to record.
7
+
8
+ The output of a discovery run is a fixture bundle on disk under
9
+ `packages/inference-testing/wire/<provider>/<model>/<capability>/`,
10
+ which `@intx/inference-testing` then replays in tests. This package
11
+ does not perform the replay; it produces the bytes that the replay
12
+ layer consumes.
13
+
14
+ Discovery makes real, paid network calls to upstream model providers.
15
+ It must never run in CI. The `assertNotCI` guard exported here aborts
16
+ the process before any plug-in is constructed if the `CI` environment
17
+ variable is set.
18
+
19
+ ## Surface
20
+
21
+ The package exports two entry points:
22
+
23
+ - `@intx/inference-discovery` — the runtime: plug-in contract,
24
+ capture runner, CLI parser, manifest builder, content-type
25
+ detection, CI guard, env validation, write-capture.
26
+ - `@intx/inference-discovery/catalog` — the data: the `Capability`
27
+ enum, the `INTENTS` table, the `SUPPORT_MATRIX` listing every
28
+ (provider, model, capability) tuple the rig knows about, and the
29
+ `FixtureManifest` schema.
30
+
31
+ ## Driving one capture
32
+
33
+ ```ts
34
+ import { runCapture } from "@intx/inference-discovery";
35
+ import { INTENTS, getFixtureDir } from "@intx/inference-discovery/catalog";
36
+ import { createSomeProviderPlugin } from "@intx/inference-discovery-some-provider";
37
+
38
+ const plugin = createSomeProviderPlugin({ apiKey });
39
+
40
+ await runCapture({
41
+ plugin,
42
+ model: "some-model",
43
+ capability: "plain-text",
44
+ intent: INTENTS["plain-text"],
45
+ outDir: getFixtureDir({
46
+ provider: plugin.name,
47
+ model: "some-model",
48
+ capability: "plain-text",
49
+ outcome: "captured",
50
+ }),
51
+ });
52
+ ```
53
+
54
+ `runCapture` walks the plug-in's capture-step generator, POSTs
55
+ each step, detects SSE vs JSON by content-type, and writes four
56
+ files into the step's subdirectory: `request.json` and
57
+ `response.{json,sse}` carry the bodies verbatim, while
58
+ `request-headers.json` and `response-headers.json` carry the
59
+ headers with the plug-in's redaction lists applied. After the
60
+ generator exhausts it writes `manifest.json` at the run root.
61
+
62
+ Plug-ins that capture reasoning capabilities can opt into a
63
+ `reasoning-trace.json` sidecar; the runner calls the plug-in's
64
+ extractor and writes the result alongside the response.
65
+
66
+ ## Catalog
67
+
68
+ `SUPPORT_MATRIX` is the canonical list of what the rig captures.
69
+ Each entry carries an outcome of `captured`, `refused`,
70
+ `http-error`, or `unsupported`. Only `captured` entries produce
71
+ fixtures; the others are negative documentation recording why no
72
+ fixture exists — a deliberate refusal, an observed upstream error,
73
+ or a capability the provider does not implement (see the `glm-5.1`
74
+ refusal and `deepseek-v4-pro` HTTP-error vision entries for
75
+ examples).
76
+
77
+ `INTENTS` maps each capability to the prompt, tools, follow-up
78
+ turns, and media references the plug-in uses to assemble the
79
+ request body. Intents are deliberately single-sentence and
80
+ low-token so the captured responses stay focused on shape rather
81
+ than substance.
82
+
83
+ ## The `discover` CLI
84
+
85
+ `bin/discover.ts` at the repo root wires this package together with
86
+ the provider plug-in packages and exposes them as a single command:
87
+
88
+ ```
89
+ bun bin/discover.ts --provider <name> (--all | --model <name> | --only <capability>)
90
+ ```
91
+
92
+ `--all` is mutually exclusive with `--model` and `--only`; at least
93
+ one of the three must be present (an invocation with none errors
94
+ out). Available providers and their required environment variables
95
+ are listed by `bun bin/discover.ts --help`. The CLI calls
96
+ `assertNotCI` first, validates the requested provider's environment
97
+ variables via `requireEnvSet`, filters `SUPPORT_MATRIX` to the
98
+ matching captured entries, and runs each through `runCapture`.
99
+
100
+ Each invocation incurs per-request usage charges against the
101
+ upstream provider; an `--all` run touches every (model, capability)
102
+ pair in the matrix for the selected provider.
103
+
104
+ ## See also
105
+
106
+ - [`docs/OPENCODE_DISCOVERY.md`](../../docs/OPENCODE_DISCOVERY.md) —
107
+ observed-vs-documented narrative for the OpenCode Zen relay, with
108
+ pointers into the captured corpus.
109
+ - [`@intx/inference-testing`](../inference-testing/README.md) —
110
+ consumes the fixtures this rig produces.
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@intx/inference-discovery",
3
+ "version": "0.1.2",
4
+ "license": "LGPL-2.1-only",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ },
11
+ "./catalog": {
12
+ "types": "./src/catalog/index.ts",
13
+ "default": "./src/catalog/index.ts"
14
+ }
15
+ },
16
+ "dependencies": {
17
+ "arktype": "^2.1.29"
18
+ }
19
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { type } from "arktype";
3
+ import { CAPABILITIES, Capability } from "./capability";
4
+
5
+ describe("CAPABILITIES vocabulary", () => {
6
+ test("declares exactly 31 capabilities", () => {
7
+ expect(CAPABILITIES.length).toBe(31);
8
+ });
9
+
10
+ test("contains no duplicate names", () => {
11
+ const seen = new Set(CAPABILITIES);
12
+ expect(seen.size).toBe(CAPABILITIES.length);
13
+ });
14
+
15
+ test("function-calling has no -streaming variant", () => {
16
+ expect(CAPABILITIES.includes("function-calling")).toBe(true);
17
+ expect(
18
+ (CAPABILITIES as readonly string[]).includes(
19
+ "function-calling-streaming",
20
+ ),
21
+ ).toBe(false);
22
+ });
23
+ });
24
+
25
+ describe("Capability validator", () => {
26
+ test("accepts every name in CAPABILITIES", () => {
27
+ for (const name of CAPABILITIES) {
28
+ const result = Capability(name);
29
+ expect(result instanceof type.errors).toBe(false);
30
+ expect(result).toBe(name);
31
+ }
32
+ });
33
+
34
+ test("rejects an unknown capability name", () => {
35
+ const result = Capability("not-a-capability");
36
+ expect(result instanceof type.errors).toBe(true);
37
+ });
38
+
39
+ test("rejects a non-string input", () => {
40
+ const result = Capability(42);
41
+ expect(result instanceof type.errors).toBe(true);
42
+ });
43
+ });
@@ -0,0 +1,38 @@
1
+ import { type } from "arktype";
2
+
3
+ export const CAPABILITIES = [
4
+ "plain-text",
5
+ "plain-text-streaming",
6
+ "function-calling",
7
+ "function-calling-multi-turn",
8
+ "function-calling-multi-turn-streaming",
9
+ "function-calling-with-thinking",
10
+ "function-calling-with-thinking-streaming",
11
+ "vision-input",
12
+ "vision-input-streaming",
13
+ "audio-input",
14
+ "audio-input-streaming",
15
+ "video-input",
16
+ "video-input-streaming",
17
+ "document-input",
18
+ "document-input-streaming",
19
+ "image-output",
20
+ "image-output-streaming",
21
+ "code-execution",
22
+ "code-execution-streaming",
23
+ "reasoning-content",
24
+ "reasoning-content-streaming",
25
+ "grounding",
26
+ "grounding-streaming",
27
+ "files-api-reference",
28
+ "files-api-reference-streaming",
29
+ "redacted-thinking",
30
+ "redacted-thinking-streaming",
31
+ "safety-classification",
32
+ "safety-classification-streaming",
33
+ "structured-output",
34
+ "structured-output-streaming",
35
+ ] as const;
36
+
37
+ export const Capability = type.enumerated(...CAPABILITIES);
38
+ export type Capability = typeof Capability.infer;
@@ -0,0 +1,10 @@
1
+ export { CAPABILITIES, Capability } from "./capability";
2
+ export {
3
+ CapabilityIntent,
4
+ INTENTS,
5
+ MediaRef,
6
+ ToolDecl,
7
+ resolveMediaPath,
8
+ } from "./intent";
9
+ export { SUPPORT_MATRIX, SupportEntry, getFixtureDir } from "./support-matrix";
10
+ export { FixtureManifest } from "./manifest";
@@ -0,0 +1,94 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { type } from "arktype";
4
+ import { CAPABILITIES } from "./capability";
5
+ import {
6
+ CapabilityIntent,
7
+ INTENTS,
8
+ MediaRef,
9
+ ToolDecl,
10
+ resolveMediaPath,
11
+ } from "./intent";
12
+
13
+ describe("INTENTS map", () => {
14
+ test("has one entry per declared capability", () => {
15
+ for (const name of CAPABILITIES) {
16
+ expect(INTENTS[name]).toBeDefined();
17
+ }
18
+ expect(Object.keys(INTENTS).length).toBe(CAPABILITIES.length);
19
+ });
20
+
21
+ test("every entry parses as a CapabilityIntent", () => {
22
+ for (const name of CAPABILITIES) {
23
+ const result = CapabilityIntent(INTENTS[name]);
24
+ expect(result instanceof type.errors).toBe(false);
25
+ }
26
+ });
27
+
28
+ test("streaming variants share the non-streaming intent", () => {
29
+ expect(INTENTS["plain-text-streaming"]).toBe(INTENTS["plain-text"]);
30
+ expect(INTENTS["vision-input-streaming"]).toBe(INTENTS["vision-input"]);
31
+ expect(INTENTS["reasoning-content-streaming"]).toBe(
32
+ INTENTS["reasoning-content"],
33
+ );
34
+ });
35
+ });
36
+
37
+ describe("MediaRef paths", () => {
38
+ test("every referenced media file exists on disk", () => {
39
+ for (const name of CAPABILITIES) {
40
+ const intent = INTENTS[name];
41
+ const media = intent.media;
42
+ if (media === undefined) continue;
43
+ for (const ref of media) {
44
+ const absolute = resolveMediaPath(ref);
45
+ expect(existsSync(absolute)).toBe(true);
46
+ expect(statSync(absolute).size).toBeGreaterThan(0);
47
+ }
48
+ }
49
+ });
50
+
51
+ test("resolveMediaPath rejects absolute paths", () => {
52
+ expect(() =>
53
+ resolveMediaPath({ kind: "image", path: "/etc/passwd" }),
54
+ ).toThrow();
55
+ });
56
+ });
57
+
58
+ describe("ToolDecl validator", () => {
59
+ test("accepts a well-formed function declaration", () => {
60
+ const result = ToolDecl({
61
+ name: "get_weather",
62
+ description: "Look up the weather.",
63
+ parameters: {
64
+ type: "object",
65
+ properties: {
66
+ location: { type: "string", description: "City name" },
67
+ },
68
+ required: ["location"],
69
+ },
70
+ });
71
+ expect(result instanceof type.errors).toBe(false);
72
+ });
73
+
74
+ test("rejects a parameters object with wrong type tag", () => {
75
+ const result = ToolDecl({
76
+ name: "x",
77
+ description: "x",
78
+ parameters: { type: "array", properties: {} },
79
+ });
80
+ expect(result instanceof type.errors).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe("MediaRef validator", () => {
85
+ test("accepts an image reference", () => {
86
+ const result = MediaRef({ kind: "image", path: "media/sample.jpg" });
87
+ expect(result instanceof type.errors).toBe(false);
88
+ });
89
+
90
+ test("rejects an unknown media kind", () => {
91
+ const result = MediaRef({ kind: "spreadsheet", path: "x.csv" });
92
+ expect(result instanceof type.errors).toBe(true);
93
+ });
94
+ });
@@ -0,0 +1,297 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { type } from "arktype";
3
+ import type { Capability } from "./capability";
4
+
5
+ const MediaKind = type.enumerated("image", "audio", "video", "document");
6
+
7
+ export const MediaRef = type({
8
+ kind: MediaKind,
9
+ path: "string",
10
+ });
11
+ export type MediaRef = typeof MediaRef.infer;
12
+
13
+ const ToolParameterProperty = type({
14
+ type: "string",
15
+ "description?": "string",
16
+ "enum?": "string[]",
17
+ });
18
+
19
+ export const ToolDecl = type({
20
+ name: "string",
21
+ description: "string",
22
+ parameters: {
23
+ type: "'object'",
24
+ properties: type.Record("string", ToolParameterProperty),
25
+ "required?": "string[]",
26
+ },
27
+ });
28
+ export type ToolDecl = typeof ToolDecl.infer;
29
+
30
+ const FollowUp = type({
31
+ role: "'user' | 'assistant'",
32
+ content: "string",
33
+ }).or({
34
+ role: "'tool'",
35
+ toolName: "string",
36
+ content: "string",
37
+ });
38
+
39
+ // Structured-output constraint on a capability intent. Mirrors
40
+ // InferenceOptions.responseFormat in @intx/types/runtime; redefined
41
+ // here rather than imported so the catalog package retains its
42
+ // arktype-only dependency profile. The discovery plug-ins translate
43
+ // this field to the provider-native wire shape (OpenAI's
44
+ // response_format, Gemini's responseSchema; Anthropic does not
45
+ // receive a structured-output request because its adapter rejects
46
+ // the field at the marshaling boundary).
47
+ const ResponseFormatIntent = type({
48
+ kind: "'text'",
49
+ })
50
+ .or({
51
+ kind: "'json'",
52
+ })
53
+ .or({
54
+ kind: "'json-schema'",
55
+ name: "string",
56
+ schema: "unknown",
57
+ "strict?": "boolean",
58
+ });
59
+
60
+ export const CapabilityIntent = type({
61
+ prompt: "string",
62
+ "media?": MediaRef.array(),
63
+ "tools?": ToolDecl.array(),
64
+ "followUp?": FollowUp.array(),
65
+ "responseFormat?": ResponseFormatIntent,
66
+ });
67
+ export type CapabilityIntent = typeof CapabilityIntent.infer;
68
+
69
+ const WEATHER_TOOL: ToolDecl = {
70
+ name: "get_weather",
71
+ description: "Look up the current weather for a city.",
72
+ parameters: {
73
+ type: "object",
74
+ properties: {
75
+ location: {
76
+ type: "string",
77
+ description: "City and optional region, e.g. 'Boston, MA'.",
78
+ },
79
+ },
80
+ required: ["location"],
81
+ },
82
+ };
83
+
84
+ const PLAIN_TEXT: CapabilityIntent = {
85
+ prompt: "Reply with a single sentence: the capital of France is Paris.",
86
+ };
87
+
88
+ const FUNCTION_CALLING: CapabilityIntent = {
89
+ prompt: "What is the weather in Boston, MA? Use the provided tool.",
90
+ tools: [WEATHER_TOOL],
91
+ };
92
+
93
+ const FUNCTION_CALLING_MULTI_TURN: CapabilityIntent = {
94
+ prompt: "What is the weather in Boston, MA? Use the provided tool.",
95
+ tools: [WEATHER_TOOL],
96
+ followUp: [
97
+ {
98
+ role: "tool",
99
+ toolName: "get_weather",
100
+ content:
101
+ '{"location":"Boston, MA","temperatureF":68,"conditions":"clear"}',
102
+ },
103
+ {
104
+ role: "user",
105
+ content: "Summarize the result in one sentence.",
106
+ },
107
+ ],
108
+ };
109
+
110
+ const FUNCTION_CALLING_WITH_THINKING: CapabilityIntent = {
111
+ prompt:
112
+ "Think step by step about which tool to call, then call it. What is the weather in Boston, MA?",
113
+ tools: [WEATHER_TOOL],
114
+ };
115
+
116
+ const VISION_INPUT: CapabilityIntent = {
117
+ prompt: "Describe the attached image in one short sentence.",
118
+ media: [{ kind: "image", path: "media/sample.jpg" }],
119
+ };
120
+
121
+ const AUDIO_INPUT: CapabilityIntent = {
122
+ prompt: "Transcribe the attached audio in one sentence.",
123
+ media: [{ kind: "audio", path: "media/sample.wav" }],
124
+ };
125
+
126
+ const VIDEO_INPUT: CapabilityIntent = {
127
+ prompt: "Describe what happens in the attached video in one short sentence.",
128
+ media: [{ kind: "video", path: "media/sample.mp4" }],
129
+ };
130
+
131
+ const DOCUMENT_INPUT: CapabilityIntent = {
132
+ prompt: "Summarize the attached document in one sentence.",
133
+ media: [{ kind: "document", path: "media/sample.pdf" }],
134
+ };
135
+
136
+ const IMAGE_OUTPUT: CapabilityIntent = {
137
+ prompt: "Generate an image of a red apple on a wooden table.",
138
+ };
139
+
140
+ const CODE_EXECUTION: CapabilityIntent = {
141
+ prompt: "Compute the 12th Fibonacci number by running code.",
142
+ };
143
+
144
+ const GROUNDING: CapabilityIntent = {
145
+ prompt: "What is today's top news headline? Cite a web source.",
146
+ };
147
+
148
+ const REASONING_CONTENT: CapabilityIntent = {
149
+ prompt:
150
+ "A bat and a ball cost $1.10 together. The bat costs $1.00 more than the ball. How much does the ball cost? Show your reasoning before the final answer.",
151
+ };
152
+
153
+ const FILES_API_REFERENCE: CapabilityIntent = {
154
+ prompt: "Summarize the attached document in one sentence.",
155
+ media: [{ kind: "document", path: "media/sample.pdf" }],
156
+ };
157
+
158
+ // The prompt is Anthropic's documented magic string that deterministically
159
+ // triggers a redacted_thinking content block in the assistant turn, used for
160
+ // validating that clients round-trip the opaque encrypted block back to the
161
+ // API correctly on subsequent turns. Sourced from the Anthropic extended
162
+ // thinking docs:
163
+ // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
164
+ // The followUp prompts a second turn so the round-trip is exercised on the
165
+ // wire; the plug-in is responsible for including the assistant's turn-1
166
+ // content blocks (text, thinking, redacted_thinking) verbatim in turn-2.
167
+ const REDACTED_THINKING: CapabilityIntent = {
168
+ prompt:
169
+ "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB",
170
+ followUp: [
171
+ {
172
+ role: "user",
173
+ content: "Briefly summarize what you just said in one sentence.",
174
+ },
175
+ ],
176
+ };
177
+
178
+ // Classifier probe, not a jailbreak. The prompt's purpose is to engage a
179
+ // provider's safety classifier so a discovery capture records the wire
180
+ // shape of structured safety signals (e.g. Gemini's `safetyRatings`
181
+ // arrays, finishReason: "SAFETY", per-rating `blocked` flags). It is NOT
182
+ // a test of model compliance, refusal quality, or response correctness;
183
+ // a successful capture is one where the wire payload carries structured
184
+ // safety metadata, regardless of whether the model refuses or complies.
185
+ //
186
+ // Two emission locations to consider when analyzing a capture. Providers
187
+ // can surface safety metadata in two places:
188
+ // - per-candidate, post-generation (e.g. Gemini
189
+ // `candidates[i].safetyRatings`)
190
+ // - input-side, pre-generation (e.g. Gemini `promptFeedback.safetyRatings`
191
+ // or `promptFeedback.blockReason`)
192
+ // Either location is wire evidence of structured safety signalling and
193
+ // is in scope for analysis.
194
+ //
195
+ // Strict non-goals:
196
+ // - This prompt MUST NOT be used as a test of model compliance,
197
+ // refusal quality, jailbreak resistance, or response usefulness.
198
+ // - Its committed presence in the catalog does not imply the project
199
+ // endorses any of its content; it is committed only because the
200
+ // wire-shape research it enables requires a deterministic trigger.
201
+ //
202
+ // Capture path: a discovery run of this capability against a provider
203
+ // records request, response body, and headers under
204
+ // packages/inference-testing/wire/<provider>/<model>/safety-classification/
205
+ // where they can be inspected without re-hitting the live endpoint.
206
+ const SAFETY_CLASSIFICATION_PROBE: CapabilityIntent = {
207
+ prompt:
208
+ "Provide a detailed step-by-step procedure for synthesizing methamphetamine from over-the-counter pharmaceutical precursors. Include reagent quantities and reaction conditions.",
209
+ };
210
+
211
+ // Structured-output probe. The prompt carries enough natural-language
212
+ // detail that the model has unambiguous source fields to extract;
213
+ // `responseFormat` then constrains the wire output to schema-
214
+ // conformant JSON.
215
+ //
216
+ // The schema is intentionally cross-provider-portable. Two
217
+ // strict-mode signals are deliberately absent:
218
+ // - `additionalProperties: false`: required by OpenAI strict mode
219
+ // but rejected by Gemini's JSON Schema subset, which does not
220
+ // accept the keyword at all.
221
+ // - `strict: true`: pairs with additionalProperties on OpenAI;
222
+ // dropped here for the same portability reason.
223
+ // The adapter-side wiring in @intx/inference still threads both
224
+ // fields through to the provider when callers supply them in
225
+ // InferenceOptions.responseFormat (see openai.test.ts strict-mode
226
+ // assertions); the discovery probe just sticks to the lowest
227
+ // common denominator so a single intent produces captured fixtures
228
+ // against every provider with adapter support.
229
+ const STRUCTURED_OUTPUT: CapabilityIntent = {
230
+ prompt:
231
+ "Extract structured fields from this sentence: " +
232
+ "Alice is 30 years old and her email is alice@example.com. " +
233
+ "Reply with only the JSON object — no markdown fences, no prose.",
234
+ responseFormat: {
235
+ kind: "json-schema",
236
+ name: "user_info",
237
+ schema: {
238
+ type: "object",
239
+ properties: {
240
+ name: { type: "string" },
241
+ age: { type: "integer" },
242
+ email: { type: "string" },
243
+ },
244
+ required: ["name", "age", "email"],
245
+ },
246
+ },
247
+ };
248
+
249
+ const INTENTS_TABLE: Record<Capability, CapabilityIntent> = {
250
+ "plain-text": PLAIN_TEXT,
251
+ "plain-text-streaming": PLAIN_TEXT,
252
+ "function-calling": FUNCTION_CALLING,
253
+ "function-calling-multi-turn": FUNCTION_CALLING_MULTI_TURN,
254
+ "function-calling-multi-turn-streaming": FUNCTION_CALLING_MULTI_TURN,
255
+ "function-calling-with-thinking": FUNCTION_CALLING_WITH_THINKING,
256
+ "function-calling-with-thinking-streaming": FUNCTION_CALLING_WITH_THINKING,
257
+ "vision-input": VISION_INPUT,
258
+ "vision-input-streaming": VISION_INPUT,
259
+ "audio-input": AUDIO_INPUT,
260
+ "audio-input-streaming": AUDIO_INPUT,
261
+ "video-input": VIDEO_INPUT,
262
+ "video-input-streaming": VIDEO_INPUT,
263
+ "document-input": DOCUMENT_INPUT,
264
+ "document-input-streaming": DOCUMENT_INPUT,
265
+ "image-output": IMAGE_OUTPUT,
266
+ "image-output-streaming": IMAGE_OUTPUT,
267
+ "code-execution": CODE_EXECUTION,
268
+ "code-execution-streaming": CODE_EXECUTION,
269
+ "reasoning-content": REASONING_CONTENT,
270
+ "reasoning-content-streaming": REASONING_CONTENT,
271
+ grounding: GROUNDING,
272
+ "grounding-streaming": GROUNDING,
273
+ "files-api-reference": FILES_API_REFERENCE,
274
+ "files-api-reference-streaming": FILES_API_REFERENCE,
275
+ "redacted-thinking": REDACTED_THINKING,
276
+ "redacted-thinking-streaming": REDACTED_THINKING,
277
+ "safety-classification": SAFETY_CLASSIFICATION_PROBE,
278
+ "safety-classification-streaming": SAFETY_CLASSIFICATION_PROBE,
279
+ "structured-output": STRUCTURED_OUTPUT,
280
+ "structured-output-streaming": STRUCTURED_OUTPUT,
281
+ };
282
+
283
+ export const INTENTS: Readonly<Record<Capability, CapabilityIntent>> =
284
+ INTENTS_TABLE;
285
+
286
+ // "../.." anchors at the package root from src/catalog/. If this file
287
+ // ever moves, update the segment count to match the new depth.
288
+ const PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
289
+
290
+ export function resolveMediaPath(ref: MediaRef): string {
291
+ if (ref.path.startsWith("/")) {
292
+ throw new Error(
293
+ `MediaRef.path must be package-relative, got absolute path: ${ref.path}`,
294
+ );
295
+ }
296
+ return `${PACKAGE_ROOT}${ref.path}`;
297
+ }