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.
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.

1 participant