v0.42.68.0 feat(ai): default to openai:text-embedding-3-small@1280 ahead of the ZeroEntropy sunset (#3390)

ZeroEntropy's hosted API shuts down 2026-09-04. `zeroentropyai:zembed-1` has
been DEFAULT_EMBEDDING_MODEL since v0.36.2.0, so every brain that never picked
a model explicitly was going to lose semantic retrieval on that date — query
embedding runs through the same endpoint, so existing vectors go unqueryable
too, not just new writes.

New default: openai:text-embedding-3-small at 1280 dimensions.

1280, not 1536, is load-bearing. OpenAI text-embedding-3-* is Matryoshka and
`isValidOpenAITextEmbedding3Dim` accepts any integer width up to the model's
native size (1536 for -small), so a brain created under the previous 1280-wide
ZE default keeps its existing vector(1280) column AND its HNSW index.
`applyEmbeddingMigration` only calls `runSchemaTransition` when
`col.dims !== plan.to_dims`, so migrating at the same width rebuilds vectors
only: no dimension transition, no ALTER, no index rebuild.

Two things had to move for the new default to actually work:

- The openai recipe's `dims_options` omitted 1280. That list is Tier 1 in
  `isCustomDimValidForProvider` and wins over the Tier-2 Matryoshka range
  check, so `resolveSchemaEmbeddingDim` REJECTED the shipped default config —
  `gbrain init` would have refused its own default. Verified by probe before
  and after.
- The recipe listed text-embedding-3-large first. `init`'s env detection picks
  `models[0]` and only adopts DEFAULT_EMBEDDING_DIMENSIONS when that equals the
  canonical default, so a fresh OPENAI_API_KEY-only install would have landed
  on 3-large@1536 and the declared default would have been unreachable.

Sunset banner (scaffolded in #3459) now names the concrete target and passes
`--dim` at the brain's current width, and reads the DB config plane as well as
the file plane — with the default no longer a ZE model, brains that never wrote
`embedding_model` to ~/.gbrain/config.json would otherwise stop being detected.

Adds a Default-provider policy to CLAUDE.md: a gbrain DEFAULT embedding or
reranking model must be either open-weight, or from the vendor with the longest
proven model-lifetime record. Novel/startup providers ship as opt-in recipes,
never as the default.

Also fixes a pre-existing ambient-env flake: test/e2e/fresh-install-pglite.test.ts
cleared a hardcoded pair of provider keys, so it failed on any machine with a
third provider key set (reproduced on master). It now clears every non-OpenAI
embedding provider key enumerated from the recipe registry.

The ZeroEntropy reranker default is deliberately unchanged — no replacement has
been chosen. Its sunset is called out in the banner, README, and provider doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-28 00:35:08 -07:00
co-authored by Claude Opus 5
parent fd8be831c5
commit 4d6599dd4c
26 changed files with 372 additions and 107 deletions
+47
View File
@@ -2,6 +2,53 @@
All notable changes to GBrain will be documented in this file.
## [0.42.68.0] - 2026-07-28
**The default embedding model is now `openai:text-embedding-3-small` at 1280 dimensions. Existing brains keep their column and index — only the vectors are rebuilt.**
ZeroEntropy's hosted API shuts down 2026-09-04. It has been GBrain's default embedder since v0.36.2.0, which means every brain that never picked a model explicitly was going to lose semantic retrieval on that date — not just for new writes, but for existing vectors, because query embedding runs through the same endpoint.
The new default is OpenAI `text-embedding-3-small` at **1280** dimensions. 1280, not 1536, is the whole point: OpenAI's `text-embedding-3-*` family is Matryoshka and accepts any output width up to the model's native size, so a brain created under the previous 1280-wide default keeps its existing `vector(1280)` column *and* its HNSW index. Migrating rebuilds vectors only — no dimension transition, no `ALTER`, no index rebuild.
This release also adds a **default-provider policy** to `CLAUDE.md`: a GBrain default embedding or reranking model must be either open-weight, or from the vendor with the longest proven model-lifetime record. Novel providers can ship as opt-in recipes, never as the default. The v0.36 default stranded every default-config brain on about six weeks' notice; the policy exists so that cannot repeat.
## To take advantage of v0.42.68.0
Fresh installs get the new default with no action. **Existing brains on ZeroEntropy must migrate before 2026-09-04**`gbrain upgrade` prints a one-time banner with the exact command for your brain's width.
1. **Upgrade and read the banner:**
```bash
gbrain upgrade
```
2. **Migrate off ZeroEntropy** (resumable; preview the cost first). Pass `--dim` at your brain's current width so the existing column and index are reused:
```bash
gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280 --dry-run
gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280
```
Check your current width with `gbrain doctor` if you are unsure. A killed run resumes from where it stopped — re-run the same command.
3. **If you use the ZeroEntropy reranker**, it sunsets on the same date. Either point the `llama-server-reranker` recipe at the Apache-2.0 `zerank` weights you self-host, pick another reranker, or turn it off:
```bash
gbrain config set search.reranker.enabled false
```
4. **Prefer to stay on zembed-1?** The weights are Apache-2.0. Self-host via llama-server or Ollama and point `embedding_model` at the local endpoint; your existing vectors stay valid and no migration is needed.
5. **If any step fails,** please file an issue at https://github.com/garrytan/gbrain/issues with the output of `gbrain doctor`.
### Itemized changes
#### Changed
- `DEFAULT_EMBEDDING_MODEL` is `openai:text-embedding-3-small`; `DEFAULT_EMBEDDING_DIMENSIONS` stays `1280` (`src/core/ai/defaults.ts`).
- The `openai` recipe lists `text-embedding-3-small` first, so a fresh install with only `OPENAI_API_KEY` set resolves the declared default instead of the recipe's largest tier.
- `1280` joins the `openai` recipe's `dims_options`. That list is Tier 1 in dimension validation and wins over the Matryoshka range check, so without it `gbrain init` rejected its own shipped default.
- The ZeroEntropy sunset banner in `gbrain upgrade` now names the concrete migration target and passes `--dim` at the brain's current width, and reads the database config plane as well as the file plane so brains that never wrote `embedding_model` to `~/.gbrain/config.json` are still detected.
- `gbrain advisor`'s missing-embedding-key smell checks for an OpenAI key rather than a ZeroEntropy one.
- `gbrain init`'s no-provider hint leads with the current default.
- Default-provider policy added to `CLAUDE.md`; README, `INSTALL_FOR_AGENTS.md`, the provider matrix, both tutorials, and the migration guides updated to current truth.
#### Fixed
- `test/e2e/fresh-install-pglite.test.ts` clears every non-OpenAI embedding provider key from the recipe registry instead of a hardcoded pair, so the file no longer passes or fails based on which provider keys happen to be set on the developer's machine.
## [0.42.66.1] - 2026-07-27
### Fixed
+17
View File
@@ -239,6 +239,23 @@ The install picker fires inside `gbrain init` AFTER `engine.initSchema()`
(non-TTY auto-selects). The upgrade banner fires once via `runPostUpgrade`
in `src/commands/upgrade.ts`, gated by `search.mode_upgrade_notice_shown`.
## Default-provider policy
**Default-provider policy.** A gbrain DEFAULT embedding or reranking model must
be either open-weight, or from the vendor with the longest proven model-lifetime
record. Novel/startup providers may ship as opt-in recipes, never as the
default. Rationale: the v0.36 zembed-1 default stranded every default-config
brain when ZeroEntropy was acquired and gave ~6 weeks notice.
Current defaults live in `src/core/ai/defaults.ts`:
`DEFAULT_EMBEDDING_MODEL = 'openai:text-embedding-3-small'`,
`DEFAULT_EMBEDDING_DIMENSIONS = 1280` (v0.42.68.0, #3390). 1280 — not 1536 — so
brains created under the previous ZeroEntropy default keep their existing
`vector(1280)` column AND its HNSW index: OpenAI text-embedding-3-* is
Matryoshka and `isValidOpenAITextEmbedding3Dim` accepts any width ≤ the model's
native size, so `gbrain migrate embeddings --to openai:text-embedding-3-small
--dim 1280` rebuilds vectors only, with no dimension transition.
## Eval discipline (v0.32.3)
Every metric printed by any `gbrain eval *` or `gbrain search stats` command
+8 -5
View File
@@ -40,16 +40,19 @@ restart the shell or add the PATH export to the shell profile.
## Step 2: API Keys
Ask the user for these. gbrain defaults to the ZeroEntropy embedding + reranker stack
(as of v0.36.2.0); OpenAI/Voyage are still supported as fallbacks via `gbrain config
set embedding_model <provider:model>`.
Ask the user for these. gbrain's default embedder is `openai:text-embedding-3-small`
at 1280 dimensions (as of v0.42.68.0); Voyage/Google/local providers are supported via
`gbrain config set embedding_model <provider:model>`.
```bash
export ZEROENTROPY_API_KEY=ze-... # default embedding + reranker (v0.36.2.0+)
export OPENAI_API_KEY=sk-... # fallback for vector search; also used for chat models
export OPENAI_API_KEY=sk-... # default embedding (text-embedding-3-small, 1280d); also chat models
export ANTHROPIC_API_KEY=sk-ant-... # optional, improves search quality via query expansion
```
> ZeroEntropy was the default embedder + reranker from v0.36.2.0 through v0.42.67.
> Its hosted API shuts down 2026-09-04. Brains still on it should run
> `gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280`.
Save to shell profile or `.env`. Keys are picked up by `gbrain config set` automatically
or can be stored in `~/.gbrain/config.json` (file plane). Without any embedding provider,
keyword search still works. Without Anthropic, search works but skips query expansion.
+3 -3
View File
@@ -290,8 +290,8 @@ Data flowing into the brain. Each integration is a recipe — markdown + setup h
- **Voice**: Phone calls create brain pages via Twilio + OpenAI Realtime (or DIY STT+LLM+TTS). Setup recipe: [`recipes/twilio-voice-brain.md`](recipes/twilio-voice-brain.md).
- **Email + calendar**: webhook handlers that route to brain signals. [`docs/integrations/meeting-webhooks.md`](docs/integrations/meeting-webhooks.md).
- **Embedding providers**: 16 recipes covering OpenAI (default fallback), OpenRouter, Voyage, ZeroEntropy (default), Google Gemini, Azure OpenAI, MiniMax, Alibaba DashScope, Zhipu, Ollama (local), llama.cpp llama-server (local), LiteLLM proxy. Pricing matrix + decision tree in [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md).
- **Rerankers**: ZeroEntropy `zerank-2` hosted (default in `tokenmax` mode) plus the v0.40.6.1 `llama-server-reranker` recipe for fully-local cross-encoder rerank via llama.cpp — runs Qwen3-Reranker or self-hosted ZeroEntropy weights against the same `gateway.rerank()` seam. Setup walkthrough in [`docs/ai-providers/llama-server-reranker.md`](docs/ai-providers/llama-server-reranker.md).
- **Embedding providers**: 16 recipes covering OpenAI (`text-embedding-3-small` at 1280d is the default since v0.42.68.0), OpenRouter, Voyage, Google Gemini, Azure OpenAI, MiniMax, Alibaba DashScope, Zhipu, Ollama (local), llama.cpp llama-server (local), LiteLLM proxy. Pricing matrix + decision tree in [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md). ZeroEntropy was the default from v0.36.2.0 to v0.42.67; its hosted API sunsets 2026-09-04 — existing brains move forward with `gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280` (same column width, no schema change). Default-provider policy lives in [`CLAUDE.md`](CLAUDE.md).
- **Rerankers**: ZeroEntropy `zerank-2` hosted (still the `tokenmax`-mode default; its hosted API sunsets 2026-09-04 — `gbrain upgrade` prints the switch instructions, and the `zerank` weights are Apache-2.0 so the local recipe below keeps working) plus the v0.40.6.1 `llama-server-reranker` recipe for fully-local cross-encoder rerank via llama.cpp — runs Qwen3-Reranker or self-hosted ZeroEntropy weights against the same `gateway.rerank()` seam. Setup walkthrough in [`docs/ai-providers/llama-server-reranker.md`](docs/ai-providers/llama-server-reranker.md).
- **Credential gateway**: vault-aware secret distribution. [`docs/integrations/credential-gateway.md`](docs/integrations/credential-gateway.md).
- **MCP clients**: every major MCP client is supported. [`docs/mcp/`](docs/mcp/) per-client setup.
@@ -459,4 +459,4 @@ MIT. I built GBrain to run my OpenClaw and Hermes deployments — the production
Origin story: [`docs/ethos/ORIGIN.md`](docs/ethos/ORIGIN.md).
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that ships as the default. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that shipped as the default from v0.36.2.0 to v0.42.67. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
+1 -1
View File
@@ -1 +1 @@
0.42.66.1
0.42.68.0
+17
View File
@@ -1,5 +1,22 @@
# ZeroEntropy — zembed-1 + zerank-2
> **Deprecated as a hosted provider. The ZeroEntropy hosted API shuts down
> 2026-09-04.** `zembed-1` was GBrain's default embedder from v0.36.2.0
> through v0.42.67; the default is now `openai:text-embedding-3-small` at
> 1280 dimensions (see the Default-provider policy in `CLAUDE.md`). If your
> brain still embeds through ZeroEntropy, migrate before that date:
>
> ```bash
> gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280 --dry-run
> gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280
> ```
>
> `--dim` at your brain's current width reuses the existing `vector(N)`
> column and its HNSW index — only the vectors are rebuilt. The `zembed-1`
> and `zerank` weights are Apache-2.0, so self-hosting via llama-server or
> Ollama is the other forward path and preserves your existing vectors
> outright. See [`../guides/embedding-migration.md`](../guides/embedding-migration.md).
[ZeroEntropy](https://zeroentropy.dev) ships two specialized small models
for retrieval pipelines:
+6 -5
View File
@@ -2,9 +2,10 @@
GBrain stores embeddings in a fixed-dimension `vector(N)` column on
`content_chunks`. If you switch to a model with a different dimension
(e.g. `openai:text-embedding-3-large` 1536 → `zeroentropyai:zembed-1`
1280, or `voyage:voyage-4-large` 2048), the on-disk column type doesn't
change automatically.
(e.g. `openai:text-embedding-3-large` 1536 → `voyage:voyage-4-large`
2048), the on-disk column type doesn't change automatically. Staying at
the SAME width — the v0.42.68.0 ZeroEntropy→OpenAI default swap keeps
1280 precisely so it can — needs no column change at all.
`gbrain init`, `gbrain doctor`, and `gbrain embed --stale` all detect
this mismatch and refuse to silently proceed. This doc is the recipe
@@ -63,7 +64,7 @@ single-command wrapper:
```bash
gbrain reinit-pglite \
--embedding-model zeroentropyai:zembed-1 \
--embedding-model openai:text-embedding-3-small \
--embedding-dimensions 1280
```
@@ -84,7 +85,7 @@ mv ~/.gbrain/brain.pglite ~/.gbrain/brain.pglite.bak
# every other field in ~/.gbrain/config.json (chat model,
# expansion model, API keys).
gbrain init --pglite \
--embedding-model zeroentropyai:zembed-1 \
--embedding-model openai:text-embedding-3-small \
--embedding-dimensions 1280
# 3. Re-import your brain repo. `gbrain sync` reads the brain repo
+16 -3
View File
@@ -3,9 +3,22 @@
`gbrain migrate embeddings` re-embeds an entire brain onto a different
embedding provider/model, safely and resumably. It is the forward path off a
sunsetting provider (for example ZeroEntropy's hosted API, which shuts down
2026-09-04 and is the shipped default for brains that never picked a model) —
but it is provider-agnostic: any configured `provider:model` works as a
target.
2026-09-04 and was the shipped default from v0.36.2.0 through v0.42.67) — but
it is provider-agnostic: any configured `provider:model` works as a target.
**Coming off the ZeroEntropy default?** Run:
```bash
gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280 --dry-run
gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280
```
`openai:text-embedding-3-small` @ 1280 is the v0.42.68.0 default. `--dim 1280`
matters: OpenAI text-embedding-3-* is Matryoshka and accepts any width up to
its native size, so migrating at the width you already have reuses the existing
`vector(1280)` column and its HNSW index — only the vectors are rebuilt. Omit
`--dim` and the target resolves to the recipe's 1536, forcing a needless
dimension transition (schema change + index rebuild).
Also reachable as `gbrain retrieval-upgrade` (the name `doctor` and the
README reference).
+5 -3
View File
@@ -15,7 +15,7 @@ gbrain init --pglite --model voyage # use a non-default provider
## Init resolves your provider from env keys
As of v0.37, `gbrain init --pglite` auto-detects which provider to use from your env vars. With `OPENAI_API_KEY` set, you get OpenAI. With `ZEROENTROPY_API_KEY` set, you get ZeroEntropy. If multiple provider keys are set, init fires an interactive picker. If no provider keys are set in a non-TTY context (CI, Docker build), init exits 1 with a paste-ready setup hint. Explicit flags (`--embedding-model`, `--no-embedding`) always win over env detection.
As of v0.37, `gbrain init --pglite` auto-detects which provider to use from your env vars. With `OPENAI_API_KEY` set, you get the default `openai:text-embedding-3-small` at 1280 dimensions. With `ZEROENTROPY_API_KEY` set, you get ZeroEntropy (deprecated — hosted API sunsets 2026-09-04). If multiple provider keys are set, init fires an interactive picker. If no provider keys are set in a non-TTY context (CI, Docker build), init exits 1 with a paste-ready setup hint. Explicit flags (`--embedding-model`, `--no-embedding`) always win over env detection.
The resolved provider + dimensions get persisted to `~/.gbrain/config.json` atomically, so subsequent runs are deterministic across releases.
@@ -23,8 +23,8 @@ The resolved provider + dimensions get persisted to `~/.gbrain/config.json` atom
| Provider | env vars | default dims | cost ($/1M tokens) | local? | multimodal? |
|---|---|---|---|---|---|
| `openai` (**default**) | `OPENAI_API_KEY` | `text-embedding-3-small` @ 1280 (Matryoshka, any width ≤1536); `text-embedding-3-large` @ 1536 | 0.02 (-small) / 0.13 (-large) | no | no |
| `zeroentropyai` | `ZEROENTROPY_API_KEY` | 2560 (Matryoshka to 1280/640/320/...) | 0.05 | no | no |
| `openai` | `OPENAI_API_KEY` | 1536 | 0.13 | no | no |
| `openrouter` | `OPENROUTER_API_KEY` | 1536 | 0.02 | no | model-dependent |
| `voyage` | `VOYAGE_API_KEY` | 1024 | 0.18 | no | yes (`voyage-multimodal-3`) |
| `google` | `GOOGLE_GENERATIVE_AI_API_KEY` | 768 | 0.025 | no | no |
@@ -75,7 +75,9 @@ The doctor distinguishes two repair paths:
### OpenAI
Default. Set `OPENAI_API_KEY`. Models: `text-embedding-3-large` (3072 max, 1536 default), `text-embedding-3-small` (1536). Matryoshka via the `dimensions` field — gbrain pins it from `embedding_dimensions` config so existing 1536-dim brains stay aligned across SDK upgrades.
**Default since v0.42.68.0: `openai:text-embedding-3-small` at 1280 dimensions.** Set `OPENAI_API_KEY`. Models: `text-embedding-3-small` (1536 native, $0.02/Mtok), `text-embedding-3-large` (3072 native, 1536 recipe default, $0.13/Mtok). Both are Matryoshka: `isValidOpenAITextEmbedding3Dim` accepts **any integer width up to the model's native size**, so 1280 is a first-class width — that is exactly why the v0.42.68.0 default swap needs no schema change on brains created under the previous 1280-wide ZeroEntropy default. gbrain pins `dimensions` from `embedding_dimensions` config so existing brains stay aligned across SDK upgrades.
Note the split: `DEFAULT_EMBEDDING_DIMENSIONS` (1280) is the zero-config brain width; the openai recipe's `default_dims` (1536) is what `gbrain migrate embeddings --to openai:text-embedding-3-*` resolves when you pass no `--dim`. Pass `--dim 1280` when you want to keep an existing 1280-wide column and its HNSW index in place.
Optional `OPENAI_BASE_URL` — point the native OpenAI provider at an OpenAI-compatible gateway. A bare host is normalized to carry the `/v1` suffix automatically (so `https://gw.example.com` and `https://gw.example.com/v1` both work); when unset, the SDK's default endpoint is untouched. `ANTHROPIC_BASE_URL` gets the same normalization for Anthropic chat/expansion calls.
+7 -4
View File
@@ -492,9 +492,12 @@ OAuth source scoping only guards the HTTP MCP path. If the brain's Postgres and
## Part 13: Cost and speed expectations
Real numbers from the published benchmark, running the default stack (GBrain with ZeroEntropy for embedding + reranker):
Real numbers from the published benchmark. The benchmark run used ZeroEntropy for
embedding + reranker, which was the default through v0.42.67; the default embedder is
now `openai:text-embedding-3-small` at 1280 dimensions (see the Default-provider policy
in `CLAUDE.md`).
- **Embedding cost:** $0.05 per million tokens. For comparison, GBrain configured with OpenAI is $0.13 (2.6× more expensive), Voyage is $0.18 (3.6× more).
- **Embedding cost:** $0.02 per million tokens on the current default (`text-embedding-3-small`). For comparison, `text-embedding-3-large` is $0.13 (6.5× more), Voyage is $0.18 (9× more).
- **Ingest speed:** about 22 seconds for a small test corpus of 164 pages on the host machine. For a 10K-page corpus, expect about 20 minutes the first time, then most syncs are incremental and finish in seconds.
- **Query latency:** about 122 ms median for a `gbrain search`. For comparison, the same query through GBrain with OpenAI takes about 282 ms.
- **Synthesized-answer latency:** a few seconds, dominated by the Anthropic API.
@@ -502,7 +505,7 @@ Real numbers from the published benchmark, running the default stack (GBrain wit
Full methodology and per-run receipt JSONs live in [the gbrain-evals repo](https://github.com/garrytan/gbrain-evals/blob/main/docs/benchmarks/2026-05-23-v0.40.6.0-snapshot.md).
For a 25-person company at sustained use, expect about $35 a month in embeddings (ZeroEntropy at $0.05/million tokens), $50 a month in Anthropic calls for the synthesized-answer queries, plus your hosting bill. Under $100 a month for the AI side at most companies your size.
For a 25-person company at sustained use, expect about $15 a month in embeddings (the default `text-embedding-3-small` at $0.02/million tokens), $50 a month in Anthropic calls for the synthesized-answer queries, plus your hosting bill. Under $100 a month for the AI side at most companies your size.
---
@@ -514,7 +517,7 @@ Check `gbrain auth list` on the host and confirm their client has `--source` set
### "Sync is slow and feels stuck"
The first sync embeds every page, which takes time. Check `gbrain sources status` for the live page count. If it's climbing you're not stuck, you're just embedding. If you've got a 10K-page corpus and ZeroEntropy is being throttled, the per-source parallel sync looks like progress on three sources at once rather than one source moving fast.
The first sync embeds every page, which takes time. Check `gbrain sources status` for the live page count. If it's climbing you're not stuck, you're just embedding. If you've got a 10K-page corpus and your embedding provider is being throttled, the per-source parallel sync looks like progress on three sources at once rather than one source moving fast.
### "I see a page I shouldn't see"
+2 -3
View File
@@ -103,11 +103,10 @@ Render will build a Docker container with the harness. First deploy takes about
In the AlphaClaw UI (Providers tab):
- **OpenAI API Key.** Required for embeddings if you use the OpenAI provider.
- **OpenAI API Key.** Required. GBrain's default embedder is `openai:text-embedding-3-small` at 1280 dimensions ($0.02 per million tokens).
- **Anthropic API Key.** Required for Claude (the main model the agent talks through).
- **Perplexity API Key.** Optional, for web search.
- **Voyage API Key.** Optional, alternative to OpenAI for embeddings.
- **ZeroEntropy API Key.** Recommended. GBrain ships with ZeroEntropy as the default embedder + reranker because it's about 2× faster than OpenAI and about 2.6× cheaper.
You can use the same keys across multiple agents.
@@ -236,7 +235,7 @@ Brains share through git. My main agent can populate another agent's brain by pu
|-----------|-------------|
| Render Pro (minimum viable) | about $85 |
| Supabase (small) | free to $25 |
| OpenAI API (embeddings) | $5 to $20 (much less if you use ZeroEntropy as the default) |
| OpenAI API (embeddings) | $5 to $20 (the default `text-embedding-3-small` is the cheap end of that range) |
| Anthropic API (Claude) | $50 to $500 (usage dependent) |
| **Total minimum** | **about $100 to $150 a month** |
+28 -8
View File
@@ -388,6 +388,23 @@ The install picker fires inside `gbrain init` AFTER `engine.initSchema()`
(non-TTY auto-selects). The upgrade banner fires once via `runPostUpgrade`
in `src/commands/upgrade.ts`, gated by `search.mode_upgrade_notice_shown`.
## Default-provider policy
**Default-provider policy.** A gbrain DEFAULT embedding or reranking model must
be either open-weight, or from the vendor with the longest proven model-lifetime
record. Novel/startup providers may ship as opt-in recipes, never as the
default. Rationale: the v0.36 zembed-1 default stranded every default-config
brain when ZeroEntropy was acquired and gave ~6 weeks notice.
Current defaults live in `src/core/ai/defaults.ts`:
`DEFAULT_EMBEDDING_MODEL = 'openai:text-embedding-3-small'`,
`DEFAULT_EMBEDDING_DIMENSIONS = 1280` (v0.42.68.0, #3390). 1280 — not 1536 — so
brains created under the previous ZeroEntropy default keep their existing
`vector(1280)` column AND its HNSW index: OpenAI text-embedding-3-* is
Matryoshka and `isValidOpenAITextEmbedding3Dim` accepts any width ≤ the model's
native size, so `gbrain migrate embeddings --to openai:text-embedding-3-small
--dim 1280` rebuilds vectors only, with no dimension transition.
## Eval discipline (v0.32.3)
Every metric printed by any `gbrain eval *` or `gbrain search stats` command
@@ -1030,16 +1047,19 @@ restart the shell or add the PATH export to the shell profile.
## Step 2: API Keys
Ask the user for these. gbrain defaults to the ZeroEntropy embedding + reranker stack
(as of v0.36.2.0); OpenAI/Voyage are still supported as fallbacks via `gbrain config
set embedding_model <provider:model>`.
Ask the user for these. gbrain's default embedder is `openai:text-embedding-3-small`
at 1280 dimensions (as of v0.42.68.0); Voyage/Google/local providers are supported via
`gbrain config set embedding_model <provider:model>`.
```bash
export ZEROENTROPY_API_KEY=ze-... # default embedding + reranker (v0.36.2.0+)
export OPENAI_API_KEY=sk-... # fallback for vector search; also used for chat models
export OPENAI_API_KEY=sk-... # default embedding (text-embedding-3-small, 1280d); also chat models
export ANTHROPIC_API_KEY=sk-ant-... # optional, improves search quality via query expansion
```
> ZeroEntropy was the default embedder + reranker from v0.36.2.0 through v0.42.67.
> Its hosted API shuts down 2026-09-04. Brains still on it should run
> `gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280`.
Save to shell profile or `.env`. Keys are picked up by `gbrain config set` automatically
or can be stored in `~/.gbrain/config.json` (file plane). Without any embedding provider,
keyword search still works. Without Anthropic, search works but skips query expansion.
@@ -1784,8 +1804,8 @@ Data flowing into the brain. Each integration is a recipe — markdown + setup h
- **Voice**: Phone calls create brain pages via Twilio + OpenAI Realtime (or DIY STT+LLM+TTS). Setup recipe: [`recipes/twilio-voice-brain.md`](recipes/twilio-voice-brain.md).
- **Email + calendar**: webhook handlers that route to brain signals. [`docs/integrations/meeting-webhooks.md`](docs/integrations/meeting-webhooks.md).
- **Embedding providers**: 16 recipes covering OpenAI (default fallback), OpenRouter, Voyage, ZeroEntropy (default), Google Gemini, Azure OpenAI, MiniMax, Alibaba DashScope, Zhipu, Ollama (local), llama.cpp llama-server (local), LiteLLM proxy. Pricing matrix + decision tree in [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md).
- **Rerankers**: ZeroEntropy `zerank-2` hosted (default in `tokenmax` mode) plus the v0.40.6.1 `llama-server-reranker` recipe for fully-local cross-encoder rerank via llama.cpp — runs Qwen3-Reranker or self-hosted ZeroEntropy weights against the same `gateway.rerank()` seam. Setup walkthrough in [`docs/ai-providers/llama-server-reranker.md`](docs/ai-providers/llama-server-reranker.md).
- **Embedding providers**: 16 recipes covering OpenAI (`text-embedding-3-small` at 1280d is the default since v0.42.68.0), OpenRouter, Voyage, Google Gemini, Azure OpenAI, MiniMax, Alibaba DashScope, Zhipu, Ollama (local), llama.cpp llama-server (local), LiteLLM proxy. Pricing matrix + decision tree in [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md). ZeroEntropy was the default from v0.36.2.0 to v0.42.67; its hosted API sunsets 2026-09-04 — existing brains move forward with `gbrain migrate embeddings --to openai:text-embedding-3-small --dim 1280` (same column width, no schema change). Default-provider policy lives in [`CLAUDE.md`](CLAUDE.md).
- **Rerankers**: ZeroEntropy `zerank-2` hosted (still the `tokenmax`-mode default; its hosted API sunsets 2026-09-04 — `gbrain upgrade` prints the switch instructions, and the `zerank` weights are Apache-2.0 so the local recipe below keeps working) plus the v0.40.6.1 `llama-server-reranker` recipe for fully-local cross-encoder rerank via llama.cpp — runs Qwen3-Reranker or self-hosted ZeroEntropy weights against the same `gateway.rerank()` seam. Setup walkthrough in [`docs/ai-providers/llama-server-reranker.md`](docs/ai-providers/llama-server-reranker.md).
- **Credential gateway**: vault-aware secret distribution. [`docs/integrations/credential-gateway.md`](docs/integrations/credential-gateway.md).
- **MCP clients**: every major MCP client is supported. [`docs/mcp/`](docs/mcp/) per-client setup.
@@ -1953,7 +1973,7 @@ MIT. I built GBrain to run my OpenClaw and Hermes deployments — the production
Origin story: [`docs/ethos/ORIGIN.md`](docs/ethos/ORIGIN.md).
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that ships as the default. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that shipped as the default from v0.36.2.0 to v0.42.67. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
---
+1 -1
View File
@@ -146,7 +146,7 @@
"bun": ">=1.3.10"
},
"license": "MIT",
"version": "0.42.66.1",
"version": "0.42.68.0",
"overrides": {
"@hono/node-server": "^2.0.5",
"fast-uri": "^3.1.4",
+5 -5
View File
@@ -494,9 +494,9 @@ export async function findEnvKeyTypos(
/** Emit the fail-loud "no embedding provider" message + paste-ready setup. */
function printNoEmbeddingProviderHint(typos: Array<{ userSet: string; suggested: string }>): void {
console.error('\nNo embedding provider configured. Set one of:');
console.error(' export OPENAI_API_KEY=sk-… # openai:text-embedding-3-large (1536d)');
console.error(' export ZEROENTROPY_API_KEY=ze-… # zeroentropyai:zembed-1 (2560d, Matryoshka)');
console.error(' export OPENAI_API_KEY=sk-… # default: openai:text-embedding-3-small (1280d)');
console.error(' export VOYAGE_API_KEY=pa-… # voyage:voyage-3-large (1024d)');
console.error(' export GOOGLE_GENERATIVE_AI_API_KEY=… # google:gemini-embedding-001 (768d)');
console.error('Then re-run: gbrain init --pglite');
console.error('');
console.error('Or pick explicitly:');
@@ -527,9 +527,9 @@ async function resolveEmbeddingByEnv(out: ResolvedAIOptions, nonInteractive: boo
// (DEFAULT_EMBEDDING_MODEL), use the gateway's
// DEFAULT_EMBEDDING_DIMENSIONS instead of the recipe's `default_dims`
// (which is the recipe's "largest sensible" tier). This keeps
// fresh-install schema width aligned with the v0.37.11.0 system
// default — for ZE that means 1280 (the Matryoshka step closest to
// legacy OpenAI 1536), not the recipe's 2560.
// fresh-install schema width aligned with the system default — for
// openai:text-embedding-3-small that means 1280 (v0.42.68.0), not
// the recipe's 1536.
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } =
await import('../core/ai/defaults.ts');
const { embeddingDimsForModel } = await import('../core/ai/model-resolver.ts');
+29 -5
View File
@@ -470,10 +470,30 @@ export async function runPostUpgrade(args: string[] = []): Promise<void> {
// embedding uses the same endpoint, so existing vectors become
// unqueryable. One-shot per install, gated by
// `ze_sunset_notice_shown` (same pattern as the search-mode banner).
//
// v0.42.68.0: DEFAULT_EMBEDDING_MODEL is no longer a ZE model, so the
// file plane alone would stop detecting brains that were created under
// the v0.36v0.42.67 ZE default and never wrote `embedding_model` to
// ~/.gbrain/config.json. Those brains DO carry the DB-plane row seeded
// by initSchema (pglite-schema.ts `('embedding_model', …)`), so read
// the DB plane as the second source before falling back to the default.
try {
const shown = await engine.getConfig('ze_sunset_notice_shown');
const { DEFAULT_EMBEDDING_MODEL } = await import('../core/ai/defaults.ts');
const effectiveModel = cfgSchema.embedding_model ?? DEFAULT_EMBEDDING_MODEL;
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } =
await import('../core/ai/defaults.ts');
const dbModel = await engine.getConfig('embedding_model');
const effectiveModel = cfgSchema.embedding_model ?? dbModel ?? DEFAULT_EMBEDDING_MODEL;
// Migrating AT THE CURRENT WIDTH is what keeps the existing
// vector(N) column + HNSW index in place (applyEmbeddingMigration
// only runs runSchemaTransition when col.dims !== plan.to_dims).
// Bare `--to openai:text-embedding-3-small` would resolve to the
// recipe's 1536 and force a needless dimension transition, so the
// hint carries `--dim` explicitly.
const dbDims = await engine.getConfig('embedding_dimensions');
const parsedDbDims = dbDims ? parseInt(dbDims, 10) : NaN;
const currentDims = cfgSchema.embedding_dimensions
?? (Number.isFinite(parsedDbDims) && parsedDbDims > 0 ? parsedDbDims : undefined)
?? DEFAULT_EMBEDDING_DIMENSIONS;
const rerankerModel = await engine.getConfig('search.reranker.model');
const onZeEmbedding = effectiveModel.startsWith('zeroentropyai:');
const onZeReranker = !!rerankerModel?.startsWith('zeroentropyai:');
@@ -492,9 +512,13 @@ export async function runPostUpgrade(args: string[] = []): Promise<void> {
}
console.log('═══════════════════════════════════════════════════════════════');
console.log('');
console.log('Migrate before the sunset (resumable; preview cost first):');
console.log(' gbrain migrate embeddings --to <provider:model> --dry-run');
console.log(' gbrain migrate embeddings --to <provider:model>');
console.log('Migrate before 2026-09-04 (resumable; preview cost first):');
console.log(` gbrain migrate embeddings --to ${DEFAULT_EMBEDDING_MODEL} --dim ${currentDims} --dry-run`);
console.log(` gbrain migrate embeddings --to ${DEFAULT_EMBEDDING_MODEL} --dim ${currentDims}`);
console.log('');
console.log(`${DEFAULT_EMBEDDING_MODEL} is the v0.42.68.0 default. At --dim`);
console.log(`${currentDims} it reuses your existing vector(${currentDims}) column and HNSW`);
console.log('index — only the vectors are rebuilt, no schema transition.');
console.log('');
console.log('Self-hosting zembed-1 (weights are Apache-2.0) via llama-server /');
console.log('ollama also works and preserves your existing vectors — point');
+5 -3
View File
@@ -36,14 +36,16 @@ export const collectSetupSmells: AdvisorCollector = {
collector: 'setup-smells',
ask_user: true,
});
} else if (!cfg.embedding_model && !cfg.zeroentropy_api_key && !process.env.ZEROENTROPY_API_KEY) {
} else if (!cfg.embedding_model && !cfg.openai_api_key && !process.env.OPENAI_API_KEY) {
// Default provider needs a key; none present anywhere → embeds will fail.
// v0.42.68.0 (#3390): the default is openai:text-embedding-3-small, so
// the key this checks for is OpenAI's, not ZeroEntropy's.
findings.push({
id: 'embedding_key_missing',
severity: 'warn',
title: 'No embedding provider key is set — embedding will fail at write time.',
detail: 'Set zeroentropy_api_key (or choose another provider via embedding_model).',
fix: { command_argv: ['gbrain', 'config', 'set', 'zeroentropy_api_key', '<key>'] },
detail: 'Set openai_api_key (or choose another provider via embedding_model).',
fix: { command_argv: ['gbrain', 'config', 'set', 'openai_api_key', '<key>'] },
collector: 'setup-smells',
ask_user: true,
});
+16 -6
View File
@@ -12,10 +12,20 @@
* install AND every doctor consistency check.
*/
// v0.36.0 chose ZeroEntropy as the system default after evals showed
// 11/20 wins vs OpenAI (6) and Voyage (4) on real-corpus benchmarks.
// 1280 is the closest analog to legacy OpenAI 1536d while staying on
// the high-recall section of ZE's Matryoshka curve. Valid ZE Matryoshka
// steps: {2560, 1280, 640, 320, 160, 80, 40} — see ai/dims.ts.
export const DEFAULT_EMBEDDING_MODEL = 'zeroentropyai:zembed-1';
// v0.42.68.0 (#3390): the default moved OFF ZeroEntropy. ZE's hosted API
// (including /models/embed) shuts down 2026-09-04, which would have taken
// semantic retrieval with it on every default-config brain. See the
// Default-provider policy in CLAUDE.md: a gbrain DEFAULT must be
// open-weight or from the vendor with the longest proven model-lifetime
// record. OpenAI's text-embedding-3-* has been stable since 2024-01.
//
// Why 1280 and NOT 1536 (load-bearing — do not "round up"):
// OpenAI text-embedding-3-* is Matryoshka, and
// `isValidOpenAITextEmbedding3Dim` accepts ANY integer 1..1536 for
// text-embedding-3-small (ai/dims.ts). Keeping 1280 means every brain
// created under the v0.36v0.42.67 ZE default keeps its existing
// `vector(1280)` column AND its HNSW index — `gbrain migrate embeddings
// --to openai:text-embedding-3-small` rebuilds the VECTORS only, with no
// dimension transition, no ALTER, no index rebuild.
export const DEFAULT_EMBEDDING_MODEL = 'openai:text-embedding-3-small';
export const DEFAULT_EMBEDDING_DIMENSIONS = 1280;
+23 -4
View File
@@ -12,11 +12,30 @@ export const openai: Recipe = {
},
touchpoints: {
embedding: {
models: ['text-embedding-3-large', 'text-embedding-3-small'],
// v0.42.68.0 (#3390): -small leads because it IS
// DEFAULT_EMBEDDING_MODEL (ai/defaults.ts). `init`'s env detection
// picks models[0]; when that equals the canonical default it also
// adopts DEFAULT_EMBEDDING_DIMENSIONS (1280) instead of default_dims.
// Reordering keeps "the declared default" and "what a fresh
// OPENAI_API_KEY-only install actually gets" the same thing.
models: ['text-embedding-3-small', 'text-embedding-3-large'],
default_dims: 1536,
dims_options: [256, 512, 768, 1024, 1536, 3072],
cost_per_1m_tokens_usd: 0.13,
price_last_verified: '2026-04-20',
// 1280 is here because it IS DEFAULT_EMBEDDING_DIMENSIONS (v0.42.68.0).
// `dims_options` is Tier 1 in isCustomDimValidForProvider — it wins over
// the Tier-2 `isValidOpenAITextEmbedding3Dim` range check, so a width
// missing from this list is rejected before the real Matryoshka rule is
// ever consulted. Without 1280 the shipped default config fails
// `resolveSchemaEmbeddingDim` and `gbrain init` refuses its own default.
// ponytail: curated list, not the true rule (OpenAI accepts ANY integer
// ≤ the model's native size). Delete `dims_options` here and let Tier 2
// govern if arbitrary widths ever need to work.
dims_options: [256, 512, 768, 1024, 1280, 1536, 3072],
// Tracks models[0] (`text-embedding-3-small`), same convention as the
// openrouter recipe. Display-only, for `gbrain providers list/explain`;
// all actual cost math routes through the per-model table in
// src/core/embedding-pricing.ts (-small $0.02 / -large $0.13).
cost_per_1m_tokens_usd: 0.02,
price_last_verified: '2026-07-28',
// OpenAI per-request hard cap is 300K tokens. Free/Tier-1 TPM is 1M.
// Cap batches conservatively at 100K to handle token-dense content
// (Discord/Slack markdown+JSON tokenizes at ~chars/2.7, not the chars/4
+62
View File
@@ -161,3 +161,65 @@ describe('dimsProviderOptions — prefixed model IDs (OpenRouter / proxy provide
}
});
});
// v0.42.68.0 (#3390) — the default embedding model moved off ZeroEntropy
// (hosted API sunsets 2026-09-04) to openai:text-embedding-3-small, and
// KEPT 1280 dimensions on purpose. These tests pin the two properties the
// swap rests on, so a future "tidy up to 1536" can't land silently.
describe('default embedding config (v0.42.68.0 #3390)', () => {
test('DEFAULT_EMBEDDING_MODEL / DIMENSIONS are openai:text-embedding-3-small @ 1280', async () => {
const defaults = await import('../../src/core/ai/defaults.ts');
expect(defaults.DEFAULT_EMBEDDING_MODEL).toBe('openai:text-embedding-3-small');
expect(defaults.DEFAULT_EMBEDDING_DIMENSIONS).toBe(1280);
});
test('the default width is a valid Matryoshka width for the default model', async () => {
// The no-schema-change property: brains created under the previous
// 1280-wide ZeroEntropy default keep their vector(1280) column and its
// HNSW index because OpenAI text-embedding-3-* accepts any width up to
// its native size. Derived from the constants, never hardcoded.
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } =
await import('../../src/core/ai/defaults.ts');
const bareModel = DEFAULT_EMBEDDING_MODEL.split(':')[1];
expect(isOpenAITextEmbedding3Model(bareModel)).toBe(true);
expect(isValidOpenAITextEmbedding3Dim(bareModel, DEFAULT_EMBEDDING_DIMENSIONS)).toBe(true);
expect(DEFAULT_EMBEDDING_DIMENSIONS)
.toBeLessThanOrEqual(maxOpenAITextEmbedding3Dim(bareModel)!);
});
test('dimsProviderOptions passes the default width through to the wire', async () => {
const { DEFAULT_EMBEDDING_DIMENSIONS } = await import('../../src/core/ai/defaults.ts');
expect(dimsProviderOptions('native-openai', 'text-embedding-3-small', DEFAULT_EMBEDDING_DIMENSIONS))
.toEqual({ openai: { dimensions: 1280 } });
});
test('resolveSchemaEmbeddingDim ACCEPTS the shipped default config', async () => {
// Regression guard: `dims_options` on the openai recipe is Tier 1 in
// isCustomDimValidForProvider and wins over the Matryoshka range check.
// It omitted 1280 until #3390, which made `gbrain init` reject its own
// default. Drop 1280 from the recipe and this test fails.
const { resolveSchemaEmbeddingDim } = await import('../../src/core/embedding-dim-check.ts');
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } =
await import('../../src/core/ai/defaults.ts');
const got = resolveSchemaEmbeddingDim({
embedding_model: DEFAULT_EMBEDDING_MODEL,
embedding_dimensions: DEFAULT_EMBEDDING_DIMENSIONS,
});
expect(got.ok).toBe(true);
if (got.ok) {
expect(got.dim).toBe(1280);
expect(got.model).toBe(DEFAULT_EMBEDDING_MODEL);
expect(got.provider).toBe('openai');
}
});
test('init env-detection lands on the default model when OPENAI_API_KEY is the only key', async () => {
// `resolveEmbeddingByEnv` picks touchpoints.embedding.models[0] and only
// adopts DEFAULT_EMBEDDING_DIMENSIONS when that equals the canonical
// default. If the recipe's model order regresses, a fresh install silently
// gets text-embedding-3-large @ 1536 instead of the declared default.
const { openai } = await import('../../src/core/ai/recipes/openai.ts');
const { DEFAULT_EMBEDDING_MODEL } = await import('../../src/core/ai/defaults.ts');
expect(`openai:${openai.touchpoints.embedding!.models![0]}`).toBe(DEFAULT_EMBEDDING_MODEL);
});
});
+7 -5
View File
@@ -40,12 +40,14 @@ describe('gateway configuration', () => {
expect(getExpansionModel()).toBe('anthropic:claude-haiku-4-5-20251001');
});
test('defaults are ZE 1280d as of v0.36.0.0 (D3)', () => {
// The default flipped from openai:text-embedding-3-large 1536d to
// zeroentropyai:zembed-1 1280d in v0.36.0.0. The cost story is in
// CHANGELOG.md; the rationale lives in src/core/ai/gateway.ts:45-54.
test('defaults are openai:text-embedding-3-small 1280d as of v0.42.68.0 (#3390)', () => {
// v0.36.0.0 flipped the default to zeroentropyai:zembed-1 @ 1280d.
// v0.42.68.0 flipped the MODEL off ZeroEntropy (hosted API sunsets
// 2026-09-04) but deliberately KEPT 1280 so existing ZE-default brains
// reuse their vector(1280) column + HNSW index. Rationale + policy in
// src/core/ai/defaults.ts and CLAUDE.md's Default-provider policy.
configureGateway({ env: {} });
expect(getEmbeddingModel()).toBe('zeroentropyai:zembed-1');
expect(getEmbeddingModel()).toBe('openai:text-embedding-3-small');
expect(getEmbeddingDimensions()).toBe(1280);
expect(getExpansionModel()).toBe('anthropic:claude-haiku-4-5-20251001');
});
+3 -3
View File
@@ -3,15 +3,15 @@ import { getPGLiteSchema, PGLITE_SCHEMA_SQL } from '../../src/core/pglite-schema
import { getPostgresSchema } from '../../src/core/postgres-engine.ts';
describe('getPGLiteSchema', () => {
test('default produces gateway-default schema (v0.37+: 1280d + zeroentropyai:zembed-1)', () => {
test('default produces gateway-default schema (v0.42.68.0+: 1280d + openai:text-embedding-3-small)', () => {
// v0.37 fix wave Lane A.1 + CDX2-1: defaults now track the canonical
// gateway constants in `ai/defaults.ts` instead of the stale v0.13
// OpenAI literals (1536 / text-embedding-3-large). Fixes the
// headline bug where bare `gbrain init --pglite` produced a 1536
// schema while the ZE default model emitted 1280-dim vectors.
// schema while the default model emitted 1280-dim vectors.
const sql = getPGLiteSchema();
expect(sql).toMatch(/vector\(1280\)/);
expect(sql).toMatch(/'zeroentropyai:zembed-1'/);
expect(sql).toMatch(/'openai:text-embedding-3-small'/);
expect(sql).not.toMatch(/__EMBEDDING_DIMS__/);
expect(sql).not.toMatch(/__EMBEDDING_MODEL__/);
});
+7 -5
View File
@@ -219,12 +219,13 @@ describe('hybridSearch + resolver — unknown column at entry (D11)', () => {
describe('upsertChunks — model provenance uses gateway-resolved model, not compiled default', () => {
// Regression (zbrain-rfi): when a caller builds ChunkInputs without an
// explicit `model` (as src/commands/embed.ts does), the engine used to
// stamp the compile-time DEFAULT_EMBEDDING_MODEL ('zeroentropyai:zembed-1')
// onto content_chunks.model — even though the vector was produced by the
// stamp the compile-time DEFAULT_EMBEDDING_MODEL onto
// content_chunks.model — even though the vector was produced by the
// config-resolved model. That corrupted provenance the signature-drift +
// dim-migration logic trusts. The engine must fall back to the model the
// gateway ACTUALLY resolves at write time.
test('unspecified chunk.model records the resolved model, not zeroentropyai:zembed-1', async () => {
// gateway ACTUALLY resolves at write time. Asserts against the LIVE
// constant so the guard survives a default swap (v0.42.68.0 #3390).
test('unspecified chunk.model records the resolved model, not the compiled default', async () => {
configureGateway({
embedding_model: 'openai:text-embedding-3-large',
embedding_dimensions: 1536,
@@ -248,7 +249,8 @@ describe('upsertChunks — model provenance uses gateway-resolved model, not com
);
expect(rows.length).toBe(1);
expect(rows[0].model).toBe('openai:text-embedding-3-large');
expect(rows[0].model).not.toBe('zeroentropyai:zembed-1');
const { DEFAULT_EMBEDDING_MODEL } = await import('../../src/core/ai/defaults.ts');
expect(rows[0].model).not.toBe(DEFAULT_EMBEDDING_MODEL);
resetGateway();
});
+35 -21
View File
@@ -3,7 +3,8 @@
*
* The headline behavior the v0.37 fix wave exists to fix. Pre-fix, this
* exact path broke: schema sized to 1536 (stale default), embed pipeline
* used ZE/1280, first chunk insert failed with vector dim mismatch.
* used the 1280-wide default, first chunk insert failed with vector dim
* mismatch.
*
* Hermetic: in-process (NOT a CLI subprocess), GBRAIN_HOME pinned to a
* tmpdir, embed transport stubbed via `__setEmbedTransportForTests` so we
@@ -26,36 +27,49 @@ import {
describe('E2E: fresh gbrain init --pglite → import → embed works end-to-end', () => {
let tmpHome: string;
let origHome: string | undefined;
let origZeKey: string | undefined;
let origOpenaiKey: string | undefined;
let origVoyageKey: string | undefined;
// Every embedding-provider auth env var we cleared, so afterEach restores
// exactly what was there. Enumerated from the recipe registry rather than
// hardcoded: a dev machine with MINIMAX_API_KEY (or any of the other 14
// providers) set used to trip init's disambiguation gate
// ("Multiple embedding providers env-ready: openai, minimax") before the
// test body ran, which made this file pass or fail on ambient environment.
let clearedProviderKeys: Record<string, string | undefined> = {};
beforeEach(() => {
async function clearNonOpenAIEmbeddingKeys(): Promise<void> {
const { RECIPES } = await import('../../src/core/ai/recipes/index.ts');
clearedProviderKeys = {};
for (const recipe of RECIPES.values()) {
if (recipe.id === 'openai') continue;
if (!recipe.touchpoints.embedding) continue;
for (const key of recipe.auth_env?.required ?? []) {
clearedProviderKeys[key] = process.env[key];
delete process.env[key];
}
}
}
beforeEach(async () => {
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-e2e-fresh-'));
origHome = process.env.GBRAIN_HOME;
origZeKey = process.env.ZEROENTROPY_API_KEY;
// Save + clear OPENAI_API_KEY + VOYAGE_API_KEY so init only sees
// one provider as env-ready (ZE). Without this, dev machines with
// multi-provider env (Garry's setup) fail init's disambiguation gate
// ("Multiple embedding providers env-ready: openai, voyage,
// zeroentropyai") before the test body runs.
origOpenaiKey = process.env.OPENAI_API_KEY;
origVoyageKey = process.env.VOYAGE_API_KEY;
delete process.env.OPENAI_API_KEY;
delete process.env.VOYAGE_API_KEY;
// Leave openai as the ONLY env-ready embedding provider, so bare
// `init --pglite` resolves the v0.42.68.0 default
// (openai:text-embedding-3-small @ DEFAULT_EMBEDDING_DIMENSIONS).
await clearNonOpenAIEmbeddingKeys();
clearedProviderKeys.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
process.env.GBRAIN_HOME = tmpHome;
// Stub key so init's setup-hint check passes.
process.env.ZEROENTROPY_API_KEY = 'sk-test-ze';
process.env.OPENAI_API_KEY = 'sk-test-openai';
});
afterEach(() => {
rmSync(tmpHome, { recursive: true, force: true });
if (origHome === undefined) delete process.env.GBRAIN_HOME;
else process.env.GBRAIN_HOME = origHome;
if (origZeKey === undefined) delete process.env.ZEROENTROPY_API_KEY;
else process.env.ZEROENTROPY_API_KEY = origZeKey;
if (origOpenaiKey !== undefined) process.env.OPENAI_API_KEY = origOpenaiKey;
if (origVoyageKey !== undefined) process.env.VOYAGE_API_KEY = origVoyageKey;
for (const [key, value] of Object.entries(clearedProviderKeys)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
clearedProviderKeys = {};
__setEmbedTransportForTests(null);
// Restore legacy-preload gateway state.
configureGateway({
@@ -65,7 +79,7 @@ describe('E2E: fresh gbrain init --pglite → import → embed works end-to-end'
});
});
test('bare `init --pglite`: schema sized to gateway defaults (ZE/1280)', async () => {
test('bare `init --pglite`: schema sized to gateway defaults (openai text-embedding-3-small/1280)', async () => {
// Reset gateway so init.ts has to resolve defaults from
// ai/defaults.ts. This is the actual production code path for a
// fresh install: bare `gbrain init --pglite` with no env or file
+11 -5
View File
@@ -74,15 +74,21 @@ describe('v0.37 T12 — fresh init env-detection (D1, D2, D3) + persistence (D5)
// Init may or may not succeed (depends on whether OpenAI key is real for
// any side effect — but init.ts has no live embed call, just config
// writes + schema). Assert the auto-pick stderr notice fired.
expect(r.stderr).toMatch(/Detected OPENAI_API_KEY|Using openai:text-embedding-3-large/);
expect(r.stderr).toMatch(/Detected OPENAI_API_KEY|Using openai:text-embedding-3-small/);
expect(r.exitCode).toBe(0);
// Config persisted with the right embedding fields.
// Config persisted with the right embedding fields. v0.42.68.0 (#3390):
// env-detected OpenAI now lands on the canonical default
// (openai:text-embedding-3-small @ DEFAULT_EMBEDDING_DIMENSIONS = 1280),
// not the recipe's largest tier. Asserted against the live constants so
// this test tracks a future default swap instead of pinning stale literals.
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } =
await import('../../src/core/ai/defaults.ts');
const cfgPath = join(tmpHome, '.gbrain', 'config.json');
expect(existsSync(cfgPath)).toBe(true);
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
expect(cfg.embedding_model).toBe('openai:text-embedding-3-large');
expect(cfg.embedding_dimensions).toBe(1536);
expect(cfg.embedding_model).toBe(DEFAULT_EMBEDDING_MODEL);
expect(cfg.embedding_dimensions).toBe(DEFAULT_EMBEDDING_DIMENSIONS);
expect(cfg.engine).toBe('pglite');
}, 240000);
});
@@ -104,8 +110,8 @@ describe('v0.37 T12 — D3 non-TTY no-key fail-loud', () => {
// Fail-loud message includes the canonical env var list.
expect(r.stderr).toContain('No embedding provider configured');
expect(r.stderr).toContain('OPENAI_API_KEY');
expect(r.stderr).toContain('ZEROENTROPY_API_KEY');
expect(r.stderr).toContain('VOYAGE_API_KEY');
expect(r.stderr).toContain('GOOGLE_GENERATIVE_AI_API_KEY');
// Suggests --no-embedding alternative.
expect(r.stderr).toContain('--no-embedding');
}, 60000);
+5 -4
View File
@@ -3,9 +3,10 @@
* so tests written before v0.37 (with hardcoded `new Float32Array(1536)`
* fixtures) keep working without per-file edits.
*
* v0.37 fix wave changed the canonical gateway defaults to
* `zeroentropyai:zembed-1` / 1280-d (matching the system default chosen
* in v0.36.0). Tests that don't explicitly configure the gateway
* v0.37 fix wave changed the canonical gateway defaults to a 1280-d model
* (zeroentropyai:zembed-1 then; openai:text-embedding-3-small as of
* v0.42.68.0 the width is what matters here). Tests that don't
* explicitly configure the gateway
* previously got 1536-d schemas via the stale `getPGLiteSchema()`
* default; v0.37 fixed that so the schema tracks the gateway default
* (1280 out of the box). Tests with 1536-d fixtures need the schema to
@@ -14,7 +15,7 @@
* Imported by `bunfig.toml` via `preload = ["./test/helpers/legacy-embedding-preload.ts"]`.
*
* Tests that need a different embedding shape (the new v0.37 tests,
* future ZE-1280 tests, or specific-provider tests) should call
* future 1280-d tests, or specific-provider tests) should call
* `configureGateway()` explicitly in their own beforeAll, which
* overwrites this preload.
*/
+6 -5
View File
@@ -19,13 +19,13 @@ describe('v0.37 Lane A — defaults sweep', () => {
// CDX2-1: these were file-private const; Lane A consumers (schema
// helpers, registry) need them exported. Importing here is the test.
const { DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_DIMENSIONS } = await import('../src/core/ai/gateway.ts');
expect(DEFAULT_EMBEDDING_MODEL).toBe('zeroentropyai:zembed-1');
expect(DEFAULT_EMBEDDING_MODEL).toBe('openai:text-embedding-3-small');
expect(DEFAULT_EMBEDDING_DIMENSIONS).toBe(1280);
});
test('A.0: ai/defaults.ts is the canonical source (leaf module, no SDK pulls)', async () => {
const defaults = await import('../src/core/ai/defaults.ts');
expect(defaults.DEFAULT_EMBEDDING_MODEL).toBe('zeroentropyai:zembed-1');
expect(defaults.DEFAULT_EMBEDDING_MODEL).toBe('openai:text-embedding-3-small');
expect(defaults.DEFAULT_EMBEDDING_DIMENSIONS).toBe(1280);
});
@@ -52,17 +52,18 @@ describe('v0.37 Lane A — defaults sweep', () => {
expect(sql).toContain('voyage:voyage-4-large');
});
test('A.5: embedding-column registry builtin defaults to ZE/1280 on empty config + gateway', async () => {
test('A.5: embedding-column registry builtin defaults to openai/1280 on empty config + gateway', async () => {
// The registry's resolution chain is cfg > gateway > DEFAULT. With
// no cfg AND no gateway, it should fall through to the canonical
// default (ZE/1280). Reset gateway first to exercise that path.
// default (openai:text-embedding-3-small/1280 as of v0.42.68.0).
// Reset gateway first to exercise that path.
const { resetGateway } = await import('../src/core/ai/gateway.ts');
const { getEmbeddingColumnRegistry } = await import('../src/core/search/embedding-column.ts');
resetGateway();
try {
const reg = getEmbeddingColumnRegistry({ engine: 'pglite' } as any);
expect(reg['embedding']).toBeDefined();
expect(reg['embedding'].provider).toBe('zeroentropyai:zembed-1');
expect(reg['embedding'].provider).toBe('openai:text-embedding-3-small');
expect(reg['embedding'].dimensions).toBe(1280);
} finally {
// Re-apply legacy preload defaults so the rest of the file's tests