Skip to content

Inline providers.json schema descriptions and fix schema delivery - #86

Open
sharon-wang wants to merge 5 commits into
mainfrom
inline-providers-schema
Open

Inline providers.json schema descriptions and fix schema delivery#86
sharon-wang wants to merge 5 commits into
mainfrom
inline-providers-schema

Conversation

@sharon-wang

@sharon-wang sharon-wang commented Aug 26, 2026

Copy link
Copy Markdown
Member

addresses posit-dev/positron#15434
pre-req to https://github.com/posit-dev/assistant/pull/2224

Summary

  • Add .describe() calls throughout the providers.json Zod schema so providers.schema.json carries real field descriptions instead of a bare type listing, unblocking a generated providers.json reference for the Assistant docs site.
  • Fix providers.schema.json never actually reaching packaged consumers: it was resolved at runtime via require.resolve, which throws in every bundled build (Positron, standalone) since neither ships node_modules/ai-config. generate-schema now emits the same bytes as an inlined string constant that mutateProvidersConfig writes directly, and the write now runs on every mutation instead of only during file creation.
  • Clean up wording in a few descriptions (drop "Node-backed surfaces" jargon, use "Maximum" instead of "Largest" for token limits).
  • Update CI to check both generated artifacts (providers.schema.json and the new inlined source file) stay in sync with the Zod schema.

Test plan

  • npm run generate-schema -w ai-config and diff against committed output
  • npm run test -w ai-config (includes new schema-descriptions.test.ts and updated load-config.test.ts)
  • npm run check-types -w ai-provider-bridge

The Zod schema had no `.describe()` calls at all — every bit of prose lived
in TSDoc, which Zod cannot see, so `providers.schema.json` shipped as a bare
type listing. Anything generated from it was strictly worse than hand-written
docs, which blocked generating a providers.json reference for the Assistant
docs site.

Descriptions are sourced from the existing TSDoc where it existed, from the
hand-written Provider Settings section of the docs site, and from the bridge's
ModelInfo comments. They avoid the `"` character so downstream consumers can
quote them without escaping.

The coverage test walks the generated JSON Schema and throws on any construct
it does not explicitly handle, so an unsupported shape fails loudly instead of
being skipped into vacuous success. It found eight genuinely undescribed
fields (the capability section objects) that a field-count baseline would have
missed.

Also adds a CI guard: ai-config's `prebuild` regenerates the schema into the
working tree and never diffs it, so a schema.ts change could merge against a
stale artifact with CI green. The guard diffs against HEAD rather than the
working tree, since `build` has already refreshed the file by that point.
Two bugs kept the schema from reaching users, so editors never showed
validation or hover descriptions for providers.json.

First, the schema was resolved at runtime with
`require.resolve("ai-config/providers.schema.json")`. Consumers bundle
ai-config (esbuild inlines it) and ship no `node_modules/ai-config` — the
Positron extension's .vscodeignore excludes node_modules entirely, and
standalone is a single bundled main.js. So that resolve throws in every
packaged build, lands in the catch, logs a warning, and writes nothing. Only
dev checkouts ever got a schema.

Second, the copy ran only inside the race-safe file-creation branch, so even
where it worked it wrote once and never again. An install whose providers.json
predates a schema change stayed pinned to whatever shipped that day.

generate-schema now emits the same bytes as a string constant that
`mutateProvidersConfig` writes directly. Inlining is the only form that
survives bundling. It is stored minified and pretty-printed on write, which
halves what every consumer bundles (440KB to 224KB, measured +1.5% on the
standalone server bundle) and reproduces the .json file byte for byte.

The write runs after the mutation rather than before, so it cannot perturb the
mutation's own read/write sequence, and is skipped when the mutation fails. It
compares content first, so the common path does no disk write.

Note this still only refreshes on a mutation, which fires on migrations,
Snowflake auth, and settings writes — not at every startup. A user who
upgrades and never changes a setting still will not see a refresh; closing
that needs an explicit call from the hosts.
Drop "Node-backed surfaces" jargon from providers.custom and
snowflake.connectionName, and say "Maximum" instead of "Largest" for
maxInputTokens/maxOutputTokens, matching common usage.
@sharon-wang
sharon-wang force-pushed the inline-providers-schema branch from e4f9eb3 to 099be97 Compare August 26, 2026 15:46
It said headers are "sent with every request," but several built-in
providers (copilot, google-vertex, ollama, lmstudio) never look at
customHeaders at all. Since this field is shared across every
provider block, the description needs to hold true everywhere it's
attached, not just for the providers that happen to forward it.
Also fixes a description regression the merge surfaced: origin/main's
gateway reserved-header validation (gatewayCustomHeadersSchema) built
customHeaders from a bare z.record(z.string(), z.string()) for every
built-in provider, dropping the field's description and its per-key/
value descriptions entirely (caught by schema-descriptions.test.ts).
Extracted the described base schema (customHeadersSchema) so the
gateway refinement builds on it instead of a fresh undescribed record.
@wch

wch commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

The changes to the descriptions are a good. There is something that gives me pause here, though.

This change will inline the entire providers.json schema into the ai-config JS file. According to Codex, this will increase the size of the minified JS by about 228KB.

Here's a question that I asked Codex:

When is the schema read and used?

For PR 2224 specifically, the schema is only a build-time input:

  • Read manually when regenerating the committed MDX reference page.
  • Read once by root CI’s --check command on each Assistant PR/push.
  • Not read during normal website builds, development, or by website visitors.

The generated documentation page is committed, so the website never needs the schema at runtime.

Separately, PR 86 uses the embedded schema after each successful providers.json mutation to refresh the local editor sidecar. An editor then reads that sidecar when someone opens the config file.

So if PR 2224 is the main objective, bundling the schema into every application is unnecessary. The descriptions, checked-in JSON schema, and CI synchronization are sufficient. I would separate the runtime sidecar-delivery change into another PR, where its bundle cost and delivery design can be evaluated independently.

Here's another idea: Instead of bundling it into the ai-config JS file, we could publish it somewhere, and then in the providers.json file, point $schema to that URL.

Some ideas of where we could publish immediately:

  • A Github pages site for this repo
  • The Posit Assistant website
  • schemastore.org

And then in the future, if we wanted we could migrate to a unified site for JSON schemas for Posit software. (We could do a conservative migration to a new URL this way: look at a user's providers.json, and if it has the old URL, change it to point to the new URL.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants