Skip to content

fix(rig): exclude test cassettes from the published crate - #2350

Merged
gold-silver-copper merged 1 commit into
mainfrom
chore/exclude-cassettes-from-facade-package
Aug 16, 2026
Merged

fix(rig): exclude test cassettes from the published crate#2350
gold-silver-copper merged 1 commit into
mainfrom
chore/exclude-cassettes-from-facade-package

Conversation

@gold-silver-copper

Copy link
Copy Markdown
Contributor

Description

The root manifest is the rig facade package, so every path sitting beside it gets swept into the published tarball. The facade's own src/ is ~12 KiB of re-exports; the other 99.9% of what we ship to crates.io is repo furniture, dominated by tests/cassettes/ (1,100+ provider replay fixtures).

That has been quietly compounding:

version .crate size
0.39.0 6.66 MiB
0.40.0 7.76 MiB
0.41.0 7.87 MiB
current main 9.51 MiB

crates.io enforces a 10 MiB upload limit, so main today has 517 KiB of headroom after a cycle that added ~1.5 MiB. The next release was on track to be rejected outright.

The part that makes this worth fixing pre-emptively rather than reactively: the limit is enforced server-side. cargo publish --dry-run reports success right up until the real upload returns an error. And because a release publishes 22 crates in dependency order, a failure partway through leaves the workspace half-published on crates.io with versions that cannot be reused.

Adding exclude = ["tests/cassettes/**"] to the root [package]:

  • tarball 9.51 MiB → 3.33 MiB (below where we were at 0.39.0, 6.67 MiB of headroom restored)
  • file count 1,704 → 589
  • every cargo add rig downloads 6.17 MiB less

This is safe because the cassettes are data the test binaries read at runtime from CARGO_MANIFEST_DIR (tests/common/cassette_safety.rs:11). They are never embedded with include_str!/include_bytes!, so nothing in the package needs them to compile. exclude governs packaging only, so repo-local and CI test runs are completely unaffected.

There is prior art in-repo: crates/rig-core/Cargo.toml already uses exclude for tests/macro_hygiene.rs.

Type of change

  • Bug fix

Testing

Verified in a throwaway worktree, no registry token required:

  • cargo publish -p rig --dry-runexit 0, verification build compiles the packaged tarball with the cassettes absent, zero errors and no new warnings (notably no warning: ignoring test ..., which would indicate the exclude had orphaned a test target).
  • cargo package -p rig --list | grep -c tests/cassettes/0, confirming the glob actually matches.
  • Before/after measured on the same tree: 9,968,275 B → 3,494,698 B.

