Skip to content

feat(agent-setup): configure VS Code - #438

Draft
Menci wants to merge 50 commits into
zed-agent-setupfrom
vscode-agent-setup
Draft

feat(agent-setup): configure VS Code#438
Menci wants to merge 50 commits into
zed-agent-setupfrom
vscode-agent-setup

Conversation

@Menci

@Menci Menci commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #437 — targets zed-agent-setup and stays draft until that lands.

Adds VS Code as a fourth Agent Setup target.

Why customendpoint

VS Code reaches Floway through the bundled Copilot extension's customendpoint
vendor — the only non-deprecated arbitrary-URL provider that ships in stable.
customoai is deprecated and carries when: productQualityType != 'stable',
so it is invisible in a release build; ollama is deprecated in favour of a
marketplace extension.

Its group takes an API path — chat-completions, responses, or messages
and Floway serves all three for every model, so that is one group-wide
preference rather than a per-model derivation.

Why models are enumerated, not discovered

customendpoint reads only id off a /models response and drops every model
it cannot type: it passes no known-models table and overrides no capability
resolver, so the base class's if (!modelCapabilities) { continue; } discards
all of them.

Worse, setting a group-level url is actively harmful — it short-circuits into
that discovery branch and suppresses the explicit models[] list, leaving the
provider empty. The installer never writes one.

Why the key rides in requestHeaders

The group's own apiKey property is declared secret, so VS Code runs its
${input:...} decoder over whatever it finds there. A literal has no :, so
decodeSecretKey slices it into a secret-storage miss and the provider sends an
empty Authorization header. requestHeaders passes through verbatim, and this
vendor deliberately un-reserves authorization "for endpoints behind APIM,
gateways, vanity domains".

The trade is that the document carries the credential rather than the keychain,
so it is written owner-only.

Shape

Every installed build (Code, Insiders, VSCodium) and every profile within it is
configured in one pass — the active profile is not discoverable from outside the
editor, and a named profile's directory is an opaque hash rather than its display
name.

Two shell notes. The Bash loop reads its profile list from a file rather than a
pipeline, since a while reading from a pipe runs in a subshell where a failure
could neither stop the loop nor reach the caller. The PowerShell writer restores
the array brackets by hand when ConvertTo-Json unwraps a lone group, because
-AsArray does not exist on the Windows PowerShell 5.1 baseline.

Unlike Zed, no JSONC problem arises: VS Code rewrites this whole file itself
whenever Manage Models changes anything, so comments in it are already volatile.

Persisted configuration

The strict schema parses stored rows as well as request bodies, so adding a
required vscode key needs migration 0079 to backfill it — without one, the
next acquire 500s for every existing owner, and permanently: the parse happens
before any replacement row can be written and the latest-record lookup ignores
expiry. This class has now occurred twice, so a guard seeds the configuration
shape 0060 stored when it created the table, runs every migration over it, and
parses the result. Deleting 0079 fails it naming vscode; deleting 0078
fails it naming zed.

Verification

134 passed, 0 failed in the real installer harness, seventeen of them VS Code.
Several run through PowerShell and assert the same group as Bash — the jq
program and the PowerShell projection are two implementations of one mapping.

Writing them caught two defects: an override path that skipped the existence
check and crashed against a missing directory instead of reporting it, and a
catalog fixture that never exercised discrete effort levels.

Review then caught three more that would have broken real installs, all in the
PowerShell half and all confirmed by reverting the fix and watching a test fail:
File.Replace was passed $null, which PowerShell binds as String.Empty and
the API rejects, so every Windows re-run failed; the root shape was read from
the decoded value, which cannot tell [] from an object; and limits used
PowerShell truthiness, so a catalog value of 0 silently became a fallback.

The first of those shipped unexecuted because the atomic-replacement branch is
Windows-only and no test dropped its platform conjunct. That rewrite is now
keyed per agent and throws when a guard stops matching, and both editors have a
test through it.

Full repo: 5489 tests, typecheck, and lint all green.

Notes

  • The tab icon is the VSCodium mark from simple-icons under CC0. Neither icon set
    carries a Microsoft-branded one, and the tab configures all three builds, so
    the unbranded shape is the more accurate label as well as the licensed one.

@Menci
Menci force-pushed the vscode-agent-setup branch 2 times, most recently from deb32e6 to 6c82bae Compare August 8, 2026 11:21
VS Code reaches Floway through the bundled Copilot extension's `customendpoint`
vendor, the only non-deprecated arbitrary-URL provider that ships in stable.
Its group takes one of three API paths, and Floway serves all three for every
model, so that is a group-wide preference rather than a per-model derivation.

Models are enumerated rather than discovered, for the same reason Zed's are:
`customendpoint` reads only `id` off a `/models` response and drops every model
it cannot type, and a group-level `url` is worse than useless — it is not even a
declared property, yet it short-circuits into that discovery branch and
suppresses the explicit list, leaving the provider empty. So the gateway
projects the catalog and embeds it, and the installer keeps only the merge.

The projection lives beside Zed's in one module, which is what keeps the two
halves honest: the same entries reach jq and PowerShell, and the dashboard
preview imports them too. Only the endpoint and the credential are attached
client-side — the gateway does not render its own origin, and the key already
appears once in the script. A pasted snippet has no merge to attach them, so
the preview builder does it instead.

The key rides in `requestHeaders` rather than the group's `apiKey`: that
property is declared `secret`, so VS Code runs its `${input:...}` decoder over a
literal and lands on a secret-storage miss. `requestHeaders` survives the header
sanitizer because this vendor un-reserves `authorization` for endpoints behind
gateways. The trade is that the document carries the credential, so it is
written owner-only.

Every installed build and every profile within it is configured in one pass,
because the active profile is not discoverable from outside the editor. Each
profile is its own transaction and a failure does not stop the others, so one
hand-edited file cannot leave the rest unconfigured; the run still exits
non-zero. Migration 0079 backfills the new configuration key, without which the
strict schema would lock every existing owner out of Agent Setup.
@Menci
Menci force-pushed the vscode-agent-setup branch from ae56137 to d6d7bff Compare August 8, 2026 16:29
Menci added 26 commits August 9, 2026 00:55
# Conflicts:
#	packages/agent-setup/scripts/test-installers.ts
The projection omits the per-model endpoint and the credential because a setup
run's merge attaches them — and a pasted snippet has no merge, so the dashboard
attached them separately. Two implementations of one rule is the shape of
divergence the server-side projection was built to remove, and nothing measured
them against each other: changing the snippet to emit a wrong URL and a
capital-A `Authorization` — which `customendpoint` does not un-reserve, so the
sanitizer drops it and every request 401s — left all 602 web tests and all 26
VS Code installer tests green.

`addressVSCodeModels` now states it once, beside the projection. The dashboard
builds its snippet from it, and both installers are asserted against it for all
three API paths: what a run writes must equal what the operator pastes. The
capital-A mutation fails that assertion.
# Conflicts:
#	packages/agent-setup/src/script-assets.generated.ts
ConvertTo-Json emits a subtree deeper than its limit as the literal string
"@{k=}" with only a warning, and the staged check inspects our own group alone
— so a sibling gateway's group nested deeply enough was replaced by a string
under exit 0. Same promotion the Zed half just took.

The other half of that pass does not carry over. This document is rewritten
wholesale by VS Code itself whenever Manage Models changes anything, so its
comments are already volatile and there is nothing to refuse; and it carries
the API key, so it is owner-only by design rather than inheriting whatever mode
the operator had.
# Conflicts:
#	apps/web/src/components/api-keys/agent-setup-card.tsx
#	apps/web/src/i18n/locales/en.ts
#	apps/web/src/i18n/locales/zh-Hans.ts
… damage

VS Code reads chatLanguageModels.json with its JSONC-tolerant scanner, so a
comment is content the editor accepts. jq refuses such a document, PowerShell 7
accepted it and dropped the comment on the way out — data loss reported as
success — and 5.1 errors, three outcomes for one file. Both halves refuse it
now, through the same shared helpers the Zed half uses, and the Bash message
names the comment instead of blaming the list shape.

The Bash gate also accepted a stream of JSON documents, so `[…][…]` passed and
was merged once per document. Slurping asks for exactly one.

`grep` was missing from the harness's hermetic tool list, so the Bash comment
check silently never ran — the same gap `stat` had. The suite now links it, and
reverting either half's refusal fails the new case.
# Conflicts:
#	apps/web/src/components/api-keys/agent-setup-card.tsx
#	packages/agent-setup/src/script-assets.generated.ts
The Bash half matched a line-leading `//`, so a block comment or a trailing one
fell through to the jq gate and was refused as a malformed provider list — the
wrong stated cause, which is exactly what naming the comment was meant to fix.
It now uses the shared string-aware scan the Zed half took, so both editors and
both languages answer this the same way, and the test carries a block comment
and a trailing one rather than only the form the old pattern happened to catch.
… the run

`Set-SetupAgent` treated only a `setup-handled` throw as one profile's failure
and re-threw anything else, so a backup that could not be written — a read-only
directory, a locked file, a serializer that refused — killed the whole run with
a raw .NET message and no summary. Reproduced: with an unwritable default
profile beside a writable named one, Bash reported the backup failure and still
configured the second profile; PowerShell configured neither. That contradicts
the comment directly above it, and the Bash half has never behaved that way —
there, every write failure is a counted profile.

Any exception is now this profile's failure, named where Stop-Setup has not
already named it, which is what makes the contract structural rather than a
list of anticipated throws. The backup failure also says what it was, matching
Bash's wording. A test asserts both halves keep going, and gating on
'setup-handled' again fails it.

Also aligned two smaller divergences: the Bash half now prints the user
directory it is configuring, as both Zed halves do, and the two refusal
messages for the same document read the same.

The JSONC comments in both VS Code halves stated the wrong reason. VS Code
rewrites this file whole through `model.setValue(JSON.stringify(…))` on its own
next edit, so a comment here — unlike one in Zed's document, which Zed never
writes — is not the operator's to keep. The refusal exists because jq has no
JSONC mode and the two halves must give one answer, not because anything is
being preserved.
The previous round moved the per-profile write inside a transaction but left
the enumeration outside it: `Get-SetupVSCodeProfileDirs` ran in the `foreach`
header, so an unreadable `profiles/` threw past both loops. Reproduced — Bash
configured the default profile and carried on, PowerShell wrote nothing at all
and printed a raw access-denied, which for a stale root-owned `profiles` under
Code also skipped Insiders and VSCodium. The enumeration now reports and
returns the default profile, which is what the Bash glob does when it cannot
read that directory: the named profiles are lost, not the build.

Three `Test-Path` calls accepted a file where Bash asks `[ -d ]`, so a `User`
path or a `profiles` entry that is a regular file was walked into rather than
skipped. `zed.ps1` already asked for a container.

Three instrumentation gaps behind them. The harness's Windows-replacement
rewrite had reverted to a `.replace` chain covering only Claude and Codex when
this branch was rebuilt, so neither editor's `File.Replace` ran off-Windows —
replacing that line with a throw left the suite green. It is keyed per agent
again and throws when a guard stops matching. `PowerShell writes the same
provider group as Bash` never ran Bash; it now runs both halves and diffs the
documents, as its Zed twin already did. And a comment paragraph was separated
from the test it explains for the second round running, this time by the VS
Code catalog test.

Two smaller ones: the gateway's `listModels` returned an empty catalog when the
key or its owner vanished mid-request, reporting "advertises no chat models"
for what is a deleted key — it throws now, so the listing-failure script says
something went wrong. And the README still listed three agents.
# Conflicts:
#	packages/agent-setup/scripts/test-installers.ts
`contextWindow` is the whole window and `maxInputTokens` the prompt budget —
two numbers VS Code reconciles itself, deriving the second from the first when
it is absent. The projection filled the window from whichever limit it found
and never emitted the prompt one, so a model stating both had its prompt budget
computed as window minus output instead of taken from what it announced. Both
are emitted now, and a model announcing only one keeps what it announced.

Two divergences from the Bash half, and a shared cause behind them:

`-ceq` against an array is a filter, and a non-empty result is truthy, so a
group whose `vendor` is `["customendpoint"]` matched ours and was deleted.
jq keeps it, because a non-string is not equal to a string. The same question
was asked a second time when validating the staged document — with the type
check missing there too, which is how a first repair of the filter alone turned
a silent deletion into a staging failure. It is one predicate now, asked in
both places.

A `profiles/` directory the run cannot enter yields nothing from the Bash glob,
which read as "no named profiles" and reported success; PowerShell already
warned. Both say so now.

Instrumentation: the VS Code PowerShell script route had no test at all, the
lease test named for configuring "every agent" stopped at Zed, and the
directory-vs-file test asserted only a non-zero exit — which the broken version
also produces, via a raw .NET exception instead of the reason Bash gives.
… to prompt

VS Code reserves the output budget out of the window and gives the prompt
whatever remains, so an output fallback larger than the window leaves a prompt
budget of zero — the model shows up in the picker and every request is over
budget before it starts. Ollama states a context length and no output limit at
all, so an 8k model hit exactly that: `contextWindow 8192, maxOutputTokens 8192`
resolved to `maxInputTokens: 0`. The fallback is bounded by a quarter of the
window now; a stated 0 still survives verbatim, since only the fallback arm
changed. Zed does not degenerate on the same row, so this was VS Code-only.

The `maxInputTokens` emission added last round was observed by nothing —
deleting it left the package, installer and web suites all green, because no
fixture stated both a window and a prompt limit and the projection had no VS
Code tests at all. There is now a VS Code block mirroring the Zed one, written
against what VS Code computes rather than what we emit, plus a catalog row
stating all three limits so the installer round-trip carries them. Removing
either fix fails these.
# Conflicts:
#	apps/web/src/components/api-keys/agent-setup-models.ts
#	packages/agent-setup/scripts/test-installers.ts
The shared `[1m]` reconstruction reached only the Zed projection: the edit that
was meant to apply it to `projectVSCodeModels` was written on a branch where
that function does not exist, so it silently matched nothing — the same
"rewrite that quietly stops matching" this harness now guards against
elsewhere. Verified by projecting a merged 1M row and reading the id back.

VS Code emits the addressed id now, with a unit test on both projections and
the installer expectations updated to the id a run actually writes.

Two comments stated capabilities the pinned upstream contradicts. The `messages`
default was justified as "the only one carrying thinking budgets", but the
Messages path builds a thinking config only from capabilities `customendpoint`
never forwards, so no apiType can carry one — the real reason is the experiment
flag, which is now what it says. And `maxOutputTokens` was described as merely
clamped to the window; on the Messages path it is also sent as the wire
`max_tokens`, making it a hard output cap.
# Conflicts:
#	apps/web/src/components/api-keys/agent-setup-models.ts
#	packages/agent-setup/scripts/test-installers.ts
The revert reached the Zed half through the merge but left
`projectVSCodeModels` calling a function that no longer exists, so the package
suite went red on the VS Code block. The id it sends is the id the gateway can
resolve, and its unit test now says so rather than asserting the suffix.
A mutation sweep over `models.ts` found four guards no test could see: the Zed
context-window fallback and its `max_output_tokens` presence check, VS Code's
`maxInputTokens` presence check, and `toolCalling`. Swapping `??` for `||`,
`!== undefined` for truthiness, or `true` for `false` left the whole package
suite green — including two guards added specifically by the rounds that
introduced them.

The stated-zero test could not fail on its own subject either: it paired a zero
output limit with a zero window, so the fallback arm produced 0 as well. Its
window is non-zero now.

