fix(providers): restore the runtime-agnostic exports the browser barrels dropped - #795
Merged
sroussey merged 3 commits intoAug 15, 2026
Conversation
…els dropped Five providers keep a hand-maintained `src/ai/index.browser.ts` beside `src/ai/index.ts`. Four of them omitted modules that carry no platform-specific code, so a `customConditions: ["browser"]` consumer could not import them at all. The omissions were invisible until the exports-map fix in #717 routed browser consumers to the browser declarations; they now read as `TS2305`. That they are accidental is evidenced by the bundles themselves: every omitted module is ALREADY compiled into that provider's browser bundle via the runtime entry (e.g. `registerOpenAi`, which IS in the browser barrel, imports `registerOpenAiImageValidator`), and no `src` tree of the five contains a single `node:` import. Only the `export *` line was missing. Restored, per provider: - deepseek: `assertNotTruncatedByReasoning`, `DEEPSEEK_DEFAULT_REASONING_ALLOWANCE`, `resolveMaxTokens` (`DeepSeek_Client`); `DeepSeek_ModelSearch_Stream` (`DeepSeek_ModelSearch`); `DeepSeekToolChoiceNotHonoredError`, `assertToolChoiceHonored`, `isForcingToolChoice` (`DeepSeek_ToolCalling`) - openai: `registerOpenAiImageValidator` (`OpenAI_ImageValidation`); `OpenAI_ModelSearch_Stream` (`OpenAI_ModelSearch`) - xai: `Xai_ModelSearch_Stream` (`Xai_ModelSearch`) - openrouter: `openRouterWorkerRunFnSpecs`, `deriveCapabilitiesFromMeta`, `inferOpenRouterCapabilities`, `OPENROUTER_RUN_FN_SPECS` (`OpenRouter_Capabilities`); `OpenRouterRawModel`, `OPENROUTER_FALLBACK_MODELS`, `fetchOpenRouterModels`, `mapOpenRouterModels`, `OpenRouter_ModelSearch_Stream` (`OpenRouter_ModelSearch`) `DeepSeek_ToolCalling` keeps the NAMED form the node barrel uses, which is what holds `DeepSeek_ToolCalling_Stream` out of the main-thread barrel on both platforms. ollama is deliberately unchanged: its `Ollama_ModelSearch` is exported by NEITHER barrel, so the two agree, and `_testOnly` is its only delta. `_testOnly` stays node-only everywhere — it is `@internal`, for `@workglow/test` alone, and belongs behind a `./test` entry the way `packages/ai` already did it. Co-Authored-By: Claude <noreply@anthropic.com>
…where no split exists `providers/llamacpp-server` and `providers/stable-diffusion-server` each carried a `src/ai.browser.ts` byte-identical to `src/ai.ts` and a `src/ai-runtime.browser.ts` byte-identical to `src/ai-runtime.ts`, all four naming RELATIVE specifiers (`./ai/index`, `./ai/runtime`). A relative specifier is resolved once, by the importing file's own path, and nothing in this toolchain substitutes `X.browser.ts` for `X.ts` on one: `--target=browser` changes the compile target, not the resolver, and a manifest's `browser` field applies to bare specifiers. So both entries already pulled in the same module graph — the declaration split was nominal, two `.d.ts` files kept equal only by hand. Each `.browser.ts` now re-exports its node peer, which cannot drift. These packages keep their `browser` condition: `--target=browser` produces a genuinely different bundle, so the entry earns its keep even though the source graph is shared. Bundle output is unchanged, byte for byte, for all four entries. The nine `packages/workglow/src/*.browser.ts` shims look like the same shape and are deliberately left alone — they re-export a BARE specifier (`@workglow/openai/ai`), which is re-resolved under the consumer's own conditions at every hop, so the two identical files land on different modules and their being identical IS the mechanism. Co-Authored-By: Claude <noreply@anthropic.com>
…parity
Three guards, all source-only (no build, works under `use-source`):
- `duplicateBrowserEntryViolations` (ExportTypesPairing.test.ts) reports
a `src/*.browser.ts` identical to the `src/<stem>.ts` beside it whose
specifiers are ALL relative. The relative/bare distinction is the whole
rule: a relative specifier resolves once, so both entries are the same
module graph and the split is nominal; a bare one is re-resolved under
the consumer's conditions at every hop, so two identical files land on
different modules — which is why the `packages/workglow` shims are
correctly identical and must never be reported. Both branches carry a
fixture, since no violation survives in the tree.
- `ExportBarrelParity.test.ts` parses the top-level re-exports of each
provider's `src/ai/index.ts` and `src/ai/index.browser.ts` and asserts
`node \ browser` equals a pinned `INTENTIONAL_NODE_ONLY` fixture
(`_testOnly` for each of the five providers), with a staleness check so
a pin that stops describing a real difference fails. The parser is
regex-based, so an unclassifiable statement is REPORTED rather than
skipped — the same call `buildEntryViolations` makes for an underivable
dist stem.
- `findBrowserBlock` replaces a top-level `?.browser` lookup in
`browserSplitViolations`, which disagreed with its own recursing
sibling `nodeImportTarget`: a `{ import: { browser: {…}, default: … } }`
resolved its node target through the recursion while the browser block
sat one level down, invisible to every rule keyed on it. Fixture added
for the no-implementation case reached that way.
Co-Authored-By: Claude <noreply@anthropic.com>
sroussey
merged commit Aug 15, 2026
6efb8a2
into
claude/wonderful-turing-rjtcnx-ai-types
10 of 11 checks passed
sroussey
added a commit
that referenced
this pull request
Aug 16, 2026
…els dropped (#795) * fix(providers): restore the runtime-agnostic exports the browser barrels dropped Five providers keep a hand-maintained `src/ai/index.browser.ts` beside `src/ai/index.ts`. Four of them omitted modules that carry no platform-specific code, so a `customConditions: ["browser"]` consumer could not import them at all. The omissions were invisible until the exports-map fix in #717 routed browser consumers to the browser declarations; they now read as `TS2305`. That they are accidental is evidenced by the bundles themselves: every omitted module is ALREADY compiled into that provider's browser bundle via the runtime entry (e.g. `registerOpenAi`, which IS in the browser barrel, imports `registerOpenAiImageValidator`), and no `src` tree of the five contains a single `node:` import. Only the `export *` line was missing. Restored, per provider: - deepseek: `assertNotTruncatedByReasoning`, `DEEPSEEK_DEFAULT_REASONING_ALLOWANCE`, `resolveMaxTokens` (`DeepSeek_Client`); `DeepSeek_ModelSearch_Stream` (`DeepSeek_ModelSearch`); `DeepSeekToolChoiceNotHonoredError`, `assertToolChoiceHonored`, `isForcingToolChoice` (`DeepSeek_ToolCalling`) - openai: `registerOpenAiImageValidator` (`OpenAI_ImageValidation`); `OpenAI_ModelSearch_Stream` (`OpenAI_ModelSearch`) - xai: `Xai_ModelSearch_Stream` (`Xai_ModelSearch`) - openrouter: `openRouterWorkerRunFnSpecs`, `deriveCapabilitiesFromMeta`, `inferOpenRouterCapabilities`, `OPENROUTER_RUN_FN_SPECS` (`OpenRouter_Capabilities`); `OpenRouterRawModel`, `OPENROUTER_FALLBACK_MODELS`, `fetchOpenRouterModels`, `mapOpenRouterModels`, `OpenRouter_ModelSearch_Stream` (`OpenRouter_ModelSearch`) `DeepSeek_ToolCalling` keeps the NAMED form the node barrel uses, which is what holds `DeepSeek_ToolCalling_Stream` out of the main-thread barrel on both platforms. ollama is deliberately unchanged: its `Ollama_ModelSearch` is exported by NEITHER barrel, so the two agree, and `_testOnly` is its only delta. `_testOnly` stays node-only everywhere — it is `@internal`, for `@workglow/test` alone, and belongs behind a `./test` entry the way `packages/ai` already did it. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(providers): re-export the node entry from the browser entry where no split exists `providers/llamacpp-server` and `providers/stable-diffusion-server` each carried a `src/ai.browser.ts` byte-identical to `src/ai.ts` and a `src/ai-runtime.browser.ts` byte-identical to `src/ai-runtime.ts`, all four naming RELATIVE specifiers (`./ai/index`, `./ai/runtime`). A relative specifier is resolved once, by the importing file's own path, and nothing in this toolchain substitutes `X.browser.ts` for `X.ts` on one: `--target=browser` changes the compile target, not the resolver, and a manifest's `browser` field applies to bare specifiers. So both entries already pulled in the same module graph — the declaration split was nominal, two `.d.ts` files kept equal only by hand. Each `.browser.ts` now re-exports its node peer, which cannot drift. These packages keep their `browser` condition: `--target=browser` produces a genuinely different bundle, so the entry earns its keep even though the source graph is shared. Bundle output is unchanged, byte for byte, for all four entries. The nine `packages/workglow/src/*.browser.ts` shims look like the same shape and are deliberately left alone — they re-export a BARE specifier (`@workglow/openai/ai`), which is re-resolved under the consumer's own conditions at every hop, so the two identical files land on different modules and their being identical IS the mechanism. Co-Authored-By: Claude <noreply@anthropic.com> * test(exports): flag a duplicate browser entry and pin browser barrel parity Three guards, all source-only (no build, works under `use-source`): - `duplicateBrowserEntryViolations` (ExportTypesPairing.test.ts) reports a `src/*.browser.ts` identical to the `src/<stem>.ts` beside it whose specifiers are ALL relative. The relative/bare distinction is the whole rule: a relative specifier resolves once, so both entries are the same module graph and the split is nominal; a bare one is re-resolved under the consumer's conditions at every hop, so two identical files land on different modules — which is why the `packages/workglow` shims are correctly identical and must never be reported. Both branches carry a fixture, since no violation survives in the tree. - `ExportBarrelParity.test.ts` parses the top-level re-exports of each provider's `src/ai/index.ts` and `src/ai/index.browser.ts` and asserts `node \ browser` equals a pinned `INTENTIONAL_NODE_ONLY` fixture (`_testOnly` for each of the five providers), with a staleness check so a pin that stops describing a real difference fails. The parser is regex-based, so an unclassifiable statement is REPORTED rather than skipped — the same call `buildEntryViolations` makes for an underivable dist stem. - `findBrowserBlock` replaces a top-level `?.browser` lookup in `browserSplitViolations`, which disagreed with its own recursing sibling `nodeImportTarget`: a `{ import: { browser: {…}, default: … } }` resolved its node target through the recursion while the browser block sat one level down, invisible to every rule keyed on it. Fixture added for the no-implementation case reached that way. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
sroussey
deleted the
claude/optimistic-goldberg-onotd9-browser-barrel-parity
branch
August 24, 2026 18:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #717 (
claude/wonderful-turing-rjtcnx-ai-types), not onmain.#717's 24 manifest edits are correct and are not touched here. What they do is turn an
invisible gap into a hard error: once a
customConditions: ["browser"]consumer istype-checked against the browser declarations, every symbol a browser barrel forgot to
re-export becomes
TS2305. Four providers have such omissions. This PR closes them, removesthe one class of nominal browser entry that has no split at all, and adds the guards that
would have caught both.
Why this is stacked on #717 rather than a follow-up
Shipping them apart means one released version in which
assertToolChoiceHonored,DeepSeek_ModelSearch_Stream,registerOpenAiImageValidator,OpenAI_ModelSearch_Stream,Xai_ModelSearch_Streamand OpenRouter's whole capability/model-search surface areuncompilable in the browser with no substitute — #717 makes the routing correct, and the
barrels are what the correct routing then points at. In-repo consumers already break:
packages/test/src/test/ai-provider-api/DeepSeek_ToolCalling.test.ts:9-11,OpenAI_ImageValidation.test.ts:8, andai-provider/provider-model-search.test.ts:10-16import these symbols from
@workglow/<p>/ai.M1(b) — the omitted exports, per provider
The evidence these omissions are accidental is stronger than "no
node:imports" (also true —grep 'from "node:"' over all fivesrctrees returns zero): **every omitted module is already compiled into that provider's browser bundle via the runtime entry.** E.g.providers/openai/src/ai/registerOpenAi.ts:10importsregisterOpenAiImageValidator, andregisterOpenAiIS inindex.browser.ts— so it is indist/ai.browser.jstoday and only itsexport *` was missing.common/DeepSeek_Client(widened from 2 names to the node barrel's 5)assertNotTruncatedByReasoning,DEEPSEEK_DEFAULT_REASONING_ALLOWANCE,resolveMaxTokens(plus the 2 already there)common/DeepSeek_ModelSearchDeepSeek_ModelSearch_Streamcommon/DeepSeek_ToolCalling(named form)DeepSeekToolChoiceNotHonoredError,assertToolChoiceHonored,isForcingToolChoicecommon/OpenAI_ImageValidationregisterOpenAiImageValidatorcommon/OpenAI_ModelSearchOpenAI_ModelSearch_Streamcommon/Xai_ModelSearchXai_ModelSearch_Streamcommon/OpenRouter_CapabilitiesOPENROUTER_RUN_FN_SPECS,openRouterWorkerRunFnSpecs,deriveCapabilitiesFromMeta,inferOpenRouterCapabilitiescommon/OpenRouter_ModelSearchOpenRouterRawModel,OPENROUTER_FALLBACK_MODELS,fetchOpenRouterModels,mapOpenRouterModels,OpenRouter_ModelSearch_StreamDeepSeek_ToolCallingkeeps the named form the node barrel uses rather than aexport *.That is what holds
DeepSeek_ToolCalling_Streamout of the main-thread barrel on bothplatforms — matching
index.tsexactly._testOnlyis deliberately not added anywhere. It is@internal, exists for@workglow/testalone, and belongs behind a./testentry the waypackages/aialready didit. After this PR it is the only symbol a browser consumer loses.
Two claims from the original review, refuted — please don't "fix" these
ollama has no gap. The "every
*_ModelSearchis missing from the browser barrel" claimdoes not hold here:
providers/ollama/src/ai/common/Ollama_ModelSearch.tsexists but isexported by neither barrel (it is consumed internally by
Ollama_JobRunFns/Ollama_JobRunFns.browser, which registerOllama_ModelSearch_Streamas a run-fn). The twobarrels already agree; ollama's only delta is
_testOnly. No change made.providers/ollama/src/ai-runtimeis also untouched — itsruntime.tsvsruntime.browser.tssplit is genuine (import("ollama")vsimport("ollama/browser")),cactus-class.
The nine
packages/workglowshims are correctly identical. They containexport * from "@workglow/openai/ai"— a bare specifier, re-resolved under theconsumer's conditions at every hop, so the split really happens one layer down in the
provider's exports map. The two files being identical is the mechanism. Collapsing the
browser shim onto
export * from "./openai"would resolve the bare specifier once, underthe shim's own condition, and destroy the split. Left alone, and the new guard has a
dedicated negative fixture documenting this.
M2(i) — the four genuinely nominal browser entries
providers/llamacpp-serverandproviders/stable-diffusion-servereach had asrc/ai.browser.tsbyte-identical tosrc/ai.tsand asrc/ai-runtime.browser.tsbyte-identical to
src/ai-runtime.ts, all four naming relative specifiers(
./ai/index,./ai/runtime). A relative specifier is resolved once, by the importing file'sown path —
--target=browserchanges the compile target, not the resolver, and a manifest'sbrowserfield applies to bare specifiers only. So both entries already pulled in the samegraph and the declaration split was nominal: two
.d.tsfiles kept equal by hand.Each
.browser.tsnow re-exports its node peer. Cycle safety was checked: undermoduleResolution: "bundler"a file (./ai.ts) beats a directory (./ai/index.ts), bunagrees,
ai.tsre-exports./ai/indexand never./ai.browser, and nothing in the toolchain(
examples/web/vite.config.ts,bunfig.toml, bun's--target=browser) substitutesX.browser.tsforX.tson a relative specifier. Each file keeps a one-line comment saying afuture
.browser.tsresolver plugin would break this.The
browsercondition is kept for both packages —--target=browserstill produces agenuinely different bundle, so the entry earns its keep. Confirmed by build: all four bundles
are byte-identical before and after (see table below).
Bundle-size cost — measured, and the plan's prediction was wrong in an interesting way
bun run build:packagesat the base commit vs. after,dist/ai.browser.js:Corrections to what the plan predicted:
grep -o 'import("[^"]*")'findsimport("openai")in thebaseline
ai.browser.jsof all four providers, reached throughregisterDeepSeek/registerOpenAi/registerXai/registerOpenRouter.getClientis not pulled in bythis change — it was already in every browser bundle.
ls dist/*.jsis unchanged:
ai.js,ai.browser.js,ai-runtime.js,ai-runtime.browser.jsand nothingelse). Under
--packages=externaltheawait import("openai")stays a literal dynamicimport of a bare specifier, which the downstream bundler defers — so the SDK never
contributes eager weight either way.
rebuilding openai with only
OpenAI_ImageValidationadded: 10,300 → 10,331 (+31 bytes,effectively the plan's "literal zero"); adding
OpenAI_ModelSearchon top accounts for theremaining +2,610.
fetch, no SDK)".It adds +2,572 raw / +639 gzip — its own capability-inference and model-mapping code,
including the
OPENROUTER_FALLBACK_MODELSliteral. Correct, but not zero.Worst case is deepseek at +1.5 KB gzipped, for symbols a browser consumer currently cannot
import at all.
M2(ii) + M1(b)'s regression net
duplicateBrowserEntryViolations(ExportTypesPairing.test.ts, besidebrowserSplitViolations): reports asrc/*.browser.tsidentical to itssrc/<stem>.tspeer whose specifiers are all relative. The relative/bare distinction is the entire rule
and is documented in full in the doc comment, including why the
packages/workglowshimsare correctly identical. No violation survives after M2(i), so both branches carry fixtures:
a positive pair (
export * from "./ai/index"on both → one violation), a negative pair(
export * from "@workglow/openai/ai"on both → zero violations, commented with therefutation), a differing-graph pair, and one proving a comment cannot silence the rule.
packages/test/src/test/util/ExportBarrelParity.test.ts(new): parses each provider'ssrc/ai/index.ts+src/ai/index.browser.tsinto a surface set and assertsnode \ browserequals a pinnedINTENTIONAL_NODE_ONLYfixture (each of the five providers→
["_testOnly"]), with a comment explaining why_testOnlyis node-only and pointing atthe
./testmigration. Includes a staleness check (a pin that no longer describes a realdifference fails), mirroring
ALLOWED_MISMATCHES. Source-only — no build, works underuse-source. The parser is regex-based, so an unclassifiable statement is reported, notskipped, matching how
buildEntryViolationsreports an underivable dist stem.LOW,
findBrowserBlock:browserSplitViolationsdid a top-level?.browserlookupwhile its sibling
nodeImportTargetrecurses, so a nested{ import: { browser: {…}, default: … } }was invisible — every rule keyed on the branchbehaved as if no
browsercondition existed. Replaced with a walker; fixture added for theno-implementation case reached that way.
SKIPPED, as planned: hardening
buildEntryViolations' regex against glob/variable-drivenentry lists. Fixing it properly means executing the build, and it is already fenced by
GLOB_BUILT_PACKAGES.Verification
All 10 failures are live OpenAI integration tests
(
OpenAI_Generic.integration.test.ts,OpenAI_ImageGeneration.integration.test.ts,OpenAI_UsageAccounting.integration.test.ts) failing on billing, unrelated to this change:The three consumer tests this PR unblocks pass:
Lint (the repo has no root
lintscript; each package carries one) — clean, no output, onevery changed package:
Build:
The guards actually guard
Deleting the one added
Xai_ModelSearchline:Restoring
providers/llamacpp-server/src/ai.browser.tsto its duplicated form:Both restored afterwards.
tsgo probe — #717's own methodology, reproduced
A throwaway file (not committed) importing the two symbols, compiled twice under
moduleResolution: "bundler", once withcustomConditions: ["browser"]and once without:Before M1(b) (deepseek's browser barrel reverted, rebuilt):
After M1(b):
No
tsgo-spawning vitest test was added — it needs a builtdist, is slow, and has noprecedent in the repo.
Release metadata
The repo uses bunset, not changesets (there is no
.changeset/), andCHANGELOG.mdfilesare generated from conventional-commit subjects — none were hand-edited. Once M1(b) lands, the
only symbols leaving the browser type surface are
_testOnly(internal) and ollama's genuineplatform split, so a patch bump is honest and the normal
bunset --patch --allpathapplies. The three commit subjects are the changelog entries:
fix(providers): restore the runtime-agnostic exports the browser barrels droppedrefactor(providers): re-export the node entry from the browser entry where no split existstest(exports): flag a duplicate browser entry and pin browser barrel paritySuggested description fix for #717
Not edited by this PR — for the author to apply:
./worker"; there are 9packages/workglowshims (
anthropic,deepseek,google-gemini,hf-inference,hf-transformers,ollama,openai,openrouter,xai)../workerwas already correct onmainand isuntouched by fix(providers): point each export condition's types at its own declaration #717.
ExportTypesPairing.test.tsas merged carries 8 testsin its
workspace exports mapssuite (finds branches, pairs types, source entries, typesordering, browser split, condition ordering, build entries, glob exemption) plus the
fixture suites.
customConditions: ["browser"]consumer losesonly
_testOnlyrelative to the node surface.Generated by Claude Code