Baseline for the numbers above came from a full cargo publish --workspace --dry-run simulation of the pending release PR (#2221), which verified all 22 crates green — so this is the only thing standing between the workspace and a clean 0.42.0.

Checklist:

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

Notes

Scoped deliberately to the cassettes, since they are 6.17 MiB of the 6.6 MiB of excludable bulk and fix the problem on their own. Worth a follow-up conversation, but not included here:

  • tests/providers/ (~0.48 MiB) and the root tests/*.rs still ship. They now ship without their fixtures, so a downstream consumer who runs cargo test on the unpacked crate (some distro packagers do) would see failures rather than the passes they'd have seen before. Excluding tests/** outright would be the more complete fix; I did not want to widen the blast radius of a release-unblocking change without a maintainer opinion.
  • Nothing here prevents the next category of bulk from creeping in. A CI guard asserting the packaged size stays under some threshold would turn this from a fix into a fence.

The root manifest is the `rig` facade, so every path beside it is swept into
the published tarball. That included `tests/cassettes/`, 1,100+ provider replay
fixtures, which had grown the crate to 9.51 MiB against the crates.io 10 MiB
upload limit. That left 517 KiB of headroom on a trend that added 1.5 MiB in
the last release cycle, so the next release was on track to be rejected.

The limit is enforced server-side, so `cargo publish --dry-run` reports success
right up until the real upload fails. A 22-crate release that dies partway
through leaves the workspace half-published, and the burned versions cannot be
reused.

Excluding the cassettes takes the tarball from 9.51 MiB to 3.33 MiB, and the
file count from 1,704 to 589. They are read at runtime from
`CARGO_MANIFEST_DIR` and never embedded with `include_*!`, so nothing in the
package needs them to compile. Repo-local test runs are unaffected: `exclude`
governs packaging only, not the working tree.
@gold-silver-copper
gold-silver-copper added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit 854b765 Aug 16, 2026
14 checks passed
pull Bot pushed a commit to appelgriebsch/rig that referenced this pull request Aug 20, 2026
… a recorded embedding matrix for all seven keyed providers (0xPlaygrounds#2390)

* feat(telemetry): gen_ai spans for every non-completion modality

Completions were the only instrumented model calls; the normalized usage and
identity every modality response carries since the type-erasure sweep never
reached a span. Add `ModalityOperation` + `ModalitySpanBuilder` (operation
names embeddings / rerank / transcription / image_generation /
audio_generation on target `rig::modalities`) and one shared seam,
`telemetry::instrument_modality`, which opens the canonical span, runs the
call inside it, and records `gen_ai.usage.*`, `gen_ai.response.id` and
`gen_ai.response.model` off the normalized response on success.

Unlike `CompletionSpanBuilder`, modality spans never adopt an ambient span:
the adoption contract exists so one model *turn* has exactly one completion
span, and a modality call is not a turn — an agent's RAG lookup nests as its
own child span instead of overwriting the turn's fields.

Every provider implementation of `embed_texts_response`,
`embed_images_response`, `rerank`, `transcription`, `image_generation`
and `audio_generation` now routes through the seam — the shared OpenAI
embeddings/rerank/transcription/image/audio drivers cover their whole
compatible families at once, plus the hand-written seams (Cohere, Gemini
REST, Voyage, Ollama, Copilot, Hugging Face, Mistral, OpenRouter,
Hyperbolic) and the external crates (Bedrock, Gemini gRPC, FastEmbed).

The vector-store search path needs no store-side change: `embed_text` /
`embed_texts` are defaults derived from `embed_texts_response`, so a
`top_n` query records the same telemetry through the seam the model call
opens — pinned by a test driving `InMemoryVectorIndex::top_n` over the
OpenAI driver and counting the recorded usage fields.

* fix(mistral): capture mistral-correlation-id on embedding responses

Mistral sends its transport id on embedding responses exactly as on
completions (`mistral-correlation-id`, declared on its
OpenAICompatibleProvider impl), but the embeddings ext inherited the
OpenAIEmbeddingsCompatible trait's `None` header default, so the shared
driver read no header and every normalized embedding response reported
`provider_request_id: None` — the id Mistral support asks for, silently
dropped. Latent since the embeddings driver gained request-id capture.

Found while recording the embedding matrix; pinned by the recorded
`embedding_matrix/bug_mistral_request_id_dropped` cell, whose cassette
carries the live header.

* test(embeddings): a recorded embedding matrix for every keyed provider

One `embedding_matrix` cassette suite per embedding-capable provider with an
API key in the recording environment — openai, gemini, cohere, mistral,
venice, doubleword, openrouter — recorded live and replayed in CI. Cells per
provider: the normalized response is complete (input order, provider
attribution, and usage / model echo / transport request id asserted exactly
as the recorded wire reports them — `None`/zero pinned as the documented
outcome where a provider reports nothing, never skipped); `raw` round-trips
to the provider's own wire type and re-normalizes to the same view; the
inherent `raw_embed_texts` route agrees with the normalized call (two live
exchanges per recording, the raw-parity matrices' shape); the single-text
conveniences derive from the full method; `embedding_model_with_ndims`
round-trips the width where the provider supports one (OpenAI `dimensions`,
Mistral `output_dimension` on codestral-embed, Gemini
`output_dimensionality`, Venice); and a rejected request preserves the
provider's status and raw body. Cohere adds the image half: one answer per
image, `raw` as the per-image array, token usage honestly zero (Cohere
bills image embeds as `billed_units.images`, which is not token-denominated
and stays on the raw payload).

Mistral's suite carries `bug_mistral_request_id_dropped`, the recorded pin
for the correlation-id fix in the previous commit.

Shared expectations live in `tests/common/support.rs`
(`EmbeddingMatrixExpectations` / `assert_normalized_embedding_response`);
the new `with_mistral_embedding_cassette` wrapper is registered with the
cassette-safety guard. Keys present but provider not embeddings-capable
(checked, not applicable): anthropic, xai, groq, deepseek, perplexity.
No existing cassette was modified; the new recordings total ~3.0 MiB and are
excluded from the published tarball (0xPlaygrounds#2350).
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