Each is written against the property rather than a value — a stated 0 and an
absent limit must reach the editor differently — and each of the four mutations
now goes red.
Menci added 23 commits August 9, 2026 19:44
# Conflicts:
#	packages/agent-setup/src/script-assets.generated.ts
# Conflicts:
#	packages/agent-setup/scripts/test-installers.ts
#	packages/agent-setup/src/script-assets.generated.ts
…replacing it

The VS Code half carried the same defect the Zed half just lost: chezmoi
and stow place a symlink where the editor expects `chatLanguageModels
.json`, and renaming the staged document onto that path replaced the
link, so the operator's dotfile stopped being what VS Code reads.
Resolve the managed path through the shared helper, as Zed now does.

Restricting the file is now stated once per half, before the key is
written rather than after. The PowerShell half re-restricted the renamed
document, which cannot fail on its own — the rename carries the stage's
mode — and only served to hide a stage that had not been restricted; the
Bash half restricted the stage after jq had already written the key into
it, leaving a window the umask, not this code, was closing.

Verified by mutation: removing the path resolution on either half, or
the stage's restriction on the PowerShell half, turns the suite red. The
Bash half's `chmod 600` stays unobservable because `umask 077` produces
the same mode independently — two guarantees of one property, which the
comment now says rather than implying test coverage it cannot have.
# Conflicts:
#	packages/agent-setup/__tests__/routes_test.ts
#	packages/agent-setup/src/script-assets.generated.ts
`contextWindow` was filled from `max_prompt_tokens` whenever no window
was stated, and VS Code derives the prompt budget as `contextWindow -
maxOutputTokens` — so a catalog stating a prompt limit and no window
reached the editor short by the whole reservation. The shape every Codex
model has, 120k stated, resolves to 111 808. Reachable in production
from provider-custom and from any Copilot row reporting a prompt limit
without a window.

This is the defect the Zed projection already fixed by adding the
reservation back; the VS Code half kept the raw substitution, against
this file's own note that the two fields are different numbers. The
window and the reservation are now decided together, because VS Code
reconciles them against each other and neither can be chosen alone.

The quarter-window cap on the output fallback now applies to a stated
window only. That cap exists because a reservation larger than the
window leaves no room to prompt, which can only happen when both come
out of one fixed total; where the window is rebuilt from a prompt limit
the reservation sits on top of it and crowds out nothing, so scaling it
down there would invent a ceiling the catalog never implied.

Verified by mutation: dropping the reservation from the reconstructed
window, or the cap from the stated-window branch, each turns the suite
red.
… observed

Four guards shipped with nothing watching them:

- `-WarningAction Stop` on ConvertTo-Json. Past `-Depth` the serializer
  emits what it could not reach as the literal "@{k=}" and only warns, so
  a sibling gateway's deeply nested group would be flattened into that
  string and renamed into place — someone else's provider destroyed
  quietly. A 120-level foreign group now reaches it.
- The `name -is [string]` half of the own-group test. `-ceq` against an
  array is true when any element matches, so without it PowerShell would
  delete a group jq keeps; only the `vendor` half had a case.
- The backup's mode on the PowerShell half.
- The empty-catalog refusal on the PowerShell half, which was Bash-only.

The staged-list checks stay unobserved because nothing reachable fails
them: an empty catalog is refused long before, and every malformed
document is refused before the backup. They are assertions on the merge,
not gates on input, and both halves now say so rather than implying a
coverage they cannot have. The Bash backup is restricted explicitly to
match the PowerShell half, though `umask 077` reaches the same mode
independently and no test can separate the two.

`an unreadable profiles directory does not stop the other builds` had one
user directory, never checked the exit, and duplicated the warning test's
setup; its comment described a per-profile enclosure neither half has. It
is folded into the warning test, which now runs a blocked build ahead of
a healthy one and observes what the name claims. The test override takes
several directories to allow that — the real enumeration yields several,
so an override that could name only one could not stand in for it.

