UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

881 lines (880 loc) • 36.7 kB
/** * zod validator for the single-JSON provider catalog authoring format. * Mirrors src/lib/types/providerCatalog.ts field-for-field with * z.strictObject (unknown keys are authoring mistakes) plus the * cross-field refinements the plain types cannot express. See * docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md. * * provider-catalog.schema.json is a hand-kept JSON Schema mirror for * editor validation only — this file is the authoritative validator. */ import { z } from "zod"; const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/; const KEBAB_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/; const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; const catalogPricingPerMTokSchema = z.strictObject({ input: z.number(), output: z.number(), cachedInput: z.number().optional(), }); const catalogModelStatusSchema = z.enum(["production", "preview", "retired"]); const catalogModelSpecSchema = z.strictObject({ contextWindow: z.number().optional(), maxOutputTokens: z.number().optional(), pricingPerMTok: catalogPricingPerMTokSchema.optional(), vision: z.boolean(), tools: z.boolean().optional(), status: catalogModelStatusSchema, description: z.string(), enumMember: z .string() .regex(IDENTIFIER_PATTERN, "enumMember must be a valid identifier") .optional(), }); function buildWireSchema(requireExactlyOneBaseURL) { return z .strictObject({ baseURL: z.string().optional(), baseURLTemplate: z.string().optional(), extraCredentials: z.array(z.string()).optional(), missingCredentialMessage: z.string().optional(), apiKeyFallbackEnvVars: z.array(z.string()).optional(), envOverrides: z .strictObject({ apiKey: z.string().optional(), baseURL: z.string().optional(), model: z.string().optional(), }) .optional(), }) .superRefine((wire, ctx) => { const hasBaseURL = wire.baseURL !== undefined; const hasTemplate = wire.baseURLTemplate !== undefined; if (requireExactlyOneBaseURL && hasBaseURL === hasTemplate) { ctx.addIssue({ code: "custom", path: ["baseURL"], message: "wire requires exactly one of baseURL or baseURLTemplate", }); } if (wire.extraCredentials !== undefined) { if (wire.extraCredentials.length !== 1) { ctx.addIssue({ code: "custom", path: ["extraCredentials"], message: "wire.extraCredentials must have exactly one entry", }); } else if (!IDENTIFIER_PATTERN.test(wire.extraCredentials[0])) { // Embedded verbatim in generated credential typing — a non-identifier // value would emit invalid TypeScript. ctx.addIssue({ code: "custom", path: ["extraCredentials", 0], message: "wire.extraCredentials entry must be a valid identifier", }); } if (!hasTemplate) { ctx.addIssue({ code: "custom", path: ["extraCredentials"], message: "wire.extraCredentials is only valid with baseURLTemplate", }); } } if (wire.missingCredentialMessage !== undefined && !hasTemplate) { ctx.addIssue({ code: "custom", path: ["missingCredentialMessage"], message: "wire.missingCredentialMessage is only valid with baseURLTemplate", }); } if (hasTemplate && wire.extraCredentials !== undefined && wire.extraCredentials.length === 1) { const placeholder = `{${wire.extraCredentials[0]}}`; if (!wire.baseURLTemplate?.includes(placeholder)) { ctx.addIssue({ code: "custom", path: ["baseURLTemplate"], message: "wire.baseURLTemplate must contain the extraCredentials[0] placeholder", }); } } }); } const catalogWireSchema = buildWireSchema(true); const decideWireSchema = buildWireSchema(false); // Chat providers read these; ConfiguredDecisionProvider does not. Accepting one // would give a descriptor that says "configured" next to a provider that // ignores the setting, so each is refused until the engine reads it. const UNSUPPORTED_DECIDE_WIRE_KEYS = [ "baseURLTemplate", "extraCredentials", "missingCredentialMessage", "apiKeyFallbackEnvVars", ]; const catalogErrorRuleClassSchema = z.enum([ "authentication", "rate-limit", "invalid-model", "network", "provider", ]); const catalogErrorRuleJsonSchema = z .strictObject({ status: z.number().optional(), pattern: z.string().optional(), patternStatuses: z.array(z.number().int()).min(1).optional(), class: catalogErrorRuleClassSchema, message: z.string(), }) .superRefine((rule, ctx) => { if (rule.status === undefined && rule.pattern === undefined) { ctx.addIssue({ code: "custom", path: [], message: "errorRules entry requires status or pattern", }); } if (rule.patternStatuses !== undefined && rule.pattern === undefined) { ctx.addIssue({ code: "custom", path: ["patternStatuses"], message: "errorRules patternStatuses requires pattern", }); } if (rule.pattern !== undefined) { try { new RegExp(rule.pattern, "i"); } catch { ctx.addIssue({ code: "custom", path: ["pattern"], message: "errorRules pattern must compile as a regular expression", }); } } }); const catalogQuirksSchema = z.strictObject({ maxTokensField: z.literal("max_completion_tokens").optional(), omitStreamOptions: z.boolean().optional(), timeoutErrorClass: z.literal("provider").optional(), messageContentFormat: z.literal("string").optional(), registryDefaultIgnoresModelEnvVar: z.boolean().optional(), responseFormatDowngrade: z.literal("json-schema-to-json-object").optional(), replayReasoningContent: z.boolean().optional(), rejectRequiredToolChoice: z.boolean().optional(), authHeaderStyle: z.literal("x-api-key").optional(), }); const catalogTimeoutsSchema = z.strictObject({ generateMs: z.number().optional(), streamMs: z.number().optional(), }); const catalogBillingPolicySchema = z.enum([ "free-tier", "free-with-card", "no-free-tier", "promotional-credit", ]); const catalogSetupSchema = z.strictObject({ url: z.string(), apiKeyFormat: z.string().nullable(), billingPolicy: catalogBillingPolicySchema, instructions: z.array(z.string()), description: z.string().optional(), }); const catalogProbeEvidenceSchema = z.strictObject({ date: z.string().regex(DATE_PATTERN, "date must match YYYY-MM-DD"), status: z.number().optional(), code: z.string().optional(), method: z.string().optional(), }); const catalogEvidenceSchema = z.strictObject({ rosterVerified: catalogProbeEvidenceSchema, authProbe: catalogProbeEvidenceSchema.optional(), billingProbe: catalogProbeEvidenceSchema.optional(), liveMatrix: z .strictObject({ date: z.string().regex(DATE_PATTERN, "date must match YYYY-MM-DD"), result: z.string(), }) .nullable(), addedInPR: z.string(), }); const catalogCapabilitiesSchema = z.strictObject({ text: z.boolean(), streaming: z.boolean(), tools: z.union([z.boolean(), z.literal("model-dependent")]), toolsWithStreaming: z.boolean(), structuredOutput: z.boolean(), structuredOutputWithTools: z.boolean(), embeddings: z.boolean(), thinking: z.boolean(), }); const catalogModelsSchema = z .strictObject({ default: z.string(), fallbacks: z.array(z.string()), fallbackModelName: z.string().optional(), registryDefaultModel: z.string().optional(), // Catalog defaults are real model ceilings, never an unknown/zero // placeholder. Caller maxTokens and optional model overrides are separate // contracts and deliberately keep their existing validation semantics. defaultContextWindow: z .number() .int() .positive() .max(Number.MAX_SAFE_INTEGER), defaultMaxOutputTokens: z .number() .int() .positive() .max(Number.MAX_SAFE_INTEGER), catalog: z.record(z.string(), catalogModelSpecSchema), topModels: z.array(z.string()).min(1).optional(), visionModel: z.string().optional(), // Deliberately NOT constrained to catalog keys: testModel captures what // the testing account can actually reach today, which may be a model // the transcribed catalog predates. testModel: z.string().min(1).optional(), }) .superRefine((models, ctx) => { const catalogKeys = new Set(Object.keys(models.catalog)); if (models.fallbacks.length === 0) { // The loader's fallbackModelName default is fallbacks[1] ?? fallbacks[0], // which depends on fallbacks[0] existing. ctx.addIssue({ code: "custom", path: ["fallbacks"], message: "models.fallbacks must have at least one entry", }); } if (!catalogKeys.has(models.default)) { ctx.addIssue({ code: "custom", path: ["default"], message: "models.default must be a key of models.catalog", }); } models.fallbacks.forEach((fallback, index) => { if (!catalogKeys.has(fallback)) { ctx.addIssue({ code: "custom", path: ["fallbacks", index], message: "models.fallbacks entry must be a key of models.catalog", }); } }); if (models.fallbackModelName !== undefined && !catalogKeys.has(models.fallbackModelName)) { ctx.addIssue({ code: "custom", path: ["fallbackModelName"], message: "models.fallbackModelName must be a key of models.catalog", }); } // A fallback that resolves to the default model cannot fall back. // // `loader.ts` derives it positionally as // `fallbackModelName ?? fallbacks[1] ?? fallbacks[0]`, and nothing checked // the result. A provider with one model has no choice and is left alone; // a provider with alternatives that still lands on its own default has // silently disabled its retry path — the primary fails, the SDK retries // the same model, and it fails again for the same reason. // // This is not hypothetical. `mistral.json` ships 32 models and explicitly // sets `fallbackModelName` to its own default, overriding a perfectly good // `fallbacks[1]`. Two single-model catalogs also resolve this way and are // correctly exempt. const derivedFallback = models.fallbackModelName ?? models.fallbacks[1] ?? models.fallbacks[0]; // Count only entries that could actually serve as a fallback. The whole // `catalog` map also carries retired ids, so its size both over-reports in // the message below and could fire this rule on a catalog whose only other // entries are unusable — the mirror of the single-model exemption. const selectableModels = Object.values(models.catalog).filter((spec) => spec.status !== "retired").length; if (derivedFallback !== undefined && derivedFallback === models.default && selectableModels > 1) { ctx.addIssue({ code: "custom", path: models.fallbackModelName !== undefined ? ["fallbackModelName"] : ["fallbacks"], message: `the fallback model resolves to "${derivedFallback}", which is also models.default — ` + `a fallback identical to the primary cannot fall back. This catalog declares ` + `${selectableModels} selectable models, so name a different one (or drop ` + `fallbackModelName to ` + `let fallbacks[1] apply). Single-model catalogs are exempt.`, }); } if (models.registryDefaultModel !== undefined && !catalogKeys.has(models.registryDefaultModel)) { ctx.addIssue({ code: "custom", path: ["registryDefaultModel"], message: "models.registryDefaultModel must be a key of models.catalog", }); } if (models.topModels !== undefined) { models.topModels.forEach((modelId, index) => { if (!catalogKeys.has(modelId)) { ctx.addIssue({ code: "custom", path: ["topModels", index], message: "models.topModels entry must be a key of models.catalog", }); } }); const seen = new Set(); models.topModels.forEach((modelId, index) => { if (seen.has(modelId)) { ctx.addIssue({ code: "custom", path: ["topModels", index], message: "models.topModels must not contain duplicate entries", }); } seen.add(modelId); }); } if (models.visionModel !== undefined) { const spec = models.catalog[models.visionModel]; if (!spec) { ctx.addIssue({ code: "custom", path: ["visionModel"], message: "models.visionModel must be a key of models.catalog", }); } else if (!spec.vision) { ctx.addIssue({ code: "custom", path: ["visionModel"], message: "models.visionModel entry must have vision: true", }); } } }); const catalogIdentityShape = { $schema: z.string().optional(), id: z.string().regex(KEBAB_ID_PATTERN, "id must be kebab-case"), displayName: z.string(), enumTypeName: z .string() .regex(IDENTIFIER_PATTERN, "enumTypeName must be a valid identifier") .optional(), credentialsKey: z .string() .regex(IDENTIFIER_PATTERN, "credentialsKey must be a valid identifier") .optional(), aliases: z.array(z.string()), }; export const providerCatalogJsonSchema = z.strictObject({ ...catalogIdentityShape, tier: z.literal(2).optional(), wire: catalogWireSchema, models: catalogModelsSchema, capabilities: catalogCapabilitiesSchema, errorRules: z.array(catalogErrorRuleJsonSchema), quirks: catalogQuirksSchema.optional(), timeouts: catalogTimeoutsSchema.optional(), autoSelectPriority: z.number().optional(), setup: catalogSetupSchema, evidence: catalogEvidenceSchema, }); const decideStatusSchema = z.number().int().min(100).max(599); const decidePositiveInt = z.number().int().positive(); const decidePositiveNumber = z.number().positive(); const decideErrorKindSchema = z.enum([ "authentication", "invalid_request", "max_tokens_exceeded", "rate_limit", "overloaded", "server", "timeout", "network", ]); // The most steps one `{n}` or `{n,m}` may stand for. const MAX_PATTERN_COUNT = 255; // The most ways a pattern's counts may multiply: each `?` is 2 and each `{n,m}` // is m-n+1. Adjacent bounded counts multiply on a miss (`.{0,255}` three times // took 38 s on one 2,000-character message), and a miss on a 5xx is retried. // XOR's pattern is 3,362. const MAX_PATTERN_WAYS = 4096; // What can make a pattern backtrack catastrophically on an error message, so // that a catalog author cannot write it: `*`, `+` and an open-ended `{n,}` have // no upper bound; any quantifier on a group (`(a|a){0,30}`, `(ab)?`) repeats // whatever its alternatives match, and quantifiers nested that way multiply; a // count above MAX_PATTERN_COUNT is as good as no bound. A quantifier on a plain // character, class or escape is fine. Inside a character class all of them are // plain characters, and a backslash makes the next one plain everywhere. // Counts that sit side by side multiply, so their product has a budget too // (MAX_PATTERN_WAYS); within it a pattern is still not a guarantee of linear // time, and wants reading. function hasRiskyQuantifier(pattern) { const count = /\{(\d+)(?:,(\d*))?\}/y; let inClass = false; let afterGroup = false; let ways = 1; for (let index = 0; index < pattern.length; index += 1) { const char = pattern[index]; const onGroup = afterGroup; afterGroup = false; if (char === "\\") { index += 1; } else if (inClass) { inClass = char !== "]"; } else if (char === "[") { inClass = true; } else if (char === ")") { afterGroup = true; } else if (char === "*" || char === "+" || (char === "?" && onGroup)) { return true; } else if (char === "?") { // The `?` that opens a group (`(?:`, `(?=`) is not a quantifier. if (pattern[index - 1] !== "(") { ways *= 2; } } else if (char === "{") { count.lastIndex = index; const found = count.exec(pattern); if (found && (onGroup || found[2] === "" || Math.max(Number(found[1]), Number(found[2] ?? 0)) > MAX_PATTERN_COUNT)) { return true; } if (found && found[2] !== undefined) { ways *= Number(found[2]) - Number(found[1]) + 1; } } if (ways > MAX_PATTERN_WAYS) { return true; } } return false; } function compilesAsPattern(pattern) { try { new RegExp(pattern, "i"); return true; } catch { return false; } } const decideMessagePatternSchema = z .string() .min(1) .max(200) .refine(compilesAsPattern, { message: "messagePattern is not a valid regular expression", }) .refine((pattern) => !hasRiskyQuantifier(pattern), { message: "messagePattern must be bounded: an unescaped * or +, an open-ended {n,}, a {n,m} count above 255, or any quantifier on a group, outside a character class, can backtrack catastrophically on an error message. Put ?, or {n,m} up to 255, only on a plain character, a character class or an escape, and keep the counts side by side to 4096 ways in all (each ? is 2, each {n,m} is m-n+1)", }); const decideErrorRuleSchema = z.strictObject({ status: z .union([decideStatusSchema, z.array(decideStatusSchema).min(1)]) .optional(), statusMin: decideStatusSchema.optional(), messagePattern: decideMessagePatternSchema.optional(), kind: decideErrorKindSchema, retryable: z.boolean().optional(), }); const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]); const HTTP_TOKEN_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; const decidePathSchema = z .string() .refine((path) => /^[^.\s]+(?:\.[^.\s]+)*$/.test(path) && path.split(".").every((segment) => !UNSAFE_PATH_SEGMENTS.has(segment)), { message: "path must be dot-separated segments without whitespace, and must not use __proto__, constructor or prototype", }); const decideHeaderNameSchema = z .string() .regex(HTTP_TOKEN_PATTERN, "header name must be an HTTP token"); const decideEndpointSchema = z .string() .refine((endpoint) => endpoint.trim().length > 0 && !/[{}]/.test(endpoint.split("{baseURL}").join("")), { message: "endpoint must not be blank and may only use the {baseURL} placeholder", }); const decideBodyShapeSchema = z .strictObject({ path: decidePathSchema, isArray: z.boolean().optional(), formatter: z.enum(["fastapi-validation"]).optional(), kind: decideErrorKindSchema.optional(), retryable: z.boolean().optional(), }) .refine((shape) => (shape.formatter !== undefined) === (shape.isArray === true), { path: ["formatter"], message: "a body shape sets isArray: true together with a formatter, or neither: an array without a formatter never matches, and a formatter on a body that is not an array reads the text as an empty list", }); // The keys the body already uses, and the names an object must not take as an // own key from data. const RESERVED_MEDIA_KEYS = new Set([ "model", "state", "questions", "__proto__", "constructor", "prototype", ]); const decideMediaKeySchema = z .string() .regex(/^[A-Za-z][A-Za-z0-9_]*$/, "a media key must start with a letter and use only letters, digits and underscores") .refine((key) => !RESERVED_MEDIA_KEYS.has(key), { message: "a media key must not be model, state, questions, __proto__, constructor or prototype", }); const decideMediaSchema = z .strictObject({ imagesKey: decideMediaKeySchema, videoKey: decideMediaKeySchema.optional(), }) .refine((media) => media.imagesKey !== media.videoKey, { path: ["videoKey"], message: "decide.media.videoKey must differ from imagesKey: one body key cannot carry both", }); const GUARD_LIST_MAX = 8; const ambientGuardEnvNameSchema = z .string() .max(64) .regex(/^[A-Z][A-Z0-9_]*$/, "an environment variable name must be upper-case letters, digits and underscores, and start with a letter"); // Lower-case scheme, host labels and an optional port, nothing else: the // origin is compared after the same parsing a base URL gets, so a spelling // that URL would normalise (a trailing slash, a path, an upper-case host) is // refused here instead of being quietly accepted. const AMBIENT_GUARD_ORIGIN_PATTERN = /^https?:\/\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::[0-9]{1,5})?$/; function parsesAsUrl(value) { try { new URL(value); return true; } catch { return false; } } const ambientGuardOriginSchema = z .string() .max(100) .regex(AMBIENT_GUARD_ORIGIN_PATTERN, "a trusted origin must be an http(s) origin: a lower-case scheme, host and optional port, with no path, query, fragment or userinfo") .refine(parsesAsUrl, { message: "a trusted origin must parse as a URL (a valid host, port 0 to 65535)", }); const ambientGuardCredentialKeySchema = z .string() .max(64) .regex(/^[a-zA-Z][a-zA-Z0-9]*$/, "a credentials key must be letters and digits, and start with a letter"); const ambientKeyGuardSchema = z.strictObject({ baseURLEnvVars: z.array(ambientGuardEnvNameSchema).min(1).max(GUARD_LIST_MAX), trustedOrigins: z.array(ambientGuardOriginSchema).min(1).max(GUARD_LIST_MAX), credentialBaseURLs: z .array(z.strictObject({ key: ambientGuardCredentialKeySchema, field: z.literal("baseURL"), })) .min(1) .max(GUARD_LIST_MAX) .optional(), }); const decideBlockSchema = z.strictObject({ dialect: z.enum(["system-one", "openai-decisions"]), order: decidePositiveInt, baseURLRequired: z.boolean().optional(), baseURLNormalize: z .array(z.enum(["strip-trailing-slash", "strip-v1-suffix"])) .optional(), baseURLShape: z.enum(["origin"]).optional(), endpoint: decideEndpointSchema, model: z.strictObject({ placement: z.enum(["body", "body-unless-alias"]), alias: z.string().min(1).optional(), }), media: decideMediaSchema.optional(), ambientKeyGuard: ambientKeyGuardSchema.optional(), requestIdHeaders: z.array(decideHeaderNameSchema).optional(), response: z .strictObject({ modelPaths: z.array(decidePathSchema).optional() }) .optional(), errors: z.strictObject({ rules: z.array(decideErrorRuleSchema), default: decideErrorKindSchema, bodyShapes: z.array(decideBodyShapeSchema), }), }); // A window of 0 or less would reach every reader of the derived model manifest, // and a model without its own window inherits the default's through `??`, // which keeps a 0. Chat entries keep the looser catalogModelSpecSchema. const decideModelSpecSchema = catalogModelSpecSchema.extend({ contextWindow: decidePositiveInt.optional(), maxOutputTokens: decidePositiveInt.optional(), }); // Here and not in the entry-level refinement below: that one does not run once // a sibling field fails a type check, and an author who deletes the TODO lines // of a scaffold should see these gaps beside the other errors. const decideModelsSchema = z .strictObject({ default: z.string(), catalog: z.record(z.string(), decideModelSpecSchema), topModels: z.array(z.string()).min(1).optional(), testModel: z.string().min(1).optional(), }) .superRefine((models, ctx) => { // The "must be a key" rule reports a default that is not an own key. if (!Object.hasOwn(models.catalog, models.default)) { return; } const base = models.catalog[models.default]; for (const field of ["contextWindow", "maxOutputTokens"]) { if (base[field] === undefined) { ctx.addIssue({ code: "custom", path: ["catalog", models.default, field], message: `${field} is required on the default model; the derived model manifest reads it`, }); } } }); // `media` is the allowance the base class prepares and gates a request's images // and video against; `decide.media` names the body keys they go out under, and // the entry refinement below requires the two together. const decideLimitsSchema = z.strictObject({ maxStateTokens: decidePositiveInt, maxQuestions: decidePositiveInt.optional(), nonAsciiTokensPerChar: decidePositiveNumber.optional(), digitTokensPerChar: decidePositiveNumber.optional(), symbolTokensPerChar: decidePositiveNumber.optional(), astralTokensPerChar: decidePositiveNumber.optional(), models: z .record(z.string(), z.strictObject({ maxStateTokens: decidePositiveInt, nonAsciiTokensPerChar: decidePositiveNumber.optional(), })) .optional(), media: z .strictObject({ maxImages: decidePositiveInt, video: z.boolean(), maxRequestBytes: decidePositiveInt, }) .optional(), advisory: z.boolean().optional(), }); const decideCatalogJsonSchema = z .strictObject({ ...catalogIdentityShape, inferenceKinds: z.tuple([z.literal("decide")]), tier: z.literal(2).optional(), wire: decideWireSchema, models: decideModelsSchema, timeouts: z .strictObject({ decideMs: decidePositiveInt.optional() }) .optional(), decisionLimits: decideLimitsSchema.optional(), decide: decideBlockSchema, setup: catalogSetupSchema, evidence: catalogEvidenceSchema, }) .superRefine((entry, ctx) => { const { wire, decide } = entry; const issue = (path, message) => { ctx.addIssue({ code: "custom", path, message }); }; // Two rules collide only on the same status AND the same pattern: XOR // lists a pattern rule and a plain rule at one statusMin. const seenStatus = new Set(); const seenStatusMin = new Set(); decide.errors.rules.forEach((rule, index) => { const at = ["decide", "errors", "rules", index]; if ((rule.status === undefined) === (rule.statusMin === undefined)) { issue(at, "errors.rules entry needs exactly one of status or statusMin"); } const statuses = typeof rule.status === "number" ? [rule.status] : (rule.status ?? []); const sameAs = rule.messagePattern === undefined ? "" : " with the same messagePattern"; const keyOf = (value) => JSON.stringify([value, rule.messagePattern ?? null]); for (const status of statuses) { if (seenStatus.has(keyOf(status))) { issue([...at, "status"], `errors.rules lists status ${status} more than once${sameAs}`); } seenStatus.add(keyOf(status)); } if (rule.statusMin !== undefined) { if (seenStatusMin.has(keyOf(rule.statusMin))) { issue([...at, "statusMin"], `errors.rules lists statusMin ${rule.statusMin} more than once${sameAs}`); } seenStatusMin.add(keyOf(rule.statusMin)); } }); const modelKeys = new Set(Object.keys(entry.models.catalog)); if (!modelKeys.has(entry.models.default)) { issue(["models", "default"], "models.default must be a key of models.catalog"); } const seenTopModels = new Set(); (entry.models.topModels ?? []).forEach((id, index) => { if (!modelKeys.has(id)) { issue(["models", "topModels", index], "models.topModels entry must be a key of models.catalog"); } if (seenTopModels.has(id)) { issue(["models", "topModels", index], "models.topModels must not contain duplicate entries"); } seenTopModels.add(id); }); const { alias, placement } = decide.model; if (placement === "body-unless-alias" && alias === undefined) { issue(["decide", "model", "alias"], "decide.model.alias is required when placement is body-unless-alias"); } if (placement === "body" && alias !== undefined) { issue(["decide", "model", "alias"], "decide.model.alias is only valid with placement body-unless-alias"); } if (alias !== undefined && !modelKeys.has(alias)) { issue(["decide", "model", "alias"], "decide.model.alias must be a key of models.catalog"); } for (const id of Object.keys(entry.decisionLimits?.models ?? {})) { if (!modelKeys.has(id)) { issue(["decisionLimits", "models", id], "decisionLimits.models key must be a key of models.catalog"); } } const mediaKeys = decide.media; const mediaAllowance = entry.decisionLimits?.media; if (decide.dialect === "openai-decisions") { // This dialect writes images into the request input itself, so it has no // body keys to name, and the input has no video part. The allowance // alone is how many images a request may carry. if (mediaKeys !== undefined) { issue(["decide", "media"], "decide.media is not valid with dialect openai-decisions: that dialect places images in the request input itself and has no body keys for them"); } if (mediaAllowance?.video === true) { issue(["decisionLimits", "media", "video"], "decisionLimits.media.video must be false with dialect openai-decisions: its request input has no video part"); } if (placement !== "body") { issue(["decide", "model", "placement"], "decide.model.placement must be body with dialect openai-decisions: the model is always a field of the request"); } } else { if (mediaKeys !== undefined && mediaAllowance === undefined) { issue(["decide", "media"], "decide.media needs decisionLimits.media: without an allowance the base class refuses every image and video, so these keys would never be written"); } if (mediaKeys === undefined && mediaAllowance !== undefined) { issue(["decisionLimits", "media"], "decisionLimits.media needs decide.media: without body keys the engine would prepare a request's media and then leave it out of the body"); } if (mediaKeys !== undefined && mediaAllowance !== undefined) { if (mediaAllowance.video && mediaKeys.videoKey === undefined) { issue(["decide", "media", "videoKey"], "decide.media.videoKey is required when decisionLimits.media.video is true"); } if (!mediaAllowance.video && mediaKeys.videoKey !== undefined) { issue(["decide", "media", "videoKey"], "decide.media.videoKey must be omitted when decisionLimits.media.video is false"); } } } if (decide.baseURLRequired === true && !decide.endpoint.includes("{baseURL}")) { issue(["decide", "endpoint"], "decide.endpoint must contain {baseURL} when decide.baseURLRequired is true"); } const seenShapes = new Set(); decide.errors.bodyShapes.forEach((shape, index) => { const key = `${shape.path}|${shape.isArray === true}`; if (seenShapes.has(key)) { issue(["decide", "errors", "bodyShapes", index], "errors.bodyShapes lists the same path and isArray twice"); } seenShapes.add(key); }); const seenHeaders = new Set(); (decide.requestIdHeaders ?? []).forEach((header, index) => { if (seenHeaders.has(header.toLowerCase())) { issue(["decide", "requestIdHeaders", index], "requestIdHeaders lists the same header twice; header names are case-insensitive"); } seenHeaders.add(header.toLowerCase()); }); const guard = decide.ambientKeyGuard; if (guard !== undefined) { const ownBaseURLVar = wire.envOverrides?.baseURL ?? `${entry.id.toUpperCase().replace(/-/g, "_")}_BASE_URL`; const lists = [ ["baseURLEnvVars", guard.baseURLEnvVars, "variable"], ["trustedOrigins", guard.trustedOrigins, "origin"], ]; for (const [field, values, noun] of lists) { if (new Set(values).size !== values.length) { issue(["decide", "ambientKeyGuard", field], `lists the same ${noun} twice`); } } const credentialFields = guard.credentialBaseURLs ?? []; if (new Set(credentialFields.map((c) => `${c.key}.${c.field}`)).size !== credentialFields.length) { issue(["decide", "ambientKeyGuard", "credentialBaseURLs"], "lists the same credentials field twice"); } if (guard.baseURLEnvVars.includes(ownBaseURLVar)) { issue(["decide", "ambientKeyGuard", "baseURLEnvVars"], `must not list the provider's own base-URL variable ${ownBaseURLVar}: setting it already exempts the key`); } } for (const key of UNSUPPORTED_DECIDE_WIRE_KEYS) { if (wire[key] !== undefined) { issue(["wire", key], `wire.${key} is not supported on decide entries yet; ConfiguredDecisionProvider does not read it`); } } const hasBaseURL = wire.baseURL !== undefined; const hasTemplate = wire.baseURLTemplate !== undefined; if (decide.baseURLRequired === true) { if (hasBaseURL || hasTemplate) { ctx.addIssue({ code: "custom", path: ["wire", hasBaseURL ? "baseURL" : "baseURLTemplate"], message: "wire must not set baseURL or baseURLTemplate when decide.baseURLRequired is true; the caller supplies the base URL", }); } } else if (hasBaseURL === hasTemplate) { ctx.addIssue({ code: "custom", path: ["wire", "baseURL"], message: "wire requires exactly one of baseURL or baseURLTemplate unless decide.baseURLRequired is true", }); } }); function describeIssues(error) { return error.issues .map((issue) => `${issue.path.join(".")}: ${issue.message}`) .join("; "); } function carriesInferenceKinds(raw) { return (typeof raw === "object" && raw !== null && raw.inferenceKinds !== undefined); } export function parseCatalogEntry(raw, sourcePath = "<in-memory entry>") { if (!carriesInferenceKinds(raw)) { return parseProviderCatalogJson(raw, sourcePath); } const result = decideCatalogJsonSchema.safeParse(raw); if (!result.success) { throw new Error(`Invalid decide catalog file ${sourcePath}: ${describeIssues(result.error)}`); } return result.data; } export function isDecideCatalogEntry(entry) { return "inferenceKinds" in entry; } export function parseProviderCatalogJson(raw, sourcePath) { if (carriesInferenceKinds(raw)) { throw new Error(`Invalid provider catalog file ${sourcePath}: inferenceKinds is set, so this is a decide entry; parse it with parseCatalogEntry`); } const result = providerCatalogJsonSchema.safeParse(raw); if (!result.success) { throw new Error(`Invalid provider catalog file ${sourcePath}: ${result.error.issues .map((i) => `${i.path.join(".")}: ${i.message}`) .join("; ")}`); } return result.data; }