From 7a74136bda888efde099b537bab324fe00a5142c Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Fri, 5 Jun 2026 13:25:06 -0400 Subject: [PATCH] fix(types): derive agent llm.vendor union from @jambonz/schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AgentLlm.vendor union was hand-maintained and had drifted from the schema enum — it was missing baseten, azure-openai, groq, and huggingface, so valid vendors were rejected by TypeScript. Generate the vendor list from @jambonz/schema (agent.schema.json llm.vendor.enum) via scripts/gen-llm-vendors.mjs into a committed llm-vendors.generated.ts. Wired as prebuild/pretypecheck so published artifacts always track the installed schema version. Export LlmVendor and LLM_VENDORS for consumers. Add a drift test asserting the committed generated list matches the schema enum. Co-Authored-By: Claude Opus 4.8 (1M context) --- typescript/package.json | 3 + typescript/scripts/gen-llm-vendors.mjs | 62 +++++++++++++++++++ typescript/src/types/index.ts | 4 ++ typescript/src/types/llm-vendors.generated.ts | 24 +++++++ typescript/src/types/verbs.ts | 11 +--- typescript/test/schema-drift.test.ts | 19 ++++++ 6 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 typescript/scripts/gen-llm-vendors.mjs create mode 100644 typescript/src/types/llm-vendors.generated.ts diff --git a/typescript/package.json b/typescript/package.json index a12fcd9..673ddbb 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -82,10 +82,13 @@ "examples" ], "scripts": { + "gen:types": "node scripts/gen-llm-vendors.mjs", + "prebuild": "npm run gen:types", "build": "tsup", "dev": "tsup --watch", "test": "vitest run", "test:watch": "vitest", + "pretypecheck": "npm run gen:types", "typecheck": "tsc --noEmit", "lint": "eslint src/", "docs": "typedoc", diff --git a/typescript/scripts/gen-llm-vendors.mjs b/typescript/scripts/gen-llm-vendors.mjs new file mode 100644 index 0000000..066df72 --- /dev/null +++ b/typescript/scripts/gen-llm-vendors.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/** + * Code generator: derive the LLM vendor union from the source of truth. + * + * The agent verb's `llm.vendor` is an enum defined once, in + * `@jambonz/schema` (verbs/agent.schema.json). Hand-maintaining a matching + * TypeScript union here drifts every time a vendor is added to the schema. + * Instead we read the enum at build time and emit it as a TS literal union. + * + * Output: src/types/llm-vendors.generated.ts (committed; regenerated on prebuild) + * Run: npm run gen:types + */ +import { createRequire } from 'node:module'; +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Resolve @jambonz/schema's location via its package.json (no `exports` field, +// so subpath file reads are permitted), then read the agent verb schema. +const schemaPkgJson = require.resolve('@jambonz/schema/package.json'); +const schemaRoot = dirname(schemaPkgJson); +const agentSchemaPath = join(schemaRoot, 'verbs', 'agent.schema.json'); + +const agentSchema = JSON.parse(readFileSync(agentSchemaPath, 'utf8')); +const enumValues = agentSchema?.properties?.llm?.properties?.vendor?.enum; + +if (!Array.isArray(enumValues) || enumValues.length === 0) { + console.error( + `[gen-llm-vendors] Could not find llm.vendor.enum in ${agentSchemaPath}` + ); + process.exit(1); +} + +const schemaVersion = JSON.parse(readFileSync(schemaPkgJson, 'utf8')).version; +const members = enumValues.map((v) => ` '${v}',`).join('\n'); + +const out = `// AUTO-GENERATED — DO NOT EDIT BY HAND. +// Source of truth: @jambonz/schema@${schemaVersion} verbs/agent.schema.json (llm.vendor.enum) +// Regenerate with: npm run gen:types +// +// This file derives the LLM vendor list from the JSON schema so the SDK's +// types never drift from the schema when a new vendor is added. + +/** Supported LLM vendors for the agent verb, derived from the schema enum. */ +export const LLM_VENDORS = [ +${members} +] as const; + +/** Union of LLM vendor ids accepted by the agent verb's \`llm.vendor\`. */ +export type LlmVendor = (typeof LLM_VENDORS)[number]; +`; + +const outPath = resolve(__dirname, '..', 'src', 'types', 'llm-vendors.generated.ts'); +mkdirSync(dirname(outPath), { recursive: true }); +writeFileSync(outPath, out); + +console.log( + `[gen-llm-vendors] Wrote ${enumValues.length} vendors from @jambonz/schema@${schemaVersion} -> src/types/llm-vendors.generated.ts` +); diff --git a/typescript/src/types/index.ts b/typescript/src/types/index.ts index fce92ab..4079073 100644 --- a/typescript/src/types/index.ts +++ b/typescript/src/types/index.ts @@ -58,6 +58,10 @@ export type { VerbName, } from './verbs.js'; +// LLM vendors (derived from @jambonz/schema) +export type { LlmVendor } from './llm-vendors.generated.js'; +export { LLM_VENDORS } from './llm-vendors.generated.js'; + // Session export type { AgentEvent, diff --git a/typescript/src/types/llm-vendors.generated.ts b/typescript/src/types/llm-vendors.generated.ts new file mode 100644 index 0000000..60afb0f --- /dev/null +++ b/typescript/src/types/llm-vendors.generated.ts @@ -0,0 +1,24 @@ +// AUTO-GENERATED — DO NOT EDIT BY HAND. +// Source of truth: @jambonz/schema@0.3.8 verbs/agent.schema.json (llm.vendor.enum) +// Regenerate with: npm run gen:types +// +// This file derives the LLM vendor list from the JSON schema so the SDK's +// types never drift from the schema when a new vendor is added. + +/** Supported LLM vendors for the agent verb, derived from the schema enum. */ +export const LLM_VENDORS = [ + 'openai', + 'anthropic', + 'google', + 'vertex-gemini', + 'vertex-openai', + 'bedrock', + 'deepseek', + 'baseten', + 'azure-openai', + 'groq', + 'huggingface', +] as const; + +/** Union of LLM vendor ids accepted by the agent verb's `llm.vendor`. */ +export type LlmVendor = (typeof LLM_VENDORS)[number]; diff --git a/typescript/src/types/verbs.ts b/typescript/src/types/verbs.ts index e09b6dd..4579ccf 100644 --- a/typescript/src/types/verbs.ts +++ b/typescript/src/types/verbs.ts @@ -16,6 +16,7 @@ import type { Target, Vad, } from './components.js'; +import type { LlmVendor } from './llm-vendors.generated.js'; // --------------------------------------------------------------------------- // Audio & Speech @@ -161,14 +162,8 @@ export interface AgentLlmOptions { /** `llm` block on the agent verb. */ export interface AgentLlm { - vendor: - | 'openai' - | 'anthropic' - | 'google' - | 'vertex-gemini' - | 'vertex-openai' - | 'bedrock' - | 'deepseek'; + /** LLM vendor. Derived from the @jambonz/schema agent verb enum — see llm-vendors.generated.ts. */ + vendor: LlmVendor; model: string; label?: string; auth?: { apiKey?: string; [key: string]: unknown }; diff --git a/typescript/test/schema-drift.test.ts b/typescript/test/schema-drift.test.ts index 986ec5a..f2fa2e6 100644 --- a/typescript/test/schema-drift.test.ts +++ b/typescript/test/schema-drift.test.ts @@ -181,4 +181,23 @@ describe('Schema drift detection', () => { }); } }); + + // The agent verb's llm.vendor union is code-generated from the schema enum + // (src/types/llm-vendors.generated.ts, via scripts/gen-llm-vendors.mjs). The + // generated file is committed, so it can go stale if the @jambonz/schema dep is + // bumped without re-running `npm run gen:types`. This guards the committed artifact. + describe('generated LLM vendor list matches schema enum', () => { + it('LLM_VENDORS equals agent.schema.json llm.vendor.enum', async () => { + const agentSchema = JSON.parse( + readFileSync(resolve(verbsDir, 'agent.schema.json'), 'utf-8') + ); + const schemaEnum: string[] = agentSchema.properties.llm.properties.vendor.enum; + const { LLM_VENDORS } = await import('../src/types/llm-vendors.generated.js'); + + expect( + [...LLM_VENDORS], + 'generated LLM_VENDORS is stale — run `npm run gen:types`' + ).toEqual(schemaEnum); + }); + }); });