Also drops a comment asserting that an empty effort list makes a picker
in VS Code — it returns early on a zero-length list — and tells the
operator following the pasted-snippet path that the file holds the key
in plain text, which only the script path was restricting.
`-ceq` is case-sensitive but still culture-aware, so it reported a name
carrying a soft hyphen, a zero-width space, or an NFD accent as equal to
ours. jq compares code points and says otherwise — and the disagreement
is one half deleting a `customendpoint` group the other half keeps. A
name pasted with a stray invisible, or typed where the keyboard composes
accents, is the way in; the staged assertion cannot catch it, because the
foreign group is removed rather than duplicated.

`[string]::Equals` with Ordinal answers what jq answers. The `-is
[string]` tests stay: overload resolution converts a single-element array
to its element's string form before comparing, so `["customendpoint"]`
would match without them.

Also fixes three tests that promised more than they observed. The
empty-catalog case now asserts the early gate's own message — the staged
assertion downstream refuses the same input and rolls back, so exit code
and byte-identity alone could not tell which one fired, and only the
early one names a cause. The unreadable-`profiles` case asserts the exit
is still 0, which is what "costs that build alone" means. And the
suffix-free case says why its row has a 1M window: that is what a suffix
rule keys on, so it is what keeps one from coming back.

Verified by mutation: reverting to `-ceq`, dropping either type test, or
removing the early empty-catalog gate each turns the suite red.
# Conflicts:
#	packages/agent-setup/src/script-assets.generated.ts
…t too

VS Code parses this document with `allowTrailingComma`, so a comma before
a closing bracket is syntax the editor accepts — and jq refuses it while
ConvertFrom-Json takes it and writes the file back without it. One
operator blocked, another silently rewritten, from the same document.
The comment gate already existed here for exactly this reason; the comma
case reaches it through the shared scanner now, and the refusal names
JSONC syntax rather than comments alone.

Verified by mutation: disabling the gate on either half turns the suite
red.
# Conflicts:
#	packages/agent-setup/src/script-assets.generated.ts
# Conflicts:
#	packages/agent-setup/src/script-assets.generated.ts
… versions

Same root cause as the Zed half, in three places: the embedded model
list, the operator's existing list, and the staged document were each
read with `@(… | ConvertFrom-Json)`, which is a flat array on pwsh 7 and
a one-element array holding the real one on Windows PowerShell 5.1.
ConvertTo-Json then writes the inner collection as
`{"value":[…],"Count":n}`, so VS Code reads an object where its schema
requires a list and the whole group fails to load.

The staged check compares the model count instead of asking whether the
list is non-empty, for the same reason it does on the Zed half: a nested
list has a count of 1 and passed an emptiness check, which is how this
could reach a written document.
`vscodeTokenPlan` bounded the reservation only where the catalog stated
none. A stated one went out whole, and VS Code gives the prompt whatever
the window leaves after it — so a row stating `max_output_tokens:
128_000` and no window took our 128k fallback as the window, reserved all
of it, and registered with a prompt budget of zero. Present in the
picker, over budget before the first request. Every limit is
independently optional from an upstream catalog and from operator model
config, so a row stating only an output limit is ordinary.

This is the failure the quarter-window cap was added to remove, closed
for one arm of the same function. A stated reservation is now kept to
half the window: past that the prompt gets the smaller share of a budget
it is supposed to dominate, and a model that truly answers with 128k
tokens would come with a window that says so rather than with our
fallback. The prompt-limit branch keeps neither bound — there the
reservation sits on top of the window and crowds out nothing.

The small-context test now walks every projected row asserting a
non-zero prompt budget, rather than checking the two Ollama shapes alone.

Separately, the empty-list shortcut no longer spells whitespace as `\s`,
which in .NET matches U+00A0 and the rest of Unicode's. VS Code's own
scanner does treat those as trivia — so `[ ]` really is an empty
list to the editor and to ConvertFrom-Json while jq refuses it, and the
halves answer differently. Both outcomes are safe and closing the gap
would mean rewriting the operator's whitespace before jq sees it; the
comment says so rather than implying the shortcut settles it.
# Conflicts:
#	packages/agent-setup/installers/powershell/common/json-document.ps1
#	packages/agent-setup/src/script-assets.generated.ts
`ConvertTo-Json -InputObject @(…)` keeps the brackets on a one-element
array on both Windows PowerShell 5.1 and pwsh 7 — it is the pipeline form
that unwraps a lone object. The hand-restored brackets after it could
never run, and the test named for the round-trip could not have failed on
either version. The serialization goes through `-InputObject` for exactly
this reason, and the comment now says that rather than describing a
5.1 unwrap that does not happen there.

`-WarningAction Stop` promotes nothing on 5.1, which emits no warning for
an over-deep object at all; what refuses such a document there is
ConvertFrom-Json's own recursion limit, before the merge runs. jq has no
ceiling, so the Bash half keeps a deeply nested foreign group whole. The
halves differ in how deep one may be, not in whether it survives, and the
comment says so.

The staged check's comment no longer claims the Bash half asserts the
same two properties: it asserts the same shape, and the count is exact
here because a list nested one level deep is a PowerShell failure mode jq
has no equivalent of.

The sentinel comparisons use the ordinal helper the group identity
already moved to, rather than the culture-aware `-ceq`/`-cne` beside it.
The owner-only test runs through both halves, which is what observes the
PowerShell half's explicit restriction — the Bash half's `umask 077`
reaches that mode on its own and no assertion there can separate them.
…d its prompt branch

`vscode.ps1` was the one half `Get-Content -Raw` was never removed from,
so on a stock Windows box it decoded the operator's provider list through
the system ANSI code page and wrote it back as UTF-8 — permanent mojibake
in a sibling gateway's group name or any localized string. Worse at the
staged read: group identity is compared ordinally, so a non-ASCII
provider name would not match itself after that decode and every profile
would fail validation. Both sites go through the shared reader now, and
the JSONC gate became the shared verdict, so a single-quoted string or an
unquoted key is refused here rather than accepted by Newtonsoft and
written back canonicalized.

`vscodeTokenPlan`'s prompt branch left the reservation unbounded on the
strength of it sitting on top of the window — true only when the catalog
states no window. When all three are stated the reservation comes out of
the stated window, and an output limit at or over it left VS Code with a
prompt budget of zero. It is bounded against that window now, exactly as
the branch below it already was.

Also drops the empty-list shortcut, which was dead once the array reader
started telling `[]` from `[null]` — measured 0 and 1 on both versions —
and corrects the staged assertion's message, which reported an empty list
for what is a count mismatch. The claim that every Codex model states a
prompt limit and no window is false: Codex states a window. That shape
comes from an OpenAI-compatible upstream or an operator override.
`ConvertFrom-Json` on PowerShell 7 flattens a one-element nested array
during the decode, so `[[{…}]]` arrived as `[{…}]`, passed the
element-type check, and the run rewrote the operator's document with the
nesting silently gone — where Windows PowerShell 5.1 and jq both refuse
it. `-NoEnumerate`, which exists on 6+, makes the two versions return the
same structure for every shape measured: a nested array, an empty-array
element, a two-object array, a one-object array, `[null]`, `[]`,
`[[null]]` and `[1,2]`.

The element check is indexed rather than piped as well. That is not what
fixes it — the reader is — but asking the same question without a second
dependence on how a pipeline treats a collection element is what keeps
the next edit from reintroducing it.

Both projections now read their limits through one filter. A zero,
negative or fractional limit is no bound at either editor's wire: VS
Code's fields are required numbers too, a zero window registers a model
that can never be prompted, and on the Messages path the output limit is
sent as the wire `max_tokens`, which the upstream rejects at zero. Two
tests asserted the old behavior and now assert what the editor can act
on. The prompt branch's unstated-output arm is bounded against the window
when the catalog states one, which is where the reservation comes from.

The refusal table carries the sentence each case must produce, rather
than accepting either — the point of naming a cause is lost if any name
will do.
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